Skip to content

fix(reply): keep DingTalk group card finals in card#557

Merged
soimy merged 5 commits into
mainfrom
fix/issue-555-empty-card-group-chat
May 13, 2026
Merged

fix(reply): keep DingTalk group card finals in card#557
soimy merged 5 commits into
mainfrom
fix/issue-555-empty-card-group-chat

Conversation

@soimy

@soimy soimy commented May 8, 2026

Copy link
Copy Markdown
Owner

背景

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) 发出。但 DingTalk messageType=card 已经在入站消息上创建了原始 AI Card,卡片策略本身就是可见回复承载面;如果继续继承 message_tool_only,final answer 会被导向 message tool,原卡片只剩空壳 / Done 状态,另一路消息则表现为 fallback markdown 或额外卡片。

本轮也确认了之前 22:26 的失败验证不是修复思路本身生效,而是 OpenClaw 真机调试加载的是 dist/index.js,当时只改了 src,没有先执行 runtime build,gateway 仍在运行旧构建 / Jiti 缓存代码。

目标

  • 群聊 card mode 下,入站 @bot 回复的 final answer 必须写回同一张 DingTalk AI Card。
  • 不再因为 OpenClaw 群聊 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.mdCLAUDE.mddocs/contributor/testing.mdskills/dingtalk-real-device-testing/SKILL.md:补充真机调试必须先 build runtime,再 restart gateway 的流程提醒。

实现 TODO

  • 合并最新 main 并解决冲突。
  • 定位群聊与私聊卡片行为不一致的根因。
  • 在 DingTalk card strategy 中覆盖群聊 source reply delivery mode。
  • 修正群聊共享卡片变量更新参数。
  • 增加群聊入站卡片生命周期集成回归测试。
  • 更新真机调试流程文档与 agent skill 提醒。

验证 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。
  • 群聊真机入站验证:22:37:58 用户发送群聊消息后,日志显示 final answer 进入原 AI Card,deliveredFinalCount=1 / counts.final=1,未出现 Tracking pending messaging text: tool=message 或 proactive card / markdown fallback 路径。

…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-apps

greptile-apps Bot commented May 8, 2026

Copy link
Copy Markdown

Greptile Summary

此 PR 通过恢复 createAICard 中的流式 kick 调用、移除 updateCardVariables 中多余的私有数据更新标志,并在卡片回复策略中设置 sourceReplyDeliveryMode: "automatic",修复了群聊场景下 AI Card 内容为空并回退至 Markdown 的问题(Issue 555)。

  • 核心修复(card-service.ts:创建卡片成功后立即调用 putAICardStreamingField,打开 DingTalk 服务端的流式生命周期;kick 成功后更新状态为 INPUTING、将 streamLifecycleOpened 写入持久化文件,保证崩溃恢复时能正确关闭生命周期。
  • card-callback-service.ts:移除了卡片变量更新请求中的私有数据标志,避免群聊共享卡片更新时触发 DingTalk 400 错误;单元测试已同步验证新的 cardUpdateOptions 结构。
  • reply-strategy-card.ts 与测试:新增 sourceReplyDeliveryMode: "automatic" 让运行时把最终回复投递到卡片中而非通过 message tool 单独发送;同时新增集成测试覆盖群聊完整生命周期及私有数据回归场景。

Confidence Score: 5/5

修复逻辑清晰,变更范围有限且均有测试覆盖,可以安全合并

三处核心改动均有对应单元或集成测试验证,kick 失败路径被 try-catch 正确处理不影响卡片创建主流程,未发现逻辑错误或数据损坏风险

tests/integration/inbound-group-card-lifecycle.test.ts 中 mock 模拟了 SDK 对 sourceReplyDeliveryMode 的消费逻辑,建议真机验证时重点确认该字段是否被 OpenClaw 运行时正确识别

Important Files Changed

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

Comment thread src/card-service.ts
soimy and others added 2 commits May 8, 2026 19:13
…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 zhumin-zizhu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

仔细 review 过当前 PR HEAD 8b11f54,结论:可以合并。

这个 PR 的核心修复方向是对的:createAICard 成功后恢复一次空 content 的 streaming lifecycle kick,用来显式打开 DingTalk 服务端的 streaming lifecycle。结合 issue #555 的现象,这能解释群聊里卡片创建成功但后续 blockList 变量更新不被接受、最终空卡并 fallback 到 markdown 的链路。kick 成功后也会持久化 streamLifecycleOpenedINPUTING 状态,恢复路径能正确知道后续需要关闭 lifecycle。

我重点看了 createAICardputAICardStreamingFieldcommitAICardBlocks、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 个既有 warnings
  • pnpm run build 通过
  • git diff --check 通过

非阻塞建议:PR 描述里的群聊/私聊真机验证还没勾选,合并后发版前最好补一次群聊真机 smoke,确认 #555 的空卡路径在真实 DingTalk 群空间中已恢复。

@soimy soimy changed the title fix(card): restore streaming lifecycle kick to fix empty cards in group chat fix(reply): keep DingTalk group card finals in card May 13, 2026
@soimy

soimy commented May 13, 2026

Copy link
Copy Markdown
Owner Author

本轮继续排查群聊空卡片问题后的结论和修复如下。

发现

  • messages.groupChat.visibleReplies=message_tool 来自 OpenClaw 的群聊默认策略,会把群聊 source reply delivery 解析为 message_tool_only
  • 这个默认值对普通群聊合理,但和 DingTalk messageType=card 的入站回复策略冲突:插件已经先创建了一张 AI Card 作为可见回复承载面,运行时再把 final answer 导向 message tool,就会导致原卡片保持空 / Done,另一路出现 markdown 或额外卡片消息。
  • 22:26 的真机失败验证还暴露了流程问题:当时只修改了 src,没有先运行 pnpm run build:runtime,OpenClaw gateway 实际加载的是旧的 dist/index.js / Jiti 缓存代码。因此本轮已把“源码改动后必须先 build runtime 再 restart gateway”写入 AGENTS.mdCLAUDE.mddocs/contributor/testing.mdskills/dingtalk-real-device-testing/SKILL.md

修复

  • DingTalk card reply strategy 现在显式传入 sourceReplyDeliveryMode: "automatic",让群聊 final reply 回到当前卡片策略并写入同一张 AI Card,而不是走 message_tool_only
  • 卡片变量更新去掉 updatePrivateDataByKey,避免群聊共享卡片更新带上 private-data 更新参数。
  • 新增 tests/integration/inbound-group-card-lifecycle.test.ts,模拟 visibleReplies=message_tool 下的群聊入站卡片生命周期,覆盖“不走 session webhook fallback,final 写回 card”的回归场景。

真机验证

  • 已执行 pnpm run build:runtime,清理旧 Jiti 缓存后 openclaw gateway restart,并用 openclaw channels status --probe --json 确认 DingTalk channel running=true、account connected=true
  • 22:37:58 群聊入站消息验证通过:日志显示 final payload 进入原 AI Card,deliveredFinalCount=1counts.final=1,并且没有出现 Tracking pending messaging text: tool=message 或 proactive fallback 发送路径。
  • 关键日志观察包括:deliver(final) receivedCalling commitAICardBlocks、final 文本进入 card block/content、card finalized。

本地验证

  • 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 通过。

@lingdle-devops

Copy link
Copy Markdown

@soimy 今天能合并发布新版本吗? 我们自己的项目升级可以安排上?

@soimy

soimy commented May 13, 2026

Copy link
Copy Markdown
Owner Author

争取,计划还有一个pr要合并,再更新下文档

@soimy soimy merged commit 81896e6 into main May 13, 2026
4 checks passed
@hutingjing

Copy link
Copy Markdown

同问今天可以发掉吗😂?群聊用不了蛮影响体验的。

@soimy

soimy commented May 13, 2026

Copy link
Copy Markdown
Owner Author

messages.groupChat.visibleReplies=message_tool 来自 OpenClaw 的群聊默认策略,会把群聊 source reply delivery 解析为 message_tool_only。

着急可以直接改 openclaw.json中的配置

soimy added a commit that referenced this pull request May 15, 2026
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>
soimy added a commit that referenced this pull request May 15, 2026
* 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Development

Successfully merging this pull request may close these issues.

[问题反馈] 升级插件 5.6.2 版本:群聊内容无法正常显示

4 participants