陆 · 收发

飞书

ctx.feishu

飞书机器人:发送文本或 Markdown 卡片,接收群内 @ 机器人与私聊机器人的消息并按会话分发给业务插件。

接口

const r = await ctx.feishu.send({
  text: '纯文本',                          // 或 markdown: '## 标题\n正文'(以交互卡片发送)
  title: '库存告警',                       // 可选:Markdown 为卡片标题,纯文本为正文第一行
  target: { chatId: 'oc_xxx' },           // 或 { openId: 'ou_xxx' }
  at: { openIds: ['ou_xxx'] },            // 也支持 all: true
  idempotencyKey: 'evt_...',
  traceId: 'evt_...',
  signal,
})
// r.results: Array<{ ok, messageId?, error? }>

text 与 markdown 二选一;纯文本发送时若给出 title,它会作为第一行加在正文前。target 省略时使用配置的 defaultTarget;目标、@ 列表和幂等键会先经过格式校验。idempotencyKey 映射为飞书请求的 uuid(1 小时内去重);给出时,可重试的失败最多重试 retry.maxAttempts 次,否则不自动重试。dryRun: true 时只记日志,不调用飞书接口。

接收群内 @ 机器人与私聊机器人的消息:

export const inject = ['feishu']
export async function apply(ctx: Context) {
  // 按会话注册(代码规则)
  const sub = ctx.feishu.onMessage(
    { conversationId: 'oc_orders', match: (m) => m.content.startsWith('工单') },
    async (message, { signal }) => {
      await handleOrder(message.content, { eventId: message.eventId, signal })
    },
  )
  await sub.ready
}

配合设置页的「会话 → 插件」路由,插件用 onPluginMessage 接收:

const sub = ctx.feishu.onPluginMessage('business-orders', async (message, { signal }) => {
  await handleOrder(message.content, { eventId: message.eventId, signal })
})
await sub.ready

对应配置为 groupRoutes: [{ conversationId: 'oc_orders', pluginId: 'business-orders' }],conversationId 是群或单聊的 chat_id。一个会话只会转给一个目标插件;目标插件未注册时记入未命中,不回退给其他插件。ctx.feishu.unmatchedMessages() 返回最近未命中的消息摘要,新记录在前。

查找发送目标:

const { candidates, complete } = await ctx.feishu.searchRecipients('group', '库存')
// candidates: Array<{ name, target: { chatId } }>

按群名搜索时返回机器人所在的群,以及机器人可见的公开群;选中机器人不在的群时,发送会报 permission_denied(230002)。飞书机器人身份无法按姓名搜索用户,searchRecipients('user', …) 会抛错,个人请直接填写 open_id。业务插件可在自己的设置字段上声明 kind: 'feishu-target',让本机设置页提供群搜索或手动填写 open_id。

status() 返回机器人身份(identity: 'bot')、长连接是否在线(online)和机器人名称;health() 在首次长连接建立之前为 degraded(detail 为 feishu long connection starting),连接建立后为 ok,重连中或最近一次发送失败时为 degraded,凭据无效等不可恢复错误为 failed。

消息字段

message 是 FeishuAtMessage,在公共字段之上带有飞书特有字段。正文是外部输入,业务插件必须自行校验。

字段类型说明
eventIdstring去重键,取飞书 message_id;进程内按它去重
messageIdstring飞书消息 ID
conversationIdstring会话 chat_id,群与单聊都有;路由键
chatType'p2p' | 'group'单聊或群聊
senderIdstring?发送者 open_id
contentstringtext 消息:去掉所有 @ 机器人自己的地方(不只开头那个),其他 @ 还原为 @名字,@_all(@所有人)原样保留;post 消息:只提取 text / a 节点的文字(有标题时标题为第一行),不含 @ 节点;其他类型为空串
messageTypestring飞书原始类型,如 text、post、image
mentionsArray<{ openId, name }>消息中被 @ 的对象
timestampnumber?消息创建时间(毫秒)

部署

飞书正文不可信;进程内按 eventId 去重,重启后仍可能重复投递,业务处理需自行幂等。真实飞书租户的收发仍需用自建应用端到端验收。

配置

在 Profile 的 cordis.patch.yml 中按 id agent-kit-feishu 启用并给出配置。按 id 修改 config 时整段替换,未给出的字段使用默认值。下表由本包源码中的配置 schema 生成。

- id: agent-kit-feishu
  disabled: false
  config:
    # 只写需要改的字段,其余使用下表的默认值
字段默认值取值范围说明
appId 必填 — — 飞书自建应用的 App ID
appSecretRef FEISHU_APP_SECRET — App Secret 的密钥引用:先读同名环境变量,再读 dsh 凭据文件
domain feishu feishu | lark feishu 为飞书,lark 为国际版 Lark
defaultTarget — 见说明 省略 target 时使用
groupRoutes [] 见说明 会话 ID(群或单聊)到业务插件 ID 的一对一路由;可在 Agent Kit 设置页配置
timeoutMs 15000 [1000, 120000] 单次发送超时(毫秒)
retry.maxAttempts 2 [0, 5] 给出幂等键时,可重试失败的最多重试次数。
dryRun false — 只记日志,不调用飞书接口

约束与说明

错误码

所有错误都是 KitError:用 isKitError(e) 判断,按 code 与 retryable 决定是否重试。

code可重试含义
invalid_target 否 目标(chatId / openId)格式非法,或接收方不在应用可用范围、群已解散。
auth_failed 否 App ID / App Secret 无效,或访问令牌无法获取。
permission_denied 否 应用未开通所需权限、机器人能力未启用或机器人不在群里。
rate_limited 是 触发飞书频率限制。
timeout 是 单次请求超过 timeoutMs。
aborted 否 调用方 signal 中止或 Service 卸载。
network 是 网络错误,请求未得到飞书响应。
api_error HTTP 5xx 时是 其余飞书接口错误;HTTP 5xx 时可重试。
invalid_config 否 配置或环境变量非法,Service 启动失败。