柒 · 收发

企业微信

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,在公共字段之上带有企业微信特有字段。正文是外部输入,业务插件必须自行校验。

字段类型说明
eventIdstring去重键,取企业微信 msgid;进程内按它去重
messageIdstring企业微信消息 ID,同 eventId
conversationIdstring群聊为 chatid,单聊为发送者的 userid;路由键
chatType'group' | 'single'群聊或单聊
senderIdstring?发送者 userid,可能是加密 ID
contentstring纯文本:text 取文字内容,群消息去掉开头的 @ 机器人(尽力而为的规则,尚未用真实消息验证);mixed 拼接其中的文字;voice 取转写文字;其他类型为空串。保留换行
messageTypestring企业微信原始 msgtype,如 text、mixed、voice、image
quotestring?被引用消息的文字(有则带)
timestampnumber?消息创建时间(毫秒)

主动发送的前提与频率限制

部署

企业微信正文不可信;进程内按 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 能解析

约束与说明

错误码

所有错误都是 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 启动失败。