Skip to content

fix(dingtalk): support dingtalk-stream 0.24+ SDK (async process, CallbackMessage, oapi webhooks, TextContent)#11471

Merged
teknium1 merged 2 commits into
mainfrom
hermes/hermes-e52e6172
Apr 17, 2026
Merged

fix(dingtalk): support dingtalk-stream 0.24+ SDK (async process, CallbackMessage, oapi webhooks, TextContent)#11471
teknium1 merged 2 commits into
mainfrom
hermes/hermes-e52e6172

Conversation

@teknium1

@teknium1 teknium1 commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Salvages #11257 (kevinskysunny — authorship preserved) plus a follow-up fix discovered during E2E testing against the real dingtalk-stream 0.24.3 SDK.

What's broken on main

Our gateway/platforms/dingtalk.py was written against pre-0.20 dingtalk-stream. Four incompatibilities break DingTalk in production:

  1. ChatbotHandler.process() is now async — ours was sync and used run_coroutine_threadsafe, so it never fires on the new SDK.
  2. process() now receives a CallbackMessage envelope with a .data dict — ours expected a ChatbotMessage directly.
  3. DingTalkStreamClient.start() is now a coroutineasyncio.to_thread(self._stream_client.start) never awaits it.
  4. Reply webhooks now come from oapi.dingtalk.com — our regex only allowed api.dingtalk.com, so every reply was silently rejected by the origin allowlist.

Items 1-4 close out a pile of duplicate PRs reporting the same root cause: #5038, #8477, #8954, #9131, #9764, #9828, #10153, #10369, #10820, #11257, plus issues #5037, #6986, #8811, #8816, #9149, #9752.

What kevinskysunny's commit fixes (cherry-picked as-is)

  • _DINGTALK_WEBHOOK_RE^https://(?:api|oapi)\.dingtalk\.com/
  • _run_stream awaits self._stream_client.start() directly
  • _IncomingHandler.process becomes async and parses callback_message.data via ChatbotMessage.from_dict

Follow-up fix added on top (825b0fe)

E2E testing against real dingtalk-stream==0.24.3 revealed _extract_text() was also broken by the SDK change:

Field Pre-0.20 0.20+ Old code behaviour
message.text dict with content key TextContent dataclass str(text) returned 'TextContent(content=hello)' literally
rich text message.rich_text (list) message.rich_text_content.rich_text_list silently empty

Every text message received by the agent was coming in as the string TextContent(content=...) instead of the actual user message. Fix handles both shapes via hasattr(text, 'content') and falls back through legacy paths.

Tests

  • Adds 13 new tests covering the webhook allowlist, async process(), and _extract_text() against the current SDK, the legacy SDK, and edge cases.
  • Full tests/gateway/test_dingtalk.py: 29 passed.
  • Full tests/gateway/: 3042 passed, 6 pre-existing failures in signal/telegram (unrelated to this PR).

E2E verification

Ran the adapter end-to-end against real dingtalk-stream==0.24.3:

PASS: _extract_text(real-SDK text msg) = 'hello world'
PASS: process() → _on_message → _extract_text = 'hello world'
PASS: oapi.dingtalk.com webhook passes origin validation
PASS: legacy dict-shaped text still extracted correctly

Authorship

Original commit preserved with kevinskysunny@gmail.com authorship — will merge with --rebase to keep attribution.

Supersedes / closes

Once merged, the following can be closed with credit:

kevinskysunny and others added 2 commits April 17, 2026 00:28
…hape

The cherry-picked SDK compat fix (previous commit) wired process() to
parse CallbackMessage.data into a ChatbotMessage, but _extract_text()
was still written against the pre-0.20 payload shape:

  * message.text changed from dict {content: ...} → TextContent object.
    The old code's str(text) fallback produced 'TextContent(content=...)'
    as the agent's input, so every received message came in mangled.
  * rich_text moved from message.rich_text (list) to
    message.rich_text_content.rich_text_list.

This preserves legacy fallbacks (dict-shaped text, bare rich_text list)
while handling the current SDK layout via hasattr(text, 'content').

Adds regression tests covering:
  * webhook domain allowlist (api.*, oapi.*, and hostile lookalikes)
  * _IncomingHandler.process is a coroutine function
  * _extract_text against TextContent object, dict, rich_text_content,
    legacy rich_text, and empty-message cases

Also adds kevinskysunny to scripts/release.py AUTHOR_MAP (release CI
blocks unmapped emails).
@teknium1 teknium1 merged commit 3438d27 into main Apr 17, 2026
5 checks passed
@teknium1 teknium1 deleted the hermes/hermes-e52e6172 branch April 17, 2026 07:52
This was referenced Apr 17, 2026
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.

2 participants