Files
hh_auto/DEPLOY.md

365 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Деплой 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-ключ, пароли