1. 从一次「文档打不开」说起:OpenClaw 飞书插件编辑授权链路到底卡在哪
如果你正在用 OpenClaw 接飞书做团队助手,大概率遇到过这个场景:用户在飞书里 @ 机器人,让它整理一份会议纪要或需求文档,机器人很勤快地把文档建好了,链接也发回来了,结果提问的人点进去一看——只读,改不了。想补两句话,发现没有编辑权限,只能再 @ 一次机器人让它改,体验直接断掉。
这个问题的本质不是 OpenClaw 不会建文档,而是建完文档之后没有把提问者加成协作者。飞书文档的权限模型里,创建者(也就是机器人背后的应用身份)默认拿full_access,但被服务的那个真实用户,默认什么权限都没有。OpenClaw 的飞书插件在早期版本里,createDoc只做了docx.document.create,没有调用drive.permissionMember.create,所以文档天然是「机器人的」,不是「你的」。
我这次做的事情,就是把这个链路补全:先复现权限报错,再定位到插件源码里的两个缺口,然后给出两套可落地的方案——一套是改配置就能生效的perm: true,另一套是直接扩展createDoc函数和 Schema、把「给提问者授权」变成默认行为,最后提了 PR 并被 OpenClaw 核心仓库合入。整个过程对做 OpenClaw 插件开发、飞书应用权限配置、以及想给开源项目提 PR 的人都有参考价值。
先说清楚适合谁看:如果你只是想让机器人建的文档自己能编辑,看第 3 节的配置片段就够了;如果你想理解飞书权限体系怎么和 OpenClaw 插件对接,或者想复现我提 PR 的改法,那第 4、5 节是重点。全文的命令、配置、代码都可以直接复制到你的环境里跑。
需要提前说明的是,OpenClaw 的插件目录会因为你安装方式不同而变化。通过 npm 全局安装的,插件通常在/opt/homebrew/lib/node_modules/openclaw/extensions/feishu这类路径下;用其他方式装的,可以用npm root -g先确认全局包根目录,再拼上openclaw/extensions/feishu。找到这个目录,后面所有定位才有意义。
2. 接入前的准备:TaoToken 与 OpenClaw 飞书插件的环境对齐
在动代码之前,得先把「模型从哪来」这件事理顺。OpenClaw 本身是个 Agent 框架,它需要调用大模型来完成对话和工具编排,而飞书插件只是它众多 extension 中的一个。很多人卡在权限问题上,其实前面模型接入就没配好,导致 Agent 根本没跑起来,误以为是飞书权限的锅。
我自己的做法是把模型调用统一走 TaoToken 的 API。它的 Base URL 是https://taotoken.net/api,兼容 OpenAI 风格的接口,OpenClaw 里配置 provider 时直接填这个地址就行。API Key 在控制台的 API Keys 页面生成,模型 ID 按你实际要用的填,比如对话类任务用对应的对话模型,编码类任务用 coding 系列。这里要强调一个容易踩的点:Base URL、API Key、Model ID 这三件套必须同时正确,缺一个都会在请求阶段报错,而不是在飞书授权阶段报错,排查方向完全不同。
配置的时候,OpenClaw 的模型 provider 一般写在它的主配置文件里。如果你用的是 Claude Code 这类工具做辅助开发,它的settings.json里也是同样的三件套逻辑,Base URL 指向https://taotoken.net/api,Key 填你生成的,Model ID 填对应模型。我试过把这套配置直接复用到 OpenClaw 的 provider 段,省去了重新找文档的时间。
飞书这边的前置条件有三个,缺一不可。第一,你得有一个飞书自建应用,拿到app_id和app_secret,这两个填到 OpenClaw 的 feishu channel 配置里。第二,应用要开通文档相关的权限,具体清单在第 3 节给。第三,应用要发布并通过审核,否则权限申请了也不生效。很多人权限配了没反应,就是卡在「权限申请了但应用没发布」这一步。
环境对齐之后,你可以先用一个最小请求验证模型通路是否正常。在 TaoToken 的模型对话页面直接发一条测试消息,确认能返回结果,说明 Key 和模型 ID 没问题。然后再回到 OpenClaw,用openclaw gateway restart重启网关,让配置生效。这一步做完,再去复现飞书文档的权限问题,才能保证你看到的是真正的授权 bug,而不是环境没通。
3. 可复制配置:飞书应用权限清单与 OpenClaw 插件 perm 开关
这一节给的是「不改代码就能生效」的方案,适合绝大多数只想解决问题的用户。核心就两件事:飞书应用侧把权限开够,OpenClaw 插件侧把perm打开。
先看飞书应用的权限清单。进入飞书开放平台,找到你的自建应用,在「权限管理」里至少开通下面这些。文档创建需要docx:document,权限成员管理需要drive:drive或更细的drive:permission,读取用户信息需要contact:user.base:readonly或contact:user.id:readonly用来拿 open_id。如果你还要操作文件夹,drive:file也得开。开通之后记得在「版本管理与发布」里创建版本并发布,否则权限不生效。
然后是 OpenClaw 侧的配置。配置文件在~/.openclaw/openclaw.json,找到channels.feishu.accounts下面你那个 Agent 的标识,加上tools.perm开关。完整片段如下,可以直接复制,把Agent的标识换成你自己的:
{ "channels": { "feishu": { "accounts": { "Agent的标识": { "appId": "cli_xxxxxxxxxxxx", "appSecret": "xxxxxxxxxxxxxxxx", "tools": { "perm": true } } } } } }这个perm对应的是插件里的feishu_perm权限工具接口。它默认是false,意味着 Agent 在编排工具时不会去调用加权限的能力。改成true之后,Agent 才有机会在创建文档后调用drive.permissionMember.create给提问者授权。注意,这一步只是「允许 Agent 调用权限工具」,具体授权给谁、给什么级别,还是由 Agent 在运行时决定。
如果你更习惯用 OpenClaw 的 Web 管理页面,也可以在页面上找到对应 Agent 的飞书渠道配置,把权限工具开关打开,效果和改 JSON 一样。改完配置后,执行重启:
openclaw gateway restart重启完成后,再让机器人在飞书里建一篇文档,用提问者的账号点进去看能不能编辑。实测下来,目前版本做到这一步一般就能解决大部分场景。如果还是不行,说明你的 OpenClaw 版本里createDoc压根没有授权逻辑,perm: true只是打开了工具开关,但工具没被调用,这时候就得看第 4 节的代码方案了。
这里补一句关于三件套的提醒:如果你在配置过程中同时调整了模型 provider,务必确认 Base URL 是https://taotoken.net/api、Key 是有效的、Model ID 和任务匹配。飞书权限和模型接入是两个独立的链路,别把模型报错当成权限报错来查。
4. 从复现到定位:createDoc 缺授权、Schema 缺参数,PR 改了什么
配置方案能救急,但它有个根本问题:每个用户都得手动开一次perm,而且授权行为依赖 Agent 运行时是否记得调用。从产品角度看,提问者本来就该拿到自己文档的编辑权,这应该是默认行为,不该让每个人去外显配置。所以我决定改源码,把授权内建到createDoc里。
先定位代码。飞书插件目录下,核心文件是docx.ts和doc-schema.ts,工具配置在tools-config.ts。打开tools-config.ts,能看到perm默认关闭,这就是第一个缺口。再看docx.ts里的createDoc,原始实现只做了创建:
async function createDoc(client: Lark.Client, title: string, folderToken?: string) { const res = await client.docx.document.create({ data: { title, folder_token: folderToken }, }); if (res.code !== 0) { throw new Error(res.msg); } const doc = res.data?.document; return { document_id: doc?.document_id, title: doc?.title, url: `https://feishu.cn/docx/${doc?.document_id}`, }; }创建完直接返回,没有任何权限设置。这就是第二个缺口,也是权限问题的根因。飞书的权限体系里,创建者默认full_access,但其他成员必须显式调用drive.permissionMember.create才能授权。OpenClaw 没调,所以提问者没权限。
我的改法是给createDoc增加两个可选参数:ownerOpenId和ownerPermType,默认full_access。授权逻辑用 try/catch 包起来,保证「权限添加失败不影响文档创建」,这是容错设计,避免因为授权接口抖动导致整个建文档流程挂掉。改完的createDoc如下:
async function createDoc( client: Lark.Client, title: string, folderToken?: string, ownerOpenId?: string, ownerPermType: "view" | "edit" | "full_access" = "full_access", ) { const res = await client.docx.document.create({ data: { title, folder_token: folderToken }, }); if (res.code !== 0) { throw new Error(res.msg); } const doc = res.data?.document; const docToken = doc?.document_id; if (docToken && ownerOpenId) { try { await client.drive.permissionMember.create({ path: { token: docToken }, params: { type: "docx", need_notification: false }, data: { member_type: "openid", member_id: ownerOpenId, perm: ownerPermType, }, }); } catch (err) { console.warn("Failed to add owner permission:", err); } } return { document_id: docToken, title: doc?.title, url: `https://feishu.cn/docx/${docToken}`, ...(ownerOpenId && { owner_permission_added: true, owner_open_id: ownerOpenId, owner_perm_type: ownerPermType, }), }; }调用处也要同步传参,在 action 分发的地方改成:
case "create": return json(await createDoc( client, p.title, p.folder_token, (p as any).owner_open_id, (p as any).owner_perm_type, ));最后是 Schema 文件doc-schema.ts,给createaction 补上新参数,这样 Agent 在调用工具时才知道可以传owner_open_id:
{ action: Type.Literal("create"), title: Type.String({ description: "文档标题" }), folder_token: Type.Optional(Type.String({ description: "文件夹Token" })), owner_open_id: Type.Optional(Type.String({ description: "要授权的用户Open ID" })), owner_perm_type: Type.Optional(Type.Union([ Type.Literal("view"), Type.Literal("edit"), Type.Literal("full_access") ], { description: "权限类型,默认 full_access" })), }这三处改完,授权就变成了createDoc的内建能力。Agent 只要在建文档时把提问者的 open_id 传进来,文档创建完就自动带上编辑权限。这套改动我提了 PR,编号 28295,已经被 OpenClaw 核心仓库采纳并合入。对想给开源项目贡献代码的人来说,这个 PR 的粒度很典型:问题清晰、改动局部、有容错、不破坏现有调用。
5. 验证与排障:401、local proxy failed、reading choices 这些报错怎么对号入座
改完代码或者改完配置,怎么确认真的生效了?先重启网关:
openclaw gateway restart然后让机器人在飞书里建两篇文档,用提问者账号打开,确认能编辑。再从「他人视角」看——找一个没被授权的同事账号打开同一篇文档,应该只有只读权限。如果这两个条件都满足,说明授权链路是对的:提问者拿到编辑权,其他人没有,权限边界清晰。
接下来是排障对照表,这些都是我在调试过程中真实遇到或见别人遇到的报错,按现象对号入座能省很多时间。
| 报错现象 | 可能原因 | 排查方向 |
|---|---|---|
| 401 Unauthorized | API Key 无效或过期 | 检查 TaoToken 控制台的 Key,确认 Base URL 是https://taotoken.net/api |
| local proxy failed | 本地网络或代理配置异常 | 检查本机网络连通性,确认没有残留的代理环境变量 |
| reading choices 报错 | 模型返回结构不符合预期 | 确认 Model ID 和任务类型匹配,换一个模型试 |
| OAuth 相关报错 | 飞书应用授权流程未完成 | 检查应用是否发布、权限是否审核通过 |
| 文档建了但没权限 | perm未开或createDoc未传 owner | 先开perm: true,再确认代码版本是否含授权逻辑 |
关于local proxy failed,这里要特别说明:它通常和本机网络环境有关,排查时优先看系统代理设置和环境变量,不要往飞书权限上想。而reading choices这类报错,八成是模型返回的 JSON 结构和插件预期不一致,换模型或检查 Model ID 往往能解决。
还有一个高频坑:owner_open_id传的是 open_id,不是 user_id,也不是 union_id。飞书这几种 ID 长得像但含义不同,传错了授权接口会返回错误,但被 try/catch 吞掉,表现为「文档建了但没权限」,很容易误判成代码没生效。拿 open_id 的方式是通过contact:user.id:readonly权限配合用户查询接口,或者从飞书事件回调里直接取。
如果你在配置三件套时用的是 Claude Code 的settings.json,记得 Base URL、Key、Model ID 三项和 OpenClaw 里的 provider 配置保持一致,避免出现「一个工具能跑、另一个报 401」的割裂情况。排障的核心思路是:先确认模型链路通,再确认飞书权限开,最后才怀疑代码逻辑。
6. 把这次改动用起来:从配置到 PR 的完整路径
回头看这次贡献,最有价值的不是那几十行代码,而是把「提问者应该拥有自己文档的编辑权」这个默认预期,固化进了插件的创建流程。配置方案perm: true能解决眼前问题,但代码方案才是长期正确的形态,这也是 PR 被合入的原因。
如果你现在就想用起来,路径很清晰:先按第 3 节把飞书权限清单开全、把perm打开、重启网关验证;如果版本里还没有内建授权,就按第 4 节改docx.ts、doc-schema.ts和调用处,或者直接升级到包含 PR 28295 的版本。模型侧统一走https://taotoken.net/api,Key 在控制台生成,Model ID 按任务选,三件套对齐之后再排查飞书侧,方向不会乱。
想深入的话,建议你顺着createDoc的调用链往上读,看看 Agent 是怎么拿到提问者 open_id 的,这条链路打通了,你就能给 OpenClaw 加更多「默认合理」的行为,比如建表格自动授权、建多维表格自动加协作者。给开源项目提 PR 没想象中难,把一个真实痛点改干净、带上容错、不破坏现有调用,就是一份合格的贡献。