Skip to content

feat(identity): <sender> 名字补全加群成员/主动@兜底,修 41050 每条白打 contact API#87

Open
deepcoldy wants to merge 7 commits into
masterfrom
feat/sender-name-resolution
Open

feat(identity): <sender> 名字补全加群成员/主动@兜底,修 41050 每条白打 contact API#87
deepcoldy wants to merge 7 commits into
masterfrom
feat/sender-name-resolution

Conversation

@deepcoldy
Copy link
Copy Markdown
Owner

背景

被别人 @ 时 <sender> 标签显示不出对方姓名(自己/自家 bot 正常)。根因:contact.v3.user.get 受飞书「通讯录可见范围」限制,范围外用户返回 41050 no user authority。原代码只特判 99991672(app 级缺 scope),41050 落到 logger.debug 静默吞掉且不缓存 → 每条消息白打一次 800ms 预算的 contact API

实测佐证:申晗(app 范围内)code=0;其余群成员全 code=41050

方案:4 层解析,拿到即停

  1. 身份缓存(mention 学过的,免费)
  2. contact API —— 41050 按 open_id 负缓存,不再每条重打(区别 99991672 的 app 级熔断)
  3. 群成员 API —— 新增 listChatMembersWithNames,默认翻 1 页(open_id, name),顺便整群灌缓存;不需通讯录可见范围权限,失败降级空数组
  4. 主动 @ 探针 —— 在消息所在群/话题 @ 对方发 /introduce,从自己发出消息的 getMessageDetail 回执读飞书回填的 name,随即撤回

防护与防循环

  • sender_type==='user' 才探针,bot 走 bot-openids 跨引用名字、绝不被探
  • 探针文本用 /introduce:万一误判 @ 到 bot,对方走现成 introduce 路由层 short-circuit 不被唤起干活;防循环天然成立(探针 sender 是 bot,对方 user-only gate 直接挡掉,不反向探测)
  • 话题群在 thread 内发、普通群在群里发,绝不私聊(bot 无法私聊陌生人)

改动

文件 内容
identity-cache.ts 41050 per-user 负缓存;source 联合加 chat_member/introduce_probe
client.ts 新增 listChatMembersWithNames(不动原 listChatMemberOpenIds
sender-name-fallback.ts【新】 编排第 3+4 层 + enrichSenderName gate
daemon.ts 两处 resolveSender 调用点接线(阻塞补名,传 chatId/scope/anchor)

验证

  • pnpm build / tsc --noEmit / 全量 vitest 套件 全 exit 0
  • 新增 16 个单测:41050 负缓存(per-user 非 per-app)、群成员翻页/降级、4 层降级编排、enrichSenderName gate、探针撤回兜底
  • 关键 Lark 行为已真机实测(群成员 API code=0 带 name、send→GET 拿回填 name、撤回 644ms)
  • ⚠️ 完整探针链路尚未线上实测(单测为 mock),需在真实群用一个范围外用户触发验证

已知小瑕疵

误判 @ 到 bot 时,对方现成 /introduce 处理会回一条 ack(「已认识 N 个伙伴」/「没有可登记的机器人」)刷一下屏;因 user-only gate + ④ 本身罕见,影响很小。

deepcoldy added 7 commits June 1, 2026 20:17
dogfood 产出(coder 写 + Codex review)的妙笔章节已 publish 到官方站
vkWHeJn1Fn2(PUT 原地更新,URL 不变),同步把 /tmp/botmux-docs/index.html
回写进仓库 docs-site/index.html 保持源一致(功能详解组 multi-bot 之后新增
multi-topic 一节,28 节 NAV 一一对应)。
按申晗反馈:去掉用户无需感知的内部机制(botmux dispatch/report 命令与 flag、
「子话题别 @ 主bot」告警、授权边界细节),改成「怎么用」视角(说需求→确认分配→
看进度/收结果)+ 前置条件 + 一张群内多 bot 协作效果截图(TOS)。已 publish 到
官方站 vkWHeJn1Fn2、live 验证。
申晗指正:OnCall 不是前置条件(它只是省 --repo 的便利)。子话题里的 bot 由主 bot
派活时用 /repo 拉起并配好工作目录,用户无需预先开 OnCall。前置条件只剩「把要协作的
bot 拉进群、可被 @」。
申晗指出真·跨部署(不同人各自机器)的回报没处理好:dispatch 把主编排坐标写在
编排者机器的 orchestrate-dispatch.json,对方 bot 在自己机器读不到→`botmux report`
报「缺少主编排坐标」。同机能跑通只因共用一个 data 目录。

修:新增纯函数 resolveReportTarget——有 registry entry(同机)就用它精确路由(含
thread-scope orchRoot);没有(跨机)就从子 bot 自己的会话推导:回报发到「子话题所在
chat」(= 编排者的 chat) 顶层、@ 编排者(creatorOpenId)。cmdReport 不再硬要求 registry。
工作目录/授权/跨租户按申晗意见先不碰。含 3 条回归用例。
本地 dev 反复重启 daemon 时,tmux 后端会在 restore 阶段 eager re-fork 所有
未结束会话,触发 worker_ready 重渲染(PATCH 失败时还会重发)各自的 streaming
卡片,导致飞书话题被反复刷屏。

改动:抽出纯函数 shouldAutoForkOnRestore(backendType, quietRestart),在 quiet
模式下跳过 tmux 的 eager re-fork——会话照常注册进内存,下一条真实消息再懒
恢复(forkWorker resume → selectSessionBackend 命中存活 tmux 即 isReattach 重连,
不新起 CLI、不丢活儿),与 pty 后端既有行为一致。tmux 会话本身不杀。

开关:BOTMUX_QUIET_RESTART=1(dev 机 ~/.zshrc export 一次)默认静默;生产不设
则 quietRestart=false,gate 退化为原 backendType==='tmux' 判断,行为零变化。
cli.ts 把该 env 显式透传进 pm2 ecosystem(默认 '0'),覆盖 god 进程残留旧值。

Reviewed-by: Codex
被别人 @ 时 <sender> 显示不出对方姓名:根因是 contact.v3.user.get 受飞书
「通讯录可见范围」限制,范围外用户返回 41050 no user authority。原代码只
特判 99991672(app 级缺 scope),41050 落到 debug 静默吞掉且不缓存,导致每条
消息都白打一次 800ms 预算的 contact API。

改成 4 层解析,拿到即停:
1. 身份缓存(mention 学过的,免费)
2. contact API —— 41050 按 open_id 负缓存,不再每条重打(区别 99991672 的 app 级熔断)
3. 群成员 API —— 新增 listChatMembersWithNames,默认翻 1 页拿 (open_id,name),
   顺便整群灌缓存;不需通讯录可见范围权限,失败降级空数组
4. 主动 @ 探针 —— 在消息所在群/话题 @ 对方发 /introduce,从自己发出消息的
   getMessageDetail 回执读飞书回填的 name,随即撤回

防护与防循环:
- 仅 sender_type==='user' 才探针,bot 走 bot-openids 跨引用名字、绝不被探
- 探针文本用 /introduce:万一误判 @ 到 bot,对方走现成 introduce 路由层
  short-circuit 不被唤起干活;防循环天然成立(探针 sender 是 bot,对方
  user-only gate 直接挡掉,不反向探测)
- 话题群在 thread 内发、普通群在群里发,绝不私聊(bot 无法私聊陌生人)

单测覆盖:41050 负缓存(per-user 非 per-app)、群成员翻页/降级、4 层降级
编排、enrichSenderName gate、探针撤回兜底。pnpm build / tsc / 全量套件全 exit 0。
回应 Codex 对 PR #87 的两个 P1 + 一个建议:

P1-1 防误触发漏洞(event-dispatcher):bot-originated 消息先进 isBotSenderType
分支并 route 到 handleThreadReply,而 /introduce 的 short-circuit 在后面的
user 分支才跑——所以探针误判 @ 到 bot 时,对方可能把 /introduce 当 prompt
喂进 CLI。修法:在 bot 分支里、foreign-bot 路由之前,先静默 consume
bot-originated /introduce(tryHandleIntroduceCommand 加 silent 选项,不 ack、
不 route)。防*循环*本身仍靠双侧 user-only gate;这里修的是「别误唤起对方 CLI」。

P1-2 探针 timeout + 失败冷却(sender-name-fallback):
- getMessageDetail 加读取 budget(默认 1.5s,可注入),超时也照样撤回——避免
  SDK 无超时时 get 挂住既留下可见探针又阻塞 inbound;
- 撤回改 fire-and-forget,不阻塞当前 inbound;
- 群成员 + 探针都没拿到名字时,按 open_id 加短 TTL(10min)负缓存,避免同一
  sender 每条消息都 send→get→delete 一轮刷屏。

建议项:41050 负缓存从进程内 Set 升级为 Map<key,expiresAt>,TTL 24h——可见
范围变化后隔天能重试一次 contact,而非同进程永不重试。

测试:event-dispatcher 75(+1 静默 consume)、sender-name-fallback 14
(+timeout/冷却)、identity-cache 3(+TTL 到期重试);tsc 全量 exit 0。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant