Skip to content

fix(reply): keep DingTalk markdown finals in session replies#565

Merged
soimy merged 1 commit into
mainfrom
hotfix/dingtalk-reply-recovery
May 14, 2026
Merged

fix(reply): keep DingTalk markdown finals in session replies#565
soimy merged 1 commit into
mainfrom
hotfix/dingtalk-reply-recovery

Conversation

@soimy

@soimy soimy commented May 14, 2026

Copy link
Copy Markdown
Owner

关联:#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 回复。

目标

实现

  • src/reply-strategy-markdown.tsgetReplyOptions() 显式返回 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 通过。

@greptile-apps

greptile-apps Bot commented May 14, 2026

Copy link
Copy Markdown

Greptile Summary

此 PR 仅在 reply-strategy-markdown.tsgetReplyOptions() 中新增一行 sourceReplyDeliveryMode: "automatic",并在单测中补充对应断言。该修复确保在群聊场景下(平台默认可能降级为 message_tool_only),markdown/sessionWebhook 策略始终通过自动投递路径发送最终回复,避免因投递模式不匹配导致 final payload 未到达插件侧。

  • 源码变更getReplyOptions() 返回值新增 sourceReplyDeliveryMode: "automatic",附有说明注释,改动聚焦、无副作用。
  • 测试变更:在已有的 getReplyOptions 测试用例中追加 expect(opts.sourceReplyDeliveryMode).toBe("automatic"),覆盖完整。

Confidence Score: 5/5

改动仅添加一个字段到 getReplyOptions 返回值,范围极小,可安全合并。

变更只有两处:在 getReplyOptions() 中新增 sourceReplyDeliveryMode: "automatic",以及对应的单测断言。逻辑正确,不影响任何现有发送路径,测试覆盖到位,且该字段值与 SourceReplyDeliveryMode 类型定义完全匹配。

无需特别关注的文件。

Important Files Changed

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 文案]
Loading

Reviews (3): Last reviewed commit: "fix(reply): keep DingTalk markdown final..." | Re-trigger Greptile

Comment thread src/inbound-handler.ts Outdated
Comment on lines +2017 to +2025
return true;
}
return !currentFinalText?.trim();
}

function isDeliverableBufferedFinal(bufferedFinal: unknown): boolean {
const bufferedFinalPayload =
typeof bufferedFinal === "string"
? ({ text: bufferedFinal } satisfies ReplyStreamPayload)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 媒体 URL 短路条件可能触发重复投递

shouldApplyEmptyFinalRecoveryrecoveryPayload.mediaUrls.length > 0 时直接返回 true,完全忽略 currentFinalText 是否已有内容。若 dispatcher 通过 deliver 回调投递了含 MEDIA: 指令的 block chunk(media 已发送),同时 onPartialReply 也携带同一 MEDIA: 指令更新了 recoveryPayload,而最终未收到 final chunk(deliveredFinalCount=0finalCount=0bufferedFinal 不可投递),则 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.

Comment thread src/inbound-handler.ts Outdated
Comment on lines +85 to +87
const MEDIA_DIRECTIVE_PREFIX = "MEDIA:";
const EMPTY_FINAL_DISPATCH_FALLBACK_TEXT =
"⚠️ OpenClaw 已完成任务,但没有向钉钉插件返回可发送的回复内容。请在 Web 控制台查看结果,或重试本轮消息。";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 fallback 文案在三个源文件中各自重复定义

EMPTY_FINAL_DISPATCH_FALLBACK_TEXTinbound-handler.ts)、EMPTY_FINAL_REPLYreply-strategy-card.ts)、EMPTY_FINAL_FALLBACK_TEXTreply-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.

Comment thread src/send-service.ts
Comment on lines 793 to 813
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,
},
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 终端卡片 + 媒体 + sessionWebhook 路径:媒体被静默丢弃

sendMessage 同时收到终端状态的 card、sessionWebhook 和媒体选项(mediaPath + mediaType)时,新增的 !(options.mediaPath && options.mediaType) 守卫跳过了 sendBySession,但紧接着 returnsendProactiveCardText(仅发文字)。位于第 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.

@soimy soimy force-pushed the hotfix/dingtalk-reply-recovery branch from 4b11a39 to eadd8d8 Compare May 14, 2026 11:56
@soimy soimy changed the title fix(reply): recover empty final dispatch replies fix(reply): keep DingTalk markdown finals in session replies May 14, 2026
@soimy soimy merged commit 934180f into main May 14, 2026
3 checks passed
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

1 participant