src/im/lark/ 是飞书侧接入层,核心是 event-dispatcher.ts(3880 行):长连接建立、事件 ACK/去重、bot 身份探测、@mention 路由、话题/普通群 scope 判定,最终把消息分发为「新开会话」或「回注既有会话」。会话执行与 worker 生命周期见 daemon-worker。
长连接入口
startLarkEventDispatcher(larkAppId, appSecret, handlers, brand)(event-dispatcher.ts:2634)为每个 bot 创建一个 Lark.WSClient(SDK 长连接,无需公网 webhook):
- 域名:
sdkDomain(brand)(lark-hosts.ts)——国际版租户必须连 larksuite.com,否则收不到任何事件 - 代理:
createLarkWsAgent()把 wss/ws 映射回 https/http 走 ProxyAgent,与 SDK 前置 HTTPS 引导请求对齐 - 自愈双防线(3806-3878):①
pingTimeout: 30s,发 ping 后 30s 无入站帧即掐断 socket 触发 SDK 重连(专治 TCP 半开静默卡死);② 每分钟探测连接状态,SDK 重连耗尽停在failed后重新start()发起全新握手,无需手动 restart daemon
事件注册在 new Lark.EventDispatcher({}).register({...})(3659):im.message.receive_v1(消息)、card.action.trigger(卡片回调,见 card-streaming)、im.chat.member.bot.added/deleted_v1(进群自动开工/清缓存)、群成员变更(清群统计缓存)、drive.notice.comment_add_v1(文档评论触发)、vc.bot.meeting_*(会议 agent)。
ACK 安全与去重
Lark WS SDK 只在 handler promise resolve 后才发唯一 ACK 帧;飞书有 3s 预算,超时按 15s/5min/1h/6h 分级重投。因此 scheduleAckSafeEvent(617)同步 claim + setImmediate 把重活挪到后台,handler 立即返回保 ACK 及时。claim 与 setImmediate 之间禁止 await(FIFO 保序不变量)。
去重两层:
- 消息路径:持久化
claimMessageOnce(seen-message-store,按 message_id),daemon 重启或 6h 重投不能重放已处理消息 - 其他事件:内存
claimEventOnce,2h TTL(覆盖 15s/5min/1h 重投档),BoundedMap + 定时/定容 prune
入口屏障:rawMessageIngressAnchor(661)按 chat 级(非 thread 级)串行化入站——话题种子初始没有 thread_id,后续首条回复才归一化到种子 message_id,按 thread 分 lane 无法证明到达顺序。实际执行栅栏是 serializeByAnchor(utils/anchor-serializer.ts):同 anchor 串行、跨 anchor 并发。
消息 → bot → 会话 分发链路
processMessageEvent(约 2740)是单条消息的判定主流程:
ensureBotOpenId:闭合启动竞态——open_id 探测是 fire-and-forget,窗口内到达的 @ 会因 botOpenId=undefined 被静默丢弃且事件已 ACK(永久丢失),故路由前 await 去重探针- 身份学习:从 mentions[] 学其他 bot 的 open_id(open_id 按 app 隔离,cross-ref 存盘)、学自己的 union_id(无 receive-all scope 时收不到自己消息的回声,@mention 是唯一可靠来源)
- bot 发送方分支:自己的回声只处理
/close;团队拉群里登记队友 union_id;机器人大厅只登记身份不做任务路由;message-listener 规则匹配;外部 bot 仅 @ 本 bot 才路由(例外:autoStartOnNewTopic免 @ 自动开工,含其他 bot 开的新话题),未授权陌生 bot 改发 grant 授权卡。同部署兄弟 bot 冷启动自愈:live/members/botsroster 验证后回填 cross-ref,fail-closed - 人类分支:oncall 自动绑定(必须在权限判定前)→
canTalk授权 → 斜杠元命令在路由前拦截(/grant/invite/reply-mode/substitute/introduce,mention-targets.ts 解析「命令词之后的 @ 才是目标」)→ 路由判定 → dispatch
@mention 判定
isBotMentioned(1109)按 open_id/union_id/app_id 多形态匹配 mentions[];message-parser.ts 提供 mentionOpenId/mentionAppId/extractMentionIdentities/extractPostAtParticipants(text 与 post 富文本两种形态,post 的 mentions 常为空需走 inline at 节点)。
scope/anchor 路由判定
decideRoutingWithSource(2302)产出 {scope: 'thread'|'chat', anchor}(RoutingSource:real-thread/p2p/topic-chat/regular-group-thread/regular-group-chat):
| 场景 | 判定 | 结果 |
|---|---|---|
私聊(默认 p2pMode: chat) | 整段 DM 折叠 | chat-scope,anchor=chatId |
| 带 root_id+thread_id | 既有话题内回复 | thread-scope,anchor=root_id |
| 话题群顶层消息 | getChatMode 缓存(5min TTL)= ‘topic’ | 新话题种子,anchor=本消息 message_id |
| 普通群顶层 | /reply-mode:new-topic → 每 @ 起新话题;chat/shared/chat-topic → 平铺 | thread-scope(anchor=msg) 或 chat-scope(anchor=chatId) |
| 普通群原生话题种子(omt_*) | 仅 chat-topic 模式单独隔离 | thread-scope |
/t/topic强制话题:maybeApplyForceTopicOverride(2148)把普通群 chat-scope 改写为 thread-scope、anchor=本消息,bot 首回复种子化一个新 Lark 话题- 共享话题折叠:
maybeFoldMentionedRegularGroupThreadToChat/maybeApplySharedTopicSeed把普通群 thread 折回 chat-scope 共享会话(replyRootId记录可见回复落点),与授权门共用同一个evaluateBotTalk结论,避免「能路由不能 fold」的分裂 - 群模式转换:话题群翻回普通群时 forceRefresh 复核缓存(防 5min 陈旧缓存错建 thread-scope 会话);
onChatModeConvertedhook 让 daemon 驱逐失效的 chat-scope 会话
会话分发
dispatchHumanMessageViaHandlers(1892):handlers.isSessionOwner(anchor) 为真 → handleThreadReply(daemon.ts:17781,注入既有会话),否则 → handleNewTopic(daemon.ts:16495,按 anchor 开新会话)。anchor 即会话锚:thread-scope 为话题根 message_id,chat-scope 为 chatId;beforeSessionTurn hook 可替换为 daemon 持有的合成 anchor。会话锚定与接管语义见 session-model。
防抖:转发话题的宽限窗口
转发进群的新话题种子常紧跟一条澄清回复。ForwardFollowupBuffer(forward-followup-buffer.ts)把种子延迟 forwardFollowupWaitMs(config.ts:147 默认 1.5s,上限 10s)再 dispatch;窗口内出现 root 指向该种子的同发送者回复即配对合并(种子内容作上下文)。种子持久化到 forward-followup-store,daemon 重启后恢复剩余等待;dispatch 严格按 anchor 串行(capMs=0),跟随消息不会与种子的会话创建竞速。多话题编排见 multi-topic,多 bot 路由见 multi-bot。
授权门简述
canTalk/evaluateTalk/evaluateBotTalk(1397/1414/1510)是单一 talk 谓词,放行腿包括:open 模式、allowlist、grant 授权、oncall 绑定、已知 peer bot、团队 bot(union_id 锁 bot 发送方)、平台团队成员。人/bot 判定同源(test/bot-talk-parity.test.ts 守护),bot 专属逻辑只剩 cross-ref 冷启动自愈。详细角色体系见 roles。