docs: 第一个版本的讨论文档

This commit is contained in:
chuan
2026-08-14 14:47:33 +08:00
parent a6e764936d
commit 42be14c8d0
16 changed files with 6034 additions and 0 deletions
+437
View File
@@ -0,0 +1,437 @@
# HTTP API 模块
## 1. 定位
本模块冻结 cpa-ext 的 HTTP 边界:谁可以调用、使用哪些固定路径、请求/响应如何表达、错误是否稳定,以及 UI 如何只通过公开用例访问 Core。
API 不直接暴露 Repository 表,也不允许 UI 自行拼 SQL 语义。每个写接口都对应 [core.md](core.md) 中的应用用例,每个查询都经过权限化 facade。
当前宿主基线为 CLIProxyAPI `v7.2.130`
- 插件 Management routes 位于 `/v0/management/...`,由 CPA Management Key 保护;
- plugin resource 位于 `/v0/resource/plugins/<pluginID>/...`,宿主只支持未鉴权的精确 GET
- 插件声明的路径不能包含 `:`, `*``..`
- Management 和 resource handler 都会在进入插件前被宿主完整读入内存;
- `home.enabled` 时两类插件路由均不可用;
- Management JSON 的所有字符串目前会被宿主 HTML entity 转义,resource JSON 不会。
这些是设计约束,不是实现细节。
## 2. API 表面
### 2.1 管理 API
固定基路径:
```text
/v0/management/plugins/cpa-ext/v1
```
CPA 先执行 Management 鉴权,再把请求交给插件。cpa-ext 不创建第二套管理员密码或 session。第一版 CPA 只有一把 Management Key,因此所有 Management 调用都是全管理员权限,不能在文档中虚构细粒度 RBAC。
### 2.2 Browser resources
固定基路径:
```text
/v0/resource/plugins/cpa-ext
```
用途分为:
- 静态 UI shell 和构建产物;
- 最小公开 readiness
- 由插件自行校验 downstream Key 的用户只读 JSON。
resource route 在宿主层**没有鉴权**。任何包含管理员数据、写操作、OAuth 原文或数据库备份的 handler 都不得注册为 ResourceRoute。
### 2.3 模型调用 API
模型调用仍使用 CPA 原生 Codex/OpenAI Responses 路径。cpa-ext 通过 frontend auth、interceptor、scheduler 和 response/usage callback 参与,不另建代理入口。允许路径见 [access-routing.md](access-routing.md)。
## 3. 通用 wire 约定
### 3.1 JSON
- Content-Type`application/json; charset=utf-8`
- 字段名:`snake_case`
- 未知写入字段默认拒绝;
- 空集合返回 `[]`/`{}`,不使用 `null`
- ID 是 opaque string,调用方不能解析前缀获得权限;
- 时间是 UTC RFC3339Nano
- 时长使用整数毫秒或明确命名的秒数;
- 百分比使用 `0..100` 并明确字段名,fraction 使用 `0..1`
- Token 只出现在管理员审计 DTO;
- secrets、Authorization、Prompt、Response 和 raw Auth JSON 永不进入普通 DTO。
### 3.2 金额
```json
{
"currency": "USD",
"micros": 1234567,
"display": "$1.234567"
}
```
`currency + micros` 是机器真值,`display` 由服务端统一 formatter 生成。用户看到的余额、额度、消费和账单只有金额,不返回 credits/token 余额。管理员可以在账单明细中额外看到 Token、费率和倍率。
### 3.3 成功响应
单对象:
```json
{
"data": {},
"meta": {
"request_id": "req_opaque",
"revision": 12
}
}
```
列表:
```json
{
"data": [],
"meta": {
"request_id": "req_opaque",
"next_cursor": "opaque_cursor",
"has_more": false
}
}
```
创建 Credential/轮换 secret 的第一次成功响应额外包含 `secret`,并强制 `Cache-Control: no-store`。明文只展示一次。
### 3.4 错误响应
```json
{
"error": {
"code": "billing_quota_exhausted",
"message": "Amount balance is exhausted.",
"request_id": "req_opaque",
"retryable": false,
"details": {
"field": "billing_account_id"
}
}
}
```
`message` 可本地化且不得作为程序判断依据;`code` 稳定。`details` 只包含安全字段、约束和资源 ID,不回显 secret 或原始数据库错误。
状态映射:
| HTTP | 语义 |
| ---: | --- |
| 400 | DTO/字段/游标无效 |
| 401 | 用户 Key 无效;当前 CPA frontend auth 也会把认证依赖故障折叠为 401 |
| 403 | 已认证但无权限/入口不允许 |
| 404 | 资源不存在或为防枚举而隐藏 |
| 409 | revision、状态或幂等冲突 |
| 412 | `expected_revision` 不匹配 |
| 413 | 请求体过大;真正上限必须先由反向代理/CPA HTTP 层执行 |
| 422 | 语法正确但领域规则不可满足 |
| 429 | 金额额度、并发或速率限制 |
| 500 | 未分类内部缺陷,响应脱敏 |
| 503 | 数据库、账本、价格或必需 runtime 不健康 |
### 3.5 版本与并发控制
- 路径版本为 `v1`
- 响应头返回 `X-CPA-Ext-API-Version: 1`
- 每个可写对象返回 `revision`
- PATCH/PUT/状态动作必须携带 `expected_revision`
- 版本不匹配返回 `412 revision_mismatch` 和当前 revision
- 不允许 last-write-wins 覆盖套餐、价格、路由和账户状态。
### 3.6 幂等
以下写操作必须携带 `Idempotency-Key`
- provision/签发/轮换 Credential
- 金额 adjustment
- 价格发布/回滚;
- quota 批量刷新;
- backup/rebuild 等 maintenance command。
相同 key + 相同 payload 返回同一业务结果;相同 key + 不同 payload 返回 `409 idempotency_conflict`
Credential 明文不持久化,因此签发/轮换响应重放只保证不会再创建第二个 secret:首次响应有 `secret`,后续幂等重放返回同一 Credential 但 `secret=null``secret_available=false`。如果调用方丢失首次响应,只能执行轮换。
## 4. 管理 API 路由
CPA 只允许精确路径,所以记录 ID 放 query/body,不使用 `/credentials/:id`
### 4.1 系统与诊断
| Method | Path suffix | 用途 |
| --- | --- | --- |
| GET | `/status` | 版本、runtime topology、schema/capabilities、数据库与统计状态 |
| GET | `/readiness` | 计费依赖、HMAC keyring、active price、gateway 前置条件 |
| GET | `/diagnostics` | 脱敏故障、lag、unpriced/unmeasured/persistence gap |
`readiness` 只能证明插件自身依赖。部署网关还必须验证 CPA 中插件 active、Home 关闭、exclusive/sentinel 流量探针成立,不能只信一个自报 JSON。
### 4.2 下游账户、Credential 与套餐
| Method | Path suffix | 用途 |
| --- | --- | --- |
| POST | `/provision` | 原子创建 BillingAccount、绑定 Plan/Route 并签发第一把 Key |
| GET | `/billing-accounts` | 列表或 `?id=` 详情 |
| POST | `/billing-accounts` | 创建空账户 |
| PATCH | `/billing-accounts` | 修改 label/status/并发等安全字段 |
| GET | `/credentials` | 列表或 `?id=` 详情,不返回明文/digest |
| POST | `/credentials` | 为现有账户签发 Key |
| PATCH | `/credentials` | label、有效期、模型/tier 范围 |
| POST | `/credentials/rotate` | 创建新 SecretVersion 并按策略失效旧版本 |
| POST | `/credentials/revoke` | 撤销 Credential/SecretVersion |
| GET | `/plans` | 计划列表或详情 |
| POST | `/plans` | 创建计划 |
| PATCH | `/plans` | 修改计划,必须预览影响 |
| DELETE | `/plans` | retire 未使用计划;历史引用不删除 |
| GET | `/route-policies` | 查询账户/credential 路由策略 |
| PUT | `/route-policies` | 原子替换一条策略及绑定 |
`POST /provision` 是 UI 的主路径,避免业务层出现“账户创建成功但 Key/Plan/Route 只完成一半”。其事务边界以 [core.md](core.md) 为准。
### 4.3 金额账本
| Method | Path suffix | 用途 |
| --- | --- | --- |
| GET | `/ledger` | 按 account、时间、kind 稳定分页 |
| POST | `/ledger/adjustments` | 管理员充值、扣减、refund/纠错 |
| GET | `/billing-cycles` | 周期、额度、消费、余额投影 |
Adjustment 必须包含 `billing_account_id`、Money、kind、reason、`expected_revision``Idempotency-Key`。API 不提供“直接改 balance”端点。
### 4.4 价格
| Method | Path suffix | 用途 |
| --- | --- | --- |
| GET | `/pricing/active` | active version 与模型政策 |
| GET | `/pricing/versions` | 版本历史/详情 |
| GET | `/pricing/candidates` | 候选、来源和冲突 |
| POST | `/pricing/candidates/refresh` | 拉取外部 candidate,不发布 |
| POST | `/pricing/preview` | 校验 draft、diff、golden cases |
| POST | `/pricing/publish` | 显式发布不可变版本 |
| POST | `/pricing/rollback` | 以历史内容创建并发布新版本 |
没有“直接 PUT 当前价格”的捷径。所有变更走 draft/preview/publish,详见 [pricing.md](pricing.md)。
### 4.5 上游账户和池
| Method | Path suffix | 用途 |
| --- | --- | --- |
| GET | `/upstream-accounts` | 目录、状态、priority、bindability |
| POST | `/upstream-accounts/sync` | 触发 host.auth.list reconciliation |
| POST | `/upstream-accounts/reconcile` | 人工确认 Auth replacement/merge 拒绝 |
| GET | `/upstream-quotas` | 最新 quota snapshot |
| POST | `/upstream-quotas/refresh` | 单个/有界批量按需刷新 |
| GET | `/upstream-pools` | 池列表/详情 |
| POST | `/upstream-pools` | 创建池 |
| PATCH | `/upstream-pools` | 修改名称/成员/revision |
| DELETE | `/upstream-pools` | retire 无引用池 |
Quota refresh 请求只接受内部 `upstream_account_id[]`,不接受 URL、Header、token 或任意 provider payload。
### 4.6 统计、请求与审计
| Method | Path suffix | 用途 |
| --- | --- | --- |
| GET | `/statistics/overview` | 金额/请求/健康总览 |
| GET | `/statistics/activity` | 时间序列 |
| GET | `/statistics/analysis` | 维度分析 |
| GET | `/requests` | Request 列表或 `?request_id=` 详情 |
| GET | `/executions` | Execution/重试明细 |
| GET | `/audit-events` | 管理动作与安全事件 |
图表接口返回服务端聚合。请求详情默认不含 Prompt/ResponseCPA request log 访问走单独显式授权、短期 token、审计和脱敏流程。
### 4.7 Maintenance
| Method | Path suffix | 用途 |
| --- | --- | --- |
| GET | `/maintenance/status` | checkpoint、备份、完整性和 runner 状态 |
| POST | `/maintenance/backup` | 创建 SQLite online backup |
| GET | `/maintenance/backups` | 脱敏备份清单,不直接返回任意文件路径 |
| POST | `/maintenance/integrity-check` | 启动有界检查 |
| POST | `/maintenance/rebuild` | 从指定 event_seq 重建投影 |
在线 API 不提供 restore、删除数据库或任意路径下载。恢复属于 [operations.md](operations.md) 的离线、排空流程。
## 5. 用户只读 API
固定 ResourceRoute
| Method | Path | 用途 |
| --- | --- | --- |
| GET | `/v0/resource/plugins/cpa-ext/v1/me` | 当前 Credential、共享金额账户和周期摘要 |
| GET | `/v0/resource/plugins/cpa-ext/v1/me/requests` | 当前 Credential 的逐请求金额记录 |
| GET | `/v0/resource/plugins/cpa-ext/v1/me/activity` | 当前 Credential/账户金额趋势 |
鉴权:
```http
Authorization: Bearer <downstream-key>
```
规则:
- Key 不得放 query、fragment、cookie、URL 或 localStorage
- 插件自行 HMAC lookup,并按 Credential scope 构造查询;
- 共享 BillingAccount 的余额可以显示,但默认请求列表只显示当前 Credential 产生的记录;
- 请求参数中的 account/credential ID 不能扩大 scope
- 响应 `Cache-Control: no-store``Pragma: no-cache``Vary: Authorization`
- 错误不区分不存在、撤销、过期等可枚举细节;
- 用户接口只返回 Money、模型展示名、时间和安全状态,不返回 Token、上游账户、价格规则或管理员诊断。
当前 resource 只有 GET,且没有 HttpOnly session/CSRF 模型。需要用户写操作、稳定登录或团队账号时必须增加 sidecar/public API 或扩展 CPA authenticated user plugin routes。
## 6. UI resources
至少注册:
```text
GET /v0/resource/plugins/cpa-ext/ui
GET /v0/resource/plugins/cpa-ext/assets/<每个确定文件名>
GET /v0/resource/plugins/cpa-ext/ready
```
宿主不支持 wildcard,因此每个构建产物在 `management.register` 中逐个声明。前端使用相对 base、HashRouter、不注册 service worker。
`/ready` 只返回:
```json
{"ready":true,"version":"0.1.0"}
```
它不包含数据库路径、price 内容、账户数、错误详情或 secret。外部 gateway 可以把 404/非 200 视为插件缺失,但仍需执行部署级旁路探针。
## 7. 查询、分页和导出
- 默认 `limit=50`,最大 `200`
- 使用 `(occurred_at, stable_id)` 编码的 opaque cursor,不使用大 offset
- cursor 绑定 endpoint、scope、排序和过滤条件,修改条件后 cursor 无效;
- 默认时间范围 24 小时,普通交互最大 90 天;
- 过滤字段使用 allowlist,未知字段返回 400
- 排序字段固定,禁止任意 SQL column;
- 大导出使用受控异步任务/sidecar,不能在动态库 handler 中一次加载全表;
- CSV 需要防 `=`, `+`, `-`, `@` 公式注入,并记录导出审计。
## 8. 输入与资源限制
插件 handler 内执行:
- 方法和精确路径二次校验;
- Content-Type 校验;
- strict JSON + 单文档 EOF
- 字段长度、集合数量、时间范围和分页上限;
- 响应行数/字节预算;
- context cancellation
- 不持锁执行 SQL、host callback 或网络。
但 CPA 当前在调用 handler 前执行 `io.ReadAll`,所以这些校验无法保护宿主免受超大 body。生产反向代理/CPA HTTP 层必须先限制:
- Management body 建议不超过 1 MiB
- Resource GET 不接收 body
- Header/query 总大小由入口代理限制;
- 请求读取超时和并发连接数在入口层限制。
## 9. 当前 CPA Management JSON 转义限制
`internal/pluginhost/management.go` 会对看起来像 JSON 的响应递归执行 `html.EscapeString`。因此 `A & B <x>` 在 wire 上会变成 HTML entity;这不是标准 JSON API 应有的语义。
过渡规则:
- cpa-ext Management JSON 响应头标记 `X-CPA-Ext-String-Encoding: html-entity-v1`
- 内置 UI 对**响应字符串**最多兼容 decode 一次,再通过 React text node 渲染;
- 禁止 `dangerouslySetInnerHTML`
- IDs、error codes、cursor 只使用不受影响的安全字符;
- 写 DTO 不能盲目回传未 decode 的读 DTO
- 测试覆盖 `A & B <x>` 往返和已含 entity 文本;
- 在 CPA 停止改写 JSON 或 API 移到 sidecar 前,不将 Management API 宣称为无损的通用第三方 API。
Resource JSON 不经过该 escape 路径,仍然必须使用普通 JSON encoder 和文本渲染防 XSS。
## 10. 安全头与浏览器策略
HTML
- `Content-Security-Policy`,默认 `default-src 'self'`,按实际构建最小放开;
- `X-Content-Type-Options: nosniff`
- `Referrer-Policy: no-referrer`
- `Cache-Control: no-store`HTML shell);
- 不使用 inline script,或固定构建 hash
- 不信任 iframe parent 消息,除非校验明确 origin。
敏感 JSON
- `Cache-Control: no-store`
- 默认不启用 CORS
- 不在错误响应反射 Origin/Header/Body
- 不把 Management Key 或 downstream Key 写入前端持久存储。
完整要求见 [security.md](security.md)。
## 11. 审计
以下写操作必须形成审计事件:
- provision、签发、轮换、撤销;
- 账户/套餐/路由/池修改;
- 金额 adjustment/refund
- 价格发布/回滚;
- 上游 Auth reconciliation
- quota reset credits 等有副作用 provider 操作;
- backup、rebuild、导出;
- 安全设置和热配置拒绝。
CPA 当前只有共享 Management KeyActor 至少记录 management principal fingerprint、来源 IP 的受信代理结果、request ID、reason 和变更前后 revision。调用方自报 `actor` 只能作为 label,除非受信 gateway 已验证并覆盖该 header。
## 12. 如何参考现有项目
### 12.1 CLIProxyAPI
- `sdk/pluginapi/types.go`ManagementRequest/Response、Route/Resource
- `internal/pluginhost/management.go`:精确路径、未鉴权 resource、body ReadAll、JSON escape
- `internal/api/server_management.go`Management middleware、Home 限制和 route mounting
- `examples/plugin/management-api/`ABI/RPC 形状。
### 12.2 `cpa-plugin-key-billing`
- `internal/plugin/management.go`:精确 route 表、query/body 传 ID、严格 DTO
- `internal/plugin/admin.go`:价格、计划、Key 操作用例;
- `internal/plugin/ui.html`Management 页面调用方式。
吸收其固定路径和 handler 分派;不要继承错误响应泄露内部文本、浏览器持久化 Management Key 或 UI 直接承担业务原子性的做法。
### 12.3 `cpa-usage-keeper`
- `internal/api/router.go`:管理员/Key viewer 分离、no-store 与静态资源;
- `internal/api/usage_*`:分页、范围和统计投影;
- `internal/api/pricing*.go``quota.go`:领域 API
- `internal/api/request_limits.go``errors.go`:入口限制和脱敏错误;
- API security testssession、rate limit、secret redaction、CSP、request log token。
Keeper 是独立 HTTP 服务,Gin wildcard、middleware session 和后台任务接口不能假设在 CPA plugin route 中同样可用。
## 13. 验收标准
- 所有注册 route 都是宿主允许的精确路径且无冲突;
- Management 数据必须通过 CPA Management 鉴权;
- resource 静态页不含敏感数据,用户 JSON 独立校验 downstream Key
- 用户不能通过 ID/query 越权读取其他 Credential/账户;
- 所有金额 DTO 使用 currency + micros + display
- 用户 DTO 不出现 Token、价格规则和上游账户;
- 写接口 strict decode、revision check、审计和必要幂等全部生效;
- secret 只在首次签发/轮换响应显示,重放不创建第二把 Key;
- 超大 body 在反向代理层被拒绝,不能依赖插件收到后再拒绝;
- Management JSON entity 转义兼容测试通过,且限制被明确标记;
- 列表 cursor 在并发插入下无重复/漏页;
- panic/DB 失败返回脱敏 5xx,不泄露 SQL、路径或 secret
- Home 模式返回明确不支持,UI 不显示空数据假象;
- API、Core、UI 的字段和错误码保持同一份实现定义。