docs(handoff): add handoff and ADR for telegram-send
This commit is contained in:
parent
2ce34a86a2
commit
8f47340f9e
2 changed files with 80 additions and 0 deletions
37
docs/decisions/073-pr-<PR-NUMBER>-telegram-send-tool.md
Normal file
37
docs/decisions/073-pr-<PR-NUMBER>-telegram-send-tool.md
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
# ADR-073: telegram-send plugin-tool — Bot API messaging via CLI
|
||||||
|
|
||||||
|
## Статус
|
||||||
|
|
||||||
|
Accepted (PR #<PR-NUMBER>)
|
||||||
|
|
||||||
|
## Контекст
|
||||||
|
|
||||||
|
Оркестратору нужен быстрый способ показывать пользователю результаты работы — сгенерированный `draw-image` PNG, .md-файл с архитектурой, текстовый вывод. Существующий способ показать файл — tunnel skill (запуск процесса + проброс порта через Cloudflare), что тяжело для разовой отправки и требует живого процесса.
|
||||||
|
|
||||||
|
В репо уже есть паттерн plugin-tool + CLI-проекта на примере `draw-image`: тонкая обёртка `.opencode/tools/draw-image.ts` дёргает CLI из `.opencode/draw-image/cli.ts` через `spawnSync("node", ["--experimental-strip-types", cliPath, ...args])`. Plugin-tools авто-дискаверятся opencode из папки `.opencode/tools/` — без записи в `opencode.json`, без MCP-сервера.
|
||||||
|
|
||||||
|
## Решение
|
||||||
|
|
||||||
|
Создан `telegram-send` plugin-tool + CLI-проект `.opencode/telegram/` по паттерну `draw-image`:
|
||||||
|
|
||||||
|
1. **Plugin-tool, не MCP-сервер** — `.opencode/tools/telegram-send.ts` авто-дискаверится opencode; `spawnSync` запускает CLI как дочерний процесс; результат парсится из stdout; ошибка → `⚠️ telegram-send failed (exit N): <stderr>`. Не требует записи в `opencode.json`, не требует long-running процесса.
|
||||||
|
|
||||||
|
2. **Без runtime-зависимостей** — `package.json` имеет только devDeps (`@types/node`, `typescript`, `vitest`). Рантайм использует встроенный `fetch`/`FormData`/`Blob`/`File` Node 22+. Это упрощает установку (только `npm install` для devDeps) и аудит.
|
||||||
|
|
||||||
|
3. **Один tool с параметром `action`** — `action: "text" | "document" | "photo"` вместо трёх отдельных tools. Причины: единый контрак (`{ ok, message_id, chat_id }`), один набор env-настроек, меньше шума в списке tools оркестратора. Параметры `path`/`caption`/`text` условно-обязательные (зависят от `action`).
|
||||||
|
|
||||||
|
4. **CLI как тонкий слой над api.ts** — `cli.ts` парсит argv, применяет `parse_mode` по умолчанию (`"MarkdownV2"`), диспетчеризует в `sendMessage`/`sendDocument`/`sendPhoto`. `api.ts` — низкоуровневый, не применяет дефолты (добавляет `parse_mode` в body только при явной передаче). Разделение: api.ts тестируется изолированно, cli.ts тестирует дефолты.
|
||||||
|
|
||||||
|
5. **`lib: ["ES2022", "DOM"]` в tsconfig** — DOM-типы нужны только для `fetch`/`BodyInit`/`Blob`/`FormData`/`File` (все используются в `src/api.ts`). Рантайм-значения берутся из Node 22+ глобалов, не из браузера. `allowImportingTsExtensions: true` — сознательный паттерн (как `draw-image`) для `node --experimental-strip-types`.
|
||||||
|
|
||||||
|
## Альтернативы
|
||||||
|
|
||||||
|
1. **MCP-сервер для Telegram** — отвергнуто: требует long-running процесса, записи в `opencode.json`, сложнее отладка. Plugin-tool проще и следует существующему паттерну `draw-image`.
|
||||||
|
|
||||||
|
2. **Прямой `fetch` из plugin-tool (без CLI)** — отвергнуто: plugin-tool выполняется в Bun plugin-host (`import.meta.dir` доступен), но `fetch` там может вести себя иначе; CLI-процесс на Node 22+ даёт предсказуемый рантайм с встроенным `fetch`. Плюс CLI тестируется изолированно (`npm test`).
|
||||||
|
|
||||||
|
3. **Внешние зависимости (`node-telegram-bot-api`, `telegraf`)** — отвергнуто: добавляют audit-поверхность, усложняют установку, избыточны для 3 endpoint'ов (`sendMessage`, `sendDocument`, `sendPhoto`). Встроенный `fetch` + `FormData` достаточны.
|
||||||
|
|
||||||
|
4. **Три отдельных tools (`telegram-send-text`, `telegram-send-document`, `telegram-send-photo`)** — отвергнуто: засоряет список tools, дублирует env-логику и контрак. Один tool с `action`-enum чище.
|
||||||
|
|
||||||
|
5. **Markdown → MarkdownV2 авто-конвертер** — отвергнуто (out-of-scope): пользователь сам экранирует через `escapeMarkdownV2` (экспортируется из `src/markdown.ts`) или использует `parse_mode: "plain"`/`"HTML"`. Конвертер — отдельная нетривиальная задача.
|
||||||
43
docs/handoff/pr-<PR-NUMBER>-telegram-send-tool.md
Normal file
43
docs/handoff/pr-<PR-NUMBER>-telegram-send-tool.md
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
---
|
||||||
|
pr: <PR-NUMBER>
|
||||||
|
title: feat(telegram): add telegram-send plugin-tool for Bot API messaging
|
||||||
|
---
|
||||||
|
|
||||||
|
## Что сделано
|
||||||
|
|
||||||
|
- `.opencode/telegram/` — CLI-проект (по аналогии с `.opencode/draw-image/`):
|
||||||
|
- `package.json` — `"type": "module"`, без runtime-deps; devDeps: `@types/node`, `typescript`, `vitest`; script `test: vitest run`
|
||||||
|
- `tsconfig.json` — `target: ES2022`, `module: ESNext`, `moduleResolution: Bundler`, `strict`, `allowImportingTsExtensions: true`, `lib: ["ES2022", "DOM"]` (DOM нужен для типов `fetch`, `BodyInit`, `Blob`, `FormData`, `File` — все используются в `src/api.ts`)
|
||||||
|
- `vitest.config.ts` — `environment: "node"`, `include: ["tests/**/*.test.ts"]` (e2e.manual.ts исключён — нет `.test.` в имени)
|
||||||
|
- `src/config.ts` — `loadConfig(overrides?)`: читает `TELEGRAM_BOT_TOKEN`/`TELEGRAM_CHAT_ID` из env, валидирует, override chatId имеет приоритет над env
|
||||||
|
- `src/markdown.ts` — `escapeMarkdownV2(text)`: экранирует спецсимволы `_*[]()~\`>#+-=|{}.!` (backslash НЕ в наборе), кириллицу не трогает
|
||||||
|
- `src/api.ts` — `sendMessage`/`sendDocument`/`sendPhoto` через встроенный `fetch` Node 22+; `telegramFetch` парсит `result`, возвращает `{ ok, message_id, chat_id }`; при `ok:false` — throw с `description`
|
||||||
|
- `cli.ts` — argv-парсер, диспетчер по `action` (text/document/photo), `parse_mode` по умолчанию `"MarkdownV2"`; успех → `console.log(JSON.stringify(result))`, провал → stderr + `exit(1)`
|
||||||
|
- `.opencode/tools/telegram-send.ts` — plugin-tool обёртка (~35 строк): `spawnSync("node", ["--experimental-strip-types", cliPath, ...cliArgs])`, при `status !== 0` возвращает `⚠️ telegram-send failed (exit N): ...`, иначе `stdout.trim()`
|
||||||
|
- Тесты в `.opencode/telegram/tests/` (39 unit-тестов, все проходят):
|
||||||
|
- `markdown.test.ts` — 25 кейсов: пустая строка, без спецсимволов, кириллица, каждый спецсимвол `it.each`, микс кириллицы+спецсимволов, все спецсимволы сразу, backtick, `\` не в наборе
|
||||||
|
- `config.test.ts` — 5 кейсов: валидный env, отсутствие token, отсутствие chatId, override приоритет, override без env
|
||||||
|
- `api.test.ts` — 9 кейсов: URL/method/body/headers sendMessage, parse_mode omit/plain/HTML, возвращаемое значение, Telegram API error, network error, sendDocument FormData, sendPhoto FormData (spy на `FormData.prototype.append`)
|
||||||
|
- `e2e.manual.ts` — ручной скрипт (НЕ в `npm test`)
|
||||||
|
- `.env.example` — добавлена `TELEGRAM_CHAT_ID=your-telegram-chat-id` в секцию `# Telegram (optional)` (после `TELEGRAM_BOT_TOKEN`)
|
||||||
|
|
||||||
|
## Почему
|
||||||
|
|
||||||
|
Оркестратору нужен быстрый способ показывать пользователю результаты работы — например, сгенерированный `draw-image` PNG или .md-файл с архитектурой проекта. Сейчас единственный способ показать файл — через tunnel skill (запуск процесса, проброс порта, ссылка), что тяжело для разовой отправки. Прямая отправка в Telegram через Bot API решает это одним вызовом tool'а: `telegram-send({ action: "photo", path })` — и файл у пользователя.
|
||||||
|
|
||||||
|
Plugin-tool (не MCP-сервер) выбран как паттерн: авто-дискаверится opencode из `.opencode/tools/`, не требует записи в `opencode.json`, тонкая TS-обёртка дёргает CLI через `spawnSync`. Без runtime-зависимостей — только встроенный `fetch`/`FormData`/`Blob` Node 22+.
|
||||||
|
|
||||||
|
## Pending
|
||||||
|
|
||||||
|
- Реальная E2E-проверка с живым ботом (нужны `TELEGRAM_BOT_TOKEN` + `TELEGRAM_CHAT_ID` в env) — `node --experimental-strip-types tests/e2e.manual.ts`
|
||||||
|
- Проверка цепочки `draw-image` → `telegram-send photo` в реальном сценарии оркестратора
|
||||||
|
- Возможно: интеграция `escapeMarkdownV2` в оркестратор для авто-экранирования текста перед `action: "text"`
|
||||||
|
|
||||||
|
## Watch out
|
||||||
|
|
||||||
|
- `parse_mode: "MarkdownV2"` — формат по умолчанию в `cli.ts:29`, но НЕ в `api.ts` (api-функции добавляют `parse_mode` в body только когда явно передан `MarkdownV2`/`HTML`; `undefined`/`"plain"` → поле отсутствует). Это сознательно: api.ts — низкоуровневый, cli.ts — применяет дефолт
|
||||||
|
- `lib: ["ES2022", "DOM"]` в `tsconfig.json` — DOM нужен только для типов `fetch`/`BodyInit`/`Blob`/`FormData`/`File`; рантайм-значения берутся из Node 22+ глобалов (НЕ из браузера)
|
||||||
|
- `allowImportingTsExtensions: true` — сознательный паттерн проекта (как `draw-image`), т.к. запуск через `node --experimental-strip-types` с `.ts`-импортами
|
||||||
|
- Node 22+ требуется (встроенные `fetch`, `Blob`, `FormData`, `File` глобально)
|
||||||
|
- `tests/e2e.manual.ts` исключён из `npm test` двумя механизмами: отсутствие `.test.` в имени + `include: ["tests/**/*.test.ts"]` в vitest config — двойная защита
|
||||||
|
- Plugin-tool НЕ MCP-сервер — НЕ добавлять в `opencode.json`, авто-дискаверится из `.opencode/tools/`
|
||||||
Loading…
Add table
Reference in a new issue