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
+22 -11
View File
@@ -23,7 +23,7 @@ val skipVectorMemory: Boolean =
// `jvm()` target). Чтобы не таскать лишние sourceSet'ы, ВСЁ живёт в
// commonMain + commonTest — даже зависимости, которые формально JVM-only
// (ktor-server-cio, litert-openai, ...). KMP-плагин тут
// только ради бесшовного потребления KMP-зависимостей (`:proto`, `:server`,
// только ради бесшовного потребления KMP-зависимостей (`:proto`,
// `:journal-api`, ...); компилируется всё ровно в одну JVM-таргет.
kotlin {
jvmToolchain(21)
@@ -40,7 +40,6 @@ kotlin {
commonMain.dependencies {
// Протокол + KMP storage API
implementation(project(":proto"))
implementation(project(":server"))
implementation(project(":journal-api"))
implementation(project(":outbox-api"))
implementation(project(":context-api"))
@@ -74,9 +73,10 @@ kotlin {
// Bounded-tail live event stream + per-event TTL.
implementation(project(":outbox-inmemory"))
// Персистентный счётчик событий (CursorStore) поверх ksqlite —
// Персистентный счётчик событий (CursorHolder) поверх ksqlite —
// offset'ы переживают рестарт, клиент продолжает инкрементально.
implementation(project(":outbox-ksqlite"))
implementation(project(":cursor-ksqlite"))
implementation(project(":cursor-inmemory"))
// :agent-api — MutableAgent + Component + ToolProvider/SystemPromptProvider.
// ChatAgent реализует MutableAgent; компоненты (McpBridgeComponent и т.п.)
@@ -90,8 +90,16 @@ kotlin {
// Skill-подсистема как Component: тулы + SkillMiningComponent.
implementation(project(":skill-mining"))
// Generic MCP-bridge (McpConfig, McpRegistry, McpLiteToolAdapter).
implementation(project(":mcp-bridge"))
// Generic MCP-bridge (McpConfig, McpRegistry, McpLiteToolAdapter).
implementation(project(":mcp-bridge"))
// Внешние мессенджер-интеграции. Подключаются как обычные
// :implementation (не api), чтобы при квант-like опциях в AppConfig
// можно было не класть их в classpath. Сейчас обе всегда включены —
// inactive-код стоит ~нулевых килобайт в fatjar (jar'ы маленькие),
// а опция «выключить интеграцию» живёт в `config.integrations.telegram`,
// не в classpath. См. README `:integrations-telegram`.
implementation(project(":integrations-telegram"))
// litert-kmp: контракт (commonMain)
api(libs.litert.api)
@@ -102,7 +110,7 @@ kotlin {
// litert-google: встроенный LiteRT-LM движок, нужен только на runtime
api(libs.litert.google)
// Ktor server (для :server facade + a2aServer)
// Ktor server (для A2A-фасада)
// Используем CIO вместо Netty — он KMP (jvm + linuxX64/ios/...), нам нужен
// для будущей native-сборки; на JVM работает идентично для нашего сценария.
implementation(libs.ktor.server.core)
@@ -111,8 +119,8 @@ kotlin {
implementation(libs.ktor.server.content.negotiation)
implementation(libs.ktor.serialization.kotlinx.json)
// Транспортные фасады (A2A остаётся заготовкой; на v1 не подключается в Main.kt —
// см. docs/ARCHITECTURE.md §3).
// A2A-фасад (agent↔agent). Proto user↔agent (:server) сюда НЕ подключается —
// standalone больше не выставляет :proto по сети.
implementation(libs.a2a.server)
// MCP (Model Context Protocol) клиент — подключение внешних/внутренних MCP-серверов
@@ -129,10 +137,13 @@ kotlin {
commonTest.dependencies {
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.coroutines.test)
implementation(libs.kotlinx.serialization.json)
implementation(libs.kotlin.test)
// Ktor test engine для smoke-тестов HTTP
implementation(libs.ktor.server.test.host)
// Ktor server нужен ТОЛЬКО тестам: ModelDownloaderTest поднимает fake
// HTTP-сервер на CIO, чтобы проверять скачивание/resume/HEAD.
implementation(libs.ktor.server.core)
implementation(libs.ktor.server.cio)
}
}
}