feat(standalone): :integrations-telegram — мост Telegram-чат ↔ agent

Новый модуль :integrations-telegram, опциональный «плагин» для standalone:
  * TelegramBridgeComponent — Component-имплементация, устанавливается
    в agent через agent.install(...) только при заданном
    AGENTIK_TELEGRAM_TOKEN. Нет токена — нет polling'а, classpath не
    содержит pw.binom.telegram.* (factory возвращает AutoCloseable,
    а Component-объект создаётся внутри factory и утекает в
    standalone только через agent-api).
  * Long-polling getUpdates?timeout=N — не webhook, не нужен публичный URL.
  * chatId ↔ ConversationId маппинг в общей SQLite-БД standalone'а
    (таблица tg_chat_map); persistent=true переиспользует conv_id,
    persistent=false — каждый Telegram-message = свежий temp-диалог.
  * Group-чаты и /new стартуют временный диалог, не пишут в мапу.
  * Tool-call показывает «⚙️ обрабатываю…» через sendChatAction(typing)
    по onlineOutbox.onlineEvents.
  * Outbound streaming через editMessageText с guard'ом «message is not
    modified»; End с пустым телом → «(empty response)».
  * Ошибки conv.send — одной строкой в чат, polling не падает.
  * uninstall отменяет scope, polling и stream-джобы корректно
    останавливаются.

Конфиг — через env (AppConfig.fromEnv):
  * AGENTIK_TELEGRAM_TOKEN         (default пусто → модуль не активен)
  * AGENTIK_TELEGRAM_POLLING_TIMEOUT  (default 30s, ок 25–35)
  * AGENTIK_TELEGRAM_PERSISTENT      (default true)

Зависимость: наша in-house либа caffeine-mgn/telegramClient
(pw.binom.telegram:telegramClient:1.0.0-SNAPSHOT из mavenLocal).

standalone/Main.kt:
  * integrationsScope (SupervisorJob + Dispatchers.Default) создаётся
    до server'а; tgClient закрывается shutdown-hook'ом ДО agent.close()
    и integrationsScope.cancel() — порядок важен, иначе polling
    long-poll не прерывается и JVM висит на выходе.

Тесты (8/8 ✅):
  * первый входящий текст → persistent conv + conv.send
  * повтор из того же chat → тот же conv
  * non-persistent → каждый message = новый temp-conv
  * typing-индикатор на каждом сообщении
  * outbound streaming → 3 editMessage подряд
  * End с пустым телом → draft заменяется на «(empty response)»
  * ошибка conv.send → одна строка в чат, polling не падает
  * uninstall → polling остановлен, новые push'и не обрабатываются

standalone/README.md — добавлен раздел «Telegram-интеграция» с env-таблицей,
описанием поведения и инструкцией по включению.
This commit is contained in:
2026-10-05 23:33:57 +03:00
parent 9e888227a3
commit 24212a1003
8 changed files with 1238 additions and 11 deletions
+69
View File
@@ -89,9 +89,78 @@ AGENTIK_GOOGLE_MODEL_PATH=/root/gemma-4-E2B-it.litertlm \
| `AGENTIK_MEMORY_DIR` | `~/.agentik/memory/` | Каталог для md-памяти |
| `AGENTIK_TOOLSETS_DEFAULT` | `memory,skills,files,web` | Включённые тулы |
| `AGENTIK_DEBUG` | `0` | `1` = verbose logging |
| `AGENTIK_TELEGRAM_TOKEN` | (пусто) | Bot token от `@BotFather`. Пусто — Telegram-интеграция выключена, standalone работает ровно как без неё |
| `AGENTIK_TELEGRAM_POLLING_TIMEOUT` | `30` | Long-poll timeout в секундах. Bot API держит HTTP-соединение открытым до N секунд, возвращает 200 OK с пустым массивом если апдейтов нет. Меньше — больше polling-нагрузки, больше — дольше «висит» соединение. 25–35 обычно ок |
| `AGENTIK_TELEGRAM_PERSISTENT` | `true` | `true` → диалог из чата сохраняется в SQLite как обычный `ConversationRecord` и переживает рестарт. `false` → каждый Telegram-message стартует свежий `temp=true` диалог (как «temporary chat» в Android-клиенте) |
Значения читаются через `AgentikConfig.fromEnv()` в `:standalone/.../Main.kt`.
## Telegram-интеграция
Опциональный «мост» между Telegram-чатом и standalone-агентом.
Реализован отдельным модулем `:integrations-telegram` и подключается
ТОЛЬКО при заданном `AGENTIK_TELEGRAM_TOKEN` (нет токена — нет polling'а,
нет зависимости в classpath при выключенном флаге, см. ниже).
### Что умеет
- Long-polling через Bot API (`getUpdates?timeout=N`). Не webhook —
не нужен публичный URL, всё работает за NAT.
- Маппинг `chatId ↔ ConversationId` в общей SQLite-БД standalone'а
(таблица `tg_chat_map`). Один Telegram-чат = один диалог с агентом;
сообщения из чата стримят ответы обратно через `editMessageText`.
- Group-чаты (`group`, `supergroup`) и команда `/new` стартуют **новый
временный** диалог (`ConversationRecord.isTemporal = true`) — не
пишут в мапу, удаляются при следующем `/new`.
- Tool-call показывает «⚙️ обрабатываю…» через `sendChatAction(typing)` —
включается на `onlineOutbox.onlineEvents` пока обрабатывается ход.
- Ошибки агента стримятся в чат одной строкой, polling не падает.
### Как включить
1. Создать бота через `@BotFather`, скопировать токен.
2. Запустить standalone с токеном:
```bash
AGENTIK_TELEGRAM_TOKEN=123456:ABC-DEF... \
java --enable-native-access=ALL-UNNAMED \
-jar agentik-0.1.0-all.jar
```
3. Написать боту в личку (или добавить в группу). В логе standalone'а
появится:
```
telegram: enabled (polling timeout=30s, persistent=true)
```
4. Ответы приходят стримингом — `editMessageText` обновляет одно и то
же сообщение каждые ~1с пока агент генерирует.
### Persistent vs temporary
| Сценарий | `AGENTIK_TELEGRAM_PERSISTENT` | Поведение |
|---|---|---|
| Личка с ботом | `true` (default) | Один диалог на пользователя, переживает рестарт, `/new` стартует новый |
| Личка с ботом | `false` | Каждое сообщение — свежий `temp` диалог |
| Group-чат | (любое) | Всегда `temp` — каждый message может быть от другого юзера, диалоги не персистятся |
| `/new` | (любое) | Стартует новый `temp` диалог (для `persistent=true` это закрывает старый `ConversationRecord`) |
### Технические заметки
- Либа — своя, [`caffeine-mgn/telegramClient`](https://github.com/caffeine-mgn/telegramClient)
(`pw.binom.telegram:telegramClient`), опубликованная через
`publishToMavenLocal`. Артефакт `pw.binom.telegram.*` НЕ утекает в
classpath standalone'а — host-сайд видит только фабрику
`createTelegramBridgeComponent`, которая возвращает `AutoCloseable`.
- Polling/stream-джобы живут на отдельном `integrationsScope`. В
shutdown-hook'е он отменяется **до** `agent.close()` — иначе
long-poll держит соединение до `AGENTIK_TELEGRAM_POLLING_TIMEOUT`
и блокирует JVM-выход.
- Один бот на standalone (таблица `tg_chat_map` глобальная). Если
когда-нибудь захочется multi-agent + multi-bot — мапу надо будет
разводить по агентам.
## Эндпоинты
| Метод | Путь | Transport | Описание |