Skip to content

fix(runtime): route DingTalk 1:1 replies by stamped conversation type - #5116

Open
ggbdpq wants to merge 1 commit into
apache:mainfrom
ggbdpq:fix/dingtalk-oto-reply-routing
Open

fix(runtime): route DingTalk 1:1 replies by stamped conversation type#5116
ggbdpq wants to merge 1 commit into
apache:mainfrom
ggbdpq:fix/dingtalk-oto-reply-routing

Conversation

@ggbdpq

@ggbdpq ggbdpq commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #5111. Real DingTalk 1:1 and group conversationIds share the same cid prefix, so the send router's startsWith('cid') guess sent every direct reply to the group endpoint — which rejects it with a misleading resource.not.found. The 1:1 endpoint additionally addresses by senderStaffId, which the payload parser dropped, so no direct reply could ever be delivered even with the route fixed.

The fix follows the existing QQ bridge convention (pickQQSendRoute routes on receive-side-stamped prefixes):

  • DingTalkBotMessagePayload now captures senderStaffId.
  • dingTalkPayloadToEvent stamps the chatId from the authoritative conversationTypegroup:<conversationId> for groups, oto:<senderStaffId> for 1:1 — instead of storing the bare conversation id.
  • pickDingTalkSendRoute decodes the stamp and fails closed (null) on unstamped ids rather than guessing. A 1:1 message received without senderStaffId still arrives (bare conversationId kept as the session key) but the send side declines instead of firing a request destined to fail. Previously stored conversations could not be replied to either way (old path 400s), and new inbound messages pick up the stamp.

Verification

Claim Evidence
Red new tests reference the stamping contract and realistic cid…= shapes; they fail against the old prefix guess (the old fixtures user-99/cidp-abc encoded the same wrong assumption as the implementation)
Green node --test dist/bots/__tests__/*.test.js → 102/102 (dingtalk-bridge 9/9, including: bare real-shaped cid…= ids route to null, stamped ids route to their own endpoints, senderStaffId is captured)
Runtime build npm --workspace @maka/runtime run build exit 0
Format / headers biome format clean on touched files; ASF headers intact

Not verified locally: end-to-end delivery against a live DingTalk Stream connection (the issue reporter's live experiment pins the API behavior: conversationId → 400 on the group endpoint, senderId → 400 staffId.notExisted, senderStaffId → delivered). Group replies were not exercised in the report either; the stamped group path sends the same body to the same endpoint as before.

AI use

Implemented with ZCode (GLM-5.3-Flash): followed the issue's three-way experiment and suggested fix shape, mirrored the QQ bridge stamping convention, rewrote the test fixtures to realistic identifier shapes per the acceptance criteria, and carried the Generated-by trailer in the commit.

Checklist

  • Root-cause fix (route decision made once at receive time, not guessed at send time)
  • Tests use realistic identifier shapes, so a prefix-guessing regression fails
  • senderStaffId captured from the callback payload
  • Graceful behavior when senderStaffId is absent (fail-closed send, event still delivered)
  • Scope limited to the DingTalk bridge

Real DingTalk 1:1 and group conversationIds share the same `cid`
prefix, so `pickDingTalkSendRoute`'s `startsWith('cid')` guess routed
every direct reply to the group endpoint, which rejects it with a
misleading `resource.not.found`. The 1:1 endpoint also addresses by
`senderStaffId`, which the payload parser dropped — so even with the
route fixed no direct reply could be delivered (apache#5111).

Follow the QQ bridge convention: `dingTalkPayloadToEvent` stamps the
chatId from the authoritative `conversationType` (`group:` for groups,
`oto:` + `senderStaffId` for 1:1, captured from the payload), and
`pickDingTalkSendRoute` decodes the stamp instead of guessing the id
shape, failing closed on unstamped ids. A 1:1 message without
`senderStaffId` still arrives (its bare conversationId stays unstamped)
but cannot be replied to rather than sending a request destined to
fail; previously stored conversations share that fate either way, and
new messages pick up the stamp.

Tests now use realistic identifier shapes — both conversation kinds
carry `cid…=` ids — so a prefix-guessing regression fails the suite,
where the old fixtures (`user-99`) encoded the same wrong assumption
as the implementation.

End-to-end delivery against a live DingTalk Stream connection was not
run locally; routing and payload mapping are pinned by unit tests at
the bridge's public helpers. Group replies were not exercised in the
report either; the stamped group path sends the same body to the same
endpoint as before.

Fixes apache#5111
Generated-by: GLM-5.3-Flash (ZCode)
@github-actions github-actions Bot added the effort/M Under 500 readable lines label Sep 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/M Under 500 readable lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(runtime): DingTalk 1:1 replies are unroutable — conversationId misrouted to the group endpoint and senderStaffId is never captured

1 participant