Skip to content

feat(card): add cardStreamingMode and stabilize card timeline ordering#494

Merged
soimy merged 22 commits into
mainfrom
reasoning-card-split
Apr 4, 2026
Merged

feat(card): add cardStreamingMode and stabilize card timeline ordering#494
soimy merged 22 commits into
mainfrom
reasoning-card-split

Conversation

@soimy

@soimy soimy commented Apr 3, 2026

Copy link
Copy Markdown
Owner

AI-assisted: 是

背景

  • 现有 DingTalk AI Card 流式配置同时存在 cardRealTimeStreamcardStreamReasoning,语义重叠且不易解释。
  • /reasoning on/reasoning stream、tool tail 与 final answer 的时间线顺序在 card 模式下还存在边界情况,容易出现 API 调用偏多或答案顺序不稳定。
  • 用户文档也仍然围绕旧布尔开关展开,和当前设计方向不一致。

目标

  • 引入单一的 cardStreamingMode: off | answer | all 配置语义,并兼容旧的 cardRealTimeStream
  • 收敛 AI Card 时间线的刷新策略,减少重复 streamAICard 调用并修复相关竞态。
  • 加固 final_seen 生命周期,允许 late thinking/tool tail 补齐,同时忽略 late partial answer,但仍可吸收 late answer block/final。
  • 更新 onboarding 与用户文档,使配置、行为与文档保持一致。

实现

  • 配置兼容
    • 新增 cardStreamingMode 与解析 helper。
    • 保留 cardRealTimeStream 作为兼容项,并在未显式设置新枚举时回退到 all
    • 从公开配置面清理 cardStreamReasoning,仅通过内部兼容路径保留旧运行时行为直到新策略接管。
    • off 作为生效默认值落在解析层,避免破坏 control-ui schema 与 legacy fallback 语义。
  • CardDraftController
    • 增加渲染级去重,避免重复发送相同卡片内容。
    • 修复 pending/in-flight 竞态,确保不会因为去重而丢失纠正性重发。
  • Card reply strategy
    • reply-strategy-card.ts 改为按 cardStreamingMode 驱动 off | answer | all
    • delivered reasoning 统一走 mode policy,而不是绕过策略层。
    • 加入 open -> final_seen -> sealed 生命周期。
    • final_seen 后允许 late reasoning/tool 进入时间线;late partial answer 仍忽略,但 late answer block/final 会更新冻结中的最终答案。
    • 修复 late tool 在 frozen final answer 前后的排序问题,包括 answer 已 seal 的情况。
  • Onboarding / 文档
    • 在 setup wizard 中加入 cardStreamingMode 选择。
    • 更新 AI Card 功能页、配置参考、API 成本说明,统一到 cardStreamingMode 语义。
    • 明确 cardStreamInterval 的作用,以及 cardRealTimeStream 的弃用兼容行为。

实现 TODO

  • 引入 cardStreamingMode 配置兼容层
  • 为 CardDraftController 添加去重与竞态修复
  • 将 card reply strategy 切换到 mode-driven 行为
  • 加固 final_seen 生命周期、late tail 排序,并允许 late answer block/final 更新冻结答案
  • 在 onboarding 中接入 cardStreamingMode
  • 更新用户文档与成本说明

验证 TODO

  • pnpm exec vitest run tests/unit/card-streaming-mode.test.ts tests/unit/config-schema.test.ts tests/unit/config-advanced.test.ts tests/unit/types.test.ts tests/unit/card-draft-controller.test.ts tests/unit/reply-strategy-card.test.ts tests/unit/inbound-handler.test.ts
  • pnpm exec vitest run tests/unit/reasoning-answer-split.test.ts tests/unit/reply-strategy-card.test.ts tests/unit/session-peer-store.test.ts tests/unit/inbound-handler.test.ts
  • npm run lint(通过;存在仓库内既有 warnings,无 lint error)
  • npm run type-check
  • pnpm test
  • 真机环境切换到当前 worktree,并执行 openclaw gateway restart
  • 真机验证:cardStreamingMode=off/reasoning on 的思考块与最终答案顺序
  • 真机验证:cardStreamingMode=answer 下 late thinking/tool tail after final 不会覆盖最终答案,且 late answer block/final 可更新冻结答案
  • 真机验证:cardStreamingMode=all 下思考与答案实时流式更新,以及 cardStreamInterval 节流效果
  • 将本次实际真机场景、结果、已知限制回填到 PR 描述

soimy and others added 18 commits April 3, 2026 17:21
Add card-side reasoning/answer text splitting for `/reasoning on` flows
so thinking blocks render in quoted format while answer text stays as
plain markdown. Introduce two new config options:

- `cardStreamReasoning` (bool, default false): when true, reasoning text
  streams in-place to the card throttled by `cardStreamInterval`; when
  false, only one `streamAICard` call per completed block to conserve
  API quota.
- `cardStreamInterval` (int ≥ 200, default 1000ms): throttle interval
  for live reasoning stream updates.

Includes card-draft-controller `sealActiveThinking` for turn-boundary
sealing, split helper with comprehensive tests, and cross-repo handoff
documentation for the upstream `fix/reasoning-block-reply-answer-loss`
branch.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@vercel

vercel Bot commented Apr 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
openclaw-channel-dingtalk Ready Ready Preview, Comment Apr 4, 2026 8:12am

@greptile-apps

greptile-apps Bot commented Apr 3, 2026

Copy link
Copy Markdown

Greptile Summary

此 PR 引入 cardStreamingMode: off | answer | all 统一枚举,替代语义模糊的 cardRealTimeStream 布尔开关,并加固了 AI Card 的 open to final_seen to sealed 生命周期,修复了 late tool/reasoning/answer block 的时间线排序竞态。所有先前 PR 评论均已落实:__ 推理行误判已修正,legacy throttle 兼容路径已保留 300ms 默认值,warnedLegacyConfigs 意图已在代码中注释说明。

Confidence Score: 5/5

此 PR 可安全合并,所有先前 P0/P1 评论均已落实,仅存一个 P2 边界情况

代码逻辑清晰、测试覆盖全面、生命周期状态机设计严谨,所有先前评审关注点均已修复。唯一未覆盖的是多次 final 携带 mediaUrls 的极端场景,属 P2 style 层面,不影响合并

src/reply-strategy-card.ts 中 deliver(final) 的媒体投递未限制在首次 final,其余文件无需特别关注

Important Files Changed

Filename Overview
src/card/card-streaming-mode.ts 新增 resolveCardStreamingMode 与一次性警告 helper,正确处理 off/answer/all 三值语义及 cardRealTimeStream 兼容回退
src/reply-strategy-card.ts 核心改动:open to final_seen to sealed 生命周期、cardStreamingMode 驱动策略、late reasoning/tool/answer 排序修复均有充分测试
src/card-draft-controller.ts 渲染级去重与 in-flight 竞态修复正确;appendToolBeforeCurrentAnswer 在 splice 后正确更新两个活跃索引
src/card/reasoning-answer-split.ts isWrappedReasoningLine 门限已修正为 >=3,规避 __ 被误判为推理行;回归测试已补充
src/config.ts normalizeLearningConfig 在 cardRealTimeStream=true 时正确返回 cardStreamingMode: undefined 以保持兼容路径
src/types.ts DingTalkConfig 新增 cardStreamingMode;stripRemovedLegacyFieldsFromPublicAccount 与 config.ts 中逻辑重复(已确认待后续重构)
tests/unit/reply-strategy-card.test.ts 全面覆盖 off/answer/all 模式、final_seen 生命周期、late tool/reasoning/answer 的吸收与忽略;legacy throttle 兼容性亦有测试

Sequence Diagram

sequenceDiagram
    participant Runtime
    participant CardReplyStrategy
    participant CardDraftController
    participant DingTalkAPI
    Runtime->>CardReplyStrategy: onPartialReply(text)
    CardReplyStrategy->>CardDraftController: updateAnswer(text)
    CardDraftController-->>DingTalkAPI: streamAICard throttled
    Runtime->>CardReplyStrategy: onReasoningStream(text)
    CardReplyStrategy->>CardDraftController: updateReasoning or appendThinkingBlock
    Runtime->>CardReplyStrategy: deliver kind=final
    note over CardReplyStrategy: lifecycleState = final_seen
    Runtime-->>CardReplyStrategy: deliver kind=tool or block late
    CardReplyStrategy->>CardDraftController: appendToolBeforeCurrentAnswer
    Runtime->>CardReplyStrategy: finalize
    CardReplyStrategy->>CardDraftController: flush and waitForInFlight
    CardReplyStrategy->>DingTalkAPI: finishAICard with overrideAnswer
    note over CardReplyStrategy: lifecycleState = sealed
Loading

Greploops — Automatically fix all review issues by running /greploops in Claude Code. It iterates: fix, push, re-review, repeat until 5/5 confidence.
Use the Greptile plugin for Claude Code to query reviews, search comments, and manage custom context directly from your terminal.

Reviews (2): Last reviewed commit: "fix(card): align streaming mode behavior..." | Re-trigger Greptile

Comment thread src/card/card-streaming-mode.ts
Comment thread src/reply-strategy-card.ts
Comment thread src/types.ts
Comment thread src/card/reasoning-answer-split.ts
@soimy soimy changed the title feat(card): add cardStreamingMode and harden final_seen lifecycle feat(card): add cardStreamingMode and stabilize card timeline ordering Apr 3, 2026
@soimy

soimy commented Apr 4, 2026

Copy link
Copy Markdown
Owner Author

对抗性审查意见与修复进展简报

本轮基于 main 对当前 PR 做了合并前对抗性审查,重点核对了:

  • docs/spec/2026-04-02-dingtalk-card-reasoning-answer-split-design.md
  • docs/spec/2026-04-03-dingtalk-card-streaming-mode-design.md
  • 对应 implementation plan 的实现覆盖
  • card reply strategy / inbound handler / config 兼容链路中的潜在逻辑错误

主要发现

  1. cardStreamingMode=off 时,onPartialReply 没有注册,导致 answer snapshot 会被直接丢弃;当 final 为空或仅附件时,会退化成 ✅ Done,与 spec 中“只缓存、不实时发送”的语义不一致。
  2. 多账号场景下,schema 会把账号级未填写的 cardRealTimeStream 注入成 false,从而吞掉顶层 legacy cardRealTimeStream=true 的继承,解析结果会错误落到 off
  3. mixed Reasoning: block/final 在 off/answer 模式下存在 thinking 重复 flush 风险,同一段 Reason: 可能进入 timeline 两次。
  4. legacy cardRealTimeStream=true 仍保留旧的 300ms 默认节流,与本轮 cardStreamingMode 方案里“统一走 cardStreamInterval / 默认 1000ms”的目标不一致。

修复进展

  • 已提交并推送修复提交:01290b5 fix(card): align streaming fallback and answer buffering
  • 本次修复包含:
    1. off 模式继续接收 partial answer,但只做本地缓存,不触发实时刷卡;final 为空时仍可正确复用缓存答案。
    2. 去掉 schema 对账号级 cardRealTimeStream 的默认 false 注入;getConfig() 会把 legacy true 正确解析为生效的 cardStreamingMode: all
    3. 修正 mixed reasoning payload 在 Reason: 路径上的重复 flush,确保同一段 thinking 只进入一次 timeline。
    4. 将 legacy fallback 的默认节流统一为 1000ms,与 cardStreamingMode 方案保持一致。
  • 同时补齐了对应单测 / 集成测试,防止上述问题回归。

验证结果

  • npm run type-check:通过
  • pnpm test:通过,72 passed / 873 passed
  • npm run lint:通过(仓库仍存在历史 warning,未在本次修复中顺手清理;本次未引入新的 lint error)

这轮修复后,前述 4 个对抗性审查问题都已完成收口,当前分支状态适合继续走合并前复核。

@soimy soimy merged commit d268a2e into main Apr 4, 2026
4 checks passed
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.

1 participant