Compare commits

...
1 Commits
Author SHA1 Message Date
skycurtain c5f7d93d67 feat(api): 实装三个公开接口——groups/perf-metrics/rankings(全站视角数据)
- 新增 /api/user/groups:用户分组与倍率(未启用分组时为空对象),与 /api/pricing group_ratio 互补
- 新增 /api/perf-metrics/summary:全站模型服务质量(延迟/成功率/TPS/小时级序列),hours 参数;对拼车工具补「模型健康度」视角
- 新增 /api/rankings:全站模型用量排行(名次/份额/环比增长),period 参数;车长选主推模型的数据依据
- 三者均 authMode none,公开接口组置于清单最前;/api/status 的「唯一」表述改为公开接口组之一
- spec 断言 6→9(路径顺序、鉴权分布 none×4+strict×3+lenient×2);交互设计文档 §1 清单重排补全站视角定位说明;README 计数更新
- 验证:test 89/89 + type-check + lint + build 四绿
2026-09-10 00:47:30 +08:00
4 changed files with 104 additions and 16 deletions
+1 -1
View File
@@ -35,7 +35,7 @@ bun run build # type-check + vite build(dist/)
## 使用 ## 使用
1. 左侧选择接口(6 个,平铺不分组)→ 右侧开 tab 1. 左侧选择接口(9 个,平铺不分组)→ 右侧开 tab
2. tab 内展开「注意事项」看该接口的实证调研坑文档 2. tab 内展开「注意事项」看该接口的实证调研坑文档
3. 填 sk-key(每 tab 独立、仅存内存、刷新即重置,不落盘;切换 tab 输入保留) 3. 填 sk-key(每 tab 独立、仅存内存、刷新即重置,不落盘;切换 tab 输入保留)
4. 点「发送请求」→ 响应区显示元信息(状态/耗时/字符数)+ 虚拟滚动高亮 JSON 4. 点「发送请求」→ 响应区显示元信息(状态/耗时/字符数)+ 虚拟滚动高亮 JSON
+13 -10
View File
@@ -8,21 +8,24 @@
## 1. 项目定位 ## 1. 项目定位
替代 Postman 的专用调试台(bench):左侧选接口 → 右侧 tab 内看文档、填 key、发请求、看原始响应。对接 6 个接口,服务 karpool 拼车项目的接口调试与换算验证。 替代 Postman 的专用调试台(bench):左侧选接口 → 右侧 tab 内看文档、填 key、发请求、看原始响应。对接 9 个接口,服务 karpool 拼车项目的接口调试与换算验证。
**接口清单**(数据源:`docs/NewAPI-sk-key接口文档.md`,按此顺序展示,不分组): **接口清单**(数据源:`docs/NewAPI-sk-key接口文档.md`,按此顺序展示,不分组):
| 序 | 路径 | 鉴权 | 响应特征 | | 序 | 路径 | 鉴权 | 响应特征 |
|---|------|------|---------| |---|------|------|---------|
| 0 | `GET /api/status` | 无(完全公开) | 站点配置与计费参数:`quota_per_unit`(换算比率权威来源)、`quota_display_type`、汇率等 | | 0 | `GET /api/user/groups` | 无(完全公开) | 用户分组与倍率列表;未启用分组时为空对象 |
| 1 | `GET /v1/dashboard/billing/subscription` | TokenAuth(严格) | 小 JSON,美元额度(无限额时 1 亿占位) | | 1 | `GET /api/perf-metrics/summary` | 公开门控(默认无需凭证) | 全站模型服务质量:延迟/成功率/TPS/小时级序列;`hours` 参数 |
| 2 | `GET /v1/dashboard/billing/usage` | TokenAuth(严格) | 小 JSON,累计美分 | | 2 | `GET /api/rankings` | 公开门控(默认无需凭证) | 全站模型用量排行:名次/份额/环比增长;`period` 参数 |
| 3 | `GET /v1/models` | TokenAuth(严格) | 中等 JSON,模型数组 | | 3 | `GET /api/status` | 无(完全公开) | 站点配置与计费参数:`quota_per_unit`(换算比率权威来源)、`quota_display_type`、汇率等 |
| 4 | `GET /api/usage/token` | TokenAuthReadOnly(宽松) | 小 JSON,累计 quota | | 4 | `GET /v1/dashboard/billing/subscription` | TokenAuth(严格) | 小 JSON,美元额度(无限额时 1 亿占位) |
| 5 | `GET /api/log/token` | TokenAuthReadOnly(宽松) | **大响应**:最近 1000 条日志,pretty-print 2~4MB | | 5 | `GET /v1/dashboard/billing/usage` | TokenAuth(严格) | 小 JSON,累计美分 |
| 6 | `GET /v1/models` | TokenAuth(严格) | 中等 JSON,模型数组 |
| 7 | `GET /api/usage/token` | TokenAuthReadOnly(宽松) | 小 JSON,累计 quota |
| 8 | `GET /api/log/token` | TokenAuthReadOnly(宽松) | **大响应**:最近 1000 条日志,pretty-print 2~4MB |
> 0 完全公开(无需任何凭证,面板隐藏 sk-key 输入行);4、5 宽松鉴权(key 过期/耗尽仍可查);1~3 严格鉴权(key 无效直接被拒)。tab 内标出该差异,避免误判「接口坏了」。 > 0~3 完全公开或默认公开(面板隐藏 sk-key 输入行);7、8 宽松鉴权(key 过期/耗尽仍可查);4~6 严格鉴权(key 无效直接被拒)。tab 内标出该差异,避免误判「接口坏了」。
> 4 与 2 构成换算交叉验证对(total_used ÷ 500000 ≈ total_usage ÷ 100),Tabs 并行对账是核心工作流。0 是换算比率的权威来源(quota_per_unit 可被管理员修改,500000 只是默认值)。 > 7 与 5 构成换算交叉验证对(total_used ÷ 500000 ≈ total_usage ÷ 100),Tabs 并行对账是核心工作流。3 是换算比率的权威来源(quota_per_unit 可被管理员修改,500000 只是默认值)。1、2 是「全站视角」数据——单个 sk-key 日志看不到的服务质量与用量排行,服务模型健康度判断。
--- ---
@@ -53,7 +56,7 @@
无分组无编号,按清单顺序) 无分组无编号,按清单顺序)
``` ```
- **左 sider**(~300px):接口列表,**不分组、不编号**,按 §1 清单顺序平铺 6 个路径;选中态高亮 - **左 sider**(~300px):接口列表,**不分组、不编号**,按 §1 清单顺序平铺 9 个路径;选中态高亮
- **右主区**:`t-tabs` 顶部,激活 tab 内容 = 请求行(method + 路径 + 鉴权徽标)→ 调试行(sk-key 输入 + 发送按钮)→ 响应区(元信息 + 虚拟滚动 JSON) - **右主区**:`t-tabs` 顶部,激活 tab 内容 = 请求行(method + 路径 + 鉴权徽标)→ 调试行(sk-key 输入 + 发送按钮)→ 响应区(元信息 + 虚拟滚动 JSON)
- 暗色默认(TDesign dark theme + CSS 变量) - 暗色默认(TDesign dark theme + CSS 变量)
+10 -4
View File
@@ -3,17 +3,20 @@ import { describe, expect, it } from 'vitest';
import { endpointList, findEndpointByPath } from './endpoints'; import { endpointList, findEndpointByPath } from './endpoints';
describe('endpointList 数据完整性', () => { describe('endpointList 数据完整性', () => {
it('恰好 6 个接口(设计文档 §1 定案的对接清单)', () => { it('恰好 9 个接口(设计文档 §1 定案的对接清单)', () => {
expect(endpointList).toHaveLength(6); expect(endpointList).toHaveLength(9);
}); });
it('路径唯一(path 是 tab / queryKey 的唯一标识)', () => { it('路径唯一(path 是 tab / queryKey 的唯一标识)', () => {
const pathList = endpointList.map((endpoint) => endpoint.path); const pathList = endpointList.map((endpoint) => endpoint.path);
expect(new Set(pathList).size).toBe(6); expect(new Set(pathList).size).toBe(9);
}); });
it('顺序与用户指定清单一致', () => { it('顺序与用户指定清单一致', () => {
expect(endpointList.map((endpoint) => endpoint.path)).toEqual([ expect(endpointList.map((endpoint) => endpoint.path)).toEqual([
'/api/user/groups',
'/api/perf-metrics/summary',
'/api/rankings',
'/api/status', '/api/status',
'/v1/dashboard/billing/subscription', '/v1/dashboard/billing/subscription',
'/v1/dashboard/billing/usage', '/v1/dashboard/billing/usage',
@@ -31,8 +34,11 @@ describe('endpointList 数据完整性', () => {
} }
}); });
it('鉴权模式分布正确(公开 1 + 严格 3 + 宽松 2)', () => { it('鉴权模式分布正确(公开 4 + 严格 3 + 宽松 2)', () => {
expect(endpointList.map((endpoint) => endpoint.authMode)).toEqual([ expect(endpointList.map((endpoint) => endpoint.authMode)).toEqual([
'none',
'none',
'none',
'none', 'none',
'strict', 'strict',
'strict', 'strict',
+80 -1
View File
@@ -2,6 +2,9 @@
* 对接接口的静态元数据 * 对接接口的静态元数据
* 数据源:docs/NewAPI-sk-key接口文档.md(源码实证调研) * 数据源:docs/NewAPI-sk-key接口文档.md(源码实证调研)
* 顺序即 sider / tab 展示顺序(设计文档 §1 定案,不分组不编号) * 顺序即 sider / tab 展示顺序(设计文档 §1 定案,不分组不编号)
*
* 鉴权三态:none = 完全公开(无需任何凭证)→ strict = TokenAuth → lenient = TokenAuthReadOnly
* 公开接口组(/api/user/groups、/api/perf-metrics/summary、/api/rankings、/api/status)排在清单最前
*/ */
export type AuthMode = 'strict' | 'lenient' | 'none'; export type AuthMode = 'strict' | 'lenient' | 'none';
@@ -24,6 +27,82 @@ export interface EndpointDefinition {
} }
export const endpointList: readonly EndpointDefinition[] = [ export const endpointList: readonly EndpointDefinition[] = [
{
path: '/api/user/groups',
method: 'GET',
authMode: 'none',
authMiddlewareName: '无(公开接口)',
responseSummary: '用户分组与倍率列表(未启用分组时为空对象)',
notes: [
'完全公开:无任何鉴权中间件(本工具对接清单中三个免凭证接口之一)',
'返回站点配置的用户分组(default / vip 等)及其倍率;站点未启用分组时 data 为空对象',
'用途:解释「同一模型为什么计费不同」——分组倍率会影响同一模型的实际消耗',
'与 /api/pricing 的 group_ratio 字段互补:本接口是分组清单,pricing 是模型定价',
],
sampleResponse: { success: true, message: '', data: {} },
},
{
path: '/api/perf-metrics/summary',
method: 'GET',
authMode: 'none',
authMiddlewareName: 'HeaderNavModulePublicOrUserAuth(默认公开)',
responseSummary: '全站模型服务质量汇总(延迟/成功率/TPS)',
notes: [
'公开接口:管理员未在 HeaderNav 中开启 pricing 模块的 requireAuth 时无需任何凭证',
'query 参数 hours:统计窗口小时数(默认 24)',
'返回每个模型的 avg_latency_ms(平均延迟)、success_rate(成功率 %)、avg_tps(每秒 token 数)、recent_success_series(按小时采样的成功率时间序列)',
'对拼车工具的意义:单个 sk-key 日志看不到全局服务质量,本接口补上「模型当前健康度」视角——伙伴反馈「慢/报错多」时用它定位是模型问题还是自身问题',
'明细版 /api/perf-metrics?model=xxx 需 query 参数(model 必填,另支持 group/hours),面板暂不支持 query 输入,可用上方注意事项了解参数后经代理手动拼接',
],
sampleResponse: {
success: true,
data: {
models: [
{
model_name: 'gpt-5.6-sol',
avg_latency_ms: 16823,
success_rate: 94.73,
avg_tps: 25.81,
recent_success_series: [
{ ts: 1788872400, success_rate: 100 },
{ ts: 1788876000, success_rate: 99.16 },
],
},
],
},
},
},
{
path: '/api/rankings',
method: 'GET',
authMode: 'none',
authMiddlewareName: 'HeaderNavModulePublicOrUserAuth(默认公开)',
responseSummary: '全站模型用量排行(名次/份额/增长)',
notes: [
'公开接口:同 perf-metrics,受 HeaderNav 模块开关控制,默认无需凭证',
'query 参数 period:统计周期(默认 week)',
'返回每个模型的 rank(名次)、previous_rank(上期名次)、total_tokens(总用量)、share(份额)、growth_pct(环比增长 %)',
'对拼车工具的意义:看全站都在用什么模型——车长选「主推模型」(便宜且稳定)的数据依据;配合 perf-metrics 的成功率可识别衰退模型',
],
sampleResponse: {
success: true,
data: {
models: [
{
rank: 1,
previous_rank: 2,
model_name: 'gpt-5.6-sol',
vendor: 'OpenAI',
vendor_icon: 'OpenAI',
category: 'all',
total_tokens: 1583845480,
share: 0.6549,
growth_pct: 60.4253,
},
],
},
},
},
{ {
path: '/api/status', path: '/api/status',
method: 'GET', method: 'GET',
@@ -31,7 +110,7 @@ export const endpointList: readonly EndpointDefinition[] = [
authMiddlewareName: '无(公开接口)', authMiddlewareName: '无(公开接口)',
responseSummary: '站点配置与计费参数(含 quota_per_unit)', responseSummary: '站点配置与计费参数(含 quota_per_unit)',
notes: [ notes: [
'完全公开:无任何鉴权,不持有 sk-key 也能访问(本工具对接清单中唯一)', '完全公开:无任何鉴权,不持有 sk-key 也能访问(对接清单公开接口组之一)',
'quota_per_unit 是 QuotaPerUnit 当前生效值(默认 500000,NewAPI 以 $0.002 / 1K tokens 的基准定价为锚点设计)——quota ÷ 该值 = 美元,换算比率的唯一权威来源,管理员可改,不要硬编码', 'quota_per_unit 是 QuotaPerUnit 当前生效值(默认 500000,NewAPI 以 $0.002 / 1K tokens 的基准定价为锚点设计)——quota ÷ 该值 = 美元,换算比率的唯一权威来源,管理员可改,不要硬编码',
'想用 billing 接口(subscription / usage)反推换算比率不可靠:它们内部就是拿 quota ÷ QuotaPerUnit 换算的,站点改了配置它们会跟着变,永远「吻合」——比率唯一权威只有本接口的 quota_per_unit', '想用 billing 接口(subscription / usage)反推换算比率不可靠:它们内部就是拿 quota ÷ QuotaPerUnit 换算的,站点改了配置它们会跟着变,永远「吻合」——比率唯一权威只有本接口的 quota_per_unit',
'quota_display_type 影响 /v1/dashboard/billing/subscription 与 /v1/dashboard/billing/usage 的语义:USD 直接是美元;CNY 先 ÷QuotaPerUnit 再 ×usd_exchange_rate;TOKENS 是原始 token 数', 'quota_display_type 影响 /v1/dashboard/billing/subscription 与 /v1/dashboard/billing/usage 的语义:USD 直接是美元;CNY 先 ÷QuotaPerUnit 再 ×usd_exchange_rate;TOKENS 是原始 token 数',