Skip to content

fix(dingtalk): skip uppercase webhook reaction targets - #5466

Merged
wenshao merged 1 commit into
QwenLM:mainfrom
tt-a1i:fix/dingtalk-uppercase-webhook-reactions
Jun 20, 2026
Merged

fix(dingtalk): skip uppercase webhook reaction targets#5466
wenshao merged 1 commit into
QwenLM:mainfrom
tt-a1i:fix/dingtalk-uppercase-webhook-reactions

Conversation

@tt-a1i

@tt-a1i tt-a1i commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

What this PR does

  • Treats uppercase HTTP:// and HTTPS:// DingTalk webhook URLs as webhook fallback targets instead of conversation IDs.
  • Adds prompt reaction regression tests for uppercase webhook handling on both prompt start and prompt end.
  • Adds DingTalk package test and test:ci scripts so the adapter tests are included in the package test workflow.

Why it's needed

The DingTalk adapter skips prompt reactions for webhook fallback targets, but the old startsWith('http') check only handled lowercase schemes. An uppercase webhook URL could be misclassified as a conversation ID and then used as openConversationId in DingTalk emotion API calls.

Reviewer Test Plan

How to verify

  • Confirm that onPromptStart and onPromptEnd skip uppercase webhook URLs such as HTTPS://oapi.dingtalk.com/robot/send?....
  • Confirm that normal DingTalk conversation IDs still send reactions.
  • Run npm --workspace packages/channels/dingtalk run test.
  • Run npm --workspace packages/channels/dingtalk run test:ci.
  • Run npm --workspace packages/channels/dingtalk run build.
  • Run npx prettier --check packages/channels/dingtalk/package.json packages/channels/dingtalk/src/DingtalkAdapter.ts packages/channels/dingtalk/src/DingtalkAdapter.test.ts.
  • Run npx eslint packages/channels/dingtalk/src/DingtalkAdapter.ts packages/channels/dingtalk/src/DingtalkAdapter.test.ts.

Evidence (Before & After)

Before: onPromptStart('HTTPS://...') and onPromptEnd('HTTPS://...') could send DingTalk emotion reply/recall requests with the webhook URL incorrectly placed in openConversationId. After: uppercase webhook URLs are detected as webhook targets and skipped; real conversation IDs still send reactions. The new tests are revert-proof because the uppercase webhook cases fail against the old lowercase-only check.

Tested on

OS Status
🍏 macOS ✅ tested
🪟 Windows ⚠️ not tested locally; CI passed
🐧 Linux ⚠️ not tested locally; CI passed

Environment (optional)

Local DingTalk package test/build/lint/prettier checks on macOS.

Risk & Scope

  • Main risk or tradeoff: the check now only skips real http(s):// webhook URLs, so a literal conversation ID beginning with http but not a URL would no longer be skipped.
  • Not validated / out of scope: live DingTalk API calls against a real bot; the regression is covered at the adapter boundary.
  • Breaking changes / migration notes: none.

Linked Issues

Fixes #5465

中文说明

What this PR does

  • 将大写 HTTP://HTTPS:// DingTalk webhook URL 识别为 webhook fallback 目标,而不是会话 ID。
  • 为 prompt start 和 prompt end 两条路径增加大写 webhook 的 reaction 回归测试。
  • 给 DingTalk package 增加 testtest:ci 脚本,让 adapter 测试进入 package 测试流程。

Why it's needed

DingTalk adapter 会跳过 webhook fallback 目标的 prompt reaction,但旧的 startsWith('http') 只覆盖小写协议。大写 webhook URL 可能被误判成会话 ID,然后作为 openConversationId 传给 DingTalk emotion API。

Reviewer Test Plan

How to verify

  • 确认 onPromptStartonPromptEnd 会跳过 HTTPS://oapi.dingtalk.com/robot/send?... 这类大写 webhook URL。
  • 确认普通 DingTalk conversation ID 仍然会发送 reaction。
  • 运行 npm --workspace packages/channels/dingtalk run test
  • 运行 npm --workspace packages/channels/dingtalk run test:ci
  • 运行 npm --workspace packages/channels/dingtalk run build
  • 运行上面英文部分列出的 prettier 和 eslint 命令。

Evidence (Before & After)

修复前:onPromptStart('HTTPS://...')onPromptEnd('HTTPS://...') 可能发出 DingTalk emotion reply/recall 请求,并把 webhook URL 错误放进 openConversationId。修复后:大写 webhook URL 会被识别为 webhook 目标并跳过;真实 conversation ID 仍然会发送 reaction。新增测试是 revert-proof 的,因为大写 webhook 用例在旧的小写限定检查上会失败。

Tested on

OS Status
🍏 macOS ✅ tested
🪟 Windows ⚠️ not tested locally; CI passed
🐧 Linux ⚠️ not tested locally; CI passed

Environment (optional)

在 macOS 上运行了 DingTalk package 的 test/build/lint/prettier 检查。

Risk & Scope

  • 主要风险或取舍:现在只会跳过真正的 http(s):// webhook URL,所以字面以 http 开头但不是 URL 的 conversation ID 不再被跳过。
  • 未验证 / 不在范围内:没有对真实 bot 做 live DingTalk API 调用;回归已在 adapter 边界覆盖。
  • Breaking changes / migration notes:无。

Linked Issues

Fixes #5465

AI Assistance Disclosure

I used Codex to review the changes, sanity-check the implementation against existing patterns, and help spot potential edge cases.

@wenshao wenshao 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.

⚠️ Downgraded from Approve to Comment: CI still running.

The regex fix itself is correct — /^https?:\/\//i properly handles case-insensitive URL detection where the old startsWith('http') missed uppercase webhook URLs. The negation is preserved, and the regex is anchored and ReDoS-safe.

Test coverage notes: the new tests cover uppercase HTTPS URLs and conversation IDs on both prompt start/end, but don't cover the most common lowercase https:// case or the HTTP:// (no TLS) branch. Worth adding as follow-up.


const { DingtalkChannel } = await import('./DingtalkAdapter.js');

function createChannel(): DingtalkChannel {

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.

[Suggestion] TypeScript errors in test file: DingtalkChannel is a value-import via await import() but used as a type annotation on lines 33 and 52 (TS2749). The config object on line 36 is also not assignable to ChannelConfig (TS2345). These don't block the build (tsconfig excludes src/**/*.test.ts), but they will show as red squiggles in editors.

Suggested change
function createChannel(): DingtalkChannel {
import type { DingtalkChannel } from './DingtalkAdapter.js';

— DeepSeek/deepseek-v4-pro via Qwen Code /review

groupPolicy: 'open',
groups: {},
},
{} as never,

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.

[Suggestion] {} as never is semantically misleading — never is TypeScript's uninhabited bottom type asserting the value can never exist, yet {} is a real runtime object. The rest of the test file uses the as unknown as Target double-cast pattern for escaping private access. Consider using that pattern here for consistency:

Suggested change
{} as never,
{} as unknown as AcpBridge,

— DeepSeek/deepseek-v4-pro via Qwen Code /review

"build": "tsc --build"
"build": "tsc --build",
"test": "vitest run",
"test:ci": "vitest run"

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.

[Suggestion] "test:ci": "vitest run" is identical to "test" on the line above. No other channel package (qqbot, weixin, feishu, telegram) has a separate test:ci script. Unless a CI pipeline specifically invokes test:ci and expects it to differ from test, this adds no value and creates confusion about which to use.

— DeepSeek/deepseek-v4-pro via Qwen Code /review

@wenshao

wenshao commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator

✅ Local end-to-end verification (real fetch boundary, tmux)

Verified by driving the real built DingtalkAdapter (onPromptStart / onPromptEndattachReaction/recallReactionemotionApi) and intercepting the real global.fetch the emotion API uses — no mocking of the fix's own methods. Only the two external deps the fix doesn't touch (the DingTalk stream SDK and ChannelBase) are stubbed, exactly as the PR's own test does. Both sides built to real dist:

  • BASE = merge-base 5924ab3a (!chatId.startsWith('http'))
  • PR = head 75bfcd13 (!/^https?:\/\//i.test(chatId))

A reaction "going out" == a POST https://api.dingtalk.com/v1.0/robot/emotion/{reply|recall}.

A/B result (real built dist, real fetch)

Probe BASE 5924ab3a PR 75bfcd13
onPromptStart('HTTPS://…/robot/send?…') POST emotion/reply, openConversationId="HTTPS://…webhook" ✅ no request (skipped)
onPromptEnd('HTTPS://…/robot/send?…') POST emotion/recall, openConversationId="HTTPS://…webhook" ✅ no request (skipped)
onPromptStart('cid-123') — control ✅ POST emotion/reply, openConversationId="cid-123" ✅ POST emotion/reply, openConversationId="cid-123"
onPromptStart('https://…') lowercase — control ✅ skipped ✅ skipped
========== BASE (5924ab3a) ==========
  ✅ onPromptStart  UPPERCASE webhook   fetch -> emotion/reply   openConversationId="HTTPS://oapi.dingtalk.com/robot/send?access_token=token"
  ✅ onPromptEnd    UPPERCASE webhook   fetch -> emotion/recall  openConversationId="HTTPS://oapi.dingtalk.com/robot/send?access_token=token"
  ✅ onPromptStart  real conversationId fetch -> emotion/reply   openConversationId="cid-123"
  ✅ onPromptStart  lowercase webhook   NO fetch (reaction skipped)
========== PR (75bfcd13) ==========
  ✅ onPromptStart  UPPERCASE webhook   NO fetch (reaction skipped)
  ✅ onPromptEnd    UPPERCASE webhook   NO fetch (reaction skipped)
  ✅ onPromptStart  real conversationId fetch -> emotion/reply   openConversationId="cid-123"
  ✅ onPromptStart  lowercase webhook   NO fetch (reaction skipped)

So on BASE an uppercase webhook URL is mis-classified as a conversation ID and the bot fires a malformed emotion reply/recall POST with the webhook URL stuffed into openConversationId; on PR it is correctly treated as a webhook fallback and skipped. Real conversation IDs still react on both builds (no regression), and lowercase webhooks were already skipped on both (confirming the bug is specifically case-sensitivity).

Unit test + revert-proof + static checks

  • vitest run on the PR source → 3/3 pass (DingtalkAdapter.test.ts).
  • Revert-proof: running the PR's new test file against the base source makes the two uppercase tests fail (skips uppercase webhook URLs when starting/ending a prompt) while the conversation-ID control passes — they are genuine regression tests, not always-green.
  • New test / test:ci scripts run the package suite: 29/29 pass (pre-existing markdown.test.ts 26 + new DingtalkAdapter.test.ts 3), wiring the adapter tests into workspace CI.
  • ESLint clean (exit 0); Prettier clean on all 3 files; tsc --build clean.

Note

The new regex also tightens the non-URL branch: the old startsWith('http') would mis-skip a conversation ID literally beginning with http (e.g. httpfoo), whereas /^https?:\/\//i only matches real http(s):// schemes. This is a small correctness improvement with no downside for real DingTalk conversation IDs.

Verdict: LGTM — the fix resolves the uppercase-webhook reaction bug end-to-end with no regression.

🇨🇳 中文版本(点击展开)

✅ 本地端到端验证(真实 fetch 边界,tmux)

通过驱动真实编译产物中的 DingtalkAdapteronPromptStart / onPromptEndattachReaction/recallReactionemotionApi),并在 emotion API 实际使用的真实 global.fetch 处拦截来验证——没有 mock 修复本身的任何方法。仅对修复未触及的两个外部依赖(DingTalk stream SDK 和 ChannelBase)做桩,方式与 PR 自带测试一致。两个版本都编译为真实 dist

  • BASE = merge-base 5924ab3a!chatId.startsWith('http')
  • PR = head 75bfcd13!/^https?:\/\//i.test(chatId)

发出一个 reaction == 一次 POST https://api.dingtalk.com/v1.0/robot/emotion/{reply|recall}

A/B 结果(真实 dist + 真实 fetch)

探针 BASE 5924ab3a PR 75bfcd13
onPromptStart('HTTPS://…/robot/send?…') POST emotion/replyopenConversationId="HTTPS://…webhook" ✅ 无请求(跳过)
onPromptEnd('HTTPS://…/robot/send?…') POST emotion/recallopenConversationId="HTTPS://…webhook" ✅ 无请求(跳过)
onPromptStart('cid-123') —— 对照 ✅ POST,openConversationId="cid-123" ✅ POST,openConversationId="cid-123"
onPromptStart('https://…') 小写 —— 对照 ✅ 跳过 ✅ 跳过

也就是说:在 BASE 上,大写的 webhook URL 被误判为会话 ID,机器人会带着被塞进 openConversationId 的 webhook URL 发出错误的 emotion reply/recall 请求;在 PR 上则正确识别为 webhook 兜底并跳过。真实会话 ID 在两个版本都正常加 reaction(无回归),小写 webhook 在两个版本本来就会跳过(说明该 bug 正是大小写敏感导致的)。

单元测试 + 反向验证 + 静态检查

  • 在 PR 源码上 vitest run3/3 通过DingtalkAdapter.test.ts)。
  • 反向验证:把 PR 新增的测试文件放到 base 源码上运行,两个大写用例失败skips uppercase webhook URLs when starting/ending a prompt),而会话 ID 对照用例通过——证明它们是真正的回归测试,而非恒绿。
  • 新增的 test / test:ci 脚本会运行整个包的测试套件:29/29 通过(既有的 markdown.test.ts 26 个 + 新增 DingtalkAdapter.test.ts 3 个),将适配器测试接入 workspace CI。
  • ESLint 通过(exit 0);Prettier 三个文件均通过;tsc --build 通过。

补充说明

新正则也顺带收紧了非 URL 分支:旧的 startsWith('http') 会把字面以 http 开头的会话 ID(例如 httpfoo)误跳过,而 /^https?:\/\//i 只匹配真正的 http(s):// 协议。这是一处小的正确性改进,对真实 DingTalk 会话 ID 没有任何副作用。

结论:LGTM —— 修复在端到端层面解决了大写 webhook 的 reaction 误触发问题,且无回归。

@tt-a1i
tt-a1i marked this pull request as ready for review June 20, 2026 11:57
@wenshao

wenshao commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot qwen-code-ci-bot 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.

Hey @tt-a1i, thanks for the fix!

The PR body doesn't follow our PR template. Could you update it to match? Specifically:

  • Missing sections: ## What this PR does, ## Why it's needed, ## Risk & Scope, ## Linked Issues, and the ## Reviewer Test Plan subsections (### How to verify, ### Evidence (Before & After), ### Tested on)
  • Current headings (## Summary, ## Test Plan) don't map to the template structure
  • Missing Chinese translation in a <details> block

The actual content looks reasonable — it's mostly a restructuring exercise. Once the template is filled in, happy to re-run triage.

中文说明

@tt-a1i,感谢修复!

PR 正文没有按照我们的 PR 模板 填写。能否更新一下使其匹配?具体来说:

  • 缺少的章节: ## What this PR does## Why it's needed## Risk & Scope## Linked Issues,以及 ## Reviewer Test Plan 的子章节(### How to verify### Evidence (Before & After)### Tested on
  • 当前的标题## Summary## Test Plan)与模板结构不匹配
  • 缺少 <details> 中的中文翻译

内容本身看起来合理——主要是重新按模板组织一下。模板填好后可以重新跑 triage。

Qwen Code · qwen3.7-max

@wenshao wenshao 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.

No issues found. LGTM! ✅

— DeepSeek/deepseek-v4-pro via Qwen Code /review

@wenshao

wenshao commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator

✅ Maintainer verification — code is correct & verified

Built and ran the real test suite locally (under tmux) against the PR head 75bfcd13d. The fix is correct, the new tests genuinely guard the bug (verified by toggling the fix off), and the full stated test plan reproduces green. This also confirms the earlier "CI still running" caveat — CI is now fully green.

Environment

  • Dedicated worktree off PR head 75bfcd13d (clean — source matches the PR exactly, no local edits)
  • Node v22.22.2, npm 10.9.7
  • Real npm ci (no symlinked node_modules) → exit 0 in 2m17s

Results — full PR test plan reproduced

Step Command Result
Unit tests npm --workspace packages/channels/dingtalk run test:ci ✅ 29 pass / 0 fail (2 files)
Build npm --workspace packages/channels/dingtalk run build ✅ exit 0
Prettier prettier --check package.json DingtalkAdapter.{ts,test.ts} ✅ exit 0
ESLint eslint DingtalkAdapter.ts DingtalkAdapter.test.ts ✅ exit 0
Whitespace git diff --check ✅ exit 0

(29 = 3 new DingtalkAdapter.test.ts cases + 26 pre-existing markdown.test.ts cases — see note below.)

A/B proof that the tests actually guard the bug

To confirm the new tests are not no-ops, I reverted only isConversationId back to the original !chatId.startsWith('http') and re-ran:

FAIL  DingtalkAdapter.test.ts > skips uppercase webhook URLs when starting a prompt
FAIL  DingtalkAdapter.test.ts > skips uppercase webhook URLs when ending a prompt
  AssertionError: expected "spy" to not be called at all, but actually been called 1 times
  Tests  2 failed | 27 passed (29)

Restoring the fix → back to green (29 pass). So both uppercase-webhook cases fail without the fix and pass with it, while the conversation-ID case (and the 26 markdown tests) pass either way — the tests isolate exactly the fixed behavior. ✔

Root cause & fix (confirmed in source)

onPromptStart / onPromptEnd gate reaction calls on isConversationId(chatId), where chatId = conversationId || sessionWebhook. With the old case-sensitive startsWith('http'), an uppercase webhook URL (HTTPS://oapi.dingtalk.com/robot/send?...) was misclassified as a conversation ID, so the adapter fired attachReaction / recallReaction against a webhook target instead of skipping it. The new /^https?:\/\//i check is case-insensitive, anchored, and ReDoS-safe; the negation is preserved.

Code assessment

  • Minimal one-line fix; matches the case-insensitive ^https?:// convention used elsewhere in the repo.
  • Bonus: the added test / test:ci scripts don't just run the new file — they also pull the pre-existing markdown.test.ts (26 tests) into CI, which previously had no test script to run them.
  • Test-script wiring (root-hoisted vitest, no local dep) is consistent with the sibling channels/qqbot / channels/telegram packages. All three OS Test jobs are green on CI.

Note on the CHANGES_REQUESTED status

The only "changes requested" review is from qwen-code-ci-bot, and it is purely about PR-template section headings — no code change is requested. CI is otherwise fully green (Classify, CodeQL, Lint, Test on ubuntu/macos/windows · Node 22.x). Mergeable from a correctness standpoint — only the PR-body formatting is outstanding, your call.


🇨🇳 中文版(点击展开)

✅ 维护者验证 — 代码正确且已验证

已在本地(tmux 下)针对 PR head 75bfcd13d 构建并运行了真实测试套件。修复正确;新增测试确实能拦住该 bug(通过关掉修复来验证);PR 描述里的整套 test plan 均复现为绿。此前「CI 还在跑」的顾虑现也已确认——CI 现已全绿。

环境

  • 基于 PR head 75bfcd13d 的独立 worktree(干净,源码与 PR 完全一致,无本地改动)
  • Node v22.22.2,npm 10.9.7
  • 真实 npm ci(非软链 node_modules)→ 退出码 0,用时 2m17s

结果 — 完整复现 PR test plan

步骤 命令 结果
单元测试 npm --workspace packages/channels/dingtalk run test:ci ✅ 29 通过 / 0 失败(2 个文件)
构建 npm --workspace packages/channels/dingtalk run build ✅ 退出码 0
Prettier prettier --check package.json DingtalkAdapter.{ts,test.ts} ✅ 退出码 0
ESLint eslint DingtalkAdapter.ts DingtalkAdapter.test.ts ✅ 退出码 0
空白检查 git diff --check ✅ 退出码 0

(29 = 3 个新增 DingtalkAdapter.test.ts 用例 + 26 个既有 markdown.test.ts 用例,见下方说明。)

A/B 验证(证明测试确实能拦住该 bug)

为确认新测试不是空跑,我isConversationId 改回原来的 !chatId.startsWith('http') 后重跑:两个「大写 webhook URL」用例失败(expected "spy" to not be called at all, but actually been called 1 times),共 2 失败 / 27 通过;恢复修复后重新变绿(29 通过)。即两个大写用例没有修复时失败、有修复时通过,而 conversation-ID 用例(及 26 个 markdown 用例)两种情况都通过——测试精准隔离了被修复的行为。

根因与修复(已在源码确认)

onPromptStart / onPromptEnd 通过 isConversationId(chatId) 来决定是否打 reaction,其中 chatId = conversationId || sessionWebhook。旧的大小写敏感 startsWith('http') 会把大写 webhook URL(HTTPS://oapi.dingtalk.com/robot/send?...)误判为 conversation ID,于是对一个 webhook 目标错误地调用了 attachReaction / recallReaction,而非跳过。新的 /^https?:\/\//i 大小写不敏感、有锚定、且 ReDoS 安全;取反逻辑保持不变。

代码评估

  • 单行最小修复,符合仓库中 ^https?:// 大小写不敏感的惯例。
  • 额外收益: 新增的 test / test:ci 脚本不只跑新文件,还把既有的 markdown.test.ts(26 个用例)纳入了 CI——此前该包没有 test 脚本来运行它们。
  • 测试脚本接线(根部 hoisted 的 vitest,本地不声明)与兄弟包 channels/qqbot / channels/telegram 一致;CI 上三大 OS 的 Test 任务均为绿。

关于 CHANGES_REQUESTED 状态

唯一一条「requested changes」来自 qwen-code-ci-bot,且仅针对 PR 模板的小标题——并未要求任何代码改动。其余 CI 全绿(Classify、CodeQL、Lint、ubuntu/macos/windows · Node 22.x 测试)。从正确性角度可以合并——仅剩 PR 描述格式待定,由你决定。

@tt-a1i

tt-a1i commented Jun 20, 2026

Copy link
Copy Markdown
Contributor Author

updated the PR description to match the template. thanks.

@wenshao

wenshao commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR, @tt-a1i!

Template looks good ✓

On direction: this is a clean correctness fix for a real bug. The DingTalk adapter's isConversationId used a case-sensitive startsWith('http') check, so uppercase webhook URLs (HTTPS://...) were misclassified as conversation IDs and sent to the DingTalk emotion API as openConversationId. That produces malformed API calls — worth fixing. Fixes #5465.

On approach: the one-line fix (/^https?:\/\//i) is exactly the right call — it's the same case-insensitive URL-scheme regex pattern used in 16+ other places across the codebase (proxyUtils, web-fetch, AuthMessageHandler, mcp/add, etc). The three new tests cover the right cases (uppercase skip on start/end, conversation ID still works), and they're genuinely revert-proof: I verified the two uppercase tests fail without the fix and pass with it. Adding test/test:ci scripts to the dingtalk package also brings the pre-existing 26 markdown tests into CI — a nice bonus.

Scope is minimal: one regex fix, three focused tests, two script additions. No drive-by changes. Moving on to code review. 🔍

中文说明

感谢 @tt-a1i 的 PR!

模板完整 ✓

方向:这是一个针对真实 bug 的正确性修复。DingTalk adapter 的 isConversationId 使用了大小写敏感的 startsWith('http') 检查,导致大写 webhook URL(HTTPS://...)被误判为会话 ID,并作为 openConversationId 发送到 DingTalk emotion API,产生错误的 API 调用——值得修复。Fixes #5465

方案:单行修复(/^https?:\/\//i)完全正确——与代码库中 16+ 处使用的大小写不敏感 URL scheme 正则一致(proxyUtils、web-fetch、AuthMessageHandler、mcp/add 等)。三个新测试覆盖了正确的场景(start/end 路径的大写跳过、会话 ID 仍然有效),且确认是 revert-proof 的:去掉修复后两个大写用例失败,恢复后通过。给 dingtalk 包增加 test/test:ci 脚本还顺带将既有的 26 个 markdown 测试接入了 CI。

范围最小化:一个正则修复、三个针对性测试、两个脚本新增,没有夹带无关改动。进入代码审查 🔍

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code Review

Independent proposal: Before reading the diff, I'd replace startsWith('http') with /^https?:\/\//i — anchored, scheme-specific, case-insensitive, and ReDoS-safe (no unbounded quantifiers). This is exactly what the PR does.

Diff assessment: The PR matches the independent proposal. Three files, all necessary:

  • DingtalkAdapter.ts: one-line regex swap, correct semantics. The i flag handles all case permutations. Anchored at ^ so only scheme position matters.
  • DingtalkAdapter.test.ts: three tests at the adapter boundary — uppercase webhook skip on onPromptStart, uppercase webhook skip on onPromptEnd, conversation ID still sends reaction. Mocks are scoped to external deps only (dingtalk SDK, channel-base), not the code under test.
  • package.json: test/test:ci scripts follow the channels/qqbot convention.

No correctness bugs, no security concerns, no AGENTS.md violations. The regex pattern is ReDoS-safe (no nested quantifiers or alternation blowup).

Test Results

All checks pass locally on the PR head (75bfcd13d):

$ npm --workspace packages/channels/dingtalk run test:ci

 RUN  v3.2.4

 ✓ src/markdown.test.ts (26 tests) 18ms
 ✓ src/DingtalkAdapter.test.ts (3 tests) 10ms

 Test Files  2 passed (2)
      Tests  29 passed (29)
   Duration  411ms
$ npx prettier --check packages/channels/dingtalk/{package.json,src/DingtalkAdapter.ts,src/DingtalkAdapter.test.ts}
Checking formatting...
All matched files use Prettier code style!

$ npx eslint packages/channels/dingtalk/src/DingtalkAdapter.ts packages/channels/dingtalk/src/DingtalkAdapter.test.ts
(clean — exit 0)

$ npm --workspace packages/channels/dingtalk run build
(clean — exit 0)

Revert-proof verification — reverting only isConversationId back to !chatId.startsWith('http'):

 FAIL  src/DingtalkAdapter.test.ts
  ❯ skips uppercase webhook URLs when starting a prompt
  ❯ skips uppercase webhook URLs when ending a prompt
    AssertionError: expected "spy" to not be called at all, but actually been called 1 times
  Tests  2 failed | 27 passed (29)

The two uppercase-webhook tests fail without the fix and pass with it. The conversation-ID control passes either way. These are genuine regression guards.

CI (all green): Classify ✓ · CodeQL ✓ · Lint ✓ · Test (ubuntu/macos/windows · Node 22.x) ✓

Note on tmux testing: This fix targets an internal adapter path (DingTalk webhook → emotion API routing), not user-facing CLI behavior. It cannot be meaningfully driven through qwen -p "..." in a terminal. Unit tests at the adapter boundary are the correct verification approach here.

中文说明

代码审查

独立方案: 在读 diff 之前,我会把 startsWith('http') 替换为 /^https?:\/\//i——有锚定、限定 scheme、大小写不敏感、且 ReDoS 安全(无无限量词)。PR 的做法与此完全一致。

Diff 评估: PR 与独立方案吻合。三个文件均必要:

  • DingtalkAdapter.ts:单行正则替换,语义正确。i flag 覆盖所有大小写组合。^ 锚定只匹配 scheme 位置。
  • DingtalkAdapter.test.ts:三个 adapter 边界测试——onPromptStart/onPromptEnd 大写 webhook 跳过、会话 ID 仍然发送 reaction。mock 仅限于外部依赖(dingtalk SDK、channel-base),不 mock 被测代码。
  • package.jsontest/test:ci 脚本与 channels/qqbot 惯例一致。

无正确性 bug、无安全隐患、无 AGENTS.md 违规。正则模式 ReDoS 安全(无嵌套量词或交替膨胀)。

测试结果

本地 PR head(75bfcd13d)全部通过:29/29 测试通过、Prettier 通过、ESLint 通过、构建通过。

反向验证——仅将 isConversationId 回退为 !chatId.startsWith('http'):两个大写 webhook 用例失败,会话 ID 对照用例通过。确认是真正的回归守护。

CI(全绿):Classify ✓ · CodeQL ✓ · Lint ✓ · Test (ubuntu/macos/windows · Node 22.x) ✓

关于 tmux 测试: 此修复针对内部 adapter 路径(DingTalk webhook → emotion API 路由),非用户可见的 CLI 行为,无法通过终端 qwen -p "..." 有效驱动。adapter 边界的单元测试是此处正确的验证方式。

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Reflection

This is what a good bug-fix PR looks like. One-line fix, three targeted tests, and the tests actually guard the bug (confirmed by reverting the fix — both uppercase cases fail immediately).

The approach matches my independent proposal exactly. The regex /^https?:\/\//i is the established codebase convention (used in 16+ places), the fix is anchored and ReDoS-safe, and the scope is exactly the minimal change needed — nothing more. The test/test:ci script additions also pull 26 pre-existing markdown tests into CI coverage, which is a small but worthwhile bonus.

CI is fully green across all platforms and all check types. The CHANGES_REQUESTED status was only about PR-template headings, which the author has since addressed. No outstanding code concerns.

Approved. ✅

中文说明

反思

这是一个优秀的 bug 修复 PR 应有的样子。单行修复、三个针对性测试,且测试确实能拦住该 bug(回退修复后两个大写用例立即失败)。

方案与我的独立提案完全一致。正则 /^https?:\/\//i 是代码库中的既有惯例(16+ 处使用),修复有锚定且 ReDoS 安全,范围恰好是所需的最小改动——没有多余内容。test/test:ci 脚本新增还顺带将 26 个既有 markdown 测试纳入了 CI 覆盖,虽是小改进但很有价值。

CI 在所有平台和所有检查类型上全绿。CHANGES_REQUESTED 状态仅针对 PR 模板标题,作者已修正。无遗留代码顾虑。

已批准 ✅

Qwen Code · qwen3.7-max

@qwen-code-ci-bot qwen-code-ci-bot 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.

LGTM, looks ready to ship. ✅

@wenshao
wenshao merged commit fc15ae6 into QwenLM:main Jun 20, 2026
58 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.

DingTalk reactions treat uppercase webhook URLs as conversation IDs

3 participants