飞书 channel 接入指南
把 anet 节点接上飞书,让飞书用户跟 agent 直接对话。claude-agent-sdk runtime + 内置在 anet(不再走外部插件)。
状态(preview,跟踪 #179):anet
2.2.22-preview.2/ agent-node2.4.15-preview.2起 ship —— 私聊 + 群 @bot + 文本 + 图片可用;完整 commhub-gateway / Dashboard 透传是后续 PR。设计文档:RFC-020。
1. 状态 + 适用范围
| 维度 | 当前能力 |
|---|---|
| Runtime | 仅 claude-agent-sdk(claude-code-cli / codex-sdk / grok-build-acp 待后续) |
| 触达 | ✅ 私聊(白名单内 open_id)/ ✅ 群里 @bot |
| 媒体 | ✅ 文本 / ✅ 图片(需 vision-capable 模型,见 §4) |
| Dashboard 拓扑 | ❌ 暂不进(commhub-gateway 后续 PR) |
| 主动推送 | ❌ agent 仅响应入站消息(anet im send 主动推排 RFC-020 §12.9) |
2. 前置:飞书自建应用
在 飞书开放平台 建一个企业自建应用,按下面的顺序配置:
- 在「应用能力 → 添加应用能力」中启用机器人。
- 在「权限管理」申请以下权限(两项 readonly 接收权限见飞书官方「接收消息」事件文档,发送 / 资源权限用于下列 bridge 能力):
im:message.p2p_msg:readonly— 接收用户发给机器人的单聊消息im:message.group_at_msg:readonly— 接收群聊中 @机器人的消息im:message— 读取消息内容并调用消息接口im:message:send_as_bot— 以机器人身份发消息im:resource— 上传机器人要发送的图片等资源
- 在「凭证与基础信息」复制 App ID + App Secret。
- 先不要保存事件订阅配置。 先带着这组凭证按 §3(Docker)或 §8(手动)启动一条测试长连接,并保持进程运行。
- 测试连接启动后,再回到「事件订阅 → 配置方式」选 「使用长连接」(不是「使用 Webhook URL」)并保存,然后只订阅 「接收消息 -
im.message.receive_v1」。- 飞书官方长连接配置说明要求:先启动长连接客户端,再保存订阅方式。
- 不需要公网 IP / webhook URL / Encrypt Key。
- 创建并发布版本,等待管理员审批。
⚠️ 真坑(用户实战卡过的)
- 先起测试连接,再保存订阅方式 —— 顺序反了时,飞书后台会检测不到长连接客户端。
- 配置方式必须选「使用长连接」 —— 不是「使用 Webhook URL」。飞书后台菜单原话是「长连接」,不叫「WebSocket」。
- 事件订阅别选成「用户进入会话(
bot_p2p_chat_entered)」 —— 只勾「接收消息im.message.receive_v1」这一条;多勾会触发非预期路径。 - 网络可达性:官方 SDK 先通过
https://open.feishu.cn/callback/ws/endpoint鉴权并取得平台动态下发的wss://...地址,再连接该 WebSocket。企业防火墙 / 代理至少要放行open.feishu.cn的 HTTPS,以及响应里返回的动态 WSS 目标;不要硬编码旧域名或单个 WSS 域名 / IP。 - 群聊先把 bot 拉进群 —— bot 没在群里你 @ 也 @ 不出来。群管理员在群设置 → 群机器人 → 添加机器人 → 选你刚发布的自建应用。后续 §6 还会强调 group
chat_id也要加白名单。
3. 🚀 Docker 一键(推荐)
推荐做法
Docker 一键 = 填 .env + docker compose up -d,容器 entrypoint 自动 anet login + 创建节点 + 绑定飞书 + 启动,飞书 bridge 立刻起来。Vincent 首选。
模板文件在仓库的 docker/feishu/ 目录:
docker/feishu/
├── docker-compose.yml # BYOH 默认 + 可选 local-hub profile
├── Dockerfile # node:22-bookworm-slim + bun + 钉版本号
├── .env.example # 字段模板, 注释里带验证过的 vendor+model 对照
├── entrypoint.sh # login → node create → channel add feishu → start;重启时重跑 bootstrap
└── README.md # 三步快启 + .env 清单 + 故障排查表三步快启
git clone https://github.com/sleep2agi/agent-network.git
cd agent-network/docker/feishu/
cp .env.example .env
$EDITOR .env # 填账号 / App / 模型, 见下表
docker compose up -d # 首次 build 约 2 min
docker compose logs -f feishu-agent # 跟启动 + 运行日志首次配置新 App 时,让容器保持运行,然后回到 §2 第 5 步保存「使用长连接」并添加事件订阅。当前版本的 bridge online 只表示 Worker 已启动,不能单独证明飞书鉴权和 WebSocket 已成功;还要确认日志没有 failed to obtain token / [ws] ws connect failed,并确认飞书后台能识别这条测试连接。
容器 entrypoint 跑完整 bring-up:hub init → anet login → anet node create → anet channel add feishu → anet node start。节点状态持久化在 ./data/.anet/;每次重启都会按当前 .env 重新写入 Docker bootstrap 白名单。
.env 关键字段(以 docker/feishu/.env.example 为准)
| 字段 | 作用 | 必填 |
|---|---|---|
HUB_URL | 你已有的 commhub-server URL | ✅ |
HUB_USER / HUB_PASSWORD | 该 hub 上的账号(非交互式 anet login --username/--password,每次启动重新登录,不用预签 ntok_) | ✅ |
FEISHU_APP_ID / FEISHU_APP_SECRET | §2 拿到的飞书自建应用凭证 | ✅ |
ANET_MODEL + ANTHROPIC_BASE_URL + ANTHROPIC_AUTH_TOKEN | 模型后端三件套(详见 §4) | ✅ |
FEISHU_ALLOW_FROM | 允许私聊的、当前 App 下的 open_id(多个用逗号分隔) | ✅ 与下一项至少填一项 |
FEISHU_ALLOW_CHATS | 允许群聊的、当前 App 下的 chat_id(多个用逗号分隔) | ✅ 与上一项至少填一项 |
NODE_ALIAS | 节点 alias(默认 feishu-agent) | 可选 |
ACK_PLACEHOLDER | "⏳ 处理中…" 占位开关(默认 true,详见 §7) | 可选 |
.env.example 注释里带验证过的 vendor + model 对照:DeepSeek / MiniMax (M2.7 文本 / M3 vision) / Claude Sonnet / 书生 intern-s2-preview 等。
BYOH(Bring Your Own Hub)
默认 docker compose up 只起 agent service,连你自己的 hub(HUB_URL=https://your-hub...),不内置起 hub。生产场景就这条路径。
🧪
--profile local-hub是自测用,跑一个一次性本地 hub 容器配合 agent,做隔离 smoke。默认不启用,需要时显式docker compose --profile local-hub up -d且把.env里HUB_URL=http://hub:9200。日常忽略即可。
卷映射 + 安全边界(两个子目录)
docker-compose.yml 只映射 cwd 的两个子目录(不是整个 cwd),blast radius 严格限定——agent 的 Bash / 完整工具碰不到主机上的 compose 文件、.env、或其他目录:
| 宿主(cwd 子目录) | 容器 | 装什么 |
|---|---|---|
./data | /work | 节点配置、channels(.env + access.json)、logs、SQLite,全在 ./data/.anet/ |
./claude | /root/.claude | claude-agent-sdk 对话历史 projects/-work/<session>.jsonl |
两个都重要:config.json 里的 session 只是 resume id(在 ./data),真正的对话记录在 /root/.claude(不在 /work)。不挂 ./claude 的话,重建容器对话历史就丢了 → SDK 没法 resume,报「No conversation found」、bot 忘记之前上下文。 挂上后,重建 / 升级容器 bot 都能接着之前的记忆聊。两个目录首次 up 自动创建。
版本钉死 + 升级
镜像通过 Docker ARG 钉死 preview 版本号(当前 agent-network 2.2.22-preview.2 + agent-node 2.4.15-preview.2),不浮动 @preview tag,保证可复现。要换更新的 preview:
ANET_VERSION=2.2.23-preview.0 \
ANET_NODE_VERSION=2.4.16-preview.0 \
docker compose build
docker compose up -d正式 latest 发版后这里会同步切换。
启动验证 / 故障排查(5 行表)
| log line | 怎么处理 |
|---|---|
HUB_URL: missing — set HUB_URL=... | .env 某必填字段空, 修了 .env 再 docker compose up -d |
❌ Cannot reach hub: Cannot connect to CommHub server | HUB_URL 写错 / hub 没起,主机上 curl $HUB_URL/health 验 |
❌ Login failed: invalid username or password | HUB_USER / HUB_PASSWORD 跟 hub 账号不匹配,可以本机 anet login 先验账号 |
[claude] image attachments (N) received but ... text-only | ANET_MODEL 不是 vision-capable,切到 MiniMax-M3 / Claude Sonnet 等支持 vision 的 model |
容器 restart-loop 始终没到 [start] exec agent-node | bring-up 某一步 fail-fast,docker compose logs feishu-agent 看是哪一步 |
更多排查见 docker/feishu/README.md + 本页 §9。
4. 模型后端(claude-agent-sdk runtime)
飞书 bridge 把入站消息塞给 claude-agent-sdk runtime 处理,所以模型后端通过三个 env 配:
| env | 作用 |
|---|---|
ANTHROPIC_BASE_URL | 走哪家厂商的 Anthropic-compatible endpoint |
ANTHROPIC_AUTH_TOKEN | 该厂商的 API Key |
ANET_MODEL | 厂商支持的具体 model id |
文本场景 任意兼容供应商皆可(DeepSeek / Anthropic Sonnet / MiniMax / 智谱 GLM / Kimi / 书生 InternLM / 小米 MiMo / OpenRouter 等,详见 多模型配置)。
图片场景 后端必须 vision-capable,常见选择:
| 后端 | model 示例 | 备注 |
|---|---|---|
| Anthropic 官方 | claude-sonnet-4-6 | 主线 vision |
| MiniMax | MiniMax-M3 | 国内可达 |
| 小米 MiMo(vision) | mimo-v2.5-pro 等 | 国内可达 |
不支持图片的后端 + 飞书收到图片消息 → bridge warn-only 降级为纯文本处理(M5b 行为),不挂掉。
📖 完整多模型 / 多供应商对照:多模型配置
5. 访问白名单
bridge 不接受全网消息——必须在 access.json 里把允许的 人(open_id)和 群(chat_id)显式列出。
Docker 路径(推荐):第一次启动前,.env 的 FEISHU_ALLOW_FROM(当前 App 下的真实 open_id)和 FEISHU_ALLOW_CHATS(chat_id)必须至少填一项;留空或只有逗号/空白不是「允许所有人」,容器会直接拒绝启动。多个 ID 用逗号分隔;entrypoint 会 trim、去空、去重并合并到 access.json。通过 anet channel allow 追加的 ID 会在重启后保留;删除仍使用对应的 --rm-* 命令。
手动 anet 路径:用 CLI 增删:
# 加一个允许私聊的人
anet channel allow feishu <node> --add-from ou_<your-open-id>
# 加一个允许群聊的群
anet channel allow feishu <node> --add-chat oc_<group-chat-id>
# 删除
anet channel allow feishu <node> --rm-from ou_<your-open-id>
anet channel allow feishu <node> --rm-chat oc_<group-chat-id>flags 是 repeatable 的(一次 --add-from a --add-from b 加多个)。不支持逗号语法——--add-from a,b,c 会被当成单个 open_id 原样存储。
查看当前白名单:
anet channel ls
# 会列出每个 node 的 channels + allowFrom + allowChats改完必须 restart 节点
access.json 不热加载——改完一定要 anet node stop <node> + anet node start <node>(Docker 路径下 docker compose restart <node-service>)。bridge 启动时一次性把 access.json 读进内存。
换飞书 App 必踩:open_id / chat_id 按 App 隔离
飞书的 open_id(用户身份)和 chat_id(会话)都是按 App 隔离的——同一个人,在不同 App 下 open_id 完全不同。所以换 App 后,access.json 里存的旧 ID 全部失效,每条消息都会命中「发件人不在白名单」被静默拒收。
换 App 时三处都要同步改:① 节点环境 / 部署里的 FEISHU_APP_ID / FEISHU_APP_SECRET ② channel 的 .env ③ access.json 的 allowFrom / allowChats(最容易漏)。
漏第三处的典型症状:client ready 正常、能看到事件、但用户消息「毫无反应」。完整复盘见 → 经典案例:飞书 Bot 静默拒收。
6. 群 @ 机制
bot 要在群里能用,两步:
- 群管理员把 bot 拉进群(飞书后台「机器人管理」或直接群里 @邀请)
- 该群的
chat_id加进allowChats(见 §5)
入群后默认 group policy = mention —— 只有群消息真的 @bot 才触发 agent;普通群消息直接忽略(防群噪音)。
判断 @bot 的方式:bridge 把消息 mentions[].id.open_id 跟 bot 自己的 open_id 比对(bridge 初始化时通过 /open-apis/bot/v3/info 拉自己的 open_id)。
私聊场景不需要 @,发什么 bot 就回什么(白名单内)。
线程
bot 的 reply 会跟着原消息的 root_id 进同一条线程,不会污染主频道。
7. ⏳ 处理中…(ackPlaceholder)
收到消息秒回一条"⏳ 处理中…"占位,让用户立刻知道 bot 收到了;agent 思考完,答复以新消息发出(不就地编辑占位)。Vincent 6/26 Option A 决策。
为什么不编辑占位而发新消息?
- IM 客户端没有「消息被编辑」的 push 通知 —— 用户除非主动回看群/会话,不知道 bot 答完了
- 发新消息 → 用户得到第二条 push ——「⏳处理中…」(bot 收到了)+「答复」(bot 干完了),两次都有信号
超时通知(TIMEOUT_NOTICE_TEXT)也走 new-message 路径,同理。
默认 on。关掉:
在
.env里设ACK_PLACEHOLDER=true开启(见docker/feishu/.env.example)。
8. 进阶:手动 anet node 配置(非 Docker)
如果你不想用 Docker,或想直接装在宿主机:
8.1 安装 preview
npm install -g \
@sleep2agi/agent-network@2.2.22-preview.2 \
@sleep2agi/agent-node@2.4.15-preview.2
# 或装当前 preview tag:
# npm install -g @sleep2agi/agent-network@preview @sleep2agi/agent-node@preview飞书 channel 仍在 preview 通道;待
latest升级后会同步更新本页。
8.2 创建节点 + 绑定飞书
需要节点已存在;如未创建:
anet node create <node-name> --runtime claude-agent-sdk绑定飞书 channel:
anet channel add feishu <node-name> \
--app-id cli_xxxxxxxxxxxxxx \
--app-secret yyyyyyyyyyyyyyyyy \
--allow ou_<your-open-id> # 允许私聊的 open_id
--allow-chat oc_<group-chat-id> # 可选:允许群聊的 chat_id不带 flags 进交互模式:
anet channel add feishu <node-name>写入 .anet/nodes/<node-name>/channels/feishu/:
| 文件 | 内容 | 权限 |
|---|---|---|
.env | FEISHU_APP_ID + FEISHU_APP_SECRET | chmod 600 |
access.json | {allowFrom: [open_id...], allowChats: [chat_id...]} | chmod 644 |
8.3 启动节点
anet node start <node-name>首次配置新 App 时,保持节点运行,然后回到 §2 第 5 步保存长连接订阅并发布版本。
启动 log 会先出现这些 Worker 进程标记:
[agent-node] channels: feishu(/path/.anet/nodes/<node-name>/channels/feishu)
[agent-node] [feishu] forked worker (pid 12345) for ... via ...
[feishu:worker] bridge online — node=<node-name> dir=... ipc=yesbridge online 在当前版本里不是连接成功证据:即使 App 凭证鉴权失败,仍可能先打印这一行。连接层走查还必须确认没有 failed to obtain token / [ws] ws connect failed,并确认飞书后台在保存「使用长连接」时识别到正在运行的测试客户端;不要通过发消息来代替连接层验证。
worker 路径默认 dist/src/im/feishu/worker.js(@sleep2agi/agent-network 安装后即有)。如需覆盖:
export ANET_FEISHU_WORKER_PATH=/path/to/your/worker.js8.4 触发策略 sanity
- 私聊:
sender.open_id ∈ allowFrom才触发;不在白名单 → bridge stderr 出[feishu:audit] deny from=...,不派 IPC - 群聊:群
chat_id ∈ allowChats且 消息 @bot 才触发;不 @ bot → 静默忽略 - 线程:bot reply 跟
root_id进同一线程
9. 故障排查
| 现象 | 排查 |
|---|---|
节点启动报 unsupported channel: feishu | agent-node 版本太老,升级到 agent-node@2.4.15-preview.2 或更新 |
[feishu] worker path not found warn | 设 ANET_FEISHU_WORKER_PATH,或确认 @sleep2agi/agent-network 已安装 + 编译 |
failed to obtain token | App ID / Secret 错、App 状态不可用或鉴权失败;bridge online 不能覆盖这条错误 |
[ws] ws connect failed | 检查到 open.feishu.cn 的 HTTPS,以及平台动态返回的 WSS 目标是否被企业网 / 代理拦截 |
只有 bridge online / client ready | 仅是本地初始化线索,不足以证明鉴权或 WebSocket 已连上;按上两行先排错误,并看飞书后台能否识别测试连接 |
| 群里 @bot 不响应 | 1) bot 已加群且有发言权限 2) access.json allowChats 含目标 chat_id 3) /open-apis/bot/v3/info 是否返回有效 open_id 4) 改完 access.json 是否 restart 过节点(不热加载,见 §5) |
| 私聊不响应 | open_id 是否在 allowFrom 内(anet channel ls 看一下) |
| 连上了、有事件、但消息「没反应」 | bridge stderr grep deny / allowFrom:消息收到了但发件人不在白名单。换 App 后最常见(open_id 按 App 变了,见 §5 危险提示) |
| 收到图片 bot 答非所问 / 报错 | 后端不是 vision-capable(见 §4)—— 切到支持 vision 的 model |
| 发图片 bot 回「收到事件但没有可处理的文本/图片内容」 | 图片没被抠出来,两种根因依次排:① 节点没开图片能力(flags.modelImageCapable 默认不设,见 §4)② 下载失败——日志看 [feishu:image] … download FAILED,最常见是旧版本的 downloadImage SDK 误用 bug(#324,2.2.22-preview.2 等旧版带),升级到含修复的 preview 即根治。详见 经典案例 · 同源篇 |
诊断口诀(按层收窄):① 先找 failed to obtain token / [ws] ws connect failed,bridge online 或 client ready 单独出现不等于连接成功;② 飞书后台已识别测试连接、但零事件 = 检查是否漏 im.message.receive_v1 / 未使用长连接 / 未发布版本;③ 已有事件、但不回 = 应用层拒了,grep deny / allowFrom;④ 发图无反应 = 查 modelImageCapable flag + 下载是否失败(旧版本 bug,升级)。完整复盘 → 经典案例:飞书 Bot 静默拒收。
10. 已知限制(preview scope)
- 不进 Dashboard 拓扑 — 飞书消息不走 commhub task 路径,Dashboard 看不见。完整 commhub-gateway(RFC-020 §2.9 schema 增量)是收尾 PR
- agent 不能主动推飞书 — 仅响应入站消息。主动推(
anet im send)排 P1.5(RFC-020 §12.9) - 仅
claude-agent-sdkruntime —claude-code-cli仍走 社区 plugin;codex-sdk/grok-build-acp后续
参考
- RFC-020 IM 兼容层 — 完整设计
- RFC-002 channel-bind-cli — telegram-bridge 模式前身
- issue #179 — 父 RFC tracker
- Channel 接入总览 — Telegram / 微信 / 飞书 整体定位
- 多模型配置 — 模型后端选型
- 社区 SDK 调用层 prior art —
claude-code-cli路径