
用 Hermes Agent 搭个人 AI 助手时最常被卡住的是这一句permission denied。脚本前一天还能跑第二天换了身份登录就全线崩溃。把 lark-cli 的 137 个 scope 拉出来看你才发现当前身份只拿到十几个 granted所谓「服务不可用」大半是 scope 没配齐。而这类排查恰恰是 Agent 最擅长的事——让 Hermes Agent 去跑auth scopes并解读 JSON比你逐行翻日志快得多。代价是 Agent 每次推理都要消耗模型 Token所以你需要一个不折腾的模型接入通道TaoToken。先打开 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end 注册创建 Key把 Hermes Agent 的模型 API Base URL 指向 https://taotoken.net/api不要拼 /v1再让它去跑 lark-cli 的调试命令它会立刻告诉你contact:user.id:readonly和im:message是 granted 还是 missing。这样排查 permission denied 的时间能压到十分钟内。1. 为什么 Hermes Agent 排查 lark-cli 的 scope 这么费劲1.1 137 个 scope 不是一锤子买卖lark-cli 的权限模型和 GitHub PAT、Slack token 完全不是一回事。GitHub 一个 token 可以走天下飞书这边不行它把权限拆成domain:resource:action三段比如contact:user.id:readonly和im:message是两个完全独立的授权项。到写这篇文章的版本为止飞书一共开放了 137 个 scope数字还会随版本往上涨。这意味着写自动化脚本时第一步必须是「清点你当前身份到底拿到了哪些 scope」。我一开始没意识到这件事把lark-cli auth status当成万能诊断结果它只告诉我当前是 bot 还是 user不告诉我某个具体权限漏没漏。真正有用的是auth scopes它把当前身份的 scope 列表和 granted 状态一次性导出成 JSON后面所有排查都从这份清单开始。1.2 Agent 排查问题模型调用也在烧 Token让 Hermes Agent 去查 scope 是件舒服的事它能把「列出 scope → 过滤 grantedfalse → 定位到具体命令 → 给出补权限建议」这几步串起来。但舒服是有代价的——Agent 每一次工具调用、每一次解析 JSON 输出背后都是模型 API 的一次计费。手上 Key 一多今天用这个明天用那个Base URL 也换来换去排查完 scope 问题新的问题又来了到底哪个 Key 还有额度所以我先解决底座问题统一模型接入。TaoToken 只做一件事——给你一个稳定的 API Key 和 Base URL让 Hermes Agent 的模型调用不再散落在各个渠道。它不替代 lark-cli也不碰飞书的权限体系lark-cli 的 scope 清单永远来自你的飞书应用身份TaoToken 只负责让帮你排查的 Agent 跑得动、跑得起。2. 先拿 Key把 Hermes Agent 的模型调用指到统一通道2.1 注册并创建 API Key配置 Hermes Agent 之前先去拿一个 Key打开 TaoToken 注册在控制台创建 API Key选一个模型 ID 记下来。模型 ID 不要凭空猜以官网模型广场展示的为准。2.2 Hermes Agent 的模型配置里填什么拿到 Key 之后在 Hermes Agent 的模型配置入口填两个值BASE_URLhttps://taotoken.net/api API_KEYYOUR_API_KEY注意两个细节Base URL 末尾不要加/v1TaoToken 的接口根路径就是https://taotoken.net/api模型 ID 用你在模型广场看到的那个不要自己造后缀。配置完可以先用一条最简单的 prompt 验证连通性再进入 lark-cli 的排查流程。顺便把职责边界说清楚TaoToken 给你的是模型 API 的统一接入通道不是飞书 OAuth 的替代品。lark-cli 的 scope 授权、device flow、refresh_token 这些仍然走飞书自己的体系。两者各管一段——飞书管「你有没有权限」TaoToken 管「帮你分析权限的 Agent 用谁的模型额度」。3. lark-cli auth scopes 一行拉出全部授权状态3.1 命令与输出结构配好模型通道后第一件事就是跑下面这条命令把当前身份的 scope 全量导出来lark-cli auth scopes --app-id cli_xxxxxxxxxxxxxxxx --format json输出是一份 JSON 数组每个元素就是一个 scope 的授权状态{ data: { scopes: [ { scope: contact:user.id:readonly, granted: true, type: user }, { scope: im:message, granted: true, type: tenant }, { scope: im:message.group_at_msg, granted: false, type: user } ] } }granted: true表示当前身份可以直接调false表示申请了但没批或者根本没申请。后面排查 permission denied就是把报错命令里涉及的 scope 拿去和这份清单比对。3.2 快速找出缺失项清单拉出来后用grep直接筛lark-cli auth scopes | grep im:message如果想更省事让 Hermes Agent 读这份 JSON它会把所有granted: false的 scope 汇总成一张缺失清单再对照报错命令告诉你缺哪个。这就是我开头说的Agent 适合干这种多步骤编排前提是它的模型调用得先有稳定的通道。4. 五个高频 scope 的真实命令4.1 contact:user.id:readonly——按 ID 反查用户在通讯录里按user_id拿用户基本信息bot 身份也能调但只能拿到open_id、union_id和mobile_visible这几个字段lark-cli contact get-user --user-id ou_5bd5540825ba46b071285c220fa19d48想拿姓名、手机号、部门这些字段必须切换到 user 身份并申请contact:user:readonly。所以这个 scope 的排查要点是先看清当前身份是 bot 还是 user再核对返回字段是否符合预期。4.2 im:message——发消息机器人发消息到指定会话这是 cron 脚本里最常见的动作lark-cli im messages-send \ --chat-id oc_xxxxxxxxxxxxxxxxxxxxxxxx \ --text cron task finished, see /tmp/result.jsonim:message是 tenant 级 scopebot 身份默认就有。如果你发现某条消息带着 user 头像发出去了那说明走的是im:message.send_as_user那是另一套授权逻辑。4.3 drive:file:readonly——查文件元信息按 URL 查飞书 docx、sheet、bitable 的标题、创建者、修改时间lark-cli drive inspect --url https://xxx.feishu.cn/docx/xxxxxxxxxxxxxxxxxxxxxxxx这里有个坑用--token会报 unknown flag必须用--url传完整 URL 或 token 字符串都行。4.4 docs:document:readonly——读 docx 全文把整篇 docx 拉成 JSON分 v1 和 v2 两个模式lark-cli docs fetch --doc TOKEN --api-version v1 --format json lark-cli docs fetch --doc TOKEN --api-version v2 --format jsonv1 返回的是 XML 格式的完整 content适合脚本里做字符串替换v2 返回 markdown适合人眼 review。两个模式返回的content字段结构不一样别混用。4.5 docs:document——写 docx在已有文档里做精确替换lark-cli docs update --doc TOKEN \ --doc-format markdown \ --command str_replace \ --pattern 旧段落 \ --content 新段落写文档的坑比读文档多默认--command overwrite会把整篇文档覆盖掉批注全丢markdown 模式下要转义替换前最好先fetch确认旧字符串只出现一次改完再验证一次新旧字符串的计数。5. scope 分类速查表按业务域找对应权限5.1 九个高频域权限域关键 scope用途通讯录 contactcontact:user.id:readonly/contact:user:readonly反查用户信息即时消息 imim:message/im:message.send_as_user/im:message.group_at_msg发消息、私聊、群 云文档 docsdocs:document/docs:document:readonlydocx 读写云盘 drivedrive:file/drive:file:readonly文件管理多维表格 basebase:app/base:table/base:recordBase 读写日历 calendarcalendar:calendar/calendar:calendar.event日程管理任务 tasktask:task/task:task:readonly任务协作审批 approvalapproval:approval/approval:approval:readonly审批流邮箱 mailmail:mail/mail:mail:readonly邮件收发5.2 命名格式三件套第一scope 是单层冒号不是路径风格。contact:user.id:readonly是对的contact/user/id:readonly是错的。第二读权限认准readonlydrive:file:read这种写法不存在。第三飞书后来加了 wildcard比如im:message.*表示该域下所有 action但申请难度比精确 scope 大优先精确申请缺啥补啥。6. device flow 登 user 身份补齐私有 scope6.1 三步拿到 user 级权限bot 身份只能碰 tenant 级 scope想用im:message.send_as_user、contact:user:readonly这类 user 级 scope必须走 device flowlark-cli auth login --no-wait --json \ --scope im:message,im:message:send_as_user,contact:user:readonly返回 JSON 里有两个关键字段data.device_code和data.verification_uri。把verification_uri发给用户扫码十秒授权然后换 tokenlark-cli auth login --device-code device_code6.2 bot 和 user 的切换方式单次命令可以用--as指定身份lark-cli --as user contact get-user --user-id open_id lark-cli --as bot im messages-send --chat-id id --text hello注意 lark-cli 默认是 strict-modebot直接--as user会被拒先跑lark-cli config strict-mode off。6.3 三个绕不开的坑--scope是追加语义不是替换。第一次登录申请了 5 个 scope第二次想加 1 个必须把老 5 个和新 1 个一起传。另外auth status里identities.user.statusmissing是正常状态说明 user 身份还没登不代表配置坏了。最后refresh_token 默认 7 天过期到期前 lark-cli 会自动刷新如果连续一周没跑就要重新走一遍 device flow。7. 137 个 scope 的实时清单三个途径7.1 官方 API 一行查如果你有应用管理权限直接调接口拿全部申请记录lark-cli api GET /open-apis/application/v6/applications/app_id/app_version返回的data.app_version.scopes是全部申请记录scopes_added是已批准的。7.2 本地 schema 离线查lark-cli 自带 schema可以离线拉取全部 scopelark-cli schema --format pretty 21 | grep -oE scope[\.a-z_:] | sort -u /tmp/all_scopes.txt wc -l /tmp/all_scopes.txt文件行数就是当前版本的 scope 总数作者写文时是 137 行其中部分是新版本新增的。7.3 版本更新会影响数字飞书每个版本都可能新增 scope。升级后数字会变lark-cli update lark-cli schema | grep -c scope排查 scope 问题时先确认本机 lark-cli 是最新版避免拿着旧清单去对新的权限报错。8. 六次 scope 申请踩坑独立开发者最容易翻车的地方第一次申请im:message:send_as_user飞书开发者中心直接返回「权限范围受限企业专享」。重配三次全被拒查文档才知道这是企业自建应用才有的 scope个人应用只能以 bot 身份发im:message。第二次是contact:user.id:readonlybot 身份调get-user返回的 JSON 里只有open_id、union_id、mobile_visible没有 name、mobile、department。官方文档写的「读用户基本信息」其实是 user 身份 contact:user:readonly才能拿全。第三次是跨租户文档外部租户分享来的 docx 链接drive inspect直接 403。同一个租户内 bot 身份 drive:file:readonly就能读跨租户必须 user 身份 docs:document:readonly还得对方开了分享授权。第四次是docs:document想插入一段高亮 HTML 代码直接报「不支持 raw HTML」。docx 只认 markdown 元素和部分富文本标签HTML 块得先转成 markdown 或走 callout 富文本 API。第五次是往飞书 Base 批量插记录一次 1500 行报exceed batch size limit。Base API 单次上限 1000 行改成每批 800 行就稳定了。第六次是改日历事件的开始时间后已存在的提醒没有重置。calendar:event的 PATCH 不会重置reminders字段必须显式 PATCH 一次{reminders: [{minutes: 5}]}才会重发通知。这六个坑现在都沉淀在项目里每次新增 cron 脚本前先对照一遍能省掉大半调试时间。9. Hermes Agent 基础设施里的 scope 分配与调试三件套9.1 cron 任务和 scope 的对应关系我在项目里用 lark-cli 跑了多个 cron 任务每个任务所需的 scope 都很固定早安图推送只要im:message每日日报填表要base:app base:table base:recordRSS 入库要drive:file docs:document。写新脚本前先列出本任务必备 scope避免后期临时补权限。9.2 不需要额外 scope 的命令lark-cli doctor、auth status、config get/set、skills read、update这些都是内置命令不消耗任何 scope。所以排查权限问题前先用doctor确认环境健康再用auth status确认身份最后才用auth scopes核对清单别一上来就怀疑是权限问题。9.3 三个排查脚本粘贴即用第一个跑一遍 scope 清单并统计 granted 数量把结果落到/tmp/scopes.json后续脚本统一从这里读lark-cli auth scopes /tmp/scopes.json python3 -c import json data json.load(open(/tmp/scopes.json)) scopes data.get(data, {}).get(scopes, []) granted [s for s in scopes if s.get(granted)] print(fgranted: {len(granted)}/{len(scopes)}) for s in granted: print(s.get(scope)) 第二个看当前身份、bot/user 状态和 refresh_token 剩余有效时间避免在身份切换上浪费半小时lark-cli auth status /tmp/auth_status.json python3 -c import json d json.load(open(/tmp/auth_status.json)) print(d[identity]) 第三个调不通时按固定顺序兜底先auth scopes | grep missing_scope确认是不是权限问题再--as user切身份重试还不通就检查 refresh_token 是否需要重新 device flow最后才是lark-cli update lark-cli doctor收尾。这套流程把单次排查时间从两小时压到十分钟。10. 写在最后scope 拉出来只是开始关键是沉淀成脚本137 个 scope 本身不是障碍它是「精度的代价」。飞书把权限拆得越细自动化就越安全代价是你在排查时必须先看清自己的授权状态。用了 Hermes Agent 之后这个「看清」的动作变成一句话的事Agent 读 JSON、对比报错、给出缺失 scope全程只需要稳定的模型通道支撑。现在你可以去 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end 的控制台看一眼这次配置之后的模型调用有没有正常入账。然后把lark-cli auth scopes的输出存成一份基线文件下次报 permission denied 时先对比基线再决定是补 scope 还是换身份。脚本会替你记住这些坑Agent 会替你把排查过程跑完你要做的只是把 Key 配好然后让工具去干活。