From 4566ce8d580ec55d7dac87ce830bf9e8a4834147 Mon Sep 17 00:00:00 2001 From: skycurtain Date: Sat, 3 Oct 2026 19:35:02 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E6=8A=80=E6=9C=AF=E9=80=89=E5=9E=8B?= =?UTF-8?q?=E4=B8=8E=20API=20=E5=A5=91=E7=BA=A6=E5=AE=9A=E7=A8=BF=E2=80=94?= =?UTF-8?q?=E2=80=94=E5=9B=9B=E7=AF=87=E7=BB=8F=E4=BD=9C=E8=80=85=E9=80=90?= =?UTF-8?q?=E7=AF=87=E5=AE=A1=E9=98=85=E5=AE=9A=E7=A8=BF=EF=BC=8C=E4=BB=93?= =?UTF-8?q?=E5=BA=93=E8=BD=AC=E5=85=A5=E5=AE=9E=E7=8E=B0=E4=B8=BB=E7=BA=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 4 +- README.md | 8 +- docs/design/API契约-公共约定.md | 91 +++++++++++ docs/design/API契约-成员视图.md | 91 +++++++++++ docs/design/API契约-管理面.md | 268 ++++++++++++++++++++++++++++++++ docs/design/技术选型.md | 102 ++++++++++++ 6 files changed, 560 insertions(+), 4 deletions(-) create mode 100644 docs/design/API契约-公共约定.md create mode 100644 docs/design/API契约-成员视图.md create mode 100644 docs/design/API契约-管理面.md create mode 100644 docs/design/技术选型.md diff --git a/AGENTS.md b/AGENTS.md index 0ba58c2..2221d4c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -12,11 +12,11 @@ ## 项目速览(速记) -- **`design`(本分支)= 新版设计文档中心,无代码** +- **`design`(本分支)= 设计文档中心 + 实现主线**:实现代码与文档同分支演进(代码落点已定案),monorepo 结构(`apps/server` · `apps/web` · `packages/core` · `packages/db`)见 `docs/design/技术选型.md` §4 - 数据模型:四张基线表(`api_keys` · `usage_logs` · `payments` · `qpu_history`)+ 三张访问主体表(`members` · `users` · `sessions`)设计见本目录,**七篇已定稿(2026-10-01,经作者逐篇审阅)**;同步架构(轮询 + `request_id` 去重 + 攒库)的动因由上游物理约束决定,架构结论不变 - 需求总集与待裁决项(平台身份体系、费用分摊与账单、充值折算、多订阅、数据资产保护)见 `docs/design/需求清单.md`;交付形态已裁决(双形态并行,完整形态先行) - 上游约束速记:只有 sk-key、无时间过滤接口 → 轮询攒库是唯一路径(README「架构的根本动因」) ## 命令 -本分支为纯文档分支,无构建 / 测试命令。 +实现骨架尚未搭建,暂无构建 / 测试命令;骨架落地后在此登记(Bun 工具链,选型见 `docs/design/技术选型.md`)。 diff --git a/README.md b/README.md index c9365d1..b8fe91e 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,8 @@ | 项目 | 内容 | |------|------| -| 当前状态 | **新版设计阶段**(`design` 分支,文档中心)——详细设计推进中 | -| 技术栈 | 服务器形态栈待架构设计评估(双形态交付已裁决,见 [`docs/design/需求清单.md`](docs/design/需求清单.md) R13/R14) | +| 当前状态 | **实现启动阶段**(`design` 分支 = 文档中心 + 实现主线)——需求 / 数据模型 / 交互 / 技术选型 / API 契约全部定稿,余同步引擎设计(与实现并行),骨架 + 在线文档页动工 | +| 技术栈 | 全栈 TypeScript(选型定稿见 [`docs/design/技术选型.md`](docs/design/技术选型.md)):母体 Bun + Hono + Drizzle + PostgreSQL · 前端 Vue 3 + antdv-next | ## 项目沿革 @@ -23,6 +23,7 @@ | 文档 | 什么时候读 | 状态 | |------|-----------|------| | [`docs/design/需求清单.md`](docs/design/需求清单.md) | **需求权威索引与裁决档案**——R1–R15 全部定案(含裁撤与延后),R 编号体系为后续设计文档的引用锚点 | 定稿(2026-10-01) | +| [`docs/design/技术选型.md`](docs/design/技术选型.md) | 母体技术栈裁决(CFW 延后至功能全集实现后,仅零成本 R14 预留):全栈 TS;Bun + Hono + Drizzle/pg;前端 Vue 3 + antdv-next;分层落地与回退;antdv-next 核验通过(§7.1) | 定稿(2026-10-02) | | [`docs/design/api_keys表字段设计.md`](docs/design/api_keys表字段设计.md) | 数据模型第一张表:key 登记、生命周期与成员归属 | 定稿(2026-10-01) | | [`docs/design/usage_logs表字段设计.md`](docs/design/usage_logs表字段设计.md) | 数据模型第二张表:日志明细、来源提列、去重与写入策略 | 同上 | | [`docs/design/payments表字段设计.md`](docs/design/payments表字段设计.md) | 数据模型第三张表:支付记录(订阅/充值同构,币种无关,只存事实) | 同上 | @@ -38,6 +39,9 @@ | [`docs/design/交互设计-成员管理.md`](docs/design/交互设计-成员管理.md) | 开户、令牌签发/重发(明文仅一次)、停用 | 同上 | | [`docs/design/交互设计-审计日志.md`](docs/design/交互设计-审计日志.md) | 管理操作留痕时间线 | 同上 | | [`docs/design/交互设计-成员视图.md`](docs/design/交互设计-成员视图.md) | 成员单页世界——R12 携带形态定案(徽章 + 展开行) | 同上 | +| [`docs/design/API契约-公共约定.md`](docs/design/API契约-公共约定.md) | API 契约总纲:路由隔离与鉴权两档、错误信封、时间 / 数值 / 分页 / 命名(8 条拟定约定经审通过)——后两篇的引用锚点 | 定稿(2026-10-02) | +| [`docs/design/API契约-管理面.md`](docs/design/API契约-管理面.md) | 管理面 16 端点:引导与会话三态、引擎状态、总览聚合、key 登记四态、下钻明细、支付、成员(令牌明文仅一次)、审计;5 项裁定(经审通过)+ 4 项遗留(key 详情已裁决:实现期定) | 定稿(2026-10-02) | +| [`docs/design/API契约-成员视图.md`](docs/design/API契约-成员视图.md) | 成员视图 3 端点:令牌验证与页头、单页聚合(含支付参数卡判定)、日志明细(DTO 与管理面同构);4 项裁定(经审通过)+ 1 项遗留 | 定稿(2026-10-02) | | [`docs/references/NewAPI-接口范围.md`](docs/references/NewAPI-接口范围.md) | 计费平台在 NewAPI 中可用的接口参考:鉴权档位、请求与返回结构、字段语义、实测行为、源码出处(**唯一上游参考**) | 事实依据 | - 命名与分区:**目录用英文、文档名用中文**;`docs/design/`(我们定的方案与决策)· `docs/references/`(平台可用接口参考——实证事实,不做设计决策) diff --git a/docs/design/API契约-公共约定.md b/docs/design/API契约-公共约定.md new file mode 100644 index 0000000..cbf7d13 --- /dev/null +++ b/docs/design/API契约-公共约定.md @@ -0,0 +1,91 @@ +# API契约-公共约定 + +> 两个世界(管理面 / 成员视图)共用的 HTTP 层约定:路由隔离、鉴权两档、错误信封、时间 / 数值 / 分页 / DTO 命名。本篇是《API契约-管理面》《API契约-成员视图》的引用锚点——跨面共性在此定死,端点逐个定义归后两篇。 + +| 项目 | 内容 | +|------|------| +| 状态 | 定稿(2026-10-02) | +| 关联 | 《交互设计-总览与共性》(§1 访问拓扑 · §3 呈现约定)·《交互设计-登录与会话》(鉴权档位与 401 分流)· 七篇表字段设计(DTO 字段权威)·《技术选型》§4(DTO 类型属 `packages/core`) | +| 产出 | 路由前缀与隔离机制 · 鉴权两档 · 错误信封 · 时间 / 数值 / 分页 / 命名约定 | + +--- + +## 1. 总则 + +- JSON over HTTP,UTF-8;请求与响应体均为 `application/json` +- 一套路由代码,两个世界靠**路径前缀与凭证中间件物理隔离**(共性 §1:令牌进不了管理面、会话进不了成员视图) +- DTO 类型定义于 `packages/core`,web 以纯类型依赖引用——前后端一份契约类型(技术选型 §4 依赖规则) +- **端点面向交互设计,不与数据表对齐**——每个端点对应一个功能面(页面 / 流程需要什么);表只是存储模型,按其事实性质获得恰好够用的操作面(引擎专写表纯只读、人工抄录表全 CRUD、账本实体软删除) + +## 2. 路由前缀与隔离机制 + +| 面 | 路由前缀 | 凭证 | 校验中间件 | +|----|---------|------|-----------| +| 管理面 | `/api/admin/*` | 会话 cookie | sessions 校验 | +| 成员视图 | `/api/member/*` | Bearer 令牌 | members 哈希比对 + `status` 检查 | + +- **会话 cookie**:`HttpOnly + Secure + SameSite=Lax`,`Path=/api/admin`(「路径限管理面」裁决的落地);随请求自动携带、登出即删行 +- **成员令牌**:`Authorization: Bearer `;明文令牌只存在于登录壳输入与 sessionStorage(关标签即失效——《交互设计-登录与会话》§4) +- 401 分流为前端职责:管理面任意 401 → 登录页「会话已过期」;成员视图任意 401 → 登录壳「令牌无效或已停用」——后端 401 响应不带面特化信息,前端按所在面分流(共性 §1) +- **命名空间约定:`/api/*` 专属接口**——前端页面路由永不进入 `/api` 前缀;未知 `/api/*` 路径 = 接口 `404` 错误信封,非 `/api` 请求归前端托管(SPA history 回退「非 `/api` → index.html」,细则归骨架与部署设计) + +## 3. 错误信封 + +统一错误体(成功响应无信封包装,见 §6): + +```json +{ "error": { "code": "invalid_credentials", "message": "用户名或密码错误" } } +``` + +- `message`:面向使用者的中文、说人话(共性 §3.6 文案基调);不携带堆栈与内部细节 +- `code`:机器可读枚举(snake_case);表单校验失败可加 `fields`(字段名 → 中文说明);判别已足时允许端点附机器可读附注字段(如重复登记的 `409` 附 `key_id` 定位既有行) + +| HTTP 状态 | 语义 | 本项目对应 | +|-----------|------|-----------| +| 200 / 201 | 成功 / 创建成功 | 创建类写操作返 201 | +| 400 | 请求格式错误 | 参数缺漏、JSON 解析失败 | +| 401 | 凭证无效或过期 | 会话过期、令牌停用 / 重发 | +| 404 | 资源不存在 | 非法 id | +| 409 | 冲突 | 重复登记(sk-key 已存在等) | +| 422 | 校验失败 | 表单字段校验(带 `fields`) | +| 429 | 请求过频 | 登录节流(管理面,最小实现) | +| 5xx | 服务端故障 | `message` 统一「服务暂时不可用,请稍后重试」,不泄内部细节 | + +- **HTTP 状态码是成功/失败的唯一判别**——客户端永远先看状态码、再解析响应体,不从响应体形状推断成败;错误信封的解析集中在前端唯一的统一请求封装内,各端点代码只碰成功类型;中间人返回的非 JSON 错误体(代理 502 页等)回退为通用文案 +- **业务错误即请求失败**——状态码表即业务错误分类表(409 重复登记、422 规则校验失败、404 资源不存在等),一律非 2xx + 错误信封;**不存在「请求成功但携带业务错误」的响应形态**。系统状态类数据例外:响应携带的「坏事」是事实数据而非请求错误——如总览健康带的「最近轮次失败 2」走 200,失败的轮次是数据、不是本次请求的错误 + +## 4. 时间口径 + +- **传输格式:ISO 8601 UTC 字符串**(如 `2026-10-02T14:22:05Z`)——自描述;存储层维持 UTC Unix 秒(表设计约定),DTO 层转换;北京时间展示由前端完成(共性 §3.3) +- **时间范围参数**:`from` / `to`,秒级 ISO 8601 UTC,**左闭右开 `[from, to)`**——快捷项边界无歧义、无跨桶重复计数 +- 快捷项(最近 7 / 30 天、本订阅期间、自定义起止)是前端预置逻辑:前端算出 `from/to` 再请求,后端无快捷参数;「本订阅期间」的期间数据来源归后两篇各自定义 + +## 5. 数值口径(对齐共性 §3.2) + +- **quota 双单位**:用量一律成对返回 `quota`(整数原值,口径内精确显示)+ `quota_usd`(**字符串,两位小数**,如 `"25.00"`) +- **换算责任在后端**:按日志时刻取 QPU(`qpu_history` 时间线)逐行换算——单一事实源,前端只呈现不换算;占比 / 分段条比例由前端现算(纯呈现职责) +- **取整规则**:换算内部以微分(1/100 分)整数运算,仅输出时四舍五入到分;聚合桶先微分求和再一次取整——小行贡献不丢失、单一舍入点 +- **支付金额**:`paid_amount` 原样数字(币种无关,R9 裁决),无符号、无换算、无派生金额 +- **次数**:`count`(日志行数) + +## 6. 分页与响应形态 + +- **列表类端点**(日志明细、审计日志等):`page`(1 起)+ `page_size`(默认 50,上限 200)→ `{ "items": [...], "total": 123 }` +- **聚合类端点**(按成员 / key / 来源分组):不分页——分组基数有限(成员个位数、key 10–25),返回全量数组 +- **成功响应无信封**:对象直返、列表 `items/total`、创建返创建后实体——信封只用于错误(§3);空响应体的动作类成功(如登出)返 `204 No Content` 无响应体 + +## 7. DTO 命名 + +- **snake_case、与表字段同名同构**——七篇表字段设计即 DTO 字段权威,无映射层 +- 前端不做 camelCase 转换:全栈 snake_case(组件 props 是 antd 的 API、与数据 DTO 互不干扰) + +## 8. 本稿拟定约定(交互设计未覆盖,请作者重点审) + +1. 时间传输格式 ISO 8601 UTC 字符串(§4); +2. 时间范围左闭右开 `[from, to)`(§4); +3. `quota_usd` 字符串两位小数 + 后端换算责任 + 微分取整规则(§5); +4. DTO 与表字段 snake_case 同构、无映射层(§7); +5. 分页 `page/page_size` + `items/total`,聚合类不分页(§6); +6. 错误信封单层 `{error: {code, message, fields?}}`,成功无信封(§3/§6); +7. 空响应体动作类成功返 `204`(§6);HTTP 状态码为成功/失败的唯一判别(§3); +8. 业务错误一律非 2xx——不存在 200 携业务错误的形态;系统状态类数据(健康带轮次失败数等)例外,是响应数据、不是请求错误(§3)。 diff --git a/docs/design/API契约-成员视图.md b/docs/design/API契约-成员视图.md new file mode 100644 index 0000000..c0437ef --- /dev/null +++ b/docs/design/API契约-成员视图.md @@ -0,0 +1,91 @@ +# API契约-成员视图 + +> 成员视图(令牌档)全部端点:令牌验证与页头、页面聚合、日志明细。《API契约-公共约定》(定稿)全部条款在此适用不再重述——前缀 `/api/member`、Bearer 鉴权、ISO 8601 UTC 左闭右开、`quota_usd` 后端换算、snake_case 同构;管理面草案 §10 的裁定 1(聚合仅 `type = 2`)与裁定 2(`token_name` 取当前名)在此沿用。成员世界**无**审计、无健康带、无改密(定位边界,《交互设计-成员视图》§3)。 + +| 项目 | 内容 | +|------|------| +| 状态 | 定稿(2026-10-02) | +| 关联 | 《API契约-公共约定》(定稿——总纲)·《API契约-管理面》(草案——裁定沿用)·《交互设计-成员视图》·《交互设计-登录与会话》§4(登录壳)· `members` / `usage_logs` / `payments` 表字段设计(DTO 字段权威) | +| 产出 | 端点清单(3 个)、请求 / 响应字段、裁定项(§5)、遗留项(§6) | + +--- + +## 1. 端点总览 + +| # | 端点 | 方法 | 用途 | +|---|------|------|------| +| 1 | `/api/member/me` | GET | 令牌验证 + 页头(登录壳提交的落点) | +| 2 | `/api/member/overview` | GET | 单页聚合:总用量、按来源分解、我的 key、支付参数卡 | +| 3 | `/api/member/logs` | GET | 日志明细(名下 key,分页) | + +## 2. 令牌验证与页头 + +### GET `/api/member/me` → 200 + +`{ "id": 1, "name": "小张" }` + +- 登录壳提交 = 前端将令牌存 sessionStorage 后调本端点(`Authorization: Bearer`):验证通过(哈希命中 + `status = active`)→ 进入成员视图;`401` → 登录壳(前端固定文案「令牌无效或已停用」,不区分停用与无效——公共约定 §2) +- **不建会话**:令牌即长期凭证(`members` 表设计 3.2——无会话语义、无登出端点);旧令牌在作者重发后于下一请求自然 `401` + +## 3. 单页聚合 + +### GET `/api/member/overview?from&to` → 200 + +`from/to` 必填(快捷项为前端预置);聚合仅名下 key(经 `api_key_id → api_keys.member_id` 当前归属联查)的 `type = 2` 日志。 + +```json +{ + "member": { "id": 1, "name": "小张" }, + "usage": { "count": 300, "quota": 1500000, "quota_usd": "3.00" }, + "by_source": [ + { "billing_source": "subscription", "count": 280, "quota": 1400000, "quota_usd": "2.80" }, + { "billing_source": "wallet", "count": 20, "quota": 100000, "quota_usd": "0.20" } + ], + "keys": [ + { "key_id": 5, "token_name": "小张-01", "log_synced_at": "2026-10-02T08:00:00Z", + "usage": { "count": 300, "quota": 1500000, "quota_usd": "3.00" } } + ], + "payments": [ + { "id": 3, "kind": "subscription", "paid_at": "2026-09-01T00:00:00Z", + "paid_amount": 170, "remark": "九月拼车", + "period_start": "2026-09-01T00:00:00Z", "period_end": "2026-10-01T00:00:00Z", + "subscription_id": 3, "quota_granted": 12500000, "quota_usd": "25.00" } + ] +} +``` + +- `usage`:该时段总用量(聚合卡);`by_source`:订阅 / 充值分解(分段条);`keys`:我的 key 窄卡(`log_synced_at` 支撑停滞标注) +- `payments`:**支付参数卡 + 徽章展开行 + 「本订阅期间」快捷项的数据源**,判定规则见 §5 裁定 2;对成员**全字段可见**(含 `remark`——完全公开透明裁决);`payments` 全空 = 纯用量模式中性空态(前端事) +- 徽章展开行仅限「订阅 #N」徽章:「充值」不展开(余额池扣款挂不到具体某笔,展开无答案——§5 裁定 4)、「未关联」(日志缺 `subscription_id`)不可展开;降级:徽章 `subscription_id` 不在 `payments` 列表 → 「该订阅无登记信息」——空值场景各自归位(《交互设计-成员视图》§1.6) + +## 4. 日志明细 + +### GET `/api/member/logs?from&to` → 200 `{items, total}` + +分页(公共约定 §6);过滤 `from/to`(必填);范围恒为名下 key、恒 `type = 2`(§5 裁定 1——成员世界是「我的用量」视角,错误行排查属管理面)。 + +明细行 DTO 与管理面 `GET /api/admin/logs` **同构**(公共约定 §7——一份 DTO 类型两处复用): + +```json +{ + "id": 12345, "api_key_id": 5, "created_at": "2026-10-01T14:22:05Z", + "token_name": "小张-01", "model_name": "gpt-5", "type": 2, "content": "", + "quota": 250000, "quota_usd": "0.50", + "prompt_tokens": 1200, "completion_tokens": 800, + "use_time": 3, "is_stream": true, + "billing_source": "subscription", "subscription_id": 3 +} +``` + +`token_name` 取当前名(管理面裁定 2 沿用)。 + +## 5. 本稿裁定项(请作者重点审) + +1. **成员视图明细恒 `type = 2`**——对齐聚合口径;无 `type` 筛选参数(管理面明细的全类型排查能力不进成员世界); +2. **「该时段涉及」支付记录的判定**:订阅 = 该时段日志(`type = 2`)出现过的 `subscription_id` 对应记录 **∪ 进行中的订阅**(`period_end` 为空或未来——支撑「本订阅期间」快捷项与「该订阅无登记信息」降级);充值 = `paid_at ∈ [from, to)`(充值消耗无逐笔关联可查,按支付时间锚定); +3. **明细行 DTO 与管理面同构**:同一 `LogItem` 类型,成员端经归属与 `type` 恒定过滤; +4. **「充值」徽章不展开**——余额池扣款与具体充值记录无逐笔关联,展开回答不了「这笔花在哪」(《交互设计-成员视图》§1.6 未定义处的落定:徽章展开行仅限订阅徽章,「充值」为纯标识)。 + +## 6. 遗留项 + +1. **订阅编号同一性核查**(日志 `subscription_id` ↔ 支付记录是否同一体系)——需求清单既有标注,实现期真实数据核查后再终验(徽章文案预留「未关联」正常位)。 diff --git a/docs/design/API契约-管理面.md b/docs/design/API契约-管理面.md new file mode 100644 index 0000000..a5a2f18 --- /dev/null +++ b/docs/design/API契约-管理面.md @@ -0,0 +1,268 @@ +# API契约-管理面 + +> 管理面(作者,会话档)全部端点:引导与会话、引擎状态、全员总览、key 管理、日志明细(下钻)、支付记录、成员管理、审计日志。《API契约-公共约定》(定稿)全部条款在此适用不再重述——前缀 `/api/admin`、会话 cookie 鉴权、错误信封、ISO 8601 UTC 左闭右开、`quota_usd` 后端换算、snake_case 同构。 + +| 项目 | 内容 | +|------|------| +| 状态 | 定稿(2026-10-02) | +| 关联 | 《API契约-公共约定》(定稿——总纲)· 八篇交互设计(定稿)· `api_keys` / `usage_logs` / `payments` / `qpu_history` / `members` 表字段设计(DTO 字段权威)· `需求清单.md` §9(访问主体模型) | +| 产出 | 端点清单、请求 / 响应字段、错误码、本稿裁定项(§10)与遗留项(§11) | + +--- + +## 1. 端点总览 + +| # | 端点 | 方法 | 用途 | +|---|------|------|------| +| 1 | `/api/admin/setup-state` | GET | 入口三态判定(登录页 / 引导页分流) | +| 2 | `/api/admin/setup` | POST | 引导页落库(一次性) | +| 3 | `/api/admin/session` | POST / GET / DELETE | 登录 / 当前会话 / 登出 | +| 4 | `/api/admin/password` | PUT | 改密(删其他会话) | +| 5 | `/api/admin/engine` | GET | 引擎状态(最近轮次摘要) | +| 6 | `/api/admin/overview` | GET | 全员总览(健康带数据 + 三维聚合) | +| 7 | `/api/admin/keys` | GET / POST | key 清单 / 登记 | +| 8 | `/api/admin/keys/:id` | PATCH | 启停 / 归属变更 | +| 9 | `/api/admin/logs` | GET | 日志明细(下钻,分页) | +| 10 | `/api/admin/payments` | GET / POST | 支付记录清单 / 登记 | +| 11 | `/api/admin/payments/:id` | PUT / DELETE | 编辑 / 删除 | +| 12 | `/api/admin/qpu/latest` | GET | 当前换算率(表单美元双输入用) | +| 13 | `/api/admin/members` | GET / POST | 成员清单 / 开户(令牌明文仅一次) | +| 14 | `/api/admin/members/:id` | PATCH | 停用 / 启用 | +| 15 | `/api/admin/members/:id/token` | POST | 令牌重发(明文仅一次) | +| 16 | `/api/admin/audit-logs` | GET | 审计日志(分页) | + +## 2. 引导与会话 + +### GET `/api/admin/setup-state` → 200 + +`{ "initialized": true }`——入口三态判定端点(幂等): + +- `users` 非空 → `true`(登录页);空库 + env 凭证在 → **静默落库**后 `true`;空库 + 无凭证 → `false`(引导页) +- 「永不复现」保证:`initialized` 恒以库内事实为准 + +### POST `/api/admin/setup` → 200 `{username}` + Set-Cookie + +引导页提交 `{username, password}`(确认密码为前端校验)——落库 + 直接建立会话(免二次登录)。仅空库 + 无 env 凭证时有效,否则 `409 already_initialized`(与三态判定构成双保险)。 + +### POST `/api/admin/session` → 200 `{username}` + Set-Cookie + +登录 `{username, password}`。失败 `401 invalid_credentials`(通用文案,不区分用户名 / 密码错);连续失败触发 `429`(节流最小实现——失败延迟 / 计数,参数实现期定)。cookie 名 `admin_session`,形态见公共约定 §2。 + +### GET `/api/admin/session` → 200 `{username}` + +当前会话(刷新后恢复侧边栏用户名);无有效会话 → `401`。 + +### DELETE `/api/admin/session` → 204 + +登出(删会话行)。 + +### PUT `/api/admin/password` → 204 + +`{old_password, new_password}`——成功后**删除其他会话、保留当前**。旧密码不正确 → `422`(`fields: {old_password: "旧密码不正确"}`)。 + +## 3. 引擎状态 + +### GET `/api/admin/engine` → 200 + +```json +{ + "running": true, + "last_round": { + "started_at": "2026-10-02T08:00:00Z", + "keys_total": 18, "succeeded": 16, "failed": 2, + "failed_api_key_ids": [3, 7] + } +} +``` + +`failed_api_key_ids` 支撑 Key 管理「最近轮次失败」筛选与健康带失败数跳转;`last_round` 为 `null`(引擎尚未跑过)合法。**状态存储与字段细节归同步引擎设计(待立稿),两稿以本端点字段为衔接对齐点。** + +## 4. 全员总览 + +### GET `/api/admin/overview?from&to` → 200 + +`from/to` **必填**(杜绝无口径数字;快捷项为前端预置)。聚合仅 `type = 2`(消费行,§10 裁定 1)。 + +```json +{ + "data_cutoff": "2026-10-02T08:00:00Z", + "unassigned_subscription": { "count": 0, "quota": 0, "quota_usd": "0.00" }, + "by_source": [ + { "billing_source": "subscription", "count": 420, "quota": 2100000, "quota_usd": "4.20" }, + { "billing_source": "wallet", "count": 80, "quota": 400000, "quota_usd": "0.80" } + ], + "by_member": [ + { "member_id": 1, "member_name": "小张", "key_count": 3, + "data_cutoff": "2026-10-02T08:00:00Z", + "usage": { "count": 300, "quota": 1500000, "quota_usd": "3.00" } }, + { "member_id": null, "member_name": null, "key_count": 2, + "data_cutoff": "2026-10-01T06:00:00Z", + "usage": { "count": 200, "quota": 1000000, "quota_usd": "2.00" } } + ], + "by_key": [ + { "key_id": 5, "token_name": "小张-01", "member_id": 1, "status": "active", + "data_cutoff": "2026-10-02T08:00:00Z", + "usage": { "count": 120, "quota": 600000, "quota_usd": "1.20" } } + ] +} +``` + +- `data_cutoff`:全池 `log_synced_at` 最慢;`by_member` 行为名下最慢、`member_id: null` 行为「未归属(归作者)」固定卡 +- `unassigned_subscription`:该时段 `billing_source = subscription` 且 `subscription_id` 缺失的量——正常恒为零(健康指示器,非零即异常信号);`billing_source = wallet` 的缺失属正常、不进此分组 +- 占全员比例条由前端现算(公共约定 §5) + +## 5. Key 管理 + +### GET `/api/admin/keys` → 200 `[key]` + +全量直返(基数 10–25,不分页——§10 裁定 3)。筛选参数:`member_id`(id 或 `unassigned`)、`status`、`recent_failed=true`(引擎最近轮次失败)。排序默认数据截止降序(前端事)。 + +```json +{ + "id": 5, "token_name": "小张-01", "status": "active", + "member_id": 1, "member_name": "小张", + "registered_at": "2026-09-20T10:00:00Z", + "log_synced_at": "2026-10-02T08:00:00Z", "usage_synced_at": "2026-10-02T08:00:00Z", + "total_used": 600000, "total_available": 940000, "total_granted": 1540000, + "unlimited_quota": false, "expires_at": null +} +``` + +`key_hash` / `key_ciphertext` 永不出 API(凭证形态);`model_limits` / `model_limits_enabled` 已入库暂无 UI 消费方,不出(随表设计 3.4 待 UI 稳定回审)。 + +### POST `/api/admin/keys` → 201 `key` + +`{ "sk_key": "sk-…", "member_id": null }`——登记四态: + +| 结果 | 响应 | +|------|------| +| 成功 | `201` + key 实体(首轮日志同步异步补,不阻塞响应——表设计 §4) | +| 已登记(hash 命中,查库优先、先于上游验证) | `409 key_already_registered`,信封附注 `key_id` 定位既有行 | +| 上游拒绝(401) | `422`(`fields: {sk_key: "上游拒绝了这枚 key——可能不存在或已被禁用"}`) | +| 上游暂不可达 | `502 upstream_unreachable`(「上游站点暂时不可达,请稍后重试」) | + +### PATCH `/api/admin/keys/:id` → 200 `key` + +`{ "status": "disabled" }`(启用 / 停用)或 `{ "member_id": 3 }`(归属变更;`null` = 归还未归属)——部分更新,单字段或并存均可,审计按实际变更字段各落一行(§9 枚举)。非法值 `422`;key 不存在 `404`;`member_id` 引用不存在成员 `422`(`fields`)。 + +## 6. 日志明细(管理面下钻) + +### GET `/api/admin/logs?from&to` → 200 `{items, total}` + +分页(`page/page_size`,公共约定 §6)。过滤:`from/to`(必填)、`api_key_id`、`member_id`(经 `api_key_id → api_keys.member_id` 联查**当前归属**——行级归属语义)、`billing_source`、`subscription_id`、`type`。默认全类型(错误行排查入口——§10 裁定 1 的另一半)。 + +```json +{ + "id": 12345, "api_key_id": 5, "created_at": "2026-10-01T14:22:05Z", + "token_name": "小张-01", "model_name": "gpt-5", "type": 2, "content": "", + "quota": 250000, "quota_usd": "0.50", + "prompt_tokens": 1200, "completion_tokens": 800, + "use_time": 3, "is_stream": true, + "billing_source": "subscription", "subscription_id": 3 +} +``` + +`token_name` 取**当前名**(联查 `api_keys`,与「展示名以当前为准」对齐——§10 裁定 2);`username` / `channel` / `ip` / `token_id` / `request_id` / `other` 等其余表字段暂不出 API,界面需要时再扩。 + +> **界面落点待补**:总览「行点击 → Key 管理页该 key 详情」——交互设计无成篇的「key 详情」视图,本端点先行、界面归交互补篇或实现期定(§11 遗留 1)。 + +## 7. 支付记录 + +### GET `/api/admin/payments` → 200 `[payment]` + +全量直返(年十位数,不分页),时间倒序(后端排序)。 + +```json +{ + "id": 3, "kind": "subscription", "paid_at": "2026-09-01T00:00:00Z", + "paid_amount": 170, "remark": "九月拼车", + "period_start": "2026-09-01T00:00:00Z", "period_end": "2026-10-01T00:00:00Z", + "subscription_id": 3, "quota_granted": 12500000, "quota_usd": "25.00" +} +``` + +`quota_usd` 按支付时刻(`paid_at`)QPU 换算(§10 裁定 4);`kind = topup` 行 `period_*` / `subscription_id` 为 `null`。 + +### POST `/api/admin/payments` → 201 `payment` + +`kind` 分形态必填校验(`422`):公共 `{kind, paid_at, paid_amount, remark?}`;`subscription` 追加必填 `period_start` / `subscription_id` / `quota_granted`(`period_end` 可空 = 进行中);`topup` 追加必填 `quota_granted`。表单美元双输入为前端换算(按 `qpu/latest` 预览),落库恒原始 quota(表设计 3.6)。 + +### PUT `/api/admin/payments/:id` → 200 `payment` / DELETE → 204 + +编辑全字段、删除有确认(前端);改错即修,均落审计。 + +### GET `/api/admin/qpu/latest` → 200 + +`{ "qpu": 500000, "effective_from": "2026-09-01T00:00:00Z" }`——当前换算率(支付表单美元 ↔ quota 双输入换算用)。 + +## 8. 成员管理 + +### GET `/api/admin/members` → 200 `[member]` + +全量直返(个位数,不分页)。 + +```json +{ + "id": 1, "name": "小张", "remark": "同事", "status": "active", + "token_issued_at": "2026-09-18T10:00:00Z", "created_at": "2026-09-18T10:00:00Z", + "key_count": 3 +} +``` + +`key_count` 为派生聚合计数(当前归属名下 key 数);`token_hash` 永不出 API。名称唯一冲突 `409 member_name_taken`。 + +### POST `/api/admin/members` → 201 + +`{name, remark?}`——开户即自动签发令牌,**响应附明文(仅此一次)**: + +```json +{ "member": { "…": "如上实体" }, "token": "(明文令牌——关闭后不可再得,库里只有哈希)" } +``` + +### PATCH `/api/admin/members/:id` → 200 `member` + +`{ "status": "disabled" }` / `active`——停用即成员视图访问立即失效(令牌校验含状态判断)。 + +### POST `/api/admin/members/:id/token` → 201 + +`{ "token": "…" }`——重发:旧令牌即刻失效(旧令牌的成员侧下一请求 `401`)。明文仅此响应一次。 + +## 9. 审计日志 + +### GET `/api/admin/audit-logs` → 200 `{items, total}` + +分页;时间倒序;过滤 `domain`(`key` / `payment` / `member` / `account`)。 + +```json +{ + "id": 99, "created_at": "2026-10-02T09:15:00Z", + "operator": "aaron", "domain": "key", "action": "key.disable", + "summary": "key 小张-01 · 停用" +} +``` + +`operator` 为用户名;**系统触发事件为 `null`**(如空库静默重建 `account.init`)。操作类型枚举(审计域全集): + +| 域 | action 枚举 | +|----|------------| +| key | `key.register` / `key.enable` / `key.disable` / `key.reassign`(归属变更) | +| payment | `payment.create` / `payment.update` / `payment.delete` | +| member | `member.create` / `member.enable` / `member.disable` / `member.token_issue`(开户自动签发)/ `member.token_reissue` | +| account | `account.password_change` / `account.init`(系统,operator null) | + +每个管理面写操作各落一行;查询类与成员侧动作不留痕。**表结构归技术稿(members / api_keys 表设计既有标注),立稿时以本文 DTO 字段为对齐锚点**(§11 遗留 2)。 + +## 10. 本稿裁定项(表设计 / 公共约定的待定落定,请作者重点审) + +1. **用量聚合口径仅 `type = 2`(消费行)**——usage_logs §5 待定「type 过滤策略」落定:聚合与统计只计消费行(错误行入聚合即失真);明细查询默认全类型、可按 `type` 筛(错误排查入口); +2. **日志明细 `token_name` 取当前名**(联查 `api_keys`;快照名存库不出 API)——表设计 2.3 待定「展示口径」落定; +3. **有限基数清单不分页**:`keys` / `members` / `payments` 直返全量数组;公共约定 §6 的「列表类分页」指无界列表(`logs` / `audit-logs`); +4. **支付记录 `quota_usd` 按支付时刻(`paid_at`)QPU 换算**——对齐「按日志时刻取值」的历史正确性原则; +5. **公共约定随本稿与审阅增补四处**:状态表加 `429`(登录节流)、错误信封允许端点附机器可读附注字段(`409 key_already_registered` 附 `key_id`)、`/api` 命名空间专属接口(页面路由永不进入)、端点面向交互设计不与数据表对齐。 + +## 11. 遗留项(归后续立稿或作者裁决) + +1. **管理面日志明细的界面落点**——**已裁决:实现期定**(先做「跳 Key 管理定位到该行」,详情视图待真实数据再定); +2. **审计日志表结构**——归技术稿,以本文 §9 DTO 字段为对齐锚点; +3. **`GET /api/admin/engine` 的状态存储**——归同步引擎设计(待立稿),两稿衔接对齐; +4. **令牌生成细节**(随机源 / 长度 / 前缀)——归技术稿。 diff --git a/docs/design/技术选型.md b/docs/design/技术选型.md new file mode 100644 index 0000000..da94bab --- /dev/null +++ b/docs/design/技术选型.md @@ -0,0 +1,102 @@ +# 技术选型 + +> 母体形态(完整前后端 + PostgreSQL,R13 先行交付形态)的技术栈裁决:语言、逐轴选型、分层落地、验收标准与回退预案。**范围**:功能全集在母体上设计与实现;CFW 抽取版为 R13 后续阶段,待功能全集实现后启动——本稿不覆盖其形态面(Workers / D1 / Cron Triggers / Pages),只保留其中现阶段零成本的预留(§5)。选型总原则:**一项选择既要是母体上的好选择,也不给后续抽取挖坑。** + +| 项目 | 内容 | +|------|------| +| 状态 | 定稿(2026-10-02) | +| 关联 | `需求清单.md`(R13/R14 · 裁决记录——PBKDF2 两形态统一实现)· `交互设计-总览与共性.md`(§3.7 组件能力清单——前端验收标准)· `README.md` | +| 取代 | 无(首篇选型文档) | + +--- + +## 1. 结论总览 + +**全栈 TypeScript,一套语言贯穿前端与后端。** + +| 层 | 选定 | +|----|------| +| 运行时与工具链 | **Bun**(runtime + 包管理 + 测试 + 打包一体化) | +| HTTP | **Hono** | +| 数据访问 | **Drizzle ORM** + postgres.js + **PostgreSQL** | +| 调度 | 进程内定时(细则归同步引擎设计) | +| 加密 | WebCrypto(Bun 原生) | +| 前端 | **Vue 3 + antdv-next + Vite** | +| 部署 | **Docker**(倾向,§6) | + +## 2. 语言:TypeScript 全栈 + +1. 前端横竖是 JS 家族(Vue 3)——前后端同语言,类型系统、模块、异步语义一份学完,DTO 类型前后端共用一份契约; +2. 作者点名学习目标(Bun + Hono + Drizzle + PostgreSQL)即 TS 生态; +3. R13 后续的 CFW 形态原生运行时也只有 JS/TS——全栈 TS 顺带保留了那扇门(现阶段不作为选型依据,零成本预留见 §5)。 + +## 3. 逐轴选型与依据 + +| 轴 | 选定 | 依据 | 回退 | +|----|------|------|------| +| 运行时与工具链 | **Bun** | runtime + 包管理 + 测试 + 打包一体化,学习只认一个工具;作者点名学习目标,认可其生产可用性 | Node——业务代码不动(§7) | +| HTTP 框架 | **Hono** | TS 原生、极轻、中间件模型干净;Bun 原生适配 | `@hono/node-server`(Node) | +| 数据访问 | **Drizzle ORM** | TS 优先、查询保持 SQL 形态(不藏 SQL,利于学与审计);schema 即代码,drizzle-kit 迁移配套齐全 | Kysely / 裸驱动 + 手写仓储 | +| pg 驱动 | **postgres.js** | Drizzle 文档主推;查询构建器式 API;内置连接池;Bun 兼容好 | node-postgres | +| 前端框架 | **Vue 3** | 作者近年常用栈;实现期自由度(共性 §3.5「作者即前端」) | — | +| 组件库 | **antdv-next**(作者指定尝试项) | Vue 3 企业组件库、对齐 ant-design(React 上游)设计体系;**已核验通过**(§7.1):活跃周更、§3.7 清单逐项有对应组件、Agent 文档齐备 | Element Plus / ant-design-vue(§3.7 全覆盖) | +| 加密 | **WebCrypto** | HKDF / AES-256-GCM / HMAC-SHA256 / PBKDF2 与裁决一致(密钥体系见各表设计);Bun 原生 | HKDF 若 Bun 覆盖滞后:基于 HMAC 的纯 TS 垫片(同原语) | +| 构建 / 测试 | **Vite**(web)+ **bun test**(core/db/server) | Bun 自带 test runner;Vite 为 Vue 生态默认 | 前端组件测试可加 vitest(实现期定) | +| API 文档与校验 | **zod** + **@hono/zod-openapi** + **Scalar**(作者确认) | 一源三用:TS 类型 / 请求校验(`422` 与 `fields` 的机器来源)/ OpenAPI 生成;Scalar 为 Hono 官方搭档,规范与渲染器解耦 | Swagger UI(规范不动、换皮即可) | + +## 4. 分层落地:monorepo(Bun workspaces) + +```text +(仓库根 · design 分支) +├─ apps/ +│ ├─ server/ # 母体后端:Hono serve 装配 · Drizzle/pg · 进程内调度 · 环境变量与密钥注入 +│ └─ web/ # 前端:Vue 3 + antdv-next + Vite +├─ packages/ +│ ├─ core/ # 业务核心:轮询与 request_id 去重 · 折算链 · 加密体系 · API 语义与 DTO 类型 +│ └─ db/ # 数据访问层:Drizzle schema(pg 方言)+ 仓储实现 +``` + +**依赖规则(单向)**:`apps/*` → `packages/db` → `packages/core`;core 导出的 DTO 类型可被 web 以纯类型依赖引用(前后端共用一份契约类型)。 + +**代码落点**:实现代码与设计文档同在 design 分支演进。 + +## 5. R14 预留:现阶段零成本,只为后续抽取不挖坑 + +R13 的 CFW 抽取版仍是长期裁决,功能全集实现后启动,届时按 R14「从母体抽业务核心、换基础设施层」执行。本稿只保留两条现阶段零成本、届时免重写的预留: + +1. **Bun 纪律**:`packages/core` 只使用 TypeScript 标准库与 Web 标准 API(`fetch` / WebCrypto / `URL` / `TextEncoder` / `setTimeout` 等);`Bun.*`、`bun:*`、`node:*` 与具体数据库驱动只出现在 `apps/server` 与 `packages/db`——届时 CFW 形态的绑定物(D1 等)同样只落在新的装配层,core 原样搬运; +2. **方言不外泄**:pg 特有物(`jsonb` 等)只出现在 `packages/db` 的 schema 与仓储实现内,不进 core——届时补一份 SQLite 方言 schema 即可,core 与仓储接口不动。 + +Hono(双运行时可跑)与 WebCrypto 本身恰好与 CFW 兼容,属顺手之得,不构成现阶段约束。 + +## 6. 部署通道(倾向,细则归后续部署设计) + +母体以 **Docker 镜像**交付(server 容器 + PostgreSQL,compose 编排):满足仓库「一条命令链可部署」约定;运营方服务器零环境依赖(Bun 运行时随镜像内置),升级 = 换镜像重启;作者不接触服务器,镜像化把运行时选择与运营方环境解耦。 + +## 7. 风险与回退 + +| 风险 | 评估 | 对策 | +|------|------|------| +| antdv-next 项目年轻、维护者 3 人 | 桌面核验通过(§7.1);未核验项:社区采用规模 | 实现期第一步做 **§3.7 能力 spike**(实测);锁版本;实测不满足即触发回退(Element Plus / ant-design-vue) | +| Bun 生态资料少于 Node、边角兼容性 | 本项目规模小、依赖面窄(Hono / Drizzle / postgres.js 均成熟) | **Bun → Node 回退不伤业务代码**:换 `@hono/node-server` 装配即可,packages/* 原样 | +| Bun 的 WebCrypto 原语覆盖(HKDF 等) | 未逐项实测 | 实现期首轮加密任务实测;缺失则 HMAC 纯 TS 垫片 | + +### 7.1 antdv-next 核验记录(2026-10-02,一手源直查) + +口径:npm registry / GitHub API / 官网 llms.txt 直查(当轮无浏览器检索通道——社区口碑与采用规模未核验)。 + +| 核验点 | 结果 | +|--------|------| +| 包与许可 | `antdv-next`@1.6.0(2026-10-01 发布);MIT;ESM + 类型完备(`dist/index.d.ts`) | +| 活跃度 | 仓库 2025-12 建立、最后推送 2026-10-01;近 15 版分布于 2026-05-17 ~ 10-01(约 1–2 周一版);951 星 / 115 fork / 开放 issue 仅 7 | +| 工程质量 | CI + codecov;单测覆盖工件随 release 发布;性能回归基线(每实例响应式计数上限)+ `pnpm bench` 基准;web-types 自 `global.d.ts` 生成、覆盖全部 128 个全局组件;发布说明双语、带性能数据与逐 PR 出处 | +| §3.7 覆盖 | 清单逐项有对应文档组件:Layout(Sider) / Drawer / Grid;Table(expandable 展开行、scroll 横向滚动);Modal / Popconfirm / Message / Alert(closable) / Notification;DatePicker(RangePicker) / Input(TextArea) / InputNumber;Badge / Tag / Segmented / Skeleton / Pagination | +| Agent 友好 | llms.txt / llms-full / llms-semantic(中英)+ For Agents / MCP Server / Skills 专用文档——超出「宣称」水平,利好本仓库 AI 协作流程 | +| 残留风险 | 项目约 10 个月、维护者 3 人(单点风险);minor 版本随依赖下限整体抬升(锁版本对冲);社区采用规模未核验;官网 llms.txt 一处条目标签笔误(Flex 误标 Empty——工程细节小瑕疵) | + +结论:**通过核验,维持选定**;§3.7 spike 与回退预案照旧——桌面核验不替代实测。 + +## 8. 待裁决项 + +1. Docker 细则:基础镜像、compose 形态、密钥注入方式——部署设计时定; +2. 前端测试设施是否引入 vitest。