opencode-config/docs/handoff/pr-174-telegram-send-tool.md
Sergey 1bc8b8ed05
feat(telegram): add telegram-send plugin-tool for Bot API messaging (#174)
* feat(telegram): add CLI project with Bot API client

* feat(telegram): add telegram-send plugin-tool

* test(telegram): add unit tests for markdown, config, api

* chore(env): add TELEGRAM_CHAT_ID to .env.example

* docs(handoff): add handoff and ADR for telegram-send

* docs(handoff): set PR number 174

* docs: update project map for telegram-send tool

---------

Co-authored-by: opencode-agent <agent@opencode.local>
2026-07-31 20:42:33 +03:00

5.8 KiB
Raw Blame History


pr: 174 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.jsontarget: ES2022, module: ESNext, moduleResolution: Bundler, strict, allowImportingTsExtensions: true, lib: ["ES2022", "DOM"] (DOM нужен для типов fetch, BodyInit, Blob, FormData, File — все используются в src/api.ts)
    • vitest.config.tsenvironment: "node", include: ["tests/**/*.test.ts"] (e2e.manual.ts исключён — нет .test. в имени)
    • src/config.tsloadConfig(overrides?): читает TELEGRAM_BOT_TOKEN/TELEGRAM_CHAT_ID из env, валидирует, override chatId имеет приоритет над env
    • src/markdown.tsescapeMarkdownV2(text): экранирует спецсимволы _*[]()~\>#+-=|{}.!` (backslash НЕ в наборе), кириллицу не трогает
    • src/api.tssendMessage/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-imagetelegram-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/