「收得到,发不出去」:一次飞书 OpenAPI 21001 策略拦截的完整排查
先说结论
某天开始,我的飞书机器人消息照收不误,但一条回复都发不出去。
根因不在我的代码、不在网络、不在凭证,而是飞书侧的一条未公开的访问控制策略:
用「豆包账号」身份登录的飞书账号,其名下自建应用只要配置了任何 OpenAPI 权限,在获取访问凭证(
tenant_access_token)时就会被策略拒绝,返回未收录于官方文档的错误码21001;而未配置任何权限的应用反而一切正常。换用手机号/邮箱注册的飞书账号建同权限的应用后,立即恢复。
这不是推测,是可以 100% 复现的实测结果(文末给了可复制的验证步骤)。
一、故障现象:一个反直觉的「半瘫」
| 通道 | 状态 |
|---|---|
| 入站(飞书 → 我的机器人,WebSocket 长连接) | ✅ 完全正常,消息收得到,甚至能正常打「正在输入」表情 |
| 出站(我的机器人 → 飞书,HTTP OpenAPI) | ❌ 全线失败:发消息、加表情、写云文档,一条不落 |
出站返回的是:
1 | HTTP 200 |
第一个关键信息就藏在这里:HTTP 状态码是 200。
不是超时、不是 5xx、不是连不上——请求稳稳到达了飞书并被正常处理,是飞书在业务层明确拒绝。这一条就把「网络问题」「防火墙」「DNS」全部排除掉了。
第二个关键信息:入站正常、出站失败。这个不对称非常有价值——它说明不是整个连接死了,而是某个只在 HTTP API 侧生效的检查没通过。
二、排查路径
第 1 步:先分清入站和出站
看网关日志:接收侧的长连接一直是稳的,发送侧每一条都在报同一个错误。故障定位于「出站 / 鉴权层」。
第 2 步:凭证对照实验(一分钟把「凭证错」排掉)
拿正确凭证和故意写错的凭证各打一次取 token 的接口:
| 使用的凭证 | 飞书返回 | 含义 |
|---|---|---|
| 正确的 app_secret | 21001 访问控制策略拒绝 |
凭证校验通过了,被后面的策略拦下 |
| 故意写错的 app_secret | 10014 app secret invalid |
凭证无效 |
结论很干净:飞书是先校验凭证、再走策略的。既然正确凭证没报「凭证无效」而是报「策略拒绝」,说明凭证是对的,卡点在策略层。
(这也说明:任何「重置 App Secret」的操作对本故障都是无效的——我确实也试过,白费一轮。)
第 3 步:21001 到底是什么?
这是整个排查最难受的部分——官方查不到:
- 飞书官方的《通用错误码》列表里没有 21001;
- 官方站内搜索
21001,国内站和国际站都是 0 结果; - 客服渠道问了一圈,没有有效回复。
唯一的官方线索来自飞书官方 CLI 的源码:
1 | const ( |
也就是说:这个码属于「安全策略类」,不是凭证错误、不是权限不足、不是频率限制。而且官方还在这条错误上预留了一个「验证/申诉入口」(ChallengeURL)字段——只是从没写过它什么时候会被触发。
第 4 步:★ 我在这里踩了一个大坑(把结论带偏了一天)
我当时的设计是:在同一个账号里新建一个「什么都没有」的裸应用,和出故障的原应用做对照。
结果非常好,好到让我错了:
| 应用 | 配置 | 结果 |
|---|---|---|
| 原应用 | 配了消息相关权限 | ❌ 21001 |
| 新建的裸应用 | 什么都没有:无权限、无机器人、未发布版本 | ✅ code 0,正常签发凭证 |
据此我得出了结论:「这个账号没问题,是我这个应用被单独限制了」——然后沿着「应用被限制」的方向查了一整天,毫无进展。
第二天我才意识到问题出在哪:那个「什么都没有」的裸应用,恰好绕过了这条策略。
教训:对照组选错,得到的结论比没有结论更危险。
这里的变量其实有两个——「账号身份」和「应用是否持有权限」。用空权限的应用去做对照,等于同时把这两个变量都改了,看起来找到了差异,实际是把真正的变量藏了起来。
第 5 步:二维对照实验,定案
重新设计对照:账号 × 应用权限,两个维度都取到:
| 带权限的应用 | 空权限的应用 | |
|---|---|---|
| 账号 A(豆包账号身份登录) | ❌ 21001 | ✅ code 0 |
| 账号 B(手机号/邮箱注册) | ✅ code 0 |
✅ code 0 |
只有「账号 A + 带权限」这一格是红的。
把它拆成可验证的断言:
- 同一个账号下,空权限应用通过、带权限应用被拒 → 不是「账号被封」,是权限维度的策略评估;
- 同一个权限配置下,账号 A 被拒、账号 B 通过 → 不是「应用有问题」,是账号身份维度;
- 两者在同一时刻、同一台机器、同一出口 IP、同一客户端上测的 → 源 IP、网络、客户端全部无关。
我随后又独立跑了两轮复核:8 次调用 8 次一致,零抖动。
三、可复制的验证步骤(约 2 分钟)
如果你怀疑自己遇到了同一条策略,可以这样验证。准备两个应用:一个随便加两个权限,一个什么都不加,然后用同一个账号的凭证分别请求:
1 | curl -s -X POST 'https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal' \ |
预期结果(如果你也在被拦):
| 应用 | 预期返回 |
|---|---|
| 带权限的应用 | {"code":21001,"msg":"Access denied by access control policy..."} |
| 空权限的应用 | {"code":0,"msg":"ok","tenant_access_token":"..."} |
| 带权限的应用 + 故意写错的 secret | {"code":10014,"msg":"app secret invalid"}(证明应用被平台正常识别) |
第三行是关键的对照组:如果错误凭证能拿到「凭证无效」而正确凭证拿到「策略拒绝」,那就说明你的凭证没问题,问题在策略层。
排查时注意把飞书返回的
x-tt-logid(请求追踪 ID)留存下来——它是唯一能向官方定位这次调用的凭据。
四、能查到的和查不到的
官方侧存在、但不点名的信号:
- 飞书开放平台 changelog,2026-07 下旬:「Agent 访问官方接口时增加账号保护:保护策略默认开启,只对飞书或 Lark 官方接口生效」
- 飞书开放平台 changelog,2026-10 上旬:「登录、获取凭证和身份验证遇到安全策略拦截时,保留服务端返回的提示、验证入口和规则信息」
查不到的:
21001的官方定义、触发条件、解除方式- 开发者后台里没有任何提示:没有站内信、没有邮件、没有横幅、没有违规通知
- 「豆包账号登录飞书后……此类账号没有企业管理后台」——这意味着这类身份连「去哪改设置、去哪申诉」的入口都不存在
同期社区里有其他人中招: 2026-10-13 前后,飞书官方仓库的两个 issue(larksuite/cli#2815、larksuite/openclaw-lark#639)以及若干技术论坛帖,记录的是同一个现象:入站长连接正常、出站全部 21001、无效凭证返回的却是另一个错误码。其中一条 issue 的标题直接点名了账号类型,评论里也有多位「豆包账号」用户报告同样的失败。
需要说清楚的一点: 以上是我能实测到的行为,不是官方说法。飞书至今没有就这条策略发布任何公告,本文不对其动机做任何推断——我只陈述可复现的事实。
五、如果你也遇到了:判别手册
第一步:把入站和出站切开。
长连接正常 + HTTP 全挂 → 问题在出站/鉴权层,不是网络。
第二步:看错误码定性。
| 错误码 | 含义 | 方向 |
|---|---|---|
21001 / 21000 |
访问控制策略拒绝(官方文档未收录) | 策略层 |
10014 / 10015 / 10003 |
凭证无效 / secret 错误 / 参数非法 | 凭证层 |
99991401 |
IP 白名单拒绝(且官方明确它不作用于 token 类接口) | 网络策略 |
99991403 / 429 |
用量配额或频率限制 | 用量 |
99991672 |
缺少某个权限点 | 权限申请 |
第三步:做凭证对照实验。(见上文,一分钟出结论)
第四步:做二维对照实验——这是最关键的一步。
1 | 带权限的应用 空权限的应用 |
- 只有换账号才好 → 账号身份维度
- 只有空权限应用能通 → 权限维度
- 千万别只用空权限应用做对照,它会给你一个看起来完美、实则错误的答案。
第五步:留存证据找官方。 记下 app_id、tenant_key、完整响应体、x-tt-logid。21001 未文档化,一线客服大概率无解,需要真正到 oncall 才能定位。
六、三条教训
- HTTP 200 + 业务错误码 ≠ 你的代码有问题。 当状态码成功而业务码异常时,要立刻把怀疑方向从「我的实现」转向「中间的策略层」。
- 对照实验的对照组选错,比没有对照更危险。 我因为一个「空权限的裸应用」,得出「只有我的应用被限制」的结论,白查了一整天。控制变量必须同时锁住你怀疑的每一个维度,哪怕这意味着多建两个应用。
- 平台静默变更 + 未文档化错误码,意味着你只能靠可复现实验自救。 这类故障里,社区里别人的复现记录(issue、论坛帖)往往比官方文档更有用——因为文档里根本没有这个东西。
顺便说一句:这次故障从发生到被发现静默了 17 小时——期间机器人一切看起来都在正常「工作」,只是所有回复都发不出去。如果这类链路对你重要,值得加一条主动健康检查,而不是等用户告诉你「你怎么不回我」。
后记
我已经把应用迁到手机号/邮箱注册的飞书账号上,收发恢复正常,并把整套判别流程记进了自己的运维笔记。
写这篇的目的很简单:这条策略是静默生效的,官方既没有公告也没有文档,遇到的人大概率会先怀疑自己的代码、网络或凭证,然后在「重置密钥」这种无效操作上耗掉大半天。如果这篇能让你在 10 分钟内定位到「是策略层」,那就值了。




