- 筛选区:key 标签输入(粘贴多行/逗号拆分、去重、localStorage 记忆)+ datetimerange(默认近 7 天、今天/近7天/近30天 快捷项)+ 查询(点按才发请求) - 卡片区:调用次数 / Token 用量 / 预估消费(2 位小数);未折算条数提示条;数据截止右端小字 + 悬停 per-key 明细(同步中 / 未收录 / 已停用) - 明细表格:8 列 + 展开行(缓存 / 流式 / request_id / 错误详情 / 请求路径 / quota 原值 / 订阅归属完整组);错误行 Tag;分页器(页码 + 20/50/100/200) - 六态:加载(卡片骨架 + 表格 loading)/ 初始引导 / 未收录提示 / 时段无记录 / 数据同步中… / 失败提示 + 重试 - 修复:naive-ui 展开行须写在列定义(type: 'expand');useMessage 显式导入(auto-imports 全局声明在 vue-tsc 6 下未生效) - 走查:Playwright + 系统 Chrome 逐态断言与截图,无 console 错误;新增 format 工具单测 6 项
101 lines
13 KiB
Markdown
101 lines
13 KiB
Markdown
# 实施计划
|
||
|
||
> 开发过程的推进方案:**loop 模式**(每轮一个可验证批次)+ 八个阶段的验收标准。任务口径全部指向 `docs/design/` 的设计文档;本文件只回答「先做什么、怎样算做完、下一步做什么」。
|
||
|
||
| 项目 | 内容 |
|
||
|------|------|
|
||
| 状态 | 定稿(2026-09-14;**进度表随 loop 持续更新**) |
|
||
| 关联 | `docs/design/API契约.md` · `docs/design/同步引擎设计.md` · `docs/design/交互设计.md` · 四张表字段设计 · `AGENTS.md`(命令) |
|
||
| 范围 | 阶段划分、每批验收标准、loop 机制与推进规则;技术细节一律以设计文档为准 |
|
||
| 当前进度 | 阶段 0–6 已完成;下一批:**阶段 7**(前端管理面:口令门帘 + 侧边栏 + 三页) |
|
||
|
||
---
|
||
|
||
## 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、补录、时间线全操作 | ☐ |
|
||
| 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 摘除——不阻塞其他阶段,做完在报告中列出待验项
|