#3156·CowAgent

[Feature] DingTalk channel improvements: file/image receive, streaming output & markdown cards, QR-code login

Author: cowagentCreated Sep 14, 2026Updated Sep 14, 2026

Background

Developer feedback on the DingTalk channel (channel/dingtalk/) has produced three improvement requests that are closely related — they all touch the same channel, the same DingTalk open-platform capabilities, and the same permission/compatibility constraints. They are tracked together here rather than as three separate issues.

  1. Receive files and images. The channel currently only parses a subset of incoming message types.
  2. Streaming output and markdown card rendering. Replies are sent once, after the model has finished, and card markdown is rendered inconsistently.
  3. QR-code login. Onboarding requires the user to copy credentials by hand from the DingTalk developer console.

Each item is described in detail below.


1. Support receiving file and image messages

Current behaviour

In channel/dingtalk/dingtalk_message.py, DingTalkMessage maps message_type as follows:

Incoming message_type Handling
text ContextType.TEXT
audio speech-to-text, then ContextType.TEXT
picture ContextType.IMAGE (image downloaded to tmp)
richText text + downloaded images attached as [图片: path]
anything else falls through to ContextType.IMAGE with content [未找到图片]

So image receive already works (single picture and images embedded in richText), but there is no branch for files.

Problem

  1. Files cannot be received. When a user sends a document (PDF/Word/Excel/zip/txt, ...) to the bot, DingTalk delivers it with a different message_type (e.g. file) that has no branch in DingTalkMessage. It falls through to the generic else, is mis-typed as ContextType.IMAGE, and the content becomes [未找到图片]. The user's attachment is silently dropped and the agent has nothing to work with.
  2. No download path for incoming files. download_image_file() only handles picture downloadCodes; there is no equivalent for files, so even if the type were recognised the file would not be fetched into the workspace tmp dir. (Compare: the outgoing side already uploads files via upload_media(..., media_type="file") and sampleFile, so send works — only receive is missing.)

Expected behaviour

  • Recognise the incoming file message_type, download the attachment into the workspace tmp dir, and expose it to the agent (same pattern as images: cache it and attach a [文件: path] reference to the following text query, via channel.file_cache).
  • Keep the existing behaviour for text / audio / picture / richText unchanged.
  • Images: verify the picture path against the current DingTalk Robot message spec and make sure the download works for both downloadCode and direct URLs.

Acceptance criteria

  • A file sent to the bot in both single chat and group chat is downloaded and made available to the agent.
  • Images continue to work; unsupported types produce an explicit log line instead of a misleading [未找到图片].
  • Added/updated tests cover the new message-type mapping.

2. Streaming output and markdown card improvements

Current behaviour

The DingTalk channel sends replies in one of two ways (channel/dingtalk/dingtalk_channel.py):

  • Default (dingtalk_card_enabled = false): reply_text(reply.content, incoming_message) — a plain text message.
  • With dingtalk_card_enabled = true: generate_button_markdown_content() builds a single markdown string which is posted once via reply_ai_markdown_button(...).

Either way the whole answer is produced and sent once, after the model has finished. There is no incremental delivery.

Problems

  1. No streaming output. Other channels already stream (see agent/chat/service.py send_chunk_fn and the Feishu progress-card path in channel/feishu/). DingTalk users wait for the full answer with no feedback. For long answers this is a noticeably worse experience than Web / Feishu.
  2. Markdown rendering is inconsistent. generate_button_markdown_content() embeds reply.content verbatim inside a fixed template. Notes:
    • Tables, nested lists, fenced code blocks and long lines are not guaranteed to render correctly in the AI card; DingTalk's markdown support is a subset of GitHub-flavoured markdown.
    • When dingtalk_card_enabled is false, the text is sent as plain text, so markdown is shown raw.
    • In group chat the card path also posts a 您有一条新的消息,请查看。 notice; combined with the card this can read as duplicated content.
  3. Card status is never finalised. CustomAICardReplier.start() sets flowStatus = AICardStatus.PROCESSING but nothing transitions it to the finished state, so a card may keep showing the "processing" indicator after the answer is complete.

Expected behaviour

  • Stream the answer into the AI card incrementally (create card → append/update content → mark finished), mirroring the approach already used for Feishu.
  • Render markdown in a way that is safe for DingTalk's supported subset (or degrade gracefully to plain text when a feature is unsupported).
  • Properly close the card lifecycle (flowStatus → finished) so it does not stay in "processing".

Acceptance criteria

  • With streaming enabled, the card updates as tokens arrive and is finalised when the turn ends.
  • Markdown renders correctly for common cases (code blocks, lists, tables) or degrades explicitly.
  • Card never remains in the PROCESSING state after completion.
  • Behaviour with dingtalk_card_enabled = false is unchanged.

3. QR-code login / easier onboarding

Current behaviour

Setting up the DingTalk channel today requires the user to go through the DingTalk developer console by hand: create an app, open 凭证与基础信息, and copy Client ID and Client Secret into config.json (dingtalk_client_id / dingtalk_client_secret). See docs/zh/channels/dingtalk.mdx.

Problem

This manual flow is the main friction point for new users and a common source of setup mistakes (wrong credential pair, insufficient Robot permissions, missing stream-mode configuration). Other channels / clients in this project offer guided onboarding, so DingTalk is comparatively harder to get running.

Proposal

Provide a QR-code / scan-to-login onboarding path for the DingTalk channel:

  • Generate a QR code that the user scans with the DingTalk mobile app, in order to authorise and provision the bot credentials automatically, instead of copying Client ID / Client Secret by hand.
  • After authorisation, write the resulting credentials into the config the same way the manual flow does, so runtime behaviour is unchanged.
  • Keep the existing manual configuration working as a fallback.

Acceptance criteria

  • A user can complete DingTalk channel setup by scanning a QR code, without manually copying credentials.
  • Existing manual dingtalk_client_id / dingtalk_client_secret configuration keeps working.
  • Credentials are never printed to logs or committed to the repository.

Open question

Is this intended to be implemented inside the CowAgent runtime, or in the desktop / web console onboarding UI? Worth aligning before implementation.


Permissions / compatibility notes (apply to all three items)

  • Permissions. Items 1 and 2 require the corresponding message / AI-card permissions to be enabled for the Robot in the DingTalk developer console; item 3 depends on the DingTalk open-platform authorisation flow available to the developer account. When a capability is unavailable, the channel must log an explicit error and fall back to the current behaviour rather than failing or silently dropping traffic.
  • Compatibility. Existing behaviour must be preserved: dingtalk_card_enabled = false stays plain text, existing manual credential configuration keeps working, and already-configured installs must not break. New branches should be additive.
  • SDK. The incoming payload shape (event.message_type, event.get_image_list(), ...) and the card update API are both defined by the dingtalk_stream SDK; the supported version should be verified and guarded/pinned accordingly.
  • Secrets. Credentials obtained through any of these paths are secrets: store them the same way as the manual ones, never log them, and never write them into git-tracked files.