feat телеграм-робот: проход опроса — сам спрашивает работу у портала
Своей петли нет: запускается таймером, зависший проход не мешает следующему. Номера кладутся во временный файл и убираются при любом исходе. Проверено живьём против поднятого портала: верный токен — «Работы нет.» и код 0, чужой токен — понятная ошибка 401. Заодно закрыта мина: токен уходит в заголовок HTTP, куда пускают только латиницу. С русскими буквами робот падал нечитаемым сбоем Node ещё до обращения к порталу — теперь говорит по-человечески. Наступил на неё сам, проверяя план дословно. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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=
|
||||
|
||||
@@ -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"`)
|
||||
|
||||
Если модерация МТС **отклонила** кампанию, робот может её починить: войти в
|
||||
|
||||
@@ -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);
|
||||
@@ -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-сессий.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user