feat телеграм-робот: проход опроса — сам спрашивает работу у портала

Своей петли нет: запускается таймером, зависший проход не мешает следующему.
Номера кладутся во временный файл и убираются при любом исходе.

Проверено живьём против поднятого портала: верный токен — «Работы нет.» и код 0,
чужой токен — понятная ошибка 401.

Заодно закрыта мина: токен уходит в заголовок HTTP, куда пускают только латиницу.
С русскими буквами робот падал нечитаемым сбоем Node ещё до обращения к порталу —
теперь говорит по-человечески. Наступил на неё сам, проверяя план дословно.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-07-31 09:22:26 +03:00
parent 16091204e0
commit 9100e1fdda
4 changed files with 114 additions and 4 deletions
+10
View File
@@ -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=
+34
View File
@@ -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"`)
Если модерация МТС **отклонила** кампанию, робот может её починить: войти в
+66
View File
@@ -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);
+4 -4
View File
@@ -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-сессий.