fix(reply): keep DingTalk markdown finals in session replies#565
Conversation
Greptile Summary此 PR 仅在
Confidence Score: 5/5改动仅添加一个字段到 getReplyOptions 返回值,范围极小,可安全合并。 变更只有两处:在 getReplyOptions() 中新增 sourceReplyDeliveryMode: "automatic",以及对应的单测断言。逻辑正确,不影响任何现有发送路径,测试覆盖到位,且该字段值与 SourceReplyDeliveryMode 类型定义完全匹配。 无需特别关注的文件。
|
| Filename | Overview |
|---|---|
| src/reply-strategy-markdown.ts | 在 getReplyOptions() 中新增 sourceReplyDeliveryMode: "automatic",确保 group chat 场景下 markdown 策略不被降级为 message-tool-only 投递 |
| tests/unit/reply-strategy-markdown.test.ts | 新增对 sourceReplyDeliveryMode === "automatic" 的断言,与源码变更完全对齐 |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[inbound-handler 收到消息] --> B{创建 ReplyStrategy}
B -->|messageType=markdown| C[createMarkdownReplyStrategy]
C --> D["getReplyOptions 返回\ndisableBlockStreaming + sourceReplyDeliveryMode: automatic"]
D --> E{群聊 or 直聊}
E -->|群聊平台默认 message_tool_only| F[automatic 覆盖平台默认值]
E -->|直聊| G[保持 automatic 不变]
F --> H[dispatcher 投递 final payload]
G --> H
H -->|deliveredFinalCount=0| I[empty final recovery]
H -->|deliveredFinalCount GT 0| J[正常结束]
I --> K{recoveryPayload 可用}
K -->|是| L[deliver recoveryPayload]
K -->|否| M[发送固定 fallback 文案]
Reviews (3): Last reviewed commit: "fix(reply): keep DingTalk markdown final..." | Re-trigger Greptile
| return true; | ||
| } | ||
| return !currentFinalText?.trim(); | ||
| } | ||
|
|
||
| function isDeliverableBufferedFinal(bufferedFinal: unknown): boolean { | ||
| const bufferedFinalPayload = | ||
| typeof bufferedFinal === "string" | ||
| ? ({ text: bufferedFinal } satisfies ReplyStreamPayload) |
There was a problem hiding this comment.
shouldApplyEmptyFinalRecovery 在 recoveryPayload.mediaUrls.length > 0 时直接返回 true,完全忽略 currentFinalText 是否已有内容。若 dispatcher 通过 deliver 回调投递了含 MEDIA: 指令的 block chunk(media 已发送),同时 onPartialReply 也携带同一 MEDIA: 指令更新了 recoveryPayload,而最终未收到 final chunk(deliveredFinalCount=0、finalCount=0、bufferedFinal 不可投递),则 isEmptyFinalDispatch 仍为 true,recovery 会再次调用 strategy.deliver(recoveryPayload),导致同一张图片被发送两次。可以在条件中同时检查 currentFinalText,或将"recovery 仅在没有任何 deliver 调用时才触发"的语义也反映到 deliveredFinalCount 的统计范围中(目前只统计 kind==="final" 的投递,block 投递不计入)。
Rule Used: What: All code review comments and feedback must b... (source)
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/inbound-handler.ts
Line: 2017-2025
Comment:
**媒体 URL 短路条件可能触发重复投递**
`shouldApplyEmptyFinalRecovery` 在 `recoveryPayload.mediaUrls.length > 0` 时直接返回 `true`,完全忽略 `currentFinalText` 是否已有内容。若 dispatcher 通过 `deliver` 回调投递了含 `MEDIA:` 指令的 block chunk(media 已发送),同时 `onPartialReply` 也携带同一 `MEDIA:` 指令更新了 `recoveryPayload`,而最终未收到 final chunk(`deliveredFinalCount=0`、`finalCount=0`、`bufferedFinal` 不可投递),则 `isEmptyFinalDispatch` 仍为 `true`,recovery 会再次调用 `strategy.deliver(recoveryPayload)`,导致同一张图片被发送两次。可以在条件中同时检查 `currentFinalText`,或将"recovery 仅在没有任何 deliver 调用时才触发"的语义也反映到 `deliveredFinalCount` 的统计范围中(目前只统计 `kind==="final"` 的投递,block 投递不计入)。
**Rule Used:** What: All code review comments and feedback must b... ([source](https://app.greptile.com/review/custom-context?memory=af4da0ce-8d7c-48c9-a88a-82361a98dddf))
How can I resolve this? If you propose a fix, please make it concise.| const MEDIA_DIRECTIVE_PREFIX = "MEDIA:"; | ||
| const EMPTY_FINAL_DISPATCH_FALLBACK_TEXT = | ||
| "⚠️ OpenClaw 已完成任务,但没有向钉钉插件返回可发送的回复内容。请在 Web 控制台查看结果,或重试本轮消息。"; |
There was a problem hiding this comment.
EMPTY_FINAL_DISPATCH_FALLBACK_TEXT(inbound-handler.ts)、EMPTY_FINAL_REPLY(reply-strategy-card.ts)、EMPTY_FINAL_FALLBACK_TEXT(reply-strategy-markdown.ts)均为同一段中文字符串,连同 4 个测试文件中的本地副本,共计 7 处定义。若需调整措辞,需逐一修改所有位置,容易遗漏导致不一致。建议将该常量提取到共享模块(如 reply-strategy-types.ts 或专门的常量文件)统一导出,各文件 import 使用。
Rule Used: What: All code review comments and feedback must b... (source)
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/inbound-handler.ts
Line: 85-87
Comment:
**fallback 文案在三个源文件中各自重复定义**
`EMPTY_FINAL_DISPATCH_FALLBACK_TEXT`(`inbound-handler.ts`)、`EMPTY_FINAL_REPLY`(`reply-strategy-card.ts`)、`EMPTY_FINAL_FALLBACK_TEXT`(`reply-strategy-markdown.ts`)均为同一段中文字符串,连同 4 个测试文件中的本地副本,共计 7 处定义。若需调整措辞,需逐一修改所有位置,容易遗漏导致不一致。建议将该常量提取到共享模块(如 `reply-strategy-types.ts` 或专门的常量文件)统一导出,各文件 import 使用。
**Rule Used:** What: All code review comments and feedback must b... ([source](https://app.greptile.com/review/custom-context?memory=af4da0ce-8d7c-48c9-a88a-82361a98dddf))
How can I resolve this? If you propose a fix, please make it concise.| if (messageType === "card" && options.card && !options.forceMarkdown) { | ||
| const card = options.card; | ||
| if (isCardInTerminalState(card.state)) { | ||
| if (options.sessionWebhook) { | ||
| if (options.sessionWebhook && !(options.mediaPath && options.mediaType)) { | ||
| await sendBySession(config, options.sessionWebhook, text, options); | ||
| return { ok: true }; | ||
| } | ||
|
|
||
| const proactiveResult = await sendProactiveCardText(config, conversationId, text, log); | ||
| if (!proactiveResult.ok) { | ||
| return { ok: false, error: proactiveResult.error || "Card send failed" }; | ||
| } | ||
| return { | ||
| ok: true, | ||
| tracking: { | ||
| processQueryKey: proactiveResult.processQueryKey, | ||
| outTrackId: proactiveResult.outTrackId, | ||
| cardInstanceId: proactiveResult.cardInstanceId, | ||
| }, | ||
| }; | ||
| } |
There was a problem hiding this comment.
终端卡片 + 媒体 + sessionWebhook 路径:媒体被静默丢弃
当 sendMessage 同时收到终端状态的 card、sessionWebhook 和媒体选项(mediaPath + mediaType)时,新增的 !(options.mediaPath && options.mediaType) 守卫跳过了 sendBySession,但紧接着 return 于 sendProactiveCardText(仅发文字)。位于第 816 行的专用媒体路由块因此永远不会被触达——媒体被静默丢弃。修改前该路径会走入 sendBySession 执行原生媒体上传并发送;修改后退化为纯文字接口,媒体附件完全丢失且不报错。可在 sendProactiveCardText 之前添加与非卡片路径对称的媒体守卫,或将终端卡片 + 媒体的情况明确重定向至 sendProactiveMedia。
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/send-service.ts
Line: 793-813
Comment:
**终端卡片 + 媒体 + sessionWebhook 路径:媒体被静默丢弃**
当 `sendMessage` 同时收到终端状态的 card、`sessionWebhook` 和媒体选项(`mediaPath` + `mediaType`)时,新增的 `!(options.mediaPath && options.mediaType)` 守卫跳过了 `sendBySession`,但紧接着 `return` 于 `sendProactiveCardText`(仅发文字)。位于第 816 行的专用媒体路由块因此永远不会被触达——媒体被静默丢弃。修改前该路径会走入 `sendBySession` 执行原生媒体上传并发送;修改后退化为纯文字接口,媒体附件完全丢失且不报错。可在 `sendProactiveCardText` 之前添加与非卡片路径对称的媒体守卫,或将终端卡片 + 媒体的情况明确重定向至 `sendProactiveMedia`。
How can I resolve this? If you propose a fix, please make it concise.4b11a39 to
eadd8d8
Compare
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>
关联:#556、#563
背景
OpenClaw 2026.5.7 之后,群聊默认/显式
messages.groupChat.visibleReplies=message_tool会让 source reply delivery 被解析成message_tool_only。这对普通群聊消息是合理默认,但 DingTalk 插件的 markdown/sessionWebhook strategy 本身就是可见回复承载面;如果不覆盖该模式,上游 final reply 会被导向 message tool 路径,插件侧收不到 final,自然也不会进入 sessionWebhook 文本/markdown 回复链路。PR #557 已经用同类方法修复了 card strategy:card mode 在
getReplyOptions()中声明sourceReplyDeliveryMode: "automatic",让 final 回到卡片策略。本 PR 将同样的策略应用到 markdown/sessionWebhook 回复。目标
messages.groupChat.visibleReplies=message_tool下,DingTalk markdown/sessionWebhook 回复仍能收到 runtime final。openclaw.json行为,不新增配置项。实现
src/reply-strategy-markdown.ts:getReplyOptions()显式返回sourceReplyDeliveryMode: "automatic"。tests/unit/reply-strategy-markdown.test.ts:补充断言,防止 markdown strategy 再次遗漏该覆盖。实现 TODO
验证 TODO
pnpm vitest run tests/unit/reply-strategy-markdown.test.ts -t "getReplyOptions enables block streaming"先失败后通过。pnpm vitest run tests/unit/reply-strategy-markdown.test.ts tests/unit/inbound-handler.test.ts tests/unit/inbound-handler-media.test.ts tests/unit/reply-strategy-card.test.ts通过,4 files / 131 tests。pnpm type-check通过。pnpm lint通过,0 errors,仍有既有 warnings。git diff --check通过。