陆 · 收发
飞书
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,在公共字段之上带有飞书特有字段。正文是外部输入,业务插件必须自行校验。
| 字段 | 类型 | 说明 |
|---|---|---|
eventId | string | 去重键,取飞书 message_id;进程内按它去重 |
messageId | string | 飞书消息 ID |
conversationId | string | 会话 chat_id,群与单聊都有;路由键 |
chatType | 'p2p' | 'group' | 单聊或群聊 |
senderId | string? | 发送者 open_id |
content | string | text 消息:去掉所有 @ 机器人自己的地方(不只开头那个),其他 @ 还原为 @名字,@_all(@所有人)原样保留;post 消息:只提取 text / a 节点的文字(有标题时标题为第一行),不含 @ 节点;其他类型为空串 |
messageType | string | 飞书原始类型,如 text、post、image |
mentions | Array<{ openId, name }> | 消息中被 @ 的对象 |
timestamp | number? | 消息创建时间(毫秒) |
部署
- 在飞书开发者后台创建自建应用,开启机器人能力;事件订阅选「长连接」,并订阅「接收消息 v2.0」(
im.message.receive_v1)。 - 开通权限
im:message:send_as_bot、im:message.group_at_msg:readonly、im:message.p2p_msg:readonly。 - 安装可选依赖
@larksuiteoapi/node-sdk(^1.76.0);未安装时doctor报告,Service 启动失败。 - App Secret 不写入配置:用
appSecretRef(默认FEISHU_APP_SECRET)引用,先读同名环境变量,再读 dsh 凭据文件。国际版 Lark 把domain设为lark。 - 本地没有飞书应用时,可设
dryRun: true,并用@baoyx/dsh-agent-kit/testing的假 SDK 测试。
飞书正文不可信;进程内按 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 | — | 只记日志,不调用飞书接口 |
约束与说明
- 机器人(自建应用)身份;需安装可选依赖 @larksuiteoapi/node-sdk ^1.76.0。
- App Secret 通过 appSecretRef 引用(默认 FEISHU_APP_SECRET),先读环境变量,再读 dsh 凭据文件。
- 长连接接收:同一飞书应用的多个连接只有一个会收到某条消息,一个应用只接一个 Profile。
- 群搜索返回机器人所在的群以及机器人可见的公开群;选中机器人不在的群时,发送会报 permission_denied(230002)。
- 开发者后台需开启机器人能力、事件订阅选长连接,并开通 im:message:send_as_bot、im:message.group_at_msg:readonly、im:message.p2p_msg:readonly。
错误码
所有错误都是 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 启动失败。 |