贰 · 消息
钉钉消息
ctx.dingtalk
通过钉钉 dws CLI 的 user 身份发送消息;统一监听群内 @ 消息,并按群 ID 与内容规则下发给业务插件。
接口
const r = await ctx.dingtalk.send({
text: '纯文本', // 或 markdown: '## 标题\n正文'
target: { chatId: 'cid...' }, // 或 { userId } / { openDingtalkId }
at: { openDingtalkIds: ['...'] }, // 也支持 all: true
idempotencyKey: 'evt_...',
traceId: 'evt_...',
signal,
})
// r.results: Array<{ target, ok, messageId?, error? }>仅支持当前 dws 登录的 user 身份,不提供机器人或 webhook 身份。target 省略时使用配置的 defaultTarget;目标、@ 列表和幂等键会先经过格式校验。给出 idempotencyKey 时,可重试的失败最多重试 retry.maxAttempts 次;否则不自动重试,避免重复发送。
查找业务插件的转发目标
业务插件可以用 ctx.dingtalk.searchRecipients('group', '群名关键词') 或 searchRecipients('user', '姓名') 查询当前登录账号可见的候选。返回项带真实 { chatId }、{ userId } 或 { openDingtalkId } 和 complete 标记;重名时由配置人员明确选择,不自动采用第一项。业务插件也可在自己的设置字段上声明 kind: 'dingtalk-target',让本机设置页提供群/个人搜索与选择。此操作只保存业务插件自己的发送目标,不影响下方“群消息进入哪个插件”的路由;查找不会发送消息。
登录状态与登录
const status = await ctx.dingtalk.status()
const session = await ctx.dingtalk.login()
await showToUser(session.verificationUrl)
const after = await session.completed
await ctx.dingtalk.logout()login() 启动 dws auth login --device --no-browser 并返回真实授权链接;扫码授权完成后 completed 返回最新状态。logout() 只退出当前账号,不退出同机的其他 dws 账号。启动时以及每隔 preflightIntervalMs 检查登录状态;dryRun: true 时不真实发送,也跳过启动登录检查。
设置页把 dws 返回的授权链接在本机生成二维码;二维码不是模拟授权,但是否能扫码并完成授权仍需在具备组织权限的真实账号上验证。页面不会单独展示 identity、defaultTarget 或 dryRun;这些仍是配置/API 字段,业务插件发送时应明确选择目标。
dwsPath 必须是可执行文件的绝对路径;省略时从 PATH 查找。Windows 上可以自动解析 npm 的 dws.cmd 到实际的 dws.js,手动指定时请直接指向 .exe 或 .js。
群内 @ 消息路由
在 Agent Kit 设置页的钉钉卡片中,可添加“群 ID → 业务插件 ID”映射,并从未命中消息选群。群名仅用于展示。插件用相同 ID 注册接收器:
export const inject = ['dingtalk']
export async function apply(ctx: Context) {
const sub = ctx.dingtalk.onPluginMessage('business-orders', async (message, { signal }) => {
await handleOrder(message.content, { eventId: message.eventId, signal })
})
await sub.ready
}对应配置为 groupRoutes: [{ conversationId: 'cid-orders', pluginId: 'business-orders' }]。一个群只会转给一个目标插件;目标插件未注册时记入未命中,不回退给其他插件。设置页的业务插件列表提供 ID 输入建议,但登记设置项不等于注册了消息接收器。
兼容旧接口:业务插件仍可用 ctx.dingtalk.onMessage({ conversationId, match?, priority? }, handler) 在代码中注册规则,设置页配置的群优先走指定业务插件。旧接口先按群的 conversationId 匹配,再由可选的 match 判断正文;数值更高的 priority 优先。每条消息只交给第一个命中的处理器,不会广播给所有插件。
export const inject = ['dingtalk']
export async function apply(ctx: Context) {
const sub = ctx.dingtalk.onMessage(
{ conversationId: 'cid-orders', match: (m) => m.content.startsWith('工单') },
async (message, { signal }) => {
await handleOrder(message.content, { eventId: message.eventId, signal })
},
)
await sub.ready
}Agent Kit 为全部业务插件共用一个常驻的 dws @我 事件监听进程。插件卸载时自动注销规则,但监听继续记录未命中的消息,Service 卸载时退出。最近 200 条未命中记录会保留群 ID、可查到的群名、时间和最多 240 字预览;可在本机设置页的钉钉卡片查看,或调用 ctx.dingtalk.unmatchedMessages() 读取。dsh Profile 下按 dws 账号分别保存于 .agent-kit/dingtalk-unmatched/<账号哈希>.json,不会自动转发给其他插件。message.content 是不可信输入;业务插件负责校验、决定是否回复。进程内按 eventId 去重,但重启后仍可能重复投递,业务处理需自行幂等。dryRun 模式不能启动监听。
本地测试覆盖了监听与路由流程;真实钉钉租户的授权、群事件收取和消息发送仍需用具备 CLI 数据访问权限的账号端到端验收。
配置
在 Profile 的 cordis.patch.yml 中按 id agent-kit-dingtalk 启用并给出配置。按 id 修改 config 时整段替换,未给出的字段使用默认值。下表由本包源码中的配置 schema 生成。
- id: agent-kit-dingtalk
disabled: false
config:
# 只写需要改的字段,其余使用下表的默认值 | 字段 | 默认值 | 取值范围 | 说明 |
|---|---|---|---|
identity | user | — | 仅支持已登录的 user 身份 |
defaultTarget | — | 见说明 | 省略 target 时使用 |
groupRoutes | [] | 见说明 | 群 ID 到业务插件 ID 的一对一路由;可在 Agent Kit 设置页配置 |
dwsPath | — | — | dws 可执行文件的绝对路径;默认从 PATH 解析。需指向可执行文件、.exe 或 .js(不支持 Windows 的 .cmd/.bat,因为不经过 shell 启动子进程) |
timeoutMs | 15000 | [1000, 120000] | 单次 dws 调用的超时时间。 |
killGraceMs | 5000 | ≥ 0 | 超时或卸载时,SIGTERM 之后等待多久再发 SIGKILL。 |
retry.maxAttempts | 2 | [0, 5] | 给出幂等键时,可重试失败的最多重试次数。 |
preflightIntervalMs | 3600000 | ≥ 0 | 检查 dws 登录态的间隔,0 表示只在启动时检查 |
dryRun | false | — | 附加 --dry-run,只解析参数、不真实发送;同时跳过登录态检查。 |
约束与说明
- 自动重试只在给出 idempotencyKey 时进行,避免重复发送。
- status() 实时检查登录态;login() 发起设备流登录,拿到授权链接即返回,由业务包决定怎么交给要登录的人;logout() 退出登录。
- dws 的登录态是本机共享的,同一系统用户下的其他 dws 程序也会看到登录与退出;logout() 只退出当前账号(--profile=<corpId>:<userId>)。
- groupRoutes 按群 ID 指定业务插件;插件用 onPluginMessage(pluginId, handler) 接收,未命中消息可在本机设置页查看。onMessage() 兼容代码注册的细分规则。
- searchRecipients(group | user, query) 查找群或个人候选;业务插件的 dingtalk-target 设置字段只保存选中的稳定 ID,接收路由与发送目标独立。
错误码
所有错误都是 KitError:用 isKitError(e) 判断,按 code 与 retryable 决定是否重试。
| code | 可重试 | 含义 |
|---|---|---|
timeout | 是 | dws 超时,子进程按「先 SIGTERM、宽限后 SIGKILL」回收。 |
exit_nonzero | 视情况 | dws 非零退出;按 dws 错误类别(network / timeout / rate_limit / server / unavailable)判断是否可重试。 |
bad_output | 否 | dws 输出无法解析。 |
invalid_target | 否 | 目标、@ 列表或幂等键格式非法。 |
send_failed | 否 | dws 报告发送失败。 |
aborted | 否 | 调用方 signal 中止或 Service 卸载。 |
spawn_failed | 否 | 无法启动 dws 子进程。 |
login_failed | 否 | dws auth login 在给出授权链接之前就结束了。 |
invalid_config | 否 | 配置或环境变量非法,Service 启动失败。 |