qwen-code/docs/users/features/channels/dingtalk.md
BaboBen edb420393e
Some checks are pending
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 1/2 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 2/2 (push) Waiting to run
E2E Tests / channel-plugin E2E (nightly) (push) Waiting to run
E2E Tests / cron-interactive E2E (nightly) (push) Waiting to run
E2E Tests / web-shell Browser Regression (push) Waiting to run
SDK Java / ubuntu-latest / Java 11 (push) Waiting to run
SDK Java / ubuntu-latest / Java 17 (push) Waiting to run
SDK Java / macos-latest / Java 21 (push) Waiting to run
SDK Java / ubuntu-latest / Java 21 (push) Waiting to run
SDK Java / windows-latest / Java 21 (push) Waiting to run
SDK Java / Real daemon E2E / Java 11 (push) Waiting to run
fix(channels): manage DingTalk interactive card config (#8517)
* fix(channels): manage DingTalk interactive card config

* test(cli): cover nested channel object validation

* fix(channels): harden nested management metadata

* fix(channels): isolate invalid management descriptors

* fix(channels): isolate invalid management metadata from channel runtime

* fix(channels): reject reserved unknown keys in management config upserts

* fix(channels): harden channel management validation and editor checks

Reject management descriptors that lack a fields array at registration so
broken plugins are stripped to unmanageable instead of being advertised as
manageable and failing every upsert with an unmapped TypeError. Reserve the
top-level "type" field key and require enum fields to declare at least one
option, both of which the settings store could never accept. Treat
whitespace-only number drafts as empty in the channel editor, consistent
with the module's other emptiness checks.

Also give the SDK descriptor mirror test a runtime wire-shape walk over the
built-in catalog, add the parser's timeout rejection boundary, and restore
the exact built-in catalog membership assertion.

* fix(channels): validate management field shapes and editor bounds (#8517)

* fix(channels): align management validation layers and pin gate behavior (#8517)

Read envResolvable by truthiness in the settings store so it matches the
registration gate and the editor, instead of rejecting the advertised
environment references of untyped plugins. Fail closed at registration on
non-finite exclusiveMinimum values, empty object property lists, and async
validateConfig functions, all of which would otherwise advertise a field
or save path that can never succeed. Strip invalid management metadata
over a prototype-preserving copy so class-instance plugins keep their
createChannel implementation.

Move the unchanged-value preservation exemption ahead of the object shape
rejection so a stored non-record value (for example a hand-written null)
no longer locks every unrelated management edit of that channel. Clamp
DingTalk question-card timeouts at the maximum setTimeout delay, since
Node treats larger delays as one millisecond and would expire cards
instantly.

Pin the previously untested load-bearing behaviors: per-key previous
threading in the recursive validation, the preservation exemption's
precedence over nested required enforcement, nested "type" properties,
depth-2 nesting rules, and the nested-only constraints of the daemon
descriptor wire contract.

* fix(channels): close reserved-key preservation gaps and pin gate behavior (#8517)

* test(cli): tolerate IPv6-less hosts in serve ::1 bind tests (#8517)

The self-hosted CI containers can have no IPv6 loopback, where the two
runQwenServe tests that bind ::1 fail with EADDRNOTAVAIL. Probe the
interfaces once and skip only the IPv6-dependent binds there; every
assertion still runs on IPv6-capable hosts.

* fix(channels): align descriptor type contracts with runtime validation (#8517)

The registry already rejects object fields without a non-empty
properties array and enums without unique options, but the descriptor
types still admitted both, so TS-authored plugins only learned about
it when registration stripped their management surface. Make
`properties` required, give enums a dedicated descriptor member with
required `options`, and drop the never-honored `envResolvable` flag
from number descriptors, in both channel-base and the SDK mirror, and
export the descriptor sub-types through the webui barrels. Also map a
throwing `validateConfig` to the usual invalid-config error and pin
the store contracts that had no distinguishing tests: omitting a
parent object drops the stored object without checking its nested
required, writes replace nested values wholesale, unchanged stored
scalars are still re-validated, and valid plugins register by
original reference.

* fix(channels): defuse validateConfig rejection leak and close descriptor gate gaps (#8517)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-07 15:47:20 +00:00

8.8 KiB

DingTalk (Dingtalk)

This guide covers setting up a Qwen Code channel on DingTalk (钉钉).

Prerequisites

  • A DingTalk organization account
  • A DingTalk bot application with AppKey and AppSecret (see below)

Creating a Bot

  1. Go to the DingTalk Developer Portal
  2. Create a new application (or use an existing one)
  3. Under the application, enable the Robot capability
  4. In Robot settings, enable Stream Mode (机器人协议 → Stream 模式)
  5. Note the AppKey (Client ID) and AppSecret (Client Secret) from the application credentials page

Stream Mode

DingTalk Stream mode uses an outbound WebSocket connection — no public URL or server is needed. The bot connects to DingTalk's servers, which push messages through the WebSocket. This is the simplest deployment model.

Configuration

Add the channel to ~/.qwen/settings.json:

{
  "channels": {
    "my-dingtalk": {
      "type": "dingtalk",
      "clientId": "$DINGTALK_CLIENT_ID",
      "clientSecret": "$DINGTALK_CLIENT_SECRET",
      "useConnectionManager": true,
      "senderPolicy": "open",
      "sessionScope": "user",
      "cwd": "/path/to/your/project",
      "instructions": "You are a concise coding assistant responding via DingTalk.",
      "groupPolicy": "open",
      "atSender": true,
      "groups": {
        "*": { "requireMention": true }
      }
    }
  }
}

Set the credentials as environment variables:

export DINGTALK_CLIENT_ID=<your-app-key>
export DINGTALK_CLIENT_SECRET=<your-app-secret>

Or define them in the env section of settings.json:

{
  "env": {
    "DINGTALK_CLIENT_ID": "your-app-key",
    "DINGTALK_CLIENT_SECRET": "your-app-secret"
  }
}

Interactive Cards

Add an interactiveCards object to opt in to DingTalk status and question cards. Omitting the object disables interactive cards. When the object is present, the overall switch and both card types default to enabled, and question cards time out after 270,000 milliseconds (270 seconds).

{
  "channels": {
    "my-dingtalk": {
      "type": "dingtalk",
      "clientId": "$DINGTALK_CLIENT_ID",
      "clientSecret": "$DINGTALK_CLIENT_SECRET",
      "interactiveCards": {
        "enabled": true,
        "statusCard": { "enabled": true },
        "questionCard": {
          "enabled": true,
          "timeoutMs": 270000
        }
      }
    }
  }
}

Set interactiveCards.enabled to false to disable all interactive cards. Use statusCard.enabled or questionCard.enabled to disable one card type, and set questionCard.timeoutMs to a finite positive number to change how long Qwen Code waits for a question-card response. Values above 2,147,483,647 milliseconds (about 24.8 days) are capped at that maximum. Interactive cards are configured through settings.json or the management API; the Web Shell channel editor does not render them, and it preserves the stored object when you edit other fields.

Connection Recovery

useConnectionManager defaults to true. The connection manager monitors the Stream WebSocket and replaces the DingTalk SDK client when the connection stops responding. You should normally leave it enabled.

Set "useConnectionManager": false to disable Qwen Code's connection manager and fall back to the SDK's keepalive and automatic reconnect behavior.

Running

# Start only the DingTalk channel
qwen channel start my-dingtalk

# Or start all configured channels together
qwen channel start

Open DingTalk and send a message to the bot. You should see a 👀 emoji reaction appear while the agent processes, followed by the response.

Daemon Webhook Delivery

When the channel runs under qwen serve, authenticated external Webhook events can trigger unattended agent tasks and deliver the final Markdown response to either a DingTalk user or group. Use the existing Webhook target fields; no separate channel type is required:

{
  "webhooks": {
    "sources": {
      "manual-test": {
        "secretEnv": "QWEN_CHANNEL_DINGTALK_TEST_SECRET",
        "targets": {
          "operator": {
            "chatId": "DINGTALK_USER_ID",
            "senderId": "webhook:manual-test",
            "isGroup": false
          },
          "team": {
            "chatId": "OPEN_CONVERSATION_ID",
            "senderId": "webhook:manual-test",
            "isGroup": true
          }
        }
      }
    }
  }
}

Every target must set isGroup explicitly. For a direct message, chatId is the recipient's DingTalk user ID. For a group message, chatId is the group's openConversationId. Thread targets and incoming robot Webhook URLs are not supported for proactive delivery. See Webhook-triggered tasks for the complete channel configuration and request format.

Group Chats

DingTalk bots work in both DM and group conversations. To enable group support:

  1. Set groupPolicy to "allowlist", "pairing", or "open" in your channel config
  2. Add the bot to a DingTalk group
  3. @mention the bot in the group to trigger a response
  4. If using groupPolicy: "pairing", approve the group's pairing request once before responses start

By default, the bot requires an @mention in group chats (requireMention: true). Set "requireMention": false for a specific group to make it respond to all messages. See Group Chats for full details.

Set "atSender": true to have the bot @mention the member whose group message triggered its response. It is off by default and only applies to agent replies with a DingTalk staff ID. Replies are sent as DingTalk markdown whether or not they carry a mention; the mention prefix is included in the first message chunk.

Finding a Group's Conversation ID

DingTalk uses conversationId to identify groups. You can find it in the channel service logs when someone sends a message in the group — look for the conversationId field in the log output.

Images and Files

You can send photos and documents to the bot, not just text.

Photos: Send an image (screenshot, diagram, etc.) and the agent will analyze it using its vision capabilities. This requires a multimodal model — add "model": "qwen3.5-plus" (or another vision-capable model) to your channel config. DingTalk supports sending images directly or as part of rich text messages (mixed text + images).

Files: Send a PDF, code file, or any document. The bot downloads it from DingTalk's servers and saves it locally so the agent can read it with its file tools. Audio and video files are also supported. This works with any model.

Key Differences from Telegram

  • Authentication: AppKey + AppSecret instead of a static bot token. The SDK manages access token refresh automatically.
  • Connection: WebSocket stream instead of polling — no public IP or webhook URL needed.
  • Formatting: Responses use DingTalk's markdown dialect. Markdown tables are passed through to the DingTalk client; long messages are split into chunks at ~3800 characters.
  • Working indicator: A 👀 emoji reaction is added to the user's message while processing, then removed when the response is sent.
  • Media download: Two-step process — a downloadCode from the message is exchanged for a temporary download URL via DingTalk's API.
  • Groups: DingTalk uses isInAtList for @mention detection instead of parsing message entities.

Tips

  • Use DingTalk markdown-aware instructions — DingTalk supports headings, bold text, links, code blocks, and tables. Keep tables compact because narrow screens may scroll horizontally.
  • Restrict access — In an organization context, senderPolicy: "open" may be acceptable. For tighter control, use "allowlist" or "pairing". See DM Pairing for details.
  • Referenced messages — Quoting (replying to) a user message includes the quoted text as context for the agent. Quoting bot responses is not yet supported.

Troubleshooting

Bot doesn't connect

  • Verify your AppKey and AppSecret are correct
  • Check that the environment variables are set before running qwen channel start
  • Make sure Stream Mode is enabled in the bot's settings on the DingTalk Developer Portal
  • Check the terminal output for connection errors

Bot doesn't respond in groups

  • Check that groupPolicy is set to "allowlist", "pairing", or "open" (default is "disabled")
  • If using "pairing", verify the group's pairing request has been approved
  • Make sure you @mention the bot in the group message
  • Verify the bot has been added to the group

"No sessionWebhook in message"

This means DingTalk didn't include a reply endpoint in the message callback. This can happen if the bot's permissions are misconfigured. Check the bot's settings in the Developer Portal.

"Sorry, something went wrong processing your message"

This usually means the agent encountered an error. Check the terminal output for details.