Skip to content

feat(card): 在回复卡片页脚展示 Context 和 Token 用量 - #637

Open
ChaseElectr wants to merge 2 commits into
deepcoldy:masterfrom
ChaseElectr:feat/qi/card-usage-footer
Open

feat(card): 在回复卡片页脚展示 Context 和 Token 用量#637
ChaseElectr wants to merge 2 commits into
deepcoldy:masterfrom
ChaseElectr:feat/qi/card-usage-footer

Conversation

@ChaseElectr

@ChaseElectr ChaseElectr commented Jul 28, 2026

Copy link
Copy Markdown

改了什么

  • 在普通 Bot 回复卡片页脚展示 Agent CLI 原生提供的 Context Usage 与 Token Usage。
  • 单项数据缺失时只省略该项,不根据正文或模型名估算。
  • 新增 Bot 级开关 showUsageInCardFooter,默认开启;支持以下热更新入口:
    • Dashboard:Bot 配置 → 选择 Bot → 卡片行为 → 卡片页脚用量
    • 飞书 /botconfig 配置卡
    • /botconfig set showUsageInCardFooter off|on
  • Dashboard 开关保存失败时回滚 UI,避免页面状态与运行时配置不一致。
  • 将展示判断覆盖 daemon final output、adopt preamble、local-turn 与直接 botmux send;PTY、tmux、zellij、Riff 环境保持一致。
  • 展示开关只影响回复卡片,不停止 Usage Ledger 或其它统计消费。

为什么

此前卡片无法直接看到当前会话的上下文占用和原生 Token 计数,用户需要进入 CLI 或其它页面判断容量。实现坚持“只展示原生事实”:CLI 没有提供的数据不推断、不占位。

影响面

  • 公共层core/cost-calculatorcore/worker-pool、daemon IPC、Bot 配置存储。
  • Dashboard:Bot 配置的卡片行为区新增开关,复用既有 card-prefs 读写链路并即时热更新。
  • IM:仅普通飞书回复卡片页脚;流式状态卡、控制卡、自定义卡、纯语音/视频输出不受影响。
  • CLI:支持原生 transcript 用量解析的 CLI 会展示数据;其它 CLI 自动保持原样。
  • 后端/会话:覆盖 PTY、tmux、zellij、Riff,以及普通、adopt、restore/local-turn 路径。
  • 兼容性:配置缺省为开启;关闭时仅隐藏 Context/Token,品牌和收件人等页脚内容保留。

效果图

回复卡片

回复卡片页脚用量 ON/OFF 对比

Dashboard 配置入口

Dashboard 卡片页脚用量开关

验证

  • 相关定向测试:
    • 18 个测试文件、762 个测试全部通过。
    • 覆盖默认开启、关闭落盘、重新开启删键、daemon 内存热更新、Dashboard 聚合 payload、React 开关成功路径与失败回滚。
  • pnpm build
    • TypeScript 编译、Dashboard bundle、domain audit 与 dist audit 全部通过。
  • live Dashboard 实测:
    • Bot Config → Card Behavior 可见 Usage in card footer,默认开启。
  • live daemon 重启验证:
    • 4 个 Bot daemon 与 dashboard 均 online,重启计数为 0。

@ChaseElectr
ChaseElectr marked this pull request as ready for review July 28, 2026 11:58
@ChaseElectr
ChaseElectr requested a review from deepcoldy as a code owner July 28, 2026 11:58
@ChaseElectr
ChaseElectr force-pushed the feat/qi/card-usage-footer branch from f074e2a to 55afd8e Compare July 28, 2026 12:14
@xu4wang

xu4wang commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

似乎也可以在卡片签名里面,扩展一些变量 ctx , model,token 之类的来自己写markdown支持。
image

@deepcoldy deepcoldy left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

首次 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-retriescardUsage ??= ... 在首次 attempt 读一次并穿透重试;隐藏开关返回具体 {context:null,tokens:null} 对象(truthy),故 ??= 不会重读,"隐藏"状态也正确冻结。

6. token accounting 重构输出等价getSessionTokenUsage 签名与返回类型不变,2 个既有消费者(usage-ledger / dashboard-rows)未受影响,finalizeTokenUsage 逐字未改;latestContextUsage 是纯增量字段。

7. env / 多路径BOTMUX_SHOW_USAGE_IN_CARD_FOOTERBOTMUX_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 修剪 / 阈值边界、Codex total_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)请复审。未经申晗确认不合码。

@chatgpt-codex-connector

Copy link
Copy Markdown

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

@deepcoldy deepcoldy left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex 复审:🔴 请求修改(2 个阻塞项,1 个 parser 边界项)

我按 parser 边界、cost-calculator 原生事实语义,以及跨 CLI / 后端 / 沙盒路径重新独立走了一遍。整体结构和默认开启开关的实现是扎实的,但目前有两个配置下会把已有原生事实静默显示为空,与本 PR「单项缺失只省略单项」和「sandbox on/off 行为一致」的承诺冲突,因此先请求修改;未做合并

P1:Codex context-only token_counttotal_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=50252model_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 deepcoldy left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

首审结论修正 —— 撤回"无阻塞",确认 🔴 需要修改

@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 返回 nullfoldCodexLine 整个 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.tsgetSessionTokenUsage 都传 larkAppId;本 PR 新增两 reader 都漏:

  • worker-pool.ts getDaemonSessionUsageSnapshot(daemon final / adopt / local-turn 全走它)
  • cli.ts readCardUsageSnapshotForSend 本地回退分支

transcript-resolver.tsClaude 分支 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 条)

  1. context / cumulative token 解耦:foldCodexLine 先无条件 extractCodexContextUsage 更新 latestContextUsage,再独立提 token。(extractCodexContextUsage 只读 last_token_usage+model_context_window,不碰 total_token_usage,解耦可行。)补一条 context-only fixture 钉死。
    • 附:foldGenericLine 也有同一 extractCodexTokenCountUsage gate 且未调 context——若 generic 路径可能吃 Codex 格式事件,建议一并核。
  2. 两个新增 reader 补 larkAppId(修 Claude 沙盒)。
  3. daemon 侧为 sandbox Codex 增加 BOT_HOME/CODEX_HOME 等价解析(修 Codex 沙盒的更深洞)。
  4. custom brand 的 marker-loss 策略 + 测试(如让隐藏 marker 独立成段/行,不依赖行首锚)。

⭐ 我首审翻车的根因(自省)

审 token accounting 重构时,我验了「输出等价」(签名不变、2 消费者未动、finalizeTokenUsage 逐字未改),却没有把新增 reader 与既有姊妹消费者逐参数 diff——恰恰漏了 larkAppId。footer 正则我测了「正文误删」对抗用例,却漏测 brand 三态(默认/关闭/custom)。教训:新增数据读取入口必与既有同类消费者逐参数比对(尤其 larkAppId/cwd 这类沙盒敏感参数);对抗用例必穷举配置态 × 降级态。


等作者修复上述 4 条 + 补真实 context-only / 沙盒测试后,由 @codex 复审。未经申晗确认不合码。

@chatgpt-codex-connector

Copy link
Copy Markdown

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

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.

3 participants