cloud: fill gaps (memory/KB/cost/models endpoints, WAL concurrency, OAuth scaffold, push subscriptions)

This commit is contained in:
host committed 2026-10-26 01:32:57 +08:00
1 parent b74d99e959
commit 076ba01c74
11 files changed
+2831 -28

No files matched your search

+64 -26
View File
@@ -12,27 +12,16 @@ webhook)。零第三方依赖,只用 Python 标准库。
```
客户端 / GitHub / Slack
│ HTTP + X-API-Key(webhook 靠签名,见下)
│ HTTP + X-API-Key(webhook 靠签名,OAuth 靠 state,见下)
▼
cloud/server.py(控制面,ThreadingHTTPServer)
├─ 项目 / 任务:建、查、取消、心跳、stale 回收
├─ 项目 / 任务:建、查、取消、心跳、stale 回收、任务成本
├─ 记忆 / 知识库 / 成本 / 模型:已接成 HTTP 端点(见 API 表)
├─ OAuth:cloud/oauth.py(state + 授权 URL + 回调,脚手架)
├─ 推送订阅与分发:cloud/push.py(订阅表 + 偏好过滤)
├─ 认证 / 权限 / 审计:cloud/auth.py(AuthStore)
├─ 并发:cloud/db.py(WAL + busy_timeout + 有限重试)
└─ webhook 入站:cloud/webhooks.py(验签 + 解析,只解析不执行)
│ (后续由 worker 接线)
▼
worker(尚未接成 HTTP 端点,本版需另行调用模块)
├─ 派发顺序:cloud/queue.py(order_tasks / ready_tasks /
│ budget_exceeded / TaskQueue;状态机与 host.py 对齐)
├─ 执行:cloud/sandbox.py(LocalExecutor 调 host.py CLI 做只读
│ 封装;DockerExecutor / CloudExecutor 仍是占位,未接通)
├─ 记忆:cloud/memory.py(MemoryStore:任务摘要,供 prompt 上下文)
├─ 知识库:cloud/kb.py(KBStore:按项目隔离的文档检索)
├─ 模型路由:cloud/models.py(ModelRouter / record_fallback:
│ 只决定 fallback,不跑模型回合)
├─ 成本:cloud/cost.py(CostStore / fetch_account_usage;
│ usage 来自调用方注入的 client,本模块不联网)
└─ 通知:cloud/webhooks.py(NotificationService:webhook + Slack
占位分发;Web Push / APNs / FCM 明确未接,需要证书才做)
```
状态库:单一 SQLite 文件。AuthStore 用 `users` / `project_members` /
@@ -82,10 +71,44 @@ user_id, api_key_plain = store.create_user("alice")
## API 列表
全部返回 JSON,`Content-Type: application/json`。除 `GET /health`
与 `/webhooks/*` 外,所有端点要求请求头 `X-API-Key`,无 key 或 key
错返回 `401 {"error": "unauthorized"}`。`/webhooks/*` 不走 X-API-Key,
因为外部服务无法持本站 API key,改靠各自的签名认证(见下)。
全部返回 JSON,`Content-Type: application/json`。除 `GET /health`、
`/webhooks/*`、`/oauth/*` 外,所有端点要求请求头 `X-API-Key`,无 key
或 key 错返回 `401 {"error": "unauthorized"}`。`/webhooks/*` 不走
X-API-Key,因为外部服务无法持本站 API key,改靠各自的签名认证。
`/oauth/*` 不走 X-API-Key,因为 OAuth 登录的起点就是用户还没有
本站 API key——它的认证靠随机 state(10 分钟过期、一次性)加
client_secret(只走环境变量),不是无认证后门。
### OAuth 接线步骤(脚手架,已实现,未接真实应用)
1. 注册真实 OAuth 应用(GitHub 或 Google),拿到 client_id,
回调地址填本站的 `/oauth/{provider}/callback` 公网地址。
2. `GET /oauth/{provider}/login?client_id=...&redirect_uri=...` 拿
authorization_url,把用户送到 provider 授权页。
3. provider 回调本站并带回 code + state,调
`POST /oauth/{provider}/callback`,验 state → 换 token → 映射
本站用户并记审计。client_secret 只从环境变量读
(`GITHUB_OAUTH_CLIENT_SECRET` / `GOOGLE_OAUTH_CLIENT_SECRET`),
绝不写文件、绝不进 URL、换 token 前未配好就明确报错。
4. 仍缺:真实应用注册、回调公网地址、已有本站账号的绑定流程
(目前是确定性用户名新建/复用,不做账号绑定)。
### 推送凭证清单
- webhook / slack:需要可注入 sender(worker 侧),HTTP 层没有
sender 时明确报错,不静默成功。
- webpush:需要 VAPID 密钥对(私钥绝不进仓库)。
- apns:需要 APNs 证书。
- fcm:需要 FCM key。
以上三者目前只有接口 + 缺凭证报错,没有真实发送端。
### SQLite 并发上限
server 的连接统一走 `cloud/db.py`:WAL 模式、busy_timeout 5000ms、
写锁冲突有限重试 3 次(退避 0.05s/0.1s/0.2s,重试耗尽抛原始错误,
绝不无限循环)。多线程并发写测试已覆盖。但这只是单机缓解:
写仍是单写者串行,写密集或跨机器时必须换 Postgres,这是下一阶段,
不是本轮假装解决。
| 方法与路径 | 权限 | 说明 |
| --- | --- | --- |
@@ -98,6 +121,20 @@ user_id, api_key_plain = store.create_user("alice")
| `POST /tasks/{id}/cancel` | 该项目 `create_task` | `queued` / `running` 改为 `interrupted` 并记审计 `cancel_task`;已是终态(completed / failed / interrupted / merged / merge-failed)返回 `400`;不存在 `404` |
| `POST /tasks/{id}/heartbeat` | 该项目 `create_task` | 更新 `last_heartbeat = now`;状态为 `queued` 则改为 `running`;返回任务 |
| `POST /admin/sweep` | 认证即可 | 调 `sweep_stale_tasks(db_path, threshold_seconds=300)`(body 可传 `threshold_seconds` 覆盖默认值);把 `running` 且 `last_heartbeat`(无心跳则用 `updated_at`)早于 `now - threshold` 的任务改为 `interrupted` 并记审计 `sweep_interrupted`;返回 `{"swept": [ids]}` |
| `POST /tasks/{id}/cost` | 该任务所属项目 `read` | 成本聚合:`{task_id, project_id, total_tokens, usd_micros, turns, fallback_count, actual_tokens, estimated_tokens}`;真数与估算分开;不存在 `404`,越权 `403` |
| `POST /projects/{id}/memory` | 该项目 `create_task` | 写线程摘要 body `{thread_id?, goal, conclusion, files_changed?, open_issues?}`,返回 `201 {id}`,并记审计 `create_memory` |
| `GET /projects/{id}/memory?top_k=5` | 该项目 `read` | 最近 active 摘要(默认 top_k=5)+ `prompt_context`(只含 top_k,不全量注入);不存在 `404`,越权 `403` |
| `POST /projects/{id}/memory/{mid}/supersede` | 该项目 `create_task` | 把摘要标废弃(deprecated,不是物理删除),并记审计 `supersede_memory`;摘要不属于该项目则 `404` |
| `POST /projects/{id}/kb/index` | 该项目 `create_task` | 索引文档 body `{path, content, kind?}`,同项目同 path 重复索引会替换旧文档,返回 `201 {id}` |
| `GET /projects/{id}/kb/search?q=&top_k=5` | 该项目 `read` | FTS + LIKE 回退,仍按项目过滤,跨项目文档绝不返回;`q` 缺失 `400` |
| `GET /projects/{id}/cost` | 该项目 `read` | 项目成本总览 + 按天:`?day=YYYY-MM-DD` 查单天,`?days=N` 查最近 N 天;无参数只给总览 |
| `GET /models` | 认证即可 | 优先级列表(环境变量 `MODELS_PRIORITY` 逗号分隔,未设用默认)+ 只含调用者成员项目的模型统计与 fallback 事件(`fallback_count > 0` 的任务);不返回 prompt 内容 |
| `GET /oauth/{provider}/login?client_id=&redirect_uri=&scope=` | 无需 X-API-Key(pre-auth) | 返回 `{provider, state, authorization_url}`,只返回 URL 不跳转;provider 仅 github/google,未知 `400`;state 已入库,10 分钟过期 |
| `POST /oauth/{provider}/callback` | 无需 X-API-Key(pre-auth) | body `{code, state, client_id, redirect_uri}`;验 state → 换 token → 映射到本站用户(复用 users 表,用户名 `oauth:<provider>:<login>`)并记审计 `oauth_login`;state 无效/过期、换 token 失败、无 secret 一律 `400` 且不建会话;成功不返回 API key(发放方式由调用方另行决定) |
| `POST /projects/{id}/push/subscriptions` | 该项目 `create_task` | 记订阅 body `{channel, target, preference?}`,channel 为 webhook/slack/webpush/apns/fcm,preference 为 instant/digest/silent,返回 `201` |
| `GET /projects/{id}/push/subscriptions` | 该项目 `read` | 只列调用者自己的订阅(不泄露他人 target) |
| `DELETE /projects/{id}/push/subscriptions/{sid}` | 该项目 `create_task` | 只能删自己的订阅,项目 owner 可删任意;不存在 `404`,他人订阅 `403` |
| `POST /admin/push/dispatch` | 该项目 `read` | body `{event_type, project_id, payload?}`;只发状态变化事件(completed/failed/goal-unmet/merge-failed/review-requested);HTTP 层无注入 sender:webhook/slack 报“无 sender”,webpush/apns/fcm 报缺凭证,都不静默成功;真实发送端由 worker 注入 sender 后调 `push.dispatch_event` |
| `POST /webhooks/github` | GitHub 签名 | 见“webhook 端点” |
| `POST /webhooks/slack` | Slack 签名 | 见“webhook 端点” |
@@ -184,8 +221,9 @@ interrupted 且终态任务 cancel 返回 400、心跳后 sweep 不扫刚心跳
Secure Vault 注入、或其它经用户明确同意的渠道),而不是把 key
写进配置文件、数据库或代码里凑合。定下之前,定时自动化与 worker
的模型回合都只能停在“失败路径可用、模型跑不起来”的状态。
- **其它待接线(非外部条件,但同样未完成)**:memory / KB / 成本 /
模型路由尚未接成 HTTP 端点;OAuth(GitHub/Google)未接,首版只有
API key + 哈希认证;Web Push / APNs / FCM 推送未接,需要证书才做;
单机 SQLite 在高并发写下仍有 busy 锁上限,生产量上来后需评估
换库或单写者队列。
- **其它待接线(非外部条件,但同样未完成)**:OAuth 已接成脚手架
端点(state + 授权 URL + 回调),但真实应用注册、回调公网地址、
已有账号绑定仍缺;推送已接成订阅 + 分发端点,但 Web Push /
APNs / FCM 的真实凭证与发送端仍缺;单机 SQLite 在高并发写下
仍有上限,生产量上来后需换 Postgres;worker 执行、云端执行、
真实通知与真实模型回合均未接通。