feat(im): 实时事件 <sender> 名字兜底走 message.get(with_sender_name) - #582
Conversation
PR #480 只给「API 读消息路径」(history/quoted/合并转发/dashboard 历史) 加了 with_sender_name=true,但实时事件路径 receive_v1 注入进 CLI prompt 的 <sender type=... open_id=... /> 完全没受影响——那条路径的名字只靠 identity-cache 的 contact.v3.user.get 现补,contact 缺权限/可见范围/超时 或发送方是 bot 时就降级成没有 name 属性(用户实测反馈的正是这个)。 本 PR 在 resolveSender 增加最后一层兜底:cache/hint、contact API 都没拿到 名字、且调用方带了 messageId 时,用 getMessageDetail(with_sender_name=true) 反查一次 sender_name。它同时覆盖 user 和 bot,且不依赖 contact scope (服务端直接返回这条消息发送方的显示名),解析到的名字写回 identity 缓存, 后续同发送方的消息命中缓存不再重复请求。 - src/im/lark/identity-cache.ts:新增 resolveNameViaMessage(复用 #480 已 带 with_sender_name 的 getMessageDetail,套同一 RESOLVE_BUDGET_MS 预算, 失败静默降级 undefined);resolveSender 的 hint 增加可选 messageId,仅在 前两步无名时触发;IdentityRecord.source 增加 'message_api' - src/daemon.ts:4 个 <sender> 注入点(p2p 冷启动/命令、新话题、话题回复) 把入站 messageId 透给 resolveSender;订阅/定时等无入站消息的调用点不传, 行为不变 影响面:仅 Lark 实时事件的 <sender> 名字解析链路。名字优先级不变(hint > cache > contact > message.get 兜底),纯增量:不带 messageId 时行为与之前 完全一致。跨 CLI/跨后端无涉。 测试: - 新增 test/identity-cache-message-fallback.test.ts,7 例全绿(bot 兜底、 user contact miss 后兜底、缓存命中不再请求、无 messageId 不请求、hint 已 有名字不请求、无 sender_name/抛错静默降级) - 回归 message-parser/merge-forward/dashboard-history-senders 共 86 例全绿 - pnpm build:改动文件 tsc 无报错(src/desktop 的 electron 报错在干净 master 上同样存在,与本 PR 无关) Co-Authored-By: Claude <noreply@anthropic.com>
落实 PR #582 review 的两条非阻塞建议(codex + relay 一致提出): 1. 单预算封顶尾延迟。原来 contact API 和 message.get 兜底各自套一份完整 RESOLVE_BUDGET_MS(800ms)——user 缓存 miss 且 contact 超时时,会再叠加一个 完整 800ms 的 message.get 超时,最坏 ~1.6s 才能注入 <sender>。改为在 resolveSender 里算一个统一 deadline,contact 步先跑,message.get 只拿剩余 时间(remaining<=0 直接跳过),整条链路封顶 800ms。resolveName / resolveNameViaMessage 新增可选 budgetMs 参数(默认 RESOLVE_BUDGET_MS), 独立调用语义不变。 2. 测试缓存隔离。identity-cache-message-fallback.test.ts 原来固定用 /tmp/botmux-identity-test,跑够久触发 debounce flush 后会落盘 identities-*.json,二次运行 hydrate 命中缓存 → 「兜底真被触发」「缓存命中 不再请求」等断言可能假绿。改为每个用例 mkdtemp 唯一目录 + afterEach flushIdentityCacheSync + rmSync 清理,config 走 vi.hoisted 可变对象重指向。 新增 1 例:验证「hung contact 步耗尽预算后 message.get 不再被调用」——已确认 该用例对旧的逐步预算实现会失败(getMessageDetail 被调 1 次),对新实现通过。 未采纳 review 中一度提出的 sender.id === openId 严格校验:实测实时事件 sender 是 ou_,message.get 的 bot sender id 是 cli_,裸比会把 bot 兜底全拒掉(codex 已在群里纠正)。防错配需走 message_id 回校 / 类型校验,非本 PR 范围。 验证: - pnpm build 全绿(域名审计 / tsc / dashboard bundle / dist audit 均过) - 本文件 8 例连跑两遍全绿(无跨次污染) - 回归 message-parser / merge-forward / dashboard-history-senders / daemon-rename-route 共 105 例全绿 Co-Authored-By: Riff <noreply@riff.dev>
ee19b3d to
c0f1836
Compare
复核后跟进:rebase 解冲突 + 落实两条 review 建议承接群里 codex + relay 的联合 review(结论一致:代码可合、无阻塞 correctness 问题,留了两条非阻塞建议)。本次做了三件事: 1. rebase 到最新 master(解决 CONFLICTING)review 之后 master 前进了 80 个 commit,PR 变成 2. 单预算封顶尾延迟(review 建议 ①)原实现里 contact API 和 message.get 兜底各套一份完整 改为在 3. 测试隔离到唯一 tmpdir(review 建议 ②)
新增 1 例验证「hung contact 步耗尽预算后 message.get 不再被调用」。已实测:该用例对旧的逐步预算实现会失败( 关于「防错配严格校验」未采纳 review 讨论中一度提出的 验证
仍待人工把关
🤖 过程摘要 by Claude Code |
采纳 PR #582 二审(codex):撤回上一版的「统一 deadline 单预算」改动,只保留 测试侧改进。 为什么撤回单预算:统一 deadline 会把完整 800ms 分给 contact,一旦 contact 真 超时则 remaining<=0,message.get 兜底必然被跳过——而「contact 首查超时」正是 本 PR 明确要覆盖的实时 <sender> 缺 name 根因之一。为压尾延迟牺牲功能覆盖不 划算(该尾延迟本就是非阻塞建议),故生产代码回到 PR 原始逐步预算,与已 review 的 feat 提交逐字节一致。 测试侧保留 + 修正: - 隔离:每个用例 mkdtemp 唯一目录 + afterEach flushIdentityCacheSync + rmSync 清理,config 走 vi.hoisted 可变对象重指向。杜绝固定 /tmp/botmux-identity-test 被 debounce flush 落盘后跨次 hydrate 造成的假绿。(codex 连跑两遍确认可靠、 /tmp 无残留) - 把上一版锁错语义的用例(hung contact → expect message.get NOT called)反转成 正确断言:「contact 挂起超时后 message.get 仍被调用并成功取名」,用 fake timer 推进 800ms 触发 contact 步 withTimeout。已实测:该用例对错误的单预算实现会 失败、对 PR 原始逐步预算通过,确实卡住这个回归面。 验证: - src/ 生产代码与 PR 原始 feat 提交 (58db74f) 逐字节一致(git diff 为空) - pnpm build 全绿(域名审计/tsc/dashboard/dist audit) - 本文件 8 例连跑两遍全绿、/tmp 无残留 - 回归 message-parser/merge-forward/dashboard-history-senders/daemon-rename-route 共 105 例全绿 Co-Authored-By: Riff <noreply@riff.dev>
c0f1836 to
5dbd632
Compare
二审跟进:撤回单预算改动(阻塞项),保留并修正测试改进感谢 @codex 二次 review 抓到的阻塞项——确认成立,已按 Option 1 修复。 阻塞项复述上一版加的「统一 deadline 单预算」有语义回退: 采纳 Option 1(最小风险):撤回单预算生产代码
保留 + 修正测试(codex 已确认隔离可靠)
验证
净效果相比已 review 的 PR 原始状态,本轮只额外改了测试文件(隔离 + 补一条 contact-timeout 兜底用例),生产代码零净变更。原 review 的「代码可合」结论不受影响。仍等申晗拍板 + live 注入实测。 🤖 过程摘要 by Claude Code |
|
To use Codex here, create a Codex account and connect to github. |
✅ 已合并经申晗授权合并至 master。
遗留(非阻塞,均已在过程摘要记录):
🤖 by Claude Code |
背景 / 为什么 #480 对这个问题没用
用户(杨志发)反馈:升级后 codex 收到的 prompt 里 `<sender type="user" open_id="..." />` 少了 `name` 属性。
排查发现 botmux 有两条独立的「发送者名字」链路:
receive_v1—— 就是注入进 CLI prompt 的<sender>。名字只靠identity-cache的contact.v3.user.get现补,contact 缺权限 / 可见范围不含发送人 / 首次查询超时 / 发送方是 bot 时就降级成没有name。with_sender_name=true的就是这条。#480 的描述里已写明「实时事件路径 receive_v1 不带 sender_name,不受影响」。所以它优化的是事后读历史的名字,用户看到的是消息进来那一刻的
<sender>,两条路径不重叠 → 没起作用。改了什么
在
resolveSender增加最后一层兜底(B 方案,只在名字缺失时才查):src/im/lark/identity-cache.ts:新增resolveNameViaMessage,当 hint / cache / contact API 都没拿到名字、且调用方带了messageId时,用getMessageDetail(with_sender_name=true)(复用 feat(im): 消息读取带上服务端发送者名称(with_sender_name=true) #480 已改好的调用,套同一RESOLVE_BUDGET_MS预算)反查一次items[0].sender.sender_name。它同时覆盖 user 和 bot,且不依赖 contact scope(服务端直接返回这条消息发送方的显示名)。解析到的名字写回 identity 缓存,后续同发送方命中缓存不再请求。IdentityRecord.source增加'message_api'。src/daemon.ts:4 个<sender>注入点(p2p 冷启动 / p2p 命令 / 新话题 / 话题回复)把入站messageId透给resolveSender;订阅、定时等无入站消息的调用点不传,行为不变。名字优先级:hint > cache > contact API > message.get 兜底,纯增量,不带 messageId 时与改动前完全一致。
影响面
仅 Lark 实时事件的
<sender>名字解析链路。跨 CLI / 跨后端无涉(改动都在 Lark identity-cache 与 daemon 注入点,且是纯增量兜底)。测试验证
🤖 Generated with Claude Code