From f0386af319414f09cf0de432e5731292714ea6ae Mon Sep 17 00:00:00 2001 From: root Date: Mon, 28 Sep 2026 02:23:22 +0800 Subject: [PATCH] docs(runbook): Pi upstreams and subscription login Co-Authored-By: Claude Opus 5.5 --- docs/runbook/agent.md | 79 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 66 insertions(+), 13 deletions(-) diff --git a/docs/runbook/agent.md b/docs/runbook/agent.md index 07ced5ca..6a990012 100644 --- a/docs/runbook/agent.md +++ b/docs/runbook/agent.md @@ -108,21 +108,69 @@ curl -X POST localhost:4380/api/v1/automation-grants -H "Authorization: Bearer < **有界尝试**:`evidence.autoRecover.attempt` 记录该根因的第几次自动恢复。同一去重键(如 `download_failed:`)下,历史上 `recoverAttempted=true` 的事项数达到 `agent.investigateMaxAttempts`(缺省 2)后,同根因的新事项不再唤醒模型,直接 `awaiting_user`,并写 `skipped=attempts_exhausted`。 -## 配置上游 +## 上游与登录(W7-A) -在设置页或 `PATCH /settings` 里配置: +实现:`apps/server/src/modules/pi/`(上游 CRUD、能力表、测试、服务端订阅登录、刷新任务)、`packages/agent/src/pi/{catalog,upstream,fake-upstream}.ts`、 +合同 `packages/contracts/src/pi.ts`、迁移 `0027_pi_upstreams`。研究结论见 `docs/design/pi-providers.md`。 + +**模型**:一个上游(`pi_upstream`)= 一个 pi provider + 一份凭据。provider 是 pi-ai 0.87.1 内置的 41 个之一,或 +`openai-compatible`(自定义兼容端点,baseUrl 与模型必填,`extra.api` 选 openai-completions / openai-responses / anthropic-messages / google-generative-ai), +或 `fake`(脚本化假模型,不联网)。同一 provider 可以建多个上游(两个 Codex 账号、两个 OpenAI key):运行时每个上游一个独立的 pi `Models`, +凭据存储只映射到本上游的 secret。认证方式 `authKind`: + +| authKind | 凭据 | 说明 | +|---|---|---| +| `api_key` | secret 表存明文 key(`llm_api_key`),`extra.env` 存非密配置(Cloudflare ACCOUNT_ID、Azure 资源名等,见能力表 `envFields`) | 不读宿主环境变量,避免误用宿主的 key | +| `oauth` | secret 表存 pi 的 token 包 JSON(access / refresh / expires / accountId…) | 订阅登录,见下 | +| `none` | 不存密钥 | fake、无鉴权的本地端点、Bedrock 凭据链 / Vertex ADC(只有这一种会读进程环境变量与 `~/.aws`、gcloud 文件) | + +默认上游是 `settings.pi.defaultUpstreamId`(只能经 `/pi/upstreams` 的 `makeDefault` 改;第一条上游自动成为默认)。 +runAgent 按 `runAgent{upstreamId}` → `agent_run.upstream_id` → 默认上游 → 旧字段(`pi.provider` / `pi.apiKeySecretRef`,仅在没有任何默认上游时兼容,e2e 的 `pi.provider = "fake"` 走这里)选上游, +选中的上游写回 `agent_run.upstream_id / provider / model`。上游停用、未登录(`auth_required`)或登录失效(`expired`)时 run 直接失败并给出原因。 +0027 迁移把已保存的旧 `settings.pi`(anthropic / openai-compatible / fake)转成一条上游并设为默认,旧 `apiKeySecretRef` 移交给上游(设置里清空)。 + +**端点**(全部 admin scope;写、测试、登录只接受控制台会话,API Key 403): + +| 端点 | 说明 | +|---|---| +| `GET /pi/providers?models=false` | 能力表:authKinds(第一个是推荐项)、`oauth.modes`(browser / device)、`termsNote`(条款风险)、`baseUrlOverridable`、`envFields`、静态模型表 | +| `GET /pi/upstreams`、`GET /pi/upstreams/{id}` | 不含密钥,只给 `hasCredential`、`credentialMask`(前 4 后 4)、`account`、`oauthExpiresAt`、`isDefault` | +| `POST /pi/upstreams`、`PATCH /pi/upstreams/{id}`、`DELETE …` | `apiKey` 只写(PATCH 传 null 清除);provider 不可改;改 authKind 会删掉旧凭据;删除连同 secret | +| `POST /pi/upstreams/{id}/test {mode?: auth\|completion, model?}` | 缺省 `auth`:解析凭据(OAuth 必要时刷新)+ 校验模型在目录中,**不产生费用**;`completion` 再发一次最多 16 token 的补全 | +| `GET /pi/upstreams/{id}/models` | 静态目录(已配置凭据时按 provider 过滤,如 Copilot);Radius 先动态刷新,失败时回静态表并给 `refreshError` | +| `POST /pi/upstreams/{id}/oauth/start {mode?, enterpriseDomain?}` | 开始订阅登录,见下 | +| `POST /pi/upstreams/{id}/oauth/complete {sessionId, pastedUrlOrCode?, waitMs?}` | 完成 / 轮询登录 | +| `POST /pi/upstreams/{id}/oauth/logout` | 删除本地 token,状态回到 `auth_required`(不会撤销提供方侧授权) | + +**上游状态**(服务端维护):`auth_required`(新建 oauth / 未填 key / 已登出)→ 登录或填 key → `ok`;测试失败 → `error`(凭据被拒 → `auth_required`); +OAuth 刷新失败 → `expired` + 待处理事项「模型上游 X 登录已失效,需要重新登录」(去重键 `pi_upstream_expired:`),重新登录成功后事项自动关闭。 + +**订阅登录**(8 种:Anthropic Claude Pro/Max、OpenAI Codex、GitHub Copilot、xAI、Kimi Code、Meta Muse、OpenRouter、Radius;OpenCode Go / Zen 只有 API Key): +服务端调用 pi 的 `models.login`,注入挂起式交互 —— 选方式用 start 的 `mode` 作答,Copilot 的企业域名用 `enterpriseDomain` 作答。状态机: + +1. `start` → `{sessionId, mode, state: pending, authorizeUrl | userCode + verificationUrl, expiresAt}`(会话 15 分钟)。 +2. **browser**(Anthropic、OpenRouter、Codex / Radius 可选):用户在自己的浏览器打开 `authorizeUrl`,授权后会跳到 `localhost:<端口>`,页面打不开是预期的, + 把**地址栏完整 URL** 粘回来 → `complete {sessionId, pastedUrlOrCode}`。state 不匹配、code 缺失返回 `failed` 与可读原因。 +3. **device**(Copilot、xAI、Kimi、Meta,Codex / Radius 推荐):用户在 `verificationUrl` 输入 `userCode`;服务端轮询。前端反复调 + `complete {sessionId}`,每次最多等 `waitMs`(缺省 25s),未完成返回 `pending`。Codex 设备码返回 404 表示账号未开启,改用 browser。 +4. 终态:`succeeded`(token 包已加密写入,上游 `ok`)、`failed`、`expired`、`cancelled`(被同一上游的新登录、登出或删除取消)。 + +约束:同一 provider 的登录串行(另一个上游在登录中时 start 返回 409);pi 的浏览器流程会在服务端监听固定回调端口 +(Anthropic 53692、Codex 1455、Radius 1456),端口被占用时返回「服务端回调端口 N 被占用」,容器里不要让别的进程占这些端口。 +登录会话只在 api 进程内存里,多实例部署要把 `/pi/upstreams/*/oauth/*` 粘到同一实例。 +**条款**:订阅登录借用的是官方 CLI / IDE 的客户端身份(Anthropic 还会伪装成 Claude Code),用于自动化后台服务可能违反提供方条款、有被限流或封号风险; +能力表每个订阅型 provider 都带 `termsNote`,设置页须展示。 + +**刷新**:worker 每 10 分钟入队 `refreshPiTokens`,对 30 分钟内过期的 OAuth 上游调 pi 的 `getAuth(minOAuthValidityMs)`。 +pi 在我们的 `CredentialStore.modify` 里双重检查过期时间再刷新;`modify` 在事务里 `select … for update` 锁住上游行, +所以 api(测试端点)与 worker(Agent 运行时的按需刷新、定时任务)同时刷新也只刷新一次,轮换后的 refresh token 原子写回。 +OpenRouter 换到的是永久 key,不刷新。 + +## 运行参数 ```jsonc { - "pi": { - "provider": "anthropic", // 或 "openai-compatible"(baseUrl 要带 /v1);"fake" = 演示假模型 - "baseUrl": null, // Anthropic 写根地址,不带 /v1 - "model": "<模型 ID>", - "thinkingLevel": "medium", - "contextBudget": 128000, - "compaction": { "enabled": true, "thresholdRatio": 0.8 }, - "apiKey": "<只写,存为 secret>" - }, + "pi": { "contextBudget": 128000, "compaction": { "enabled": true, "thresholdRatio": 0.8 }, "subagentsEnabled": false }, "agent": { "maxToolCalls": 40, "maxDurationSec": 900, // 单次运行预算 "maxTokensPerRun": null, "maxCostUsdPerRun": null, @@ -135,14 +183,17 @@ curl -X POST localhost:4380/api/v1/automation-grants -H "Authorization: Bearer < } ``` +上下文预算裁剪模型的 contextWindow;思考档位取上游的 `thinkingLevel`,运行时夹到模型支持的档位。 + - 会话目录由环境变量 `AGENT_SESSION_DIR` 指定,缺省为 `/data/agent-sessions`。api 进程和 worker 进程要能读到同一个目录,这是崩溃恢复的前提。 - TMDB 密钥放在 `metadata.tmdbApiKey`(只写,存为 secret `metadata_api_key`),地址放在 `metadata.tmdbBaseUrl`(可选)。设置里没有时,回退到环境变量 `TMDB_API_KEY` / `TMDB_ACCESS_TOKEN` / `TMDB_BASE_URL`。 ## fake 模式演示 +在设置页新建一个 provider = fake、authKind = none 的上游并设为默认(没有任何上游时,旧的 `PATCH /settings {"pi":{"provider":"fake"}}` 仍然生效)。 +fake 上游也支持 authKind = oauth,用来联调登录流程:browser 模式粘回任意带 `code` 与正确 `state` 的 URL,device 模式约 3 秒后自动批准。 + ```sh -curl -X PATCH localhost:4380/api/v1/settings -H "Authorization: Bearer umt-dev-admin-key" \ - -H 'content-type: application/json' -d '{"pi":{"provider":"fake","model":"fake-model"}}' curl -X POST localhost:4380/api/v1/agent/runs -H "Authorization: Bearer umt-dev-admin-key" \ -H 'content-type: application/json' -d '{"goal":"看看库里有什么","mode":"plan_only"}' ``` @@ -187,4 +238,6 @@ curl -X POST localhost:4380/api/v1/agent/runs -H "Authorization: Bearer umt-dev- - 编译后的二进制(`deno compile`)只授予 `--allow-read=/app`,没有写权限。Agent 会话目录和入库 Copy/Move 在容器里都需要写权限,需要在 W4 统一补 `--allow-write=<数据目录,库根>`,在此之前生产容器里的 runAgent 会失败。 - 子代理(`subagentsEnabled`)只做记录,没有实现。 +- 41 个 provider 中只有请求形状与凭据装配经过测试(fake、自定义兼容端点真实走 HTTP);各家真实上游与 8 种订阅登录没有联网实测,首次接入请用「测试」按钮的 completion 模式确认。 +- 登录会话在 api 进程内存,进程重启后进行中的登录丢失,需要重新开始。 - 追加消息与正在收尾的 run 存在很小的竞态窗口。`finalize` 会检查一次未消费的消息并补唤醒一次 run。