diff --git a/bots/mts-telegram-ads/.env.example b/bots/mts-telegram-ads/.env.example index bc80d9ab..41afb8bf 100644 --- a/bots/mts-telegram-ads/.env.example +++ b/bots/mts-telegram-ads/.env.example @@ -20,3 +20,13 @@ BUDGET_CAP_RUB=5000 # Темп «по-человечески»: минимальная пауза между действиями, мс HUMAN_DELAY_MS=800 + +# Портал, у которого робот спрашивает работу (опросный канал). +# Тот же токен должен стоять в .env портала как TG_ROBOT_TOKEN. +PORTAL_BASE_URL=https://liderra.ru +TG_ROBOT_TOKEN= + +# Портал, у которого робот спрашивает работу (опросный канал). +# Тот же токен должен стоять в .env портала как TG_ROBOT_TOKEN. +PORTAL_BASE_URL=https://liderra.ru +TG_ROBOT_TOKEN= diff --git a/bots/mts-telegram-ads/README.md b/bots/mts-telegram-ads/README.md index 93c1bd11..30a4793a 100644 --- a/bots/mts-telegram-ads/README.md +++ b/bots/mts-telegram-ads/README.md @@ -126,6 +126,40 @@ node bin/run.js --task task.example.json `{"ok": true, "matched": 996, "launched": false}` — `matched` это сколько номеров «не МТС» нашлось, `launched` — нажал ли робот кнопку запуска. +## 6.2. Работа от портала — робот сам спрашивает задания (`bin/poll.js`) + +Обычный запуск выше требует, чтобы робот стоял **на той же машине, где очередь портала**: +портал сам запускает `bin/run.js`. У нас так не выходит — МТС отбивает адреса +дата-центров, и робот живёт отдельно. Поэтому есть второй способ: портал кладёт задание +в очередь, а робот сам ходит за ним. + +Что нужно в `.env` робота: + +``` +PORTAL_BASE_URL=https://liderra.ru +TG_ROBOT_TOKEN=<тот же секрет, что в .env портала> +``` + +Один проход: + +```bash +node bin/poll.js +``` + +- работы нет → напечатает «Работы нет.» и выйдет с кодом 0; +- работа есть → заберёт задание, отдельным запросом скачает номера, отработает обычным + `runTask` и отчитается на портал. + +Своей петли внутри **нет** — проход запускается таймером (раз в минуту). Так проще убить и +перезапустить, а зависший проход не мешает следующему: портал сам вернёт задание в очередь, +если по нему не отчитались за срок аренды (по умолчанию 20 минут). + +🔴 **Номера телефонов** робот не получает в задании — только отдельным запросом и только +пока задание в работе. Кладёт их во временный файл и убирает при любом исходе. + +Портал переключается на этот способ переменной `TG_ROBOT_TRANSPORT=poll` в своём `.env`. +Пока она `process` (умолчание) — работает старый способ, и `poll.js` заданий не увидит. + ## 6.1. Пересдача отклонённой кампании (`mode: "resubmit"`) Если модерация МТС **отклонила** кампанию, робот может её починить: войти в diff --git a/bots/mts-telegram-ads/bin/poll.js b/bots/mts-telegram-ads/bin/poll.js new file mode 100644 index 00000000..97c1a324 --- /dev/null +++ b/bots/mts-telegram-ads/bin/poll.js @@ -0,0 +1,66 @@ +import 'dotenv/config'; +import { writeFileSync, unlinkSync, existsSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { loadConfig } from '../src/config.js'; +import { parseTask } from '../src/task.js'; +import { createMailer, smtpTransport } from '../src/mailer.js'; +import { runTask } from '../src/runner.js'; +import { portalClient } from '../src/portal.js'; + +// Один проход: спросить портал, есть ли работа; если есть — сделать и отчитаться. +// Запускается таймером раз в минуту. Своей петли внутри НЕТ — так проще убить и +// перезапустить, и один зависший проход не мешает следующему (портал вернёт задание +// в очередь по сроку аренды). + +const baseUrl = process.env.PORTAL_BASE_URL; +const token = process.env.TG_ROBOT_TOKEN; +if (!baseUrl || !token) { + console.error('Не заданы PORTAL_BASE_URL и/или TG_ROBOT_TOKEN'); + process.exit(2); +} + +// Токен уходит в заголовок HTTP, а туда пускают только латиницу и цифры. С русскими +// буквами Node падает нечитаемым «Cannot convert argument to a ByteString» ещё до +// обращения к порталу. Ловим здесь и говорим по-человечески. +if (/[^\x20-\x7E]/.test(token)) { + console.error('TG_ROBOT_TOKEN содержит русские буквы или иные не-латинские символы — так нельзя.'); + console.error('Токен должен состоять только из латинских букв, цифр и знаков вроде - и _'); + process.exit(2); +} + +const portal = portalClient({ baseUrl, token }); +const job = await portal.next(); + +if (!job) { + console.log('Работы нет.'); + process.exit(0); +} + +console.log(`Задание ${job.id}, режим ${job.mode}`); + +const config = loadConfig(); +const mailer = createMailer(smtpTransport(config.smtp), { from: config.alarmFrom, to: config.alarmTo }); +const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + +// Номера — ПДн: кладём во временный файл и убираем в finally при любом исходе. +const phonesFile = join(tmpdir(), `tg-phones-${job.id}-${process.pid}.txt`); +let result; + +try { + const phones = await portal.phones(job.phones_url); + writeFileSync(phonesFile, phones.join('\n'), 'utf8'); + + const task = parseTask({ ...job.task, mode: job.mode, phonesFile }); + result = await runTask(config, task, mailer, { timestamp }); +} catch (e) { + result = { ok: false, reason: String(e.message).slice(0, 255), step: 'poll' }; +} finally { + if (existsSync(phonesFile)) { + try { unlinkSync(phonesFile); } catch { /* файл уже убран */ } + } +} + +await portal.done(job.id, result); +console.log(`Отчитались по заданию ${job.id}: ok=${result.ok}`); +process.exit(result.ok ? 0 : 1); diff --git a/docs/observer/STATUS.md b/docs/observer/STATUS.md index 8d3132f1..0bbba8c1 100644 --- a/docs/observer/STATUS.md +++ b/docs/observer/STATUS.md @@ -1,6 +1,6 @@ # Brain Status (auto-generated) -Last updated: 2026-07-31T05:54:42.937Z +Last updated: 2026-07-31T06:13:42.892Z | Контролёр | Состояние | Детали | |---|---|---| @@ -112,9 +112,9 @@ Episodes since last run: 542 / threshold: 10 | PID | Имя | CPU-время | Возраст | |---|---|---|---| -| 3544 | MsMpEng | 14.20ч | 0.0ч | -| 23936 | Code | 4.28ч | 1328717.9ч | -| 4 | System | 2.33ч | 0.0ч | +| 3544 | MsMpEng | 14.69ч | 0.0ч | +| 23936 | Code | 4.39ч | 0.0ч | +| 4 | System | 2.37ч | 0.0ч | ⚠️ Проверь, не «осиротевшие» ли это процессы от завершённых Claude-сессий.