柒 · 收发
企业微信
ctx.wecom
企业微信智能机器人:以 Markdown 发送消息,长连接接收群内 @ 机器人与单聊消息,按会话分发;可直接回复收到的消息。
接口
const r = await ctx.wecom.send({
markdown: '## 标题\n正文', // 或 text: '纯文本'(同样作为 Markdown 内容发送)
title: '库存告警', // 可选:加粗首行
target: { chatId: 'wrk…' }, // 或 { userId: 'zhangsan' }
at: { userIds: ['zhangsan'], all: false },
idempotencyKey: 'evt_...',
traceId: 'evt_...',
signal,
})
// r.results: Array<{ ok, messageId?, error? }>text 与 markdown 二选一,都以 Markdown 发送,内容上限 20480 字节。target 省略时使用配置的 defaultTarget;群为 chatId,单聊为 userId。idempotencyKey 只在本进程内去重,发送失败不会自动重试。SDK 的消息回执超时固定为 5 秒,所以 timeoutMs 设得再大,单次发送实际最多约等 5 秒。dryRun: true 时只记日志,不连接企业微信;但仍需安装 SDK,且 Secret 必须能解析。
接收群内 @ 机器人与单聊机器人的消息,并直接回复:
export const inject = ['wecom']
export async function apply(ctx: Context) {
const sub = ctx.wecom.onMessage(
{ conversationId: 'wrk_orders', match: (m) => m.content.startsWith('工单') },
async (message, { signal, reply }) => {
const result = await handleOrder(message.content, { eventId: message.eventId, signal })
await reply({ markdown: `已处理:${result}` })
},
)
await sub.ready
}reply() 基于收到的消息帧回复,失败写在返回结果里,不抛错(失败时记带 traceId 与 code 的告警);text 与 markdown 同样二选一,同时传入返回 invalid_target;reply() 忽略 idempotencyKey。回复须在收到消息后 24 小时内,且同一条消息不能重复回复。配合设置页的「会话 → 插件」路由,插件用 onPluginMessage 接收:
const sub = ctx.wecom.onPluginMessage('business-orders', async (message, { signal, reply }) => {
await reply({ text: '收到' })
})
await sub.ready对应配置为 groupRoutes: [{ conversationId: 'wrk_orders', pluginId: 'business-orders' }];群的 conversationId 是 chatid,单聊是对方的 userid。一个会话只会转给一个目标插件;目标插件未注册时记入未命中,不回退给其他插件。ctx.wecom.unmatchedMessages() 返回最近未命中的消息摘要,新记录在前。
查找发送目标:
const { candidates, complete } = await ctx.wecom.searchRecipients('group', 'wrk')
// candidates: Array<{ name, target: { chatId } }>企业微信没有按名称搜索群或成员的机器人接口,候选来自本机记录的「说过话的会话」,name 就是会话 ID。
status() 返回机器人身份与长连接是否在线;health() 在连接建立之前为 degraded,连接建立后为 ok。只有认证失败重试用尽(凭据无效)和被其他连接挤下线为 failed;网络错误(连接被拒、DNS 失败、连接中断)与单次认证失败时 SDK 会自动重连,状态为重连中(degraded),等待中的订阅不会被拒绝。
消息字段
message 是 WecomAtMessage,在公共字段之上带有企业微信特有字段。正文是外部输入,业务插件必须自行校验。
| 字段 | 类型 | 说明 |
|---|---|---|
eventId | string | 去重键,取企业微信 msgid;进程内按它去重 |
messageId | string | 企业微信消息 ID,同 eventId |
conversationId | string | 群聊为 chatid,单聊为发送者的 userid;路由键 |
chatType | 'group' | 'single' | 群聊或单聊 |
senderId | string? | 发送者 userid,可能是加密 ID |
content | string | 纯文本:text 取文字内容,群消息去掉开头的 @ 机器人(尽力而为的规则,尚未用真实消息验证);mixed 拼接其中的文字;voice 取转写文字;其他类型为空串。保留换行 |
messageType | string | 企业微信原始 msgtype,如 text、mixed、voice、image |
quote | string? | 被引用消息的文字(有则带) |
timestamp | number? | 消息创建时间(毫秒) |
主动发送的前提与频率限制
- 主动发送前,目标会话里须有人先给机器人发过消息;否则返回
not_interacted,请先在群里 @ 机器人一次(单聊则让对方先发一句)。 - Service 在本机记录与机器人说过话的会话(最多 500 个,超出淘汰最久没说话的),作为主动发送的目标与
searchRecipients的候选。 - 每个会话每分钟 30 条、每小时 1000 条;超出时返回
rate_limited,可稍后重试。 - 单条内容上限 20480 字节,超出返回
invalid_target。
部署
- 在企业微信管理后台创建智能机器人,开启 API 模式并选择「长连接」,记下 BotID 与 Secret。
- 安装可选依赖
@wecom/aibot-node-sdk(^1.0.7);未安装时doctor报告,Service 启动失败。 - Secret 不写入配置:用
secretRef(默认WECOM_BOT_SECRET)引用,先读同名环境变量,再读 dsh 凭据文件。私有部署企业可用wsUrl指定长连接地址。 doctor只检查 SDK、BotID 与 Secret,不在线检查连接:一个机器人只能有一条长连接,检查时建连会把运行中的 Agent 挤下线。admin 的 setup 提供需要确认的连接测试。- 本地没有机器人时,可设
dryRun: true,并用@baoyx/dsh-agent-kit/testing的假 SDK 测试。dryRun仍要求已安装 SDK 且 Secret 能解析。
企业微信正文不可信;进程内按 eventId 去重,重启后仍可能重复投递,业务处理需自行幂等。真实企业微信租户的收发仍需用智能机器人端到端验收。
配置
在 Profile 的 cordis.patch.yml 中按 id agent-kit-wecom 启用并给出配置。按 id 修改 config 时整段替换,未给出的字段使用默认值。下表由本包源码中的配置 schema 生成。
- id: agent-kit-wecom
disabled: false
config:
# 只写需要改的字段,其余使用下表的默认值 | 字段 | 默认值 | 取值范围 | 说明 |
|---|---|---|---|
botId 必填 | — | — | 企业微信智能机器人的 BotID |
secretRef | WECOM_BOT_SECRET | — | Secret 的密钥引用:先读同名环境变量,再读 dsh 凭据文件 |
defaultTarget | — | 见说明 | 省略 target 时使用;群为 chatId,单聊为 userId |
groupRoutes | [] | 见说明 | 会话 ID(群 chatid 或单聊 userid)到业务插件 ID 的一对一路由;可在 Agent Kit 设置页配置 |
timeoutMs | 15000 | [1000, 120000] | 单次请求超时(毫秒);SDK 的消息回执超时固定为 5 秒,单次发送实际最多约等 5 秒 |
wsUrl | — | — | 私有部署企业的长连接地址;默认使用企业微信官方地址 |
dryRun | false | — | 只记日志,不调用企业微信;仍需安装 SDK 且 Secret 能解析 |
约束与说明
- 智能机器人长连接;需安装可选依赖 @wecom/aibot-node-sdk ^1.0.7。
- Secret 通过 secretRef 引用(默认 WECOM_BOT_SECRET),先读环境变量,再读 dsh 凭据文件。
- 主动发送前需对方先给机器人发过消息;每会话每分钟 30 条、每小时 1000 条。
- 一个机器人只能有一条长连接;被其他连接挤下线后不会自动重连,需要重载。
- idempotencyKey 只在本进程内去重,不会自动重试。
- doctor 不在线检查连接:建连会把运行中的 Agent 挤下线。
错误码
所有错误都是 KitError:用 isKitError(e) 判断,按 code 与 retryable 决定是否重试。
| code | 可重试 | 含义 |
|---|---|---|
invalid_target | 否 | 目标、@ 列表或内容非法(含超过 20480 字节),或企业微信报告 userid / chatid 不存在、不在群里。 |
auth_failed | 否 | BotID / Secret 无效,或机器人未开启长连接模式。 |
not_interacted | 否 | 该会话还没有人给机器人发过消息,无法主动发送;先在群里 @ 机器人一次。 |
rate_limited | 是 | 超过每会话每分钟 30 条、每小时 1000 条的限制,或触发企业微信接口频率限制。 |
not_connected | 是 | 长连接未就绪(如重连中)。 |
timeout | 是 | 单次请求超过 timeoutMs;超时不代表未送达。 |
aborted | 否 | 调用方 signal 中止或 Service 卸载。 |
api_error | 否 | 其余企业微信错误,含 req_id 过期(超过 24 小时的回复)与重复回复。 |
invalid_config | 否 | 配置或环境变量非法,Service 启动失败。 |