feat(standalone): :integrations-telegram — bridge stability + markdown rendering
Outbound (DurableEvent-driven): - typing дёргается ДО conv.send (а не только по online-событиям); рефреш по online — каждые ≥4 с, чтобы не получить 429-спам (Telegram rate-limit ~250 ms на sendChatAction). - финальный текст — sendMessage(... ParseMode.HTML) на DurableEvent.AssistantMessage, без промежуточных editMessage. - служебные строки ((empty response), ошибки) идут как plain text (без parseMode=HTML), чтобы 400 can't parse entities не выбил предупреждение в чат. Inline markdown → Telegram HTML (MarkdownToTelegram): - **bold** / __bold__ / *italic* / _italic_ / `code` → <b>/<i>/<code>; fenced ``` → <pre>; [text](url) → <a href>. - markdown-таблицы → Unicode-рамка (┌─┬─┐││├─┼─┤└─┴─┘) внутри <pre> (Telegram Bot API <table> не поддерживает). - escapeOutsideTags теперь ведёт стек открытых тегов: раньше брал первый попавшийся закрывающий тег и терял парность (агент вставил <i> внутри <code> → Telegram 400 can't find end tag для <code>). - экранирует только вне наших тегов; внутри — уже подготовленный текст. - сообщения > 4096 режутся по \n вне тегов. Видимость ошибок: - kotlin-logging в build.gradle.kts. - runCatching'и заменены на try/catch + log.error/warn с контекстом (chatId, convId, htmlLen, превью первых ≤120 символов HTML). Раньше Telegram 400 на sendMessage с 'can't parse entities' уходил молча — теперь видно в логе с полным телом запроса. Тесты: 23 + 12 + 28 = 63 в :integrations-telegram, 132 в :standalone. :standalone:assemble собирает fatjar.
This commit is contained in:
+31
-9
@@ -22,6 +22,9 @@
|
||||
|
||||
## Как запустить
|
||||
|
||||
### Переменные среды
|
||||
`AGENTIK_SKILLS_DIR=./agentik/skills;AGENTIK_SYSTEM_PROMPT=Ты полезный ассистент.;OPENAI_BASE_URL=http://192.168.88.135:8001/v1;AGENTIK_DB_PATH=./agentik/db.db;OPENAI_CONTEXT_WINDOW=115000;AGENTIK_MEMORY_DIR=./agentik/memory;AGENTIK_LLM_BACKEND=openai;AGENTIK_PORT=8080;OPENAI_API_KEY=sk-76R2p5nxQxflPkIROr6r2xGiuSYzUCYM;OPENAI_MODEL=/root/.cache/huggingface/Qwen3.8-27B-NVFP4-RTX5090;AGENTIK_SOUL=./agentik/SOUL.md;AGENTIK_TELEGRAM_TOKEN=8857094360:AAFeVYMlutSaHptbhpO4mUryR6Je7OUJsm4`
|
||||
|
||||
### Требования
|
||||
|
||||
- JVM 21+.
|
||||
@@ -107,14 +110,32 @@ AGENTIK_GOOGLE_MODEL_PATH=/root/gemma-4-E2B-it.litertlm \
|
||||
- Long-polling через Bot API (`getUpdates?timeout=N`). Не webhook —
|
||||
не нужен публичный URL, всё работает за NAT.
|
||||
- Маппинг `chatId ↔ ConversationId` в общей SQLite-БД standalone'а
|
||||
(таблица `tg_chat_map`). Один Telegram-чат = один диалог с агентом;
|
||||
сообщения из чата стримят ответы обратно через `editMessageText`.
|
||||
(таблица `tg_chat_map`). Один Telegram-чат = один диалог с агентом.
|
||||
- **«печатает…» в чате** через `sendChatAction(TYPING)`:
|
||||
- одно сразу при получении апдейта (ДО вызова LLM — пользователь
|
||||
видит статус ровно в момент отправки текста);
|
||||
- рефреш на каждом online-событии для диалога (Working /
|
||||
StartReasoning / StartResponse / AppendText / AppendImage / End) —
|
||||
Telegram гасит статус через ~5 с, поэтому для длинных tool-call'ов
|
||||
(когда дельт текста нет) мост продолжает рефрешить typing по
|
||||
`Working`.
|
||||
- **Финальное сообщение** приходит одним `sendMessage(...)` по
|
||||
`DurableEvent.AssistantMessage` (целиковый текст из `Content.Text`).
|
||||
Никакого стриминга через `editMessage` — UX проще, и Telegram
|
||||
нормально индексирует одиночное сообщение.
|
||||
- **Markdown → Telegram HTML**: мост конвертирует CommonMark-подобный
|
||||
markdown агента в `ParseMode.HTML` (`<b>`, `<i>`, `<code>`, `<pre>`,
|
||||
`<a href>`); остальные символы экранируются. Сообщения длиннее
|
||||
4096 символов режутся по переводам строк (вне HTML-тегов).
|
||||
- **Markdown-таблицы** (`| col | col |` + `| --- | --- |`) рендерятся
|
||||
через Unicode box-drawing (`┌─┬─┐ │ │ │ ├─┼─┤ └─┴─┘`) внутри `<pre>` —
|
||||
Telegram Bot API не поддерживает `<table>`, поэтому табличный вид
|
||||
даёт только моноширинная рамка. Markdown/HTML-разметка внутри
|
||||
ячеек снимается (в `<pre>` inline-форматирование не работает).
|
||||
- Group-чаты (`group`, `supergroup`) и команда `/new` стартуют **новый
|
||||
временный** диалог (`ConversationRecord.isTemporal = true`) — не
|
||||
пишут в мапу, удаляются при следующем `/new`.
|
||||
- Tool-call показывает «⚙️ обрабатываю…» через `sendChatAction(typing)` —
|
||||
включается на `onlineOutbox.onlineEvents` пока обрабатывается ход.
|
||||
- Ошибки агента стримятся в чат одной строкой, polling не падает.
|
||||
временный** диалог (`ConversationRecord.isTemporal = true`).
|
||||
- Ошибки агента (`DurableEvent.Error`, исключение из `conv.send`)
|
||||
приходят одной строкой, polling не падает.
|
||||
|
||||
### Как включить
|
||||
|
||||
@@ -134,8 +155,9 @@ AGENTIK_GOOGLE_MODEL_PATH=/root/gemma-4-E2B-it.litertlm \
|
||||
telegram: enabled (polling timeout=30s, persistent=true)
|
||||
```
|
||||
|
||||
4. Ответы приходят стримингом — `editMessageText` обновляет одно и то
|
||||
же сообщение каждые ~1с пока агент генерирует.
|
||||
4. Ответ приходит одним сообщением (после завершения хода ассистента).
|
||||
Пока агент думает / вызывает тулзы / стримит токены — в чате горит
|
||||
«печатает…».
|
||||
|
||||
### Persistent vs temporary
|
||||
|
||||
|
||||
Reference in New Issue
Block a user