From b2e0aa4a6c612e0be7a6b6ef1aebbd97c88682ad Mon Sep 17 00:00:00 2001 From: skycurtain Date: Tue, 8 Sep 2026 00:49:12 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E6=8E=A5=E5=8F=A3=E8=B0=83=E7=A0=94?= =?UTF-8?q?=E4=B8=8E=E8=AE=BE=E8=AE=A1=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NewAPI sk-key 接口实证调研(计费参数/接口清单/坑记录)、交互设计定稿、实施计划。数据层与应用层后续按阶段提交。 --- docs/NewAPI-sk-key接口文档.md | 430 ++++++++++++++++++++++++++++++++++ docs/bench交互设计文档.md | 197 ++++++++++++++++ docs/bench实施计划.md | 143 +++++++++++ 3 files changed, 770 insertions(+) create mode 100644 docs/NewAPI-sk-key接口文档.md create mode 100644 docs/bench交互设计文档.md create mode 100644 docs/bench实施计划.md diff --git a/docs/NewAPI-sk-key接口文档.md b/docs/NewAPI-sk-key接口文档.md new file mode 100644 index 0000000..7deb76a --- /dev/null +++ b/docs/NewAPI-sk-key接口文档.md @@ -0,0 +1,430 @@ +# NewAPI sk-key 查询接口文档 + +> 基于源码实证调研(QuantumNous/new-api),整理所有可通过 `sk-` API key 直接访问的查询接口。 +> +> 调研日期:2025-07 月 · 源码版本:main 分支 + +--- + +## 0. 通用说明 + +### 认证方式 + +所有接口均使用 API key 鉴权,在请求头中携带: + +``` +Authorization: Bearer sk-xxxxxxxxxxxx +``` + +**不需要**账号登录态(access token / session)。这是拼车场景能成立的关键前提。 + +### BaseURL + +即 NewAPI 站点地址,例如 `https://your-friend-domain.com`。所有路径前不带多余前缀,直接拼接。 + +### 两种鉴权中间件 + +| 中间件 | 说明 | +|--------|------| +| `TokenAuth()` | 标准版:校验 key 有效、未过期、有剩余额度。过期或耗尽的 key 会被拒绝。 | +| `TokenAuthReadOnly()` | 宽松版:只校验 key 存在且用户未封禁。**即使 key 已过期、已禁用、额度耗尽也能查询**。 | + +> `/api/usage/token` 和 `/api/log/token` 用的是 `TokenAuthReadOnly()`,所以伙伴的 key 即使用完了,仍能查到自己的用量记录。 + +### 已确认的换算参数 + +| 参数 | 值 | 来源 | +|------|-----|------| +| `QuotaPerUnit` | 500000 | 实测验证:接口① `total_used` ÷ 500000 = 接口④ `total_usage` ÷ 100 | + +### 关键开关:DisplayTokenStatEnabled + +| 接口 | 受开关影响? | +|------|-------------| +| billing 接口(③④⑤⑥) | ✅ 受影响。开启 -> 返回 token 级额度;关闭 -> 回退到账号级额度(暴露整个账号余额) | +| `/api/usage/token`、`/api/log/token` | ❌ 不受影响,恒返回 token 级数据 | + +**实测已确认当前平台开关为开启状态**(billing 接口返回的 `hard_limit_usd` 是 token 级占位值,非账号总额)。 + +开关位于后台:设置 -> 计费设置(Billing)-> "货币与显示"(Currency & Display)-> `Display Token Statistics`。仅管理员可改。 + +--- + +## 1. 接口清单总览 + +| 序号 | 方法 | 路径 | 鉴权 | 用途 | +|------|------|------|------|------| +| ① | GET | `/api/usage/token` | TokenAuthReadOnly | 查 token 的累计已用/剩余/总额(原始 quota) | +| ② | GET | `/api/log/token` | TokenAuthReadOnly | 查该 key 最近 1000 条调用日志 | +| ③ | GET | `/v1/dashboard/billing/subscription` | TokenAuth | 查总额度上限(OpenAI 兼容,美元) | +| ④ | GET | `/v1/dashboard/billing/usage` | TokenAuth | 查累计已用(OpenAI 兼容,美分) | +| ⑤ | GET | `/dashboard/billing/subscription` | TokenAuth | 同 ③,不带 /v1 前缀 | +| ⑥ | GET | `/dashboard/billing/usage` | TokenAuth | 同 ④,不带 /v1 前缀 | +| ⑦ | GET | `/v1/models` | TokenAuth | 列出可用模型(OpenAI 格式) | +| ⑧ | GET | `/v1/models/:model` | TokenAuth | 查单个模型详情 | +| ⑨ | GET | `/v1beta/models` | TokenAuth | 列出可用模型(Gemini 格式) | +| ⑩ | GET | `/v1beta/openai/models` | TokenAuth | 列出可用模型(OpenAI 格式) | + +> ③和⑤是同一个接口,④和⑥是同一个接口,只是路径前缀不同(为兼容不同客户端写法)。测一组即可。 +> +> ⑦~⑩是模型列表接口,与用量统计无关,列出仅为完整覆盖。 + +--- + +## 2. 用量/额度查询接口详解 + +### ① GET `/api/usage/token` - token 级用量汇总 + +**鉴权**:`TokenAuthReadOnly()`(宽松版,key 过期/耗尽也能查) + +**请求参数**:无 + +**请求示例**: + +``` +GET /api/usage/token +Authorization: Bearer sk-xxxx +``` + +**返回结构**(源码:`controller/token.go` -> `GetTokenUsage`): + +```json +{ + "code": true, + "message": "ok", + "data": { + "object": "token_usage", + "name": "令牌名称", + "total_granted": 6012, + "total_used": 673419457, + "total_available": -673413445, + "unlimited_quota": true, + "model_limits": {}, + "model_limits_enabled": false, + "expires_at": 0 + } +} +``` + +**字段说明**: + +| 字段 | 类型 | 说明 | +|------|------|------| +| `name` | string | 令牌名称 | +| `total_granted` | int | 总额度 = RemainQuota + UsedQuota(原始 quota 单位) | +| `total_used` | int | **累计已用**(原始 quota 单位)。从 key 创建以来累计,非当月。换算美元:`total_used ÷ 500000` | +| `total_available` | int | 剩余 = total_granted − total_used。无限额度时为负数,无实际意义 | +| `unlimited_quota` | bool | 是否无限额度。true 时 total_granted/total_available 无实际意义 | +| `model_limits` | object | 模型限制(map) | +| `model_limits_enabled` | bool | 是否启用模型限制 | +| `expires_at` | int | 过期时间(Unix 秒),0 = 永不过期 | + + + +**注意事项**: +- `total_used` 是**累计值**,从 key 创建以来累计,不是当月用量。 +- 额度单位是**原始 quota 整数**,需自行除以 `QuotaPerUnit`(默认 500000)换算美元。 +- 禁用再解禁不会重置 `total_used`(源码验证:`Update()` 的 `.Select(...)` 列表不含 `used_quota`)。 + +--- + +### ② GET `/api/log/token` - token 级调用日志 + +**鉴权**:`TokenAuthReadOnly()` + +**请求参数**:无(不支持任何 query 参数) + +**请求示例**: + +``` +GET /api/log/token +Authorization: Bearer sk-xxxx +``` + +**返回结构**(源码:`controller/log.go:62` -> `GetLogByKey`): + +```json +{ + "success": true, + "message": "", + "data": [ + { + "id": 123456, + "user_id": 12, + "created_at": 1751328000, + "type": 2, + "content": "模型倍率 1.0,分组倍率 1.0", + "username": "用户名", + "token_name": "令牌名", + "model_name": "gpt-4o", + "quota": 2500, + "prompt_tokens": 1500, + "completion_tokens": 500, + "use_time": 2, + "is_stream": true, + "channel": 0, + "channel_name": "", + "token_id": 100, + "group": "default", + "ip": "", + "request_id": "req_xxx", + "upstream_request_id": "", + "other": "" + } + ] +} +``` + +**字段说明**: + +| 字段 | 类型 | 说明 | +|------|------|------| +| `created_at` | int | 调用时间(Unix 秒)- **客户端可据此筛当月** | +| `type` | int | 日志类型:2=消费,5=错误,1=充值等 | +| `model_name` | string | 调用的模型名 | +| `quota` | int | 本次消耗的 quota(换算美元:`quota ÷ 500000`) | +| `prompt_tokens` | int | 输入 token 数 | +| `completion_tokens` | int | 输出 token 数 | +| `use_time` | int | 耗时(秒) | +| `is_stream` | bool | 是否流式 | +| `channel_name` | string | 渠道名(普通用户会被清空) | + +**关键限制**: + +- **硬上限 1000 条**:固定返回最近 1000 条日志(`Limit(common.MaxRecentItems)`,`MaxRecentItems = 1000`,定义于 `common/constants.go`)。 +- **不支持任何请求参数**:`GetLogByKey` 不读任何 `c.Query()`,没有分页(page/limit/offset),没有时间过滤(start_timestamp/end_timestamp)。 +- **固定按 `id desc` 排序**:即最新 1000 条。 +- **超过 1000 条的旧记录无法获取**:无法翻页,无法扩大 limit。 +- **当月调用超 1000 次时,此接口无法覆盖完整当月数据**。 + +--- + +### ③ GET `/v1/dashboard/billing/subscription` - 总额度上限 + +**鉴权**:`TokenAuth()` + +**请求参数**:无 + +**请求示例**: + +``` +GET /v1/dashboard/billing/subscription +Authorization: Bearer sk-xxxx +``` + +**返回结构**(源码:`controller/billing.go:13` -> `GetSubscription`): + +```json +{ + "object": "billing_subscription", + "has_payment_method": true, + "soft_limit_usd": 100000000, + "hard_limit_usd": 100000000, + "system_hard_limit_usd": 100000000, + "access_until": 0 +} +``` + +**字段说明**: + +| 字段 | 类型 | 说明 | +|------|------|------| +| `hard_limit_usd` | float | **总额度上限**(美元)。无限额度 key 返回 `100000000`(占位值,无实际意义) | +| `soft_limit_usd` | float | 软上限(同 hard_limit_usd) | +| `system_hard_limit_usd` | float | 系统上限(同 hard_limit_usd) | +| `access_until` | int | token 过期时间(Unix 秒),0 = 永不过期 | + +**注意事项**: +- 受 `DisplayTokenStatEnabled` 开关影响。开启时返回 token 级额度;关闭时返回账号级额度(会暴露整个账号余额)。 +- 无限额度 key 返回 `100000000`(一亿)作为占位值,无参考意义。 +- 受 `quota_display_type` 影响:USD 模式直接是美元;CNY 模式会做货币转换;Tokens 模式是原始 token 数。 + +--- + +### ④ GET `/v1/dashboard/billing/usage` - 累计已用 + +**鉴权**:`TokenAuth()` + +**请求参数**:无 + +**请求示例**: + +``` +GET /v1/dashboard/billing/usage +Authorization: Bearer sk-xxxx +``` + +**返回结构**(源码:`controller/billing.go:75` -> `GetUsage`): + +```json +{ + "object": "list", + "total_usage": 134683.8914 +} +``` + +**字段说明**: + +| 字段 | 类型 | 说明 | +|------|------|------| +| `total_usage` | float | **累计已用**(单位:**美分**)。换算美元:`total_usage ÷ 100` | + +**实测验证**: + +``` +total_usage = 134683.8914 美分 ÷ 100 = 1346.84 美元 +与接口① total_used(673419457) ÷ 500000 = 1346.84 美元 完全吻合 +``` + +**注意事项**: +- `total_usage` 是**累计值**(从 key 创建至今),不是当月用量。 +- 受 `DisplayTokenStatEnabled` 开关影响(开启时为 token 级)。 +- 受 `quota_display_type` 影响(USD 模式才是美元,否则需另行换算)。 +- `hard_limit_usd`(接口③)− `total_usage ÷ 100`(接口④)= 剩余额度(仅有限额 key 有意义)。 + +--- + +## 3. 模型列表接口(与用量无关) + +> 以下接口 sk-key 可访问,但返回的是模型列表,与用量统计无关。列出仅为完整覆盖。 + +### ⑦ GET `/v1/models` + +返回当前 key 可用的模型列表,OpenAI 兼容格式。 + +```json +{ + "object": "list", + "data": [ + { + "id": "gpt-4o", + "object": "model", + "created": 1686935000, + "owned_by": "openai" + } + ] +} +``` + +### ⑧ GET `/v1/models/:model` + +返回单个模型详情。将 `:model` 替换为模型名,如 `/v1/models/gpt-4o`。 + +> **已知限制**:此接口与 `/v1/models` 数据源不同。`/v1/models` 查数据库 `abilities` 表(反映渠道实际配置的模型),而此接口查代码内硬编码的静态模型列表(`openAIModelsMap`)。当渠道配了新模型或自定义模型名但代码未收录时,会出现"列表里有但单查返回 `model_not_found`"的现象。不影响实际模型调用。 + +### ⑨ GET `/v1beta/models` + +返回模型列表(Gemini 兼容格式)。 + +### ⑩ GET `/v1beta/openai/models` + +返回模型列表(OpenAI 格式,Gemini 路由下)。 + +--- + +## 4. 确认不存在的接口(排除清单) + +以下接口在源码中**不存在**或**不可用 sk-key 访问**,测试时应返回 404 或 401: + +### 4.1 不存在的接口(返回 404) + +| 路径 | 状态 | +|------|------| +| `/v1/usage` | 不存在 | +| `/v1/quota` | 不存在 | +| `/api/token/self` | 不存在 | + +### 4.2 存在但需要登录态的接口(sk-key 访问返回 401) + +这些接口支持时间范围查询和 `token_name` 过滤,但**必须用账号登录态**(access token + `New-Api-User` header),sk-key 无法访问: + +| 路径 | 鉴权 | 能力 | +|------|------|------| +| `/api/log/self` | `UserAuth()` | 支持 `start_timestamp`、`end_timestamp`、`token_name` 过滤,返回日志明细 | +| `/api/log/self/stat` | `UserAuth()` | **支持时间范围 + token_name,返回 quota 汇总**(调 `SumUsedQuota`) | +| `/api/data/self` | `UserAuth()` | 按日聚合的看板数据 | +| `/api/token/` (GET/POST/PUT/DELETE) | `UserAuth()` | token 的增删改查 | +| `/api/user/self` | `UserAuth()` | 用户自身信息 | + +> **特别说明**:`/api/log/self/stat` 是 NewAPI 中最适合算"当月用量"的接口--传当月起止时间 + `token_name` 即可返回该 key 当月消耗的 quota 总和。但它需要登录态,是拼车场景下"当月用量"难以纯客户端实现的核心障碍。 + +### 4.3 参数被忽略的情况 + +| 请求 | 实际行为 | +|------|---------| +| `/api/log/token?p=2` | 参数被忽略,仍返回最近 1000 条 | +| `/api/log/token?limit=5000` | 参数被忽略,仍返回最近 1000 条 | +| `/api/log/token?start_timestamp=1751328000` | 参数被忽略,仍返回最近 1000 条 | +| `/v1/dashboard/billing/usage?start_date=2025-07-01` | 参数被忽略 | + +--- + +## 5. 用量换算速查 + +### 核心换算公式 + +``` +原始 quota ÷ 500000 = 美元 +``` + +### 实测数据验证 + +| 来源 | 原始值 | 换算 | 结果 | +|------|--------|------|------| +| 接口① `total_used` | 673,419,457 quota | ÷ 500000 | 1346.84 美元 | +| 接口④ `total_usage` | 134,683.8914 美分 | ÷ 100 | 1346.84 美元 | + +两者完全吻合,确认 `QuotaPerUnit = 500000`。 + +--- + +## 6. 关键限制总结 + +### 6.1 时间维度 + +- **所有 sk-key 接口返回的用量都是累计值**(从 key 创建至今),没有任何接口直接返回"当月用量"。 +- 唯一带时间戳的是 `/api/log/token`,但硬限 1000 条且不支持时间过滤。 +- 支持时间范围的接口(`/api/log/self`、`/api/log/self/stat`)需要登录态,sk-key 无法访问。 + +### 6.2 日志条数 + +- `/api/log/token` 硬上限 **1000 条**(`MaxRecentItems = 1000`,定义于 `common/constants.go`)。 +- 不支持分页、不支持时间过滤、不支持改变 limit。 +- 月调用量超过 1000 次时,此接口无法覆盖完整当月数据。 + +### 6.3 写操作 + +- **sk-key 没有任何写接口**(没有 POST/PUT/DELETE 能改 token 自身)。 +- token 的增删改查(`/api/token/`)全部需要登录态 `UserAuth()`。 +- 无法通过 sk-key 重置 `used_quota`(没有"重置用量"接口,`Update()` 的 Select 列表不含 `used_quota`)。 + +### 6.4 禁用/解禁行为 + +- 禁用 token 只改 `status` 字段,**不重置 `used_quota`**,不清零 `remain_quota`,不丢历史日志。 +- 解禁后 `used_quota`、`remain_quota`、历史日志全部原封不动。 +- 唯一让 `used_quota` 归零的途径是**删除 token 后新建**(产生全新 Id/Key,旧日志按旧 token_id 关联,新 token 查不到旧日志)。 +- 解禁保护:额度耗尽(`Exhausted`)且 `RemainQuota ≤ 0` 且非无限额度的 token 不能直接解禁,需先充值;已过期的 token 也不能直接解禁,需改过期时间。 + +--- + +## 7. 源码参考 + +| 关注点 | 文件路径 | 关键函数/位置 | +|--------|---------|--------------| +| 路由注册 | `router/api-router.go` | usageRoute、logRoute 组 | +| 路由注册 | `router/dashboard.go:14-23` | billing 路由组 | +| 路由注册 | `router/relay-router.go` | models 路由 | +| API key 鉴权 | `middleware/auth.go:171` | `TokenAuthReadOnly()` | +| API key 鉴权 | `middleware/auth.go` | `TokenAuth()` / `SetupContextForToken` | +| token 用量查询 | `controller/token.go` | `GetTokenUsage` | +| 日志查询 | `controller/log.go:62` | `GetLogByKey` | +| 日志查询(model) | `model/log.go` | `GetLogByTokenId`(Limit 1000) | +| billing 查询 | `controller/billing.go:13` | `GetSubscription` | +| billing 查询 | `controller/billing.go:75` | `GetUsage` | +| 显示开关 | `common/constants.go` | `DisplayTokenStatEnabled` | +| 日志上限 | `common/constants.go` | `MaxRecentItems = 1000` | +| 配额单位 | `common/constants.go` | `QuotaPerUnit`(默认 500000) | +| token 更新 | `model/token.go` | `Update()`(Select 不含 used_quota) | +| 额度增减 | `model/token.go` | `increaseTokenQuota` / `decreaseTokenQuota` | + +> 仓库地址:https://github.com/QuantumNous/new-api diff --git a/docs/bench交互设计文档.md b/docs/bench交互设计文档.md new file mode 100644 index 0000000..0fc1b1b --- /dev/null +++ b/docs/bench交互设计文档.md @@ -0,0 +1,197 @@ +# bench 交互设计与施工方案 + +> new-api-key-bench:NewAPI sk-key 接口的「文档 + 调试」二合一前端工具。 +> +> 本文档是施工蓝图:所有交互与技术决策已定案,按此施工,不再讨论结构性问题。 + +--- + +## 1. 项目定位 + +替代 Postman 的专用调试台(bench):左侧选接口 → 右侧 tab 内看文档、填 key、发请求、看原始响应。对接 5 个接口,服务 karpool 拼车项目的接口调试与换算验证。 + +**接口清单**(数据源:`docs/NewAPI-sk-key接口文档.md`,按此顺序展示,不分组): + +| 序 | 路径 | 鉴权 | 响应特征 | +|---|------|------|---------| +| 1 | `GET /v1/dashboard/billing/subscription` | TokenAuth(严格) | 小 JSON,美元额度(无限额时 1 亿占位) | +| 2 | `GET /v1/dashboard/billing/usage` | TokenAuth(严格) | 小 JSON,累计美分 | +| 3 | `GET /v1/models` | TokenAuth(严格) | 中等 JSON,模型数组 | +| 4 | `GET /api/usage/token` | TokenAuthReadOnly(宽松) | 小 JSON,累计 quota | +| 5 | `GET /api/log/token` | TokenAuthReadOnly(宽松) | **大响应**:最近 1000 条日志,pretty-print 2~4MB | + +> 4、5 宽松鉴权(key 过期/耗尽仍可查);1~3 严格鉴权(key 无效直接被拒)。tab 内标出该差异,避免误判「接口坏了」。 +> 4 与 2 构成换算交叉验证对(total_used ÷ 500000 ≈ total_usage ÷ 100),Tabs 并行对账是核心工作流。 + +--- + +## 2. 布局 + +``` +┌────────────────────────────────────────────────────────────────────────────┐ +│ new-api-key-bench (暗色默认) │ +├──────────────────────────┬─────────────────────────────────────────────────┤ +│ 接口列表 │ ┌──────────────┬──────────────┬───────────┐ │ +│ │ │/v1/dashboar… │/api/log/token│/api/usage…│ × │ +│ /v1/dashboard/billing/ │ ├──────────────┴──────────────┴───────────┴──┤ │ +│ subscription │ │ GET /v1/dashboard/billing/subscription │ │ +│ /v1/dashboard/billing/ │ ├─────────────────────────────────────────────┤ │ +│ usage │ │ sk-key ┌─────────────────────────┐ │ │ +│ /v1/models │ │ │ sk-xxxxxxxxxxxx 👁 │ [发送请求]│ │ +│ /api/usage/token │ │ └─────────────────────────┘ │ │ +│ /api/log/token │ ├─────────────────────────────────────────────┤ │ +│ │ │ 200 OK · 156ms · 234 KB [复制] [⤓] │ │ +│ │ │ ┌─────────────────────────────────────────┐ │ │ +│ │ │ │ 1 { │ │ │ +│ │ │ │ 2 "object": "billing_subscription", │ │ │ +│ │ │ │ 3 "hard_limit_usd": 100000000, │ │ │ +│ │ │ │ (虚拟滚动 JSON · DOM 恒 ~40 行) │ │ │ +│ │ │ └─────────────────────────────────────────┘ │ │ +└──────────────────────────┴─────────────────────────────────────────────────┘ + ~300px sider(纯路径列表, 自适应(tab 内自带 sk-key 框) + 无分组无编号,按清单顺序) +``` + +- **左 sider**(~300px):接口列表,**不分组、不编号**,按 §1 清单顺序平铺 5 个路径;选中态高亮 +- **右主区**:`t-tabs` 顶部,激活 tab 内容 = 请求行(method + 路径 + 鉴权徽标)→ 调试行(sk-key 输入 + 发送按钮)→ 响应区(元信息 + 虚拟滚动 JSON) +- 暗色默认(TDesign dark theme + CSS 变量) + +--- + +## 3. 交互细则 + +### 3.1 Tabs + +- 点击 sider 接口:该接口**无 tab 则新开,有则激活**(Postman 语义) +- 关闭 tab:丢弃该 tab 状态(响应缓存随之释放);若关的是激活 tab,激活相邻 tab;全部关光 → 空态引导页(提示从左侧选择接口) +- tab 标签直接显示接口路径(如 `/v1/models`、`/api/log/token`),与 sider 列表一致;宽度不够时省略号截断 +- tab 上限即 5(接口总数),无需溢出策略 + +### 3.2 sk-key(每 tab 独立) + +- 每个 tab 自带 key 输入框(`t-input` type=password + 显隐切换),互不影响,可并行调试多把 key +- **不做持久化**:key 仅存 Pinia 内存——切 tab 不丢,F5 刷新随整页状态一并重置(tabs、key、响应缓存同生共死,语义干净);不写 localStorage / sessionStorage,sk-key 不落盘 + +### 3.3 请求 + +- 纯手动:点「发送」才发请求;TanStack Query `staleTime: Infinity`,切走再切回 tab **不自动重发** +- queryKey:`[endpointId, skKey]`——同 tab 换 key 视为新查询;不同 tab(不同 key)缓存天然隔离 +- 发送前校验:key 为空 → 输入框标红提示,不发请求 +- 请求中按钮 loading;响应元信息记录:状态码、耗时 ms、响应体字节数 + +### 3.4 响应区 + +- **虚拟滚动 JSON 文本视图**(对齐 DevTools Network Response): + - `JSON.stringify(data, null, 2)` → 按 `\n` 切行 → 虚拟滚动窗口渲染 + - 行号列 + 等宽字体;**语法高亮仅对可视区行**做正则着色(行级懒高亮) + - DOM 恒定 ~40 行,1000 条日志滚动帧率与 10 条无异 + - JSON 解析失败(非 JSON 响应)→ 原样文本展示,不高亮 +- 空态(未发送):占位提示「填写 sk-key 后发送请求」 +- 错误态:网络错误(fetch reject)与 HTTP 非 2xx 分开展示;非 2xx 时仍展示响应体(调试 401/403 场景是刚需) +- 操作:复制(格式化 JSON)、下载 `.json` 文件 + +--- + +## 4. 数据与状态设计 + +### 4.1 端点元数据(静态) + +`src/api/endpoints.ts`——接口定义(按清单顺序)、鉴权方式、注意事项文案、示例响应。纯数据无逻辑,不含分组概念。 + +### 4.2 Pinia stores + +- `useBenchStore`:`openTabs: EndpointId[]`(开启顺序)、`activeTab: EndpointId | null`;tab 的 key 由 queryKey 直接承载,无需单独 store +- sk-key 不做独立 store 与持久化:作为 EndpointPanel 内的组件局部状态(`ref`),生命周期与 tab 内容一致,随刷新重置 + +### 4.3 请求层 + +- `src/api/client.ts`:薄 fetch wrapper——拼 Authorization 头、计时、返回 `{ status, ms, bytes, text, json }`;无拦截器无重试(调试工具要看到真实失败) +- `src/api/queries.ts`:TanStack Query 接入,`useQuery({ queryKey: [epId, skKey], enabled: 手动触发 })` + +### 4.4 组件结构 + +``` +src/ +├── api/ +│ ├── endpoints.ts # 5 接口静态元数据 +│ ├── client.ts # fetch wrapper(计时/字节数/解析) +│ └── queries.ts # TanStack Query 查询定义 +├── stores/ +│ └── bench.ts # tabs 状态 +├── components/ +│ ├── side-nav.vue # 左侧接口菜单(平铺路径列表) +│ ├── request-tabs.vue # t-tabs 容器(开/关/激活) +│ ├── endpoint-panel.vue # 单接口完整面板(请求行+调试+响应) +│ ├── doc-card.vue # 请求行/鉴权徽标/注意事项(tab 内文档信息) +│ └── json-text-view.vue # 虚拟滚动 JSON 文本视图(核心性能组件) +├── lib/ +│ └── jsonView.ts # 行窗口计算、行级高亮(纯函数,vitest 覆盖) +├── App.vue # 双栏布局骨架 +└── main.ts # 装配 pinia / vue-query / tdesign / router +``` + +--- + +## 5. 工程配置 + +### 5.1 代理(vite.config.ts) + +```ts +const GATEWAY = process.env.GATEWAY ?? 'https://ai-gateway.furtherverse.com' + +server: { + proxy: { + '/api': { target: GATEWAY, changeOrigin: true }, + '/v1': { target: GATEWAY, changeOrigin: true }, + }, +} +``` + +只配 `/api`、`/v1` 两条(5 接口仅命中此前缀);`GATEWAY` 环境变量可覆盖。`changeOrigin: true` 改写 Host 头,网关虚拟主机路由才正确。 + +### 5.2 质量 + +- **命名规范**:文件名一律 kebab-case 且**语义化完整,宁长勿缩写**(如 `json-text-view.vue` 而非 `jtv.vue`,`endpoint-panel.vue` 而非 `ep.vue`;变量/函数同理,`endpointList` 优于 `epList`);模板中引用组件用大驼峰(``、``) +- prettier:`semi: true`、单引号、100 宽(已生效并提交) +- oxlint + eslint 双引擎(脚手架自带链路) +- vitest:纯函数层测试(`lib/jsonView.ts` 的窗口/高亮计算、`client.ts` 的 URL 与头拼装);`.spec.ts` 与源码同目录 +- `build` 保持 `type-check && build-only`(脚手架默认) + +### 5.3 依赖(已安装) + +`tdesign-vue-next@1.20.7`、`@tanstack/vue-query@5.102.8`、`@vueuse/core@14.4.0`、`vitest@5.0.0` + 脚手架自带(vue-router / pinia / oxlint / eslint / prettier) + +> **VueUse 使用边界**(避免与既有职责重叠):`json-text-view.vue` 用 `useElementSize`(容器尺寸监听)、`useThrottleFn`(滚动节流);响应区复制按钮用 `useClipboard`。loading/error 态归 TanStack Query,tab 状态归 Pinia,输入显隐归 TDesign——这些场景**不用** VueUse。 + +> vue-router:空 routes 保留(单页应用,路由层暂不启用,留作后续扩展位) + +--- + +## 6. 施工清单(按序) + +1. vite.config 补代理 + vitest include;`src/` 清模板残留(counter store、HelloWorld 引用) +2. `api/endpoints.ts` 静态数据 + `lib/jsonView.ts` 纯函数(含单测) +3. `stores/bench.ts`(tabs 状态)+ `api/client.ts` + `api/queries.ts`(sk-key 为 EndpointPanel 局部状态,无持久化) +4. App 骨架 + SideNav + RequestTabs + EndpointPanel + DocCard +5. JsonTextView 虚拟滚动(大响应性能验收:`/api/log/token` 1000 条日志不卡顿) +6. 暗色主题落地 + 空态/错误态打磨 + 复制/下载 +7. 全量验证:lint + type-check + build + test,真机调通 5 接口 + +--- + +## 7. 决策记录(已闭环,不再开放) + +| 决策点 | 定案 | 备注 | +|--------|------|------| +| 组件库 | TDesign | Table/虚拟滚动成熟、腾讯周更维护、暗色 CSS 变量 | +| 请求层 | TanStack Query + fetch | 不引 axios,避免双层抽象 | +| JSON 展示 | 虚拟滚动文本视图 | 不解析业务结构(Postman/DevTools 先例,解析是正式项目的职责) | +| 布局 | 左 sider + 右 tabs | Postman 形态;sider 平铺 5 路径,不分组不编号,tab 标签即路径 | +| tab 内文档 | 鉴权徽标 + 注意事项 | 说明内容都在 tab 页里,sider 只管路径 | +| key 输入 | 每 tab 独立 | queryKey 天然隔离 | +| key 持久化 | 不做 | 仅 Pinia 内存(tab 组件局部 ref),刷新重置,不落盘 | +| 请求语义 | 手动 + staleTime Infinity | 调试工具的响应不该自己变 | +| 排除清单 | 不做 | 知识留在 docs/ 接口文档 | +| 主题 | 暗色默认 | — | +| 命名规范 | 文件 kebab-case 且语义化(宁长勿缩写)/ 模板引用大驼峰 | 见 §5.2 | +| 目录名 | new-api-key-bench | — | diff --git a/docs/bench实施计划.md b/docs/bench实施计划.md new file mode 100644 index 0000000..ef38bfa --- /dev/null +++ b/docs/bench实施计划.md @@ -0,0 +1,143 @@ +# bench 实施计划 + +> new-api-key-bench 施工执行计划。设计依据:`bench交互设计文档.md`(已定稿,决策不再开放)。 +> +> 本文档回答「怎么干」:阶段划分、每步产出、验收标准。与设计文档的分工——设计文档说「是什么」,本文档说「怎么做、做到什么程度算完成」。 + +--- + +## 0. 施工前置认知 + +- **遵循的规范**(生效中,施工时不可违背): + - 文件名 kebab-case 且语义化完整(`json-text-view.vue` 而非 `jtv.vue`);模板引用大驼峰(``) + - prettier `semi: true` / 单引号 / 100 宽(已配置,`bun run format` 兜底) + - composable 一律 setup 顶层调用(VueUse / pinia / vue-query 均适用) + - 代码内标识符语义化(`endpointList` 优于 `epList`) +- **可用的能力**:TDesign MCP(组件文档/DOM/图标)、antfu skills(vue / pinia / vitest / vueuse 等,写前查) +- **验收基线**:lint ✅ + type-check ✅ + build ✅ + vitest ✅,缺一不可 + +--- + +## 1. 阶段划分(六阶段,严格按序) + +### 阶段一:工程配置收尾 ✅(已完成 2026-09-07) + +> 验收记录:type-check / test(冒烟 1 用例)/ lint(oxlint 0 警告)/ build(63.08 kB)四绿通过;dev server 5199 端口起服务正常。 +> 施工决策:vitest 配置独立为 `vitest.config.ts`(Vite 8 `UserConfig` 类型不收 `test` 字段,TS2769);`src/lib/smoke.spec.ts` 冒烟测试守护管线。 + +| # | 任务 | 产出 | +|---|------|------| +| 1.1 | `vite.config.ts` 补网关代理(`/api`、`/v1` → `GATEWAY` 环境变量可覆盖)+ vitest include 配置 | vite.config.ts 更新 | +| 1.2 | 清模板残留:`src/stores/counter.ts`、`src/router/` 空路由及 main.ts 引用、无用 assets | src/ 干净 | +| 1.3 | `package.json` scripts 增补 `test`(vitest run) | scripts 齐备 | + +**验收**:`bun run dev` 起服务无报错;`bunx vitest run`(空测试集)退出码 0。 + +### 阶段二:数据层(纯 TS,无 UI)✅(已完成 2026-09-07) + +> 验收记录:32 用例全绿(endpoints 8 / gateway-client 4 / json-text-processor 19 / smoke 1);type-check / lint / build 三绿。 +> 施工决策:`formatJsonOrRawText` 对 `null` 显式降级(`JSON.stringify(null)` 会返回字面量 `"null"`);`computeVisibleRowWindow` 增加 scrollTop 超界钳制(惯性滚动 shrink 场景);tokenize 输出结构化 span 而非 HTML 字符串(转义与着色归 UI 层,杜绝注入)。 + +| # | 任务 | 产出 | +|---|------|------| +| 2.1 | `src/api/endpoints.ts`:5 接口静态元数据(路径/方法/鉴权/注意事项文案/示例响应),按设计文档 §1 顺序 | endpoints.ts | +| 2.2 | `src/api/gateway-client.ts`:fetch wrapper——拼 `Authorization: Bearer` 头、performance 计时、返回 `{ status, ms, bytes, text, json }`;无重试无拦截 | gateway-client.ts | +| 2.3 | `src/lib/json-text-processor.ts`:JSON pretty-print 行切分、可视窗口计算(`scrollTop / rowHeight` → 起止行号)、行级语法高亮(仅对可视区行执行正则) | json-text-processor.ts | +| 2.4 | vitest 单测:endpoints 数据完整性(5 项、路径唯一、顺序正确)、client 的 URL/头拼装、json-processor 的窗口边界(0 行、超界、恰好整除) | 同目录 `.spec.ts` | + +**验收**:`bunx vitest run` 全绿;纯函数 100% 可脱离 Vue 运行(node 环境可 import)。 + +### 阶段三:状态与请求集成 ✅(已完成 2026-09-07) + +> 验收记录:43 用例全绿(新增 bench-store 10 + endpoint-queries 集成冒烟 1);type-check / lint / build 三绿。 +> 施工决策:endpoint-queries 走 `useQuery` + `enabled` 门控 + `refetch()` 显式触发的组合(方案 A 落地,`fetchQuery` 备选未启用);集成冒烟测试用 jsdom 单文件环境(`@vitest-environment jsdom` 注释),node 环境测试不受影响;jsdom 进 devDependencies。 +> TDD 战果:store 单测抓出「关中间 tab 后激活态断言」用例自身的设计错误(重开去重导致顺序与预期不符),拆成两个语义纯净的用例修复。 + +| # | 任务 | 产出 | +|---|------|------| +| 3.1 | `src/stores/bench-store.ts`:`openTabs: string[]`(endpoint 路径数组)、`activeTab: string \| null`、open/close/activate actions(Postman 语义:已开则激活;关激活 tab 则激活相邻) | bench-store.ts | +| 3.2 | `src/api/endpoint-queries.ts`:TanStack Query 接入——`queryKey: [endpointPath, skKey]`、`staleTime: Infinity`、`enabled` 由「发送」动作置位 | endpoint-queries.ts | +| 3.3 | `src/main.ts` 装配:TDesign(按需引 style)、Pinia、VueQueryPlugin、暗色主题注入 | main.ts | + +**验收**:type-check 过;`bench-store` 的 tab 开关逻辑配 vitest(store 单测不依赖组件)。 + +### 阶段四:UI 骨架(布局可跑)✅(已完成 2026-09-07) + +> 验收记录:type-check / lint / test 43 / build 四绿;dev server 起服务正常。 +> 产出:app-shell(双栏)+ side-nav(5 路径平铺)+ request-tabs(t-tabs 卡片风、destroyOnHide=false 保 tab 状态)+ endpoint-panel(信息行/sk-key/发送/响应占位)+ endpoint-info-line(method/路径/鉴权徽标带 title 提示)。 +> 施工决策:暗色用 `:root { theme-mode: dark }`(TDesign 官方暗色变量挂载点)而非暗色 class 包裹;t-tabs 全量 CSS 引入暂不按需(阶段六再评估 tree-shake);endpoint-panel 已接 useManualEndpointQuery(响应区为原文占位,阶段五换 json-text-view)。 +> 补充交互(2026-09-07):t-tabs 拖拽排序——store 加 reorderTab(含越界/路径不匹配防御),request-tabs 开 :drag-sort + @drag-sort,7 个重排单测(前移/后移/相邻/原位/防御/激活态不变)。TDesign drag-sort 只发事件不改数据,重排由 store splice 落地。 +> Bug 修复(2026-09-07,手测发现):①「所有 tab 内容挤同一面板」——destroyOnHide=false 让全部 panel 常驻 DOM 靠 display:none 隐藏,而我的样式把 .t-tab-panel 覆盖成 flex,破坏了隐藏机制;②「关 tab 无效」——受控 value 与 remove 事件的内部状态打架。修复:去掉 destroyOnHide,panel 内容改为 `v-if="tabPath === activeTab"` 只挂激活 tab,状态保留交给 Query 缓存(queryKey 含 path+skKey),样式不再覆盖 .t-tab-panel 的 display。 +> Bug 修复二(2026-09-07,「选项卡与内容都没关掉」):插槽写法(t-tab-panel 子组件)经 getSlotPanels() 从 vnode 反挖 panel 列表,在受控 value + 动态增删下与 openTabs 脱钩,remove 后 nav 不更新。弃用插槽,改官方 list prop 数据驱动(Tabs + tabPanelList computed,panel 用 h() 渲染),显式 import Tabs 绕开 resolveComponent。@remove 事件签名按源码修正为 { value, index, e }。 + +| # | 任务 | 产出 | +|---|------|------| +| 4.1 | `src/app-shell.vue`:双栏布局(左 ~300px sider + 右自适应),暗色底 | app-shell.vue | +| 4.2 | `src/components/side-nav.vue`:5 路径平铺列表,选中态高亮,点击 → store.openTab | side-nav.vue | +| 4.3 | `src/components/request-tabs.vue`:`t-tabs` 容器,tab 标签即路径,可关闭 | request-tabs.vue | +| 4.4 | `src/components/endpoint-panel.vue`:单接口面板骨架(请求行 + 调试行 + 响应区占位);sk-key 为组件局部 `ref`(无持久化) | endpoint-panel.vue | +| 4.5 | `src/components/endpoint-info-line.vue`:method 徽标 + 路径 + 鉴权徽标(宽松/严格) | endpoint-info-line.vue | + +**验收**:浏览器手测——点 sider 开 tab、切 tab、关 tab(含关激活 tab、关光),布局不塌。 + +### 阶段五:核心组件(性能决战)✅(已完成 2026-09-07,性能验收留待真机) + +> 产出:json-text-view.vue(虚拟滚动 + 懒高亮 + token-gap CSS 间隙)+ endpoint-panel 响应区完整化(元信息/复制 useClipboard legacy 降级/下载 Blob)+ 注意事项 t-collapse 折叠。 +> 验收记录:四绿(type-check / lint / test 53 / build);dev server 正常。性能硬指标(1000 条滚动不掉帧、首屏小于 200ms)待真机验收(需真实 sk-key)。 +> 施工决策:eslint vue 模板解析器把注释里「< 200ms」当标签开头(invalid-first-character-of-tag-name),改中文「小于」;  直接写模板会被 prettier 折叠搞坏,改 .token-gap(width: 1ch);vitest.config 补 @vitejs/plugin-vue 才能编译组件测试;复制用 useClipboard legacy 降级(localhost 非 https 环境 execCommand 兜底)。 + +| # | 任务 | 产出 | +|---|------|------| +| 5.1 | `src/components/json-text-view.vue`:虚拟滚动 JSON 视图——`useElementSize`(容器尺寸)+ `useThrottleFn`(滚动节流)+ 行窗口渲染 + 行号列 + 可视区行懒高亮 | json-text-view.vue ✅ | +| 5.2 | 响应区完整化:状态码/耗时/字节数元信息行、`useClipboard` 复制、`.json` 下载、空态(未发送)、错误态(网络错误 / 非 2xx 仍展示响应体) | endpoint-panel.vue 扩展 | +| 5.3 | 注意事项折叠(`t-collapse`):每接口的坑文档(累计值/占位值/单位换算等,文案来自 endpoints.ts) | endpoint-info-line.vue 扩展 | + +**验收(硬指标)**:`/api/log/token` 真实响应(1000 条)渲染 + 全程滚动**不掉帧**;`performance.now()` 量测从「收到响应」到「首屏渲染」< 200ms。 + +### 阶段六:打磨与全量验证 ✅(已完成 2026-09-07,真机调通留待用户) + +> 产出:README 重写(运行/使用/架构/已知边界);TODO/console.log 残留扫描零命中。 +> 验收记录:四绿收官——type-check / lint(oxlint 0 警告 + eslint 0 错误)/ test 53 用例 / build(1.36MB JS + 449KB CSS,TDesign 全量引入的代价,单工具可接受;按需引入留作后续优化)。 +> 待用户完成的验收:真机调通 5 接口(需真实 sk-key)+ /api/log/token 大响应性能手感(滚动帧率/首屏耗时)。 + +| # | 任务 | 产出 | +|---|------|------| +| 6.1 | 暗色主题细调(TDesign CSS 变量)、空 tab 引导页(`t-empty`)、响应区各状态视觉统一 | UI 打磨 | +| 6.2 | 全量验证:`bun run lint` + `bun run build`(含 type-check)+ `bunx vitest run` | 验证记录 | +| 6.3 | 真机调通 5 接口(真实 sk-key 全链路) | 截图/记录 | +| 6.4 | README 重写(运行说明、代理配置、已知边界) | README.md | + +**验收**:四绿(lint/type-check/build/test)+ 5 接口真机可用。 + +--- + +## 2. 风险与预案 + +| 风险 | 概率 | 预案 | +|------|------|------| +| 虚拟滚动行高不齐(tab/空格混排)导致窗口计算偏差 | 中 | 固定 `line-height`、等宽字体,行高取像素常量;超长行 `overflow-x` 平铺 | +| TanStack Query `enabled` 手动触发模式与 v5 API 的契合度 | 中 | 若 `enabled` 模式别扭,改用 `queryClient.fetchQuery` 在点击处理器里显式调用(语义更直白) | +| TDesign 暗色 CSS 变量与自定义样式冲突 | 低 | 优先用 TDesign token;覆盖样式集中在 `src/styles/` 单文件 | +| 大 JSON `JSON.stringify` 本身的耗时(2~4MB) | 低 | 阶段五实测;若 > 50ms 再考虑 Web Worker 切行 | + +--- + +## 3. 里程碑与提交策略 + +- **每阶段一次提交**(等你逐阶段验收,不擅自 commit): + 1. `阶段一+二`:工程配置 + 数据层(含单测)→「bench 数据层与工程配置」 + 2. `阶段三+四`:状态集成 + UI 骨架 →「bench UI 骨架」 + 3. `阶段五`:虚拟滚动核心 →「bench 虚拟滚动响应区」 + 4. `阶段六`:打磨 + 验证 →「bench 全量验证与打磨」 +- 提交信息格式:`bench xxx`,正文列该阶段产出与验收结果 +- push 永远由你手工执行 + +--- + +## 4. 完成定义(DoD) + +- [x] 六阶段全部完成,四绿通过(真机验收两项留待用户:5 接口调通 + 大响应性能) +- [ ] 5 接口真机调通(含 `/api/log/token` 大响应性能达标)——待用户验收 +- [x] 设计文档 §7 决策记录中 11 项决策在实现中全部落实,无偏离 +- [x] 无 TODO/FIXME 残留;无 console.log 残留 +- [x] README 反映真实运行方式