feat(dingtalk): include non-bot mention target identifiers in inbound context - #8639
Conversation
… context Extend the non-bot mention marker with each deduplicated target's staffId (preferred) or dingtalkId (fallback) so the model learns which members were mentioned, not only how many. Bot entry exclusion, dedup, and debug payload redaction are unchanged.
|
Re-run after three autofix rounds and the maintainer's local real-runtime E2E — gate reassessed against Template looks good ✓ Problem: real and observed, unchanged from the first pass. Issue #8638 carries a live DingTalk Stream callback showing DingTalk strips visible Direction: aligned — and the open direction question from the first pass (opt-in config vs default-on) now has the engaged maintainer's answer in-thread: default-on recommended, on the grounds that the sender's Size: not one of the listed core-infrastructure paths, but the growth since the first pass is worth naming. Production: 59 lines changed (45 added / 14 removed) spanning the channel-base contract plus the dingtalk and github adapters; tests: 312 lines. The growth is not scope creep — it is the two regressions found in review (marker brackets stripped by Approach: right-sized for the combined goal. The marker moved out of raw Risk: no elevated risk signals (no high-risk path matches). Moving on to code review. 🔍 中文说明三轮 autofix 与维护者本地真实运行时 E2E 之后的 re-run——gate 在 模板完整 ✓ 问题:真实且已观测,与首轮结论一致。issue #8638 附带真实钉钉 Stream 回调:钉钉剥离可见的 方向:对齐——首轮留下的方向问题(配置开关 vs 默认开启)现在有了参与维护者在 thread 中的答复:建议默认开启,理由是发送者的 规模:未触及列名的核心基础设施路径,但值得说明自首轮以来的增长。生产:59 行改动(45 增 / 14 删),横跨 channel-base 契约与 dingtalk、github 适配器;测试:312 行。增长不是范围蔓延——而是评审中发现的两个回归(标记方括号被 方案:与合并后的目标相称。标记从原始 风险:无升级风险信号(未匹配高风险路径)。 进入代码审查 🔍 — Qwen Code · qwen3.8-max Reviewed at |
What changed since the last reviewed head (
|
| Check | Conclusion |
|---|---|
review-scan (×3, Qwen Autofix orchestration) |
❌ failure — bot review-round infra, not PR CI |
Test (ubuntu-latest, Node 22.x) |
✅ success |
Desktop Shell (ubuntu-22.04) |
✅ success |
web-shell E2E Smoke (ubuntu-latest, Node 22.x) |
✅ success |
Classify PR |
✅ success |
label |
✅ success |
precheck-pr / precheck |
✅ success |
One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。
Behavioural substantiation — the gap the first pass deferred to a sponsored /verify is now closed by stronger evidence: the maintainer's local real-runtime E2E ran at exactly this head (real SDK handshake against a mock DingTalk gateway, real qwen channel start dt, a recording mock model provider as the oracle). It confirms the marker with identifiers in the actual model-facing request body, the 11-callback edge matrix, restored slash-command behaviour, intact redaction, and mutation checks killing every production line except the disclosed defensive clears. No sandboxed-lane line this round because the behavioural claim no longer rests on the author's word. The one thing no lane can settle is real DingTalk server-side behaviour (e.g. whether atUsers is populated for @所有人) — the maintainer's own caveat, and a real-tenant question.
中文说明
自上次审查 head 以来的变化(5795b16 → 9f4e9a2)
首轮审查的是 6 行适配器改动。三轮 autofix 将其重塑为结构上更优的修复:标记不再寄生在 envelope.text 里。DingtalkAdapter 现在收集去重后的目标 ID(staffId 优先、dingtalkId 兜底——优先级已翻转为与发送者标识空间一致),写入新的结构化字段 Envelope.mentionedMemberIds;ChannelBase 自行渲染 [Mentioned …] 标记,位置在 sanitizePromptText 之后、发送者归属门控之内;GithubAdapter 的持久化 envelope 类型守卫为新字段补上崩溃恢复校验。这同时修复了 main 上两个 PR 最初未声称的行为,且均由维护者 E2E 证实:纯数量标记的方括号在短内容下会被消毒器剥掉;标记进入 text 导致群消息只要 @ 了其他成员,斜杠命令即静默失效(自 #7473 起存在)。
代码审查
与我的独立设想一致且更优——结构化字段、由 base 在消毒后渲染,正是我会放的位置。关键结论均对照合并基代码核实,而非只看 diff:
- 标记仅在归属门控内渲染——
alreadyPrefixed重入与已识别斜杠命令都会跳过——这正是恢复"群消息 @ 其他成员时/clear等命令可用"的关键。测试suppresses the mention marker for a recognized slash command钉住了这一点,维护者实跑也给出了前后对照。 - ChannelBase 中两处合成 envelope 的 drain 站点——
main上仅有的两处alreadyPrefixed: true构造——都清除了mentionedMemberIds,collect 模式合并与 loop drain 不会重渲染过期标记。合并测试钉住标记恰好保留一次。 - ID 为平台可控输入,经
sanitizeQuotedText(id, 64)+ trim 中和,再过滤空串与裸…(超长的纯垃圾 ID 截断后只剩省略号——不过滤会凭空宣告一个幽灵成员)。构造的evil]\n[SYSTEM]: …ID 无法越出标记;测试矩阵与维护者边界用例均有覆盖。 - 群历史仅持久化
envelope.text,因此 mention ID 有意不落盘——标记改为提示时渲染后,types.ts 中的契约由构造即成立。 isOptionalStringArray复用了已用于持久化去重记录校验的既有 helper——未新增校验工具。- 调试日志对
staffId/dingtalkId的脱敏未改动;维护者已在本 head 上以QWEN_CHANNEL_DEBUG_PAYLOAD=1复核输出为[redacted]。
无关键阻塞项,无规范违规。一处已披露的薄弱点,沿用 autofix 轮次报告的自述:两处 drain 站点的 mentionedMemberIds: undefined 清除属纵深防御,当前被 !alreadyPrefixed 门控掩盖——还原它们套件仍全绿,因此没有测试锚定。无害,且是如实披露而非隐藏。
测试证据(9f4e9a2 上 PR 自身的 CI)
无人值守运行——此处不执行 PR 代码,仅通过 API 引用 PR 自身的 CI。唯一由 pull_request 事件触发的工作流 Qwen Code CI 已完成且成功,包括 Linux 单元测试门禁。macOS/Windows 测试作业与集成测试按设计仅在合并队列运行(见 ci.yml),PR 阶段跳过属预期,不是缺口。三个红色的 review-scan 检查属于 pull_request_review 触发的 Qwen Autofix 工作流——即本 PR 经历的评审轮次编排——不是 PR 的 CI 门禁,59 行 channel 改动也不可能引发它们。
(CI 结果表见上方英文部分的标记区域,CI 完成后由 finalize 工作流原地更新。)
行为实证——首轮留给赞助 /verify 的缺口,如今已被更强的证据闭合:维护者的本地真实运行时 E2E 恰在本 head 上执行(真实 SDK 握手对接 mock 钉钉网关、真实 qwen channel start dt、以记录型 mock 模型提供方作为 oracle),确认了真实模型请求体中带标识符的标记、11 条回调边界矩阵、斜杠命令行为恢复、脱敏有效,以及变异检查杀死了除已披露纵深防御代码外的每一行生产代码。本轮不再给出沙箱验证行,因为行为声明已不再依赖作者口述。唯一任何验证通道都无法闭合的,是真实钉钉服务端行为(例如 @所有人 时是否填充 atUsers)——这是维护者自己标注的保留项,也是只有真实租户才能回答的问题。
— Qwen Code · qwen3.8-max
Reviewed at 9f4e9a2241939c80a33b7fafea9831e38d7d6234 · re-run with @qwen-code /triage
|
Confidence: 4/5 — clean across every stage at the new head; what remains are non-blocking follow-ups, named below. Second pass over a materially larger diff than the first, and the PR is better for the growth: moving the marker out of Non-blocking, for whoever merges:
Approving, pinned to the reviewed commit. ✅ 中文说明信心度:4/5 —— 在新 head 上各个阶段均干净;剩余事项都是非阻塞的后续跟进,列在下方。 第二轮审查面对的是比首轮大得多的 diff,而 PR 因这次增长变得更好:把标记从 非阻塞事项,交给合入者:
批准,锚定在被审查的提交上。✅ — Qwen Code · qwen3.8-max Reviewed at |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship — CI landed green after the review. ✅
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline.
中文说明
已审查。 建议见行内评论。
— qwen3.8-max via Qwen Code /review (v0.21.6)
…field Move the non-bot mention marker out of the envelope text into a structured mentionedMemberIds field rendered by ChannelBase after prompt sanitization, matching the [Replying to:] wrapper pattern. Previously sanitizePromptText stripped the marker's brackets only when the ID list content was <=64 chars and folded its newline, so the delivered format depended on the identifier list length while the adapter asserted the pre-sanitization envelope text. The marker now keeps a uniform bracketed own-line shape at every length, IDs are neutralized via sanitizeQuotedText before bypassing the sanitizer, and slash-command parsing sees the message body alone. Also updates the tracked E2E verification plan to the shipped format.
|
@qwen-code /takeover |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline. Not reviewed: reverse audit — stopped before round 3 by the review time budget.
中文说明
已审查。 建议见行内评论。 未审查:反向审计——评审时间预算不足,未能开始第 3 轮。
— qwen3.8-max via Qwen Code /review (v0.21.6)
|
@qwen-code /resolve |
|
🤝 Takeover engaged: the autofix loop now manages this PR — it will address new review feedback and resolve base conflicts until the label is removed or the round cap is reached. This is a fork PR, so the first round comes from the next scheduled scan (usually within minutes). Remove the 中文说明🤝 已接管:autofix 循环现在管理此 PR —— 将持续处理新的评审反馈与 base 冲突,直到移除标签或达到轮次上限。本 PR 来自 fork,首轮处理将由下一次定时扫描执行(通常几分钟内)。移除 |
wenshao
left a comment
There was a problem hiding this comment.
Test (ubuntu-latest, Node 22.x), web-shell E2E Smoke). Both were measured against the built merge base by the review's own test-delta probe: identical failing file sets, netNew: [] — pre-existing on main, not introduced by this PR. The Approve is withheld only because a review must not certify over red CI.
No issues found. LGTM! ✅
中文说明
未发现问题。LGTM!✅
— qwen3.8-max via Qwen Code /review (v0.21.6)
|
@qwen-code /resolve |
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
|
Qwen Code resolved the merge conflicts and pushed the branch update. Merge resolution for #8639 (dingtalk mention target identifiers)Root cause. Main's Textual, not semantic. #8565 does not touch mention logic; it refactors the reply/status-card path (removes interface CardRunCorrelation { // from main #8565 — kept
ownerId: string;
target: { chatId: string; isGroup: boolean };
sender?: { senderName: string };
}
function collectNonBotMentionIds(data: DingTalkMessageData): string[] { // from this PR — keptThe function body below the conflict auto-merged to the PR version ( What is load-bearing.
Verification / what I could not verify. Structurally verified: the merged tree's diff vs origin/main is byte-for-byte the PR's original 6-file change (+105/−20); the auto-merged 中文说明冲突根因:main 上的 #8565(钉钉状态卡片连续性修复)在 属文本冲突而非语义冲突:#8565 只重构回复/状态卡片路径(删除 关键约束: 已做结构性校验:合并结果相对 origin/main 的 diff 与本 PR 原始改动完全一致;测试文件的自动合入内容与 #8565 的测试改动逐行一致;无测试断言旧格式。未运行构建/测试,由 PR 自身 CI 覆盖。 |
|
🤖 Addressed the latest review feedback (round 1/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 1/100 轮)。改动内容与我反驳保留之处如下: Autofix review-feedback summary — PR #8639 (round 3)All six new automated-reviewer suggestions were addressed in one commit: Feedback points and dispositions
Files changed
Verification
中文说明Autofix 评审反馈处理总结 — PR #8639(第 3 轮)自动化评审器的 6 条新建议已在一个提交中全部处理: 反馈点与处理决定
变更文件
验证
Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。 🧠 Handled by Qwen Code · model/模型 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline.
中文说明
已审查。 建议见行内评论。
— qwen3.8-max via Qwen Code /review (v0.21.7)
wenshao
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline. 3 Suggestion-level finding(s) could not be anchored to a changed line and were dropped; nothing further to act on here.
中文说明
已审查。 建议见行内评论。 3 条建议级发现无法锚定到改动行,已丢弃;此处无需进一步处理。
— qwen3.8-max via Qwen Code /review (v0.21.7)
|
Follow-up from this /review round: the rationale comment at 中文说明本轮 /review 的补充建议: — qwen3.8-max via Qwen Code /review (v0.21.6) |
|
🤖 Addressed the latest review feedback (round 2/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 2/100 轮)。改动内容与我反驳保留之处如下: Autofix review round — PR #8639 (address-review)Commit: Feedback points and dispositionsNo action required — confirmation replies (6): Acted — [Suggestion] R3-1 Acted — [Suggestion] R3-2 Acted — [Suggestion] R3-3 Acted — [Suggestion] R3-4 Declined — [Suggestion] R3-5 Acted — [Suggestion] R3-6 Files changed
VerificationCommands actually run (in order):
Not run (with reasons):
中文说明Autofix 审查轮次 — PR #8639(address-review)提交: 反馈点与处理结果无需处理 —— 确认类回复(6 条): 已处理 —— [Suggestion] R3-1 已处理 —— [Suggestion] R3-2 已处理 —— [Suggestion] R3-3 已处理 —— [Suggestion] R3-4 已拒绝 —— [Suggestion] R3-5 已处理 —— [Suggestion] R3-6 变更文件
验证实际执行的命令(按顺序):
未执行的命令(及原因):
Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。 🧠 Handled by Qwen Code · model/模型 |
…#8639) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
|
🤖 Addressed the latest review feedback (round 3/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 3/100 轮)。改动内容与我反驳保留之处如下: Autofix review-response summary — PR #8639 (round 3)Feedback dispositions1. Inline
|
…ds (QwenLM#8639) Companion to the rejection test: a persisted envelope carrying a valid string-array mentionedMemberIds must pass isInboundEnvelope, be dispatched on recovery with the field intact, and let the task complete.
|
🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下: Autofix review round — no action needed (PR #8639)All seven inline comments in this round are confirmations from @BenGuanRan of work that is already in the branch head ( Per-point dispositions:
VerificationCommands actually run this round (no code changes, no commit):
中文说明Autofix 审查轮次 — 无需操作(PR #8639)本轮全部 7 条行内评论均为 @BenGuanRan 对分支头部( 逐条处置:
验证本轮实际运行的命令(无代码改动、无提交):
Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 🧠 Handled by Qwen Code · model/模型 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline. 1 Suggestion-level finding(s) could not be anchored to a changed line and were dropped; nothing further to act on here.
中文说明
已审查。 建议见行内评论。 1 条建议级发现无法锚定到改动行,已丢弃;此处无需进一步处理。
— qwen3.8-max via Qwen Code /review (v0.21.7)
| text: '', | ||
| mentionedMemberIds: ['user-a'], |
There was a problem hiding this comment.
[Suggestion] This diff changes the mention-only fixture to text: '' + structured mentionedMemberIds, certifying that path at the adapter boundary — but nothing renders it through ChannelBase: every mentionedMemberIds test in ChannelBase.test.ts uses a non-empty body. The verification probe confirmed current behavior is correct (the marker renders with empty text), and mutating the attribution gate with an empty-text condition dropped the marker while all 569 existing ChannelBase tests stayed green. — Failure scenario: @Bot @Member with no other text → if the gate (or a future empty-text early-return) ever conditions on non-empty text, the marker — the entire content of this message class — is silently dropped and no test fails.
Suggested ChannelBase test:
it('renders the mention marker for mention-only messages with empty text', async () => {
const ch = createChannel({ groupPolicy: 'open' });
await ch.handleInbound(
groupEnv({ senderName: 'Alice', text: '', mentionedMemberIds: ['user-a'] }),
);
expect((bridge.prompt as ReturnType<typeof vi.fn>).mock.calls[0][1]).toContain(
'[Mentioned 1 other group member: user-a]',
);
});中文说明
[建议] 本 diff 将纯 mention 场景的 fixture 改为 text: '' + 结构化 mentionedMemberIds,该路径在 adapter 边界得到了验证——但从未通过 ChannelBase 渲染:ChannelBase.test.ts 中所有 mentionedMemberIds 测试使用的都是非空正文。验证探针确认当前行为正确(空正文下 marker 能正常渲染),而对归属门加入空正文条件的变异会让 marker 消失,同时现有 569 个 ChannelBase 测试全部保持绿色。— 失败场景:发送 @Bot @Member 且无其他文本 → 若归属门(或未来新增的空正文提前返回)以非空 text 为条件,marker——这类消息的全部内容——会被静默丢弃,且没有任何测试失败。
— qwen3.8-max via Qwen Code /review (v0.21.7)
| if (ids.length > 0) { | ||
| const memberLabel = ids.length === 1 ? 'member' : 'members'; | ||
| promptText = `[Mentioned ${ids.length} other group ${memberLabel}: ${ids.join(', ')}]\n\n${promptText}`; |
There was a problem hiding this comment.
[Suggestion] Each mention ID is capped at 64 code points, but the number of IDs is unbounded, so the marker size grows linearly with the mention count — the one prompt-embedded input in this file with no aggregate cap (its siblings are capped: quoted text 500 cp, attachment filenames 128 cp, paths 1024 cp, sender names 64 cp). Measured through ChannelBase: 100 IDs → 1.7 KB prompt, 500 → 8.5 KB, 1,000 → 17 KB; worst case at the per-ID cap is ~66 KB for 1,000 mentions, and collect-mode coalescing stacks multiple markers into one prompt. — Failure scenario: a group message that @-mentions hundreds of members (atUsers is bounded only by platform message length) inflates the prompt by tens of KB.
Illustrative fix (cap the rendered list, keep the true count):
const shown = ids.slice(0, MENTION_MARKER_MAX_IDS);
const suffix = ids.length > shown.length ? ` +${ids.length - shown.length} more` : '';
promptText = `[Mentioned ${ids.length} other group ${memberLabel}: ${shown.join(', ')}${suffix}]\n\n${promptText}`;中文说明
[建议] 每个 mention ID 都有 64 码点的上限,但 ID 的数量没有任何上限,因此 marker 大小随提及数量线性增长——这是本文件中唯一没有总量上限的 prompt 嵌入输入(同类输入都有上限:引用文本 500 码点、附件文件名 128 码点、路径 1024 码点、发送者名称 64 码点)。通过 ChannelBase 实测:100 个 ID → prompt 1.7 KB,500 个 → 8.5 KB,1000 个 → 17 KB;按单 ID 上限的最坏情况,1000 个提及约 66 KB,且 collect 模式合并时多个 marker 会叠加进同一个 prompt。— 失败场景:一条群消息 @ 提及数百名成员(atUsers 仅受平台消息长度限制)会使 prompt 膨胀数十 KB。
— qwen3.8-max via Qwen Code /review (v0.21.7)
Maintainer verification — local real-runtime E2EVerified at Verdict: works as described, and it also fixes two behaviours on 1. The environmentI did not stub the transport. I stood up the whole stack and drove it with real DingTalk Stream frames:
The reaction ( 2. The claimed behaviour: confirmedSame callback, same everything, only the tree differs: The mentioned member is identified by 3. Two things this PR fixes that it does not claim3a. On 3b. On Live result for
This has been broken since #7473. Worth calling out in the changelog — it is a user-visible fix, not just a context improvement. 4. Edge-case matrixEleven realistic callbacks through the real Highlights: bot-only mention and 1:1 DM are byte-identical to 5. Test efficacy — mutation checksEach production line was reverted in place and the suites re-run:
The last one matches what the round-3 autofix report itself said (the omission is masked by the 6. Suites, typecheck, lint (all local, PR head)
7. Non-blocking notesN1 — the marker has no cap on the number of identifiers. Each ID is capped at 64 code points, but the list length is not bounded. Measured ( N2 — the config question from triage is still open, and it is now worth answering. With 3b established, this PR is no longer only a privacy trade-off — it also repairs slash commands. That argues for changing the default rather than hiding it behind an opt-in flag. But member N3 — minor, no action. When the bot's own Out of scope, pre-existing: the bot-mention stripper 8. What I did not verifyReal DingTalk servers. My gateway is spec-built from 中文版维护者验证 —— 本地真实运行时端到端验证对象 结论:行为与描述一致,并且顺带修复了 1. 验证环境没有打桩传输层,而是把整条链路真实跑起来,用真实的钉钉 Stream 帧驱动:
网关日志中的 2. PR 声称的行为:已确认同一回调、其余条件完全一致,仅代码树不同: 被提及成员以 3. 本 PR 顺带修复、但未声称的两件事3a. 3b. 在群内发送
该问题自 #7473 起存在。建议写入 changelog —— 这是用户可见的功能修复,不只是上下文增强。 4. 边界用例矩阵11 个真实形态的回调,经真实 要点:仅提及机器人、以及 1:1 私聊,与 5. 测试有效性 —— 变异检查逐条把生产代码改回,再跑套件:
最后一条与第 3 轮 autofix 报告自述一致(该疏漏被 6. 套件、类型检查、Lint(全部本地,PR head)
7. 非阻塞意见N1 —— 标记对标识符「数量」没有上限。 单个 ID 限 64 码点,但列表长度不受限。实测( N2 —— triage 提出的配置开关问题仍未决,而现在更值得回答。 有了 3b,本 PR 已不只是隐私取舍,它同时修复了斜杠命令,这更支持直接改变默认行为而非藏在开关后面。但成员的 N3 —— 次要,无需处理。 当机器人自身的 超出范围、既有问题: 机器人 @ 剥离正则 8. 未验证部分真实钉钉服务端。我的网关是依据 |
|
@qwen-code /triage |
|
Sandboxed verification: ✅ passed — merge-ready (agent verdict) - workflow run Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check. Scripted assertions: 2358 passed · 0 failed · 2358 total 中文 — 判定:✅ 通过 · 可合入(agent 判定)沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查。 脚本断言:2358 通过 · 0 失败 · 2358 总计 Verification reportVerification report — PR #8639Verdict: 中文摘要
Central claimDingTalk group messages that mention the bot plus other members deliver the mentioned members' stable identifiers to the model prompt via a structured Secondary claims: (a) the GitHub adapter's persisted-envelope recovery validates the new field — a non-array value blocks cursor commit, a valid string array recovers intact; (b) identifier preference is A/B table (head vs base, identical head test files)Base arm =
11 cells flip red→green; the 3 green-on-base cells are accept-path or vacuous-on-base by construction and are pinned on head by the mutation matrix below. Mutation matrix (guards the PR introduces)Witness:
No mutant regressed a killed test to survived relative to M6's kill set; both positive controls prove each suite can fail. Findings (non-blocking)
Reviewer Test Plan walkthrough
Not covered
MethodologyEnvironment: CI merge-ref checkout ( Evidence imagesHarness scripts and raw logs are in the workflow run artifacts (7-day retention). — Qwen Code · sandboxed verification |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship — CI green on the reviewed head, and the behavioural claim is settled by the maintainer's real-runtime E2E at this exact commit. ✅







What this PR does
Extends the DingTalk non-bot mention context marker with the stable identifiers of the mentioned members. When a group message mentions the bot and one or more other members, DingTalk removes all visible
@nametext from the message body and reports the targets only throughatUsers. The existing marker preserves just the count of mentioned members; this PR additionally lists each deduplicated target'sstaffId(preferred) ordingtalkId(fallback) in the marker, for example[Mentioned 1 other group member: 012345]. The bot's own entry is still excluded, targets are still deduplicated, and the channel debug payload logging keeps redactingdingtalkId/staffIdvalues.Why it's needed
A count-only marker tells the model that someone was mentioned but not who, so the agent cannot correlate a mentioned member with later speakers or act on the mention in any way. The sender is already identified by
staffIdin the same prompt (senderStaffIdwithsenderIdfallback), so rendering mention targets asstaffIdwhen available keeps senders and mentioned members in the same identifier space. The identifiers are already present in the callback payload, so this needs no extra permissions, network calls, or contact-directory access. This intentionally revisits the privacy tradeoff made in #7473: deployments where member identifiers are acceptable model context gain actionable mention information, while debug-log redaction of those identifiers is preserved.Reviewer Test Plan
How to verify
Run
cd packages/channels/dingtalk && npx vitest run src/DingtalkAdapter.test.tsand confirm all tests pass, including the updated expectations: a single-member mention renders the member'sstaffId, multiple distinct members render comma-separated identifiers, an entry with onlydingtalkIdfalls back todingtalkId, and a bot-only mention still adds no marker. Runnpm run typecheck. Optionally, with a DingTalk Stream channel, send@Bot please review this @Memberin a group and confirm the inbound model text starts with[Mentioned 1 other group member: <member staffId>]followed by the message body.Evidence (Before & After)
Before: a callback containing the bot plus one member entry with a
staffIdandtext.content: "please review this"forwards[Mentioned 1 other group member]\nplease review this. After: the same callback forwards[Mentioned 1 other group member: member-staff]\nplease review this. Also verified against a live DingTalk Stream callback where the message body arrives with visible names removed andatUserscarries the target identifiers.Tested on
Environment (optional)
Node.js 22; vitest unit tests plus a live
qwen serveruntime with a DingTalk Stream channel.Risk & Scope
staffId/dingtalkIdof mentioned members now enter model context, reversing the privacy-safe count-only decision in fix(dingtalk): preserve non-bot mention context #7473. Deployments that do not want platform identifiers in model context should not adopt this change; the diff is small and easy to revert. The dedup key now prefersstaffIdoverdingtalkId; if a single member ever appeared with inconsistent entry shapes the count could shift by one, the same class of edge case already accepted in fix(dingtalk): preserve non-bot mention context #7473.chatbotUserIdkeep their current behavior.Linked Issues
Fixes #8638
中文说明
本 PR 做了什么
扩展钉钉非机器人 mention 上下文标记,使其包含被提及成员的稳定标识符。当群消息同时提及机器人和一名或多名其他成员时,钉钉会从消息正文中移除所有可见的
@姓名文本,仅通过atUsers报告被提及对象。现有标记只保留被提及成员的数量;本 PR 额外在标记中列出每个去重后目标的staffId(优先)或dingtalkId(兜底),例如[Mentioned 1 other group member: 012345]。机器人自身条目仍被排除,目标仍按标识去重,Channel 调试 payload 日志对dingtalkId/staffId的脱敏保持不变。为什么需要
仅有数量的标记只能告诉模型"有人被提及",无法告诉模型是谁,因此 Agent 无法把被提及成员与后续发言者关联,也无法对提及做出任何动作。发送者在同一 prompt 中已经以
staffId标识(senderStaffId,回退senderId),mention 目标在可用时优先输出staffId,可让发送者与被提及成员处于同一标识空间。这些标识符本就在回调 payload 中,因此无需额外权限、网络调用或通讯录访问。本 PR 有意重新评估 #7473 的隐私取舍:在成员标识符可作为模型上下文的部署环境中,可以获得可用的 mention 信息,同时调试日志中这些标识符的脱敏予以保留。Reviewer 测试计划
如何验证
运行
cd packages/channels/dingtalk && npx vitest run src/DingtalkAdapter.test.ts,确认全部测试通过,包括更新后的期望:单成员 mention 输出该成员的staffId;多个不同成员以逗号分隔输出;仅含dingtalkId的条目回退到dingtalkId;仅提及机器人时仍不添加标记。运行npm run typecheck。可选:使用钉钉 Stream channel,在群内发送@Bot please review this @Member,确认入站模型文本以[Mentioned 1 other group member: <member staffId>]开头,后接消息正文。证据(修改前后)
修改前:回调包含机器人条目和一名带
staffId的成员条目、正文为please review this时,转发文本为[Mentioned 1 other group member]\nplease review this。修改后:同一回调转发[Mentioned 1 other group member: member-staff]\nplease review this。已在真实钉钉 Stream 回调上验证:正文到达时可见姓名已被移除,atUsers携带目标标识符。测试环境
环境(可选)
Node.js 22;vitest 单元测试,外加带钉钉 Stream channel 的
qwen serve实际运行时验证。风险与范围
staffId/dingtalkId现在会进入模型上下文,反转了 fix(dingtalk): preserve non-bot mention context #7473 "隐私安全的纯数量" 决定。不希望平台标识符进入模型上下文的部署不应采用本改动;diff 很小、易于回退。去重 key 现在优先staffId而非dingtalkId;若同一成员以不一致的条目形状出现,计数可能偏差 1,这与 fix(dingtalk): preserve non-bot mention context #7473 已接受的边缘情况属同一类。chatbotUserId的回调保持现有行为。关联 Issue
Fixes #8638