Qwen Code 接入钉钉(DingTalk):Stream 模式机器人频道配置与使用实战指南
【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code
本篇技术指南讲解如何将开源 AI 编程代理 Qwen Code 以钉钉机器人频道(channel)的形式接入钉钉组织,实现"在钉钉聊天窗口里直接指挥编程助手"的工作方式。内容覆盖从钉钉开发者后台创建 Stream 模式机器人、在~/.qwen/settings.json中完成频道配置,到交互卡片、群聊、图片与文件收发、聊天记录转发、Webhook 驱动无人值守任务等完整功能链路,并深入剖析其底层实现原理。读完本文,你将能够独立搭建一个稳定可用的钉钉机器人频道,并理解其长消息分片、连接恢复、媒体下载等关键机制。
频道概念与前置条件
Qwen Code 采用频道(channel)抽象来对接各类即时通讯平台,钉钉是其中一等公民。频道适配器以独立包形式存在于仓库中:packages/channels/dingtalk,其插件定义(packages/channels/dingtalk/src/index.ts)声明了频道类型为dingtalk,并将clientId与clientSecret设为必填配置项。
搭建钉钉频道需要满足以下前置条件:
- 一个钉钉组织(企业)账号,且具备在开发者后台创建应用的权限;
- 一个已开启**机器人(Robot)**能力、并启用了Stream 模式的应用,且能获取到该应用的AppKey(Client ID)与AppSecret(Client Secret)。
从源码看,缺少凭据时频道无法启动:DingtalkChannel构造函数会直接抛出Channel "..." requires clientId and clientSecret for DingTalk.(见 packages/channels/dingtalk/src/DingtalkAdapter.ts),且插件声明了requiredConfigFields: ['clientId', 'clientSecret'],这两项是硬性依赖。
创建机器人应用与 Stream 模式
创建应用并启用机器人
- 打开钉钉开放平台开发者后台(open-dev.dingtalk.com),创建新应用(或复用已有应用);
- 在应用能力中启用**机器人(Robot)**能力;
- 在机器人设置中开启Stream 模式(机器人协议 → Stream 模式);
- 在应用凭据页面记录AppKey(Client ID)与AppSecret(Client Secret)。
Stream 模式:无需公网地址的出站连接
钉钉 Stream 模式的核心价值在于免公网部署:机器人主动向钉钉服务器发起出站 WebSocket 连接,钉钉通过这条 WebSocket 把消息推送给机器人。整个过程不需要公网 IP、不需要域名、不需要配置 Webhook 回调 URL,也无需内网穿透,是部署成本最低的接入模型。
这一机制在源码中有直接体现:DingTalk 频道依赖官方 SDKdingtalk-stream-sdk-nodejs(见 packages/channels/dingtalk/package.json),并注册了TOPIC_ROBOT(机器人消息)与TOPIC_CARD(卡片回调)两类订阅主题(packages/channels/dingtalk/src/DingtalkAdapter.ts)。连接成功后,频道会输出Connected via stream.日志(同文件第 1346 行)。
频道配置
基础配置
在~/.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 } } } } }各字段作用如下:
| 字段 | 取值 | 说明 |
|---|---|---|
type | dingtalk | 频道类型,必须与插件声明的channelType一致 |
clientId | AppKey | 支持环境变量引用($DINGTALK_CLIENT_ID) |
clientSecret | AppSecret | 支持环境变量引用 |
useConnectionManager | true/false | 是否启用连接管理器,默认true |
senderPolicy | open/allowlist/pairing | 发信人访问控制策略,组织内场景可放开为open |
sessionScope | user等 | 会话作用域 |
cwd | 工作目录绝对路径 | 机器人执行命令与读写文件的根目录 |
instructions | 任意字符串 | 注入给 Agent 的系统提示,用于约束语气与行为 |
groupPolicy | disabled/allowlist/pairing/open | 群聊策略,默认disabled |
atSender | true/false | 回复时是否 @ 触发消息的成员,默认false |
groups | 对象 | 按群配置requireMention等行为 |
凭据注入的两种方式
方式一:以环境变量注入(与配置中的$DINGTALK_CLIENT_ID引用配合):
export DINGTALK_CLIENT_ID=<your-app-key> export DINGTALK_CLIENT_SECRET=<your-app-secret>方式二:直接在settings.json的env段定义:
{ "env": { "DINGTALK_CLIENT_ID": "your-app-key", "DINGTALK_CLIENT_SECRET": "your-app-secret" } }从插件声明(packages/channels/dingtalk/src/index.ts)可以看出,clientId与clientSecret都是envResolvable: true的字段,环境变量引用是官方支持的一等配置方式,便于把密钥与配置文件分离。
交互卡片(Interactive Cards)
钉钉频道支持状态卡(status card)与提问卡(question card)两类交互卡片。是否启用完全由interactiveCards对象是否存在决定:省略该对象则整组卡片关闭;只要对象存在,总开关与两类卡片默认全部开启,提问卡默认超时时间为270,000 毫秒(270 秒)。
{ "channels": { "my-dingtalk": { "type": "dingtalk", "clientId": "$DINGTALK_CLIENT_ID", "clientSecret": "$DINGTALK_CLIENT_SECRET", "interactiveCards": { "enabled": true, "statusCard": { "enabled": true }, "questionCard": { "enabled": true, "timeoutMs": 270000 } } } } }控制粒度和边界条件:
interactiveCards.enabled设为false可关闭全部交互卡片;statusCard.enabled与questionCard.enabled可单独关闭某一类卡片;questionCard.timeoutMs设为有限正数可改变等待提问卡响应的时长;超过2,147,483,647 毫秒(约 24.8 天)的值会被封顶到该上限;- 交互卡片只能通过
settings.json或管理 API 配置;Web Shell 的频道编辑器不会渲染卡片字段,但在编辑其他字段时会原样保留已存储的卡片配置对象。
源码层面,卡片配置由parseDingtalkInteractiveCardConfig解析(packages/channels/dingtalk/src/DingtalkAdapter.ts),并根据开关分别实例化StatusCardController、QuestionCardController与PermissionCardController(同文件第 1040-1084 行);卡片回调走TOPIC_CARD主题,按钮动作(如btn_stop停止运行)通过routeCardCallback分发给对应控制器(同文件第 1198-1220 行)。
连接恢复(Connection Recovery)
useConnectionManager默认值为true。启用后,连接管理器会持续监控 Stream WebSocket 的活动状态,一旦连接停止响应,就替换掉整个 DingTalk SDK 客户端并重建连接——这比单纯依赖 SDK 自身的 keepalive 更激进也更可靠。
建议平时保持开启。若设"useConnectionManager": false,则退化为 SDK 自带的 keepalive 与自动重连行为。
源码佐证:createClient中keepAlive与client.config.autoReconnect均取!useConnectionManager(packages/channels/dingtalk/src/DingtalkAdapter.ts);连接管理器实现在 packages/channels/dingtalk/src/DingtalkConnectionManager.ts,当收到SYSTEM disconnect帧时还会主动触发requestReconnect(DingtalkAdapter.ts)。
后台 Agent 响应
当后台(Background)Agent 产生输出时,频道会在每个响应分段可用时立即发送,而不是等整轮结束。每条消息都会标注 Agent 名称,保证并发工作时每条输出都可以追溯到来源。
启动运行
启动频道使用qwen channel命令:
# 只启动钉钉频道 qwen channel start my-dingtalk # 或者一次性启动所有已配置的频道 qwen channel start启动后,在钉钉中向机器人发送一条消息即可验证。处理期间,机器人会给你的消息加上一个 👀 表情反应(处理中的工作指示),随后返回正式回复。这一"处理中指示"在源码中有完整实现:ACK_REACTION_NAME = '👀',配合状态表情(❌ Failed、⏹️ Stopped、✅ Done)通过钉钉 emotion API 施加/移除(packages/channels/dingtalk/src/DingtalkAdapter.ts)。
Daemon Webhook 投递:无人值守任务触发
当频道运行在qwen serve(Daemon)模式下,经过鉴权的外部 Webhook 事件可以触发无人值守的 Agent 任务,并把最终 Markdown 回复投递到钉钉用户或群。这里复用已有的 Webhook 目标字段,不需要单独的频道类型:
{ "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 } } } } } }要点约束:
- 每个 target 必须显式设置
isGroup; - 单聊投递:
chatId填收件人的钉钉用户 ID; - 群聊投递:
chatId填群的openConversationId; - 线程(thread)目标与机器人入站 Webhook URL 不支持用于主动投递。
完整的频道配置与请求格式请参阅 Webhook 触发任务。
群聊支持
钉钉机器人同时支持单聊(DM)与群聊。启用群聊的步骤:
- 将频道配置中的
groupPolicy设为"allowlist"、"pairing"或"open"(默认是"disabled"); - 把机器人添加到钉钉群;
- 在群里@ 机器人以触发响应;
- 若使用
groupPolicy: "pairing",需要先审批一次群的配对请求,之后才会开始响应。
默认情况下,群聊中机器人要求被 @ 才响应(requireMention: true)。若希望某个群对所有消息都响应,可在该群配置中设"requireMention": false。完整群聊语义见 Group Chats。
@ 发送者(atSender):设"atSender": true后,机器人回复时会 @ 触发该轮响应的群成员。该选项默认关闭,且仅对带有钉钉 staffId 的 Agent 回复生效。无论是否携带 @,回复都以钉钉 markdown 格式发送;@ 前缀包含在第一条消息分片中。
关于被 @ 文本的处理细节:Qwen Code 在构造规范消息时原样保留钉钉提供的文本内容,不会自行删除前导的 @提及。因此:
- 当钉钉在纯文本回调中省略了机器人 @ 时,像
/clear或!command这样的正文仍以命令标记开头,遵循正常的本地命令规则; - 当回调保留了前导 @(富文本回调可能如此)时,
@Bot /clear与@Bot !command会被当作普通 Agent 输入,因为规范文本不以/或!开头; - 群消息是否真正指向机器人,始终由
isInAtList字段决定。
查找群的会话 ID
钉钉使用conversationId标识群。当有人在群里发消息时,可以从频道服务的日志中找到它——在日志输出中查找conversationId字段即可。从源码看,群消息缺少conversationId时会被判定为不可路由而丢弃(isUnroutableGroupMessage,见 packages/channels/dingtalk/src/DingtalkAdapter.ts),因为会话无法落到稳定的共享 session 上。
图片与文件收发
钉钉频道支持文字之外的多种消息类型。
图片(照片):发送图片(截图、示意图等)后,Agent 会利用视觉能力分析图片内容。这要求频道配置使用多模态模型,例如在频道配置中加入"model": "qwen3.5-plus"(或其他支持视觉的模型)。钉钉支持直接发送图片,也支持在富文本消息中混发文字与图片。
文件:发送 PDF、代码文件或任意文档,机器人会从钉钉服务器下载并保存到本地,Agent 随后可用文件工具读取。音频与视频文件同样受支持,且不要求多模态模型。
生成的附件:显式要求 Agent 发送某个已完成的本地产物文件时,Agent 可以将其作为钉钉原生附件返回。约束如下:
- 文件必须非空;
- 单文件不超过20 MB;
- 文件必须位于配置的工作区内或系统临时目录内;
- 单条回复最多发送5 个文件;
- 上传或投递失败时,会在最终文本中以
[File delivery failed: <name>]之类的提示反馈,而不是静默丢失。
源码中,Agent 通过[FILE: /absolute/path/to/file]与[IMAGE: /absolute/path/to/file.png]标记触发文件/图片投递(见 DingtalkAdapter.ts 中的IMAGE_INSTRUCTIONS与FILE_INSTRUCTIONS注入指令),文件校验与上传逻辑位于 outbound-file.ts 与 outbound-image.ts。
转发聊天记录(合并转发)
你可以把另一段会话的**合并转发(combined forward)**消息发给机器人——既可以作为独立消息,也可以作为"回复"的消息。机器人会把记录展开成文本交给 Agent:
- 记录的标题与摘要变成一行标题;
- 每条转发消息以
Sender: message的形式列在[Chat record messages]之下; - 非文本正文显示为占位符:
[image]、[file: <name>]、[audio]、[video]。
长度上限(有上限且会明示):
| 维度 | 上限 |
|---|---|
| 转发消息条数 | 最多 50 条 |
| 总字符数 | 最多 4000 字符 |
| 单条消息 | 最多 500 字符 |
被截断的内容会在同一段文本中告知 Agent:
- 被丢弃的消息追加一行
[N more message(s) not shown]; - 被缩短的单条消息追加
[truncated]标记。
因此 Agent 明确知道自己是在回答一份不完整的记录;如果需要完整内容,请分批转发。
回复引用的记录:如果你回复(引用)一条记录,它会被当作引文处理,而非原文发送。引文在所有频道上都被限制为500 字符——所以回复引用的记录按 500 字符预算渲染(而不是 4000 字符),并在该预算内做同样的截断宣告。回复引用的记录通常只携带标题和一两条消息;如需让 Agent 看到完整内容,请把记录作为独立消息转发。
安全设计:转发记录由其他人书写,因此从记录中提取的一切内容——标题、发送者姓名、消息正文——在到达 Agent 之前都会被**中和(neutralize)**处理,转发消息无法伪装成对机器人的指令。这一处理在源码中有完整实现:sanitizeChatRecordField、bracketSafeChatRecordField与startOfLineSafeChatRecordField三个函数层层防御(DingtalkAdapter.ts),上限常量MAX_CHAT_RECORD_ENTRIES = 50、MAX_CHAT_RECORD_CHARS = 4000、MAX_CHAT_RECORD_LINE_CHARS = 500也在同文件第 297-299 行定义。
群聊中的差异:以上多行布局是 1:1 单聊中 Agent 看到的形态。在群里,整条消息在到达 Agent 前会再被中和一次,结果会折叠成单行,并去掉标记外的方括号;内容与截断宣告不变。
与 Telegram 频道的关键差异
如果你熟悉 Qwen Code 的 Telegram 频道,钉钉频道的差异点如下:
| 维度 | 钉钉 | Telegram |
|---|---|---|
| 鉴权 | AppKey + AppSecret,SDK 自动刷新 access token | 静态 bot token |
| 连接方式 | WebSocket Stream,无需公网 IP 与 Webhook URL | 轮询 |
| 消息格式 | 钉钉 markdown 方言;表格透传;长消息按约 3800 字符分片 | — |
| 处理中指示 | 给用户消息添加 👀 表情反应,回复发出后移除 | — |
| 媒体下载 | 两步:先用消息中的downloadCode换取临时下载 URL | — |
| 群 @ 检测 | 依赖isInAtList字段 | 解析消息实体 |
其中3800 字符分片与代码块跨片处理在 packages/channels/dingtalk/src/markdown.ts 中实现:DINGTALK_CHUNK_LIMIT = 3800,分片算法会跟踪代码围栏(code fence)状态,在跨分片处自动关闭并重新打开 ````` ``` ````,保证代码块在钉钉客户端渲染不破;escapeDingTalkMarkdown负责转义钉钉 markdown 中的特殊字符(同文件第 11-13 行)。
使用建议
- 使用钉钉 markdown 感知的指令:钉钉支持标题、加粗、链接、代码块与表格。回复中表格尽量紧凑,因为窄屏下表格可能横向滚动。
- 控制访问范围:组织场景下
senderPolicy: "open"或许可接受;如需更严的控制,使用"allowlist"或"pairing"。参见 DM Pairing。 - 引用消息:回复(引用)用户消息时,引文文本会作为上下文交给 Agent。富文本引文保留文本顺序并附带内嵌图片;若被引消息是图片、文件、音频或视频消息,机器人会像直接发送一样下载并附加。暂不支持引用机器人自己的回复。
故障排查
机器人无法连接
- 核对 AppKey 与 AppSecret 是否正确;
- 确认运行
qwen channel start之前环境变量已设置好; - 确认钉钉开发者后台的机器人设置中已开启Stream 模式;
- 查看终端输出中的连接错误信息。
群里机器人不响应
- 检查
groupPolicy是否设置为"allowlist"、"pairing"或"open"(默认是"disabled"); - 若使用
"pairing",确认群的配对请求已审批; - 确认群消息中确实@ 了机器人;
- 确认机器人已被添加到该群。
报错 "No sessionWebhook in message"
说明钉钉在消息回调中没有附带回复端点(sessionWebhook)。这通常是机器人权限配置不正确导致的——请在开发者后台检查机器人权限设置。
报错 "Unable to process this message"
回复会指出失败类别并给出下一步建议。若问题持续,请把回复中显示的参考号(reference)交给机器人管理员;频道进程日志中详细错误旁边会出现同一个参考号。这一机制在源码中由presentInboundError实现:它会将错误归类为"机器人配置错误""请求超时""服务繁忙""服务暂时不可用"等类别,并附上参考号(DingtalkAdapter.ts),方便定位问题。
小结
DingTalk 频道让 Qwen Code 获得了零公网部署的即时通讯入口:Stream 模式免去网络暴露,AppKey/AppSecret 鉴权由 SDK 托管,连接管理器保障长连接稳定,3800 字符智能分片与代码围栏跨片处理保证长回复可读,表情反应与交互卡片完善了"处理中/提问/权限"的交互闭环,而图片文件收发与聊天记录转发则把多模态分析与上下文搬运带进了聊天窗口。结合 频道总览 与其他频道文档,你可以按同样模式扩展 Telegram、飞书、企业微信等更多入口。
【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考