跳转到主要内容

背景与使用场景

这个能力做什么

飞书机器人允许你的应用主动向飞书用户或群组发送消息,支持纯文本、富文本(post)、交互卡片(interactive card)等多种格式。有两种发送通道:Webhook(免鉴权、仅限特定群)和 API(需要 tenant_access_token,可触达任何用户或群)。
本能力是多个下游模块的基础。消息看板、告警播报等能力都依赖机器人先完成配置并加入群聊,才能正常工作。

业界怎么用

  • 监控告警推送:服务器异常、接口报错、业务指标超阈值时,自动给运维群发送告警卡片,包含关键数据和一键跳转按钮。这是最高频的用法——几乎所有用飞书的企业都有监控机器人。
  • 业务流转通知:审批通过、订单状态变更、任务分配等业务事件发生时,机器人即时通知相关人,替代人工通知。
  • 定时报表播报:每日早报、周报、月报自动推送到对应群组,包含关键业务指标和趋势图。
  • 交互式操作入口:通过卡片消息的按钮让用户在聊天窗口内直接完成操作(审批、确认、快速回复),不需跳转到外部系统。
  • 自动化工作流终端:作为自动化管线的最后一环,将处理结果通知到责任人——CI/CD、数据处理、审计等流程结束后自动汇报。

为什么这么做

  1. 消息到达率极高:飞书是员工日常工作的主界面,机器人消息的打开率远高于邮件(邮件平均打开率 ~20%,飞书消息接近 90%)。
  2. Webhook 零门槛:创建一个群机器人只需 5 分钟,获得 Webhook URL 后任何能发 HTTP 请求的系统都能推送消息,不需要审批权限。
  3. 卡片消息可交互:不只是「通知」,而是「入口」。用户可以直接在卡片上点按钮完成操作,形成「消息即服务」的体验。
  4. 双通道互补:Webhook 简单快速但只能发到固定群,API 模式灵活强大但需要权限审批——两者结合覆盖了几乎所有消息推送场景。

下游依赖关系

本模块是以下能力的前置依赖
下游模块依赖关系说明
消息看板强依赖机器人必须加入群聊 → 才能接收群消息事件 → 才能在看板展示
告警播报强依赖告警本质上是通过机器人发送卡片消息(Webhook 或 API 通道)
事件订阅弱依赖部分事件(如 im.message.receive_v1)需要机器人在群中才能触发

一、整体架构

本模块提供 Webhook 和 API 两种消息发送通道,统一通过 feishu-contacts Edge Function 路由处理。
┌─────────────────────────────────────────────────────────────┐
│  BotPanel (React)                                           │
│  ┌───────────────────┐     ┌──────────────────────────────┐ │
│  │ Webhook 模式       │     │ API 模式                      │ │
│  │ 用户贴 Webhook URL │     │ 自动拉群列表 / 手动输入 ID    │ │
│  └────────┬──────────┘     └──────────┬───────────────────┘ │
│           │     useFeishuBot hook     │                      │
│           └───────────┬───────────────┘                      │
│                       │ supabase.functions.invoke             │
│                       │ (feishu-contacts)                     │
└───────────────────────┼─────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│  Edge Function: feishu-contacts                              │
│                                                              │
│  ┌─ Webhook 通道 ────────────────────────────────────────┐  │
│  │ action: send_webhook                                   │  │
│  │ → POST https://open.feishu.cn/open-apis/bot/v2/hook/   │  │
│  │   {webhook_token}                                      │  │
│  │ → 不需要 token,URL 本身即鉴权                          │  │
│  └────────────────────────────────────────────────────────┘  │
│                                                              │
│  ┌─ API 通道 ────────────────────────────────────────────┐  │
│  │ ① APP_ID + APP_SECRET → tenant_access_token            │  │
│  │ ② action: list_chats → GET /im/v1/chats               │  │
│  │ ③ action: send_message → POST /im/v1/messages          │  │
│  └────────────────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────────────┘

二、数据流程

Webhook 模式流程

用户在 BotPanel 选择「Webhook」模式


粘贴 Webhook URL
(格式: https://open.feishu.cn/open-apis/bot/v2/hook/{token})


选择消息类型 → 编辑消息内容


调用 supabase.functions.invoke("feishu-contacts", {
  body: { action: "send_webhook", webhook_url, msg_type, content }
})


Edge Function 构造 payload:
  ├─ text/post: { msg_type, content: {...} }
  └─ interactive: { msg_type: "interactive", card: {...} }


POST 到 webhook_url → 飞书群收到消息

API 模式流程

用户在 BotPanel 选择「API 发送」模式


面板自动调用 list_chats action
→ Edge Function 获取 tenant_access_token
→ GET /im/v1/chats?page_size=50
→ 返回机器人所在群列表


用户选择目标群(或手动输入 receive_id)


选择消息类型 → 编辑消息内容


调用 supabase.functions.invoke("feishu-contacts", {
  body: { action: "send_message", receive_id, receive_id_type, msg_type, content }
})


Edge Function:
  ① 获取 tenant_access_token
  ② content 做 JSON.stringify(飞书 API 要求 content 是 JSON 字符串)
  ③ POST /im/v1/messages?receive_id_type={type}


飞书群 / 个人收到消息

三、环境变量

变量来源必须用途
SUPERUN_FEISHU_APP_IDEdge Function SecretAPI 模式必须飞书应用 App ID
SUPERUN_FEISHU_APP_SECRETEdge Function SecretAPI 模式必须飞书应用 App Secret
Webhook 模式不需要上述凭证,webhook_url 由前端传入,URL 本身即为鉴权凭证。

四、关键代码

4.1 Webhook 发送

// Edge Function — send_webhook action
// 关键点:interactive 类型用 card 字段,其他用 content 字段
const payload = msgType === "interactive"
  ? { msg_type: msgType, card: content }     // 卡片消息用 card 字段
  : { msg_type: msgType, content: content };  // 其他消息用 content 字段

const response = await fetch(webhookUrl, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify(payload),
});
const result = await response.json();
// 成功: { StatusCode: 0, StatusMessage: "success" }
// 失败: { StatusCode: 9499, StatusMessage: "invalid hook" }
要点
  • Webhook URL 格式固定:https://open.feishu.cn/open-apis/bot/v2/hook/{token}
  • interactive 类型必须用 card 字段,不能用 content——这是飞书 Webhook 的特殊要求
  • content 直接传对象,不需要 JSON.stringify(与 API 模式不同)

4.2 API 模式 — 获取群列表

// Edge Function — list_chats action
const response = await fetch(
  `https://open.feishu.cn/open-apis/im/v1/chats?page_size=50`,
  {
    headers: {
      Authorization: `Bearer ${accessToken}`,
      "Content-Type": "application/json",
    },
  }
);
const result = await response.json();
// result.data.items: [{ chat_id: "oc_xxx", name: "群名", owner_id: "ou_xxx", ... }]
// result.data.has_more: boolean
// result.data.page_token: string(用于翻页)
要点
  • 只返回机器人作为成员的群——机器人未加入的群不会出现
  • 如果返回空列表,大概率是机器人没有加入任何群,而不是权限问题
  • 分页支持 page_token,但一般群数量不多,50 一页足够

4.3 API 模式 — 发送消息

// Edge Function — send_message action
// ⚠️ 关键:content 必须是 JSON 字符串!
const contentString = typeof content === "string" ? content : JSON.stringify(content);

const response = await fetch(
  `https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=${receiveIdType}`,
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${accessToken}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      receive_id: receiveId,
      msg_type: msgType,
      content: contentString,  // 必须是 JSON 字符串
    }),
  }
);
const result = await response.json();
// 成功: { code: 0, data: { message_id: "om_xxx", ... } }
要点
  • content 字段必须是 JSON 字符串(被 stringify 两次),这是飞书 API 的强制要求
    • 文本消息:"{\"text\":\"hello\"}"
    • 卡片消息:"{\"config\":{...},\"elements\":[...]}
  • receive_id_type 决定了 receive_id 的含义:chat_id(群)、open_id(个人)、user_idunion_idemail
  • 向群发消息时,机器人必须是该群的成员(否则报 19024)

五、消息类型详解

类型msg_typeWebhook payload 格式API content 格式(JSON 字符串)
纯文本text{ msg_type: "text", content: { "text": "hello" } }"{\"text\":\"hello\"}"
富文本post{ msg_type: "post", content: { "zh_cn": { "title": "标题", "content": [[...]] } } }同左结构,序列化为字符串
交互卡片interactive{ msg_type: "interactive", card: { "config": {...}, "elements": [...] } }"{\"config\":{...},\"elements\":[...]}"
图片image不支持 Webhook 直接发"{\"image_key\":\"img_xxx\"}"

⚠️ Webhook vs API 的 interactive 差异

这是最常踩的坑:
对比项Webhook 模式API 模式
字段名card 字段content 字段
值类型JavaScript 对象JSON 字符串
示例{ msg_type: "interactive", card: cardObj }{ msg_type: "interactive", content: JSON.stringify(cardObj) }

富文本 content 结构

{
  "zh_cn": {
    "title": "项目更新",
    "content": [
      [
        { "tag": "text", "text": "项目状态:" },
        { "tag": "text", "text": "已完成", "style": ["bold"] }
      ],
      [
        { "tag": "a", "text": "查看详情", "href": "https://example.com" }
      ]
    ]
  }
}

六、useFeishuBot Hook 接口

const {
  // 发送模式
  sendMode,           // "webhook" | "api"
  setSendMode,        // 切换模式

  // 群列表(API 模式)
  chats,              // ChatItem[] - 机器人所在群列表
  chatStatus,         // "idle" | "loading" | "ready" | "error"
  chatError,          // string | null
  fetchChats,         // () => Promise<void> - 手动拉取群列表

  // 发送状态
  sendStatus,         // "idle" | "sending" | "success" | "error"
  sendError,          // string | null
  sendHistory,        // SendRecord[] - 发送历史记录

  // 发送方法
  sendWebhookMessage, // (url: string, msgType: string, content: object) => Promise<void>
  sendApiMessage,     // (receiveId: string, receiveIdType: string, msgType: string, content: string | object) => Promise<void>
  clearSendStatus,    // () => void - 清空发送状态
} = useFeishuBot();

ChatItem 数据结构

interface ChatItem {
  chat_id: string;        // 群唯一标识
  name: string;           // 群名称
  avatar: string;         // 群头像 URL
  owner_id: string;       // 群主 open_id
  chat_mode: string;      // "group" 群组 | "p2p" 单聊
  external: boolean;      // 是否外部群
}

SendRecord 数据结构

interface SendRecord {
  id: string;             // 记录 UUID
  mode: "webhook" | "api";
  target: string;         // Webhook URL 或 receive_id
  msgType: string;        // text | post | interactive
  status: "success" | "error";
  message: string;        // 结果描述
  timestamp: Date;
}

七、BotPanel UI 结构

┌─ Header ────────────────────────────────────────────────────────┐
│ 飞书机器人消息                                    [连接状态徽标]  │
├─ Mode Tabs ─────────────────┬───────────────────────────────────┤
│ [Webhook] [API 发送]         │                                   │
├─ Target Area ───────────────┤  发送历史                          │
│ Webhook: URL input          │  ┌──────────────────────────────┐  │
│ API: 群列表 + 手动输入       │  │ ✅ 14:32 文本 → 技术群       │  │
├─ MsgType Tabs ──────────────┤  │ ❌ 14:28 卡片 → webhook      │  │
│ [文本] [富文本] [交互卡片]   │  │ ✅ 14:25 文本 → 张三         │  │
├─ Content Editor ────────────┤  │ ...                          │  │
│ textarea / JSON editor      │  └──────────────────────────────┘  │
├─ Send Bar ──────────────────┤                                   │
│ [发送消息]                   │                                   │
└──────────────────────────────┴───────────────────────────────────┘

八、连接状态

BotPanel 通过 onConnectionChange 回调向 DebugWorkbench 报告连接状态:
条件状态含义
API 模式群列表加载成功 (chatStatus === "ready")✅ 已连接APP_ID/SECRET 正确,且有群可发
Webhook 模式不影响Webhook 不需要凭证验证
群列表加载失败❌ 未连接检查凭证或权限配置

九、接口验证清单

本场景涉及以下飞书 API,可逐一验证是否在当前系统中调通。

① 获取 tenant_access_token(API 模式内部调用)

项目
方法POST
URLhttps://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal
鉴权无需 token,使用 app_id + app_secret 请求体鉴权
所需权限无(仅需有效的应用凭证)
用途获取 tenant_access_token,用于 send_message 和 list_chats
请求体{ "app_id": "cli_xxx", "app_secret": "xxx" }
成功响应{ "code": 0, "tenant_access_token": "t-xxx", "expire": 7200 }
常见错误10003 → app_id 不存在;10014 → app_secret 错误
官方文档自建应用获取 tenant_access_token
Webhook 模式不需要此接口,webhook URL 本身即为鉴权凭证。

② Webhook 消息发送

项目
方法POST
URLhttps://open.feishu.cn/open-apis/bot/v2/hook/{webhook_token}
鉴权无需 token,URL 中的 webhook_token 即为鉴权凭证
所需权限无(只需群中添加自定义机器人并获取 Webhook 地址)
用途向指定群发送消息(文本/富文本/交互卡片)
请求体(文本){ "msg_type": "text", "content": { "text": "hello" } }
请求体(卡片){ "msg_type": "interactive", "card": { "config": { "wide_screen_mode": true }, "header": { "title": { "tag": "plain_text", "content": "标题" } }, "elements": [{ "tag": "div", "text": { "tag": "plain_text", "content": "内容" } }] } }
成功响应{ "StatusCode": 0, "StatusMessage": "success" }
常见错误StatusCode: 9499 → “invalid hook”,URL 无效或 token 已过期;msg_type not support → interactive 类型应用 card 字段而非 content
官方文档自定义机器人指南

③ API 发送消息

项目
方法POST
URLhttps://open.feishu.cn/open-apis/im/v1/messages?receive_id_type={type}
鉴权Authorization: Bearer {tenant_access_token}
所需权限im:message:send_as_botim:message(以应用身份发消息)
用途向任意群或个人发送消息
关键参数receive_id_type(query,可选 chat_id / open_id / user_id / union_id / email
请求体{ "receive_id": "oc_xxx", "msg_type": "text", "content": "{\"text\":\"hello\"}" }
⚠️ content 格式content 必须是 JSON 字符串(被 stringify 两次),这是飞书 API 的强制要求。直传对象会返回格式错误
成功响应{ "code": 0, "data": { "message_id": "om_xxx", "root_id": "", "parent_id": "", "msg_type": "text", "create_time": "1700000000000", "body": { "content": "{\"text\":\"hello\"}" } } }
常见错误19001 → 缺少 im:message:send_as_bot 权限;19024 → 机器人未在目标群中,需先将机器人添加为群成员;99991672 → 权限未开通或应用未发布新版本
官方文档发送消息

④ 获取机器人所在群列表

项目
方法GET
URLhttps://open.feishu.cn/open-apis/im/v1/chats?page_size=50
鉴权Authorization: Bearer {tenant_access_token}
所需权限im:chat:readonlyim:chat(获取群组信息)
用途获取机器人已加入的所有群聊列表,用于在面板中让用户选择发送目标
关键参数page_size(每页数量,最大 100)、page_token(分页令牌)
成功响应{ "code": 0, "data": { "items": [{ "chat_id": "oc_xxx", "name": "技术群", "avatar": "https://...", "owner_id": "ou_xxx", "chat_mode": "group", "external": false }], "has_more": false, "page_token": "" } }
常见错误99991672 → 缺少 im:chat:readonly 权限;返回空列表 → 机器人未加入任何群(不是权限问题)
官方文档获取用户或机器人所在的群列表

十、踩坑记录

10.1 消息格式与发送类

问题现象原因解决方案
Webhook 发卡片失败返回 msg_type not supportinteractive 类型使用了 content 字段Webhook 模式 interactive 必须用 card 字段:{ msg_type: "interactive", card: {...} }
API 发消息格式报错返回 param invalid 类错误content 传了对象而不是 JSON 字符串API 模式 content 必须是 JSON 字符串(stringify 两次):content: JSON.stringify({text: "hello"})
富文本内容为空发送成功但飞书端看到空消息post 类型缺少 zh_cnen_us 顶层 key富文本必须有语言 key 包裹:{ zh_cn: { title: "...", content: [[...]] } }
Webhook URL 无效返回 invalid hookURL 不完整、token 已被回收、或群已解散重新在群设置中获取 Webhook URL

10.2 权限与配置类

问题现象原因解决方案
群列表返回空list_chats 返回 items: []机器人未加入任何群将机器人添加到至少一个群(群设置 → 群机器人 → 添加 → 搜索应用名)
发消息报 19001”no permission to send message”应用未开通 im:message:send_as_bot 权限飞书开发者后台 → 权限管理 → 搜索 im:message:send_as_bot → 开通 → 发布新版本
发消息报 19024”bot not in this chat”机器人不在目标群中将机器人添加到目标群(群设置 → 群成员 → 添加 → 搜索应用名)
权限开通后仍报错刚在后台开权限就测试权限变更需创建新版本并发布后才生效版本管理 → 创建新版本 → 发布 → 等审核通过
发消息给个人报错open_id 正确但报 permission用户不在应用可见范围内在应用设置中扩大可见范围,或让管理员将用户加入

10.3 与下游模块联动

问题现象原因解决方案
消息看板群列表为空看板中加载群列表返回空机器人未开通或未加群先完成本模块的「启用机器人能力 + 加入群聊」步骤
消息看板收不到新消息事件订阅配了但看板无反应机器人不在群中,无法接收群消息事件将机器人添加到目标群后,群内消息事件才会推送
告警发送到群失败告警播报的 Webhook/API 发送报错Webhook URL 过期或 API 权限未配置Webhook: 重新获取 URL;API: 确认 send_as_bot 权限已开通

附录:Agent 权限与安全配置参考

Agent 权限配置

scopenametype
im:message:send_as_bot以应用身份发送消息tenant
im:chat:readonly获取机器人所在群列表tenant
permissions
[
  { "scope": "im:message:send_as_bot", "name": "以应用身份发送消息", "type": "tenant" },
  { "scope": "im:chat:readonly", "name": "获取机器人所在群列表", "type": "tenant" }
]
batch_import_json
{
  "scopes": {
    "tenant": ["im:message:send_as_bot", "im:chat:readonly"],
    "user": []
  }
}