# Деплой 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 <логин> ` (лимит, 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 <логин> ` ### Миграция с однопользовательской версии - Старые `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-ключ, пароли