Skip to content

feat(im): 实时事件 <sender> 名字兜底走 message.get(with_sender_name) - #582

Merged
deepcoldy merged 2 commits into
masterfrom
feat/resolve-sender-name-via-message-get
Jul 26, 2026
Merged

feat(im): 实时事件 <sender> 名字兜底走 message.get(with_sender_name)#582
deepcoldy merged 2 commits into
masterfrom
feat/resolve-sender-name-via-message-get

Conversation

@DeepColds

Copy link
Copy Markdown
Collaborator

背景 / 为什么 #480 对这个问题没用

用户(杨志发)反馈:升级后 codex 收到的 prompt 里 `<sender type="user" open_id="..." />` 少了 `name` 属性。

排查发现 botmux 有两条独立的「发送者名字」链路:

  1. 实时事件路径 receive_v1 —— 就是注入进 CLI prompt 的 <sender>。名字只靠 identity-cachecontact.v3.user.get 现补,contact 缺权限 / 可见范围不含发送人 / 首次查询超时 / 发送方是 bot 时就降级成没有 name
  2. API 读消息路径(history / quoted / 合并转发 / dashboard 历史)—— PR feat(im): 消息读取带上服务端发送者名称(with_sender_name=true) #480 加了 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 注入点,且是纯增量兜底)。

测试验证

  • 新增 `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 无关)。
  • ⚠️ live 实测待补:此 checkout 的 desktop 依赖缺失导致完整 build 失败、dist 未刷新,未在飞书实测。建议在干净构建环境 `switch:here && daemon:restart` 后,让 contact 缺权限的 bot 发一条消息验证 prompt 里 `` 带上了 name。

🤖 Generated with Claude Code

@DeepColds
DeepColds requested a review from deepcoldy as a code owner July 24, 2026 05:30
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>
deepcoldy added a commit that referenced this pull request Jul 26, 2026
落实 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>
@deepcoldy
deepcoldy force-pushed the feat/resolve-sender-name-via-message-get branch from ee19b3d to c0f1836 Compare July 26, 2026 01:04
@deepcoldy

Copy link
Copy Markdown
Owner

复核后跟进:rebase 解冲突 + 落实两条 review 建议

承接群里 codex + relay 的联合 review(结论一致:代码可合、无阻塞 correctness 问题,留了两条非阻塞建议)。本次做了三件事:

1. rebase 到最新 master(解决 CONFLICTING)

review 之后 master 前进了 80 个 commit,PR 变成 CONFLICTING。冲突只在 src/daemon.ts 的一处:master 在「新话题」注入点前新增了 resolveGroupChatNameForNativeTitle(...) 调用,与本 PR 给同一行 resolveSender{ messageId } 参数相邻。解冲突时两者都保留。其余 3 个注入点是纯行漂移,rebase 自动合入。identity-cache.ts 在 master 无改动,核心逻辑干净落地。现已 MERGEABLE

2. 单预算封顶尾延迟(review 建议 ①)

原实现里 contact API 和 message.get 兜底各套一份完整 RESOLVE_BUDGET_MS(800ms)——user 缓存 miss 且 contact 超时时会叠加第二个完整 800ms,最坏 ~1.6s 才注入 <sender>

改为在 resolveSender 里算一个统一 deadline = now + RESOLVE_BUDGET_MS:contact 步先跑,message.get 兜底只拿剩余时间remaining <= 0 直接跳过)。整条链路封顶 800ms。resolveName / resolveNameViaMessage 新增可选 budgetMs 参数(默认 RESOLVE_BUDGET_MS),独立调用语义不变。

3. 测试隔离到唯一 tmpdir(review 建议 ②)

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 回校 / 类型校验 / app-id↔open-id 映射,非本 PR 范围。

验证

  • pnpm build 全绿(域名审计 / tsc / dashboard bundle / dist audit 均过)——本 checkout desktop 依赖完整,补上了原作者环境缺失导致未跑成的完整 build
  • 本文件 8 例连跑两遍全绿(无跨次污染)
  • 回归 message-parser / merge-forward / dashboard-history-senders / daemon-rename-route105 例全绿

仍待人工把关

  • live 注入实测:在获准的干净 daemon 上让 contact 缺权限的 user/bot 发消息,确认 prompt 里 <sender> 真带上 nameswitch:here + daemon:restart 会切换所有 bot、中断会话,未获授权不擅动。
  • 合码需申晗拍板。

🤖 过程摘要 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>
@deepcoldy
deepcoldy force-pushed the feat/resolve-sender-name-via-message-get branch from c0f1836 to 5dbd632 Compare July 26, 2026 01:15
@deepcoldy

Copy link
Copy Markdown
Owner

二审跟进:撤回单预算改动(阻塞项),保留并修正测试改进

感谢 @codex 二次 review 抓到的阻塞项——确认成立,已按 Option 1 修复。

阻塞项复述

上一版加的「统一 deadline 单预算」有语义回退:deadline = now + 800ms 后仍把完整 800ms 交给 contact 步,一旦 contact 真超时则 remaining <= 0,message.get 兜底必然被跳过。而「contact 首查超时」正是本 PR 问题陈述里明确列为实时 <sender> 缺 name 的根因之一——为压尾延迟牺牲功能覆盖不划算(该尾延迟本就是非阻塞建议)。上一版新增的 fake-timer 用例还把这个错误语义锁成了预期断言。

采纳 Option 1(最小风险):撤回单预算

生产代码 src/im/lark/identity-cache.ts / src/daemon.ts 回到 PR 原始逐步预算,与已 review 的 feat 提交 (58db74f9) 逐字节一致(git diff 58db74f9 -- src/ 为空)。contact 与 message.get 各自独享 RESOLVE_BUDGET_MS,最坏约 1.6s——这是可接受的非阻塞取舍。

未选 Option 2(拆分预算,contact 400–500ms / message.get 300–400ms):那样会收窄 contact 窗口,为追一个没人阻塞的尾延迟去牺牲更高质量的主名字源(真实用户名),得不偿失。功能覆盖 > 尾延迟。

保留 + 修正测试(codex 已确认隔离可靠)

  • 隔离保留:每例 mkdtemp 唯一目录 + afterEach flushIdentityCacheSync + rmSync,config 走 vi.hoisted 重指向。codex 连跑两遍确认可靠、/tmp 无残留。
  • 反转错误断言:上一版「hung contact → expect(message.get).not.toHaveBeenCalled()」改成正确的 「contact 挂起超时后 message.get 仍被调用并成功取名」(fake timer 推进 800ms 触发 contact 步 withTimeout)。这条恰好把 PR 的核心承诺锁进测试——原 PR 反而没覆盖这个场景。
  • 已实测该用例的判别力:对错误的单预算实现失败、对 PR 原始逐步预算通过

codex 提的 module-level stores 跨用例共享(靠每例 openId 不同规避)与 Date.now() 时钟回拨,均为非阻塞,未在本次处理。

验证

  • src/ 生产代码与 PR 原始 feat 提交逐字节一致
  • pnpm build 全绿(域名审计 / tsc / dashboard bundle / dist audit)
  • 本文件 8 例连跑两遍全绿、/tmp 无残留
  • 回归 message-parser / merge-forward / dashboard-history-senders / daemon-rename-route105 例全绿
  • rebase 后 master 又前进,git merge-tree 无冲突,GitHub MERGEABLE

净效果

相比已 review 的 PR 原始状态,本轮只额外改了测试文件(隔离 + 补一条 contact-timeout 兜底用例),生产代码零净变更。原 review 的「代码可合」结论不受影响。仍等申晗拍板 + live 注入实测。

🤖 过程摘要 by Claude Code

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@deepcoldy
deepcoldy merged commit c5748c7 into master Jul 26, 2026
1 check passed
@deepcoldy

Copy link
Copy Markdown
Owner

✅ 已合并

经申晗授权合并至 master。

  • 合并方式:merge commit c5748c7d(保留 2 个 commit:原始 feat + 测试隔离/回归用例)
  • 合并前状态:CI build ✅ / MERGEABLE
  • mergeStateStatus 曾为 BLOCKED,原因是分支保护要求一次 GitHub 正式 review approval(REVIEW_REQUIRED);codex 三审均在群里给出 approve 但按约定未提交 GitHub formal review。经申晗明确授权,使用 admin 合并(未自我伪造 review approval)。

遗留(非阻塞,均已在过程摘要记录):

  • live <sender name> 注入实测仍待在获准的干净 daemon 上补验(contact 缺权限的 user/bot 发消息 → prompt <sender> 带 name)。
  • 已知非阻塞项:最坏 ~1.6s 尾延迟(两 API 都慢时)、测试态 module-level stores 共享。

🤖 by Claude Code

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.

2 participants