feat(card): implement card template v2 with rich block content and action btns#480
feat(card): implement card template v2 with rich block content and action btns#480
Conversation
第二轮 Codex Adversarial Review 修复进展基于第二轮 Codex adversarial review 发现的 3 个问题,已全部修复并推送( 修复内容1.
|
| 文件 | 变更类型 |
|---|---|
src/draft-stream-loop.ts |
新增 onEveryNSends / persistInterval 参数 |
src/card-draft-controller.ts |
新增 appendRecoveryMarkerBlock,注入持久化回调 |
src/card-service.ts |
新增 updatePendingCardLastContent,重排 finishAICard 顺序,移除 hideButton 调用 |
src/card/card-stop-handler.ts |
移除 Phase 3 hideCardStopButton |
| 4 个测试文件 | 更新 PUT 次数断言、mock 补充、recovery marker 断言 |
🔄 卡片生命流程更新方式 — 测试验证总结基于对 v2 模板 (`9a9138ed-35d3-498e-9019-748b59ddc39a.schema`) 的测试,验证了卡片生命流程的正确更新方式: 生命周期 API 对照
关键发现
测试脚本
详细实现指南见 `docs/plans/2026-04-03-card-v2-implementation-handoff.md` |
AI Card v2 混合流式更新 - 实现进展✅ 已完成 (2026-04-04)Commit: 实现了混合 API 路由策略,解决了 streaming API 对复杂 loopArray 类型返回 500 错误的问题: 架构设计关键变更
配置开关行为
测试状态
参考文档
下一步: 真机测试验证两种模式的卡片渲染效果 |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
96ad981 to
c9f7316
Compare
0150205 to
22516ac
Compare
📋 Image Block 测试迁移完成进度更新测试状态: ✅ 829/829 全部通过 新增 Commits
核心变更格式迁移: 从 markdown ( // 旧格式
> 思考中...
> 工具调用...
最终答案
// 新格式
[
{"type": 1, "markdown": "思考中..."},
{"type": 2, "markdown": "工具调用..."},
{"type": 0, "markdown": "最终答案"},
{"type": 3, "mediaId": "xxx"}
]Media 处理变更:
下一步
|
📦 资源文件已添加新增两个 JSON 资源文件用于 Card v2 模板:
这些资源可用于理解 v2 模板结构和进行测试数据准备。 |
🔧 V2 Card Template 修复完成修复清单
核心变更Finalize 链路重构:
Controller 方法拆分:
Media 处理增强:
验证结果
文档实施记录已归档至 🤖 Generated with Claude Code |
…askInfo display Migrate AI Card from markdown streaming to JSON CardBlock[] rendering via DingTalk instances API. Key changes: - CardDraftController: timeline entries rendered as CardBlock[] instead of markdown; supports thinking/answer/image block kinds - commitAICardBlocks: single instances API call for finalize (replaces finishAICard + streamAICard dual-path) - Inline media: images uploaded via prepareMediaInput and embedded as image blocks in card timeline - quoteContent: shows inbound message text in card header - taskInfo: displays model/usage/elapsed metadata in card footer - Card run registry: bridges sendMedia mediaId to active card controller
- Add missing card-service mocks (updateAICardBlockList, streamAICardContent, clearAICardStreamingContent) - Update test expectations for V2 finalize path (commitAICardBlocks instead of finishAICard) - Fix reasoning streaming assertions to match cardStreamingMode behavior - Add streamingKey/blockListKey to DingTalkCardTemplateContract - Fix missing closing brace in card-draft-controller test - Update handoff document with completed status All 870 tests now pass.
692a032 to
076cbb1
Compare
🎉 Card V2 Rebase & Test Fix 完成进度总结Rebase PR #494 已完成,所有测试均已通过: 修复内容
测试修复详情
冲突解决8 个冲突文件全部已解决,详细记录在 后续工作
分支已准备好进行 review 或 merge。 |
Design for splitting 7770-line test file into ~10 domain-focused files with shared mock fixtures and redundancy cleanup. Includes documentation update plan for test file scale control guidelines.
19 tasks covering mock factory, 11 split files, and 6 doc updates
Create a reusable mock factory module that will be imported by all split test files during the inbound-handler.test.ts refactor.
Ensure local/private markdown images are rerouted into card image blocks with inline placeholders, while task metadata and session timing stay current through card creation, streaming, and finalize.
|
补充说明:最近围绕卡片 V2 又收敛了两类和真机场景强相关的修复,分别是 media 链路以及 1. media 链路修复 参考 docs/plans/2026-04-14-card-v2-consecutive-dm-image-sessionkey-minus-handoff.md,这次主要解决的是单聊里连续两次请求发图时,第二次图片掉出 card、变成独立消息的问题。根因是 OpenClaw runtime 在 message tool 的 这部分修复做了三层兜底:
这意味着单聊连续发图场景下,message tool 不再因为 2. 这部分前后其实修了两层问题: 第一层是数值“卡住”的问题。此前 第二层是数值“偏大”的问题。实际排查里发现 answer 模式下有两个过度调用来源:
这次的修复是:
这样一来, 3. 当前状态 相关修复已经补了类型检查和单测回归,重点覆盖了:
后续仍建议继续做真机验证,尤其关注:
|
|
补充一轮 review follow-up 修复,已推送 commit a168a6f。
验证:
|
Greptile Summary本 PR 将卡片渲染从单一 content 字段迁移到结构化 blockList(CardBlock 数组),支持 thinking/tool/answer/image 差异化渲染,并引入 per-card taskInfo(model/effort/dapi_usage/taskTime)页脚追踪。同时修复了之前审查发现的 P1 问题(stop 路径混用 JSON 与 markdown)以及媒体链路 sessionKey 为连字符时导致图片无法嵌入卡片的问题。
Confidence Score: 5/5主要用户路径无阻断性问题,815 个测试全部通过,可合并。 之前报告的 P0/P1 问题均已修复;剩余两处 P2 问题仅影响服务重启后 pending card 恢复的运维可观测性,不影响正常流程。 src/card-service.ts 中的 finalizePendingCardsByAccount 恢复失败处理逻辑需与 PR 描述保持一致。 Important Files Changed
Sequence DiagramsequenceDiagram
participant IH as inbound-handler
participant RS as reply-strategy-card
participant CDC as card-draft-controller
participant CS as card-service
IH->>CS: createAICard (taskInfo初始化 dapi_usage=1)
CS-->>IH: AICardInstance
loop 流式内容
IH->>RS: deliver(block/tool)
RS->>CDC: updateAnswer / appendTool / appendThinkingBlock
CDC->>CS: streamAICardContent (content key)
CDC->>CS: updateAICardBlockList (blockList key)
CS->>CS: incrementCardDapiCount
end
RS->>RS: onModelSelected
RS->>CS: updateAICardTaskInfo (taskInfo key)
IH->>RS: finalize()
RS->>CDC: flush + waitForInFlight
RS->>CS: commitAICardBlocks (blockList + content + taskInfo + flowStatus=3)
CS->>CS: state = FINISHED, removePendingCard
note over CS: 停止路径
IH->>CS: finalizeStoppedAICard
CS->>CS: buildStoppedCardFinalizePayload
CS->>CS: state = STOPPED, removePendingCard
Reviews (3): Last reviewed commit: "fix(lint): add curly braces to satisfy e..." | Re-trigger Greptile |
|
补充一轮 review follow-up 修复,已同步到本 PR 并回写到对应线程。 本轮已完成并标记 resolved 的点:
验证:
当前保留 open 的 review 点:
|
Token Usage Tracking — 通过
|
| 文件 | 用途 |
|---|---|
src/run-usage-store.ts |
runId↔session 映射 + token usage 累加器 |
tests/unit/run-usage-store.test.ts |
11 个测试用例 |
tests/unit/llm-output-hook.test.ts |
hook 注册 + 事件处理验证 |
docs/plans/2026-04-19-card-taskinfo-token-usage-implementation.md |
实施计划 |
修改文件
| 文件 | 变更 |
|---|---|
index.ts |
registerFull 中注册 api.on("llm_output", ...) |
src/reply-strategy-card.ts |
onAgentRunStart 记录 runId 映射;buildTaskInfoJson() 读取 usage;card 终态时清理 |
docs/spec/2026-03-30-card-template-v2-design.md |
CardTaskInfo 新增 5 个字段 |
CardTaskInfo 扩展
export interface CardTaskInfo {
model?: string;
effort?: string;
dap_usage?: number;
taskTime?: number;
// 新增
inputTokens?: number; // 输入 token(含 cache)
outputTokens?: number; // 输出 token
cacheRead?: number; // 缓存命中读取
cacheWrite?: number; // 缓存写入
totalTokens?: number; // 总计
}Commits (6ab9a2c..e1ec736)
6ab9a2cfeat(card): add run-usage-store for tracking token consumption per session1d2e514feat(card): register llm_output hook to capture token usagefb9899dfix(test): type mockApi as OpenClawPluginApi872d17cfeat(card): wire token usage tracking into card reply strategy9eea441fix(test): add on mock to runtime-peer-index teste1ec736docs: add token usage tracking implementation plan
验证
- 98 test files / 1016 tests 全部通过
- TypeScript strict type-check 通过
- Lint 无新增 error
Switch run-usage-store from accountId:conversationId keys to runId keys. Each agent run gets its own isolated usage accumulation, so clearing a card's usage on finalization no longer wipes sibling agent runs sharing the same conversation. Add runId to TaskMeta for lookup in buildTaskInfoJson().


背景
当前 AI Card 使用单一
content变量承载 markdown 文本进行流式渲染,无法区分 thinking/tool/answer 等不同语义的内容块,模板无法针对不同块类型做差异化渲染。本次重新设计了新一代卡片模板,支持blockList结构化数据契约,允许每个块携带type字段驱动条件渲染。目标
blockList替代单一content字段承载流式内容CardBlock类型系统,包含text/markdown/type/mediaId/btns字段实现
数据契约
CardBlock/CardBlockType/CardBtn/CardTaskInfo类型定义(src/types.ts)CardBlockType: 0=answer, 1=think, 2=tool, 3=image, 4=approval, 5=topic模块变更
src/card/card-template.tsisV2,保留DINGTALK_CARD_TEMPLATE_ID作为运维兜底 overridesrc/card-draft-controller.tsrenderTimeline()始终输出CardBlock[]JSON;新增PROCESS_BLOCK_TEXT_MAX_LENGTH截断常量;新增appendStopMarkerBlock()保留结构化内容src/card-service.tscreateAICard初始化taskInfo变量;streamAICardfinalize 前先写 content 再发isFinalize=true;新增streamTaskInfo();新增incrementCardDapiCount()/getCardTaskTimeSeconds()src/session-state.tsmodel/effort;移除dapiCount/taskStartTimesrc/types.tsAICardInstance新增taskInfo字段(per-card metrics)src/reply-strategy.tsModelSelectedContext接口和onModelSelected回调src/reply-strategy-card.tsonModelSelected:model/effort 变化时同步到所有活跃卡片src/card/card-run-registry.tslistActiveCardsByConversation()用于多卡片同步src/inbound-handler.tsinitSessionState(),不再清除已存在的 session stateFinalize 顺序
修复了 Codex 对抗性审核发现的 P1 问题,调整 finalize 调用顺序:
content字段(复制按钮用,isFinalize=false)blockList(isFinalize=true)确保 DingTalk API 在关闭流后复制按钮仍能正常工作。
Codex 对抗性审核修复
基于 Codex adversarial review(
/codex:adversarial-review --base main)发现的问题进行了以下修复:P0: Pending-card recovery rollback safety
问题:持久化记录不包含模板/schema 版本标识,服务重启或 rollback 时可能用错误的字段契约恢复卡片,导致失败后删除记录丢失恢复线索。
修复:
PendingCardRecord新增lastContent字段存储完整的CardBlock[]JSONlastContent而非 fallback 文本lastContent)直接删除,不尝试恢复P1: Stop 内容丢失
问题:停止时将
CardBlock[]扁平化为纯文本,丢失mediaId/btns等结构化内容。修复:
appendStopMarkerBlock()函数CardBlock[]结构,仅追加停止标记块P2: TaskInfo 跨 run 状态污染
问题:
dap_usage/taskTime按accountId:conversationId存储,多 run 场景下状态混乱。修复:
dap_usage/taskTime移入AICardInstance.taskInfo(per-card 隔离)SessionState仅保留model/effort(session-level 共享)onModelSelected回调同步到所有活跃卡片incrementCardDapiCount(card)累加特定卡片的计数getCardTaskTimeSeconds(card)计算特定卡片耗时实现 TODO
验证 TODO
pnpm run type-check通过pnpm run lint通过(无新增警告)pnpm test— 71 文件,815 用例全部通过Breaking Changes
DINGTALK_CARD_TEMPLATE_ID环境变量 override,作为模板灰度/紧急回退时的运维兜底;默认仍使用内置 v2 模板lastContent字段)将在恢复时被删除相关设计文档
AI 辅助声明
本 PR 由 AI 辅助完成。提交者已审查全部变更,确认类型检查和测试通过。
后续补充修复
Media 链路修复
参考
docs/plans/2026-04-14-card-v2-consecutive-dm-image-sessionkey-minus-handoff.md,这次补上了单聊连续发图场景下 card run 归属丢失的问题。根因是 OpenClaw runtime 在 message tool 的handleAction阶段会传sessionKey=-,导致 channel 侧无法稳定从sessionKey还原conversationId,同时 runtime 也不会自动补expectedCardOwnerId,于是图片发送时无法命中当前活跃 card run,最终退化成独立消息。这部分修复主要包括:
src/card/card-run-registry.ts新增resolveCardRunByOwner,允许仅按accountId + ownerUserId回查最近活跃 card run。src/send-service.ts调整图片命中策略:优先conversation + owner,其次conversation only,最后在conversationId缺失但expectedCardOwnerId可用时走owner-only fallback。src/channel.ts的 message tool action 层不再只依赖 runtime 透传的sessionKey,而是在 direct target 场景下从当前target回推出conversationId和expectedCardOwnerId,使sessionKey=-时仍然能把图片嵌回当前用户的活跃 card。这让单聊连续两次请求发图时,第二次图片也能继续走 card 内嵌链路,而不是掉到 card 外的主动消息。
dapi_usage修复这部分前后收敛了两层问题:
此前
taskInfo在早期刷新后会把ctx.taskMeta.usage固定成旧值,导致 finalize 时即使卡片上的dapi_usage已增长,最终写回卡片页脚的仍然是旧值,表现为一直停在较小的数字。现在 finalize 生成taskInfoJson时会优先读取最新的card.taskInfo.dapi_usage,避免被旧的ctx.taskMeta.usage缓存覆盖。实际排查中又发现 answer 模式下有两个额外放大来源:
content流更新绕过了统一的cardStreamInterval节流。answer模式的 partial reply 一边走 streaming content,一边又每次都触发updateAICardBlockList,导致本来只该在 think/tool/assistant-turn/finalize 边界刷新的blockList被按 partial 频率重复刷新。这次的修复是:
src/card-draft-controller.ts中把content和blockList拆成两套独立 loop,content现在走独立的 latest-wins 节流发送,真正受cardStreamInterval控制。answer模式下,partial answer 只实时更新content,不再每次都实时更新blockList;blockList只在 think/tool/assistant-turn/finalize 这些边界统一 flush。card/instances收口提交。这样一来,
dapi_usage不仅不会再被旧缓存卡住,也不会因为 answer 模式下的重复 content/blockList 刷新而被明显放大。Review follow-up 修复(2026-04-14, a168a6f)
taskTime计算,按 card 生命周期与已有 meta/session 取最大值,避免同会话 timer reset 后污染当前卡片。mediaUrlAllowlist透传,覆盖payload.mediaUrls和 deferred 非图片附件。dapi_usage计数,成功重试也会准确累计一次。补充验证:
pnpm vitest run tests/unit/reply-strategy-card.test.ts tests/unit/card-service.test.ts tests/unit/inbound-handler-card-streaming.test.ts tests/unit/message-actions.test.ts tests/unit/send-service-media-owner-fallback.test.ts tests/unit/send-service-media.test.tsnpm run type-check