Интеграция IM (IM Integration)
Интеграция IM позволяет подключать Агентов к платформам обмена сообщениями, таким как Lark, Feishu, DingTalk, Slack, Telegram и другим. Пользователи могут задавать вопросы ботам на этих платформах, а aihelpme.ru будет выполнять поиск и отвечать на основе конфигурации привязанного Агента.
Новые каналы создаются в разделе «Настройки → Интеграция IM». Выберите платформу, введите учетные данные приложения и привяжите Агента, затем активируйте канал. В режиме Webhook необходимо указать URL обратного вызова в настройках платформы; в режиме постоянного соединения внешний URL обратного вызова для получения сообщений не требуется. Для каждого канала можно отдельно настроить «Язык ответа», фиксируя язык ответа Агента для данного канала; если поле пустое, используется язык по умолчанию для развертывания.
Поддерживаемые платформы также могут импортировать полученные файлы в базу знаний и предоставлять встроенные команды, такие как /help. Конкретное поведение зависит от возможностей платформы и конфигурации канала.
Сравнение поддерживаемых платформ и их возможностей
В internal/handler/im.go определено 16 допустимых платформ в validIMPlatforms. Возможности каждой платформы (согласно утверждениям времени компиляции factory.go и адаптеров):
| Платформа | Режим подключения (по умолчанию жирным) | Потоковая отправка ответов StreamSender | Скачивание файлов FileDownloader | Идентификатор треда/темы ThreadID | Основные поля учетных данных (credentials JSON) |
|---|---|---|---|---|---|
Lark / Feishu wecom | websocket (длинное соединение умного бота) / webhook (обратный вызов для внутреннего приложения) | Только в режиме websocket | Поддерживается в обоих режимах | Нет | Для websocket: bot_id, bot_secret, ws_endpoint, bot_name; для webhook: corp_id, agent_secret, token, encoding_aes_key, corp_agent_id, api_base_url |
Lark feishu | websocket (поток событий длинного соединения) / webhook | Да (потоковые карточки) | Да | Да (root_id, для сообщений верхнего уровня используется собственный message_id) | app_id, app_secret, verification_token, encrypt_key |
Lark lark | Аналогично Feishu (один адаптер, RegionLark указывает на open.larksuite.com) | Да | Да | Да | Аналогично Feishu |
Slack slack | websocket (Socket Mode) / webhook (Events API) | Да | Да | Да (thread_ts) | Для websocket: app_token + bot_token; для webhook: bot_token + signing_secret |
Telegram telegram | websocket (длинный опрос getUpdates) / webhook | Да (редактирование сообщений) | Да | Да (message_thread_id для Forum Topics) | bot_token; для webhook также secret_token |
DingTalk dingtalk | websocket (режим Stream, поддерживается только с v0.8.2; миграция 000096 изменит каналы webhook на websocket, потребуется включить Stream в настройках разработчика DingTalk) | Да (AI карточки) | Да | Нет | client_id, client_secret, card_template_id |
Mattermost mattermost | webhook (поддерживает только Outgoing Webhook + REST API) | Да | Да | Да (root_id) | site_url, bot_token, outgoing_token (обязательно), bot_user_id, post_to_main |
WeChat wechat (iLink бот) | longpoll (обязательно; при создании бэкенд принудительно устанавливает mode=longpoll, output_mode=full) | Нет (только вывод полного текста) | Да | Нет | bot_token, ilink_bot_id (оба обязательны) |
QQ бот qqbot | websocket (только поддержка) | Нет | Нет | Нет | app_id, client_secret, api_base_url, gateway_url |
Yunzhijia yunzhijia | webhook / websocket (определяется по send_msg_url, выводится WS адрес) | Нет | Да | Да (верхний msgId / replyRootMsgId для ответа) | send_msg_url (обязательно), secret, app_id, app_secret, allowed_webhook_host_suffix, timeout_seconds |
MAX max | webhook (авто-подписка с периодическим re-POST раз в 5 мин) / longpoll (dev) | Да (редактирование через PUT /messages?message_id=) | Да | Нет | bot_token (обязательно), webhook_secret (обязательно для webhook) |
LINE line | webhook (только) | Нет (полный вывод) | Да | Нет | channel_secret, channel_access_token (обязательны), channel_id (опционально) |
Viber viber | webhook (только) | Нет (полный вывод) | Да | Нет | auth_token, webhook_secret (обязательны), api_base (опционально) |
WhatsApp whatsapp (green-api) | longpoll (опрос уведомлений) | Нет (полный вывод) | Да | Нет | instance_id, api_token (обязательны), api_base (опционально) |
OK ok (Одноклассники) | webhook (авто-подписка с периодической проверкой) | Нет (полный вывод) | Да | Нет | access_token, secret_key (обязательны), api_base (опционально) |
Discord discord | websocket (Gateway protocol v10, только) | Да (редактирование с троттлингом 1100 мс) | Да | Нет | bot_token (обязательно), api_base (опционально) |
Встроенная система команд
Фреймворк команд находится в command.go / command_registry.go: команды лишь декларируют намерение (CommandResult.Action), а побочные эффекты выполняются Сервисом; LooksLikeCommand различает "попытку команды" (например, /help) и текстовый путь, который следует передать QA (например, /api/v2/users), — первое, если команда не зарегистрирована, вернет "неизвестная команда", второе — передастся в обычный цикл QA.
Все команды, зарегистрированные в NewService:
| Команда | Файл реализации | Функция | Побочный эффект |
|---|---|---|---|
/help [имя команды] | cmd_help.go | Отображает список доступных команд или подробную информацию о конкретной команде | Нет |
/info | cmd_info.go | Показывает информацию и возможности привязанного Агента: режим Агента/RAG, список активных баз знаний ( KBSelectionMode all/selected/none), Skills, MCP сервисы, статус поиска в сети, режим вывода | Нет |
/search <ключевое слово> | cmd_search.go | Выполняет гибридный поиск (векторный + ключевой) по доступным Агенту базам знаний, возвращая фрагменты текста (без ИИ-саммаризации); отображает максимум 5 результатов, по 200 рун каждый, с процентом совпадения. Диапазон поиска соответствует resolveKnowledgeBasesFromAgent в конвейере QA (с учетом фильтрации по возможностям режима Агента) | Нет |
/stop | cmd_stop.go | Прерывает текущий ответ (может прервать длинную цепочку ReAct-реакций) | ActionStop: удаляет из очереди или отменяет текущую операцию; отправляет событие остановки в StreamManager (использует тот же механизм, что и StopSession в веб-интерфейсе, поддерживает кросс-инстансное прерывание — находит sessionID/messageID через im:inflight:; затем записывает в Redis маркер im:stop: для запросов, которые были поставлены в очередь, но еще не выполнены |
/clear | cmd_clear.go | Очищает историю диалога | ActionClear: мягко удаляет текущий ChannelSession, следующему сообщению будет создан новый aihelpme.ru сеанс |
Поведение в групповых и личных чатах
ChatTypeопределяется адаптером:direct(личный чат,ChatIDпуст) илиgroup.- Feishu/Lark: В групповых чатах обычно требуется @упоминание бота (длинное соединение подписывается на групповые сообщения с префиксом
@_user_N, адаптер циклически удаляет его перед обработкой); при ответе групповые чаты по умолчанию используют reply-in-thread (ответ в теме), если тема не поддерживается (например, ошибка 230071), происходит откат к обычному отправке сообщения (логика отката вadapter.go). - Slack: Групповые сообщения поступают как
AppMentionEvent(@бот) иMessageEventиз канала/группы (фильтруются сообщения от ботов, не являющихсяfile_share, и сообщения подтипаBotID); ответы всегда отправляются в тему (thread_tsиспользуется для сообщений верхнего уровня). - Telegram:
group/supergroupраспознаются как групповые чаты, префикс@botnameудаляется; ответы отправляются сreply_to_message_id. - Mattermost: Ключевое слово для Outgoing Webhook должно быть первым словом сообщения, иначе оно будет интерпретировано как пустое сообщение (специальная обработка для диагностики в
handler/im.go); параметрpost_to_mainв учетных данных определяет, отправлять ли ответ в основной канал или в тему. - Изоляция сеансов: в режиме
userодин и тот же пользователь в "личном чате", "группе A" и "группе B" имеет разныеChannelSession(ключ содержитchat_id); в режимеthreadвсе пользователи в одной теме разделяют один сеанс.
Обработка файловых сообщений
Файлы и изображения обрабатываются как вложения QA: контент документа предоставляется модели, а изображения распознаются, если модель их поддерживает. Поэтому, даже если канал не настроен на использование базы знаний для файлов, бот сможет понять вложения и ответить на основе их содержимого.
knowledge_base_id определяет, будет ли вложение дополнительно сохранено в базу знаний. Оно должно принадлежать той же базе знаний, что и канал (для API с ограниченными правами требуется также соответствие белому списку баз знаний), иначе при создании или обновлении канала будет возвращен код 400. После настройки сохранение выполняется в фоновом режиме, не влияя на текущий ответ QA и не отправляя дополнительных сообщений о добавлении в базу или завершении обработки. Текст для обработки ограничен 500 строками и 32 КБ; при достижении любого из этих лимитов модель получит общее уведомление об усечении. Если вложение не удается прочитать, платформа не поддерживает его скачивание или размер файла превышает 32 МБ, бот предложит пользователю описать его текстом или отправить заново.
Переопределение внешних ссылок на изображения в ответах (resource://)
При цитировании изображений из базы знаний в ответе, внутренние ссылки типа resource://, local:// или minio:// не могут быть напрямую загружены IM-клиентом. Функция rewriteStorageURLs (internal/im/service.go) перед отправкой заменяет их на доступные HTTP(S) URL:
- Если результат не является HTTP(S) (например, внутренний путь
storage://), ссылка сохраняется как есть, и записывается предупреждение в журнал (с которым можно работать, так как у имеющих доступ к логам будет возможность использовать ссылку в течение срока ее действия). - Успешное переписывание регистрируется в журнале INFO (включая подписанный URL для отладки, давая доступ к ссылке в течение ее срока действия).
Чтобы изображения отображались корректно, выполните одно из следующих действий:
- Сделайте бэкенд хранилища доступным извне: Используйте публичный endpoint объектного хранилища (или установите
MINIO_ENDPOINTна публичный хост), иresource://будет преобразован в подписанный URL бэкенда. - Настройте
APP_EXTERNAL_URL:resource://будет переписан в<APP_EXTERNAL_URL>/r/<token>, запросы будут перенаправляться обратно в приложение черезlocation ^~ /r/в nginx. Официальный образ фронтенда уже имеет эту конфигурацию; при самостоятельном развертывании nginx необходимо добавить ее, иначе запросы попадут в SPA fallback и вернут пустую страницу.
Внутреннее развертывание MinIO ( minio:9000) и бэкенд local по умолчанию работают только по второму варианту. Если IM-канал включен, но APP_EXTERNAL_URL пуст, LoadAndStartChannels выведет предупреждение при запуске (imImageConfigWarning).
Если изображения по-прежнему не отображаются, проверьте все варианты доступа к файлам, как описано в Доступ к файлам и изображениям, — там собраны способы получения URL для четырех типов ссылок и для каждого канала.
Уведомление MCP OAuth (Привязка личности)
В сценариях IM нет интерактивного фронтенда для выполнения OAuth-авторизации в рамках сеанса для MCP-сервисов, поэтому:
withIMIdentityдобавляет в контекст маркерMCPOAuthNonInteractive. Когда Агент сталкивается с неавторизованным MCP-сервисом OAuth, он не блокирует выполнение, а генерирует одноразовое событиеEventMCPOAuthRequired.handleMessageStreamсобирает эти события (дедуплицируя по ServiceID), и после завершения ответа генерирует авторизационное уведомлениеbuildIMMCPAuthNotice, добавляемое в конец ответа: еслиAPP_EXTERNAL_URLнастроен иOAuthManagerдоступен, для каждого сервиса генерируется уникальная ссылка авторизации (URL обратного вызова<APP_EXTERNAL_URL>/api/v1/mcp-oauth/callback, основной субъект —PrincipalIMUser, то есть авторизация привязывается к "клиенту + каналу + платформе + IM пользователю"); в противном случае пользователю предлагается пройти авторизацию через административную панель aihelpme.ru.- После завершения авторизации пользователем по ссылке, повторите отправку исходного сообщения, чтобы использовать этот MCP-сервис.
Конфигурация и запуск
Модель и конфигурация канала (internal/im/types.go)
IMChannel (таблица im_channels) связывает бота определенной платформы с Агентом:
| Поле | Описание |
|---|---|
AgentID | Привязанный пользовательский Агент; ответы используют конфигурацию этого Агента (модель, база знаний, Skills, MCP, поиск в сети) |
Platform / Mode | Платформа и режим подключения. По умолчанию: mattermost/yunzhijia/max/line/viber/ok → webhook; wechat/whatsapp → longpoll (и принудительно output_mode=full); discord → websocket; остальные → websocket |
OutputMode | stream (по умолчанию, потоковый) или full (ответ отправляется одним сообщением после получения полного текста) |
Locale | Язык ответа: zh-CN / en-US / ja-JP / ko-KR / ru-RU; другие значения возвращают 400. Если поле пустое (по умолчанию), используется WEKNORA_LANGUAGE; если не установлено, то zh-CN. Accept-Language из заголовков обратных вызовов IM поступает от платформы, а не от спрашивающего, поэтому не влияет на выбор языка ответа |
KnowledgeBaseID | Опциональная "база знаний для файлов". Независимо от настройки, файлы/изображения скачиваются для понимания QA; при настройке сохранение выполняется в фоновом режиме (см. ниже) |
SessionMode | user (по умолчанию, сеанс отображается по платформе+пользователю+чату) или thread (каждое верхнее сообщение/тема открывает новый сеанс, отображается по платформе+треду+чату) |
BotIdentity | Уникальный идентификатор бота, выводимый из платформы+режима+учетных данных (computeBotIdentity, например feishu:<app_id>, telegram:<botID>, wecom:ws:<bot_id>); уникальный индекс в БД предотвращает привязку одного бота к двум каналам (ошибка duplicate_bot: при checkDuplicateBot → HTTP 409) |
Credentials | JSONB учетные данные. Интерфейс списка (IMChannelSummary) никогда не возвращает содержимое учетных данных, только булево значение credentials_configured |
ChannelSession (таблица im_channel_sessions) сопоставляет (platform, user_id, chat_id, thread_id, tenant_id) с aihelpme.ru session_id, обеспечивая непрерывность диалога на стороне IM. Если базовый сеанс удален из веб-интерфейса, HandleMessage обнаруживает ErrSessionNotFound, мягко удаляет устаревшее сопоставление и автоматически воссоздает его (исправляя проблему "вечной недоступности бота" в #1046, #1499).
API управления каналами (internal/handler/im.go + routes_agent.go)
| Метод и путь | Описание |
|---|---|
POST /api/v1/agents/:id/im-channels | Создание канала для Агента (проверяет допустимость платформы, устанавливает режимы по умолчанию mode/output_mode) |
GET /api/v1/agents/:id/im-channels | Получение списка каналов Агента (без учетных данных) |
GET /api/v1/im-channels | Общий обзор каналов по всем Агентам в клиенте |
PUT /api/v1/im-channels/:id | Частичное обновление (name/mode/output_mode/locale/session_mode/knowledge_base_id/credentials/enabled/agent_id); установка knowledge_base_id в пустую строку отключает базу знаний для файлов |
DELETE /api/v1/im-channels/:id | Удаление |
POST /api/v1/im-channels/:id/toggle | Включение/отключение |
POST /api/v1/wechat/qrcode, POST /api/v1/wechat/qrcode/status | Привязка WeChat (iLink) сканированием QR-кода: генерация QR-кода и опрос статуса |
GET / POST /api/v1/im/callback/:channel_id | URL обратного вызова платформы (в режиме webhook настраивается в панели управления платформы; проходит проверку подписи платформы, не требует API ключа aihelpme.ru) |
В режиме webhook URL https://<ваш_домен>/api/v1/im/callback/<channel_id> указывается в настройках подписки на события/обратных вызовов платформы; aihelpme.ru сначала отвечает на вызов проверки URL платформы (HandleURLVerification, например, эхо challenge для Feishu или расшифровка echostr для WeCom), а затем каждый обратный вызов проходит проверку подписи VerifyCallback. В режимах WebSocket/длинного соединения внешний URL обратного вызова не требуется, aihelpme.ru сама подключается к шлюзу платформы.
Дополнительные платформы (MAX, LINE, Viber, WhatsApp (green-api), OK, Discord):
- MAX (
max) — webhook (по умолчанию, авто-подписка с периодическим re-POST раз в 5 мин) или longpoll (dev). Требуютсяbot_tokenиwebhook_secret; в webhook-режиме обязательноAPP_EXTERNAL_URL. Ответы стримятся через редактирование сообщения. - LINE (
line) — только webhook. Требуютсяchannel_secretиchannel_access_token; правка сообщений не поддерживается, поэтому вывод принудительноfull. - Viber (
viber) — только webhook. Требуютсяauth_tokenиwebhook_secret; выводfull. - WhatsApp (
whatsapp, green-api) — только longpoll (опрос уведомлений). Требуютсяinstance_idиapi_token; выводfull. - OK (
ok, Одноклассники) — только webhook, авто-подписка с периодической проверкой. Требуютсяaccess_tokenиsecret_key; callback URL содержит?secret=<secret_key>; выводfull. - Discord (
discord) — только websocket (Gateway protocol v10). Требуетсяbot_token; ответы стримятся через редактирование сообщения (троттлинг 1100 мс).
Обратное проксирование Feishu/Lark
credentials.api_base_url может переопределять origin API и одновременно служить начальным доменом для SDK длинного соединения. Если поле пустое, используются стандартные облачные адреса Feishu/Lark; в частной сети можно указать https://feishu-proxy.example.com, без добавления конкретного пути API в конце. Прокси должен перенаправлять запросы к API платформы и для инициализации длинного соединения, а возвращаемый адрес WebSocket также должен быть доступен с сервера aihelpme.ru. Проксирование только через веб-консоль не решает сетевых проблем между сервером и Feishu.
{"platform":"feishu","mode":"websocket","credentials":{"app_id":"<app-id>","app_secret":"<app-secret>","api_base_url":"https://feishu-proxy.example.com"}}При выборе session_mode=thread для Yunzhijia сеанс используется повторно в рамках темы; верхние сообщения инициируют новую тему, ответы продолжаются в той же теме. Богатые текстовые сообщения DingTalk извлекают читаемый контент; изображения в сообщениях Feishu обрабатываются как изображения.
Надежность длинного соединения: выборы лидера и Supervisor
- Выборы лидера для кросс-инстансного соединения (
service.go): В развертываниях с несколькими экземплярами (при наличии Redis) каналы websocket/longpoll используютSETNX im:ws:leader:<channelID>(TTL 15с, обновление каждые 5с) для гарантии, что только один экземпляр поддерживает длинное соединение; не-лидер экземпляры пытаются захватить блокировку каждые 10с, и при сбое лидера происходит автоматическое переключение. После остановки канала longpoll блокировка сохраняется до истечения срока действия, чтобы избежать кратковременного двойного выполнения при переключении между старым и новым экземплярами. При неудачной попытке обновления (потеря статуса лидера) запускаетсяhandleWSLeadershipLoss: сначала останавливается адаптер текущего экземпляра, затем канал возвращается в цикл повторных попыток захвата блокировки — перед попыткой выполняется повторное чтение строки канала из БД, поэтому каналы, удаленные, отключенные или измененные в конфигурации в процессе, не будут активированы старым исполняемым кодом. - Поддержание соединения (
supervisor.go,RunSupervised): Некоторые SDK (DingTalk, Feishu) могут иметь внутренние механизмы переподключения, которые, тем не менее, могут привести к состоянию, когда объект соединения существует, но сообщения не поступают. Supervisor принудительно пересоздает соединение каждые 6 часов (defaultRecycleInterval), при сбое подключения выполняется повторная попытка с экспоненциальной задержкой (5с), ограничивая время простоя до интервала пересоздания. - Длинное соединение Yunzhijia (
yunzhijia/websocket.go): Отправляет сигнал Keep-Alive каждые 15с, при отсутствии данных в течение 45с соединение считается недействительным и переподключается; каждое соединение принудительно пересоздается через 6 часов. Повторное подключение выполняется с задержкой 1с, 2с, 5с, 10с, 30с, 60с.
Ключевые моменты мульти-экземплярного развертывания
Все распределенные состояния централизованы в константах префиксов ключей Redis в service.go:
| Ключ Redis | Назначение |
|---|---|
im:ws:leader:<channelID> | Выборы лидера для каналов WebSocket/longpoll (TTL 15с, обновление каждые 5с, повторные попытки захвата блокировки каждые 10с) |
im:dedup:<messageID> | Дедупликация сообщений между экземплярами (TTL 5мин) |
im:stop:<userKey> | Префикс для маркеров выполнения команды /stop между экземплярами (TTL 30с) |
im:inflight:<userKey> | Сопоставление userKey → sessionID:messageID, используется для отправки события остановки в StreamManager при кросс-инстансном /stop |
im:queue:user:<userKey> | Глобальный счетчик очереди для одного пользователя |
im:ratelimit:<key> | Скользящее окно для ограничения скорости (ZSET) |
im:global:active | Глобальный счетчик активных QA воркеров (атомарная операция INCR+проверка через Lua, TTL 5мин для самовосстановления) |
При отсутствии Redis (режим Lite/один экземпляр) все эти функции работают в локальной памяти, не влияя на функциональность, но теряя кросс-инстансное поведение.
Поток обработки сообщений
IMCallback (webhook) или обратные вызовы длинного соединения в конечном итоге попадают в Service.HandleMessage, после чего передаются в очередь QA для выполнения:
Ключевые детали (см. service.go):
- Дедупликация:
MessageIDзаписывается в Redisim:dedup:(TTL 5 минут) или локальныйsync.Map(в режиме одного экземпляра); обратные вызовы от IM-платформ, полученные повторно, игнорируются. - Ограничение скорости: Скользящее окно на основе
channelID:userID:chatID[:threadID](по умолчанию 10 запросов за 60 секунд, можно переопределить черезconfig.IM); команды с косой чертой игнорируют ограничение, гарантируя пользователю возможность/stopдаже в условиях пиковой нагрузки. - Очередь QA (
qaqueue.go): Ограниченная очередь + фиксированный пул воркеров (по умолчанию 5 воркеров, максимальная длина очереди 50, тайм-аут ожидания в очереди 60 секунд). В мульти-экземплярных развертываниях используется Redis для реализации глобального лимита для одного пользователя (im:queue:user:) и опционального глобального барьера конкурентности (im:global:active+ Lua скрипт,GlobalMaxWorkersв конфигурации), создавая обратное давление на LLM. Если позиция в очереди > 0, сначала отправляется уведомление "в очереди". - Разбор сеанса: В режиме
userсеансы разделяются по пользователям; заголовок имеет формат "Имя пользователя · Групповой чат 1a2b3c4d"; в режимеthreadкаждый верхний пост/тема использует свой сеанс (Slack thread, Feishu topic group, Telegram Forum Topic, Mattermost root_id). Первый ответ асинхронно генерирует заголовок сеанса (GenerateTitleAsync). - Внедрение личности (
withIMIdentity): IM обратные вызовы используют подпись платформы, а не статус входа aihelpme.ru, поэтому внедряется синтетическая личностьsystem-<tenantID>+PrincipalIMUser(tenantID:channelID:platform:userID) + роль Viewer, чтобы обеспечить корректную работу таких функций, как общий доступ к базе знаний, зависящих от UserID; также устанавливается флагMCPOAuthNonInteractive(см. Уведомление MCP OAuth (Привязка личности)). - Потоковое отображение (
handleMessageStream+think.go+tool_display.go): Подписка на события EventBusEventAgentThought(размышление),EventAgentToolCall/EventAgentToolResult(строка состояния инструмента, внутренние инструменты фильтруютсяisToolVisibleToUser; быстрый ответ отображает только конвейерные инструменты RAGquery_understand/knowledge_search),EventAgentFinalAnswer(фрагменты ответа),EventAgentReferences(ссылки),EventAgentComplete. В режиме Агента "оптимистичный ответ" может быть отозван в блок размышлений при последующем вызове инструмента (аналогично superseded preamble в веб-интерфейсе). Каждые 300 мс буферизованный контент отправляется целиком (семантика замены дляUpdateStreamContent);holdbackCutoffудерживает неполные URLprovider://, изображения Markdown и XML-теги на границах фрагментов, чтобы избежать мерцания неполного контента. ФинальныйFinalizeStreamсохраняет только текст ответа (StripThinkBlocks), очищая теги цитирования<kb/>,<web/>и XML<image>, а также переписывая URL хранилищаprovider://в доступные для внешнего мира ссылки (cleanIMContent/rewriteStorageURLs). - Завершение потока: В режиме Агента получение
EventAgentCompleteили возвращаемого значения от AgentQA (что раньше произойдет) завершает потоковую отправку, предотвращая зависание карточки в состоянии "генерация"; ошибки, возникающие после события завершения, все равно собираются и добавляются к финальному ответу. Потоки ответов быстрого ответа (KnowledgeQA) асинхронны и завершаются вместе с потоком. Потоковые карточки Feishu остаются активными во время длительных ответов и не отменяются из-за истечения срока действия. - Непотоковый режим: Для каналов с
output_mode=full, адаптеров, не поддерживающихStreamSender, или при сбоеStartStream, используетсяrunQAдля агрегирования полного ответа, после чего он отправляется одним сообщением черезSendReply. - Цитирование сообщений (
Quote, в настоящее время заполняется адаптером WeCom для длинных соединений и т. д.): Текстовые цитаты встраиваются в контекст LLM в виде<quoted_message>(до 500 рун, различая "цитирование собственного ответа бота"); при цитировании изображений/файлов/видео и т. д. вместо них отправляется инструкция, "сообщающая пользователю о невозможности просмотра этого контента", чтобы модель не пыталась угадать непрочитаемый контент.
Обзор архитектуры
Интерфейс Adapter (internal/im/adapter.go)
Каждый адаптер платформы реализует унифицированный интерфейс Adapter, стандартизируя различия платформы в четырех методах:
type Adapter interface {
Platform() Platform
// VerifyCallback проверяет подпись/токен входящего запроса обратного вызова
VerifyCallback(c *gin.Context) error
// ParseCallback преобразует исходный обратный вызов платформы в унифицированный IncomingMessage (возвращает nil, если это не событие сообщения)
ParseCallback(c *gin.Context) (*IncomingMessage, error)
// SendReply отправляет ответ обратно на платформу IM
SendReply(ctx context.Context, incoming *IncomingMessage, reply *ReplyMessage) error
// HandleURLVerification обрабатывает вызов проверки URL платформы
HandleURLVerification(c *gin.Context) bool
}Два опциональных интерфейса расширения определяют различия между платформами:
StreamSender— потоковая отправка ответа (StartStream→UpdateStreamContent(семантика полной замены) →FinalizeStream(сохраняет только ответ, удаляя размышления/процессы инструментов) →EndStream). Реализации: Feishu/Lark (потоковые карточки), DingTalk (AI карточки, требуетcard_template_id), Slack, Telegram (редактирование сообщений), Mattermost, WeCom в режиме WebSocket.FileDownloader— скачивание пользовательских файлов/изображений с платформы (DownloadFile). Реализации: все платформы, кроме QQ бота (WeCom поддерживает оба режима).
Унифицированная модель сообщений IncomingMessage содержит поля, такие как Platform, MessageType (text/file/image), UserID, ChatID, ChatType (direct/group), Content, MessageID (для дедупликации), FileKey/FileName/FileSize, ThreadID (ID темы/треда), Quote (цитируемое сообщение) и другие.
Оркестрация Сервиса (internal/im/service.go)
im.Service является центром обработки сообщений и отвечает за (см. комментарии к коду):
- Прием унифицированного
IncomingMessageот Адаптера; - Разрешение или создание сеанса aihelpme.ru для данного IM-канала (Session);
- Приоритетная диспетчеризация команд с косой чертой (не входят в конвейер QA);
- Обработка обычных сообщений через конвейер aihelpme.ru QA (
KnowledgeQA/AgentQA); - Сбор потоковых ответов и отправка их обратно через Адаптер.
Адаптеры платформ регистрируются через AdapterFactory (internal/container/container.go, registerIMAdapterFactories):
imService.RegisterAdapterFactory("wecom", wecom.NewFactory())
imService.RegisterAdapterFactory("feishu", feishu.NewFactory(feishu.RegionFeishu))
imService.RegisterAdapterFactory("lark", feishu.NewFactory(feishu.RegionLark)) // Lark и Feishu используют один адаптер, различаясь только доменом API
imService.RegisterAdapterFactory("slack", slack.NewFactory())
imService.RegisterAdapterFactory("telegram", telegram.NewFactory())
imService.RegisterAdapterFactory("dingtalk", dingtalk.NewFactory())
imService.RegisterAdapterFactory("mattermost", mattermost.NewFactory())
imService.RegisterAdapterFactory("wechat", wechat.NewFactory())
imService.RegisterAdapterFactory("qqbot", qqbot.NewFactory())
imService.RegisterAdapterFactory("yunzhijia", yunzhijia.NewFactory())
imService.RegisterAdapterFactory("max", maxPkg.NewFactory())
imService.RegisterAdapterFactory("line", linePkg.NewFactory())
imService.RegisterAdapterFactory("viber", viberPkg.NewFactory())
imService.RegisterAdapterFactory("whatsapp", whatsappPkg.NewFactory())
imService.RegisterAdapterFactory("ok", okPkg.NewFactory())
imService.RegisterAdapterFactory("discord", discordPkg.NewFactory())Справочник по реализации
- Основной фреймворк и оркестрация:
internal/im/(adapter.go,service.go,supervisor.go,command*.go,qaqueue.go,session/stream/think/tool_displayи др.) - Адаптеры для каждой платформы:
internal/im/{wecom,feishu,dingtalk,slack,telegram,mattermost,wechat,qqbot,yunzhijia,max,line,viber,whatsapp,ok,discord}/ - Уровень HTTP-интерфейса:
internal/handler/im.go - Маршрутизация:
RegisterIMRoutes/RegisterIMChannelRoutesвinternal/router/routes_agent.go
