diff --git a/README.md b/README.md index 56080f1..17fb437 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ bun run build # type-check + vite build(dist/) ## 使用 -1. 左侧选择接口(5 个,平铺不分组)→ 右侧开 tab +1. 左侧选择接口(6 个,平铺不分组)→ 右侧开 tab 2. tab 内展开「注意事项」看该接口的实证调研坑文档 3. 填 sk-key(每 tab 独立、仅存内存、刷新即重置,不落盘;切换 tab 输入保留) 4. 点「发送请求」→ 响应区显示元信息(状态/耗时/字符数)+ 虚拟滚动高亮 JSON diff --git a/docs/NewAPI-sk-key接口文档.md b/docs/NewAPI-sk-key接口文档.md index 7deb76a..1be27f6 100644 --- a/docs/NewAPI-sk-key接口文档.md +++ b/docs/NewAPI-sk-key接口文档.md @@ -37,6 +37,14 @@ Authorization: Bearer sk-xxxxxxxxxxxx |------|-----|------| | `QuotaPerUnit` | 500000 | 实测验证:接口① `total_used` ÷ 500000 = 接口④ `total_usage` ÷ 100 | +**此值是 NewAPI 的默认值(源码 `common/constants.go:22`:`var QuotaPerUnit = 500 * 1000.0 // $0.002 / 1K tokens`),并非硬编码定律**: + +- 它是 `var` 且挂入 OptionMap(`model/option.go:166`),站长可在后台修改(`model/option.go:605` 会用数据库值覆盖) +- 接口①④交叉实测吻合的原因:billing 接口内部就是拿 quota `÷ QuotaPerUnit` 换算(`controller/billing.go:50`),同一常量自证,**吻合验证不了管理员是否改过此值** +- **正确做法:换算比率不硬编码,从 `/api/status` 的 `quota_per_unit` 字段读取**(见接口⓪),启动时拉取并定期校验,站点改配置后工具自动跟随 + +> `500000` 的设计来由:NewAPI(继承自 One API)以 OpenAI 早期基准定价 **$0.002 / 1K tokens** 为锚点,内部用整数 quota 计费避免浮点误差,1 quota = $0.000002,反推 1 美元 = 500,000 quota。 + ### 关键开关:DisplayTokenStatEnabled | 接口 | 受开关影响? | @@ -54,6 +62,7 @@ Authorization: Bearer sk-xxxxxxxxxxxx | 序号 | 方法 | 路径 | 鉴权 | 用途 | |------|------|------|------|------| +| ⓪ | GET | `/api/status` | **无(完全公开)** | 站点配置与计费参数(含 `quota_per_unit`),工具校准换算比率的依据 | | ① | GET | `/api/usage/token` | TokenAuthReadOnly | 查 token 的累计已用/剩余/总额(原始 quota) | | ② | GET | `/api/log/token` | TokenAuthReadOnly | 查该 key 最近 1000 条调用日志 | | ③ | GET | `/v1/dashboard/billing/subscription` | TokenAuth | 查总额度上限(OpenAI 兼容,美元) | @@ -73,6 +82,45 @@ Authorization: Bearer sk-xxxxxxxxxxxx ## 2. 用量/额度查询接口详解 +### ⓪ GET `/api/status` - 站点配置与计费参数(完全公开) + +**鉴权**:无。挂载于 `router/api-router.go:26`,在鉴权中间件组之外,任何人(不持有任何 key)可直接访问。本清单中唯一无需凭证的接口。 + +**请求参数**:无 + +**请求示例**: + +``` +GET /api/status +``` + +**返回结构**(源码:`controller/misc.go:44` -> `GetStatus`):外层为 `{ success, message, data }`,`data` 含 60+ 字段(版本、登录方式、前端模块开关、公告等)。**与本项目相关的计费字段**: + +| 字段 | 类型 | 说明 | 实测值(2026-09-09) | +|------|------|------|------| +| `quota_per_unit` | float | **QuotaPerUnit 当前生效值**。quota ÷ 该值 = 美元。管理员可改,工具不得硬编码 | `500000` | +| `quota_display_type` | string | 站点额度展示模式:`USD` / `CNY` / `TOKENS`。影响 billing 接口(③④)的语义 | `"USD"` | +| `usd_exchange_rate` | float | USD→CNY 展示汇率(`quota_display_type=CNY` 时 billing 接口用它换算) | `6.8` | +| `display_in_currency` | bool | 是否以货币展示额度(旧字段,兼容保留) | `true` | +| `custom_currency_symbol` | string | 自定义货币符号(`¤` = 未自定义) | `"¤"` | +| `custom_currency_exchange_rate` | float | 自定义货币汇率 | `1` | +| `price` | float | 充值价格(人民币/单位),站点充值汇率相关 | `6.8` | +| `version` | string | NewAPI 版本号 | `"v1.0.0-rc.35"` | + +**对本项目的意义**: + +1. **校准换算比率**:`quota_per_unit` 是工具换算美元的唯一权威来源——管理员改配置后,工具启动时拉取 + 定期校验即可自动跟随,不会算错 +2. **感知展示模式切换**:若管理员把 `quota_display_type` 切到 `CNY` 或 `TOKENS`,billing 接口(③④)返回的数字语义会变(CNY 是先 ÷QuotaPerUnit 再 ×`usd_exchange_rate`;TOKENS 直接返回 token 数),工具可提前探测到,避免拿旧逻辑解析新语义 +3. **零成本**:完全公开、无鉴权、无调用门槛,可放心定期轮询 + +**注意事项**: + +- 返回字段很多且含大量前端 UI 配置(`HeaderNavModules`、`SidebarModulesAdmin` 等),解析时只取计费相关字段,其余忽略 +- 字段集合随版本演进(如开启公告开关会追加 `announcements`),解析需容忍未知字段 +- 该接口不泄露敏感信息(secret 类 OptionMap 键已被 NewAPI 过滤),这也是它敢完全公开的原因 + +--- + ### ① GET `/api/usage/token` - token 级用量汇总 **鉴权**:`TokenAuthReadOnly()`(宽松版,key 过期/耗尽也能查) @@ -364,7 +412,7 @@ total_usage = 134683.8914 美分 ÷ 100 = 1346.84 美元 ### 核心换算公式 ``` -原始 quota ÷ 500000 = 美元 +原始 quota ÷ quota_per_unit(/api/status 实时读取,当前站点值 500000)= 美元 ``` ### 实测数据验证 @@ -423,7 +471,9 @@ total_usage = 134683.8914 美分 ÷ 100 = 1346.84 美元 | billing 查询 | `controller/billing.go:75` | `GetUsage` | | 显示开关 | `common/constants.go` | `DisplayTokenStatEnabled` | | 日志上限 | `common/constants.go` | `MaxRecentItems = 1000` | -| 配额单位 | `common/constants.go` | `QuotaPerUnit`(默认 500000) | +| 配额单位 | `common/constants.go:22` | `QuotaPerUnit`(var,默认 500000,`// $0.002 / 1K tokens`,可被 OptionMap 覆盖) | +| 配额单位(覆盖入口) | `model/option.go:605` | `case "QuotaPerUnit"` 运行时改写 | +| 站点配置暴露 | `controller/misc.go:44` + `router/api-router.go:26` | `GetStatus`(`/api/status`,无鉴权,含 `quota_per_unit`) | | token 更新 | `model/token.go` | `Update()`(Select 不含 used_quota) | | 额度增减 | `model/token.go` | `increaseTokenQuota` / `decreaseTokenQuota` | diff --git a/docs/bench交互设计文档.md b/docs/bench交互设计文档.md index 0fc1b1b..c7300f8 100644 --- a/docs/bench交互设计文档.md +++ b/docs/bench交互设计文档.md @@ -8,20 +8,21 @@ ## 1. 项目定位 -替代 Postman 的专用调试台(bench):左侧选接口 → 右侧 tab 内看文档、填 key、发请求、看原始响应。对接 5 个接口,服务 karpool 拼车项目的接口调试与换算验证。 +替代 Postman 的专用调试台(bench):左侧选接口 → 右侧 tab 内看文档、填 key、发请求、看原始响应。对接 6 个接口,服务 karpool 拼车项目的接口调试与换算验证。 **接口清单**(数据源:`docs/NewAPI-sk-key接口文档.md`,按此顺序展示,不分组): | 序 | 路径 | 鉴权 | 响应特征 | |---|------|------|---------| +| 0 | `GET /api/status` | 无(完全公开) | 站点配置与计费参数:`quota_per_unit`(换算比率权威来源)、`quota_display_type`、汇率等 | | 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 并行对账是核心工作流。 +> 0 完全公开(无需任何凭证,面板隐藏 sk-key 输入行);4、5 宽松鉴权(key 过期/耗尽仍可查);1~3 严格鉴权(key 无效直接被拒)。tab 内标出该差异,避免误判「接口坏了」。 +> 4 与 2 构成换算交叉验证对(total_used ÷ 500000 ≈ total_usage ÷ 100),Tabs 并行对账是核心工作流。0 是换算比率的权威来源(quota_per_unit 可被管理员修改,500000 只是默认值)。 --- @@ -52,7 +53,7 @@ 无分组无编号,按清单顺序) ``` -- **左 sider**(~300px):接口列表,**不分组、不编号**,按 §1 清单顺序平铺 5 个路径;选中态高亮 +- **左 sider**(~300px):接口列表,**不分组、不编号**,按 §1 清单顺序平铺 6 个路径;选中态高亮 - **右主区**:`t-tabs` 顶部,激活 tab 内容 = 请求行(method + 路径 + 鉴权徽标)→ 调试行(sk-key 输入 + 发送按钮)→ 响应区(元信息 + 虚拟滚动 JSON) - 暗色默认(TDesign dark theme + CSS 变量) diff --git a/src/api/endpoints.spec.ts b/src/api/endpoints.spec.ts index 2623365..871c6b6 100644 --- a/src/api/endpoints.spec.ts +++ b/src/api/endpoints.spec.ts @@ -3,17 +3,18 @@ import { describe, expect, it } from 'vitest'; import { endpointList, findEndpointByPath } from './endpoints'; describe('endpointList 数据完整性', () => { - it('恰好 5 个接口(设计文档 §1 定案的对接清单)', () => { - expect(endpointList).toHaveLength(5); + it('恰好 6 个接口(设计文档 §1 定案的对接清单)', () => { + expect(endpointList).toHaveLength(6); }); it('路径唯一(path 是 tab / queryKey 的唯一标识)', () => { const pathList = endpointList.map((endpoint) => endpoint.path); - expect(new Set(pathList).size).toBe(5); + expect(new Set(pathList).size).toBe(6); }); it('顺序与用户指定清单一致', () => { expect(endpointList.map((endpoint) => endpoint.path)).toEqual([ + '/api/status', '/v1/dashboard/billing/subscription', '/v1/dashboard/billing/usage', '/v1/models', @@ -25,13 +26,14 @@ describe('endpointList 数据完整性', () => { it('全部为 GET 且带鉴权元数据', () => { for (const endpoint of endpointList) { expect(endpoint.method).toBe('GET'); - expect(['strict', 'lenient']).toContain(endpoint.authMode); + expect(['strict', 'lenient', 'none']).toContain(endpoint.authMode); expect(endpoint.authMiddlewareName).toBeTruthy(); } }); - it('严格/宽松鉴权分布正确(前 3 严格,后 2 宽松)', () => { + it('鉴权模式分布正确(公开 1 + 严格 3 + 宽松 2)', () => { expect(endpointList.map((endpoint) => endpoint.authMode)).toEqual([ + 'none', 'strict', 'strict', 'strict', diff --git a/src/api/endpoints.ts b/src/api/endpoints.ts index db80c2c..4bc7171 100644 --- a/src/api/endpoints.ts +++ b/src/api/endpoints.ts @@ -1,17 +1,17 @@ /** - * 5 个对接接口的静态元数据 + * 对接接口的静态元数据 * 数据源:docs/NewAPI-sk-key接口文档.md(源码实证调研) * 顺序即 sider / tab 展示顺序(设计文档 §1 定案,不分组不编号) */ -export type AuthMode = 'strict' | 'lenient'; +export type AuthMode = 'strict' | 'lenient' | 'none'; export interface EndpointDefinition { /** 接口路径(唯一标识,同时用作 tab 标签与 queryKey 组成部分) */ readonly path: string; /** HTTP 方法(当前全部为 GET,保留扩展位) */ readonly method: 'GET'; - /** 鉴权模式:strict = TokenAuth(key 无效/过期/耗尽直接被拒);lenient = TokenAuthReadOnly(宽松,废 key 也能查) */ + /** 鉴权模式:strict = TokenAuth(key 无效/过期/耗尽直接被拒);lenient = TokenAuthReadOnly(宽松,废 key 也能查);none = 完全公开(无需任何凭证) */ readonly authMode: AuthMode; /** 鉴权中间件名(展示用) */ readonly authMiddlewareName: string; @@ -24,6 +24,35 @@ export interface EndpointDefinition { } export const endpointList: readonly EndpointDefinition[] = [ + { + path: '/api/status', + method: 'GET', + authMode: 'none', + authMiddlewareName: '无(公开接口)', + responseSummary: '站点配置与计费参数(含 quota_per_unit)', + notes: [ + '完全公开:无任何鉴权,不持有 sk-key 也能访问(本工具对接清单中唯一)', + 'quota_per_unit 是 QuotaPerUnit 当前生效值(默认 500000,源码 $0.002 / 1K tokens 锚点)——quota ÷ 该值 = 美元,换算比率的唯一权威来源,管理员可改,不要硬编码', + '接口①④交叉实测吻合验证不了管理员是否改过此值:billing 接口内部就是拿 quota ÷ QuotaPerUnit 换算,同一常量自证', + 'quota_display_type 影响 billing 接口语义:USD 直接是美元;CNY 先 ÷QuotaPerUnit 再 ×usd_exchange_rate;TOKENS 是原始 token 数', + '返回 60+ 字段(版本、登录方式、前端模块开关、公告等),解析时只取计费相关字段,其余忽略;字段集合随版本演进,需容忍未知字段', + ], + sampleResponse: { + success: true, + message: '', + data: { + quota_per_unit: 500000, + quota_display_type: 'USD', + usd_exchange_rate: 6.8, + display_in_currency: true, + custom_currency_symbol: '¤', + custom_currency_exchange_rate: 1, + price: 6.8, + system_name: 'AI Gateway', + version: 'v1.0.0-rc.35', + }, + }, + }, { path: '/v1/dashboard/billing/subscription', method: 'GET', diff --git a/src/components/endpoint-info-line.vue b/src/components/endpoint-info-line.vue index 23a49cd..ea52610 100644 --- a/src/components/endpoint-info-line.vue +++ b/src/components/endpoint-info-line.vue @@ -1,18 +1,33 @@ @@ -186,6 +193,12 @@ function downloadResponseAsFile(): void { max-width: 480px; } +.no-key-hint { + align-self: center; + font-size: 12.5px; + color: var(--td-text-color-placeholder); +} + .response-section { flex: 1; min-height: 220px; /* 响应区保底高度:窄窗口下不被上部固定分区压缩到不可用 */ diff --git a/src/components/side-nav.vue b/src/components/side-nav.vue index 3504956..3abfcfb 100644 --- a/src/components/side-nav.vue +++ b/src/components/side-nav.vue @@ -1,6 +1,6 @@