438 lines
18 KiB
Markdown
438 lines
18 KiB
Markdown
# 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/Response;CPA 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 Key,Actor 至少记录 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 tests:session、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 的字段和错误码保持同一份实现定义。
|