Skip to content

fix(gateway/dingtalk): support dingtalk-stream >= 0.24#8954

Closed
sputnicyoji wants to merge 1 commit into
NousResearch:mainfrom
sputnicyoji:fix/dingtalk-sdk-024-compat
Closed

fix(gateway/dingtalk): support dingtalk-stream >= 0.24#8954
sputnicyoji wants to merge 1 commit into
NousResearch:mainfrom
sputnicyoji:fix/dingtalk-sdk-024-compat

Conversation

@sputnicyoji

@sputnicyoji sputnicyoji commented Apr 13, 2026

Copy link
Copy Markdown

Summary

The DingTalk adapter was written against an older dingtalk-stream release and no longer runs on the current PyPI version (>= 0.24). Three API changes need to be absorbed:

  • DingTalkStreamClient.start() became a coroutine. Wrapping it in asyncio.to_thread raises RuntimeWarning: coroutine ... was never awaited and the stream never connects. Detect via iscoroutinefunction and await or thread-dispatch accordingly (keeps backward compatibility with older SDKs).
  • ChatbotHandler.process() became an async callback and now receives a CallbackMessage (raw envelope) instead of a ChatbotMessage. Make the handler async and parse callback.data into ChatbotMessage via from_dict. Drops the now-unused run_coroutine_threadsafe plumbing and _loop field.
  • ChatbotMessage.text is now a TextContent dataclass. The old str(text).strip() path produces <TextContent object at 0x...>. Check hasattr(text, "content") first, keep dict/str fallbacks for older SDKs and custom handlers.

Test plan

  • Existing tests/gateway/test_dingtalk.py suite passes (21 tests) — legacy dict-shaped text mocks continue to work via the fallback path.
  • Live-tested locally against dingtalk-stream==0.24.x with a group-chat bot: connect, receive text, and reply successfully.

The adapter was written against an older dingtalk-stream release and no
longer runs on the current PyPI version:

- `DingTalkStreamClient.start()` became a coroutine; wrapping it in
  `asyncio.to_thread` raises `RuntimeWarning: coroutine ... was never
  awaited` and the stream never connects. Detect via
  `iscoroutinefunction` and await or thread-dispatch accordingly.
- `ChatbotHandler.process()` became an async callback that receives a
  `CallbackMessage` (raw envelope), not a `ChatbotMessage`. Make the
  handler async and parse `callback.data` into `ChatbotMessage` via
  `from_dict`. Drops the now-unused `run_coroutine_threadsafe` plumbing
  and `_loop` field.
- `ChatbotMessage.text` is now a `TextContent` dataclass; the old
  `str(text).strip()` path produces `<TextContent object at 0x...>`.
  Check `hasattr(text, "content")` first, keep dict/str fallbacks for
  older SDKs and custom handlers.

Tests stay green against the existing test doubles (dict-shaped `text`).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
sputnicyoji pushed a commit to sputnicyoji/hermes-agent that referenced this pull request Apr 13, 2026
…ync process handler

# Conflicts:
#	gateway/platforms/dingtalk.py
sputnicyoji pushed a commit to sputnicyoji/hermes-agent that referenced this pull request Apr 13, 2026
Previously the adapter replaced any `picture` or inline `richText` image
with the literal string `[图片]`; vision tools therefore never saw the
actual bytes. This patch exchanges each `downloadCode` for the temporary
CDN URL via `/v1.0/robot/messageFiles/download`, fetches the bytes, and
caches them locally so `MessageEvent.media_urls` points at a real file.

Implementation notes:
- Piggyback on the dingtalk-stream SDK: `register_callback_handler`
  wires `handler.dingtalk_client = stream_client`, which gives us the
  SDK's access-token helper and `get_image_download_url`. Keep the
  handler reference on the adapter (`self._handler`).
- `get_image_download_url` is sync (requests-based) — offload via
  `asyncio.to_thread` to avoid blocking the event loop.
- Failures at any stage (missing code, empty URL, HTTP error) are
  logged but degrade gracefully: the event still dispatches with the
  `[图片]` text placeholder so the agent knows something arrived.
- `message.get_image_list()` handles both `picture` and `richText`
  shapes, so the same path supports inline images in rich messages.
- On successful download the placeholder text is replaced with
  `[图片 × N]` as a short caption alongside the attached media list.

Tests cover: missing handler, single-image success, empty downloadUrl,
partial HTTP failure (continue on remaining codes), and full end-to-end
picture dispatch through `_on_message`.

Depends on NousResearch#8954 (SDK 0.24 async process) and NousResearch#8960 (msgtype dispatch).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
sputnicyoji pushed a commit to sputnicyoji/hermes-agent that referenced this pull request Apr 13, 2026
Previously the adapter replaced any `picture` or inline `richText` image
with the literal string `[图片]`; vision tools therefore never saw the
actual bytes. This patch exchanges each `downloadCode` for the temporary
CDN URL via `/v1.0/robot/messageFiles/download`, fetches the bytes, and
caches them locally so `MessageEvent.media_urls` points at a real file.

Implementation notes:
- Piggyback on the dingtalk-stream SDK: `register_callback_handler`
  wires `handler.dingtalk_client = stream_client`, which gives us the
  SDK's access-token helper and `get_image_download_url`. Keep the
  handler reference on the adapter (`self._handler`).
- `get_image_download_url` is sync (requests-based) — offload via
  `asyncio.to_thread` to avoid blocking the event loop.
- Failures at any stage (missing code, empty URL, HTTP error) are
  logged but degrade gracefully: the event still dispatches with the
  `[图片]` text placeholder so the agent knows something arrived.
- `message.get_image_list()` handles both `picture` and `richText`
  shapes, so the same path supports inline images in rich messages.
- On successful download the placeholder text is replaced with
  `[图片 × N]` as a short caption alongside the attached media list.

Tests cover: missing handler, single-image success, empty downloadUrl,
partial HTTP failure (continue on remaining codes), and full end-to-end
picture dispatch through `_on_message`.

Depends on NousResearch#8954 (SDK 0.24 async process) and NousResearch#8960 (msgtype dispatch).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@dingxiaobo

Copy link
Copy Markdown

👍🏻 works for me

sputnicyoji pushed a commit to sputnicyoji/hermes-agent that referenced this pull request Apr 15, 2026
Previously the adapter replaced any `picture` or inline `richText` image
with the literal string `[图片]`; vision tools therefore never saw the
actual bytes. This patch exchanges each `downloadCode` for the temporary
CDN URL via `/v1.0/robot/messageFiles/download`, fetches the bytes, and
caches them locally so `MessageEvent.media_urls` points at a real file.

Implementation notes:
- Piggyback on the dingtalk-stream SDK: `register_callback_handler`
  wires `handler.dingtalk_client = stream_client`, which gives us the
  SDK's access-token helper and `get_image_download_url`. Keep the
  handler reference on the adapter (`self._handler`).
- `get_image_download_url` is sync (requests-based) — offload via
  `asyncio.to_thread` to avoid blocking the event loop.
- Failures at any stage (missing code, empty URL, HTTP error) are
  logged but degrade gracefully: the event still dispatches with the
  `[图片]` text placeholder so the agent knows something arrived.
- `message.get_image_list()` handles both `picture` and `richText`
  shapes, so the same path supports inline images in rich messages.
- On successful download the placeholder text is replaced with
  `[图片 × N]` as a short caption alongside the attached media list.

Tests cover: missing handler, single-image success, empty downloadUrl,
partial HTTP failure (continue on remaining codes), and full end-to-end
picture dispatch through `_on_message`.

Depends on NousResearch#8954 (SDK 0.24 async process) and NousResearch#8960 (msgtype dispatch).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@teknium1

Copy link
Copy Markdown
Contributor

Closing as superseded by #11471 (#11471) which salvaged @kevinskysunny's minimal fix (#11257) and added a follow-up for the broken _extract_text() path found during E2E testing.

Thanks for the fix — a lot of contributors hit this SDK break at the same time. Your investigation helped confirm the root cause.

@teknium1 teknium1 closed this Apr 17, 2026
@sputnicyoji

Copy link
Copy Markdown
Author

Thanks for the update and for getting the compatibility fix into main via #11471. Happy to see it resolved. Appreciate the review.

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.

3 participants