365 lines
15 KiB
Markdown
365 lines
15 KiB
Markdown
# Деплой hh_auto на физический Linux-сервер (многопользовательский, без VNC)
|
||
|
||
## Архитектура
|
||
|
||
```
|
||
Браузер пользователя
|
||
│ HTTPS (443)
|
||
┌───────┴───────┐
|
||
│ nginx │ Let's Encrypt
|
||
│ reverse proxy│
|
||
└───────┬───────┘
|
||
│ :18091
|
||
▼
|
||
gunicorn (Flask: авторизация, изоляция данных, лимиты генераций)
|
||
│
|
||
▼
|
||
Playwright (Chromium: headless или на Xvfb :99)
|
||
│
|
||
▼
|
||
скриншот заполненной формы → проверка в веб-интерфейсе → кнопка «Отправить»
|
||
```
|
||
|
||
**Ключевые принципы:**
|
||
1. **Авторизация** — логин/пароль на входе в веб-интерфейс (Flask-сессия)
|
||
2. **Изоляция данных** — у каждого пользователя своя папка `users/<логин>/` с его аккаунтами hh.ru, резюме, cookies, прогрессом
|
||
3. **Без VNC** — браузер работает на сервере (headless или на виртуальном экране Xvfb), заполненная форма показывается **скриншотом** в веб-интерфейсе, человек подтверждает отправку кнопкой
|
||
4. **Лимиты** — дневной лимит генераций на пользователя (вы платите за LLM)
|
||
5. **LLM-ключ общий** (в `.env` на сервере) — пользователи его не видят
|
||
|
||
**Как работает отклик без VNC:**
|
||
1. Пользователь вставляет cookies hh.ru (из своего браузера) — они подставляются в браузер на сервере автоматически
|
||
2. Playwright открывает вакансию, жмёт «Откликнуться», выбирает резюме, вставляет текст
|
||
3. Делает скриншот заполненной формы → показывает его в веб-интерфейсе
|
||
4. Человек смотрит скриншот и жмёт «Отправить отклик» → скрипт кликает кнопку в браузере
|
||
5. Вакансия помечается отправленной
|
||
|
||
---
|
||
|
||
## Модель пользователей и данных
|
||
|
||
```
|
||
/opt/hh_auto/
|
||
├── .env # LLM_API_KEY, PROXY_URL, SESSION_SECRET, HH_HEADLESS (chmod 600)
|
||
├── users.json # пользователи: логин, hash пароля, лимит, счётчик генераций
|
||
├── manage_users.py # CLI: add / list / passwd / limit / remove
|
||
└── users/
|
||
├── admin/ # вы (владелец)
|
||
│ └── accounts/
|
||
│ └── my_hh/ # config.json, resume.txt, session.txt,
|
||
│ # profile/, progress.json, screenshots/
|
||
└── friend1/ # друг
|
||
└── accounts/
|
||
└── friend1_hh/
|
||
```
|
||
|
||
`users.json`:
|
||
```json
|
||
{
|
||
"admin": {"password_hash": "...", "llm_limit": 50, "llm_usage": {"2026-09-12": 3}, "created": "2026-09-12"},
|
||
"friend1": {"password_hash": "...", "llm_limit": 20, "llm_usage": {}, "created": "2026-09-12"}
|
||
}
|
||
```
|
||
|
||
Пароли — только хэши (werkzeug), в открытом виде нигде не хранятся.
|
||
|
||
---
|
||
|
||
## Предположения
|
||
|
||
- ОС: **Debian 12 / Ubuntu 22.04+**, есть **root**
|
||
- Домен или статический IP; порты **22, 80, 443** открыты
|
||
- Круг пользователей небольшой (2–10 человек). Два варианта:
|
||
- **Открытая регистрация** (по умолчанию): любой может создать аккаунт на странице `/register`
|
||
- **Закрытая** (`ALLOW_REGISTRATION=0` в `.env`): пользователей создаёт владелец через `manage_users.py`
|
||
|
||
---
|
||
|
||
## Этап 0 — подготовка сервера
|
||
|
||
```bash
|
||
apt update && apt upgrade -y
|
||
useradd -m -s /bin/bash hh
|
||
apt install -y ufw
|
||
ufw allow 22/tcp && ufw allow 80/tcp && ufw allow 443/tcp && ufw --force enable
|
||
```
|
||
|
||
---
|
||
|
||
## Этап 1 — системные пакеты
|
||
|
||
```bash
|
||
apt install -y python3 python3-venv python3-pip \
|
||
nginx certbot python3-certbot-nginx \
|
||
xvfb fonts-liberation
|
||
|
||
# Зависимости Chromium (если playwright install-deps недоступен):
|
||
apt install -y libnss3 libxcomposite1 libxdamage1 libxrandr2 libgbm1 \
|
||
libasound2 libatk-bridge2.0-0 libcups2 libpango-1.0-0 libcairo2 \
|
||
libxkbcommon0 libatspi2.0-0
|
||
```
|
||
|
||
> **Xvfb** нужен только если браузер работает «видимым» (рекомендуется — меньше шансов, что hh.ru распознает автоматизацию). Если хотите совсем без дисплея — поставьте `HH_HEADLESS=1` в `.env`, тогда Xvfb не нужен.
|
||
|
||
---
|
||
|
||
## Этап 2 — деплой кода
|
||
|
||
```bash
|
||
cd /opt && git clone https://git.itguild.info/apuc/hh_auto
|
||
chown -R hh:hh /opt/hh_auto
|
||
su - hh
|
||
cd /opt/hh_auto
|
||
python3 -m venv .venv
|
||
.venv/bin/pip install -r requirements.txt
|
||
.venv/bin/playwright install chromium
|
||
|
||
cat > /opt/hh_auto/.env << 'EOF'
|
||
LLM_API_KEY=ваш_ключ_здесь
|
||
LLM_BASE_URL=https://opencode.ai/zen/v1
|
||
LLM_MODEL=deepseek-v4-flash
|
||
PROXY_URL=http://LOGIN:PASSWORD@185.168.251.163:8000
|
||
SESSION_SECRET=$(python3 -c "import secrets; print(secrets.token_hex(32))")
|
||
HH_HEADLESS=0
|
||
HH_NO_SANDBOX=1
|
||
EOF
|
||
chmod 600 /opt/hh_auto/.env
|
||
mkdir -p /opt/hh_auto/users && chmod 700 /opt/hh_auto/users
|
||
```
|
||
|
||
> `HH_NO_SANDBOX=1` — если Chromium не стартует из-за sandbox (root/контейнер). `PROXY_URL` — если hh.ru блокирует IP сервера.
|
||
|
||
---
|
||
|
||
## Этап 3 — виртуальный дисплей (если HH_HEADLESS=0)
|
||
|
||
```bash
|
||
cat > /etc/systemd/system/xvfb.service << 'EOF'
|
||
[Unit]
|
||
Description=Xvfb virtual display :99
|
||
After=network.target
|
||
|
||
[Service]
|
||
ExecStart=/usr/bin/Xvfb :99 -screen 0 1400x900x24 -nolisten tcp +extension GLX
|
||
Restart=always
|
||
RestartSec=3
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
EOF
|
||
|
||
systemctl daemon-reload
|
||
systemctl enable --now xvfb
|
||
```
|
||
|
||
---
|
||
|
||
## Этап 4 — пользователи
|
||
|
||
**Вариант А — открытая регистрация** (по умолчанию, `ALLOW_REGISTRATION=1`):
|
||
любой посетитель создаёт аккаунт сам на странице `/register` (логин + пароль,
|
||
сразу входит). Лимит генераций для таких пользователей — `REGISTER_LLM_LIMIT`
|
||
(по умолчанию 50 в день). Владельца всё равно стоит создать через CLI:
|
||
|
||
```bash
|
||
# Владелец (лимит 50 генераций в день)
|
||
/opt/hh_auto/.venv/bin/python /opt/hh_auto/manage_users.py add admin --password СЛОЖНЫЙ_ПАРОЛЬ --limit 50
|
||
```
|
||
|
||
**Вариант Б — закрытая регистрация** (`ALLOW_REGISTRATION=0` в `.env`):
|
||
пользователей создаёт только владелец:
|
||
|
||
```bash
|
||
# Владелец (лимит 50 генераций в день)
|
||
/opt/hh_auto/.venv/bin/python /opt/hh_auto/manage_users.py add admin --password СЛОЖНЫЙ_ПАРОЛЬ --limit 50
|
||
|
||
# Друг (лимит 20)
|
||
/opt/hh_auto/.venv/bin/python /opt/hh_auto/manage_users.py add friend1 --password ПАРОЛЬ_ДРУГА --limit 20
|
||
|
||
# Просмотр
|
||
/opt/hh_auto/.venv/bin/python /opt/hh_auto/manage_users.py list
|
||
```
|
||
|
||
Другие команды: `passwd <логин>` (смена пароля), `limit <логин> <N>` (лимит, 0 = без лимита), `remove <логин>` (удаление с данными).
|
||
|
||
---
|
||
|
||
## Этап 5 — веб-приложение
|
||
|
||
### 5.1 Gunicorn
|
||
|
||
```bash
|
||
cat > /etc/systemd/system/hh-web.service << 'EOF'
|
||
[Unit]
|
||
Description=hh_auto Flask app (gunicorn)
|
||
After=network.target xvfb.service
|
||
|
||
[Service]
|
||
User=hh
|
||
Group=hh
|
||
WorkingDirectory=/opt/hh_auto
|
||
Environment=DISPLAY=:99
|
||
EnvironmentFile=/opt/hh_auto/.env
|
||
ExecStart=/opt/hh_auto/.venv/bin/gunicorn \
|
||
-w 1 --threads 8 --timeout 300 -b 127.0.0.1:18091 webui:app
|
||
Restart=always
|
||
RestartSec=5
|
||
LimitNOFILE=65536
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
EOF
|
||
|
||
systemctl daemon-reload
|
||
systemctl enable --now hh-web
|
||
```
|
||
|
||
> `-w 1`: один воркер — in-memory кэш и браузеры живут в процессе. `--timeout 300`: открытие вакансии в браузере занимает до минуты. `DISPLAY=:99`: браузер открывается на Xvfb (если HH_HEADLESS=0).
|
||
|
||
### 5.2 Nginx + HTTPS
|
||
|
||
```bash
|
||
# Сертификат (домен):
|
||
certbot certonly --webroot -w /var/www/html -d hh.example.com
|
||
# Или самоподписанный (IP):
|
||
openssl req -x509 -newkey rsa:4096 -keyout /etc/ssl/private/hh.key \
|
||
-out /etc/ssl/certs/hh.crt -days 365 -nodes -subj "/CN=hh-auto"
|
||
|
||
cat > /etc/nginx/sites-available/hh-auto << 'EOF'
|
||
server {
|
||
listen 80;
|
||
server_name hh.example.com;
|
||
return 301 https://$host$request_uri;
|
||
}
|
||
|
||
server {
|
||
listen 443 ssl;
|
||
server_name hh.example.com;
|
||
|
||
ssl_certificate /etc/letsencrypt/live/hh.example.com/fullchain.pem;
|
||
ssl_certificate_key /etc/letsencrypt/live/hh.example.com/privkey.pem;
|
||
# Если самоподписанный:
|
||
# ssl_certificate /etc/ssl/certs/hh.crt;
|
||
# ssl_certificate_key /etc/ssl/private/hh.key;
|
||
|
||
client_max_body_size 20M;
|
||
|
||
location / {
|
||
proxy_pass http://127.0.0.1:18091;
|
||
proxy_set_header Host $host;
|
||
proxy_set_header X-Real-IP $remote_addr;
|
||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto $scheme;
|
||
proxy_read_timeout 600;
|
||
}
|
||
}
|
||
EOF
|
||
|
||
ln -sf /etc/nginx/sites-available/hh-auto /etc/nginx/sites-enabled/hh-auto
|
||
rm -f /etc/nginx/sites-enabled/default
|
||
nginx -t && systemctl reload nginx
|
||
systemctl enable --now certbot.timer
|
||
```
|
||
|
||
---
|
||
|
||
## Этап 6 — первый вход и проверка
|
||
|
||
1. Откройте `https://hh.example.com/` → страница входа → логин `admin`
|
||
2. Создайте аккаунт hh.ru (имя, запросы, регион), загрузите резюме
|
||
3. Войдите на hh.ru **в своём браузере** → F12 → Application → Cookies → скопируйте **все** cookies (`hhtoken`, `hhuid`, `hhrole`, `_xsrf`) → вставьте в поле «Ключ сессии hh.ru» → «Сохранить ключ»
|
||
4. «Обновить список» рядом с «Резюме на hh.ru» → выберите резюме
|
||
5. «Найти вакансии» → отметьте нужные → «Сгенерировать тексты» → проверьте/отредактируйте
|
||
6. «Открыть в браузере» → появится **скриншот заполненной формы** → проверьте → «Отправить отклик»
|
||
7. То же самое друг делает под своим логином — видит только свои аккаунты и свой лимит
|
||
|
||
---
|
||
|
||
## Этап 7 — бэкапы и мониторинг
|
||
|
||
```bash
|
||
# /opt/hh_auto/backup.sh — бэкапим users.json + users/ + .env
|
||
#!/bin/bash
|
||
BACKUP_DIR=/var/backups/hh_auto
|
||
mkdir -p "$BACKUP_DIR"
|
||
DATE=$(date +%Y%m%d_%H%M)
|
||
tar czf "$BACKUP_DIR/hh_auto_$DATE.tar.gz" \
|
||
-C /opt/hh_auto users.json users .env
|
||
find "$BACKUP_DIR" -name '*.tar.gz' -mtime +30 -delete
|
||
|
||
echo "0 3 * * * root /opt/hh_auto/backup.sh" > /etc/cron.d/hh-backup
|
||
chmod 644 /etc/cron.d/hh-backup
|
||
```
|
||
|
||
```bash
|
||
# Логи
|
||
journalctl -u hh-web -f
|
||
|
||
# Здоровье
|
||
curl -sf http://127.0.0.1:18091/api/accounts > /dev/null || systemctl restart hh-web
|
||
```
|
||
|
||
---
|
||
|
||
## Чек-лист проверки
|
||
|
||
| Тест | Ожидаемый результат |
|
||
|---|---|
|
||
| `https://DOMAIN/` без логина | Редирект на страницу входа |
|
||
| Вход admin / friend1 | Каждый видит только свои аккаунты |
|
||
| Попытка friend1 открыть `/api/accounts/admin` | 404 |
|
||
| Поиск вакансий | Список вакансий (или капча — см. «Риски») |
|
||
| Генерация текстов | Текст отклика; счётчик в шапке растёт |
|
||
| Превышение лимита | «Достигнут дневной лимит генераций» |
|
||
| «Открыть в браузере» | Скриншот заполненной формы |
|
||
| «Отправить отклик» | Кнопка нажата, вакансия помечена |
|
||
| `journalctl -u hh-web` | Без ошибок |
|
||
|
||
---
|
||
|
||
## Риски и обходы
|
||
|
||
### hh.ru блокирует IP дата-центра / капча
|
||
- **Симптом**: капча при поиске, HTTP 403, пустой список
|
||
- **Обход**: `PROXY_URL` в `.env`; если не помогает — residential-прокси
|
||
|
||
### hh.ru распознаёт автоматизацию
|
||
- **Симптом**: капча именно при открытии вакансии в браузере
|
||
- **Обход**: `HH_HEADLESS=0` + Xvfb (видимый Chromium, сложнее распознать); свежие cookies; не слишком частые действия
|
||
|
||
### Cookies инвалидируются
|
||
- **Симптом**: поиск пуст, скриншот показывает страницу входа
|
||
- **Обход**: заново скопировать cookies из браузера
|
||
|
||
### Chromium не стартует
|
||
- **Симптом**: `hh-web` падает, в `journalctl` ошибки
|
||
- **Обход**: `HH_NO_SANDBOX=1`; проверить `DISPLAY=:99` и `systemctl status xvfb`; проверить зависимости (`ldd ~/.cache/ms-playwright/chromium-1234/chrome-linux64/chrome | grep "not found"`)
|
||
|
||
### Одновременные пользователи
|
||
- Один gunicorn-воркер и один браузерный поток — операции выполняются по очереди. Для 2–10 пользователей достаточно; при большей нагрузке — переработка архитектуры
|
||
|
||
### Расходы на LLM
|
||
- Ключ общий, генерации пользователей идут за ваш счёт. Лимиты задаются в `manage_users.py limit <логин> <N>`
|
||
|
||
### Миграция с однопользовательской версии
|
||
- Старые `accounts/*` → `users/admin/accounts/*` (просто `mv`)
|
||
|
||
---
|
||
|
||
## Итого
|
||
|
||
| Этап | Время |
|
||
|---|---|
|
||
| Подготовка сервера + пакеты | 15–20 мин |
|
||
| Деплой кода + .env | 5 мин |
|
||
| Xvfb (опционально) | 5 мин |
|
||
| Пользователи (manage_users) | 5 мин |
|
||
| Gunicorn + nginx + HTTPS | 15–20 мин |
|
||
| Первый вход + проверка | 15–20 мин |
|
||
| **Итого** | **~1–1.5 часа** |
|
||
|
||
### Что получают пользователи
|
||
- Свой логин/пароль → свой веб-интерфейс (только свои аккаунты, резюме, вакансии)
|
||
- Скриншоты заполненных форм → сами подтверждают отправку
|
||
- Свой дневной лимит генераций
|
||
- Не видят: чужие аккаунты, резюме, cookies, прогресс, LLM-ключ, пароли |