IM 통합(IM Integration)
IM 통합은 에이전트를 WeCom, Feishu, DingTalk, Slack, Telegram 등의 채팅 플랫폼에 연결합니다. 사용자가 플랫폼에서 봇에게 질문하면 WeKnora가 연결된 에이전트 설정에 따라 검색하고 답변합니다.
'설정 → IM 통합'에서 채널을 만들고 플랫폼을 선택한 뒤 앱 자격 증명을 입력하고 에이전트를 연결하여 활성화합니다. Webhook 모드는 플랫폼 관리 화면에 콜백 주소를 입력해야 합니다. 지속 연결 모드는 메시지 수신을 위한 공개 콜백 주소가 필요하지 않습니다.
채널 목록과 개별 채널 설정 양식(플랫폼 유형, 자격 증명, 연결된 Agent, 콜백 주소)을 표시합니다.
website-docs/public/screenshots/im-channels.png지원되는 플랫폼에서는 받은 파일을 지식 베이스로 가져올 수 있으며 /help 등의 내장 명령도 제공합니다. 구체적인 동작은 플랫폼 기능과 채널 설정에 따라 달라집니다.
지원 플랫폼 및 기능 비교
internal/handler/im.go의 validIMPlatforms는 유효한 플랫폼 10개를 정의합니다. 플랫폼별 기능은 다음과 같습니다(각 factory.go와 adapter의 컴파일 타임 단언 기준).
| 플랫폼 | 연결 모드(기본값은 굵게) | 스트리밍 답변 StreamSender | 파일 다운로드 FileDownloader | 스레드/주제 ThreadID | 주요 자격 증명 필드(credentials JSON) |
|---|---|---|---|---|---|
WeCom 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 |
Feishu 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 | 예(메시지 편집) | 예 | 예(Forum Topics의 message_thread_id) | bot_token; webhook은 추가로 secret_token |
DingTalk dingtalk | websocket(Stream 모드) / webhook | 예(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 |
내장 명령 시스템
명령 프레임워크는 command.go / command_registry.go에 있습니다. 명령은 의도(CommandResult.Action)만 선언하고 부수 효과는 Service가 실행합니다. LooksLikeCommand는 "명령 시도"(/help)와 QA에 그대로 전달할 경로 텍스트(/api/v2/users)를 구분합니다. 전자가 미등록 명령이면 "알 수 없는 명령이에요"라고 답하고 후자는 정상적으로 질의응답으로 전달합니다.
NewService에 등록된 전체 명령:
| 명령 | 구현 파일 | 기능 | 부수 효과 |
|---|---|---|---|
/help [명령 이름] | cmd_help.go | 사용 가능한 모든 명령 나열 또는 특정 명령의 상세 사용법 확인 | 없음 |
/info | cmd_info.go | 현재 연결된 Agent의 정보 및 기능 표시: Agent/RAG 모드, 활성 지식 베이스 목록(KBSelectionMode all/selected/none), Skills, MCP 서비스, 웹 검색 스위치, 출력 모드 | 없음 |
/search <키워드> | cmd_search.go | Agent가 접근 가능한 지식 베이스에서 직접 하이브리드 검색(벡터+키워드)하여 원문 청크 반환(AI 요약 없음); 최대 5개, 각 200 rune, 일치도 백분율 포함. 지식 베이스 범위는 QA 파이프라인의 resolveKnowledgeBasesFromAgent와 동일(Agent 모드 기능 필터 포함) | 없음 |
/stop | cmd_stop.go | 현재 진행 중인 답변 중단(긴 ReAct 추론 체인도 중단 가능) | ActionStop: 먼저 큐에서 제거하거나 로컬 in-flight 취소; 다음으로 StreamManager에 stop 이벤트 기록(Web의 StopSession과 같은 방식, 인스턴스 간 중단 지원 — im:inflight: 매핑으로 sessionID/messageID 조회); 마지막으로 Redis im:stop: 표시를 기록해 "큐에 있지만 아직 실행되지 않은" 요청도 처리 |
/clear | cmd_clear.go | 대화 기억 지우기 | ActionClear: 현재 ChannelSession 소프트 삭제, 다음 메시지에서 새 WeKnora 세션 생성 |
그룹 채팅 및 개인 채팅 동작
- 어댑터가
ChatType을direct(개인 채팅,ChatID는 비어 있음) 또는group으로 판단합니다. - Feishu/Lark: 그룹 채팅에서는 일반적으로 @봇 멘션이 필요합니다(지속 연결로 구독한 그룹 메시지 텍스트에는
@_user_N접두사가 붙으며, 어댑터가 반복 제거한 뒤 처리). 그룹 답변은 reply-in-thread(주제 답글)를 우선 사용하고, 그룹이 주제를 지원하지 않으면(오류 코드 230071 등) 일반 메시지 전송으로 자동 대체합니다(adapter.go의 fallback 로직). - Slack: 그룹 메시지는
AppMentionEvent(@봇)와 channel/group의MessageEvent에서 받습니다(BotID가 있는 봇 메시지 및file_share가 아닌 subtype은 필터링). 답변은 항상 thread에 보냅니다(thread_ts, 최상위 메시지는 자신의 타임스탬프 사용). - Telegram:
group/supergroup을 그룹 채팅으로 판단하고@botname멘션 접두사를 제거합니다. 답변에reply_to_message_id를 포함합니다. - Mattermost: Outgoing Webhook 트리거 단어는 메시지의 첫 단어여야 합니다. 아니면 콜백이 빈 메시지로 파싱됩니다(
handler/im.go에 이를 위한 진단 로그가 있음).post_to_main자격 증명은 답글을 메인 채널에 보낼지 스레드에 보낼지 제어합니다. - 세션 격리:
user모드에서는 같은 사용자라도 "개인 채팅", "그룹 A", "그룹 B"마다 서로 다른ChannelSession을 사용합니다(key에chat_id포함).thread모드에서는 같은 스레드의 모든 사용자가 세션을 공유합니다.
파일 메시지 처리
파일과 이미지는 QA 첨부 파일로 처리합니다. 문서 콘텐츠는 모델에 제공하고 이미지는 모델이 지원하면 직접 인식합니다. 따라서 채널에 파일 지식 베이스를 설정하지 않아도 봇은 첨부 콘텐츠를 바탕으로 정상적으로 답변합니다.
knowledge_base_id는 첨부 파일을 지식 베이스에도 저장할지만 결정합니다. 설정하면 저장 작업을 백그라운드에서 실행하므로 현재 QA 답변에 영향을 주지 않으며 "지식 베이스에 저장했어요"나 "파싱을 완료했어요" 같은 추가 메시지도 보내지 않습니다. 파싱 텍스트는 앞 500행까지만, 최대 32 KiB로 유지하며 어느 한도든 도달하면 모델에 일반적인 잘림 안내를 제공합니다. 첨부 파일을 읽을 수 없거나 플랫폼이 다운로드를 지원하지 않거나 파일이 32 MiB를 넘으면 봇이 텍스트로 설명하거나 다시 보내도록 안내합니다.
답변의 이미지 외부 링크(resource:// 재작성)
답변이 지식 베이스 이미지를 인용할 때 본문에는 resource:// 또는 local:// / minio:// 같은 내부 참조가 들어가므로 IM 클라이언트가 직접 가져올 수 없습니다. rewriteStorageURLs(internal/im/service.go)가 전송 전에 접근 가능한 http(s) URL로 바꿉니다.
- 해석 결과가 http(s)가 아니면(예: 여전히 내부
storage://경로) 원래 참조를 유지하고 조치 가능한 WARN을 남깁니다. IM에서 반드시 로드에 실패할 링크로 바꾸어 보내지는 않습니다. - 재작성 성공 시 INFO 로그를 남깁니다(진단을 위한 서명 URL 포함. 대신 로그 권한이 있는 사람은 유효 기간 내 해당 링크를 사용할 수 있습니다).
이미지를 정상적으로 표시하려면 다음 중 하나를 선택합니다.
- 저장소 백엔드를 공개 네트워크에서 접근 가능하게 설정: 객체 저장소에 공개 endpoint를 사용하거나
MINIO_ENDPOINT를 공개 host로 설정하면resource://가 백엔드 사전 서명 URL로 대체됩니다. APP_EXTERNAL_URL설정:resource://를<APP_EXTERNAL_URL>/r/<token>으로 바꾸고, 요청을 nginx의location ^~ /r/을 통해 app으로 리버스 프록시합니다. 공식 프런트엔드 이미지에는 이 location이 내장되어 있습니다. 자체 리버스 프록시에는 직접 추가해야 하며, 없으면 요청이 SPA fallback으로 들어가 빈 페이지를 반환합니다.
기본 MinIO 내부망 배포(minio:9000)와 local 백엔드는 두 번째 방법만 사용할 수 있습니다. IM 채널이 활성화되어 있지만 APP_EXTERNAL_URL이 비어 있으면 LoadAndStartChannels가 시작 경고를 한 번 출력합니다(imImageConfigWarning).
이미지가 여전히 보이지 않으면 이미지와 파일의 외부 접근의 점검표를 항목별로 확인하세요. 네 가지 URL 형식과 각 채널에서 가져오는 방식을 정리한 문서입니다.
MCP OAuth 권한 부여 알림(신원 바인딩)
IM 환경에는 MCP 서비스의 세션 내 OAuth 권한 부여를 완료할 대화형 프런트엔드가 없으므로 다음과 같이 처리합니다.
withIMIdentity가 컨텍스트에MCPOAuthNonInteractive표시를 추가합니다. Agent가 권한이 없는 OAuth MCP 서비스를 만나면 기다리며 차단하지 않고 일회성EventMCPOAuthRequired이벤트를 보냅니다.handleMessageStream이 이벤트를 수집하고(ServiceID로 중복 제거), 답변 완료 후buildIMMCPAuthNotice가 권한 부여 안내를 만들어 답변 끝에 추가합니다.APP_EXTERNAL_URL이 설정되어 있고 OAuthManager를 사용할 수 있으면 서비스별 전용 권한 부여 링크를 생성합니다(콜백 주소<APP_EXTERNAL_URL>/api/v1/mcp-oauth/callback, 주체는PrincipalIMUser로 권한이 "테넌트+채널+플랫폼+IM 사용자"에 바인딩됨). 그 외에는 WeKnora 관리 화면에서 권한 부여를 완료하도록 안내합니다.- 사용자가 링크를 눌러 권한 부여를 완료한 뒤 원래 메시지를 다시 보내면 해당 MCP 서비스를 사용할 수 있습니다.
설정 및 운영 참고
채널 모델 및 설정(internal/im/types.go)
IMChannel(테이블 im_channels) 하나는 특정 플랫폼 봇을 특정 Agent에 연결합니다.
| 필드 | 설명 |
|---|---|
AgentID | 연결된 사용자 정의 에이전트; 답변은 해당 Agent의 설정(모델, 지식 베이스, Skills, MCP, 웹 검색) 사용 |
Platform / Mode | 플랫폼과 연결 모드. 기본값: mattermost/yunzhijia → webhook, wechat → longpoll(output_mode=full도 강제), 나머지 → websocket |
OutputMode | stream(기본값, 스트리밍) 또는 full(전체 답변이 완성되면 한 번에 답변) |
KnowledgeBaseID | 선택적 "파일 지식 베이스". 설정 여부와 무관하게 파일/이미지는 다운로드하여 QA 이해에 사용하며, 설정 시 백그라운드 수집도 수행(아래 참고) |
SessionMode | user(기본값, 플랫폼+사용자+그룹 기준 세션 매핑) 또는 thread(플랫폼+스레드+그룹 기준, 최상위 메시지마다 새 세션) |
BotIdentity | 플랫폼+모드+자격 증명에서 유도한 봇 고유 식별자(computeBotIdentity, 예: feishu:<app_id>, telegram:<botID>, wecom:ws:<bot_id>). DB 고유 인덱스가 같은 봇의 두 채널 중복 설정 방지(checkDuplicateBot이 duplicate_bot: 접두사 오류 반환 → HTTP 409) |
Credentials | JSONB 자격 증명. 목록 API(IMChannelSummary)는 자격 증명 내용을 절대 반환하지 않고 credentials_configured 불리언만 반환 |
ChannelSession(테이블 im_channel_sessions)은 (platform, user_id, chat_id, thread_id, tenant_id)를 WeKnora session_id에 매핑하여 IM 대화의 연속성을 구현합니다. 기반 Session을 Web UI에서 삭제하면 HandleMessage가 ErrSessionNotFound를 감지하여 오래된 매핑을 소프트 삭제하고 자동 재생성합니다(#1046, #1499의 "봇이 영구적으로 응답하지 않는" 문제 수정).
채널 관리 API(internal/handler/im.go + router.go)
| 메서드 및 경로 | 설명 |
|---|---|
POST /api/v1/agents/:id/im-channels | Agent 채널 생성(platform 유효성 검증, 기본 mode/output_mode 채우기) |
GET /api/v1/agents/:id/im-channels | Agent 채널 목록(자격 증명 제외) |
GET /api/v1/im-channels | 테넌트 내 전체 Agent의 채널 개요 |
PUT /api/v1/im-channels/:id | 업데이트(name/mode/output_mode/knowledge_base_id/credentials/enabled/agent_id) |
DELETE /api/v1/im-channels/:id | 삭제 |
POST /api/v1/im-channels/:id/toggle | 활성화/비활성화 |
GET / POST /api/v1/im/callback/:channel_id | 플랫폼 콜백 주소(webhook 모드에서 각 플랫폼 관리 화면에 설정; 플랫폼 자체 서명 검증을 사용하므로 WeKnora API Key 불필요) |
Webhook 모드에서는 https://<你的域名>/api/v1/im/callback/<channel_id>를 플랫폼의 이벤트 구독/콜백 주소에 입력합니다(<你的域名>은 자신의 도메인으로 바꿉니다). WeKnora가 먼저 플랫폼의 URL 검증 챌린지에 응답합니다(HandleURLVerification, 예: Feishu challenge 반환, WeCom echostr 복호화). 이후 모든 콜백은 VerifyCallback 서명 검증을 거칩니다. WebSocket/지속 연결 모드는 공개 콜백 주소 없이 WeKnora가 플랫폼 게이트웨이에 능동적으로 연결합니다.
Feishu/Lark 리버스 프록시
credentials.api_base_url로 API origin을 재정의할 수 있으며, 지속 연결 SDK의 bootstrap domain으로도 사용합니다. 비우면 각각 Feishu/Lark 기본 클라우드 주소를 사용합니다. 사설 네트워크에서는 https://feishu-proxy.example.com을 입력할 수 있으며 끝에 특정 API 경로를 붙이지 않습니다. 프록시는 플랫폼 API와 지속 연결 시작 요청을 전달해야 하고, 시작 응답의 WebSocket 주소 역시 WeKnora 서버에서 접근할 수 있어야 합니다. 웹 콘솔만 프록시해서는 서버와 Feishu 간 네트워크 문제를 해결할 수 없습니다.
{"platform":"feishu","mode":"websocket","credentials":{"app_id":"<app-id>","app_secret":"<app-secret>","api_base_url":"https://feishu-proxy.example.com"}}Yunzhijia에서 session_mode=thread를 선택하면 주제별로 세션을 재사용합니다. 최상위 메시지는 새 스레드를 시작하고 답글은 루트 메시지의 스레드를 이어갑니다. DingTalk 리치 텍스트 메시지는 읽을 수 있는 콘텐츠를 추출하고, Feishu post 메시지의 이미지는 이미지 처리로 전달합니다. output_mode=full은 중간 과정과 출력 진행 상황을 표시할 수 있으며 answer_only는 최종 답변만 유지합니다.
지속 연결의 신뢰성: leader 선출 및 Supervisor
- 다중 인스턴스 leader 선출(
service.go): websocket/longpoll 채널은 Redis가 있는 다중 인스턴스 배포에서SETNX im:ws:leader:<channelID>(TTL 15s, 5s마다 갱신)로 인스턴스 하나만 지속 연결을 유지하도록 합니다. leader가 아닌 인스턴스는 10s마다 잠금 획득을 재시도하고 leader 장애 시 자동 인계받습니다. longpoll 채널 중지 후 잠금을 TTL 만료까지 유지하여 이전/새 인스턴스가 잠깐 동시에 쓰는 일을 방지합니다. 갱신 실패(leader 자격 상실) 시handleWSLeadershipLoss가 먼저 로컬 어댑터를 중지한 뒤 채널을 잠금 획득 재시도 루프로 돌려보냅니다. 재시도 전에 DB 채널 행을 다시 읽으므로 그동안 삭제, 비활성화, 설정 변경된 채널을 이전 런타임이 되살리지 않습니다. - 연결 유지(
supervisor.go의RunSupervised): 일부 SDK(DingTalk, Feishu)의 내부 재연결에서 연결 객체는 있지만 메시지를 받지 못하는 상태가 생길 수 있습니다. Supervisor는 6시간마다(defaultRecycleInterval) 연결을 능동적으로 재생성하며, 연결 실패 시 5s 백오프로 재시도하여 최악의 중단 시간을 재생성 간격 이내로 제한합니다.
다중 인스턴스 배포 핵심 사항
모든 분산 상태는 service.go의 Redis key 접두사 상수에 모아 정의합니다.
| Redis Key | 용도 |
|---|---|
im:ws:leader:<channelID> | WebSocket/롱 폴링 채널 leader 선출(TTL 15s, 5s 갱신, 10s 잠금 획득 재시도) |
im:dedup:<messageID> | 인스턴스 간 메시지 중복 제거(TTL 5min) |
im:stop:<userKey> | 인스턴스 간 /stop 실행 전 표시(TTL 30s) |
im:inflight:<userKey> | userKey → sessionID:messageID 매핑, 인스턴스 간 /stop이 StreamManager 중지 이벤트 기록 시 사용 |
im:queue:user:<userKey> | 전역 사용자별 대기 수 |
im:ratelimit:<key> | 슬라이딩 윈도우 요청 제한(ZSET) |
im:global:active | 전역 동시 QA worker 수(Lua 원자적 INCR+검증, TTL 5min으로 자동 복구) |
Redis가 없으면(Lite/단일 인스턴스 모드) 모두 로컬 메모리 구현으로 대체됩니다. 기능은 같고 인스턴스 간 동작만 지원하지 않습니다.
메시지 처리 흐름
IMCallback(webhook)과 지속 연결 콜백은 모두 최종적으로 Service.HandleMessage에 들어가고 큐를 거쳐 QA를 실행합니다.
주요 세부 사항(모두 service.go 참고):
- 중복 제거:
MessageID를 Redisim:dedup:(TTL 5분) 또는 로컬sync.Map(단일 인스턴스 모드)에 기록하며, IM 플랫폼이 다시 보낸 콜백은 바로 건너뜁니다. - 요청 제한:
channelID:userID:chatID[:threadID]별 슬라이딩 윈도우 제한을 적용합니다(기본 60s 내 10개,config.IM으로 재정의 가능). 슬래시 명령은 제한을 우회하므로 메시지가 폭주해도/stop을 사용할 수 있습니다. - QA 큐(
qaqueue.go): 유한 큐 + 고정 worker 풀(기본 workers=5, 큐 상한 50, 사용자별 대기 상한 3, 대기 시간 제한 60s)을 사용합니다. 다중 인스턴스에서는 Redis 집계로 전역 사용자별 상한(im:queue:user:)과 선택적 전역 동시 실행 게이트(im:global:active+ Lua 스크립트,GlobalMaxWorkers설정)를 구현하여 하위 LLM에 배압을 적용합니다. 대기 위치 > 0이면 먼저 "답변을 기다리고 있어요"라고 안내합니다. - 세션 결정:
user모드는 사용자 기준으로 세션을 공유하며 제목은 "김민준 · 그룹 채팅 1a2b3c4d" 형태입니다.thread모드는 최상위 메시지/주제마다 세션 하나를 사용합니다(Slack thread, Feishu 주제 그룹, Telegram Forum Topic, Mattermost root_id). 첫 메시지에서 세션 제목을 비동기 생성합니다(GenerateTitleAsync). - 신원 주입(
withIMIdentity): IM 콜백은 WeKnora 로그인 상태가 아니라 플랫폼 서명을 사용하므로 합성 신원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를 구독합니다. Agent 모드의 "낙관적 답변"은 이후 도구 호출이 시작되면 사고 블록으로 철회됩니다(retractAgentLiveAnswer, Web의 superseded preamble과 동일). 300ms마다 버퍼 전체를 전송합니다(UpdateStreamContent는 교체 의미).holdbackCutoff는 조각 경계에 걸친 불완전한provider://URL, Markdown 이미지, XML 태그를 보류하여 반쪽 콘텐츠가 깜박이지 않도록 합니다. 마지막FinalizeStream은 답변 텍스트만 유지하고(StripThinkBlocks),<kb/>,<web/>인용 태그 및<image>XML을 제거하며provider://저장소 URL을 접근 가능한 링크로 재작성합니다(cleanIMContent/rewriteStorageURLs). - 비스트리밍 경로: 채널이
output_mode=full이거나 어댑터가StreamSender를 지원하지 않거나StartStream이 실패하면runQA로 전체 답변을 모은 뒤SendReply로 한 번에 보냅니다. - 인용 메시지(
Quote, 현재 WeCom 지속 연결 어댑터 등에서 채움): 텍스트 인용은<quoted_message>로 감싸 LLM 컨텍스트에 주입합니다(최대 500 rune, "봇 자신의 답변 인용" 여부 구분). 이미지/파일/동영상 등 비텍스트 메시지를 인용하면 "해당 콘텐츠를 볼 수 없음을 사용자에게 명확히 알리라"는 지침을 주입하여 모델이 읽을 수 없는 내용을 추측하지 않도록 합니다.
아키텍처 개요
Adapter 인터페이스(internal/im/adapter.go)
각 플랫폼 어댑터는 통일된 Adapter 인터페이스를 구현하여 플랫폼 차이를 네 가지 메서드로 모읍니다.
type Adapter interface {
Platform() Platform
// VerifyCallback은 콜백 요청의 서명/Token을 검증합니다.
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(인용 메시지) 등의 필드를 담습니다.
Service 오케스트레이션(internal/im/service.go)
im.Service는 메시지 처리의 중심이며 다음을 담당합니다(소스 주석 참고).
- Adapter에서 통일된
IncomingMessage수신; - 해당 IM 채널의 WeKnora 세션(Session) 결정 또는 생성;
- 슬래시 명령 우선 분배(QA 파이프라인에 진입하지 않음);
- 일반 메시지에서 WeKnora QA 파이프라인(
KnowledgeQA/AgentQA) 호출; - 스트리밍 답변 수집 및 Adapter를 통한 응답 전송.
플랫폼 어댑터는 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())구현 참고
- 핵심 프레임워크 및 오케스트레이션:
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}/ - HTTP API 계층:
internal/handler/im.go - 라우트:
internal/router/router.go의RegisterIMRoutes/RegisterIMChannelRoutes
