☰
Hermes Agent 审批按钮源码解析:feishu.py:1906-1909 的 Allow Once/Session/Always/Deny 实现与 TaoToken 接入
2026/10/17 10:13:53 网站建设 项目流程

1. 飞书里点下「Always」那一刻,Hermes Agent 到底做了什么

Hermes Agent 是一个把大模型能力接到聊天平台里的 Agent 框架,飞书、Telegram 都能挂。它最实用的一个设计,是当模型准备执行一条有副作用的命令时,不会闷头就跑,而是先在聊天窗口里弹出一排审批按钮,让你人工点一下。这四个按钮就是 Allow Once、Session、Always、Deny,对应「本次放行」「本会话放行」「永久放行」「拒绝」。

如果你正在做飞书机器人、想让 Agent 帮你跑 git、ls、cat 这类命令,又不想它哪天手滑执行 rm -rf,那这套审批机制就是你的安全阀。而它的核心实现,就藏在feishu.py第 1906 到 1909 行这短短几行里。很多人第一次看到这段代码会懵:四个按钮怎么映射到四种权限?audit log 又是在哪一步落盘的?exec_allowlist 和按钮是什么关系?

这篇就按「先看问题场景 → 再讲 TaoToken 前置 → 给出可复制配置 → 验证请求 → 排错 → 收尾」的顺序走一遍。我会把feishu.py:1906-1909的分支逻辑逐行拆开,给你能直接抄的审批回调配置片段,再带你用 exec_allowlist 验证按钮行为、确认日志真的写进了磁盘。全程本地可复现,不需要你先把整个 Agent 跑通才看得懂。

先说清楚适合谁:一是已经在用 Hermes Agent、被「Always」按钮坑过的人;二是想给自己的飞书机器人加审批交互的开发者;三是想搞明白「按钮回调 + 审计日志 + 命令白名单」这套组合拳怎么落地的人。如果你只是想让模型跑起来聊天,那这篇偏底层,可以先收藏。

2. TaoToken 前置:统一 Key 与 API 通道,别让审批链路卡在鉴权上

在拆按钮逻辑之前,得先把模型调用这条链路铺好。Hermes Agent 的审批按钮之所以会弹出来,前提是模型已经能正常对话、能发起工具调用。如果 Key 或 Base URL 配错,你连按钮都见不到,直接 401 就结束了。所以这一步是后面所有验证的地基。

我这边统一用 TaoToken 来做模型通道。它的作用是给你一个兼容 OpenAI 风格的 API 入口,把 Key 和 Base URL 收敛成一套,飞书场景、本地调试、CI 里都能复用同一个通道,不用每个平台各配一份。官网在 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 根地址是 https://taotoken.net/api ,注意这个 API 地址后面不加任何查询参数。

具体要准备三样东西,这也是后面所有配置里反复出现的「三件套」:

配置项取值说明
Base URLhttps://taotoken.net/apiOpenAI 兼容入口,末尾不要带斜杠
API Key在控制台生成形如sk-...,只显示一次,记得存好
Model ID按你订阅的模型填例如claude-sonnet-4-5之类,以控制台实际列表为准

Key 的生成入口在控制台的 API Keys 页面,地址是 https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。生成之后先别急着填进 Hermes,建议用一条 curl 单独验证通道通不通:

curl https://taotoken.net/api/v1/chat/completions \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "claude-sonnet-4-5", "messages": [{"role": "user", "content": "ping"}], "max_tokens": 16 }'

返回里能看到choices[0].message.content就说明通道没问题。这一步很关键,因为 Hermes 的审批按钮依赖模型能正常返回工具调用意图,通道不通的话,你后面调feishu.py的按钮逻辑会一直以为是代码问题,其实是 Key 没生效。

如果你打算长期跑编码类 Agent、频繁触发审批,可以考虑用 Coding Plan 那套订阅,入口在 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。它更适合高频调用场景,省得每次都在额度上卡壳。模型对话的在线调试入口则是 https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,想先确认某个 Model ID 能不能用,直接在那里试一句最省事。

把这三件套记牢:Base URL、Key、Model ID。后面无论是 Hermes 的 config.yaml,还是 Claude Code 的 settings.json,填的都是这三样,只是字段名不同。

3. 可复制配置:feishu.py:1906-1909 分支逻辑与审批回调片段

现在进入正题。feishu.py第 1906 到 1909 行,是飞书审批按钮回调的核心分支。它的结构大致是这样:收到按钮点击事件后,先取出button_id和user_id,写一条 audit log,然后根据button_id决定这次命令的放行范围。四个按钮对应四种语义:

  • allow_once:只放行当前这一条命令,执行完权限即失效。
  • allow_session:在当前会话生命周期内,同类命令不再弹窗。
  • allow_always:写入持久化白名单,之后一直放行,这是最危险的一个。
  • deny:直接拒绝,命令不执行,同时记录拒绝原因。

用伪代码还原这四行的分支意图,方便你对照自己仓库里的真实实现:

# feishu.py:1906-1909 语义还原 def on_approval_button(button_id, user_id, command, session_id): audit_log(user_id=user_id, button_id=button_id, command=command, ts=now()) if button_id == "allow_once": grant(command, scope="once", session_id=session_id) elif button_id == "allow_session": grant(command, scope="session", session_id=session_id) elif button_id == "allow_always": require_admin_confirmation(user_id) # 二次确认,别省 grant(command, scope="always") else: # deny reject(command, reason="user_denied")

注意allow_always那一步,我在还原里特意加了require_admin_confirmation。原实现里如果没做二次确认,强烈建议你补上,因为「永远批」一旦写进白名单,后面再想收回就得手动改配置文件。这是踩过的坑:早期版本里 Always 是直接生效的,结果有人点了之后,Agent 连着跑了一串危险命令都没再拦。

接下来是配置层。Hermes 的配置在~/.hermes/config.yaml,和审批、沙箱、白名单相关的字段集中在这几块:

# ~/.hermes/config.yaml terminal: backend: docker # 危险命令丢进沙箱,影响面小 security: tirith_enabled: true # 自动拦截高危命令 exec_allowlist: - "git status" - "ls" - "cat" - "pwd" exec_denylist: - "rm -rf" - "curl|sh" - "sudo"

exec_allowlist和审批按钮是互补关系:白名单里的命令直接放行,不弹按钮;白名单外、又不在 denylist 里的命令,才会触发那四个按钮。所以你想复现按钮行为,就得先确保测试命令不在 allowlist 里,否则你点不到按钮,会误以为回调没生效。

如果你用的是 Claude Code 那套配置,字段名不一样,但三件套还是那三样。settings.json里长这样:

{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_API_KEY": "sk-你的Key", "ANTHROPIC_MODEL": "claude-sonnet-4-5" } }

飞书侧的审批回调,需要在飞书开放平台把卡片按钮的 action 指向你的回调地址,payload 里带上button_id、command、session_id。回调处理函数就复用上面on_approval_button的逻辑。这样按钮点下去,audit log 先落盘,再决定放行范围,顺序不能反——先授权后记日志,出了事你查不到是谁批的。

4. 验证请求:跑一条危险命令,看四个按钮和日志落盘

配置写完,得验证。第一步先确认 Hermes 能起来:

uv run python hermes

启动后,在飞书里给机器人发一条会触发审批的命令,比如让它执行rm -rf /tmp/hermes-test(测试目录,别拿真实数据试)。正常情况下,聊天窗口会弹出那张带四个按钮的卡片。

点Allow Once,命令执行一次;再发同样的命令,应该再次弹窗,说明 once 作用域没持久化。点Session,同一会话内再发同类命令,不再弹窗。点Deny,命令不执行,返回拒绝提示。这三个行为验证完,基本能确认feishu.py:1906-1909的分支是通的。

然后是 audit log。日志默认落在 Hermes 的数据目录下,具体路径看你的配置,常见的是~/.hermes/logs/audit.log。点完按钮后,用这条命令确认落盘:

tail -n 20 ~/.hermes/logs/audit.log

你应该能看到类似这样的记录:

{"ts":"2025-01-01T10:00:00Z","user_id":"ou_xxx","button_id":"allow_once","command":"rm -rf /tmp/hermes-test","scope":"once"}

每条记录都带user_id、button_id、command、scope,这就是审计的四个关键字段。如果日志里只有 command 没有 user_id,说明你的audit_log调用漏了参数,回去补上。

再验证 exec_allowlist。把git status加进 allowlist,重启 Hermes,发一条git status,这次应该直接执行、不弹按钮。把rm -rf加进 denylist,发一条rm -rf /tmp/x,应该直接被拦,连按钮都不弹。这两步验证完,白名单和黑名单的优先级就清楚了:denylist 优先于 allowlist,allowlist 优先于按钮弹窗。

最后确认模型通道没拖后腿。如果按钮弹出来了但点完没反应,先看 Hermes 的日志里有没有模型调用报错。用前面那条 curl 再测一次通道,确认 Base URL 和 Key 还有效。通道、按钮、日志三者都通,这条链路才算完整。

5. 常见报错排查:401、local proxy failed、reading choices、OAuth

这一节按真实会撞到的报错来对。每个报错我都给现象、根因、修法。

401 Unauthorized。现象是 Hermes 启动后模型调用直接失败,飞书里连按钮都看不到。根因九成是 Key 没生效或 Base URL 写错。检查config.yaml或settings.json里的 Key 是不是完整、有没有多余空格,Base URL 是不是https://taotoken.net/api(末尾别加斜杠,也别加/v1,具体以文档为准)。修完用 curl 复测一次。

local proxy failed。现象是请求发不出去,日志里出现连接失败。根因通常是本地网络环境或代理配置干扰了出站请求。检查你的环境变量里有没有残留的HTTP_PROXY、HTTPS_PROXY,有的话先清掉再试。Hermes 走的是标准 HTTPS 出站,不需要额外代理层。

reading choices 报错。现象是模型返回了内容,但解析choices字段时报错,常见于返回体不是预期的 OpenAI 结构。根因可能是 Model ID 填错,或者通道返回了错误对象。先确认 Model ID 和控制台列表一致,再用 curl 看原始返回体里有没有choices字段。如果返回的是{"error": ...},那就是通道侧的问题,检查 Key 权限。

OAuth 相关报错。现象是飞书侧回调鉴权失败,按钮点了没反应。根因是飞书应用的 OAuth 配置或回调地址没对齐。检查飞书开放平台里的事件订阅地址、卡片回调地址是否和你的服务一致,token 校验是否通过。这块和模型通道无关,别混在一起查。

排查顺序建议固定成:先 curl 验通道 → 再看 Hermes 启动日志 → 再查飞书回调 → 最后看 audit log。按这个顺序走,能避免在错误的方向上浪费时间。如果通道侧需要重新生成 Key,去 https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 处理;接入细节看文档 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。

6. 把审批链路收进一套 Key,长期跑更省心

feishu.py:1906-1909这四行,本质是把「一次授权」拆成了四个作用域,再用 audit log 把每次点击钉在磁盘上。你复现的时候,重点盯三件事:按钮分支对不对、日志有没有 user_id、allowlist 和 denylist 的优先级是否符合预期。这三件都对了,审批链路就稳了。

模型通道这边,用 TaoToken 把 Base URL、Key、Model ID 收敛成一套,飞书、本地、CI 复用同一个入口,省得每换一个平台就重配一遍。长期跑编码类 Agent 的话,Coding Plan 那套订阅更适合高频场景,入口在 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。想先在线试模型,用 https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 最快。

最后留个实用习惯:每次改完config.yaml,先跑一条 allowlist 里的命令确认不弹窗,再跑一条 denylist 里的确认被拦,最后跑一条两者都不在的确认弹按钮。三步走完,配置才算真正生效。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询