叁 · 推送
飞书推送
ctx.feishu
通过飞书官方 CLI lark-cli 发送文本 / Markdown 消息,支持 bot / user 身份、群聊 / 单聊 / 多群、@ 人、幂等键和 dryRun。
接口
const r = await ctx.feishu.send({
markdown: '正文', // 或 text: '纯文本',二选一
title: '标题', // 可选,作为加粗首行发送
target: { chatId: 'oc_...' }, // 或 { userId: 'ou_...' } / { chatIds: ['oc_...', ...] }
at: { userIds: ['ou_...'], all: false },
idempotencyKey: 'evt_...',
traceId: 'evt_...',
signal, // 可选,中止时终止 lark-cli 子进程
})
// r.results: Array<{ target, ok, messageId?, error? }>本 Service 通过子进程调用飞书官方 CLI lark-cli(npm 包 @larksuite/cli,已验证 1.0.96)的 lark-cli im +messages-send:
- 不经过 shell,参数统一写成
--key=value形式,以-开头的正文也不会被当成选项。 - 固定附加
--format=json,并关闭 lark-cli 的更新与 skills 提示,避免它们混入输出。 - 子进程只继承环境变量白名单;应用凭据与令牌类变量不在其中,本包的其他密钥也不会传给 lark-cli。
- 配置了
profile时附加--profile=<名字>,使用 lark-cli 的对应命名配置。 - 超时或卸载时先发 SIGTERM,宽限
killGraceMs后发 SIGKILL,连同孙进程一起回收(Windows 上用taskkill /T /F)。
身份
| 身份 | 需要的准备 | 适用 |
|---|---|---|
bot | lark-cli config init,填写飞书应用的 App ID / App Secret | 服务器环境推荐,无需用户登录 |
user | 在 bot 的基础上再登录:运行 lark-cli auth login --scope "im:message.send_as_user im:message",或由业务包调用 ctx.feishu.login()(见下文) | 以登录用户的名义发送 |
两种身份都以 --as=<身份> 传给 lark-cli。凭据由 lark-cli 自己的配置与系统钥匙串保存,本包不读取、不保存。
目标
target 省略时使用配置的 defaultTarget;单次调用的 target 会覆盖它,多个业务包共用本包时互不影响。
| 目标 | 格式 | 说明 |
|---|---|---|
{ chatId } | 群 chat_id,oc_ 开头 | 发到群 |
{ userId } | 用户 open_id,ou_ 开头 | 发单聊 |
{ chatIds } | 多个 chat_id,最多 100 个 | 去重后逐个发送 |
目标、@ 列表与幂等键在拼装参数前做格式校验,格式不符或以 - 开头的值会被拒绝(invalid_target)。lark-cli 一次只发一个目标,所以多群时逐个调用:单个群失败不中断其余的群,也不抛错,体现在 results 中;只有中止(aborted)会立即抛出。单目标时失败直接抛出。
正文、标题与 @
markdown与text二选一。- 飞书的 markdown 消息没有独立标题:给了
title时,markdown 作为加粗首行,text 作为首行。 at.userIds是被 @ 用户的 open_id,at.all表示 @ 所有人,追加在正文末尾。
幂等与重试
idempotencyKey透传为 lark-cli 的--idempotency-key,同一个键在 1 小时内只发送一次。- lark-cli 的幂等键最长 50 个字符。单目标且不超长时原样使用,便于在飞书侧对账;多目标或超长时,按「原始键 + 目标」派生一个逐目标的键,否则只有第一个目标能收到。
- 只有给出
idempotencyKey时,才会对可重试的失败(超时,或 lark-cli 错误类别为 network / timeout / rate_limit / server / internal / unavailable)最多重试retry.maxAttempts次,间隔从 500ms 起指数增长。没有幂等键时不自动重试,避免重复发送。
登录状态与登录
const st = await ctx.feishu.status()
// { channel: 'feishu', identity: 'user', online: false, account?: '张三', detail, checkedAt }
const s = await ctx.feishu.login() // 仅 user 身份;{ channel, verificationUrl, userCode?, expiresAt, completed, cancel() }
await postToOps(s.verificationUrl) // 由业务包决定怎么把链接交给要登录的人
const after = await s.completed // 授权完成后的状态
await ctx.feishu.logout() // 仅 user 身份;返回退出后的状态status():执行lark-cli auth status --json实时检查所配身份是否可用;account在 user 身份下是用户名或 open_id,在 bot 身份下是应用 App ID。dryRun时只报告,不改变health()。login({ signal? }):运行lark-cli auth login --scope=im:message.send_as_user im:message --no-wait --json立即拿到授权链接并返回,随后在后台运行lark-cli auth login --device-code=<code>等待授权。对方授权后completed以新的状态 resolve;链接过期(15 分钟,lark-cli 给出有效期时以它为准)、被拒绝、cancel()或signal中止时同样 resolve,online为false。登录进行中再次调用返回同一个会话。第一步失败时抛出login_failed。logout():运行lark-cli auth logout,退出 user 身份的登录,返回退出后的状态。- 配置了
profile时,以上命令都带--profile=<名字>。 bot身份没有用户登录:它使用lark-cli config init配置的应用凭据,status()照常报告应用是否可用,login()/logout()抛出unsupported。更换应用凭据请重新运行lark-cli config init。
身份检查与 dryRun
- 启动时以及每隔
preflightIntervalMs执行lark-cli auth status --json,检查所配身份是否可用。不可用时health()返回failed,并触发agent-kit/service-failed事件。 dryRun: true时附加--dry-run,只做参数解析与日志、不真实发送,适合本地开发与测试;此时不检查身份。
larkPath 与 Windows
larkPath 必须是绝对路径;省略时启动时从 PATH 解析。Windows 上 npm 全局安装的是 lark-cli.cmd,本包会自动定位到它背后的 run.js 并用 node 运行。手动配置 larkPath 时,请指向可执行文件、.exe 或 .js,不要指向 .cmd / .bat。
配置
在 Profile 的 cordis.patch.yml 中按 id agent-kit-feishu 启用并给出配置。按 id 修改 config 时整段替换,未给出的字段使用默认值。下表由本包源码中的配置 schema 生成。
- id: agent-kit-feishu
disabled: false
config:
# 只写需要改的字段,其余使用下表的默认值 | 字段 | 默认值 | 取值范围 | 说明 |
|---|---|---|---|
identity 必填 | — | bot | user | 发送身份;服务器环境推荐 bot(只需应用凭据,无需用户登录) |
defaultTarget | — | 见说明 | 省略 target 时使用:群 chat_id(oc_…)、用户 open_id(ou_…)或多个群 |
profile | — | 匹配 /^[A-Za-z0-9._-]{1,64}$/ | lark-cli 的命名配置(--profile) |
larkPath | — | — | lark-cli 可执行文件的绝对路径;默认从 PATH 解析。需指向可执行文件、.exe 或 .js(不支持 Windows 的 .cmd/.bat) |
timeoutMs | 20000 | [1000, 120000] | 单次 lark-cli 调用的超时时间。 |
killGraceMs | 5000 | ≥ 0 | 超时或卸载时,SIGTERM 之后等待多久再发 SIGKILL。 |
retry.maxAttempts | 2 | [0, 5] | 给出幂等键时,可重试失败的最多重试次数。 |
preflightIntervalMs | 3600000 | ≥ 0 | 检查 lark-cli 身份状态的间隔,0 表示只在启动时检查 |
dryRun | false | — | 附加 --dry-run,只解析参数、不真实发送;同时跳过身份检查。 |
约束与说明
- bot 身份只需要用 lark-cli config init 配好应用的 App ID / App Secret;user 身份还需要登录:lark-cli auth login --scope "im:message.send_as_user im:message",或由业务包调用 ctx.feishu.login()。
- status() 实时检查所配身份是否可用;user 身份的 login() 发起设备流登录,拿到授权链接即返回,logout() 退出登录。bot 身份的 login() / logout() 报 unsupported。
- 应用凭据与令牌由 lark-cli 自己的配置和系统钥匙串管理,本包不保存,也不把它们传给子进程。
- 自动重试只在给出 idempotencyKey 时进行,避免重复发送;lark-cli 的幂等键最长 50 个字符,多目标时逐目标派生。
- larkPath 必须是绝对路径,指向可执行文件、.exe 或 .js,不支持 Windows 的 .cmd / .bat。
错误码
所有错误都是 KitError:用 isKitError(e) 判断,按 code 与 retryable 决定是否重试。
| code | 可重试 | 含义 |
|---|---|---|
timeout | 是 | lark-cli 超时,子进程按「先 SIGTERM、宽限后 SIGKILL」回收。 |
exit_nonzero | 视情况 | lark-cli 非零退出;按错误类别(network / timeout / rate_limit / server / internal / unavailable)判断是否可重试。 |
bad_output | 否 | lark-cli 输出无法解析。 |
invalid_target | 否 | 目标、@ 列表、正文或幂等键格式非法(例如 chat_id 不以 oc_ 开头、超过 100 个群)。 |
send_failed | 否 | lark-cli 报告 ok: false(多目标时体现在逐目标结果中)。 |
aborted | 否 | 调用方 signal 中止或 Service 卸载。 |
spawn_failed | 否 | 无法启动 lark-cli 子进程。 |
unsupported | 否 | bot 身份调用 login() / logout():bot 使用应用凭据,没有用户登录。 |
login_failed | 否 | lark-cli auth login 未能开始设备流登录。 |
invalid_config | 否 | 配置或环境变量非法,Service 启动失败。 |