Skip to content

feat(dingtalk): rich-media inbound pipeline — download, parse, persist - #17369

Open
spike2204 wants to merge 1 commit into
NousResearch:mainfrom
spike2204:feat/dingtalk-media-parsers
Open

spike2204 wants to merge 1 commit into
NousResearch:mainfrom
spike2204:feat/dingtalk-media-parsers

Conversation

@spike2204

Copy link
Copy Markdown

Summary

Add a full inbound rich-media pipeline for the DingTalk adapter: download images/files from DingTalk CDN, parse document contents (docx/pdf/xlsx/txt), persist to a local inbox directory with automatic 24h purge, and inject parsed text into the agent context.

Changes

  • File parsers: _parse_text_file, _parse_docx_file, _parse_pdf_file, _parse_excel_file with graceful fallbacks when optional deps are missing
  • _inbound_media_dir() + _cleanup_inbound_media() — 24h auto-purge
  • _download_file_to_inbox() — authenticated download with 20MB guard
  • _download_images_to_local() — batch download for multi-modal input (DingTalk signed OSS URLs are not universally accessible)
  • _extract_and_parse_file_attachments() — walk raw extensions payload for file/audio/video/richText
  • Integrate media pipeline into _on_message()
  • Add python-docx, pdfplumber, openpyxl to dingtalk optional deps in pyproject.toml

Ported from dingtalk-openclaw-connector core/message-handler.ts:700-956.

Related

Part of the DingTalk adapter enhancement series — see #12769 for the umbrella PR.

Add a full inbound rich-media pipeline for the DingTalk adapter:
download images/files from DingTalk CDN, parse document contents
(docx/pdf/xlsx/txt), persist to a local inbox directory with automatic
24h purge, and inject parsed text into the agent context.

- File parsers: _parse_text_file, _parse_docx_file, _parse_pdf_file,
  _parse_excel_file with graceful fallbacks when deps missing
- _inbound_media_dir() + _cleanup_inbound_media() — 24h auto-purge
- _download_file_to_inbox() — authenticated download via DingTalk
  /v1.0/robot/messageFiles/download with 20MB guard
- _download_images_to_local() — batch download for multi-modal input
- _extract_and_parse_file_attachments() — walk raw extensions payload
- Integrate media pipeline into _on_message()
- Add python-docx, pdfplumber, openpyxl to dingtalk optional deps

Ported from dingtalk-openclaw-connector core/message-handler.ts:700-956. to #
@alt-glitch alt-glitch added type/feature New feature or request P2 Medium — degraded but workaround exists comp/gateway Gateway runner, session dispatch, delivery platform/dingtalk DingTalk adapter labels Apr 29, 2026
@alt-glitch

Copy link
Copy Markdown

Likely duplicate of #14335 — same DingTalk rich-media inbound pipeline (download, parse, persist). Also overlaps with #9451.

@spike2204

Copy link
Copy Markdown
Author

Hi @alt-glitch, appreciate the thorough cross-referencing! 🙏

You're right that this overlaps with #14335 and #9451. Here's the full picture:

We're the DingTalk (钉钉) Open Platform team at Alibaba — @PeterGuy326 and @spike2204 are colleagues. This PR is a clean, self-contained re-submission of the media pipeline from the stacked #14335, rebased against main.

How this relates to #9451: Our implementation is a superset that covers the same ground as #9451 but adds:

  • Full document parsing pipeline (docx via python-docx, PDF via pdfplumber/PyPDF2, Excel via openpyxl/pandas) with graceful fallback when optional deps are missing
  • Persistent ~/.hermes/inbound_media/ inbox with automatic 24h purge — agent tools (vision_analyze, transcribe_audio, read_file) can reopen files long after _on_message returns
  • DingTalk signed OSS URL workaround — these URLs are bound to specific request context (IP/headers), so downstream vision tools fail with HTTP 403. We download in-process (same network context as the SDK) and pass local file:// paths instead
  • Extension-payload walking for file/audio/video message types that the SDK doesn't parse into typed attributes

This is ported from our production dingtalk-openclaw-connector (core/message-handler.ts:700-956) and has been running in production handling ~5k messages/day.

Happy to help consolidate with #9451 if the maintainers prefer — we can close whichever PR is superseded. 🚀

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

Thanks for tackling an unhandled DingTalk media path. The underlying gap remains on current main: plugins/platforms/dingtalk/adapter.py:659-670 only proceeds with _extract_media() output, and _extract_media() currently handles image and rich-text download codes (:765-824), not raw file/audio/video extension payloads.

Problems

  • gateway/platforms/dingtalk.py:1732 is the final successful-download statement in _extract_and_parse_file_attachments; it falls through without returning the (parts, attachments) tuple unpacked at :932.
  • The PR calls _resolve_single_download_url at :441 and :596, but the full PR diff contains no definition.
  • :369-372 buffers response.content without enforcing the declared _DINGTALK_MEDIA_MAX_SIZE; current shared bounded download logic is in gateway/platforms/base.py:641-669.
  • Current main moved this adapter to plugins/platforms/dingtalk/adapter.py in 5600105478ffde29d7566b45421b100eaa29c4ef; the changed files include no tests.

Suggested changes

  • Salvage the raw-extension extraction into the plugin adapter, use the shared media cache/bounded reader, complete the return and resolver paths, and add fixtures in tests/gateway/test_dingtalk.py.

Automated hermes-sweeper review.

slug = re.sub(r'[^\w]', '', msg_id)[:16] if msg_id else uuid.uuid4().hex[:12]
out_path = os.path.join(_inbound_media_dir(), f"{slug}_{base}{ext}")
async with httpx.AsyncClient(timeout=timeout, follow_redirects=True) as client:
resp = await client.get(url)

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.

This buffers the whole response before writing it, while _DINGTALK_MEDIA_MAX_SIZE is never enforced in this diff. Please use a bounded streaming read (the current shared implementation is gateway/platforms/base.py:_read_httpx_body_with_limit) before caching untrusted attachment data.

continue

mime = _mime_for_file(fname)
attachments.append((local_path, mime))

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.

After a successful attachment download this function falls through and returns None; _on_message unpacks its result as (file_parts, file_attachments). Finish the success path, including parsing/context construction, and explicitly return (parts, attachments).

@teknium1 teknium1 added sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 12, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/gateway Gateway runner, session dispatch, delivery P2 Medium — degraded but workaround exists platform/dingtalk DingTalk adapter sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants