docs(runbook): Pi upstreams and subscription login

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-28 02:23:22 +08:00
1 parent fd3ffe5a8d
commit f0386af319
1 file changed
+66 -13
+66 -13
View File
@@ -108,21 +108,69 @@ curl -X POST localhost:4380/api/v1/automation-grants -H "Authorization: Bearer <
**有界尝试**:`evidence.autoRecover.attempt` 记录该根因的第几次自动恢复。同一去重键(如 `download_failed:<downloadTaskId>`)下,历史上 `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:<id>`),重新登录成功后事项自动关闭。
**订阅登录**(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` 指定,缺省为 `<cwd>/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。