fix(reply): keep DingTalk group card finals in card#557
Conversation
…up chat The #548 fix removed the initial streaming PUT call from createAICard, replacing it with flowStatus in cardParamMap. However, flowStatus only controls the UI state — the card's server-side streaming lifecycle must be explicitly opened via the streaming API for IM_GROUP spaces to accept subsequent variable updates (blockList etc.). Without this, blockList updates fail silently in group chats, the card displays empty, and markdown fallback is triggered. Restore the initial streaming kick while preserving #548's lifecycle tracking and finalization (finalizeAICardStreamingLifecycleIfNeeded) that properly close the streaming lifecycle on card completion. Closes #555 Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
Greptile Summary此 PR 通过恢复
Confidence Score: 5/5修复逻辑清晰,变更范围有限且均有测试覆盖,可以安全合并 三处核心改动均有对应单元或集成测试验证,kick 失败路径被 try-catch 正确处理不影响卡片创建主流程,未发现逻辑错误或数据损坏风险 tests/integration/inbound-group-card-lifecycle.test.ts 中 mock 模拟了 SDK 对 sourceReplyDeliveryMode 的消费逻辑,建议真机验证时重点确认该字段是否被 OpenClaw 运行时正确识别
|
| Filename | Overview |
|---|---|
| src/card-service.ts | 恢复流式 kick 并正确持久化 streamLifecycleOpened/state;kick 失败时静默 catch 且不激活 degrade,逻辑正确 |
| src/card-callback-service.ts | 移除 updatePrivateDataByKey,修复群聊共享卡片更新时的 400 错误;测试已同步断言新的 cardUpdateOptions |
| src/reply-strategy-card.ts | 新增 sourceReplyDeliveryMode automatic,确保群聊最终回复投递至卡片而非 message tool;已有单元测试覆盖 |
| src/reply-strategy-types.ts | 新增 SourceReplyDeliveryMode 类型并扩展 ReplyOptions 接口,类型定义清晰 |
| tests/integration/inbound-group-card-lifecycle.test.ts | 新增集成测试,覆盖群聊卡片完整生命周期与 private-data 回归;mock 行为与真实 SDK 一致性依赖人工确认 |
| tests/unit/card-service.test.ts | 更新所有 createAICard 测试以注册 PUT mock 并断言 state=INPUTING;recovery 测试断言两次 PUT 顺序正确 |
| tests/unit/reply-strategy-card.test.ts | 新增对 sourceReplyDeliveryMode automatic 的单元断言,测试覆盖完整 |
Reviews (4): Last reviewed commit: "fix(reply): keep DingTalk group card fin..." | Re-trigger Greptile
…rage Address review feedback from #557: 1. After the streaming kick succeeds, call upsertPendingCard to persist the updated streamLifecycleOpened and INPUTING state so crash recovery can properly close the streaming lifecycle on the DingTalk side. 2. Add PUT mock responses to all createAICard tests that expect success, ensuring the streaming kick behavior is covered. Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
…AICard The best-effort streaming kick should not trigger account-wide card degradation on transient errors (429, 5xx). Aligns with the existing suppressDegrade usage in finalizeAICardStreamingLifecycleIfNeeded. Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
zhumin-zizhu
left a comment
There was a problem hiding this comment.
仔细 review 过当前 PR HEAD 8b11f54,结论:可以合并。
这个 PR 的核心修复方向是对的:createAICard 成功后恢复一次空 content 的 streaming lifecycle kick,用来显式打开 DingTalk 服务端的 streaming lifecycle。结合 issue #555 的现象,这能解释群聊里卡片创建成功但后续 blockList 变量更新不被接受、最终空卡并 fallback 到 markdown 的链路。kick 成功后也会持久化 streamLifecycleOpened 和 INPUTING 状态,恢复路径能正确知道后续需要关闭 lifecycle。
我重点看了 createAICard、putAICardStreamingField、commitAICardBlocks、pending-card recovery 这几条路径。按合并顺序先落 PR #554,再落这个 PR 的前提看,#554 已经处理 finalize 阶段用空 content + isFinalize=true 关闭 streaming lifecycle,因此这里恢复 create kick 不会重新引入完成瞬间重复渲染问题。
本地验证:
pnpm test -- tests/unit/card-service.test.ts通过;该命令实际跑起 105 个 test files,1094 个 tests 全部通过npm run type-check通过npm run lint通过:0 errors,88 个既有 warningspnpm run build通过git diff --check通过
非阻塞建议:PR 描述里的群聊/私聊真机验证还没勾选,合并后发版前最好补一次群聊真机 smoke,确认 #555 的空卡路径在真实 DingTalk 群空间中已恢复。
|
本轮继续排查群聊空卡片问题后的结论和修复如下。 发现
修复
真机验证
本地验证
|
|
@soimy 今天能合并发布新版本吗? 我们自己的项目升级可以安排上? |
|
争取,计划还有一个pr要合并,再更新下文档 |
|
同问今天可以发掉吗😂?群聊用不了蛮影响体验的。 |
着急可以直接改 openclaw.json中的配置 |
Fix review feedback from PR #566: replace incorrect PR #557 references with the correct PR #553 (build artifact fix) alongside PR #565 (group chat delivery fix). Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
* docs: sync PR#553 and PR#565 updates to user docs PR#553 (fix(package): publish compiled OpenClaw runtime): - Add runtime build requirement for local installs (pnpm run build) - Update install/update docs with v3.6.2+ build step - Document SecretInput source restriction (env/file only, exec removed) - Update onboarding flow description (URL display instead of auto-open browser) - Add troubleshooting section for plugin loading failures PR#565 (fix(reply): keep DingTalk markdown finals in session replies): - Document upstream group chat visibleReplies=message_tool default - Explain plugin's automatic override (sourceReplyDeliveryMode: automatic) - Add configuration reference section on upstream default behavior - Add troubleshooting section for group chat reply issues - Update reply-modes.md with group chat compatibility note Related: #553, #565, #557 Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering> * docs: correct PR references in user docs Fix review feedback from PR #566: replace incorrect PR #557 references with the correct PR #553 (build artifact fix) alongside PR #565 (group chat delivery fix). Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering> * chore: add .gitnexus to .gitignore Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering> * docs: unify package manager to pnpm - Add pnpm installation hint in install.md and development.md - Replace all `npm run` with `pnpm run` for scripts - Replace `npm install` with `pnpm install` for dependency installation - Replace `package-lock.json` with `pnpm-lock.yaml` in cleanup instructions - Keep npm-specific commands (npm version, npm publish, npm login) unchanged The repository uses pnpm as the only package manager (packageManager field in package.json, pnpm-lock.yaml, CI workflows). npm commands for running scripts would fail because scripts internally call `pnpm run`. Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Happy <yesreply@happy.engineering>
背景
Fixes #555。PR 初始判断集中在 DingTalk AI Card 的 streaming lifecycle,但本轮群聊真机验证显示旧问题仍会复现:群聊入站回复会出现一个空的 streaming 卡片,同时另发 fallback markdown / message-tool 消息;私聊卡片回复正常。
进一步排查后,根因是 OpenClaw 群聊默认
messages.groupChat.visibleReplies=message_tool会让运行时把 source reply delivery 解析成message_tool_only。普通群聊里这是合理默认:可见回复应通过message(action=send)发出。但 DingTalkmessageType=card已经在入站消息上创建了原始 AI Card,卡片策略本身就是可见回复承载面;如果继续继承message_tool_only,final answer 会被导向messagetool,原卡片只剩空壳 / Done 状态,另一路消息则表现为 fallback markdown 或额外卡片。本轮也确认了之前 22:26 的失败验证不是修复思路本身生效,而是 OpenClaw 真机调试加载的是
dist/index.js,当时只改了src,没有先执行 runtime build,gateway 仍在运行旧构建 / Jiti 缓存代码。目标
visibleReplies=message_tool默认值而触发额外 markdown / message-tool 可见回复。updatePrivateDataByKey。pnpm run build:runtime的要求写入 agent / contributor 指引。实现
src/reply-strategy-card.ts:DingTalk card reply strategy 在getReplyOptions()中显式设置sourceReplyDeliveryMode: "automatic",让运行时 final reply 回到当前 card strategy,而不是进入 group message tool-only 路径。src/reply-strategy-types.ts:补充ReplyOptions.sourceReplyDeliveryMode类型声明。src/card-callback-service.ts:卡片变量更新只设置updateCardDataByKey,不再对共享群聊卡片请求updatePrivateDataByKey。tests/integration/inbound-group-card-lifecycle.test.ts:新增群聊入站卡片生命周期回归测试,模拟visibleReplies=message_tool,验证 final 写入卡片且不会调用 session webhook fallback。tests/unit/reply-strategy-card.test.ts/tests/unit/card-service.test.ts:补充对应单元断言。AGENTS.md、CLAUDE.md、docs/contributor/testing.md、skills/dingtalk-real-device-testing/SKILL.md:补充真机调试必须先 build runtime,再 restart gateway 的流程提醒。实现 TODO
main并解决冲突。验证 TODO
pnpm run build:runtime通过。npm run type-check通过。npm run lint通过,0 errors,仍有 88 个既有 warnings。pnpm vitest run tests/integration/inbound-group-card-lifecycle.test.ts tests/unit/card-service.test.ts tests/unit/reply-strategy-card.test.ts tests/unit/card-draft-controller.test.ts tests/unit/reply-strategy-markdown.test.ts tests/unit/reply-strategy-with-reaction.test.ts tests/unit/inbound-handler-card.test.ts tests/unit/inbound-handler-card-streaming.test.ts tests/unit/inbound-handler.test.ts通过,9 files / 264 tests。git diff --check通过。pnpm run build:runtime后重启 gateway,并用openclaw channels status --probe --json确认 DingTalk channel running / connected。deliveredFinalCount=1/counts.final=1,未出现Tracking pending messaging text: tool=message或 proactive card / markdown fallback 路径。