乐于分享
好东西不私藏

飞书 * OpenClaw 集成:ID 体系全景与排坑指南

飞书 * OpenClaw 集成:ID 体系全景与排坑指南

〇、一图看懂(先看这张再往下读)

这张图想讲清楚两件事(个人用户视角):

  1. 1. 您自己一个人,在 10 个您建的飞书机器人下,有 10 个完全不同的 open_id——飞书故意这么设计
  2. 2. union_id 是把这些 open_id 串起来的”红线”——想跨机器人找回”您自己”,靠它

看完这张图,再看下面的术语和细节,就不会迷路。


一、ID 体系总览

1.0 为什么会有这么多 ID

场景设定:您是个人开发者,注册了 10 个飞书机器人(feishu_cio / feishu_writer / wuzhao / duola 等),每个机器人都是您自己跟它聊。

核心类比——您(个人开发者)经营着 10 个小工作室(一人公司模式)

完整 ID 映射表(一人公司类比):

飞书 / OpenClaw 概念
一人公司类比
例子
实际作用

(自然人)
一个真实的人
union_id
您的身份证号(跨所有工作室识别是您)
on_xxxxxx 跨工作室识别”同一个人”

(唯一能跨 10 个机器人识别是您)
open_id
您在某工作室的员工工号(A 工作室 HR-001,B 工作室 HR-002)
ou_aaaa... 发消息/调 API 的主语 ID

(im API 的 receive_id)
user_id
您的社保号(自建,部分公司有)
u_xxxx
企业内自建 ID,部分租户才有
飞书 app

(feishu_cio 等)
您开的某个一人工作室(写作工作室 / 研究工作室 / 记账工作室……)
feishu_cio = 写作工作室
工作室独立运作,互不干扰
appId

 (cli_xxxxx)
工作室的工商注册号(一个工作室一个)
cli_xxxxx
路由/鉴权/调 API 的入口 ID
appSecret
工作室的营业执照密钥(跟 appId 配对)
***
跟 appId 配对用于拿 token
tenant_access_token
工作室的法人一证通数字证书(2 小时有效,调 API 用)
t-xxxxxx
调任一 API 都必须先拿这个 token
message_id

 (om_xxx)
每次通信的运单号
om_xxxxx
消息去重(dedup 表的 key)
chat_id

 (oc_xxx)
部门群/项目组

群号
oc_xxxxx
群发消息时用
chat_type

 (direct/group)
1对1 私聊 vs 群聊
direct=私聊 / group=群
决定 session key 格式
agentId
工作室简称(”写作工作室”)
feishu_cio
OpenClaw 智能体标识
accountId
工作室在工商系统的备案名(与简称一一对应)
feishu_cio
飞书通道账号标识(与 agentId 一一对应
session key
您跟某工作室的某次具体会话(”今天上午 9 点跟写作工作室的微信会话”)
agent:feishu_cio:feishu:direct:ou_aaaa...
一次会话的唯一标识
subagent run id
您请外部顾问做的某次专项任务(一次一密)
UUID
子智能体调度的运行 ID
cron job id
工作室里的某个定时巡检任务(如每天 8 点查邮件)
UUID
定时任务的唯一 ID

一句话讲完

您 = 一个人。10 个工作室 = 10 个飞书机器人。每个工作室给您发不同工号(open_id),但您的身份证号(union_id)跨 10 个工作室都一样。

为什么飞书要把 ID 设计得这么复杂(一人公司体系为什么这么设计):

  • • 数据隔离:A 工作室看不到您在 B 工作室的工号 → 公司之间不互通工号体系(商业机密)
  • • 隐私保护:B 工作室不知道您在 A 工作室的工资 → 公司之间不知道对方薪资/资产(同业不互知)
  • • 解耦:A 工作室倒闭时,您的 A 工作室工号跟着失效,不污染 B 工作室 → app 下线时 ou_xxx 跟着失效(不影响其他机器人)
  • • API 鉴权:调 A 工作室的 API 必须用 A 工作室的工商注册号 + 营业执照密钥 → tenant_access_token 必须用对应 appId + appSecret 申请(用 A 的 token 调 B 的 API,B 不认)

这套 ID 体系在您日常工作中的「实际作用」

  1. 1. 发消息时(im/v1/messages):OpenClaw 给您发消息,用 open_id 作为 receive_id。类比:就像您给某公司发文件,必须用那家公司的工号——工号能反查到这个人在这家公司里的位置。
  2. 2. 反查用户时(contact/v3/users):拿到一个 open_id 想知道是谁,用对应 appId 拿 token 再调 API。类比:就像您拿到一个工号想查人,必须去那家公司的人事系统查——工号是公司内有效的,跨公司查不到。
  3. 3. 跨工作室协作时:让 A 工作室的输出推给 B 工作室的客户,必须用 B 工作室视角下的 open_id。类比:A 公司想联系 B 公司的客户,必须用 B 公司内部工号——A 公司工号 B 公司不认。
  4. 4. 配置 cron job 时:定时推送用 delivery.to: user:ou_xxx这个 ou_xxx 必须是该 cron job 所属 agent 视角下的。类比:您让 A 工作室每天给某客户发邮件,必须用 A 工作室内部工号——用 B 工作室的工号 A 工作室查不到这个人。

对您意味着什么(总结):

  • • 想要”我用什么 ID 给您发消息” → 用 open_id(每个工作室下您看到的都不一样)
  • • 想要”识别’您’就是同一个人” → 用 union_id(跨 10 个工作室都一样)
  • • 想调某工作室的 API → 必须用该工作室的 appId + appSecret 拿 token(跨工作室 token 互不认)
  • • 这几个 ID 各自有用途,不能混用——下面会具体讲混用会出什么问题

📌 重要前提:本节讲的”app”在飞书术语里叫”app”,但对应到您的工作流就是”您自己建的工作室/机器人”。文中”app A”和”app B”换成”feishu_cio 工作室”和”feishu_writer 工作室”会更容易理解。

1.1 飞书原生 ID(4 套)

前缀
名称
作用范围
唯一性
ou_ open_id 单个飞书应用(app)

 维度
同一用户在不同 app 下 ID 完全不同
on_ union_id 整个企业/租户

 维度
跨 app 统一,关联同一个用户
u_ user_id
企业内自建 ID
部分企业才有,未必所有用户都有
oc_ chat_id
群/会话
群维度,与用户 ID 完全不同

1.2 OpenClaw 内部 ID

字段
例子(已脱敏)
来源
作用
agentId feishu_cio

duolawuzhao
openclaw.json

 顶层 agents.list[].id
OpenClaw 中的智能体标识
accountId feishu_cio
飞书通道下的飞书账号标识
与 agentId 一一对应

(一个飞书 app 绑定一个 agent)
appId cli_xxxxxxxxxxxxxxxx
飞书开放平台
飞书应用的真实 ID,open_id 查询必须用它换 token
session key agent:feishu_cio:feishu:direct:ou_xxxxxxxx ~/.openclaw/agents/<agentId>/sessions/sessions.json
一次会话的唯一标识
subagent run id
UUID 形式
~/.openclaw/state/openclaw.sqlite
子智能体调度的运行 ID
cron job id
UUID 形式
~/.openclaw/state/openclaw.sqlite
定时任务的唯一 ID

「三同一对应」原则(理解 OpenClaw 与飞书绑定关系的关键):

飞书 1 个 app (appId: cli_xxx)   ↕ 一一对应OpenClaw 1 个 agent (agentId: feishu_cio)   ↕ 一一对应飞书通道 1 个 account (accountId: feishu_cio)

记住一句话:在 OpenClaw 里,飞书通道下写 accountId 还是 agentId,值都一样(feishu_cio)。但 appId 是另一个东西(cli_xxx 形式),在 openclaw.json 的 channels.feishu.accounts.<id>.{appId, appSecret} 里。


二、核心原理:为什么不同 agent 下 open_id 不同

2.1 飞书视角的 ID 隔离

飞书的 open_id 设计原则:

  • • 同一用户(在飞书抽象术语里叫”同一用户”),在 agent A 下看到的 open_id 是 ou_aaa
  • • 同一用户,在 机器人 B 下看到的 open_id 是 ou_bbb(完全不同的字符串)
  • • 即便两个 agent 属于同一个企业做不到直接互通 open_id

🔄 翻译成您的工作流

  • • “同一用户” = 您自己(个人开发者身份)
  • • “agent A” = 您建的 feishu_cio 机器人
  • • “agent B” = 您建的 feishu_writer 机器人
  • • 也就是说:在 feishu_cio 下看到的 open_id 是 ou_aaa,在 feishu_writer 下看到的 open_id 是 ou_bbb——两个完全不同的字符串,飞书故意不关联。

为什么这么设计(个人用户视角的具体含义):

  • • 隔离数据:feishu_cio 不能用 ou_aaa 假装是 feishu_writer 调 API 查”您”
  • • 隐私:feishu_cio 不会”知道” feishu_writer 跟”您”聊过什么,反之亦然
  • • 解耦:feishu_cio 下线时,ou_aaa 也跟着失效,不污染 feishu_writer

2.2 用 OpenClaw 真实案例讲清楚

下面这个表,能让您直接看到——您自己,在您建的 10 个机器人下,open_id 全都不一样

您建的机器人
您在此机器人下的 open_id
feishu_cio
ou_aaaa1111aaaa1111aaaa1111
feishu_co
ou_bbbb2222bbbb2222bbbb2222
feishu_writer
ou_cccc3333cccc3333cccc3333
feishu_product
ou_dddd4444dddd4444dddd4444
duola
ou_eeee5555eeee5555eeee5555
wuzhao
ou_ffff6666ffff6666ffff6666
zhangyixuan
ou_7777aaaa7777aaaa7777aaaa
zhangyu
ou_8888bbbb8888bbbb8888bbbb
yaya
ou_9999cccc9999cccc9999cccc
xie
(无活跃会话)

这意味着(个人用户视角):

  • • 上面 9 个 ou_xxxx全是您自己——不是 9 个员工、不是 9 个用户
  • • 飞书不会告诉您 “这些 open_id 是同一个人”
  • • 您只能用 union_id 跨机器人关联(比如您的 union_id 是 on_xxxxxx —— 跨 10 个机器人都一样)

实际场景举例

  • • 您给 feishu_cio 这个机器人发消息时,飞书给 feishu_cio 分配给您的 open_id 是 ou_aaaa1111aaaa1111aaaa1111
  • • 您给 feishu_writer 发消息时,飞书给 feishu_writer 分配给您的 open_id 是 ou_cccc3333cccc3333cccc3333
  • • 这两个 open_id 字符串没有任何关联性飞书故意不让你建立关联——这就是”机器人维度隔离”

为什么您会建 10 个机器人(个人开发者的常见原因):

  • • 多场景分线:写作归写作机、研究归研究机、记账归记账机——避免一个机器人的”乱聊天”污染另一个
  • • 上下文隔离:feishu_cio 不会”记住”您跟 feishu_writer 聊过的内容——每个机器人都是独立大脑
  • • 故障隔离:某个机器人出问题时(凭证过期、限流、被封),其他 9 个照常运转
  • • 配置差异:不同机器人绑不同的飞书 agent(不同 cli_xxx),凭证独立管理

2.3 OpenClaw 的会话隔离机制

OpenClaw 通过 agent 维度 隔离会话状态:

文件系统层面

~/.openclaw/agents/├── feishu_cio/│   ├── agent/│   └── sessions/│       └── sessions.json   # feishu_cio 视角下所有 session├── wuzhao/│   └── sessions/│       └── sessions.json   # wuzhao 视角下所有 session└── ...

session key 格式

agent:<agentId>:<channel>:<channelId>:<peerId>示例:agent:feishu_cio:feishu:direct:ou_aaaa1111aaaa1111aaaa1111   # feishu_cio 跟"您"私聊agent:duola:feishu:group:oc_xxxxxxxxxxxxxxxxx                  # duola 视角下某个群

为什么这么设计

  • • 同一个用户给 feishu_cio 和 wuzhao 发的消息完全隔离(不同 session 文件)
  • • 一个 agent 的上下文不会泄漏到另一个 agent
  • • cron job 推送时按 agent 路由到对应 bot

subagent 调度时

  • • 父 agent(如 feishu_cio)spawn 出子 agent
  • • 子 agent 的 session key 以 subagent:<run_id> 标识
  • • 完成后结果通过 requester_origin_json 推回父 agent 的私聊
  • • 推回的 to 字段必须用父 agent 视角下用户的 open_id

2.4 三层 ID 体系对应关系(建议结合 §〇 全景图阅读)

┌──────────────────────────────────────────────────────────────┐│ 飞书企业 (Tenant)                                              ││   ├─ "您" (union_id: on_xxxxxxxxxxxxxxxxxxxxxxxx)            ││   │   ├─ 在 feishu_cio 视角: ou_aaaa1111aaaa1111aaaa1111     ││   │   ├─ 在 feishu_co 视角: ou_bbbb2222bbbb2222bbbb2222      ││   │   ├─ 在 duola 视角: ou_eeee5555eeee5555eeee5555         ││   │   └─ ... (跨 10 个 agent)                                  ││   └─ 其他用户...                                                ││                                                                ││ 飞书 agent (appId: cli_xxxxxxxx)                                 ││   └─ 绑定到 OpenClaw agent (agentId: feishu_cio)             ││       └─ agent 视角下的用户清单                                  ││           ├─ 您: ou_aaaa1111aaaa1111aaaa1111                  ││           └─ 其他用户...                                       ││                                                                ││ OpenClaw Agent (agentId: feishu_cio)                          ││   └─ Sessions                                                  ││       ├─ agent:feishu_cio:feishu:direct:ou_aaaa1111...        ││       ├─ agent:feishu_cio:feishu:group:oc_xxxxxx             ││       └─ agent:feishu_cio:cron:xxx                            │└──────────────────────────────────────────────────────────────┘

关键点解读

  • • Tenant 是企业层,所有用户都属于一个企业
  • • App 是飞书开放平台的应用,每个 agent 绑一个 app
  • • Agent 是 OpenClaw 视角下的智能体,与 appId 一一对应
  • • open_id 是 app 维度生成的,所以同一个用户在 10 个 agent 下字符串完全不同
  • • union_id 是 Tenant 维度生成的,所以跨 10 个 app 都一样

📌 提示:本节 ASCII 框图是 §〇 mermaid 全景图的文字版。读图请优先看 §〇。


三、各 ID 的使用场景

3.1 配置 cron job 的 to 字段

最常见场景,也是最容易踩坑的场景。

{  "delivery": {    "mode": "announce",    "channel": "feishu",    "to": "user:ou_aaaa1111aaaa1111aaaa1111"  // ✅ 私聊给"您"  }}

to 字段的合法值

to

 值
含义
例子(脱敏后)
user:ou_xxx 私聊

给某个用户
user:ou_aaaa1111aaaa1111aaaa1111
user:oc_xxx 群发

到某个群
user:oc_xxxxxx9999xxxxxx9999xxxxxx
oc_xxx

(无前缀)
❌ 错误!飞书不识别
oc_xxxxxx9999xxxxxx9999xxxxxx

最关键的检查清单

  1. 1. ou_xxx 必须是 cron job 所属 agent 视角下的 ID(不是其他 agent 下的)
  2. 2. oc_xxx 必须是 bot 已经在的群(bot 不在群里 → 报 230002 错误)
  3. 3. 必须带 user: 前缀(私有和群都是 user:xxx 形式)

怎么找正确的 to 值

# 1. 在对应 agent 的 session 文件里找(以 feishu_cio 为例)cat ~/.openclaw/agents/feishu_cio/sessions/sessions.json | \  python3 -c "import json, sys, red = json.load(sys.stdin)for k in sorted(d.keys()):    if &#x27;feishu:direct:ou_&#x27; in k:        m = re.search(r&#x27;feishu:direct:(ou_[a-f0-9]{20,32})&#x27;, k)        if m: print(f&#x27;  user:{m.group(1)}&#x27;)"# 输出: user:ou_aaaa1111aaaa1111aaaa1111

如果 cron job 在 feishu_cio 下,但 to 填的是 feishu_writer 视角下的 ID

  • • 飞书返回 230002 / 230020
  • • 必须改成 feishu_cio 视角下对应的 ou_xxx

3.2 调用飞书通讯录 API 反查用户

场景:拿到一个 ou_xxx,想知道它是谁。

注意

  • • 必须用对应 agent 的 appId/appSecret 拿 token
  • • 跨 app 的 open_id 在 token 视角下查不到(HTTP 400)
# 用 feishu_cio 视角查app_id = "cli_xxxxxxxxxxxxxxxx"  # 从 ~/.openclaw/openclaw.json 读 feishu_cio 对应的 appIdapp_secret = "***"  # 同上# 1. 拿 tokenimport requeststoken = requests.post(    "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal",    json={"app_id": app_id, "app_secret": app_secret}).json()["tenant_access_token"]# 2. 查 open_idr = requests.get(    f"https://open.feishu.cn/open-apis/contact/v3/users/{open_id}?user_id_type=open_id",    headers={"Authorization": f"Bearer {token}"}).json()# 返回:# {"code": 0, "data": {"user": {"name": "您", "open_id": "ou_aaaa...", "union_id": "on_xxxxxx", "mobile": "+86xxxxxxxxxxx"}}}

三种 ID 都能作为查询路径

# 用 open_id 查GET /contact/v3/users/ou_xxx?user_id_type=open_id# 用 union_id 查(同一个用户跨 10 个 app 都用这个 union_id)GET /contact/v3/users/on_xxx?user_id_type=union_id# 用 user_id 查(部分企业才有)GET /contact/v3/users/u_xxx?user_id_type=user_id

3.3 跨 agent 调度用户

场景:feishu_cio 想给 wuzhao 视角下”您”发消息。

错的做法

// 在 feishu_cio 视角下拿到的 ou_xxx,发给 wuzhao → 必死{  "channel": "feishu",  "accountId": "wuzhao",  "to": "user:ou_aaaa1111aaaa1111aaaa1111"  // ❌ 这是 feishu_cio 视角的 ID}

对的两种方式

方式 A:直接用目标 agent 视角下的 open_id

{  "channel": "feishu",  "accountId": "wuzhao",  "to": "user:ou_ffff6666ffff6666ffff6666"  // ✅ wuzhao 视角下的"您"}

方式 B:先查 wuzhao 的 session 文件得到正确 ID

cat ~/.openclaw/agents/wuzhao/sessions/sessions.json | \  python3 -c "..." | grep "ou_"  # → 拿到 ou_ffff6666...

现实场景:为什么您会需要”跨机器人调度”?

  • • 业务场景:feishu_cio(情报机器人)发现了一条告警,希望通过 wuzhao 机器人转发给”您”
  • • 技术场景:feishu_writer 写了一篇报告,希望通过 feishu_product 机器人推送给”您”
  • • 共同点:feishu_cio / feishu_writer 自己不能直接发(它们的 bot 跟”您”的关系是隔离的)——必须切换到目标机器人视角找到对应的 open_id

3.4 Session Key 解析

场景:从 OpenClaw session 名称反推是哪个 agent 的哪个私聊。

agent:feishu_cio:feishu:direct:ou_aaaa1111aaaa1111aaaa1111   │            │      │      │   │            │      │      └─ peer open_id(feishu_cio 视角下)   │            │      └─ direct=私聊 / group=群   │            └─ channel: feishu   └─ agentId

变体

  • • agent:<id>:cron:<job_id> — 定时任务 session
  • • agent:<id>:subagent:<run_id> — 子智能体 session
  • • agent:<id>:feishu:group:oc_xxx — 群 session
  • • agent:<id>:main — 主 session(agent 自身后台用)

3.5 通过 union_id 关联跨 app 用户

场景:想知道 feishu_cio 视角下的”您”和 wuzhao 视角下的”您”是不是同一个人。

# 1. 在 feishu_cio 视角查"您"user_a = query_contact("ou_aaaa1111aaaa1111aaaa1111", token_cio)union_id = user_a["union_id"]  # on_xxxxxxxxxxxxxxxxxxxxxxxx  (跨 10 个 app 都一样)# 2. 用 union_id 去 wuzhao 视角查user_b = query_contact(union_id, token_wuzhao, user_id_type="union_id")# → 返回 wuzhao 视角下的 open_id: ou_ffff6666ffff6666ffff6666

这是唯一能”识别同一个人”的方法

  • • 您拿到 ou_aaaa... 不知道是员工 A 还是员工 B
  • • 通过通讯录 API 查 union_id
  • • 然后用 union_id 在其他 app 视角下反查 open_id
  • • 把这些 open_id 关联到同一个员工档案里

3.6 端到端实战:从您发消息到 OpenClaw 处理的完整链路

场景:您在飞书客户端里给 feishu_cio 机器人发了一条消息:「今天天气怎么样?」

完整链路图(每条线都标了用到的 ID):

12 步文字拆解(每步标了用到的 ID):

步骤
动作
用到的 ID / 凭据
1

在飞书客户端输入消息
union_id: on_xxxxxx

(您身份)
2
飞书客户端把消息发给飞书 IM 服务器
收件人标识 = bot 的 appId: cli_xxxxx
3
飞书 IM 服务器生成该消息的全局 ID
message_id: om_xxxxxxxxxxxxxxxxxxxxxx
4
飞书 IM 服务器查 feishu_cio 视角下您的 open_id
open_id: ou_aaaa1111aaaa1111aaaa1111

飞书 IM 服务侧预先映射,不是您客户端传的)
5
飞书 IM 服务器推 webhook 给 feishu_cio 的 callback URL
携带:message_id / sender.open_id / chat_id / chat_type=direct
6
OpenClaw Gateway

 接 webhook
7
OpenClaw 飞书通道按 appId 路由到 accountId
cli_xxxxx

 → accountId: feishu_cio
8
飞书通道查 dedup 表去重
key = message_id
9
OpenClaw 路由到 agent
accountId = agentId: feishu_cio
10
加载/创建 session

,把消息追加到历史
session key = agent:feishu_cio:feishu:direct:ou_aaaa1111aaaa1111aaaa1111session 文件:~/.openclaw/agents/feishu_cio/sessions/sessions.json
11
feishu_cio agent 调飞书 API 拿 token POST /auth/v3/tenant_access_token/internal

body:{app_id: cli_xxxxx, app_secret: ***}返回:tenant_access_token(2 小时有效)
12
agent 查通讯录 API反查您的姓名
GET /contact/v3/users/ou_aaaa1111aaaa1111aaaa1111?user_id_type=open_id

Header:Authorization: Bearer <tenant_access_token>返回:{name, union_id: on_xxxxxx, mobile: +86xxxxxxxxxxx}
13
agent 查天气 API(业务逻辑)
第三方 API,跟飞书 ID 无关
14
LLM 思考 + 生成回复文本
15
agent 调用 im 发送把回复推给您
POST /im/v1/messages?receive_id_type=open_id

Header:Authorization: Bearer <tenant_access_token>body:{receive_id: "ou_aaaa1111aaaa1111aaaa1111", msg_type: "text", content: {...}}⚠️ receive_id 必须用 open_id,不能用 union_id(坑 4 强调过)
16
飞书 IM 服务器把回复推给您的客户端

ID 用量速查表(一次端到端流程涉及的全部 ID):

ID 类型
例子
出现位置
出现次数
union_id on_xxxxxx
步骤 1(您身份)/ 步骤 12(API 返回)
2
appId cli_xxxxx
步骤 2(路由)/ 步骤 11(token 申请)/ 步骤 15(im 发送)
3
appSecret ***
步骤 11(token 申请)
1
tenant_access_token t-xxxxxx
步骤 11(生成)/ 步骤 12-15(携带)
2
open_id ou_aaaa1111aaaa1111aaaa1111
步骤 4(飞书生成)/ 步骤 5(webhook 携带)/ 步骤 10(session key)/ 步骤 12(API 查询路径)/ 步骤 15(receive_id)
5
chat_id oc_xxxxxx
步骤 5(webhook 携带。私聊时就是您自己的 chat_id)
1
message_id om_xxxxxx
步骤 3(飞书生成)/ 步骤 5(webhook 携带)/ 步骤 8(dedup 去重)
3
agentId

 = accountId
feishu_cio
步骤 7(路由)/ 步骤 9(agent 启动)/ 步骤 10(session key 前缀)
3
session key agent:feishu_cio:feishu:direct:ou_aaaa...
步骤 10(session 落地)
1
chat_type direct
步骤 5(webhook 携带)/ 步骤 10(session key 中段)
2

如果换成群消息:唯一区别是

  • • chat_type = group
  • • chat_id = oc_xxxxxx(群的 chat_id)
  • • session key 变成 agent:feishu_cio:feishu:group:oc_xxxxxx
  • • 步骤 15 的 receive_id 改成 oc_xxxxxx(群 ID),仍不是用 union_id

一眼记住open_id 出现 5 次——它是这条链路里最忙的 ID,理解链路就抓住它。


四、可能遇见的坑

坑 1:cron job to 字段填错

症状

OutboundDeliveryError: Feishu send failed{"feishu_code": 99992360, "feishu_msg": "Invalid ids: [feishu_cio]"}

根因to 字段填的是 agentId/accountId,不是用户的 open_id。

修法

  • • 私聊:to: user:ou_xxx(带 user: 前缀 + 用户 open_id)
  • • 群发:to: user:oc_xxx(带 user: 前缀 + 群 chat_id)

坑 2:cron job to 用群 chat_id 但 bot 不在群里

症状

OutboundDeliveryError: Feishu send failed{"feishu_code": 230002, "feishu_msg": "Bot/User can NOT be out of the chat."}

根因

  • • to: oc_xxx 是个群 chat_id
  • • 但这个飞书 bot 没被拉进这个群
  • • 飞书不会自动加 bot,必须先在飞书群聊里 @ 这个机器人

修法(二选一)

  • • 在飞书里把机器人拉进群(oc_xxx 必须是 bot 已经在的群)
  • • 改成发私聊(to: user:ou_xxx

坑 3:跨 agent 混用 open_id

症状

feishu_code: 230020feishu_msg: "user not exist"  或  "chat not exist"

根因

  • • A agent 视角下的 ou_xxx 填到了 B agent 的 cron job 里
  • • B agent 的飞书应用不识别这个 ID(飞书的设计隔离)

举例

# ❌ 错feishu_writer 的 cron job 里 to 写 ou_aaaa1111aaaa1111aaaa1111# 这个 ID 是 feishu_cio 视角下的"您"# feishu_writer 的飞书 app 不认这个 ID# ✅ 对feishu_writer 的 cron job 里 to 写 ou_cccc3333cccc3333cccc3333# 这个 ID 才是 feishu_writer 视角下"您"

修法

  • • 找到目标 agent 视角下的正确 open_id(查它的 session 文件)
  • • 千万不要直接复制别处看到的 ou_xxx

坑 4:用 union_id 当 receive_id

症状

feishu_code: 230002feishu_msg: "Invalid receive_id"

根因

  • • im/v1/messages 的 receive_id 只接受 open_id 或 chat_id(群 ID)
  • • 不接受union_id 或 user_id
  • • 哪怕 union_id 才是”用户统一标识”,发消息时也只能用 open_id

修法

  • • 用 GET /contact/v3/users/{union_id}?user_id_type=union_id 反查 open_id
  • • 然后用 open_id 发消息

五、怎么提取openid

你在飞书里直接让机器人帮你提取就行

prompt:请你把我openclaw里的每个飞书机器人视角的 open_id整理成表格

六、最佳实践

6.1 配置 cron job 推送前的检查清单

  • • [ ] to 字段格式正确user:xxx
  • • [ ] ou_xxx 来自对应 agent 的 session 文件(不是凭印象或别处的)
  • • [ ] oc_xxx 群,bot 已经被拉进群oc_xxx 必须是 bot 实际在的群)
  • • [ ] bestEffort 字段按需设置true 失败不告警 / false 失败告警)
  • • [ ] 测试推送:手动 trigger 一次,验证能收到

6.2 跨 agent 调用 open_id 时的安全检查

def safe_query(open_id, expected_agent):    """跨 agent 查询时,先确认 open_id 来源"""    # 1. 在 expected_agent 的 session 文件里找    sess_path = f"~/.openclaw/agents/{expected_agent}/sessions/sessions.json"    # ... 找到则合法    # 2. 找不到 → 跨 app 错配,不可用    raise ValueError(f"open_id {open_id} 不属于 {expected_agent}")

6.3 写飞书集成代码的契约

永远不要假设

  • • ❌ “用户只有一个 open_id” → 不同 app 下有多个
  • • ❌ “我可以从 inbound metadata 直接拿 appId” → 那个是 agentId,appId 在配置里
  • • ❌ “open_id 是稳定的” → 同一用户在 app 升级后可能变

永远要做的

  • • ✅ 用 union_id 关联跨 app 用户
  • • ✅ 查通讯录 API 前先拿对应 app 的 token
  • • ✅ session 文件里找不到的 ID 先校验

6.4 调试时的思考顺序

遇到”消息发不出去”时,按这个顺序排查:

  1. 1. to 字段格式对吗?user:ou_xxx / user:oc_xxx
  2. 2. ou_xxx 是哪个 agent 视角下的?(必须和 agent 匹配)
  3. 3. oc_xxx 群,bot 在群里吗?(飞书后台 → 群设置 → 机器人)
  4. 4. union_id 拼接过吗?(不能直接当 receive_id)
  5. 5. 消息内容超长吗?(超过限制 → 拆/换卡片)
  6. 6. token 过期吗?(2 小时重新拿)
  7. 7. rate limit 触发了吗?(加 sleep / 错峰)