免费额度不再动不动弹浏览器验证:连接器改用专用令牌

你在服务器上挂了一个定时任务,用免费额度读日历、查清单、调几个托管工具。它昨晚又挂了,日志里没有异常堆栈,只有一行打印出来的链接,然后任务安静地结束。你手动点了那个链接,一切恢复正常——但下次它可能还是在凌晨三点安静地失败。
这道“链接”就是免费额度的浏览器挑战(challenge)。上个月我们写过它为什么会突然出现,也讲过 Hermes 是怎么把它变成“打印链接、轮询状态”这种不阻塞的形式。但那只解决了一半问题:一个只用连接器的游客,本来就不该被要求证明自己是人。
v0.21.7 窗口里合入的改动把这一半补上了:连接器和托管工具现在走一枚专用令牌,压根不进挑战流程。
一个游客其实需要两种权限
先说清楚为什么会有两种令牌。免费额度用户(游客)在 Hermes 里能做两类不同的事:
- 调推理——让免费模型生成内容。这是要花算力的资源。
- 调连接器 / 托管工具——读账号、调工具网关、走连接器路由。这些大多是转发请求,不占推理额度。
挑战(含工作量证明 PoW)存在的理由是保护算力不被滥用,所以它天然只该挂在推理那一侧。可旧的实现里,游客只要碰到连接器,也会被牵着去换一个推理令牌——于是就有了一次不必要的挑战。
现在服务端会认这件事:匿名换取令牌的接口新增了 purpose 参数,purpose: "connectors" 铸出来的 JWT 只带 connectors:invoke 这一个 scope,网关对这类令牌直接免除挑战与 PoW 门槛,同时拒绝它去调推理。
scope 决定槽位,而不是“谁铸的”
这是这次改动里最值得记住的一条设计:一枚令牌进哪个槽位,由它自己的 JWT scope 声明决定,而不是由哪个铸造流程产生。
- 连接器令牌存在
providers.nous.connectors_token里(access_token+expires_at),紧挨着原来那个推理槽位。 - 推理令牌的铸造逻辑没有改动:仍然是在第一次真正调用免费模型时惰性铸造,并且仍然包在挑战流程里。
- 连接器调用方现在会优先复用手上已有的可用令牌——一枚推理令牌本来就能覆盖连接器,所以不会白铸一枚。
这条“看 scope 不看来源”的规则让兼容变得干净:如果你的服务端版本早于这次改动,它会忽略 purpose 参数、照样返回推理令牌,那枚令牌自然落进推理槽位;如果那台老服务端对一个连接器请求回了 428,连接器调用会退回今天的挑战处理方式,而不是直接失败。
三个以前会“顺手”铸推理令牌的后台路径
这部分是排查时最容易迷惑人的地方——它们都不是用户显式发起的操作:
- 认证保活(auth keepalive)。它以前每个 tick 都会刷新游客的推理令牌,而消息网关和桌面端后台会无条件启动它。在强制模式下,这就等于一个只用连接器的游客每个 tick 挨一个 428,桌面端还会弹出一个隐藏窗口。现在它遇到游客直接跳过,和启动函数本身早就有的那道判断保持一致:游客的令牌按需铸造。
- Nous 模型目录与定价。
_nous_catalog(通过provider_model_ids("nous")到达)和_resolve_nous_pricing_credentials以前跑在前台,所以你在终端里以游客身份列一下 Nous 的模型清单,就可能触发一次浏览器挑战。现在它们走后台调用方,失败时按原有逻辑退回内置的精选清单和公开定价。 - 账号读取。归入连接器一侧,因此拿的是连接器令牌。
三者共同的效果是:只有真正调用免费模型那一刻,才会出现挑战。
这次没有改的东西
- 已登录账号的行为、推理路径本身、以及游客的持久化结构都不变。
- 连接器铸造时返回 403(
signin_required/access_disabled)时,错误码和文案保持原样,没有面向用户的新文案。 - 两种铸造流程之间没有共享状态,所以也没有新增锁或按 purpose 分键的缓存:失败备忘只覆盖身份创建(它没有 purpose),5 秒的令牌备忘只存在连接器解析器上,挑战状态(工作锁、放弃冷却)只会被拿到 428 的铸造触及——在现在的服务端上那就是推理铸造。
怎么确认自己走的是哪条路
排障时你关心的是“这次调用到底去见谁了”。几个可用的观察点:
# 匿名令牌的换取与挑战状态(游客身份)
hermes logs --follow | grep -i -E "anon|challenge|connectors"
# 账号侧状态
hermes account status
# 认证令牌槽位的落盘位置(宿主 home 下)
ls -l "$HERMES_HOME/auth.json"
如果日志里出现挑战链接,而现在你只是在用连接器或托管工具,那说明你在跑老版本:升级到 v0.21.7 之后,这条路径不再需要过挑战。反过来,如果你确实要调免费模型,挑战出现是预期行为——那篇旧文讲的就是怎么让它在无人值守的场景下不阻塞。
注意
- 专用令牌不能当推理令牌用,服务端会拒绝。如果你手动把
connectors_token里的值塞进推理配置,只会得到认证失败。 - 免费池的默认值没有跟着“订阅者默认换 Krea”那条改动走,图片这类资源池的默认仍然独立判断。
- 如果你的服务端是自建/私有部署的老版本,行为取决于它:旧服务端忽略
purpose,一切照旧;回 428 时客户端退回旧处理,不会崩。
总结
这次改动的价值不在“新增了什么 API”,而在把权限切得更细了:需要证明你是人的那一侧(推理)保留挑战,纯转发的那一侧(连接器、托管工具)用一枚 scope 受限的令牌直接通行。对无人值守的定时任务来说,这意味着一个此前偶发、难以复现的失败模式消失了——不是被绕过,而是被正确地消除了。