Files
new-api-key-usage/docs/design/实施计划.md
T
skycurtain fca01798e2 feat(web): 管理面三页(阶段 7)
- key 管理:清单 8 列 + 展开行(总额度 / 模型限制 / 401 计数 / 额度新鲜度 / 停用来源)+ 状态徽章三面孔(运行中 / 已停用 / 已失效 + tooltip);登记弹窗(三类失败文案);停用弹确认、启用直接执行
- 购买记录:清单 8 列 + 右侧抽屉表单(登记 / 编辑 / 补录共用;期间顺序校验);核对提示(未登记订阅提示条 + 观测折叠区 + 补录预填订阅 ID 与观测额度)
- 换算参数:时间线两列 + 「当前生效」标记 + 页首说明(纯只读)
- 修复:补 n-config-provider 中文 locale(naive-ui 默认英文按钮);两处表格 scroll-x(列宽被压缩致表头截断)
- 走查:Playwright 15 步全操作通过(含三类登记结果、确认弹窗、补录与 CRUD)
2026-09-15 01:06:43 +08:00

14 KiB
Raw Blame History

实施计划

开发过程的推进方案:loop 模式(每轮一个可验证批次)+ 八个阶段的验收标准。任务口径全部指向 docs/design/ 的设计文档;本文件只回答「先做什么、怎样算做完、下一步做什么」。

项目 内容
状态 定稿(2026-09-14;进度表随 loop 持续更新)
关联 docs/design/API契约.md · docs/design/同步引擎设计.md · docs/design/交互设计.md · 四张表字段设计 · AGENTS.md(命令)
范围 阶段划分、每批验收标准、loop 机制与推进规则;技术细节一律以设计文档为准
当前进度 阶段 0–7 已完成;下一批:阶段 8(收尾:部署文档 + 端到端走查 + 文档状态更新)

1. loop 机制(每轮固定动作)

  1. 取任务:计划表(§3)中当前批次的第一项
  2. 实现:以设计文档为准,不自由发挥
  3. 验证:跑该批次的验收命令(type-check / 单测 / 实测 curl / 浏览器走查)
  4. 记录:勾选进度表(日期 + 验证方式)
  5. 提交:一个逻辑单元一个 commit(作者已授权 loop 内直接提交,dev 分支;git push 永远由作者手工)
  6. 报告:一句话——做了什么 / 验证结果 / 下一项
  7. 设计空白的处置(2026-09-14 定):作者可能在开发期缺席——遇到设计文档未覆盖的问题,取最保守默认继续推进(不破坏数据、不做不可逆操作、后续可改),写入下方「实现假设」节并在报告中显著标注,供作者事后补审。不停摆。
    • 例外三条——与「后端单实例」「金额口径」「凭证安全」冲突时:跳过该子任务并标注,不自行决定

实现假设(loop 中累积,供作者补审)

# 假设 依据 / 可逆性
1 server 侧单测用 bun test(内建);前端仍用 Vitest 零依赖;可换(已落地:bun test 跑 src/**/*.test.ts)
2 环境变量:全仓单一根 .env;服务与 drizzle-kit 经 src/lib/load-env.ts 显式加载(Bun 只自动加载 cwd 的 .env,不会向上找) 可改;已落地
3 上游请求超时 30 秒;失败即本轮失败(不单轮重试) 常量,可调
4 key 格式校验仅「非空 + trim」(不强制 sk- 前缀,格式真假交上游判定) 更宽松,避免误拒
5 去重指纹 = sha256(key_id|created_at|model_name|quota|prompt_tokens|completion_tokens) 表设计 §3.1 已给字段清单
6 购买期间匹配:subscription_id 优先;同 ID 多条按 [period_start, period_end);无期间记录仅在「同 ID 唯一」时兜底 边界情况,可调
7 登记成功后立即异步跑一次该 key 的单轮同步(不等 tick) 契约 §2.2 已定「立即异步补一轮」
8 前端提交时间用 toISOString()(Z 形式) 契约 约定 9 允许
9 drizzle-kit 的连库驱动用 postgres(postgres.js,纯 JS)——仅 CLI 需要;运行时仍走 Bun 内建 SQL(drizzle-orm/bun-sql) devDependency,可换 pg
10 上游客户端以接口 + 可注入实现组织(真实 HTTP / 样例文件两种实现),引擎与路由依赖接口 见 §6;可改
11 上游实现的注入开关:env UPSTREAM_SOURCE(http 默认 / fixture 读仓库根快照);fixture 的错误分支由 key 内容约定触发(含 reject → 401、forbidden → 403、unavailable → 网络故障) 阶段 2 落地;仅开发期使用,生产保持默认 http
12 登记 / 启用验证成功即写 usage_synced_at + 上游字段全组(该次 GET /api/usage/token 本身即一次额度采集);log_synced_at 仍只由日志同步写入 契约 §2.2 已定登记响应 log_synced_at: null;额度侧取「数据已入库就不撒谎」口径
13 失败分类:401 / 403 → key_rejected(上游明确拒绝);其余非 2xx / 网络故障 / 超时 / 响应结构异常 → upstream_unavailable 上游事实:403 = 账号封禁;可调
14 PATCH 幂等分支不重复调上游验证(直接返回当前行);启用时凭证解密失败 → 500 internal(提示需重新登记) 与「解密失败不摘除、需人工重新登记」一致;可调
15 一轮内「日志 + 额度」两个调用的合并判定:任一 401 → 本轮计 1 次 401(401 优先);否则任一成功 → 计数清零;全失败 → 计数不动 设计(§3.4)未覆盖「一成功一 401」的冲突情形;401 优先 + N=3 阈值使单次误报无害
16 seed 脚本用假 key 常量 sk-seed-demo-key-0001(SEED_KEY 可覆盖);apps/server/scripts/seed.ts 幂等可重跑 实施计划 §6 已定「假 key 常量」;仅开发期
17 **jsonb 列写入统一走 sql\${obj}::jsonb`**(toJsonb(),见 src/sync/log-row.ts)——drizzle + bun-sql组合直接传 JS 对象会**双重编码**成jsonb的 *string* 值,SQL 侧other->>'k'/? 'k'` 全部失效(2026-09-15 实测定位) 已在 other / model_limits 落地;新增 jsonb 列时照此办理
18 seed 为「日志行数最多的订阅」登记示例购买,其余订阅保持未登记(保留 amount_rmb = null 的展示样本) 样例实测含两个订阅(89 / 90);可改
19 naive-ui composable(useMessage 等)在页面里显式 import——auto-imports.d.ts 的 declare global 在 vue-tsc 6 / TS 6 下未被识别(2026-09-15 实测;components.d.ts 的模板组件自动导入不受影响) 若将来工具链修复可回退;显式 import 无副作用
20 表格展开行写在列定义里({ type: 'expand', renderExpand })——naive-ui 的表级 render-expand prop 不生成展开列(2026-09-15 实测) naive-ui 行为,非项目决策

2. 前提(2026-09-14 定)

  • 开发库:Docker 本地 PostgreSQL(docker-compose.yml 已落地;连接串走 .env,不入库)——四张表已建
  • 联调策略:以样例数据为主(见 §6)——作者未开测试 key;NEWAPI_BASE 可达性已验证(HTTP 200 / 5.2KB / 2.0s;quota_per_unit = 500000、quota_display_type = USD、usd_exchange_rate = 6.8,与文档一致)。真实登记 / 采集链路标「待作者补验」,不阻塞其他阶段
  • 提交:loop 内直接提交已授权;push 由作者手工(仓库有两个远端)

3. 阶段与验收标准

批次 内容 验收标准 状态
0 脚手架 + 契约类型 + 501 骨架 命令链全绿(type-check / test / build) ✅ 2026-09-14
1 数据层与密钥:crypto(HKDF 派生 / HMAC-SHA256 / AES-256-GCM)· 时间工具(+08:00 序列化、UTC 秒转换)· 建表 单测:哈希确定性 + 加解密往返;db:push 后四张表在库 ✅ 2026-09-14(15 项单测绿;四表在库——api_keys 17 列 / usage_logs 23 列 / 索引 8 个)
2 上游客户端 + key 管理(GET / POST / PATCH 三端点)· 客户端接口化(§6) curl 实测:登记 → 清单 → 停用/启用(注入样例实现:登记成功路径用样例响应驱动);三类错误码分支正确(key_rejected / already_registered / upstream_unavailable) ✅ 2026-09-15(28 项单测绿;curl 16 步——登记 201 / 重复 409 / key_rejected 400 / upstream_unavailable 502 / 停用与幂等 / 启用清 consecutive_401s / not_found 404 / 非法参数 400;顺带修正「load-env 未被服务端接线」)
3 管理面其余:purchases 五端点 + qpu 一端点 curl 实测:CRUD 全流程 + 观测聚合(matched_purchase_id 判定正确) ✅ 2026-09-15(33 项单测绿;curl 16 步——登记 201 / 缺必填与非法值与期间顺序 400 / 部分更新与显式清空 / 排序 / 404 / 物理删除 / 观测聚合 matched_purchase_id 判定正确)
4 同步引擎:tick(5 分钟 + 防重入 + 启动即跑)· 日志整批事务入库 · 额度采集 · QPU 采样 · 401 状态机(N=3)· seed 脚本(§6) 库中出现数据(seed 或真实采集);[sync] 日志符合引擎设计;401 状态机单测(连续 401 → 摘除且 disabled_source = auto_401) ✅ 2026-09-15(45 项单测绿;seed 灌入 984 条日志 + QPU 基线 + 示例购买;引擎实测——幂等重跑新增 0 条、摘要日志符合设计、401 端到端:1→2→3 次摘除且摘除后不再轮询)
5 查看面数据面:logs/query + summary + 折算链(按购买期间分段取尺) curl 实测:字段齐全、meta.data_until 取最慢、金额按购买分段累加;无归属购买的行 amount_rmb = null 且计入 unpriced_count(2026-09-14 定) ✅ 2026-09-15(56 项单测绿;curl 17 步——明细字段齐全 / 分页 / type 过滤 / 参数校验 6 类 400 / 未收录 200 空结果 / data_until 取最慢 / 金额按归属购买折算、未折算行 null + unpriced_count 计数正确)
6 前端 /usage-log:筛选区(key 标签 + datetimerange + 快捷项)· 卡片 · 表格 + 展开行 · 分页器 · 六种状态模式 浏览器实测:seed 数据走一遍;六态逐一可见(加载 / 初始 / 未收录 / 无记录 / 同步中 / 失败);金额为 null 显示「—」 ✅ 2026-09-15(Playwright + 系统 Chrome 逐态断言:初始 / 加载 / 查询(卡片 984 · 298,041,838 · 4.90)/ 展开行 / 翻页 / 未收录 / 无记录 / 同步中 / 失败,无 console 错误;金额 null 显示「—」并伴未折算提示条)
7 前端管理面:口令门帘 + 侧边栏 + 三页(key 管理 / 购买记录含核对提示与补录 / 换算参数) 浏览器实测:登记、启停、CRUD、补录、时间线全操作 ✅ 2026-09-15(Playwright 走查 15 步:错口令 → 门帘提示 / 正确口令进入 / 展开行 / 登记三类结果(拒绝 · 成功 · 已登记)/ 停用(确认弹窗)与启用 / 核对提示 / 补录(预填订阅 ID 与观测额度)/ 编辑 / 删除(确认弹窗)/ 换算参数「当前生效」/ 返回查看面;顺带补 naive-ui 中文 locale 与表格横向滚动)
8 收尾:部署文档(docs/delivery/)· 端到端走查 · 文档状态更新(设计文档 → 已实现) 一条命令链可部署(前后端独立 + 后端单实例注记);走查无阻 ☐

4. 待定问题

# 问题 状态
1 amount_rmb 在「日志无归属购买记录」时取什么值 已决(2026-09-14):取 null(展示「—」),汇总增 unpriced_count——见 API 契约 §2.1
2 充值(wallet)路径的折算近似(设计文档标「后议」) 先按「未折算」处理(null / 计入 unpriced_count);池子加权平均待后议
3 dedupe_key 内容指纹的拼接细节 已决:见「实现假设」#5(表设计 §3.1 已给字段清单)
4 上游对同账号请求的态度(限流 / 并发被拒时的表现) 遇错即按「其他故障」处理(不计数、不摘除),如实记录日志
5 request_id 在错误行上不唯一(2026-09-15 实测样板:8 个 request_id 各对应 3 条 type=5 错误记录,共 24 行)——当前按表设计原样实现 dedupe_key = key_id:request_id,同 request_id 的多条错误行合并为 1 条 待作者裁决:影响 = 失败请求次数(error_count)少算;候选方案 = 错误行去重键后缀内容指纹 / 序号(usage_logs 表设计 §3.1 需相应修订)

5. 风险

  • 真实上游联调依赖网络可达(已实测 200);真实 key 未开——以样例数据替代(§6),真实链路标「待作者补验」
  • 折算链(阶段 5)依赖已登记的购买记录——无记录时的行为已定(null + unpriced_count)
  • 后端必须单实例(同步引擎前提):部署文档(阶段 8)必须写明;本地开发也不要起两个后端

6. 离线开发与样例数据(2026-09-14 定)

结论:不需要真实 key 也能跑完全部阶段。 仓库根目录有三个实测响应快照——未入库(.gitignore 用 /api*.json 排除,绝不提交),供开发与验证使用:

文件 内容 用途
api.log.token.json /api/log/token 的 1000 条响应快照(≈3.2 天、含 type=2 与 type=5) 引擎解析 / 去重 / 入库的 fixture;seed 数据源
api.log.token.other.json 单条日志 other 字段样本(26 键:subscription_* 八件套、cache_tokens、倍率等) 解析与展示字段核对
api.status.json /api/status 响应(含 quota_per_unit = 500000 / quota_display_type = USD / usd_exchange_rate = 6.8) QPU 采样逻辑的 fixture

开发策略:

  1. 上游客户端接口化(假设 #10):定义接口(如 UpstreamClient:fetchTokenUsage / fetchLogs / fetchStatus),两套实现——真实 HTTP 实现 + 样例文件实现(从上述 JSON 读响应);引擎与路由依赖接口,联调时替换
  2. seed 脚本(阶段 4 落地,apps/server/scripts/seed.ts):从样例灌入——一个占位 api_keys 行(假 key 常量,其哈希/密文走真实 crypto 逻辑,便于用固定测试 key 查询命中)+ 1000 条 usage_logs + qpu_history 基线 + 一条示例 purchases(用于验证折算链)——使阶段 5/6/7 的开发与验证有真实形状的数据
  3. 真实链路验证清单(标「待作者补验」):登记成功路径(真实 /api/usage/token 验证)、真实采集轮次、真实 401 摘除——不阻塞其他阶段,做完在报告中列出待验项