Skip to content

管理与远程预配

启用 shunt 的管理 Web 界面,以远程预配 Claude 与 Codex 账户并查看账户池状态。

Updated View as Markdown

shunt 可以暴露一个需管理员认证的 Web 界面,用于预配上游 Claude、Codex/ChatGPT、Antigravity 账户,并查看 claude_oauth、chatgpt_oauth、antigravity_oauth 账户池的状态。它是可选启用的:当 [server.admin] 不存在时,任何 /admin* 路由都不会注册,shunt 默认的 HTTP 暴露面保持不变。

它建立在 Anthropic 多账户的存储与选择行为之上。浏览器表单可以创建可刷新的 Full OAuth 账户或一年期、仅推理的 setup token 账户。导入已有的 Claude Code credential 文件仍然仅限 CLI。

启用管理界面

添加这个可选表,并通过配置的环境变量提供至少一个管理员凭据:

[server.admin]                        # 所有键都可选;显示的是默认值
header = "x-shunt-admin-token"
tokens_env = "SHUNT_ADMIN_TOKENS"
session_ttl_secs = 3600
pending_ttl_secs = 600
# hide_observed = false                 # (默认)扫描主机 CLI/应用登录
export SHUNT_ADMIN_TOKENS="ops:$(openssl rand -hex 32)"
shunt check
shunt run

凭据使用与 SHUNT_CLIENT_TOKENS 相同的逗号分隔 name:token 格式,但它们是相互独立的安全边界。不要把 [server.auth] 的客户端 token 复用为管理员 token。如果存在 [server.admin] 但三个凭据来源 —— token 环境变量(或 tokens_file)、[[server.admin.write_keys]] 与 [[server.admin.read_keys]] —— 全部为空或格式错误,启动会安全失败(fail closed)。name:token 对属于 write 层级;read_keys 条目可以通过管理界面的所有 GET,但在所有修改操作上都会被拒绝。它可以登录仪表盘,该会话是只读的。

每个键与默认值见配置参考。端点参考列出了浏览器路由和 JSON 路由。

当仪表盘被共享或通过远程访问,不应显示 —— 甚至不应读取 —— 网关主机上的提供方登录时,请设置 hide_observed = true。此后 shunt 会完全跳过主机 CLI/应用凭据的发现:GET /admin/api/observed 仍需管理员凭据,但返回空列表,仪表盘也不会请求它。Accounts and usage 表仍保留在页面上,只列出托管的池账户,因为这些账户的用量也显示在该表中。此变更在配置热重载时生效;已打开的仪表盘在页面加载时读取该标志,因此刷新页面后才会跟随新值。添加或移除 [server.admin] 本身仍需重启。

在浏览器中预配 Claude 账户

  1. 打开 /admin,用管理员 token 登录。

  2. 输入一个只含小写字母、数字和连字符的账户名。

  3. 选择 Full OAuth (refreshable)(仪表盘默认值)或 Setup token (1-year, inference-only),然后选择 Start。

  4. 在另一个标签页打开显示的授权 URL。登录目标 Claude 账户并批准访问。

  5. 把得到的 <code>#<state> 值复制回管理页面,选择 Complete。

  6. shunt 存储该账户。accounts 列表为空的 provider 会在下一个请求时拾取它,无需重启。否则,添加一个只带名称的条目并 reload:

    [[providers.anthropic.accounts]]
    name = "backup"

已开始的流程在 pending_ttl_secs(默认 10 分钟)内保持有效,给运营者留出打开授权页并粘贴结果的时间。服务器把选定的 mode 与 pending attempt 一起记录,所以 completion 请求无法切换 token 类型。从选择 Start 起直到流程结束,仪表盘会禁用登录方式单选按钮并说明原因,因为 pending attempt 的 mode 在请求发出时就已固定。再次选择 Start 并不会解除锁定;完成该流程或重新加载页面才能重新选择。选择 Start 还会关闭屏幕上已经打开的授权步骤,因此在发出替换请求的那一刻,它的链接和 Complete 表单就会消失;如果该替换请求失败 — 无论是被拒绝,还是根本没有响应 — 失败消息都会说明先前的步骤已被关闭。对于代码已经提交的步骤,只有在那次交换本身失败之后才会提到它,因为在此之前它仍可能存储账户;如果那次交换较慢,这句说明会在失败消息出现之后稍晚追加上去。服务器上留下什么则因情况而异:被拒绝的替换请求不会触碰先前的 pending 登录,它在剩余的 pending_ttl_secs 内仍然有效;而没有响应的请求可能已经到达服务器,若使用同一账户名,就已经把那个登录替换掉了。无论哪种情况,页面都已无法触及它,所以请重新开始该流程。Full OAuth 存储 access token 与 refresh token,其 credential kind 显示为 imported;setup-token mode 存储 kind 为 setup_token 的静态 credential。完成响应会报告账户是否已存储,以及当前的 provider 配置是否让它生效。

账户存储的变化按请求发现,因此扫描模式的提供方在账户增删后不需要重启。

在浏览器中预配 Codex 账户

  1. 在 Add Codex account 中输入小写账户名,选择 Start Codex login。
  2. 打开授权 URL,登录目标 ChatGPT 账户并批准访问。
  3. 浏览器会跳转到 http://localhost:1455/auth/callback。本地页面无法加载是正常现象。
  4. 从浏览器地址栏复制完整 URL,粘贴回管理页面并选择 Complete Codex login。JSON API 也接受 <code>#<state>。
  5. shunt 交换 code,并把可刷新的 Codex credential 写入私有账户文件。

accounts 为空的 chatgpt_oauth provider(包括默认 codex)会在下一个请求中发现新账户。若使用显式账户列表,请添加一个只含名称的 entry。SHUNT_CODEX_TOKEN_URL 仅用于本地集成测试覆盖 token endpoint;生产环境请保持未设置。

在浏览器中预配 Antigravity 账户

  1. 在 Add Antigravity account 中输入小写账户名,选择 Start Antigravity login。
  2. 打开 authorize URL,登录目标 Google 账户并批准访问。
  3. 浏览器会重定向到 http://localhost:51121/oauth-callback。本地页面无法加载是预期行为。
  4. 从浏览器地址栏复制完整 URL,粘贴到管理页面,选择 Complete Antigravity login。JSON API 也接受 <code>#<state>。
  5. shunt 会交换 code、解析该账户的 Code Assist project(失败不致命,会在账户的首个请求时重试),并写入一个私有的命名凭据文件。

accounts 为空的 antigravity_oauth provider(包括默认 antigravity)会在下一个请求中发现新账户。若使用显式账户列表,请添加一个只含名称的 entry。与 Codex 不同,Antigravity 表还提供 Refresh 按钮:按需执行已存储的 refresh 授权,报告该登录是否仍然有效。SHUNT_ANTIGRAVITY_TOKEN_URL 仅用于本地集成测试覆盖 token endpoint;生产环境请保持未设置。

查看池健康状况

仪表盘展示配置了 auth = "claude_oauth"、auth = "chatgpt_oauth" 或 auth = "antigravity_oauth" 的 provider 的账户存储元数据与当前状态。Claude 行包含从上游响应观测到的 5 小时、共享 7 天与 7d_oi 使用率,以及状态和冷却状态。当上游报告了重置时间时,每个窗口单元格还会显示距重置的剩余时间(例如 3d 4h),悬停可查看绝对重置时间。Codex 行显示 x-codex-* 响应 header 中所报告的 5 小时和 7 天窗口;不受支持的窗口会被忽略,7d_oi 保持为 —,因为 Codex 没有对应项。Codex 用量也会像 Claude 用量一样,用于配额感知的账户选择(issue #195)。

失效凭据与 Refresh 按钮

没有运维人员介入就无法通过任何重试恢复的账户,会显示为 Needs re-login 而不是 cooling —— 主表格和高级池表格都是如此。这个区分很重要:冷却会在 5 分钟后自行到期并重新尝试该账户,因此永久失效的凭据否则会永远看起来和配额暂停一模一样。该标记在以下情况置位:提供方终结性拒绝了 refresh 授权、根本无法刷新的凭据(token_env 令牌或已过期的 setup token)被拒绝,以及用刚刷新的 bearer 重试的请求仍然被拒绝。它与冷却相互独立上报,且仅存于内存,因此重启后会清空,直到该账户再次失败。该标记跟随凭据,而不是碰上失败的那个提供方条目:当同一个已保存账户在多个提供方表格中按名称启用时,它的所有行会一起翻转。指向自身 credentials 文件或 token_env 的账户即便同名也是另一份凭据,因此它上面的失败不会标记任何其他账户。

恢复办法是移除并重新添加账户。imported 账户的行还带有 Refresh 按钮:它会立即执行该账户的 refresh 授权并报告该登录是否仍然有效,便于在不等待流量的情况下确认重新登录是否成功。因为会请求提供方的令牌端点,所以受限流保护;它始终经由共享凭据存储,不会与代理自身的刷新竞争;并且会像正常使用一样轮换已存储的 refresh token。它只返回新的过期时间,绝不返回任何令牌材料。setup_token 账户不含 refresh 授权,因此不会联系提供方,直接返回 400。 该按钮仅在写入级会话中渲染(见下文)。

探测成功只在标记是由授权失败置位时才会清除它。如果账户是因为提供方拒绝了它已经签发的 bearer 而被判定失效,那么授权可用这一事实无法证明它能够提供推理服务,因此标记会保留,响应也会如实说明。

账户列表只暴露元数据:账户名、凭据类型(setup_token 或 imported)、过期时间和 UUID。它绝不返回 token 材料。shunt 在选择账户时如何使用配额状态、冷却和感知模型的周桶,见 Anthropic 多账户。

在仪表板中,Claude 表格把该过期时间渲染为由凭据类型推导出的 Status,因为原始值是约 8 小时的 access token 期限,在不同类型下含义完全相反。imported 账户持有 refresh token,shunt 会按需续期,所以它显示为 Auto-refreshes(原始时间戳可悬停查看),即使日期已过也属正常。setup_token 账户根本无法续期,因此它的一年期限确实需要处理:剩余时间多于刷新缓冲期时显示 Valid until 加日期,进入该缓冲期或日期已过则显示 danger 样式的 Expired 以及重新登录提示。该缓冲期与路由所用的一致,均为 5 分钟(EXPIRY_BUFFER),因此仪表板不会把路由已经会在无 refresh token 路径上失败的时间段标记为可用。每一行还提供 Re-login 按钮,它会用该账户的名称和登录方式预填上方的添加表单 — 用已有名称完成常规流程会就地覆盖该账户。Codex 表格也有同样的 Status 列,但它是无条件的:该 store 根本没有无法续期的类型(两条写入路径都要求 refresh token),所以 shunt 会续期每一个 Codex 账户,每一行都显示 Auto-refreshes,原始时间戳同样可悬停查看。Codex 行也带有相同的 Re-login 按钮,只是没有登录方式的预选 — 进入该 store 的途径只有 ChatGPT OAuth。Antigravity 表格同样无条件显示 Auto-refreshes,除 Re-login 按钮外还带有上文所述的 Refresh 按钮。上方用量表格中的只读行不受影响;那些凭据由拥有它们的 provider 客户端续期,而非 shunt。 这里的行操作都属于写入级。以 read_keys 登录时,你会看到相同的表格和相同的 Status 列,但不会渲染行操作按钮和添加账户表单:服务端的 require_write 会拒绝这些变更,所以仪表板根本不提供。

要通过 API/curl 访问账户 metadata、池健康状况、预配或删除账户,请在配置的头部(默认 x-shunt-admin-token)或 x-api-key 中发送管理员凭据(两个槽位都只在管理路由上被接受,推理路由上绝不接受),并使用 HTTP 端点中记录的 JSON route。头部认证的请求不使用浏览器会话,免于 CSRF 检查。开始预配时发送 { "name": "backup", "mode": "oauth" } 或 mode: "setup_token";省略 mode 时,为保持 API 向后兼容,默认使用 setup_token。完成调用的上游 token 交换在服务端有 30 秒上限:超过后 shunt 返回 502 和 Claude token exchange timed out(Codex 为 Codex token exchange timed out),并且不保存任何内容。超时后待处理的登录仍然保留,该次尝试也计入 5 次上限,但授权码可能已在上游被消费,因此请重新开始流程获取新的授权码,而不是重发旧的。

CLI 与 SSH 回退

当无法在浏览器中触达 shunt 主机时,请使用 CLI。Full OAuth 通常会打开浏览器,并通过临时的 127.0.0.1 callback 完成。在 SSH 或 headless 环境中,强制使用与管理页面相同的手动粘贴 redirect:

shunt login claude --name backup --mode oauth --manual

若要导入主机当前可刷新的 Claude Code 登录:

shunt login claude --name primary --mode import

若要创建一年期、仅推理的 credential:

shunt login claude --name ci --mode setup-token

--long-lived 保留为 --mode setup-token 的 deprecated alias。管理界面支持 Claude Full OAuth/setup-token、Codex ChatGPT OAuth、Antigravity OAuth 预配;只有导入已有 credential 文件需要主机访问,因此仍限 CLI。

安全

  • 把管理界面放在 HTTPS 或 WireGuard、Tailscale 等可信隧道之后。shunt 本身提供纯 HTTP;对外暴露时请在前面做 TLS 终止。
  • 生成强管理员 token,并与 [server.auth] 客户端凭据分开保管。管理员访问可以添加和删除上游账户。
  • 浏览器登录创建 HttpOnly、SameSite=Strict 的会话 cookie。除回环主机外该 cookie 为 Secure,因此本地 HTTP 开发仍可用。
  • 产生变更的浏览器请求需要按会话的 x-csrf-token 并通过同源检查。API/curl 调用改用管理员头部认证,不携带环境(ambient)cookie 权限。
  • 预配完成受速率限制。shunt 从不记录或返回 token 材料,账户的添加与删除会按账户名记入审计日志。

没有 [server.admin],这些路由就不存在。这比留着一个未认证的闲置仪表盘更强:除非显式启用,管理界面根本不存在。

Navigation

Type to search…

↑↓ navigate↵ selectEsc close