Skip to content

Commit b2935dd

Browse files
committed
docs: 同步 B5 账号体系(Q39 / §3.13 / 表数量 / 用户与角色 / 审计)
- PROPOSAL:新增 Q39,Q18 改三角色,§5「用户不建表」改写,§7 账号端点, §8 会话校验改「存在且未禁用 + epoch 一致」、审计范围扩到登录与账号变动 - TECHNICAL:目录树补 bootstrap / admin_users / admin_audit / activate / audit; 新增 §3.13 账号体系;表数量 8→10、SCHEMA_VERSION 13→14 - README:快速开始改「首个 admin 用 CLI 建,之后在管理台」;ADMIN_USERNAMES 与 USERS_FILE 标注仅引导期;新增「用户与角色」「审计」两节;状态补 B5 - README.en:同步 - 架构图:db 组件 tag 与要点卡片改 10 表 / 三角色账号,重生成 HTML 与截图 - 计划文档:require_admin 调用点勘误为 20(14 凭证),§8.1/§8.2 标记已获批
1 parent 8650592 commit b2935dd

11 files changed

Lines changed: 123 additions & 37 deletions

‎PROPOSAL.md‎

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
| Q15/Q26 | 健康度归一化 | 剩余积分百分比,跨 provider 可比 |
2626
| Q16 | 抽象边界 | 细接口:Provider 只管发请求 + 解析 + 分类错误 |
2727
| Q17 | 登录 | 双轨(CB 轮询 / TRAE 回调),前端统一状态机 |
28-
| Q18 | 权限 | 单 admin + 普通用户,admin 管凭证 |
28+
| Q18 | 权限 | 三角色 `admin` / `operator` / `viewer`(B5 由「单 admin + 普通用户」升级,见 Q39):admin 管用户与配置,operator 管凭证,viewer 只读 |
2929
| Q19 | 交付顺序 | 串行:骨架 → TRAE → CB 基础 → CB 完整化 |
3030
| Q21 | 模型名 | 扁平,自动路由 |
3131
| Q22 | 数据迁移 | 不迁移,全新开始 |
@@ -44,6 +44,7 @@
4444
| Q36 | 积分变动流水 | 新增 `credit_events` 表(`SCHEMA_VERSION` 11→12),在额度探测写回的**同一事务**里比对余额、只增记一条。**计划原要求 `source` 标注来源(签到/成长/对话),但实测三类证据都拿不到真实归因**:diff 只见区间净变化,其间签到、成长领取与对话消耗可能同时发生。故 `source` **改为只表达归因已知度**(`observed` / `sync`),另加 `window_start` 记变化覆盖时段,前端一律说「净变化」而非「签到 +N」。余额未变不记;任一端未知仍记但 `delta` 为空(绝不量化成 0)。保留期同 `usage_events`(90 天) |
4545
| Q37 | 池健康与多 Key 出口 | `GET /healthz` 返回 `{status, service, version, credentials:{total,ready,cooling,paused,disabled}}`(保留 `GET /health` 作纯存活探针):`ready` 复用调度器 `Candidate.is_selectable` 口径,五类互斥且合计 = total——**计划原文只列 4 类**,但项目已区分「系统禁用」与「用户暂停」(Q33),少一类会让计数对不上,故补 `paused`。`api_keys` 增 `provider_binding`(`codebuddy`/`trae`/空 = 自动)与 `allowed_ips`(`SCHEMA_VERSION` 12→13);`deps.api_key_user` 升级为返回 `ApiKeyPrincipal`,并**在鉴权当场**判定来源 IP。IP 白名单**默认不信 `X-Forwarded-For`**(客户端可写),仅 `TRUST_PROXY=true` 时采信且取 XFF **最后一个**条目,故只适用于「本服务前恰好一层受信反代」。绑定渠道在 `executor` 收窄候选上游:模型归属别家渠道时 400 并给出实际归属,目录未就绪时保守放行。**不做**每 Key 配额 / 多租户(与 Q10 冲突) |
4646
| Q38 | 后台任务可视化(「任务与配置」页) | 管理台原「运行时配置」页与后台任务**合并**:13 项配置按 `HotSetting.task` 归属进任务卡片,无归属的进「网关与调度」区;新增 `GET /api/tasks`(admin)下发 6 类任务运行态,前端 30s 刷新。**运行态只存进程内、不落库**(`tasks/status.py`):重启归零比编造重启前记录更诚实,也省掉新表 + 保留期清理 + 老库迁移,**无 schema 变更**。**no-op 轮次不入账**(返回 `None` = 未到点 / 未开启),否则签到会显示成「刚刚跑过」而当天其实没签;异常入账(`last_error`),否则「一直在失败」会显示成「尚未执行」。周期与开关取**当前生效值**,不是装配快照 |
47+
| Q39 | 用户账号体系(B5) | **用户从 `users.txt` 迁入 SQLite**(`users` + `audit_events` 两表,`SCHEMA_VERSION` 13→14):`users.txt` 降级为**一次性引导导入**(首个 admin 仍可用 `scripts/hash_password.py` 或新 `scripts/create_user.py` 建),老文件不删、可作为回滚;`ADMIN_USERNAMES` 只标**引导期**提权。三角色(S1):`admin` 管用户与配置、`operator` 管凭证写操作、`viewer` 只读。会话吊销(S3)不建会话表,用 `users.session_epoch` 进签名 Cookie 的 `ep`——改密/降级/禁用/硬删一律 bump,**角色每请求现读 DB**,不进 Cookie。删除语义(S6):**禁用是主路径**(可逆、保住用量归属),硬删只在 `scripts/create_user.py --delete --force`;管理台故意不暴露 `DELETE`。建号/重置(S7)走**一次性激活令牌** + `/activate` 自设密码,明文仅响应回显一次、库里只存 SHA-256 摘要(无邮件设施下的最优解,与「API Key 明文仅一次」同一心智模型)。审计(S8):登录 + 账号变动 + 凭证写操作入 `audit_events`,**绝不记密码/令牌明文**。防锁死三层:Web 端 self_target + last-admin 守卫,CLI 端删最后活跃 admin 拒绝,bootstrap 无活跃 admin 直接启动失败 |
4748

4849
## 2. 目标与非目标
4950

@@ -241,7 +242,7 @@ v1 只接 OpenAI 出口,但上游 SSE 解析到「中立事件」这一步独
241242

242243
## 5. 数据模型
243244

244-
- **用户不建表**:`users.txt`(PBKDF2)是唯一源,路径走 `USERS_FILE`(`config.py` 的 `users_file`,默认 `secrets/users.txt`),角色走 `ADMIN_USERNAMES` env;`api_keys.username` 由应用层校验存在性,不加外键
245+
- **用户建表**(B5,Q39):`users`(PBKDF2 密码哈希 + `role` + `enabled` + `must_change_password` + `session_epoch` + 一次性激活令牌摘要)与 `audit_events`(登录/账号变动/凭证写操作)是唯一源。`users.txt` 仅在启动时**一次性导入**(已存在的用户名不覆盖,幂等),路径仍走 `USERS_FILE`(`config.py` 的 `users_file`,默认 `secrets/users.txt`);角色改由 `users.role` 决定,`ADMIN_USERNAMES` 只剩引导期提权作用。`api_keys.username` 仍由应用层校验存在性,不加外键
245246
- **API Key 存摘要**:SHA-256,明文仅创建时返回一次
246247
- **凭证加密列**:`data_enc` 走 Fernet;调度状态(`health` / `cooling_until` / `err_count` / `pinned` / `quota_expiry_ladder`)落库,重启不丢冷却状态与到期阶梯
247248
- **用量脱敏**:`usage_events`(明细 90 天)+ `usage_hourly`(小时汇总永久),`credit`/`cached_tokens` 可空、仅辅助展示
@@ -265,7 +266,7 @@ DDL 以 [src/db/schema.sql](../src/db/schema.sql) 为准,补充实现细节见
265266

266267
外部(API Key 鉴权):`POST /v1/chat/completions`(流式 + 非流式)、`POST /v1/responses`(Responses 子集,Codex CLI;与 chat 共用同一调度 / 选号 / 统计链路)、`GET /v1/models`(扁平模型名 + `providers` 字段)、`GET /v1/user/balance`(DeepSeek 兼容余额,读探测缓存聚合,不实时打上游)、`GET /health`(纯存活)、`GET /healthz`(存活 + 凭证池计数,无鉴权)。
267268

268-
管理台(会话 Cookie):凭证管理、API Key 管理、用量统计、Playground、任务与配置(admin-only),admin 管凭证与全量统计,普通用户仅见自己的数据。凭证运维端点含 `POST /api/credentials/{id}/checkin`(签到)、`GET|POST /api/credentials/{id}/growth`(成长中心状态与手动执行,仅 CodeBuddy);运行时配置与任务运行态走 `GET|PUT /api/settings` + `GET /api/tasks`。回调(无鉴权,TRAE 浏览器 302 不带 key):`GET /authorize`。
269+
管理台(会话 Cookie):凭证管理、API Key 管理、用量统计、Playground、用户管理(admin-only)、审计日志(admin-only)、任务与配置(admin-only)。admin 管用户、配置与凭证,operator 管凭证写操作(含导入/删除),viewer 只读;用量统计按角色决定是否展示全量。账号端点:`GET|POST /api/users`、`PATCH /api/users/{username}`、`POST /api/users/{username}/{disable|enable|reset-password}`(**不提供 DELETE**,硬删走 CLI);自助改密 `POST /api/auth/password`;无鉴权的一次性激活流 `GET|POST /api/auth/activate`;审计查询 `GET /api/audit`。凭证运维端点含 `POST /api/credentials/{id}/checkin`(签到)、`GET|POST /api/credentials/{id}/growth`(成长中心状态与手动执行,仅 CodeBuddy);运行时配置与任务运行态走 `GET|PUT /api/settings` + `GET /api/tasks`。回调(无鉴权,TRAE 浏览器 302 不带 key):`GET /authorize`。
269270

270271
实现以代码为准,使用说明见 [README.md](README.md)。
271272

@@ -283,12 +284,12 @@ DDL 以 [src/db/schema.sql](../src/db/schema.sql) 为准,补充实现细节见
283284
- API Key 仅存摘要,明文只在创建时返回一次;可按 Key 限定渠道绑定与来源 IP 白名单(见 [README.md](README.md))
284285
- 凭证内容加密入库,密钥走 `APP_SECRET`:最短 16 字符,弱密钥拒绝启动;**丢失 = 已存凭证全部不可解,只能重录**,不做密钥轮换。解密失败返回可行动错误码 `credential_decrypt_failed`,不暴露裸 500
285286
- 管理台会话 Cookie `SameSite=Lax` + 写操作自定义头校验(CSRF,含 logout)
286-
- 会话与 API Key 除签名 / 摘要外**校验用户仍存在于 users.txt**:删用户即失效
287+
- 会话与 API Key 除签名 / 摘要外**校验用户仍存在、启用且会话 epoch 一致**(B5):删用户、禁用、改角色或改密码(bump epoch)都会让已签发的 Cookie 当场失效
287288
- 未匹配的 `/api`、`/v1` 路径返回 JSON 404(不落到 SPA 的 200 + HTML)
288289
- 日志脱敏:不打印 Token、完整请求体
289-
- 审计:凭证增删改、pin、账号切换写 INFO 日志(含操作人)
290+
- 审计:凭证增删改、pin、账号切换、登录与账号变动写 INFO 日志(含操作人);**绝不记密码/令牌明文**
290291

291-
不做的:mTLS;**面向管理台与端口的** IP 限制(交给反向代理)。注意与上文的 API Key 来源 IP 白名单区分——后者是应用层能力,已内建。审计只覆盖凭证管理写操作,不做全量请求审计(统计表已是脱敏的请求级记录)。
292+
不做的:mTLS;**面向管理台与端口的** IP 限制(交给反向代理)。注意与上文的 API Key 来源 IP 白名单区分——后者是应用层能力,已内建。审计覆盖登录、账号变动与凭证管理写操作,不做全量请求审计(统计表已是脱敏的请求级记录)。
292293

293294
env 完整清单见 [README.md「配置」](README.md)(以 `src/config.py` 为准)。
294295

‎README.en.md‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ upstream channels, with a shared credential pool, unified scheduling, and per-us
1717
- **Cached-token accounting**: TRAE's `cache_read_input_tokens` / `cache_creation_input_tokens` are mapped to per-request `cached_tokens` and surfaced in stats
1818
- **Three-state health + tiered cooldowns**: quota exhausted 12h, rate-limited 60s, consecutive errors 10m, dead session disabled; model-scoped limits sideline only that model
1919
- **Shared credential pool**: admins maintain credentials, everyone shares them; usage tracked per user
20+
- **Three-role accounts**: `admin` / `operator` / `viewer` stored in SQLite. New users get a one-time activation link to set their own password (no shared initial secret) and must change an admin-reset password on first login; changing a role or disabling an account revokes its sessions immediately. Logins and write operations are audited
2021
- **Credential automation**: device-code login, account switching, quota probing, daily check-in (with streak), token pre-refresh, growth-center jobs (CodeBuddy only: travel gifts, Buddy dispatch, task accept/claim, streak redemption, lottery, blind boxes; irreversible steps off via `GROWTH_IRREVERSIBLE_ACTIONS=false`)
2122
- **Per-credential pause**: "Pause" removes one credential from *chat traffic only* — probing, refresh, check-in, growth and activity tasks keep running (they honor only the system hard-disable `disabled`). Distinct from "Disabled", which means the upstream rejected the session and needs re-login + "Restore"
2223
- **Activity reporting** (CodeBuddy only, **off by default**): `ACTIVITY_REPORT_ENABLED=true` posts one chat-activity event per account per day to keep the growth-center streak alive. The upstream needs a `userId` and silently drops reports without one (HTTP 200 `{"code":0}`, streak unchanged); when the credential has no `user_id`, the gateway falls back to the bearer JWT `sub`. Upstream-internal and may break without notice — not a reliability feature (terms forbid scripted tampering: disqualification + clawback)
@@ -34,10 +35,11 @@ upstream channels, with a shared credential pool, unified scheduling, and per-us
3435

3536
```bash
3637
uv sync
37-
uv run python scripts/hash_password.py admin # prompts for a password
38+
# Create the first admin (add the rest from the "Users" page later)
39+
uv run python scripts/create_user.py admin --role admin
3840
cd web && pnpm install && pnpm build && cd ..
3941

40-
APP_SECRET="pick-a-random-string" ADMIN_USERNAMES=admin \
42+
APP_SECRET="pick-a-random-string" \
4143
uv run python -m uvicorn src.main:build_app --factory --port 8000
4244
```
4345

@@ -52,6 +54,13 @@ curl http://127.0.0.1:8000/v1/chat/completions \
5254

5355
Point any OpenAI-compatible client at `http://127.0.0.1:8000/v1`.
5456

57+
> `scripts/create_user.py` writes directly to SQLite (`--db`, or `DATA_DIR`; default
58+
> `data/coding2api.sqlite3`). On startup, an existing `secrets/users.txt` is imported
59+
> once (existing usernames are never overwritten), and `ADMIN_USERNAMES` promotes the
60+
> named users to `admin`. Both are **bootstrap-only** afterwards — day-to-day user and
61+
> role management happens on the *Users* page. See [`README.md`](README.md) (Chinese)
62+
> for the role matrix, activation flow, and the audit log.
63+
5564
### Responses API (Codex CLI)
5665

5766
`POST /v1/responses` serves a Responses subset for clients that only speak the Responses API, such as the [Codex CLI](https://github.com/openai/codex). It shares the same credential selection, cooldown, rotation, accounting, and session affinity as `/v1/chat/completions`; only the inbound mapping and outbound translation differ (see [`TECHNICAL.md` §3.7](TECHNICAL.md), in Chinese):

0 commit comments

Comments
 (0)