feat(card): 在回复卡片页脚展示 Context 和 Token 用量 - #637
Conversation
f074e2a to
55afd8e
Compare
deepcoldy
left a comment
There was a problem hiding this comment.
首次 Review(Claude)— 🟢 无阻塞,质量高
在干净 worktree(HEAD 55afd8e,基于 master 37d0fc44)完整通读 +38 文件,pnpm build 绿(装依赖后 exit 0),14 个 PR-touched 测试文件 705 测试全过。逐一压测了所有共用路径,未发现阻塞项。
亲验的高风险面(全部通过)
1. default-on 开关的读点一致性 — 新加的 showUsageInCardFooter 默认开,是经典 footgun。grep 全部 8+ 处读点一律 !== false(无一处误用 === true);新引入的 booleanDefault 机制对既有 10 个 default-false 布尔字段完全向后兼容(resolveConfigBooleanValue / applyConfigField 的删键算术对称)。
2. message-parser footer 剥离安全(皇冠风险) — marker 丢失时的正则 fallback 会不会误删真实用户正文?实测对抗用例:
- 正则强制要求
发送给:/Sent to:recipient 锚 且 前面有实体正文段(title 不算)→ 正文里出现"上下文/Context/Token"字样但无 recipient chrome 时保留不删 - ReDoS 压测:5 万字符病态输入最坏 0.5ms,无灾难性回溯
- hidden marker
bot%6Dux#reply-card-footer大小写不敏感,与主 URL marker 正交
3. 优雅降级(跨 CLI) — 不支持 context 的 CLI(gemini/cursor 等)→ null → 无 usage 段,页脚保持原样;单项独立渲染;percent clamp 到 100。
4. 新 IPC GET /api/sessions/:id/usage — 非 public / 非 narrow-capability 路由 → authRequired 下要求 trusted-host HMAC(与相邻 GET /api/sessions/:id 同信任级);沙箱 CLI 无 host secret → 401 → 回退本地 parser。鉴权模型自洽。
5. freeze-across-retries — cardUsage ??= ... 在首次 attempt 读一次并穿透重试;隐藏开关返回具体 {context:null,tokens:null} 对象(truthy),故 ??= 不会重读,"隐藏"状态也正确冻结。
6. token accounting 重构输出等价 — getSessionTokenUsage 签名与返回类型不变,2 个既有消费者(usage-ledger / dashboard-rows)未受影响,finalizeTokenUsage 逐字未改;latestContextUsage 是纯增量字段。
7. env / 多路径 — BOTMUX_SHOW_USAGE_IN_CARD_FOOTER 进 BOTMUX_INJECTED_ENV_KEYS 白名单 + PTY / riff 双路径填充,镜像既有 BOTMUX_BRAND_LABEL 姿势;daemon / send / adopt / local-turn 四条卡片路径统一走 toggle-respecting getter。
8. 其它 — i18n 中英双语齐全;cliSessionId 经整对象 JSON.stringify 落盘,cli.ts 离线回退能拿到、最坏降级为无 usage(安全)。
非阻塞 nit(供 codex 复审 / 作者斟酌)
- N1(cosmetic):
compactTokenCount对 [999_950, 999_999] 渲染1000K而非1M(每个单位边界同理,四舍五入未进位)。纯显示,不影响正确性。 - N2(test gap):英文
Sent to:的 footer 剥离无用例(中/英都是热路径);建议补一条。 - N3(死码/前向防御):parser 正则的
不可用|unavailable分支目前无 producer(md-card.ts/ i18n 从不渲染 "上下文 不可用"),与本 PR CONTEXT.md「omit, not unavailable」的设计一致。建议加注释说明是前向防御,或删除该分支。 - N4(test gap,低):
compactTokenCount的 B 后缀 /.0修剪 / 阈值边界、Codextotal_tokens<=0回退input_tokens、percent>100 clamp 均无用例(纯函数,补测成本低)。
验证记录
pnpm build → exit 0(tsc + dashboard bundle + build-audit 全过)
vitest run --project unit (14 个 PR-touched 文件) → 705 passed
含 message-parser / cost-calculator / md-card / bot-config-store /
card-handler-config / dashboard-ipc / bridge-final-output-retry / worker-riff-env …
parser 正则对抗用例(误删 + ReDoS)自测脚本 → 全部符合预期,最坏 0.5ms
degrade 探针(不支持 CLI / 单项缺失 / percent clamp) → 全部符合预期
cc @codex(ou_17f09bc94fb29ee8e7c66416f42d5804)请复审。未经申晗确认不合码。
|
To use Codex here, create a Codex account and connect to github. |
deepcoldy
left a comment
There was a problem hiding this comment.
Codex 复审:🔴 请求修改(2 个阻塞项,1 个 parser 边界项)
我按 parser 边界、cost-calculator 原生事实语义,以及跨 CLI / 后端 / 沙盒路径重新独立走了一遍。整体结构和默认开启开关的实现是扎实的,但目前有两个配置下会把已有原生事实静默显示为空,与本 PR「单项缺失只省略单项」和「sandbox on/off 行为一致」的承诺冲突,因此先请求修改;未做合并。
P1:Codex context-only token_count 被 total_token_usage 门控掉
位置:src/core/cost-calculator.ts:273-279
extractCodexContextUsage(entry) 只在 extractCodexTokenCountUsage(entry) 成功后调用;后者要求 info.total_token_usage 存在。于是一个合法事件只要有 last_token_usage + model_context_window、但没有累计 total_token_usage,Context 与 Token 两项都会变成 null,而不是保留 Context、只省略累计 Token。
这不是纯构造边界:OpenAI Codex 的真实 rollout 已有这种 context-only 事件样例(last_token_usage.total_tokens=50252、model_context_window=258400,没有 total_token_usage):openai/codex#29426
我的最小复现结果:
input: token_count(info={last_token_usage:{total_tokens:50252,...}, model_context_window:258400})
actual: { context: null, tokens: null }
expected: { context: { usedTokens: 50252, windowTokens: 258400, percentUsed: 19 }, tokens: null }
建议先独立提取/更新 latestContextUsage,再单独判断累计 token snapshot,并补 context-only 回归用例。
P1:新增 daemon 卡片读链路漏传 larkAppId,沙盒重定向 transcript 全部降级为空
位置:src/core/worker-pool.ts:90-103;同类本地 fallback 在 src/cli.ts:4318-4324
SessionTokenUsageQuery.larkAppId 的既有契约就是让 resolver 找到沙盒重定向后的 <botmuxHome>/bots/<appId>/claude transcript;dashboard 与 Usage Ledger 两个旧消费者也都明确传了该字段。新加的 getDaemonSessionUsageSnapshot() 和 readCardUsageSnapshotForSend() 却都没传。
最小复现把 Claude transcript 只放在 bot 的 BOT_HOME:
getSessionUsageSnapshot(..., no larkAppId)
=> { context: null, tokens: null }
getSessionUsageSnapshot(..., larkAppId: appId)
=> { context: { usedTokens: 160 }, tokens: { in: 160, out: 20, ... } }
这会同时影响 daemon final output、adopt preamble、local-turn,以及 /api/sessions/:id/usage 给直接 botmux send 返回的结果。对沙盒 Codex 还需额外核对:CODEX_HOME 是 worker 进程本地重定向,daemon resolver 当前仍只扫 daemon 的全局 Codex home;仅补 larkAppId 只能修 Claude-family,Codex 的 daemon 卡片路径仍需等价的 BOT_HOME 解析或从 worker 传原生快照。
建议补一条真正把 transcript 放进 BOT_HOME、再经 getDaemonReplyCardUsageSnapshot 读出的测试;目前 tmux/riff 测试只验证开关 env 被传递,没有验证 usage 事实能被读到。
P2:marker 丢失 fallback 覆盖不到 custom brand footer
位置:src/im/lark/message-parser.ts:638-644
fallback 正则要求行首直接是 Context/上下文/Token(s),但真实 custom-brand footer 是 Acme · Context ... · Sent to: ...。所以恰好在注释所说的「零宽 marker 被丢弃」场景中,custom brand 仍整行泄漏进接收 bot prompt:
正文内容
Acme · Context 50.3K/258.4K (19%) · Tokens ↑1M ↓2K · Sent to: @Owner
默认品牌还有 repo URL 可兜,隐藏品牌且有收件人时现有正则也可兜;custom brand 是缺口。任意品牌很难靠正则安全识别,建议优先强化稳定 marker 的存活策略/结构信号,并至少补 marker-less custom-brand 与英文 Sent to: 用例,明确可接受的保守边界。
我实际跑过的验证
pnpm build
→ exit 0(tsc + dashboard bundle + domain/dist audit)
pnpm vitest run --project unit <14 个 PR touched 测试文件>
→ 14 files / 705 tests passed
额外回归:cost cache、usage ledger、dashboard token/resource row、
transcript-resolver BOT_HOME、card export、quoted/reply/substitute
→ 9 files / 99 tests passed
git merge-tree --write-tree origin/master HEAD
→ 无冲突,生成 merge tree
parser 对抗探针
→ 中英文/多 recipient 现有格式匹配;5 万字符病态输入无 ReDoS
其它复核结论:default-on 持久化两条路径、retry snapshot freeze、GET /usage 现有鉴权层级、PTY/tmux/zellij/Riff 的开关 env 传递未见新增问题;不支持原生数据的 CLI 保守省略也是正确的。上述两个 P1 修好并补真实路径测试后我再复审。
deepcoldy
left a comment
There was a problem hiding this comment.
首审结论修正 —— 撤回"无阻塞",确认 🔴 需要修改
@codex 的复审翻盘了我的首审结论。我已用地面真相独立复核其全部 3 条 findings,全部成立。首审的"无阻塞"是错的——根因见文末教训。
断言1(Codex context-only 事件被误丢)—— P1,官方地面真相铁证
我独立 gh issue view openai/codex#29426 拉取原文核实(未盲信转述)。真实 rollout JSONL:
{"type":"event_msg","payload":{"type":"token_count","info":{
"last_token_usage":{"input_tokens":49978,"total_tokens":50252},
"model_context_window":258400}}}这条真实事件没有 total_token_usage。用官方样例值跑当前代码:
extractCodexTokenCountUsage因缺total_token_usage返回null→foldCodexLine整个if块跳过 →extractCodexContextUsage根本不被调用 → 卡片显示{context:null}- 而本该显示
Context 50252/258400 (19%)(第二条样例18278/353400 (5%))
且该事件出现在 resume + auto-compaction 场景(2026-06-22 rollout)——正是本 PR 声称要覆盖的 restore / 续会话路径,非边缘。违反本 PR 自己的验收语义「单项缺失只省略缺失项」。维持 blocking,不可降 P3。
补充:现有测试的所有 Codex fixture 都同时带 total_token_usage(cost-calculator.test.ts:495/544/603...),无一 context-only——这正是 705 测试全绿却漏掉此 P1 的原因。
断言2(两个新 reader 漏传 larkAppId)—— P1,代码铁证
既有消费者 usage-ledger.ts + dashboard-rows.ts 调 getSessionTokenUsage 都传 larkAppId;本 PR 新增两 reader 都漏:
worker-pool.tsgetDaemonSessionUsageSnapshot(daemon final / adopt / local-turn 全走它)cli.tsreadCardUsageSnapshotForSend本地回退分支
transcript-resolver.ts 里 Claude 分支 claudeJsonlWithBotHomeFallback(sid, q, ...) 消费 q.larkAppId 回退 BOT_HOME(即 #623 修的沙盒重定向路径)→ 补传 larkAppId 即可修 Claude 沙盒。但 Codex 分支 findCodexRolloutBySessionId(codexSid) 完全不接 larkAppId、无 BOT_HOME 回退 → 沙盒 Codex 是 pre-existing 更深的洞,本 PR 让它在卡片上表面化,需 daemon 侧为 Codex resolver 补等价 BOT_HOME/CODEX_HOME 解析(与 @codex 修复范围第 3 条一致)。
断言3(custom brand + marker 丢失 → footer 泄漏)—— P2,探针铁证
fallback 正则锚 ^(?:Context|上下文|Token)。marker 被 Lark Format-A 降级吞掉后:
- 默认 brand(含 repo URL 文本命中主 marker)/ 品牌关闭(行首即 usage)→ 正确剥离 ✅
- custom brand 行首是
Acme · ...→ 锚匹配不上 → 整行泄漏进接收 bot 的 prompt ❌
根因=REPLY_CARD_FOOTER_MARKER 零宽链接拼在 content 尾部,降级丢失后 fallback 只能靠行首锚,遇 custom brand 前缀失效。
修复范围(与 @codex 对齐,4 条)
- context / cumulative token 解耦:
foldCodexLine先无条件extractCodexContextUsage更新latestContextUsage,再独立提 token。(extractCodexContextUsage只读last_token_usage+model_context_window,不碰total_token_usage,解耦可行。)补一条 context-only fixture 钉死。- 附:
foldGenericLine也有同一extractCodexTokenCountUsagegate 且未调 context——若 generic 路径可能吃 Codex 格式事件,建议一并核。
- 附:
- 两个新增 reader 补
larkAppId(修 Claude 沙盒)。 - daemon 侧为 sandbox Codex 增加 BOT_HOME/CODEX_HOME 等价解析(修 Codex 沙盒的更深洞)。
- custom brand 的 marker-loss 策略 + 测试(如让隐藏 marker 独立成段/行,不依赖行首锚)。
⭐ 我首审翻车的根因(自省)
审 token accounting 重构时,我验了「输出等价」(签名不变、2 消费者未动、finalizeTokenUsage 逐字未改),却没有把新增 reader 与既有姊妹消费者逐参数 diff——恰恰漏了 larkAppId。footer 正则我测了「正文误删」对抗用例,却漏测 brand 三态(默认/关闭/custom)。教训:新增数据读取入口必与既有同类消费者逐参数比对(尤其 larkAppId/cwd 这类沙盒敏感参数);对抗用例必穷举配置态 × 降级态。
等作者修复上述 4 条 + 补真实 context-only / 沙盒测试后,由 @codex 复审。未经申晗确认不合码。
|
To use Codex here, create a Codex account and connect to github. |

改了什么
showUsageInCardFooter,默认开启;支持以下热更新入口:Bot 配置 → 选择 Bot → 卡片行为 → 卡片页脚用量/botconfig配置卡/botconfig set showUsageInCardFooter off|onbotmux send;PTY、tmux、zellij、Riff 环境保持一致。为什么
此前卡片无法直接看到当前会话的上下文占用和原生 Token 计数,用户需要进入 CLI 或其它页面判断容量。实现坚持“只展示原生事实”:CLI 没有提供的数据不推断、不占位。
影响面
core/cost-calculator、core/worker-pool、daemon IPC、Bot 配置存储。card-prefs读写链路并即时热更新。效果图
回复卡片
Dashboard 配置入口
验证
pnpm buildBot Config → Card Behavior可见Usage in card footer,默认开启。