Skip to content

Интеграция 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 wecomwebsocket (длинное соединение умного бота) / 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 feishuwebsocket (поток событий длинного соединения) / webhookДа (потоковые карточки)ДаДа (root_id, для сообщений верхнего уровня используется собственный message_id)app_id, app_secret, verification_token, encrypt_key
Lark larkАналогично Feishu (один адаптер, RegionLark указывает на open.larksuite.com)ДаДаДаАналогично Feishu
Slack slackwebsocket (Socket Mode) / webhook (Events API)ДаДаДа (thread_ts)Для websocket: app_token + bot_token; для webhook: bot_token + signing_secret
Telegram telegramwebsocket (длинный опрос getUpdates) / webhookДа (редактирование сообщений)ДаДа (message_thread_id для Forum Topics)bot_token; для webhook также secret_token
DingTalk dingtalkwebsocket (режим Stream, поддерживается только с v0.8.2; миграция 000096 изменит каналы webhook на websocket, потребуется включить Stream в настройках разработчика DingTalk)Да (AI карточки)ДаНетclient_id, client_secret, card_template_id
Mattermost mattermostwebhook (поддерживает только 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 бот qqbotwebsocket (только поддержка)НетНетНетapp_id, client_secret, api_base_url, gateway_url
Yunzhijia yunzhijiawebhook / websocket (определяется по send_msg_url, выводится WS адрес)НетДаДа (верхний msgId / replyRootMsgId для ответа)send_msg_url (обязательно), secret, app_id, app_secret, allowed_webhook_host_suffix, timeout_seconds
MAX maxwebhook (авто-подписка с периодическим re-POST раз в 5 мин) / longpoll (dev)Да (редактирование через PUT /messages?message_id=)ДаНетbot_token (обязательно), webhook_secret (обязательно для webhook)
LINE linewebhook (только)Нет (полный вывод)ДаНетchannel_secret, channel_access_token (обязательны), channel_id (опционально)
Viber viberwebhook (только)Нет (полный вывод)ДаНет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 discordwebsocket (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Отображает список доступных команд или подробную информацию о конкретной командеНет
/infocmd_info.goПоказывает информацию и возможности привязанного Агента: режим Агента/RAG, список активных баз знаний ( KBSelectionMode all/selected/none), Skills, MCP сервисы, статус поиска в сети, режим выводаНет
/search <ключевое слово>cmd_search.goВыполняет гибридный поиск (векторный + ключевой) по доступным Агенту базам знаний, возвращая фрагменты текста (без ИИ-саммаризации); отображает максимум 5 результатов, по 200 рун каждый, с процентом совпадения. Диапазон поиска соответствует resolveKnowledgeBasesFromAgent в конвейере QA (с учетом фильтрации по возможностям режима Агента)Нет
/stopcmd_stop.goПрерывает текущий ответ (может прервать длинную цепочку ReAct-реакций)ActionStop: удаляет из очереди или отменяет текущую операцию; отправляет событие остановки в StreamManager (использует тот же механизм, что и StopSession в веб-интерфейсе, поддерживает кросс-инстансное прерывание — находит sessionID/messageID через im:inflight:; затем записывает в Redis маркер im:stop: для запросов, которые были поставлены в очередь, но еще не выполнены
/clearcmd_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 для отладки, давая доступ к ссылке в течение ее срока действия).

Чтобы изображения отображались корректно, выполните одно из следующих действий:

  1. Сделайте бэкенд хранилища доступным извне: Используйте публичный endpoint объектного хранилища (или установите MINIO_ENDPOINT на публичный хост), и resource:// будет преобразован в подписанный URL бэкенда.
  2. Настройте 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
OutputModestream (по умолчанию, потоковый) или full (ответ отправляется одним сообщением после получения полного текста)
LocaleЯзык ответа: zh-CN / en-US / ja-JP / ko-KR / ru-RU; другие значения возвращают 400. Если поле пустое (по умолчанию), используется WEKNORA_LANGUAGE; если не установлено, то zh-CN. Accept-Language из заголовков обратных вызовов IM поступает от платформы, а не от спрашивающего, поэтому не влияет на выбор языка ответа
KnowledgeBaseIDОпциональная "база знаний для файлов". Независимо от настройки, файлы/изображения скачиваются для понимания QA; при настройке сохранение выполняется в фоновом режиме (см. ниже)
SessionModeuser (по умолчанию, сеанс отображается по платформе+пользователю+чату) или thread (каждое верхнее сообщение/тема открывает новый сеанс, отображается по платформе+треду+чату)
BotIdentityУникальный идентификатор бота, выводимый из платформы+режима+учетных данных (computeBotIdentity, например feishu:<app_id>, telegram:<botID>, wecom:ws:<bot_id>); уникальный индекс в БД предотвращает привязку одного бота к двум каналам (ошибка duplicate_bot: при checkDuplicateBot → HTTP 409)
CredentialsJSONB учетные данные. Интерфейс списка (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_idURL обратного вызова платформы (в режиме 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.

json
{"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 записывается в Redis im: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): Подписка на события EventBus EventAgentThought (размышление), EventAgentToolCall/EventAgentToolResult (строка состояния инструмента, внутренние инструменты фильтруются isToolVisibleToUser; быстрый ответ отображает только конвейерные инструменты RAG query_understand/knowledge_search), EventAgentFinalAnswer (фрагменты ответа), EventAgentReferences (ссылки), EventAgentComplete. В режиме Агента "оптимистичный ответ" может быть отозван в блок размышлений при последующем вызове инструмента (аналогично superseded preamble в веб-интерфейсе). Каждые 300 мс буферизованный контент отправляется целиком (семантика замены для UpdateStreamContent); holdbackCutoff удерживает неполные URL provider://, изображения 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, стандартизируя различия платформы в четырех методах:

go
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 является центром обработки сообщений и отвечает за (см. комментарии к коду):

  1. Прием унифицированного IncomingMessage от Адаптера;
  2. Разрешение или создание сеанса aihelpme.ru для данного IM-канала (Session);
  3. Приоритетная диспетчеризация команд с косой чертой (не входят в конвейер QA);
  4. Обработка обычных сообщений через конвейер aihelpme.ru QA (KnowledgeQA / AgentQA);
  5. Сбор потоковых ответов и отправка их обратно через Адаптер.

Адаптеры платформ регистрируются через AdapterFactory (internal/container/container.go, registerIMAdapterFactories):

go
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

Версия v0.8.3 · MIT License