docs: 设计文档体系落定(七篇定稿)
- docs/design:四张表字段设计 · 交互设计(方向层 + 细节层)· API 契约 · 同步引擎设计 - docs/upstream:sk-key 接口实证(430 行)· 接口范围(9 个接口) - README(全景入口)· AGENTS.md(AI 协作者指引) - 废弃旧稿已删除(不随仓库留存)
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
# AGENTS.md
|
||||
|
||||
给 AI 协作者的仓库指引。**开始任何工作前,先读 [`README.md`](README.md)**——它是全景入口:项目是什么、为什么长这样、走到哪了。
|
||||
|
||||
## 协作约定(长期有效)
|
||||
|
||||
- **Git**:仅当项目作者明确说"提交 / commit"时才执行提交;`git push` 永远由项目作者手工执行,AI 绝不推送
|
||||
- **命名**:需求 / 设计 / 交互 / 技术方案 / 计划等**文档名**用中文(`README` / `AGENTS` / `LICENSE` 等约定俗成除外)、**目录**用英文;新文档按性质放入 `docs/design/`(我们定的方案与决策)或 `docs/upstream/`(NewAPI 的实证事实,不做设计决策),并同步更新 README 的「文档地图」;`docs/deprecated/` 只归档已废弃文档——**勿引用、勿作依据**
|
||||
- **文档状态**:头部表格标「状态」(草案 → 讨论中 → 待审阅 → 定稿(日期) → 已实现);设计文档定稿后不搬家、只改状态;被取代时删除(git 历史即考古),尚未入库的历史文档移入 `docs/deprecated/`
|
||||
- **部署**:项目作者不接触服务器,一切部署由实例运营方执行;交付物需保证「一条命令链」可跑
|
||||
|
||||
## 项目速览(速记)
|
||||
|
||||
- 针对 NewAPI 平台的 key 用量统计工具;**只有 sk-key,没有管理员账号**,一切数据走 key 级接口
|
||||
- 上游没有任何时间过滤接口 → 轮询 `/api/log/token` + `request_id` 去重攒本地库,任一时段用量 = 本地聚合
|
||||
- 数据模型四张表:`api_keys` · `usage_logs` · `purchases` · `qpu_history`(**四张表设计均已定稿**);四张表各有一篇独立表字段设计文档(`docs/design/`)
|
||||
- 脚手架已落地:前后端独立部署、后端单进程(API + 同步引擎同进程、单实例)
|
||||
|
||||
## 命令
|
||||
|
||||
monorepo(Bun workspaces:`apps/web` · `apps/server` · `packages/shared`)——根目录执行:
|
||||
|
||||
```bash
|
||||
bun install # 安装全部依赖(一次搞定)
|
||||
|
||||
bun run dev # 并行起前端(Vite :5173)与后端(Hono :3000)
|
||||
bun run dev:web # 只起前端
|
||||
bun run dev:server # 只起后端(--hot 热重载)
|
||||
|
||||
bun run build # 构建前端产物(apps/web/dist)
|
||||
bun run test # 前端单测(Vitest)
|
||||
bun run lint # 前端 lint(oxlint + eslint)
|
||||
bun run type-check # 类型检查(vue-tsc + tsc)
|
||||
|
||||
bun run db:generate # 生成 Drizzle 迁移(apps/server/drizzle)
|
||||
bun run db:migrate # 执行迁移
|
||||
bun run db:push # 直接推 schema(开发期)
|
||||
```
|
||||
|
||||
- 环境变量:复制 `.env.example` 为 `.env` 并填值(`DATABASE_URL` / `MANAGE_TOKEN` / `KEYS_MASTER_KEY` / `NEWAPI_BASE`)
|
||||
- 目录速记:前端 `apps/web/src/{api,router,views}` · 后端 `apps/server/src/{db,routes,sync,lib}` · 契约类型在 `packages/shared`
|
||||
- 开发期前端 `/api` 由 Vite 代理到 :3000(`apps/web/vite.config.ts`);生产由同域反代承担(见 README 工程注记)
|
||||
@@ -0,0 +1,99 @@
|
||||
# new-api-key-usage
|
||||
|
||||
> 针对 NewAPI 平台的 key 用量统计工具
|
||||
|
||||
定时用每个 sk-key 轮询网关日志接口,按 `request_id` 去重,攒全量日志到本地数据库;任意时间范围的用量查询,即一条本地聚合查询。
|
||||
|
||||
| 项目 | 内容 |
|
||||
|------|------|
|
||||
| 状态 | 实现起步(脚手架已落地:`apps/web` · `apps/server` · `packages/shared`;设计文档全部定稿) |
|
||||
| 技术栈 | Bun + Hono + PostgreSQL · 前端 Vite + Vue 3 + naive-ui |
|
||||
| 部署 | 实例运营方提供的服务器;交付方式为推送仓库 / 产物,由运营方执行部署 |
|
||||
|
||||
## 文档地图
|
||||
|
||||
| 文档 | 什么时候读 | 状态 |
|
||||
|------|-----------|------|
|
||||
| [`docs/design/api_keys表字段设计.md`](docs/design/api_keys表字段设计.md) | 数据模型第一张表:字段、语义与决策理由 | 定稿(2026-09-11;09-13 修订 key 登记与表名、新增 `disabled_source`) |
|
||||
| [`docs/design/usage_logs表字段设计.md`](docs/design/usage_logs表字段设计.md) | 数据模型第二张表:日志明细、去重与写入策略 | 定稿(2026-09-11) |
|
||||
| [`docs/design/purchases表字段设计.md`](docs/design/purchases表字段设计.md) | 数据模型第三张表:购买记录(实付人民币的唯一落点、折算链第二条) | 定稿(2026-09-13;同日修订查看面金额口径) |
|
||||
| [`docs/design/qpu_history表字段设计.md`](docs/design/qpu_history表字段设计.md) | 数据模型第四张表:换算参数时间线(自动采样、只读) | 定稿(2026-09-13) |
|
||||
| [`docs/design/交互设计.md`](docs/design/交互设计.md) | 界面设计:方向层 + 细节层(跨页共性、逐页细节、状态与文案) | 定稿(2026-09-13) |
|
||||
| [`docs/design/API契约.md`](docs/design/API契约.md) | 前端 ↔ 服务端接口契约:约定层 + 逐页端点(前端实现看这篇) | 定稿(2026-09-13) |
|
||||
| [`docs/design/同步引擎设计.md`](docs/design/同步引擎设计.md) | 采集侧引擎:调度、每轮动作、失败处理、可观测、部署初始化(后端实现看这篇) | 定稿(2026-09-13) |
|
||||
| [`docs/upstream/NewAPI-sk-key接口文档.md`](docs/upstream/NewAPI-sk-key接口文档.md) | 查 sk-key 可用接口的字段级事实(源码实证,430 行) | 事实依据 |
|
||||
| [`docs/upstream/NewAPI-接口范围.md`](docs/upstream/NewAPI-接口范围.md) | 本项目对接的接口范围(9 个接口的用途与鉴权) | 事实依据 |
|
||||
|
||||
- 命名与分区:**目录用英文、文档名用中文**;`docs/design/`(我们定的方案与决策)· `docs/upstream/`(NewAPI 的实证事实,不做设计决策)· `docs/deprecated/`(**已废弃的历史文档,只存档、不引用、不作依据**)· 未来的 `docs/delivery/`(给部署方看的部署文档)
|
||||
- 生命周期:设计文档定稿后不搬家、只改状态(草案 → 讨论中 → 待审阅 → 定稿(日期) → 已实现);被取代时删除(git 历史即考古),尚未入库的历史文档移入 `docs/deprecated/`
|
||||
|
||||
## 这是什么项目
|
||||
|
||||
- 名字直述用途:NewAPI 网关下按 key 查询用量(key usage)
|
||||
- 全场景统一小写 kebab-case:仓库名 / 包名 / 文档标题一致,无大写变体
|
||||
|
||||
## 背景故事
|
||||
|
||||
- 本工具源于一个真实场景:[NewAPI](https://github.com/QuantumNous/new-api)(Go + GORM 的开源 LLM 网关)实例提供订阅服务,多个使用者通过分发 API key 共享同一份订阅额度
|
||||
- 项目作者是该实例的**普通用户**:购买了订阅额度,创建多个 sk- API key 分发给多名使用者分摊使用成本——**非商业,纯成本分担**
|
||||
- 关键身份约束:项目作者**只有 sk-key,没有实例的管理员账号**。因此:
|
||||
- 一切数据获取只能走 key 级接口(账号级接口用 sk-key 访问会 401)
|
||||
- 工具部署在**实例运营方提供的服务器**上,作者不直接接触服务器——**交付方式是推送仓库 / 产物,由运营方执行部署**
|
||||
|
||||
## 核心需求(v2 定稿)
|
||||
|
||||
| # | 需求 | 说明 |
|
||||
|---|------|------|
|
||||
| 1 | key 用量查询 | 输入 sk-key + 时间范围 → 查模型请求日志与用量。支持一次输入多个 key 合并查询(项目作者借此获得全员视角) |
|
||||
| 2 | key 登记 | 管理通道手动登记:登记后入库并加入轮询;查看面凭已登记的 key 查询(纯只读) |
|
||||
|
||||
**明确不做的**(v1 → v2 的收缩边界):费用分摊计算(后议;折算基础已就绪——两张折算表设计见文档地图)、计费周期管理、多组支持、登录页、admin 角色、会话记忆。
|
||||
|
||||
## 架构的根本动因:为什么必须本地攒数据
|
||||
|
||||
这不是优化选项,是上游接口的物理约束决定的**唯一路径**:
|
||||
|
||||
1. sk-key 可用的接口(`/api/usage/token`、`/api/log/token` 等)返回的都是**累计值**或**最近 1000 条**,没有任何接口支持时间范围过滤
|
||||
2. 真正适合算"当月用量"的接口(`/api/log/self/stat`,支持时间范围 + quota 汇总)需要账号登录态,sk-key 访问 401——这是"按时间段统计用量"无法纯客户端实现的**核心障碍**
|
||||
3. `/api/log/token` 硬上限 1000 条、不支持任何参数(分页 / limit / 时间过滤全被忽略)
|
||||
|
||||
→ 结论:轮询同步 + `request_id` 去重入库 + 本地数据库。上游接口的完整实证(含源码出处)见 [`docs/upstream/NewAPI-sk-key接口文档.md`](docs/upstream/NewAPI-sk-key接口文档.md)。
|
||||
|
||||
## 已定技术决策
|
||||
|
||||
| # | 项 | 决策 | 备注 |
|
||||
|---|----|------|------|
|
||||
| 1 | 部署形态 | 实例运营方提供的服务器,标准前后端 | 弃 Cloudflare Workers(v1 方案),因运营方愿意提供服务器资源 |
|
||||
| 2 | 运行时 / 框架 | **Bun + Hono** | 单进程部署、原生 TS;Hono 轻量路由 |
|
||||
| 3 | 数据库 | **PostgreSQL** | 服务器现有实例 |
|
||||
| 4 | 鉴权 | 无 admin、无登录页;查看面凭已登记的 key 查询;管理面由 env 口令 | 写操作全部收拢在管理面(key 管理 + 折算数据);key 失效由轮询 401 自动摘除、管理面手动启停补位 |
|
||||
| 5 | 信任模型 | 实例运营方(= 网关管理员 = 服务器管理员)不是威胁对象——他们本就能看到一切;真正的防护对象是第三方拖库(sk-key 应加密存库,实现时落地) | 无法在对方控制的机器上对抗对方,接受此公理 |
|
||||
| 6 | 计费口径 | 展示 quota + $ 参考换算(÷500000),并支持折算为实付人民币 | 折算机制(两层时间线)见文档地图中的两张折算表设计 |
|
||||
| 7 | 代码组织 | 前后端分离 **monorepo**(Bun workspaces:`apps/web` · `apps/server` · `packages/shared`) | **前后端独立部署、分别更新**(2026-09-13 修订:前端静态产物独立托管;后端不托管前端产物);**后端单进程**(API + 同步引擎同进程、单实例);不引入 turbo 等编排工具 |
|
||||
| 8 | 数据库访问 | **Drizzle ORM + drizzle-kit** | TS 定义 schema;迁移由 drizzle-kit 生成 |
|
||||
| 9 | 前端 | **Vite + Vue 3** | 页面结构与 UI 组件库选型留技术稿定 |
|
||||
| 10 | 人民币折算 | 两层时间线(`quota_per_unit` 自动采样 + 购买记录人工登记),**按购买时刻取尺** | 整笔订阅一把尺;历史永远用当时的尺子量;见文档地图中的两张折算表设计 |
|
||||
| 11 | 管理通道 | `/manage/*` 独立路由 + env 口令;key 管理(登记 / 清单 / 启停)+ 购买记录的 CRUD + 换算参数时间线只读查看 | 本工具唯一写操作入口;查询域保持完全只读;见 [`docs/design/交互设计.md`](docs/design/交互设计.md) §0 |
|
||||
|
||||
## 关键事实清单(实测 / 源码确认,实现时不得违反)
|
||||
|
||||
> 出处均见 [`docs/upstream/NewAPI-sk-key接口文档.md`](docs/upstream/NewAPI-sk-key接口文档.md),此处仅索引。
|
||||
|
||||
1. **`QuotaPerUnit = 500000`**:quota ÷ 500000 = 额度美元(接口①与④交叉实测吻合)——**但它不是实付金额**:折算到实付人民币还需乘「购买汇率」(由运营方定价、可变,历史上调整过一次)。完整折算机制见文档地图中的两张折算表设计
|
||||
2. **去重必须用 `request_id`**:`/api/log/token` 返回的 `id` 是每次从 1 重编的展示序号,绝不能用于去重
|
||||
3. **`created_at` 是 UTC Unix 秒**(三字段交叉实测),北京时间输入需转换
|
||||
4. **1000 条硬上限**:窗口只保留最近 1000 条——实测当前站点的日志速率下约等于 3 天;5 分钟级同步余量充分,但同步停摆超过约 3 天会永久丢数据
|
||||
5. **`TokenAuthReadOnly`**:用量/日志接口用宽松鉴权,key 过期 / 额度耗尽仍可查询(用完也能对账);**显式禁用会被 401 拒绝**(2026-09-13 源码复核)
|
||||
6. **`DisplayTokenStatEnabled` 已开启**:billing 接口返回 token 级额度
|
||||
|
||||
## 工程注记
|
||||
|
||||
- CORS 必须由服务端代理解决(浏览器直连 NewAPI 网关会被拦)——**指上游网关**;本项目前后端之间的连线见下条
|
||||
- **前后端连线(2026-09-13 修订)**:**同域反代为主**(`/api/*` 反代到后端端口——浏览器视角同源,无 CORS、前端用相对路径)、**跨源直连为辅**(后端 CORS 白名单可配 + 前端运行时 API base;`X-Manage-Token` 自定义头会触发 OPTIONS 预检;凭证不进 Cookie,无需 credentials 模式)
|
||||
- bun runtime 跑 vue-tsc 存在已知 bug([bun#4754](https://github.com/oven-sh/bun/issues/4754)),类型检查与构建需解耦
|
||||
|
||||
## 下一步
|
||||
|
||||
1. **实现**:脚手架已落地(命令见 [`AGENTS.md`](AGENTS.md))——按 [`docs/design/API契约.md`](docs/design/API契约.md) 实现 11 个端点(查看面 `/usage-log` 先行),按 [`docs/design/同步引擎设计.md`](docs/design/同步引擎设计.md) 实现采集引擎
|
||||
2. **部署前确认清单**(开工前向实例运营方确认):服务器 OS / PostgreSQL 实例连接方式 / 有无反代与**能否配置 `/api` 反代规则** / Bun 可安装性 / **后端必须单实例运行**(同步引擎前提)
|
||||
3. 后议功能:充值折算、多订阅实测等见两张折算表设计文档的「待定事项」
|
||||
@@ -0,0 +1,10 @@
|
||||
# 已废弃文档归档
|
||||
|
||||
> **本目录只作存档**:其中的文档均已作废,**不要引用、不要作为任何设计依据**。
|
||||
|
||||
- 现行文档一律以 [`docs/design/`](../design/)(我们定的方案与决策)与 [`docs/upstream/`](../upstream/)(NewAPI 实证事实)为准,入口见仓库根 [`README.md`](../../README.md) 的「文档地图」
|
||||
- 本目录不随项目演进维护;仅供追溯历史
|
||||
|
||||
## 归档清单
|
||||
|
||||
(当前无归档文档)
|
||||
@@ -0,0 +1,271 @@
|
||||
# API 契约
|
||||
|
||||
> 前端 ↔ 服务端的接口契约:**约定层**(跨页共性)+ **逐页端点**(按 `docs/design/交互设计.md` 的页面清单推导)。只描述「前端能用到的 HTTP 接口」;上游侧调用不在本文(见 `docs/upstream/NewAPI-接口范围.md`)。
|
||||
|
||||
| 项目 | 内容 |
|
||||
|------|------|
|
||||
| 状态 | 定稿(2026-09-13:约定层 23 条 + 逐页端点——四页推完、`/dashboard` 二期占位) |
|
||||
| 关联 | `docs/design/交互设计.md`(页面清单与职责)· 四张表字段设计(数据形状)· `docs/design/交互设计.md` §0(管理面边界与口令门帘) |
|
||||
| 范围 | 路径、鉴权形态、时间 / 数字形态、分页、错误与响应形状、逐页端点;校验细节、存储、调度归技术稿 |
|
||||
| 不含 | 上游 NewAPI 的调用方式(同步引擎设计)· 展示文案与交互细节(交互设计细节层) |
|
||||
|
||||
---
|
||||
|
||||
## 1. 约定层(跨页共性)
|
||||
|
||||
> 2026-09-13 逐条过审:第 3 / 9 / 14 / 18 / 19 / 20 / 22 条专项讨论后落定,其余默认通过。本文已定稿,后续如需修订就地更新并注明日期。
|
||||
|
||||
### 1.1 路径与命名
|
||||
|
||||
| # | 约定 | 理由 |
|
||||
|---|------|------|
|
||||
| 1 | 查看面 API 前缀 `/api/*`;管理面 API 前缀 `/api/manage/*` | 两套鉴权模型物理隔离,口径一刀切在路径上;前端路由(`/usage-log`、`/manage/*`)与 API 路径不冲突 |
|
||||
| 2 | 不加版本号前缀(不做 `/api/v1`) | 单实例小工具、前后端同仓同发,无多版本并行需求 |
|
||||
| 3 | JSON 字段一律 `snake_case`,与库表列名 / 上游字段同名同形;前端 TS 类型不做 camelCase 转换 | 全链路零改名,看文档、看库、看日志是同一套词;`other`(jsonb 透传)内层无法转换,转换方案必然产生两套命名混用 |
|
||||
| 4 | 字段命名以事实为准(如换算值带 `_usd` 后缀);「不标注币种」是展示层措辞约定,不约束字段名 | 契约给开发者看,语义准确优先 |
|
||||
|
||||
### 1.2 鉴权与身份
|
||||
|
||||
| # | 约定 | 理由 |
|
||||
|---|------|------|
|
||||
| 5 | 查看面**无会话、无服务端状态**:每个查询请求自带 key 集合,服务端仅以 `key_hash` 命中判定(HMAC),不存、不回显 key 原文 | 与「无登录页、无账户体系、查询域完全平等」的既定边界一致;服务端不留任何使用者痕迹 |
|
||||
| 6 | 管理面单口令:每次请求携带 `X-Manage-Token: <env 口令>`,校验失败 → `401 manage_unauthorized`;**无登录端点、无令牌签发** | 口令门帘是「访问校验」而非会话;有会话就长出了用户体系的影子 |
|
||||
| 7 | **任何凭证不进 Cookie、不进 URL**(不进 access log / 浏览器历史 / Referer) | 不依赖 Cookie 即天然免疫 CSRF(跨站表单无法设置自定义头、JSON 体需预检);凭证不进 URL 是安全基线 |
|
||||
| 8 | 查看面对「未收录 / 已停用」的 key **不返回 HTTP 错误**:查询照常 200,per-key 状态在响应 `meta` 中反馈 | key 未收录是查询结果的一部分(交互设计 §0.2 的「未收录」提示),不是接口失败 |
|
||||
|
||||
### 1.3 时间与时区
|
||||
|
||||
| # | 约定 | 理由 |
|
||||
|---|------|------|
|
||||
| 9 | 时间一律 **ISO 8601 带显式偏移**:**输出**统一北京时间 `+08:00`(`+08:00` 是产品口径、非服务器时区);**输入宽容**——接受任何带偏移的 ISO 8601(含 `Z` / 毫秒),服务端按绝对时刻解析 | 使用者全是北京时间语境,输出即可直接展示(前端零时区转换);输入宽容免去前端格式化负担(`toISOString()` 的 `Z` 形式可直接提交);显式偏移杜绝歧义(2026-09-13 作者裁决) |
|
||||
| 10 | 所有时间范围左闭右开 `[start, end)` | 全项目期间惯例(`purchases` 设计 §3.4 同款) |
|
||||
| 11 | 时间戳类字段(`registered_at` / `log_synced_at` / `usage_synced_at` / `paid_at` / `effective_from`)同制输出 | 一处规则管全部,前端只有一个时间处理分支 |
|
||||
|
||||
### 1.4 数字与折算
|
||||
|
||||
| # | 约定 | 理由 |
|
||||
|---|------|------|
|
||||
| 12 | 整数(quota / token 计数)以 JSON number 传输,不做字符串化 | 最大量级 ~1e9(实测 `subscription_total = 8e8`),远小于 2^53 安全域 |
|
||||
| 13 | **折算一律在服务端完成**:折尺信息(购买期间 + QPU 时间线)只有服务端持有;聚合响应给折算数值,同时保留 quota 原值 | 前端拿不到折尺;折算语义(按购买时刻取尺、跨期按购买期间边界分段)是服务端职责,前端只消费结果 |
|
||||
| 14 | 服务端不做舍入、不做千分位/币种格式化:数值全精度回传 | 格式化归展示层,避免「服务端先舍入、前端再格式化」的双重失真(2026-09-13 作者裁决:保持全精度) |
|
||||
| 15 | 折算不落在日志行上、不新增日志列(沿用 `purchases` 设计 §3.2):日志响应只给 quota 原值与 `other` 原始内容 | 折算随时间线变化,任何固化到日志行的值都会与时间线脱钩 |
|
||||
|
||||
> **金额展示口径(2026-09-13 作者裁决)**:查看面的「消费」类金额一律为**折算实付人民币**(按购买期间分段,每段 `quota 占比 × 该笔实付`)——`purchases` 设计 §6 的「天然不含实付金额」边界就此放开;**仍不上查看面**的是实付总额、购买比率与购买记录本身(管理面独占)。已知代价:明细带 quota 列、QPU 公开,使用者理论上可从两者反推购买比率——接受。
|
||||
|
||||
### 1.5 查询形态与分页
|
||||
|
||||
| # | 约定 | 理由 |
|
||||
|---|------|------|
|
||||
| 16 | 明细排序固定 `created_at DESC`(不开放排序参数) | 日志天然时序;开放排序只增加索引负担 |
|
||||
| 17 | key 集合在请求内先去重、设数量上限(超限 → `400 invalid_params`) | 防御性校验:避免单请求打爆 `IN (...) + HMAC` 循环 |
|
||||
| 18 | 分页:请求 `limit` + `offset`,响应含 `total`(总条数);分页响应形态 `{ "items": [...], "total": N, "meta": {...} }` | 条数对使用者有信息价值(「这段时间共多少次调用」);时间范围过滤下深翻风险小、前端页码器直观(2026-09-13 裁决;cursor 方案已否决) |
|
||||
| 19 | 查看面查询**一律 POST + JSON 请求体**:key 集合与过滤参数统一进体(`{"keys": ["sk-…"], "start": …, "end": …, "type": [...]}`) | 多 key 数组最自然、复杂过滤(`type` 列表)好承载;密钥不进 URL、不进 access log(2026-09-13 裁决) |
|
||||
|
||||
### 1.6 响应与错误
|
||||
|
||||
| # | 约定 | 理由 |
|
||||
|---|------|------|
|
||||
| 20 | 成功 = 2xx + **裸数据体**(无 `{ data: ... }` 包装);失败 = 非 2xx + `{ "error": { "code": "...", "message": "..." } }` | 前端判断简单(`res.ok`);`code` 供分支文案,`message` 供人读(2026-09-13 作者裁决:保持此形态,信封方案已否决) |
|
||||
| 21 | `code` 为机器可读枚举:`invalid_params` · `manage_unauthorized` · `not_found` · `internal` · `key_rejected`(上游拒绝该 key;400)· `already_registered`(重复登记;409)· `upstream_unavailable`(上游故障 / 超时;502) | 按需增补;未知 code 前端兜底为通用错误文案(2026-09-13 扩展后三枚:前端可分清「输入错 / 重复 / 系统故障」三种处置) |
|
||||
| 22 | 查看面查询响应统一带 `meta` 块:`data_until`(参与 key 中**最慢**的 `log_synced_at`)、`served_at`、`keys[]`(per-key:`key_id` / `token_name` / `registered` / `status` / 各自的 `log_synced_at`;未收录项 = `{ registered: false, key_hint: "sk-…a1b2" }`) | `log_synced_at` 是「数据截止」唯一来源(`api_keys` 设计 §3.3);取最慢 = 声称的截止对每个 key 都成立——统一 tick 下正常差异为秒级(同一轮内完成先后),某 key 失败或停用才拉大,per-key 字段负责解释;`key_hint` 即时生成、不落库(`api_keys` §5 否决的是存库,不是响应携带);新登记尚未首同步的 key `log_synced_at` 为 `null`——顶层 `data_until` 取各 key 中**非 null 的最慢值**,全部未同步时为 `null` |
|
||||
| 23 | 参与 key 全部未收录时同样 200(空结果 + `meta.keys[]` 标未收录) | 与约定 8 一致:未收录是结果、不是错误;前端据此渲染「未收录」提示 |
|
||||
| 24 | 前后端连线(部署层约定):**同域反代为主**(`/api/*` 反代到后端——浏览器视角同源,无 CORS、前端用相对路径);**跨源直连为辅**——后端 CORS 白名单可配(允许 `X-Manage-Token` 等自定义头与 `OPTIONS` 预检),前端以**运行时**配置读取 API base(同一份产物可换环境) | 前后端独立部署(README 决策 7 修订);凭证不进 Cookie → CORS 无需 credentials 模式;运行时 base 免去「改地址要重新构建」(2026-09-13 作者裁决) |
|
||||
|
||||
**响应示例(形状示意;`items` 内字段级细节以逐页推导为准)**
|
||||
|
||||
```jsonc
|
||||
// 成功:查看面查询(分页 + meta)
|
||||
{
|
||||
"items": [
|
||||
{ "id": 8412, "key_id": 3, "created_at": "2026-09-11T19:37:12+08:00", "type": 2,
|
||||
"model_name": "gpt-5.6-terra", "quota": 12000 }
|
||||
],
|
||||
"total": 1873,
|
||||
"meta": {
|
||||
"data_until": "2026-09-13T21:10:00+08:00",
|
||||
"served_at": "2026-09-13T21:36:00+08:00",
|
||||
"keys": [
|
||||
{ "key_id": 3, "token_name": "小王", "registered": true, "status": "active", "log_synced_at": "2026-09-13T21:35:03+08:00" },
|
||||
{ "key_id": 7, "token_name": "小李", "registered": true, "status": "disabled", "log_synced_at": "2026-09-13T21:10:00+08:00" },
|
||||
{ "registered": false, "key_hint": "sk-…a1b2" }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// 失败:error 信封
|
||||
{ "error": { "code": "invalid_params", "message": "keys 数量超过上限(最多 20 个)" } }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 逐页端点
|
||||
|
||||
按 `交互设计.md` 的页面清单逐页推导,每页记录:端点(方法 / 路径 / 请求 / 响应 / 错误分支)+ 页面元素到字段的映射。
|
||||
|
||||
### 2.1 `/usage-log`(2026-09-13 推完)
|
||||
|
||||
**页职责**(交互设计):key 输入(多个)+ 时间范围 → 明细表格;表格上方汇总卡片(累计用量、预估消费)。
|
||||
|
||||
**端点划分(2026-09-13 定)**:明细与汇总**分两个端点**——首次并行请求(`Promise.all`),翻页只调明细、不重算汇总。
|
||||
|
||||
| # | 端点 | 职责 | 响应形状 |
|
||||
|---|------|------|---------|
|
||||
| 1 | `POST /api/usage/logs/query` | 明细行(分页) | `{ items, total, meta }` |
|
||||
| 2 | `POST /api/usage/summary` | 卡片汇总 | `{ request_count, …, meta }` |
|
||||
|
||||
**明细行字段(2026-09-13 定)**
|
||||
|
||||
- **与表 1:1 全量返回**(除本系统内部列 `dedupe_key`):`id` / `key_id` / `user_id` / `username` / `created_at` / `type` / `content` / `model_name` / `quota` / `prompt_tokens` / `completion_tokens` / `use_time` / `is_stream` / `channel` / `channel_name` / `token_id` / `group_name` / `ip` / `request_id` / `upstream_request_id` / `other`
|
||||
- 恒定 / 空值字段(`user_id` / `username` / `channel` / `channel_name` / `ip` / `group_name` / `upstream_request_id`)照返——展示与否是交互层决策
|
||||
- 两个覆盖/计算字段:
|
||||
- `token_name`——**当前名**(join `api_keys`),不用日志里的快照名(同一个 key 的每行显示同一个名字;快照列仍在库中保真)
|
||||
- `amount_rmb`——该行折算实付人民币(`quota ÷ 购买时刻 QPU × 购买汇率`;错误行 `quota = 0` → 0)
|
||||
|
||||
**汇总响应字段(2026-09-13 定)**
|
||||
|
||||
| 字段 | 含义 |
|
||||
|------|------|
|
||||
| `request_count` | 时段内总行数 |
|
||||
| `consume_count` / `error_count` | 按 `type` 拆分(2 消费 / 5 错误) |
|
||||
| `prompt_tokens` / `completion_tokens` / `cache_tokens` | token 三项合计(`cache_tokens ⊆ prompt_tokens`,单列可看缓存命中量) |
|
||||
| `quota` | quota 合计(原值,供核对) |
|
||||
| `amount_rmb` | 折算实付人民币(跨期按购买期间分段累加;非订阅来源的折算规则后议) |
|
||||
| `meta` | 同约定 22 |
|
||||
|
||||
**页面元素 → 数据来源**
|
||||
|
||||
| 页面元素 | 来源 |
|
||||
|---------|------|
|
||||
| key 输入(多个)+ 时间范围 | 前端持有(输入即查询;服务端无状态) |
|
||||
| 卡片:调用次数 | 汇总:`request_count` / `consume_count` / `error_count` |
|
||||
| 卡片:累计用量 | 汇总:token 三项 + `quota` |
|
||||
| 卡片:预估消费 | 汇总:`amount_rmb`(口径见 §1.4 说明) |
|
||||
| 卡片:数据截止 | `meta.data_until` |
|
||||
| 「未收录」提示 | `meta.keys[].registered` / `key_hint` |
|
||||
| 明细表格 | `items[]` |
|
||||
| 分页器 | `total` + `limit` / `offset` |
|
||||
|
||||
**请求参数(2026-09-13 定)**
|
||||
|
||||
```jsonc
|
||||
// POST /api/usage/logs/query
|
||||
{
|
||||
"keys": ["sk-…", "sk-…"], // 必填;去重后最多 20 个(初值,超限 → invalid_params)
|
||||
"start": "2026-09-06T00:00:00+08:00", // 必填;左闭右开 [start, end)
|
||||
"end": "2026-09-13T00:00:00+08:00",
|
||||
"type": [2, 5], // 可选;缺省 = 全部类型(0–7);只作用于本端点
|
||||
"limit": 50, // 可选;默认 50,上限 200(超限 → invalid_params)
|
||||
"offset": 0 // 可选;默认 0
|
||||
}
|
||||
|
||||
// POST /api/usage/summary —— 同一套 keys / start / end;无 type / limit / offset
|
||||
{ "keys": ["sk-…"], "start": "2026-09-06T00:00:00+08:00", "end": "2026-09-13T00:00:00+08:00" }
|
||||
```
|
||||
|
||||
- **字段集稳定**:可空列(如 `upstream_request_id`)返回 `null` 而非省略字段——前端类型可直接写 `string | null`
|
||||
- 不设跨度上限(2026-09-13 定);非法参数(`start ≥ end`、无法解析、`keys` 空数组)→ `400 invalid_params`
|
||||
|
||||
**响应示例(示意)**
|
||||
|
||||
```jsonc
|
||||
// 明细
|
||||
{
|
||||
"items": [
|
||||
{ "id": 8412, "key_id": 3, "user_id": 12, "username": "…", "created_at": "2026-09-11T19:37:12+08:00",
|
||||
"type": 2, "content": "", "token_name": "小王", "model_name": "gpt-5.6-terra", "quota": 12000,
|
||||
"prompt_tokens": 1500, "completion_tokens": 500, "use_time": 2, "is_stream": true,
|
||||
"channel": 0, "channel_name": "", "token_id": 100, "group_name": "default", "ip": "",
|
||||
"request_id": "20260911193652…", "upstream_request_id": null, "amount_rmb": 0.017,
|
||||
"other": { "cache_tokens": 29056, "model_ratio": 0.075, "subscription_id": 90, "…": "…" } }
|
||||
],
|
||||
"total": 1873,
|
||||
"meta": { "…": "见约定 22" }
|
||||
}
|
||||
|
||||
// 汇总
|
||||
{
|
||||
"request_count": 1873, "consume_count": 1849, "error_count": 24,
|
||||
"prompt_tokens": 1234567, "completion_tokens": 234567, "cache_tokens": 89012,
|
||||
"quota": 53939847, "amount_rmb": 23.45,
|
||||
"meta": { "…": "见约定 22" }
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 `/manage/api-keys`(2026-09-13 推完)
|
||||
|
||||
**端点(2026-09-13 定)**
|
||||
|
||||
| # | 端点 | 职责 |
|
||||
|---|------|------|
|
||||
| 1 | `GET /api/manage/api-keys` | 清单(按 `id` 升序,不分页) |
|
||||
| 2 | `POST /api/manage/api-keys` | 登记(body 只带 `key`;上游验证 → 入库) |
|
||||
| 3 | `PATCH /api/manage/api-keys/:id` | 启停(`{"status": "active" | "disabled"}`;启用需上游验证) |
|
||||
|
||||
**清单响应字段(2026-09-13 定)**:与表 1:1(17 列,除 `key_hash` / `key_ciphertext` 两个敏感列——**永不出接口**)。`disabled_source` 使「已失效(`auto_401`)」与「已停用(`manual`)」可辨(该列为此于同日补进 `api_keys` 表设计,见其 §3.8)。
|
||||
|
||||
**登记与启停细节(2026-09-13 定)**
|
||||
|
||||
| 项 | 定法 |
|
||||
|----|------|
|
||||
| 登记请求 | `POST {"key": "sk-…"}` |
|
||||
| 登记响应 | `201` + 新行完整对象(同清单形状;此时 `log_synced_at: null`) |
|
||||
| 登记后的可见时机 | **不阻塞**:验证(`GET /api/usage/token`)+ INSERT 即返回;首轮日志同步由同步引擎**立即异步补一轮**——未完成时 `meta` 如实给 `null`,前端显示「数据同步中」 |
|
||||
| 启停请求 / 响应 | `PATCH {"status": …}` → `200` + 更新后的完整行;**幂等**(提交当前已是的状态不报错) |
|
||||
| 启用成功的副作用 | `consecutive_401s` 清零——验证调用本身即一次成功调用,「连续」已断(否则下一轮若 401 会立刻再次摘除) |
|
||||
| 错误分支 | `key_rejected`(400,上游拒绝该 key:登记验证 / 启用验证失败)· `already_registered`(409,key 已在库)· `upstream_unavailable`(502,上游故障、无法完成验证)· `not_found`(404,`PATCH` 的 id 不存在)· `invalid_params`(400,`key` 缺失 / 格式非法) |
|
||||
|
||||
### 2.3 `/manage/purchases`(2026-09-13 推完)
|
||||
|
||||
**端点(2026-09-13 定)**
|
||||
|
||||
| # | 端点 | 职责 |
|
||||
|---|------|------|
|
||||
| 1 | `GET /api/manage/purchases` | 清单(按 `paid_at` DESC,同时间按 `id` DESC;不分页) |
|
||||
| 2 | `POST /api/manage/purchases` | 登记 |
|
||||
| 3 | `PATCH /api/manage/purchases/:id` | 编辑(部分更新) |
|
||||
| 4 | `DELETE /api/manage/purchases/:id` | 删除(物理) |
|
||||
| 5 | `GET /api/manage/purchases/observations` | 观测(核对提示的数据面) |
|
||||
|
||||
**字段与必填(2026-09-13 定)**
|
||||
|
||||
- 清单 / 登记响应:与表 1:1(9 列)
|
||||
- 登记必填:`kind` / `paid_at` / `paid_amount` / `quota_granted`(前三为支付事实;`quota_granted` 是折算链的分母);`note` 与订阅标识 / 期间(`subscription_id` / `period_start` / `period_end`)**可后补**——缺失时该笔暂无法自动匹配日志,由观测端点引导补录
|
||||
- 编辑:`PATCH` 部分更新语义,8 个录入字段全部可改(含 `kind`)
|
||||
- 删除:物理删除;该笔对应日志的 `amount_rmb` 随即算不出来——折算在展示层实时算、不落库,此为预期行为(亦可视为「改错即修」的即时反馈)
|
||||
|
||||
**观测响应(示意)**
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"items": [
|
||||
{ "subscription_id": 90, "plan_title": "Max", "observed_quota_total": 800000000,
|
||||
"first_seen_at": "2026-09-01T00:00:00+08:00", "last_seen_at": "2026-09-13T21:10:00+08:00",
|
||||
"log_count": 976, "matched_purchase_id": null }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- 数据源:日志 `other` 的 `subscription_id` / `subscription_plan_title` / `subscription_total` 按 ID 聚合(jsonb)
|
||||
- `matched_purchase_id` 非空 = 已对应某条购买记录;`null` = 观测到但未登记(核对提示的目标)
|
||||
|
||||
**错误码**:`invalid_params`(校验失败)· `not_found`(编辑 / 删除目标不存在)
|
||||
|
||||
### 2.4 `/manage/qpu`(2026-09-13 推完;无实质分叉,按默认定)
|
||||
|
||||
- **端点**:`GET /api/manage/qpu`——时间线清单(按 `effective_from` 升序;不分页)
|
||||
- **字段**:与表 1:1(`id` / `qpu` / `effective_from`,3 列)
|
||||
- **纯只读**:无任何写端点(`qpu_history` 表设计 §3.5 已锁定只读;管理通道仅提供查看)
|
||||
|
||||
### 2.5 `/dashboard`(二期候选 · 占位)
|
||||
|
||||
本页为二期候选(一期可不实现),**本期不推导**;届期按同一流程补端点与字段。数据面天然具备(本地全量日志 + 聚合能力),无需预留接口。
|
||||
|
||||
---
|
||||
|
||||
## 3. 待裁决与待定
|
||||
|
||||
| 事项 | 归属 |
|
||||
|------|------|
|
||||
| 同步引擎调度口径(一轮 = 遍历全部 `active` key 的统一 tick——2026-09-13 作者确认方向) | 同步引擎设计(失败重试策略等细节随行) |
|
||||
| 管理面口令的传输加固(常量时间比较等) | 技术稿 |
|
||||
| 各端点参数校验细则(key 格式与条数、时间解析失败、`type` 取值校验、`limit` / `offset` 边界) | 技术稿 |
|
||||
@@ -0,0 +1,137 @@
|
||||
# api_keys 表字段设计
|
||||
|
||||
> 数据模型第一张表:API key 登记表——每个被登记的 API key 一行,**由管理通道登记**(2026-09-13 起,取代原「输入即注册」)。本文记录字段清单、语义与决策理由;`usage_logs` 表待对齐后另文记录。
|
||||
|
||||
| 项目 | 内容 |
|
||||
|------|------|
|
||||
| 状态 | 定稿(2026-09-11,逐字段讨论收敛;2026-09-13 修订:key 登记改由管理通道执行、表名更改为 `api_keys`、新增 `disabled_source` 列) |
|
||||
| 关联 | `docs/upstream/NewAPI-sk-key接口文档.md`(接口①字段依据)· `docs/upstream/NewAPI-接口范围.md` · `docs/design/API契约.md` §2.2(登记 / 启停端点) |
|
||||
| 定稿范围 | 列结构 + 语义 + 写入时机;同步引擎细节(401 阈值等)不在此文 |
|
||||
| 字段数 | 17 = 9 本系统管理字段 + 8 上游接口字段 |
|
||||
|
||||
---
|
||||
|
||||
## 1. 表定位
|
||||
|
||||
- **一行 = 一个登记过的 API key**:在管理通道登记一次,这里多一行;之后所有轮询同步、查询都围绕它转
|
||||
- 它是三个基本问题的答案所在地:
|
||||
1. 「这个 key 登记过吗」→ `key_hash` 查询命中(支撑「本地查库、未命中即提示未收录」的判定流程)
|
||||
2. 「该轮询哪些 key」→ `status = active` 的行
|
||||
3. 「凭证安全存哪」→ `key_ciphertext`(加密存储)
|
||||
- 与 `usage_logs` 一对多:每条日志经 `key_id` 指向本表
|
||||
|
||||
## 2. 字段清单
|
||||
|
||||
### 2.1 管理字段(9 个,本系统产生)
|
||||
|
||||
| 列 | 类型 | 值来源 | 消费方 / 用途 |
|
||||
|----|------|--------|---------------|
|
||||
| `id` | bigserial PK | 自增 | `usage_logs.key_id` 外键指向 |
|
||||
| `key_hash` | text UNIQUE | 登记时 HMAC-SHA256(管理员粘贴输入) | 查询命中判定(「查库优先」);不可逆 |
|
||||
| `key_ciphertext` | bytea | 登记时 AES-256-GCM(管理员粘贴输入),nonce + 密文 + tag 打包 | 调上游时解密原文(唯一用途) |
|
||||
| `status` | text | 401 摘除 / 手动启停 | `active` / `disabled`;「key 已失效」提示与轮询范围 |
|
||||
| `disabled_source` | text NULL | 401 摘除 → `auto_401`;手动停用 → `manual`;启用 / 登记时清 NULL | 管理面区分「已失效」与「已停用」(见 3.8) |
|
||||
| `consecutive_401s` | int | 每轮同步:401 时 +1,成功清零 | 「连续 401 自动摘除」的判定状态 |
|
||||
| `registered_at` | timestamptz | 登记时写入 | 空态文案「数据自该 key 登记起累积」 |
|
||||
| `log_synced_at` | timestamptz NULL | 日志同步成功时写入 | **「数据截止 HH:mm」的唯一来源** |
|
||||
| `usage_synced_at` | timestamptz NULL | 额度采集成功时写入 | 额度信息的新鲜度(UI 若做)、排查 |
|
||||
|
||||
### 2.2 上游字段(8 个,取自 `GET /api/usage/token`,除 `object` 外全部落库)
|
||||
|
||||
| 列 | 类型 | 上游字段 | 说明 |
|
||||
|----|------|---------|------|
|
||||
| `token_name` | text | `name` | 展示名(约定:展示名 = token_name) |
|
||||
| `total_granted` | bigint | `total_granted` | 总额度(上游原值;无限额度时无意义,照存) |
|
||||
| `total_used` | bigint | `total_used` | 累计已用 →「累计已用」列 |
|
||||
| `total_available` | bigint | `total_available` | 按上游原值存(数字上等于 granted − used,但保留原值,含无限额度时的负数行为) |
|
||||
| `unlimited_quota` | bool | `unlimited_quota` | 驱动展示分支(「累计剩余」显示「不限」而非数字) |
|
||||
| `model_limits` | jsonb | `model_limits` | map 结构的自然落法 |
|
||||
| `model_limits_enabled` | bool | `model_limits_enabled` | |
|
||||
| `expires_at` | timestamptz NULL | `expires_at` | 上游 0 = 永不过期 → 存 NULL(见 3.5) |
|
||||
|
||||
## 3. 决策记录(含理由)
|
||||
|
||||
### 3.1 凭证双列分离:`key_hash` + `key_ciphertext`
|
||||
|
||||
- `key_hash`(HMAC-SHA256,不可逆):只服务「命中判定」——没有它,每次查询都要把全部 key 送去上游验证
|
||||
- `key_ciphertext`(AES-256-GCM,可解密):唯一用途是调上游
|
||||
- 密钥 `KEYS_MASTER_KEY` 经 env 注入(HKDF 派生 hmac / aes 子密钥);**密钥丢失 = 全部 key 需重新登记**(记入部署文档)
|
||||
|
||||
> **注解:为什么是两个字段(给非密码学背景的读者)**
|
||||
>
|
||||
> 两列各管一件事——`key_hash` 管「找得到」,`key_ciphertext` 管「用得起来」:
|
||||
>
|
||||
> - `key_hash` 像**单向粉碎机**:同样的输入永远产出同样的指纹,指纹却反推不出原文。输入 key 时(登记或查询),服务端算出的指纹与库中比对,命中即「已登记」,零上游调用;即便整库泄露,外人拿到的也只是指纹。选用 HMAC(而非裸哈希)是多掺一味只有服务器持有的「调料」:没有它,就无法用「猜测输入 → 对照指纹」的方式离线验证。
|
||||
> - `key_ciphertext` 像**保险箱**:用 `KEYS_MASTER_KEY` 加密锁存,轮询调上游时开锁取明文;AES-GCM 自带防篡改,密文被动过则解密直接报错。
|
||||
> - **两列并存的原因**:加密每次都掺随机数(同一 key 两次密文不同)→ 密文不能用于查找;哈希不可逆 → 指纹还原不出明文。「可查找」与「可还原」两个需求互斥,只能分存两份形态。
|
||||
> - **防线边界**:能同时接触数据库与环境变量的人(服务器运营方)防不住——这是已接受的信任模型前提;本设计防的是「数据库单独落到第三方手里」的常见拖库场景(SQL 注入、备份泄露)。
|
||||
> - 实现提示:两种算法均为标准件,Bun 内置 `crypto` 直接提供,无需第三方库。
|
||||
|
||||
### 3.2 命名:与上游对齐
|
||||
|
||||
- 上游字段直接采用原名(`total_granted` 等),对照 `docs/upstream/NewAPI-sk-key接口文档.md` 看表即懂
|
||||
- 例外:`name` → `token_name`(`name` 太泛;本工具以 token_name 指代展示名)
|
||||
|
||||
### 3.3 两个同步时间戳,不合并
|
||||
|
||||
- 两个上游调用(日志 / 额度)独立成功、独立失败——如实建模、数据溯源
|
||||
- 「数据截止」语义钉死:**只随日志同步成功前进**;合并会导致额度接口故障时反向失真(日志已新,却报旧时间)
|
||||
- 为未来的额度信息展示留新鲜度表达位(UI 方案未稳定,不轻易断言其无消费方)
|
||||
|
||||
### 3.4 上游字段「全体落库」
|
||||
|
||||
- `/api/usage/token` 除 `object` 外 8 个字段全部入列(含当前无消费方的 `model_limits` 等)
|
||||
- 这类数据随时可从上游重新获取,入列本质是「本地最新快照」;当前策略为宁多勿缺,待 UI 稳定后回审
|
||||
|
||||
### 3.5 `expires_at` 的 0 → NULL 转换
|
||||
|
||||
- 上游 0 = 永不过期;转 NULL 表达该语义
|
||||
- 安全性依据:8 个上游字段整组一起写入(同一条 UPDATE + `usage_synced_at`),「未获取」由 `usage_synced_at IS NULL` 承载——`expires_at` 的 NULL 专用于「永不过期」,语义不重叠
|
||||
|
||||
### 3.6 生命周期
|
||||
|
||||
- `active` →(连续 N 次 401)→ `disabled` + `disabled_source = auto_401`(数据保留、不再轮询)→(管理通道手动启用、上游验证通过)→ `active` + 清 `disabled_source`
|
||||
- 管理通道也可手动停用(`disabled_source = manual`)——与 401 摘除是同一状态、不同来源(见 `docs/design/交互设计.md` §3.2)
|
||||
- N 的取值属同步引擎设计(不在此文)
|
||||
|
||||
### 3.7 表名:从 `sk_keys` 更名 `api_keys`(2026-09-13)
|
||||
|
||||
- 理由:与页面 `/manage/api-keys` 同词对齐;`sk-` 只是 key 的实际格式前缀(不属表名职责),`api_keys` 为领域通用词,与其余三表(`usage_logs` / `purchases` / `qpu_history`)风格一致
|
||||
|
||||
### 3.8 新增 `disabled_source`:停用来源可辨(2026-09-13)
|
||||
|
||||
- 起因:「已失效(401 自动摘除)」与「已停用(手动)」需要在管理面区分——`status` 只有 `active` / `disabled` 两值,401 自动摘除与手动停用落在同一个值上,事后无法区分
|
||||
- 方案对比:
|
||||
- ① 用 `consecutive_401s` 推断——不可靠(手动停用会保留停用前的任意值,成功同步又会清零)
|
||||
- ② 拆分 `status` 取值(如 `auto_disabled` / `manual_disabled`)——污染状态机,启停与轮询逻辑分叉
|
||||
- ③ **新增一列记录来源**——最直接;取值 `manual` / `auto_401`,`active` 时为 NULL
|
||||
- 代价:字段数 16 → 17(2026-09-13 由契约推导回头补列)
|
||||
|
||||
## 4. 写入时机(两个写入者:管理通道 / 同步引擎)
|
||||
|
||||
| 时机 | 动作 |
|
||||
|------|------|
|
||||
| **登记**(管理通道,上游验证通过) | INSERT 一行:管理字段(HMAC / 加密 / registered_at,`disabled_source` 为 NULL)+ 上游字段全组(一次 `/api/usage/token` 拿全)+ 两个 synced_at;**首轮日志同步不阻塞登记响应**(由同步引擎立即异步补一轮,见 `docs/design/API契约.md` §2.2) |
|
||||
| **启停**(管理通道) | UPDATE `status` + `disabled_source`:启用(上游验证通过 → `active` + 清 NULL + `consecutive_401s` 清零——验证调用本身即一次成功调用)/ 停用(→ `disabled` + `manual`)——数据保留、不删除 |
|
||||
| **每轮轮询** | UPDATE:上游字段全组 + `usage_synced_at`;日志同步成功后写 `log_synced_at`;401 计数随结果更新;连续 N 次 401 → `status = disabled` + `disabled_source = auto_401` |
|
||||
|
||||
- 两个上游调用各自独立:**各自的成功写各自的时间戳,互不阻塞、互不污染**
|
||||
|
||||
## 5. 明确不存的(已否决字段与理由)
|
||||
|
||||
| 字段 | 否决理由 |
|
||||
|------|----------|
|
||||
| `key_hint`(`sk-…a1b2` 尾号) | 登记即有 `token_name`,回显价值归零(注:查看面响应里的 `key_hint` 是**即时生成**的,与本列否决无关) |
|
||||
| 别名 / 备注名 | 上游 `token_name` 已提供展示名(本地已同步该字段),再记一份是平行冗余;要改名去上游站点改(2026-09-13 定) |
|
||||
| `upstream_token_id` | 本地 `id` 足够;对账时日志行自有 `token_id` |
|
||||
| `last_sync_error` | 错误详情进服务器日志即可;UI 无展示位(有需求再加) |
|
||||
| `object` / `code` / `message` | 固定值 / 传输层包装,无信息量 |
|
||||
|
||||
## 6. 待定事项(关联后续环节)
|
||||
|
||||
| 事项 | 归属 |
|
||||
|------|------|
|
||||
| 401 摘除阈值 N | 同步引擎设计 |
|
||||
| 多 key 时「数据截止」的聚合语义 | 已定:取最慢(`docs/design/API契约.md` 约定 22) |
|
||||
| 「累计已用/剩余」展示格式与无限额度分支 | 交互细节层 |
|
||||
| `usage_logs` 表字段清单 | 已定稿:见 `docs/design/usage_logs表字段设计.md` |
|
||||
@@ -0,0 +1,93 @@
|
||||
# purchases 表字段设计
|
||||
|
||||
> 数据模型第三张表:购买记录——每笔支付事件(订阅购买 / 充值)一行,是「实付人民币」这一唯一只有作者知道的信息的落点,也是折算链第二层(额度美元 → 实付人民币)的事实来源。本文记录字段清单、语义与决策理由。
|
||||
|
||||
| 项目 | 内容 |
|
||||
|------|------|
|
||||
| 状态 | 定稿(2026-09-13,多轮讨论收敛;同日修订:查看面金额口径改「折算实付人民币」,§6 边界相应放开) |
|
||||
| 关联 | `docs/design/qpu_history表字段设计.md`(折算链第一条:quota → 额度美元)· `docs/design/API契约.md` §2.3(登记 / 编辑端点)· `docs/upstream/NewAPI-sk-key接口文档.md`(`other.subscription_*` 字段依据) |
|
||||
| 范围 | 列结构 + 语义 + 写入时机;登记入口见 API 契约 §2.3 |
|
||||
| 字段数 | 9 = 1 主键 + 8 人工录入(登记 4 · 期间 2 · 订阅 2) |
|
||||
|
||||
---
|
||||
|
||||
## 1. 表定位
|
||||
|
||||
- **一行 = 一笔支付事件**(订阅购买 / 充值)——同一模型兼容两种形态;**实现先做订阅路径**(实测当前用量 100% 为订阅)
|
||||
- **折算链全貌**:`日志.quota ÷ quota_per_unit` = 额度美元 → `× 购买汇率` = 实付人民币;两条链的系数**都取该日志所属购买的购买时刻**(整笔订阅一把尺,见 `qpu_history表字段设计.md` §3.3)。购买比率 = `paid_amount ÷ (quota_granted ÷ quota_per_unit)`——**只存事实、不存比率**(见 3.2)
|
||||
- 与日志的分流:日志 `other.billing_source = "subscription"` → 按本表匹配(优先 `subscription_id`,同 ID 跨期时按时间区间);`"wallet"`(充值)→ 池子近似(后议)
|
||||
- 数据规模:年个位数行(订阅按周期购买)——无需索引设计
|
||||
|
||||
## 2. 字段清单
|
||||
|
||||
### 2.1 主键与登记字段(5 个)
|
||||
|
||||
| 列 | 类型 | 值来源 | 说明 |
|
||||
|----|------|--------|------|
|
||||
| `id` | bigserial PK | 自增 | |
|
||||
| `kind` | text | 人工选择 | `subscription`(订阅)/ `topup`(充值) |
|
||||
| `paid_at` | timestamptz | 人工填写 | 支付时间 |
|
||||
| `paid_amount` | numeric | **人工必填** | **实付人民币**——唯一「只有作者知道」的信息 |
|
||||
| `note` | text | 人工填写 | 备注 |
|
||||
|
||||
### 2.2 期间与订阅字段(4 个)
|
||||
|
||||
| 列 | 类型 | 值来源 | 说明 |
|
||||
|----|------|--------|------|
|
||||
| `period_start` | timestamptz? | **人工录入**(钱包页面) | 仅订阅:该笔覆盖的用量期间起点;左闭 |
|
||||
| `period_end` | timestamptz? | **人工录入**(钱包页面) | 期间终点;左开;未结束可空 |
|
||||
| `subscription_id` | bigint? | **人工录入**(钱包页面「订阅 #N」) | 上游订阅实例 ID;多订阅并存时用于精确分流(与日志 `other.subscription_id` 匹配) |
|
||||
| `quota_granted` | bigint? | **人工录入**(钱包页面「总额度」) | 本笔额度(订阅:钱包页面总额度;充值:充值记录) |
|
||||
|
||||
## 3. 决策记录(含理由)
|
||||
|
||||
### 3.1 登记 = 手工抄录(来源只有两处)
|
||||
|
||||
购买记录全部由作者手工录入,来源仅两处:**支付记录**(`kind` / `paid_at` / `paid_amount`)与**钱包页面**(订阅 ID / 期间 / 总额度)。订阅关键信息只有作者可见(没有免 auth 接口可查)——系统不做回填与推断,日志观测仅用于核对提示(见 §4);必填 / 可空的校验细节归技术稿。
|
||||
|
||||
### 3.2 折算比率不存储——存事实、不存派生数
|
||||
|
||||
- `paid_amount` 是唯一不可替代的人工信息;比率是派生数——「记实付」比「记汇率」更省心(不用心算)且可事后核对;只存派生比率会丢失核对依据
|
||||
- **日志同样不加汇率列**(`usage_logs` 零 schema 改动):入库有滞后(1000 条窗口内可能几天后才补回)→「入库时的配置值」会错标给「更早消费的日志」,且**无从发现**;半准确的数字比没有数字更危险
|
||||
- 折算全部发生在展示 / 聚合层;关联线索(`billing_source` / `subscription_id` / 金额分解)本就在日志 `other` 里,零 schema 改动即可用
|
||||
|
||||
### 3.3 为什么额度也手工录入(不采用日志感知)
|
||||
|
||||
日志逐条自带 `subscription_total`(实测),但回填它需要「订阅 ID → 该 ID 日志 total」的关联逻辑;钱包页面已直接给出总额度——手工抄录一步到位。**系统对购买记录零回填、零推断**,观测仅用于核对(见 §4)。
|
||||
|
||||
### 3.4 命名与类型
|
||||
|
||||
- 金额列用 `numeric`(人民币金额不用浮点);时间列统一 `timestamptz`
|
||||
- 期间左闭右开(`[period_start, period_end)`)——与全项目期间惯例一致
|
||||
- 订阅实例 ID 与日志同名 `subscription_id`——同一体系、同一含义(分流匹配即用此列)
|
||||
|
||||
## 4. 写入时机与维护
|
||||
|
||||
| 时机 | 动作 |
|
||||
|------|------|
|
||||
| 人工登记(管理通道) | INSERT:录入支付信息(时间 / 实付)与订阅信息(ID / 期间 / 总额度,抄自钱包页面)→ 确认 |
|
||||
| 系统观测(仅核对) | 日志观测到的订阅 ID / `subscription_total` 仅作核对提示(如「观测到 #N 未对应任何购买记录」),不参与回填 |
|
||||
| 编辑 / 删除 | 数据为人工登记,改错即修(见 API 契约 §2.3) |
|
||||
|
||||
## 5. 明确不存的(已否决字段与理由)
|
||||
|
||||
| 字段 | 否决理由 |
|
||||
|------|----------|
|
||||
| 折算比率(购买汇率) | 派生数——存事实(实付 + 额度)即可事后核对,只存比率会丢失核对依据(见 3.2) |
|
||||
|
||||
## 6. 展示层边界(界面设计的输入)
|
||||
|
||||
- **明细应展示「订阅归属」**(数据源 = 日志 `other`):订阅名称、额度实况、(本表登记后)期间范围——给所有使用者可见
|
||||
- **查看面金额口径(2026-09-13 修订)**:卡片 / 明细的「消费」类金额 = **折算实付人民币**(按购买期间分段,每段 `quota 占比 × 该笔实付`);原「天然不含实付金额」的边界就此放开。**仍不上查看面**:实付总额、购买比率、购买记录(含 `note`)本身——管理面独占
|
||||
- 已知代价(接受):明细含 quota 列、QPU 公开(500000 可查),使用者理论上可由两者反推购买比率——本工具是成本分担场景、非对抗关系
|
||||
- 「按订阅维度统计」(本期用量/占比)数据天然支持——**待评估**,暂不列入
|
||||
- 管理页相关见 `docs/design/交互设计.md` §3.3 与 `docs/design/API契约.md` §2.3
|
||||
|
||||
## 7. 待定事项(关联后续环节)
|
||||
|
||||
| 事项 | 归属 |
|
||||
|------|------|
|
||||
| 充值(`topup`)路径的折算近似算法 | 池子加权平均等方案,后议 |
|
||||
| 多订阅并存的日志行为 | 按源码结构推理(每笔标所属 ID);实现期以真实场景实测验证 |
|
||||
| 期间与订阅 ID 的匹配细节 | 技术稿细化(ID 优先、同 ID 跨期按时间) |
|
||||
| 核对提示(观测到的订阅 ID ↔ 已录入记录比对)的交互 | 交互设计 |
|
||||
@@ -0,0 +1,76 @@
|
||||
# qpu_history 表字段设计
|
||||
|
||||
> 数据模型第四张表:换算参数时间线(表名 `qpu_history`,QPU = QuotaPerUnit 缩写)——`quota_per_unit`(quota → 额度美元)的历史取值,折算链第一层的系数来源。历史值在 sk-key 视角无从观测,本表由同步引擎自动采样、只记变化点;**纯只读**(唯一写入者是同步引擎)。本文记录字段清单、语义与决策理由。
|
||||
|
||||
| 项目 | 内容 |
|
||||
|------|------|
|
||||
| 状态 | 定稿(2026-09-13,多轮讨论收敛:只读化、按购买时刻取尺) |
|
||||
| 关联 | `docs/design/purchases表字段设计.md`(折算链第二条:额度美元 → 实付人民币)· `docs/design/API契约.md` §2.4(只读查看端点)· `docs/upstream/NewAPI-接口范围.md`(采集来源 `/api/status`) |
|
||||
| 范围 | 列结构 + 语义 + 写入规则;采样调度(同步引擎)不在此文 |
|
||||
| 字段数 | 3 = 1 主键 + 2 业务字段 |
|
||||
|
||||
---
|
||||
|
||||
## 1. 表定位
|
||||
|
||||
- **一行 = 一次「换算单位取值变化」事件**(值 + 生效时间)——正常情况全表仅 1 行(当前站点为出厂默认值,从未变过)
|
||||
- 它是折算链第一条(quota → 额度美元)的**时间线**:任何时刻的换算取值 = 「该时刻之前最近一条」
|
||||
- **为什么要自己攒**:`/api/status` 只能看到当前值,历史值无从观测(上游不留历史、管理审计不记值)——没有本表,历史日志只能用今天的尺子量,**账会串**
|
||||
- 与日志零耦合:折算全部发生在展示 / 聚合层——本表任何变动都不触发 `usage_logs` 写入
|
||||
|
||||
## 2. 字段清单
|
||||
|
||||
| 列 | 类型 | 值来源 | 说明 |
|
||||
|----|------|--------|------|
|
||||
| `id` | bigserial PK | 自增 | |
|
||||
| `qpu` | numeric | `/api/status.quota_per_unit` | 当时的 `quota_per_unit` 值 |
|
||||
| `effective_from` | timestamptz | 首次采样或检测到变化的时间 | 生效时间;采样误差 ≤ 同步周期 |
|
||||
|
||||
## 3. 决策记录(含理由)
|
||||
|
||||
### 3.1 为什么自动采样(而非人工登记)
|
||||
|
||||
它是「可登记的事实」的**反面**——历史值无从观测(只能看到当前值);自动采样(每轮同步)是唯一能沿时间轴捕获它的方式。采样误差 ≤ 同步周期(边界影响可忽略)。
|
||||
|
||||
### 3.2 只记变化点,不记每次采样
|
||||
|
||||
部署时插基线;此后每轮同步顺带读 `/api/status`,**值变化才追加一行**——行数 = 调价次数 + 1(正常为 1)。
|
||||
|
||||
### 3.3 取值语义与跨期统计
|
||||
|
||||
- **取尺基准 = 购买时刻(`paid_at`)**:一条日志的折算 = 先按 `subscription_id` 归属到购买记录(见 `purchases表字段设计.md`),再取**该购买的购买时刻**在本表中的当值——同一笔订阅的全部日志共用一把尺子。完整链:`日志.quota ÷ 购买时刻的 quota_per_unit × 购买汇率` = 实付人民币
|
||||
- **为什么不按日志自身的时刻**:订阅期内若发生重定标,同一笔订阅的「已用美元」会在调价点跳变,而额度与实付没变——分子分母尺子不一致,账就不自洽;按购买时刻取尺等价于「按 quota 占比分摊实付」,恒定成立
|
||||
- **跨期统计**:把查询时间范围按**购买期间边界**切成若干段,各段用该笔购买的系数聚合——`quota_per_unit` 只在每笔购买处取一次值,调价点不再制造分段
|
||||
- **边界**:购买时刻早于本表最早记录时(如补录历史购买),回退用「最早已知值」(近似规则)
|
||||
- **充值(`wallet`)路径**:同样取「充值执行时刻」的尺子(池子近似算法后议)
|
||||
|
||||
### 3.4 `quota_per_unit` 的可变性说明(本表存在的理由)
|
||||
|
||||
它是「全站计价单位的定义」——改它 ≈ 重定义货币单位(全站一致重定标);管理员极少动(当前站点为出厂默认值)。本设计以「按时间线折算」从根上免疫它的任何变更。
|
||||
|
||||
### 3.5 为什么不做人工修正(只读设计,2026-09-13 定)
|
||||
|
||||
- 原设计预留「修正生效时间 / 删除」等人工介入能力;源码评估后判定场景不成立——`quota_per_unit` 在上游**无编辑入口(前台默认隐藏且只读)、管理审计不记值、历史上从未变化**(见 `docs/upstream/NewAPI-接口范围.md`)
|
||||
- 本表由此定型为**纯只读时间线**:唯一写入者是同步引擎(值变化才追加);管理通道仅提供只读查看
|
||||
- 未来若真出现需要人工介入的场景(如运营方主动告知一次精确切换时刻),再评估启用
|
||||
|
||||
### 3.6 命名:表名 `qpu_history`、值列 `qpu`(2026-09-13 改)
|
||||
|
||||
- **表名**:原名 `quota_per_unit_history` 过长、逐处引用啰嗦 → 改为 `qpu_history`——QPU 是 QuotaPerUnit 的通用缩写,短且醒目
|
||||
- **值列**:原名 `value` 过泛("什么的值"不自明)→ 改为 `qpu`,与表名一体、一眼即知
|
||||
- 通用约定:**表名 / 列名用 QPU 缩写,引用上游字段 `quota_per_unit` 时保持全称**
|
||||
|
||||
## 4. 写入时机(唯一写入者:同步引擎)
|
||||
|
||||
| 时机 | 动作 |
|
||||
|------|------|
|
||||
| 部署(基线) | INSERT 基线一行(当时 `/api/status` 取值) |
|
||||
| 每轮同步 | 读 `/api/status`:**值变化才追加一行** |
|
||||
|
||||
- 除追加外无 UPDATE / DELETE——数据不可变
|
||||
|
||||
## 5. 待定事项(关联后续环节)
|
||||
|
||||
| 事项 | 归属 |
|
||||
|------|------|
|
||||
| 采样调度(读取频率、失败重试) | 同步引擎设计 |
|
||||
@@ -0,0 +1,114 @@
|
||||
# usage_logs 表字段设计
|
||||
|
||||
> 数据模型第二张表:调用日志明细——增量同步的落点,本项目「任意时间范围用量」的唯一数据来源。字段清单依据 `/api/log/token` 实测响应(2026-09-11,1000 条样本)与上游源码(`model/log.go`)双重校对;上游只有 1000 条滚动窗口,**错过即永久丢失**。
|
||||
|
||||
| 项目 | 内容 |
|
||||
|------|------|
|
||||
| 状态 | 定稿(2026-09-11,逐字段讨论收敛) |
|
||||
| 关联 | `docs/upstream/NewAPI-sk-key接口文档.md`(接口②字段依据)· `docs/design/api_keys表字段设计.md`(`key_id` 外键指向) |
|
||||
| 定稿范围 | 列结构 + 去重与写入策略;同步引擎细节(调度、401 处理)不在此文 |
|
||||
| 字段数 | 23 = 20 上游字段 + 3 系统字段(上游响应共 21 个字段,其中展示序号 `id` 不落表,见 3.2) |
|
||||
|
||||
---
|
||||
|
||||
## 1. 表定位
|
||||
|
||||
- **一行 = 一次上游调用记录**(消费 / 错误等类型全覆盖)
|
||||
- 它是本项目最核心的数据资产:上游 `/api/log/token` 只保留最近 1000 条(实测当前速率下 ≈ 3 天),**错过即永久丢失**——本地全量累积是"任意时间范围用量"的唯一实现路径
|
||||
- 与 `api_keys` 多对一:每条日志经 `key_id` 指向其所属 key
|
||||
- 数据规模:实测约 300+ 条/天,量级为数十万行/年——单表 + 时间索引完全够用,无需分区或预聚合
|
||||
|
||||
## 2. 字段清单
|
||||
|
||||
### 2.1 系统字段(3 个,本系统产生)
|
||||
|
||||
| 列 | 类型 | 值来源 | 说明 |
|
||||
|----|------|--------|------|
|
||||
| `id` | bigserial PK | 自增 | 本地主键(与上游的展示序号 `id` 无关) |
|
||||
| `key_id` | bigint FK | 同步引擎 | → `api_keys.id`;日志归属与索引锚点 |
|
||||
| `dedupe_key` | text UNIQUE | 写入前生成 | **去重唯一键**:`${key_id}:${request_id}`;`request_id` 为空时退化为内容指纹(见 3.1) |
|
||||
|
||||
### 2.2 上游字段(20 个,`/api/log/token` 响应照收)
|
||||
|
||||
| 列 | 类型 | 上游字段 | 说明 |
|
||||
|----|------|---------|------|
|
||||
| `user_id` | bigint | `user_id` | 账号 ID(同一实例内恒定) |
|
||||
| `created_at` | timestamptz | `created_at` | 上游 UTC Unix 秒,入库转换;范围查询主维度 |
|
||||
| `type` | smallint | `type` | 日志类型 0-7 枚举(完整表见接口文档);照存不过滤 |
|
||||
| `content` | text | `content` | 消费记录为空串;错误记录为错误详情 |
|
||||
| `username` | text | `username` | 账号用户名 |
|
||||
| `token_name` | text | `token_name` | 快照存储(保真;展示取当前名口径由 API 契约另定) |
|
||||
| `model_name` | text | `model_name` | |
|
||||
| `quota` | bigint | `quota` | 原始 quota |
|
||||
| `prompt_tokens` | int | `prompt_tokens` | |
|
||||
| `completion_tokens` | int | `completion_tokens` | |
|
||||
| `use_time` | int | `use_time` | 秒 |
|
||||
| `is_stream` | bool | `is_stream` | |
|
||||
| `channel` | int | `channel` | 渠道 ID |
|
||||
| `channel_name` | text | `channel_name` | 普通用户视角为空串 |
|
||||
| `token_id` | bigint | `token_id` | 上游 token ID(对账用) |
|
||||
| `group_name` | text | `group` | PG 保留字改名 |
|
||||
| `ip` | text | `ip` | 普通用户视角为空串 |
|
||||
| `request_id` | text | `request_id` | 原值保留;JSON tag 为 `omitempty`,源码写入时有兜底 |
|
||||
| `upstream_request_id` | text NULL | `upstream_request_id` | **可选字段**:`omitempty`——空值时不出现、有值才出现(实测样本中缺席) |
|
||||
| `other` | jsonb | `other` | JSON 字符串解析入库(可查询;含缓存/倍率/延迟等扩展信息) |
|
||||
|
||||
> 上游响应中的展示序号 `id`(每次从 1 重编)不落表——见 3.2。
|
||||
|
||||
### 2.3 索引
|
||||
|
||||
| 索引 | 列 | 用途 |
|
||||
|------|-----|------|
|
||||
| PRIMARY KEY | `id` | |
|
||||
| UNIQUE | `dedupe_key` | 去重(`ON CONFLICT` 的落点) |
|
||||
| 普通 | `(key_id, created_at DESC)` | 明细页 + 按 key 聚合 |
|
||||
| 普通 | `(created_at DESC)` | 跨 key 合计与统计 |
|
||||
|
||||
## 3. 决策记录(含理由)
|
||||
|
||||
### 3.1 去重:单列 `dedupe_key`(即 `request_id` 的落库形态)
|
||||
|
||||
- 去重依据就是 `request_id`(上游为每次请求生成的唯一编号);`dedupe_key` 是它在库中的形态,99.9% 的情况下等于 `${key_id}:${request_id}`
|
||||
- **为什么不把 UNIQUE 直接挂在 `request_id` 列上**:PostgreSQL 的空值语义两难——`NULL` 在唯一索引中互不冲突(空值行会重复入库);而把空值转成空串则过度冲突(第二条空值记录被静默丢弃)。单列 `dedupe_key` + 空值指纹兜底从根上规避这两种坏结局
|
||||
- `key_id:` 前缀:跨 key 隔离的零成本保险
|
||||
- 内容指纹(仅 `request_id` 为空时启用):`sha256(key_id, created_at, model_name, quota, prompt_tokens, completion_tokens)`——同一条日志的重复拉取必然同指纹(正确去重),真实不同的请求指纹不同(都保留)
|
||||
- `request_id` 原值仍单独成列(2.2 之一)——两者并行,互不替代
|
||||
- **为什么它不做主键**:主键是「行身份」(短、稳定、永不变化),`dedupe_key` 是「防重签名」(长、由业务规则拼接、可能随上游演化)——让可变规则决定行身份是耦合隐患,且长字符串在引用场景(外键、调试、游标)更笨重;代理键(自增 `id`)+ 独立唯一约束是标准做法
|
||||
|
||||
### 3.2 上游展示序号 `id` 不落表
|
||||
|
||||
它是每次响应从 1 重编的展示序号(源码 `assignDisplayLogIds`),跨请求无意义——绝不能用于去重,也没有存储价值。
|
||||
|
||||
### 3.3 `other` 用 jsonb
|
||||
|
||||
- 原样保真(解析后完整存储)+ 查询能力(`other->>'cache_tokens'`);jsonb 是 PostgreSQL 内置类型
|
||||
- 普通用户视角的 `other` 已由服务端做过一层「可见性过滤」——我们存的就是用户实际可见的部分
|
||||
|
||||
### 3.4 `raw` 兜底列:不加
|
||||
|
||||
21 个候选字段(响应理论全集)已由实测 + 源码双重校对齐全,且 `other` 已单独 jsonb 保真(信息富集区);字段集变化的风险由「同步持续运行 + 代码随上游更新」覆盖,接受历史缺值的可能性。
|
||||
|
||||
### 3.5 命名与类型
|
||||
|
||||
- 上游字段采用原名;例外:`group` → `group_name`(PG 保留字)
|
||||
- 时间列统一 `timestamptz`(应用层做 UTC ↔ 北京时间转换);`quota` 用 `bigint`
|
||||
- `type` 用 `smallint` 存原值(0-7 为稳定枚举),展示层翻译(未知值原样显示数字)
|
||||
|
||||
## 4. 写入时机与策略
|
||||
|
||||
| 时机 | 动作 |
|
||||
|------|------|
|
||||
| 登记首轮同步 | 拉最近 1000 条 → 逐条入库 |
|
||||
| 每轮轮询 | 同上;`INSERT ... ON CONFLICT (dedupe_key) DO NOTHING`——幂等,只落新行 |
|
||||
|
||||
- 写入者唯一:同步引擎(不存在其他写入路径)
|
||||
- 日志不可变:已入库的行不做 UPDATE
|
||||
- 与本表相关的读取链:从 `api_keys` 取密文 → 解密 → 调上游 → 新日志写本表
|
||||
|
||||
## 5. 待定事项(关联后续环节)
|
||||
|
||||
| 事项 | 归属 |
|
||||
|------|------|
|
||||
| 明细/统计的查询口径(`type` 过滤策略) | API 契约 |
|
||||
| `other` 内高频键是否提为独立列(如 `cache_tokens`) | 出现真实分析需求时再评估 |
|
||||
| 数据保留策略(是否清理旧数据) | 后议 |
|
||||
@@ -0,0 +1,140 @@
|
||||
# 交互设计
|
||||
|
||||
> 本工具的界面与页面设计——**方向层**(页面集合 + 职责)+ **细节层**(跨页共性 + 逐页细节)。历史旧稿已删除(2026-09-14,内容已全部并入本文与 README / API 契约)。
|
||||
|
||||
| 项目 | 内容 |
|
||||
|------|------|
|
||||
| 状态 | 定稿(2026-09-13:方向层 + 跨页共性 10 条 + 逐页细节五节全部落定) |
|
||||
| 关联 | `README.md`(决策与事实)· `docs/design/API契约.md`(数据形状依据) |
|
||||
| 范围 | 方向层(页面集合 + 职责)+ 细节层(跨页共性 + 逐页细节) |
|
||||
|
||||
---
|
||||
|
||||
## 0. 大前提(2026-09-13)
|
||||
|
||||
### 0.1 路由分区与可见性
|
||||
|
||||
- **`/manage` 前缀 = 管理面**:管理通道的页面路由
|
||||
- **无前缀 = 查看面**:给使用者直接查看的页面路由
|
||||
- **管理面入口隐藏**:查看面不放任何指向管理面的入口——使用者无论拥有多少查看 / 统计功能,都无法从界面中进入管理面;直连管理面 URL 则由口令门帘拦截(两层:入口找不到 + 撞门要口令)
|
||||
|
||||
### 0.2 查看面职责:纯查询(对 key 只读)
|
||||
|
||||
- 查看面输入 key **仅用于查询**——凭手里已登记的 key 看用量;不再触发登记 / 轮询;输入未登记的 key → 提示「未收录」(措辞归细节层)
|
||||
- key 的登记与启停全部在管理面完成——**写操作只发生在管理面**(2026-09-13 定,取代原「输入即注册」流程)
|
||||
|
||||
## 1. 页面集合(方向层 · 已收口)
|
||||
|
||||
### 1.1 查看面(无前缀)
|
||||
|
||||
| 页面 | 路由 | 职责 | 阶段 |
|
||||
|------|------|------|------|
|
||||
| 用量日志 | `/usage-log` | 指定时间范围查看模型请求日志(数据表格呈现);表格上方以数据卡片显示该时段累计用量、预估消费等汇总 | 一期 |
|
||||
| 数据看板 | `/dashboard` | 固定时间窗口的统计视图(如模型消耗分布) | 二期候选(一期可不实现) |
|
||||
|
||||
- key 维度:输入式(一次可输入多个——延续核心需求「合并查询」);输入未登记的 key → 提示「未收录」
|
||||
- 根路径 `/` 重定向到 `/usage-log`
|
||||
- 卡片口径:累计用量与预估消费直接呈现折算数值(延续全项目「不标注币种」的基调)
|
||||
|
||||
### 1.2 管理面(`/manage` 前缀)
|
||||
|
||||
| 页面 | 路由 | 职责 |
|
||||
|------|------|------|
|
||||
| key 管理 | `/manage/api-keys` | 登记 / 清单 / 启停 |
|
||||
| 购买记录 | `/manage/purchases` | 登记 / 编辑删除 / 核对提示 |
|
||||
| 换算参数时间线 | `/manage/qpu` | 只读查看 |
|
||||
|
||||
- 根路径 `/manage` 重定向到 `/manage/api-keys`
|
||||
- 能力边界见 API 契约 §2.2–2.4 与本文件 §3
|
||||
|
||||
## 2. 跨页共性(细节层,2026-09-13 起逐条推)
|
||||
|
||||
| # | 项 | 定法 | 状态 |
|
||||
|---|----|------|------|
|
||||
| 1 | 使用场景与适配 | **桌面优先**(使用者 2–3 人、以看报表为主);不做移动适配 | 已定 |
|
||||
| 2 | 视觉基调 | **浅色单主题**(不做暗色切换);组件库默认主题 + 少量调色 | 已定 |
|
||||
| 3 | UI 组件库 | **naive-ui**(2026-09-13 定)——成熟度 / 文档 / 主题变量系统更稳,表格与日期快捷选项能力已验证满足本项目;antdv-next 未采纳(社区新项目:上游为 React 版 antd 6、非 antdv 继任,生态与文档面未成熟) | 已定 |
|
||||
| 4 | 时间显示格式 | **统一到秒**:`2026-09-11 19:37:12`(当天语境可简写 `HH:mm:ss`) | 已定 |
|
||||
| 5 | 金额与数字格式 | **分层**:折算金额汇总 2 位小数 / 明细行 4 位小数;token 数与 quota 原值千分位(`53,939,847`);不标币种不变 | 已定 |
|
||||
| 6 | 通用状态模式(加载 / 空 / 未收录 / 错误) | 见下方细则(2026-09-13 定) | 已定 |
|
||||
| 7 | 导航 | 查看面:一期单页**不做导航**;管理面:**侧边栏菜单 + 内容区**(传统后台布局);「查看面 →」入口的位置形态随逐页细节 | 已定 |
|
||||
| 8 | 表格通用交互 | **分页器**:页码器 + 每页条数选择(20/50/100/200,默认 50);**行详情**:展开行(`n-data-table` 内置),展示完整字段 | 已定 |
|
||||
| 9 | 时间范围控件 | `n-date-picker type="datetimerange"` + 快捷项(今天 / 近 7 天 / 近 30 天);**默认 = 最近 7 天** | 已定 |
|
||||
| 10 | key 输入控件与前端持久化 | 标签式(`n-dynamic-tags`),粘贴多行 / 逗号自动拆分;localStorage 记住上次输入、打开自动带出;**点「查询」才发请求** | 已定 |
|
||||
|
||||
**状态模式细则(#6,2026-09-13 定)**
|
||||
|
||||
| 状态 | 呈现 |
|
||||
|------|------|
|
||||
| 加载中 | 表格 `n-data-table` 内置 loading;卡片区 `n-skeleton` |
|
||||
| 初始(未输入 key) | 页面中央引导「输入你的 key 开始查询」——不渲染卡片与表格 |
|
||||
| 未收录 | 卡片区下方 warning 提示条:「以下 key 未收录:sk-…a1b2(请联系管理员登记)」;已收录部分照常展示 |
|
||||
| 时段无记录 | 表格空态「该时段没有记录」;卡片数值显示 0 |
|
||||
| 新 key 未首同步 | `data_until` 为 `null` → 卡片「数据截止」位显示「数据同步中…」 |
|
||||
| 查询失败(网络 / 5xx) | 页面内 error 提示条「查询失败:<message>」+「重试」按钮 |
|
||||
| 操作反馈(管理面) | `n-message` toast(登记 / 启停 / 保存的成功与失败) |
|
||||
|
||||
## 3. 逐页细节(细节层)
|
||||
|
||||
### 3.1 `/usage-log`(推导中)
|
||||
|
||||
**布局(2026-09-13 定)**
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────┐
|
||||
│ 筛选区:[key 标签输入框] [时间范围 datetimerange] [查询] │
|
||||
├────────────────────────────────────────────────────────────┤
|
||||
│ 卡片区:┌调用次数┐ ┌Token 用量┐ ┌预估消费┐ 数据截止 21:10:00 │
|
||||
│ │ 1,873 │ │1,469,134│ │ 23.45 │ (小字 + 悬停明细)│
|
||||
│ │消费·错误│ │ 缓存 89k │ │额度 53.9M│ │
|
||||
├────────────────────────────────────────────────────────────┤
|
||||
│ (如有)未收录提示条 │
|
||||
├────────────────────────────────────────────────────────────┤
|
||||
│ 明细表格(展开行详情) │
|
||||
│ 分页器(页码 + 每页条数) │
|
||||
└────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
- 「数据截止」不进卡片(它是注记不是指标):卡片区右端小字 + **悬停弹出 per-key 明细**(各 key 同步进度 / 未收录 / 已停用)——`meta` 信息完整呈现且不挤占卡片位
|
||||
- 卡片主 / 副行:调用次数(主总行数,副「消费 X · 错误 Y」)· Token 用量(主 prompt+completion,副缓存命中)· 预估消费(主金额,副「额度记账」quota 原值,供对账)
|
||||
|
||||
**明细列与呈现(2026-09-13 定)**
|
||||
|
||||
- **默认 8 列**:时间(到秒)· key(`token_name`)· 模型 · 类型(Tag)· 输入 tokens · 输出 tokens · 消费(`amount_rmb`,4 位小数)· 耗时
|
||||
- **展开行**(点行展开):完整字段——缓存 tokens · 流式 · `request_id` · `content`(错误详情)· 请求路径 · quota 原值 · **订阅归属完整组**
|
||||
- **错误行**:类型列 `n-tag`(消费 = 中性 / 错误 = 红),**无整行底色**;错误详情在展开行
|
||||
- **订阅归属**:默认列展示订阅名(`other.subscription_plan_title`,如「Max」);完整归属(额度实况 / 期间范围)在展开行——`purchases` §6 的「明细展示订阅归属」完整落地
|
||||
|
||||
### 3.2 `/manage/api-keys`(2026-09-13 推完)
|
||||
|
||||
- **清单 8 列**:key 名(`token_name`)· 状态徽章 · 累计已用 · 累计剩余(无限额度显示「不限」)· 到期 · 登记时间 · 数据截止 · 操作;其余字段(总额度 / 模型限制 / `consecutive_401s` / 额度新鲜度)归展开行
|
||||
- **状态徽章三面孔**:运行中(绿)· 已停用(灰,`manual`)· 已失效(红,`auto_401`,tooltip 注明「连续 401 自动摘除」);数据未首同步的状态体现在「数据截止」列(「同步中」)
|
||||
- **登记**:页头「+ 登记 key」→ `n-modal` 弹窗(粘贴 sk-key)→ 提交;三类失败各有文案——上游拒绝该 key / 该 key 已登记 / 上游暂不可用(稍后重试);成功后 toast + 清单插入新行(`log_synced_at: null` → 「同步中」)
|
||||
- **启停**:行内按钮;**停用弹确认**(`n-popconfirm`,防误点);**启用直接执行**(安全动作,成功后 toast + 行内状态更新;失败按错误码提示)
|
||||
- 「查看面 →」入口放侧边栏底部(低调,见 §2 #7)
|
||||
|
||||
### 3.3 其余页(待推)
|
||||
|
||||
| 页 | 重点 | 状态 |
|
||||
|----|------|------|
|
||||
| `/manage/purchases` | — | **2026-09-13 推完** |
|
||||
| `/manage/qpu` | — | **2026-09-13 推完** |
|
||||
| 口令门帘 | — | **2026-09-13 推完** |
|
||||
|
||||
**`/manage/purchases` 细节(2026-09-13 定)**
|
||||
|
||||
- **清单 8 列**:时间 · 类型(订阅 / 充值)· 实付金额 · 订阅(`#N`)· 期间 · 额度(`quota_granted`)· 备注 · 操作(编辑 · 删除;删除弹确认)
|
||||
- **表单用右侧抽屉**(`n-drawer`):`kind` 单选 · `paid_at` · `paid_amount`(必填)· `quota_granted`(必填)· `subscription_id` · 期间(`period_end` 未结束可空)· `note`;校验含期间顺序(start < end)
|
||||
- **核对提示与补录**:存在「观测到但未登记」时清单上方提示条「观测到 N 个未登记的订阅」;下方折叠区列出全部观测项(`#N` · 名称 · 日志条数 · 时间范围 · 观测额度 · 已登记 / 未登记);点「补录」开表单,**预填 `subscription_id` 与 `quota_granted`(观测值)**,期间不预填(观测范围 ≠ 订阅期间)
|
||||
|
||||
**`/manage/qpu` 细节(2026-09-13 定)**
|
||||
|
||||
- 表格两列:生效时间 · QPU 值(`id` 不显示);最新一行标「当前生效」
|
||||
- 页首说明文案:「quota → 额度美元的换算比率历史;引擎自动采样、只记变化点;当前生效值 = 最近一行」
|
||||
- 只有基线一行也属正常态(无特殊空态)
|
||||
|
||||
**口令门帘(2026-09-13 定)**
|
||||
|
||||
- 形态:访问 `/manage/*` 未验证时 → 全屏居中卡片(口令输入 + 「进入」);进入后跳转原目标页
|
||||
- 校验:直接用任一管理面请求校验(401 即口令错)——**不需新端点**;口令存 **sessionStorage**(关标签即忘、刷新仍在),请求拦截器自动附加 `X-Manage-Token`
|
||||
- 错误:401 → 输入框下方「口令不正确」;不做「记住我」
|
||||
@@ -0,0 +1,112 @@
|
||||
# 同步引擎设计
|
||||
|
||||
> 采集侧的引擎设计:调度、每轮动作、失败与状态机、可观测、部署初始化。它是「数据流动」的实现依据——**写入者只有它**(`api_keys` / `usage_logs` / `qpu_history` 三张表的写入时机见各表设计);查看面完全只读。
|
||||
|
||||
| 项目 | 内容 |
|
||||
|------|------|
|
||||
| 状态 | 定稿(2026-09-13:17 条议题全部落定——见 §2 结论索引与 §3 决策记录) |
|
||||
| 关联 | `docs/upstream/NewAPI-接口范围.md`(上游调用与失败语义)· 四张表字段设计(写入时机)· `docs/design/API契约.md`(查看面消费的数据形状)· `README.md`(既定决策与关键事实) |
|
||||
| 范围 | 调度与执行、失败处理、可观测、部署初始化 |
|
||||
| 不含 | 前端交互(交互设计)· HTTP 契约(API 契约,已定稿)· 多实例协调 / 历史回填 / 告警渠道(见 §4) |
|
||||
|
||||
---
|
||||
|
||||
## 1. 前提(已由其他文档锁定,不在此重新讨论)
|
||||
|
||||
- **统一 tick 方向已定**(2026-09-13 作者确认):一轮 = 遍历全部 `active` key,逐个查日志 → 去重入库
|
||||
- **后端单进程**(README 决策 7,2026-09-13 修订):API 与同步引擎同进程、**单实例**;前端静态产物**独立部署**(后端不托管前端产物);「定时器唯一」的前提就是这**一个**后端进程
|
||||
- **三个上游调用、失败互不影响**(`api_keys` §3.3):日志(写 `log_synced_at`)· 额度(写 `usage_synced_at`)· `/api/status`(QPU 采样)
|
||||
- **物理约束**:`/api/log/token` 固定返回最近 1000 条(实测 ≈ 3.2 天);同步停摆超约 3 天即永久丢数据(README 事实 4)
|
||||
- **失败语义**(`docs/upstream/NewAPI-接口范围.md` §1):401 = key 不存在 / 显式禁用(**唯一计入摘除的**);403 = 账号封禁;500 = 数据库错误;过期 / 额度耗尽在宽松档**仍可查**
|
||||
- **登录 / 登记不阻塞**:登记只做验证 + INSERT,首轮同步由引擎立即异步补一轮(API 契约 §2.2)
|
||||
- **启用需验证**:管理面手动启用 = 上游验证通过(`api_keys` §3.6);启用成功清零 `consecutive_401s`
|
||||
|
||||
## 2. 议题清单(2026-09-13 范围确认;逐条推导,结论逐步回填)
|
||||
|
||||
### 2.1 运行形态与调度
|
||||
|
||||
| # | 议题 | 初步倾向 | 状态 |
|
||||
|---|------|---------|------|
|
||||
| 1 | 载体:定时器跑在哪 | **已定(2026-09-13)**:随 Hono 进程内的定时器(后端单进程);**不引入 worker 线程、不拆独立进程**——影响评估见 §3 | 已定 |
|
||||
| 2 | 周期值 | **已定(2026-09-13):5 分钟**(固定间隔 tick + 防重入)——见 §3.2 | 已定 |
|
||||
| 3 | 启动行为 | **已定(2026-09-13):进程启动立即跑一轮**,随后进常规节奏——见 §3.2 | 已定 |
|
||||
| 4 | 单轮内执行顺序 | **已定(2026-09-13):全部 `active` key 串行遍历**——见 §3.2 | 已定 |
|
||||
|
||||
### 2.2 每轮的动作
|
||||
|
||||
| # | 议题 | 初步倾向 | 状态 |
|
||||
|---|------|---------|------|
|
||||
| 5 | 日志同步 | **已定(2026-09-13)**:每 key 拉最近 1000 条 → **整批单事务**入库(`ON CONFLICT (dedupe_key) DO NOTHING`;全成或全不写)→ 写 `log_synced_at`(**取请求发起时刻**)——见 §3.3 | 已定 |
|
||||
| 6 | 额度采集 | **已定(2026-09-13)**:每 key 取上游字段全组 + `usage_synced_at`(同口径);与日志**各自独立**(先日志后额度)——见 §3.3 | 已定 |
|
||||
| 7 | QPU 采样 | **已定(2026-09-13)**:独立于 key 循环,每轮一次读 `/api/status`;值变化才追加——见 §3.3 | 已定 |
|
||||
|
||||
### 2.3 失败与状态机
|
||||
|
||||
| # | 议题 | 初步倾向 | 状态 |
|
||||
|---|------|---------|------|
|
||||
| 8 | 401 摘除阈值 N | **已定(2026-09-13):N = 3**——见 §3.4 | 已定 |
|
||||
| 9 | 失败状态区分 | **已定(2026-09-13)**:只有 401 计数;403 / 500 / 网络不动计数——见 §3.4 | 已定 |
|
||||
| 10 | 单轮内重试 | **已定(2026-09-13):不重试**,等下一轮——见 §3.4 | 已定 |
|
||||
| 11 | 解密失败(`KEYS_MASTER_KEY` 变更) | **已定(2026-09-13)**:记日志、跳过、不影响其他 key;不计数——见 §3.4 | 已定 |
|
||||
|
||||
### 2.4 可观测与完整性
|
||||
|
||||
| # | 议题 | 初步倾向 | 状态 |
|
||||
|---|------|---------|------|
|
||||
| 12 | 错误日志 | **已定(2026-09-13)**:人类可读一行(`[sync]` 前缀 + `key_id` / 阶段 / 上游状态 / 耗时);不引日志库、不输出 JSON——见 §3.5 | 已定 |
|
||||
| 13 | 每轮摘要日志 | **已定(2026-09-13)**:一行「轮次完成:N key,成功 X,失败 Y,新增 Z 条,耗时 W 秒」——见 §3.5 | 已定 |
|
||||
| 14 | 「3 天窗口」警示 | **已定(2026-09-13)**:落后 > 24 小时则每轮打 warning(留 2 天人工余量);不做告警渠道——见 §3.5 | 已定 |
|
||||
|
||||
### 2.5 部署与初始化
|
||||
|
||||
| # | 议题 | 初步倾向 | 状态 |
|
||||
|---|------|---------|------|
|
||||
| 15 | QPU 基线 | **已定(2026-09-13)**:首次启动且 `qpu_history` 为空时插入当时取值——见 §3.6 | 已定 |
|
||||
| 16 | 无 key 时 | **已定(2026-09-13)**:空转不报错;QPU 采样照跑(独立于 key 循环)——见 §3.6 | 已定 |
|
||||
| 17 | 时间处理 | **已定(2026-09-13)**:上游 UTC 秒 → `timestamptz`;同步时刻取服务端时钟;输出统一 `+08:00`——见 §3.6 | 已定 |
|
||||
|
||||
## 3. 决策记录(逐条推导后回填)
|
||||
|
||||
### 1. 载体:后端进程内的定时器(2026-09-13 定)
|
||||
|
||||
- 定时器跑在 API 进程内(后端单进程、单实例);**不引入 worker 线程、不拆独立进程**(2026-09-13 作者裁决:现在没必要)
|
||||
- 影响评估结论:一轮的 CPU 占用只在「JSON 解析 + 入库参数组装」两个小片段(各几十 ms 级、5 分钟级低频);且最重的活(SQL 聚合)在 PostgreSQL 进程里跑——对前端响应时间的影响可忽略
|
||||
- 升级路径(真需要时再走,现在不做):同进程 **Worker 线程**(先挪 parse / 组装)→ 独立进程
|
||||
|
||||
### 2. 调度节奏(2026-09-13 定,议题 #2–#4)
|
||||
|
||||
- **周期 5 分钟**:固定间隔 tick(与上一轮耗时无关);**防重入**——上一轮未结束时本轮跳过并记一条日志(避免轮次叠加)
|
||||
- **启动立即跑一轮**:进程重启(含部署更新)后不等第一个 tick,尽快补齐窗口
|
||||
- **单轮内串行**:全部 `active` key 逐个处理——对上游友好、日志按 key 顺序可读;数量增长到影响单轮时长时再评估并发
|
||||
|
||||
### 3. 每轮的动作(2026-09-13 定,议题 #5–#7)
|
||||
|
||||
- **日志同步(#5)**:每 key 拉最近 1000 条(上游不支持参数、无法增量)→ **整批单事务入库**(`ON CONFLICT (dedupe_key) DO NOTHING`)——全成或全不写:失败则 `log_synced_at` 不前移「数据截止」,下一轮幂等重来
|
||||
- **额度采集(#6)**:每 key 取上游字段全组 + `usage_synced_at`;与日志**各自独立**(先日志、后额度,两个独立 try——谁失败都不影响对方写自己的时间戳)
|
||||
- **QPU 采样(#7)**:独立于 key 循环,每轮一次读 `/api/status`;值变化才追加一行(`qpu_history` §4)
|
||||
- **时间戳口径(#5/#6 共同)**:一律取**该次请求的发起时刻**——上游响应内容的上限是「处理请求那一刻」;请求发起至入库完成之间产生的日志不在本次响应内,不能许诺(与「数据截止取最慢」同一保守精神)
|
||||
|
||||
### 4. 失败与状态机(2026-09-13 定,议题 #8–#11)
|
||||
|
||||
- **401 摘除阈值 N = 3**:任一调用返回 401 → 本轮 `consecutive_401s` +1;**成功即清零**;其他失败不动计数。累计到 3 → `status = disabled` + `disabled_source = auto_401`(数据保留、不再轮询;复活走管理面启用 = 上游验证通过 + 计数清零)
|
||||
- **失败区分(#9)**:只有 401 是「key 失效」信号(上游实证:宽松档下过期 / 额度耗尽仍可查,唯一 401 = key 不存在 / 显式禁用);403 / 500 / 网络 = 其他故障,不计数
|
||||
- **不重试(#10)**:单轮内失败不立即重试,等下一轮(5 分钟后自然重试);3 天窗口余量下延迟无碍
|
||||
- **解密失败(#11)**:视为该 key 的故障——记服务器日志、跳过本轮、不影响其他 key;不计数、不摘除(非 401 语义),需人工重新登记
|
||||
|
||||
### 5. 可观测(2026-09-13 定,议题 #12–#14)
|
||||
|
||||
- **错误日志(#12)**:人类可读一行——`[sync]` 前缀 + `key_id` / 阶段(日志·额度·QPU)/ 上游状态或错误 / 耗时;输出 stdout / stderr 由 systemd 收集。**不引日志库、不输出 JSON**(单人维护,`journalctl` 直读)
|
||||
- **每轮摘要(#13)**:一行——`轮次完成:N key,成功 X,失败 Y,新增日志 Z 条,耗时 W 秒`
|
||||
- **超期警示(#14)**:`log_synced_at` 落后超过 **24 小时**时每轮打一条 warning——距 3 天极限留 2 天人工介入余量;不做告警渠道(超期的可见性已由 UI 的 `data_until` 承担)
|
||||
|
||||
### 6. 部署与初始化(2026-09-13 定,议题 #15–#17)
|
||||
|
||||
- **QPU 基线(#15)**:首次启动时若 `qpu_history` 为空,插入当时 `/api/status` 取值(`qpu_history` §4)
|
||||
- **无 key 时(#16)**:空转不报错;**QPU 采样照跑**(它独立于 key 循环)
|
||||
- **时间处理(#17)**:上游 `created_at`(UTC Unix 秒)→ `timestamptz`;同步时刻类时间戳取服务端时钟;对外输出统一 `+08:00`(API 契约 约定 9)
|
||||
|
||||
## 4. 明确不做
|
||||
|
||||
- 多实例协调(单进程部署,无并发实例)
|
||||
- 历史回填 / 补数(上游无此能力,1000 条窗口之外永久不可得)
|
||||
- 告警通知渠道(无通知对象与渠道;超期信息进服务器日志与 UI 的 `data_until`)
|
||||
@@ -0,0 +1,478 @@
|
||||
# NewAPI sk-key 查询接口文档
|
||||
|
||||
> 基于源码实证调研(QuantumNous/new-api),整理所有可通过 `sk-` API key 直接访问的查询接口。
|
||||
>
|
||||
> 调研日期:2025-07 月 · 源码版本:main 分支
|
||||
|
||||
---
|
||||
|
||||
## 0. 通用说明
|
||||
|
||||
### 认证方式
|
||||
|
||||
所有接口均使用 API key 鉴权,在请求头中携带:
|
||||
|
||||
```
|
||||
Authorization: Bearer sk-xxxxxxxxxxxx
|
||||
```
|
||||
|
||||
**不需要**账号登录态(access token / session)。这是仅凭 sk-key 即可查询的关键前提。
|
||||
|
||||
### BaseURL
|
||||
|
||||
即 NewAPI 站点地址,例如 `https://your-newapi-instance.com`。所有路径前不带多余前缀,直接拼接。
|
||||
|
||||
### 两种鉴权中间件
|
||||
|
||||
| 中间件 | 说明 |
|
||||
|--------|------|
|
||||
| `TokenAuth()` | 标准版:校验 key 有效、未过期、有剩余额度。过期或耗尽的 key 会被拒绝。 |
|
||||
| `TokenAuthReadOnly()` | 宽松版:只校验 key 存在、token 未被显式禁用、用户未封禁。**key 已过期、额度耗尽仍可查询**;**显式禁用(Disabled)的 key 会被 401 拒绝**(2026-09-13 源码复核,`middleware/auth.go` 注释原文:only explicitly disabled tokens are denied)。 |
|
||||
|
||||
> `/api/usage/token` 和 `/api/log/token` 用的是 `TokenAuthReadOnly()`,所以 key 额度耗尽、甚至过期后,仍能查到自己的用量记录;但 key 被管理员**显式禁用**后会收到 401(见上表)。
|
||||
|
||||
### 已确认的换算参数
|
||||
|
||||
| 参数 | 值 | 来源 |
|
||||
|------|-----|------|
|
||||
| `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 过期/额度耗尽也能查;被显式禁用则 401)
|
||||
|
||||
**请求参数**:无
|
||||
|
||||
**请求示例**:
|
||||
|
||||
```
|
||||
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": "20260911113652…",
|
||||
"other": "{\"billing_source\":\"subscription\",\"cache_tokens\":29056,\"frt\":3931,\"model_ratio\":0.075,\"request_path\":\"/v1/messages\",\"stream_status\":{\"status\":\"ok\"},…}"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**字段说明**(共 21 个字段;已按实测响应 + 上游源码校正,2026-09-11):
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `id` | int | 展示序号,**每次从 1 重编**——跨请求无意义,绝不能用于去重 |
|
||||
| `user_id` | int | 账号 ID(同一实例内恒定) |
|
||||
| `created_at` | int | 调用时间(UTC Unix 秒) |
|
||||
| `type` | int | 日志类型(完整取值见下);token 日志实见 2=消费、5=错误 |
|
||||
| `content` | string | 消费记录为空串;错误记录为错误详情文本 |
|
||||
| `username` | string | 账号用户名 |
|
||||
| `token_name` | string | 令牌名 |
|
||||
| `model_name` | string | 调用的模型名 |
|
||||
| `quota` | int | 本次消耗的 quota(换算美元:`quota ÷ 500000`);错误记录为 0 |
|
||||
| `prompt_tokens` | int | 输入 token 数;错误记录为 0 |
|
||||
| `completion_tokens` | int | 输出 token 数;错误记录为 0 |
|
||||
| `use_time` | int | 耗时(秒);错误记录为 0 |
|
||||
| `is_stream` | bool | 是否流式 |
|
||||
| `channel` | int | 渠道 ID |
|
||||
| `channel_name` | string | 渠道名(普通用户视角为空串) |
|
||||
| `token_id` | int | 上游 token ID(对账用) |
|
||||
| `group` | string | 分组名(默认 `default`) |
|
||||
| `ip` | string | 请求 IP(普通用户视角为空串) |
|
||||
| `request_id` | string | 请求 ID(格式:`yyyyMMddHHmmss` 前缀 + 随机串);JSON tag 为 `omitempty`,但源码写入时有兜底保证非空——**去重唯一依据** |
|
||||
| `upstream_request_id` | string | 上游请求 ID;JSON tag 为 `omitempty`——**空值时不出现、有值时才出现**(实测样本中缺席);消费方需按「可选字段」处理 |
|
||||
| `other` | string | JSON 字符串,扩展信息(结构见下) |
|
||||
|
||||
**`type` 完整取值(上游源码 `model/log.go`;常量注释明确「刻意不用 iota,以保证枚举值永不改变」)**:
|
||||
|
||||
| 值 | 常量 | 含义 | 可能出现在 token 日志? |
|
||||
|----|------|------|------------------------|
|
||||
| 0 | `LogTypeUnknown` | 未知 | 兜底值 |
|
||||
| 1 | `LogTypeTopup` | 充值 | 否(账号级,无 token_id) |
|
||||
| 2 | `LogTypeConsume` | 消费 | **是**(实测) |
|
||||
| 3 | `LogTypeManage` | 管理操作 | 否(审计类) |
|
||||
| 4 | `LogTypeSystem` | 系统 | 否 |
|
||||
| 5 | `LogTypeError` | 错误 | **是**(实测) |
|
||||
| 6 | `LogTypeRefund` | 退款 | 可能(任务计费链路带 token_id) |
|
||||
| 7 | `LogTypeLogin` | 登录 | 否(账号级) |
|
||||
|
||||
> 本接口按 `token_id` 过滤、不按 `type` 过滤——凡是「带 token_id 写入」的类型都可能返回。
|
||||
|
||||
**`other` 字段(JSON 字符串,内容随 `type` 变化,实测 2026-09-11)**:
|
||||
|
||||
- 消费记录(`type=2`):`billing_source`、`cache_ratio`、`cache_tokens`、`completion_ratio`、`frt`、`group_ratio`、`model_price`、`model_ratio`、`reasoning_effort`、`request_conversion`、`request_path`、`stream_status`(对象)、`subscription_*`(订阅维度,共 8 个)、`user_group_ratio`、`wallet_quota_deducted`
|
||||
- 错误记录(`type=5`):`error_code`、`error_type`、`request_path`、`status_code`
|
||||
- 注:普通用户视角下,服务端会先对 `other` 做一层「可见性过滤」再返回——上表为过滤后实际可见的键
|
||||
|
||||
**关键限制**:
|
||||
|
||||
- **硬上限 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 = 美元
|
||||
```
|
||||
|
||||
### 消费记录的 `quota` 是怎么算出来的(实测反推,2026-09-13)
|
||||
|
||||
按 tokens 计费的模型,`quota` 在**请求时**由「倍率 × 用量」算出并固化进日志(此后不再变化):
|
||||
|
||||
```
|
||||
quota = ( (prompt_tokens − cache_tokens) ← 提示 tokens(未命中缓存部分)
|
||||
+ cache_tokens × cache_ratio ← 缓存命中部分按折扣
|
||||
+ completion_tokens × completion_ratio ) ← 补全 tokens 按倍率
|
||||
× model_ratio ← 模型倍率
|
||||
× group_ratio ← 分组倍率
|
||||
```
|
||||
|
||||
- 验证:6/6 条实测日志逐条吻合(差值仅为四舍五入)
|
||||
- 按次计费模型走 `model_price` 分支:`quota = model_price × QuotaPerUnit × group_ratio`(源码 `relay/helper/price.go`)
|
||||
- **公式中没有 `QuotaPerUnit`**:倍率与美元的锚定(`model_ratio = 1` ⇔ `$0.002 / 1K tokens`)已隐含它——倍率制下改 `QuotaPerUnit` 等于全站重定标;美元定价路径(`model_price` / 表达式 `$/1M`)才显式乘 `QuotaPerUnit`
|
||||
|
||||
### 实测数据验证
|
||||
|
||||
| 来源 | 原始值 | 换算 | 结果 |
|
||||
|------|--------|------|------|
|
||||
| 接口① `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
|
||||
@@ -0,0 +1,93 @@
|
||||
# NewAPI 接口范围
|
||||
|
||||
> new-api-key-usage 使用的 NewAPI 接口范围:本项目对接的全部 9 个接口,含鉴权档位与各自对本项目的用途——本项目所有上游调用都出自这份范围。与 `NewAPI-sk-key接口文档.md` 的分工:那边是「sk-key 能访问什么」的源码实证全集(430 行,字段级细节);本文是「本项目实际使用哪些接口」的范围清单。
|
||||
>
|
||||
> 整理日期:2026-09-09 · 重定位:2026-09-11 · 2026-09-13 增补 §3(范围之外)与 ④ 行为事实 · 依据:NewAPI 源码考证 + 接口实测 · 实测环境:NewAPI 实例
|
||||
|
||||
---
|
||||
|
||||
## 1. 鉴权三态总览
|
||||
|
||||
本项目对接的接口按鉴权中间件分三档:
|
||||
|
||||
| 鉴权档 | 中间件 | 语义 | 本项目的请求方式 |
|
||||
|--------|--------|------|----------------|
|
||||
| **公开**(none) | 无 / `HeaderNavModulePublicOrUserAuth` | 不持有任何凭证即可访问 | 直接请求,无需 key |
|
||||
| **严格**(strict) | `TokenAuth` | 校验 key 有效、未过期、有剩余额度;无效直接被拒 | 携带 sk-key 请求 |
|
||||
| **宽松**(lenient) | `TokenAuthReadOnly` | 只验 key 存在且用户未封禁;**过期/额度耗尽仍可查**,但**显式禁用(Disabled)会被 401 拒绝** | 携带 sk-key 请求;**本项目主要依赖此档** |
|
||||
|
||||
> 宽松档的「显式禁用被拒」是源码考证的补充结论(`middleware/auth.go` 的 `TokenAuthReadOnly`),影响本项目的 401 摘除逻辑:收到 401 应区分「key 被禁用」(摘除)与「瞬时故障」(重试)。
|
||||
>
|
||||
> 失败状态码全集(2026-09-13 源码复核):**401** = key 不存在(`MsgTokenInvalid`)或 token 被显式禁用(`MsgTokenStatusUnavailable`);**403** = 账号被封禁;**500** = 数据库错误。→ 摘除计数只认 401;403 / 500 视为其他故障,不计入。另:宽松档**允许** key 过期(Expired)与额度耗尽(Exhausted)状态查询,源码注释明确「only explicitly disabled tokens are denied」。
|
||||
|
||||
## 2. 接口清单
|
||||
|
||||
### 公开接口组(无需 sk-key)
|
||||
|
||||
#### ① `GET /api/user/groups` — 用户分组与倍率
|
||||
|
||||
- **返回**:站点配置的用户分组(default / vip 等)与倍率;**未启用分组时 `data` 为空对象**(当前站点实测即此状态)
|
||||
- **用途**:解释「同一模型为什么计费不同」——分组倍率影响同一模型的实际消耗
|
||||
- **本项目用途**:低优先级;仅当站点启用分组倍率后才有内容
|
||||
|
||||
#### ② `GET /api/perf-metrics/summary` — 全站模型服务质量
|
||||
|
||||
- **参数**:`hours`(统计窗口小时数,默认 24)
|
||||
- **返回**:每个模型的 `avg_latency_ms`(平均延迟)、`success_rate`(成功率 %)、`avg_tps`(每秒 token 数)、`recent_success_series`(按小时采样的成功率时间序列);明细版 `/api/perf-metrics?model=xxx`(model 必填,另支持 group/hours)
|
||||
- **鉴权细节**:`HeaderNavModulePublicOrUserAuth("pricing")`——默认公开,管理员可为 pricing 模块开启 requireAuth
|
||||
- **实测**:HTTP 200,含站点真实模型的服务质量数据
|
||||
- **本项目用途**:**高**——本工具独有视角。单个 sk-key 日志看不到全局服务质量;使用者反馈「慢/报错多」时,此接口能定位是模型问题还是自身问题。技术设计时可作为「模型健康度」功能的数据源
|
||||
|
||||
#### ③ `GET /api/rankings` — 全站模型用量排行
|
||||
|
||||
- **参数**:`period`(统计周期,默认 week)
|
||||
- **返回**:每个模型的 `rank`(名次)、`previous_rank`(上期名次)、`total_tokens`(总用量)、`share`(份额)、`growth_pct`(环比增长 %)
|
||||
- **实测**:HTTP 200,14KB 数据
|
||||
- **本项目用途**:中——项目作者选「主推模型」(便宜且稳定)的数据依据;配合②的成功率可识别衰退模型
|
||||
|
||||
#### ④ `GET /api/status` — 站点配置与计费参数
|
||||
|
||||
- **返回**:60+ 字段,与本项目相关的计费字段:`quota_per_unit`(**换算比率当前生效值,管理员可改**)、`quota_display_type`(USD/CNY/TOKENS,影响 billing 接口语义)、`usd_exchange_rate`、`price` 等
|
||||
- **本项目用途**:**关键**——换算比率的唯一权威来源。`QuotaPerUnit = 500000` 只是 NewAPI 默认值(源码 `common/constants.go:22`,`$0.002 / 1K tokens` 锚点),实现时必须从本接口实时读取、不得硬编码;同时感知 `quota_display_type` 切换(若切到 CNY/TOKENS,billing 接口数字语义会变)
|
||||
- **补充:`quota_per_unit` 的行为事实(2026-09-13 源码评估)**:它由全局变量承载,改设置是「先写库、再热更新内存」(`model/option.go` 的 `UpdateOption` → `updateOptionMap` 的 `case "QuotaPerUnit"`)——改后**立即全站生效、无需重启**。三条关键行为:
|
||||
- **无历史**:`options` 表一 key 一行、改即覆盖——任何接口都读不到过去的取值;管理审计只记"键名被改过"、**不记值**
|
||||
- **无编辑入口**:后台设置页该字段默认隐藏且只读(`web/src/features/system-settings/general/pricing-section.tsx`),修改只能走 API
|
||||
- **解读跟随当前值**:一切对外的金额换算(历史记录解读、billing 接口、界面显示)都在读取时实时使用当前值——改它 = 全站金额表达整体平移(数据库里的 quota 整数不受影响)
|
||||
|
||||
### 严格鉴权组(TokenAuth,sk-key)
|
||||
|
||||
#### ⑤ `GET /v1/dashboard/billing/subscription` — 总额度上限(美元)
|
||||
|
||||
- 返回 `hard_limit_usd` 等;无限额度 key 返回 `100000000`(一亿占位值,无参考意义)
|
||||
- 受 `DisplayTokenStatEnabled` 与 `quota_display_type` 影响;与⑥相减可得剩余额度
|
||||
- **本项目用途**:中——按 key 统计表的「累计剩余」列候选数据源
|
||||
|
||||
#### ⑥ `GET /v1/dashboard/billing/usage` — 累计已用(美分)
|
||||
|
||||
- 返回 `total_usage`(美分,÷100 = 美元);可与⑧的 `total_used ÷ quota_per_unit` 交叉对账(不一致 = 站点改过比率,见④)
|
||||
- **本项目用途**:中——同上,对账与校验用途
|
||||
|
||||
#### ⑦ `GET /v1/models` — 可用模型列表
|
||||
|
||||
- 数据源是数据库(渠道实际配置);与 `/v1/models/:model` 单查接口数据源不同
|
||||
- **本项目用途**:低——调试期验证渠道配置用
|
||||
|
||||
### 宽松鉴权组(TokenAuthReadOnly,sk-key)
|
||||
|
||||
#### ⑧ `GET /api/usage/token` — token 级用量汇总(原始 quota)
|
||||
|
||||
- 返回 `total_used`(累计,非当月)、`total_granted`、`total_available`、`unlimited_quota`
|
||||
- **本项目用途**:**高**——按 key 统计表「全周期累计(已用/剩余)」两列的直接数据源;key 耗尽后仍可查(用完也能对账)
|
||||
|
||||
#### ⑨ `GET /api/log/token` — 最近 1000 条调用日志
|
||||
|
||||
- **硬上限 1000 条**(`common.MaxRecentItems = 1000`)、不支持任何参数、最新在前;响应里的 `id` 是每次从 1 重编的展示序号,**跨请求无意义,去重必须用 `request_id`**
|
||||
- 大响应:pretty-print 后 2~4MB——本项目同步引擎需直面的单次响应体积
|
||||
- **本项目用途**:**核心**——增量同步引擎的唯一数据源;本地攒全量后,任意时间范围的用量/明细/模型统计都是本地聚合查询
|
||||
|
||||
## 3. 范围之外(sk-key 不可达)
|
||||
|
||||
1. **账号级统计接口**(如 `/api/log/self/stat`,支持时间范围 + quota 汇总):需账号登录态,sk-key 访问 401——本项目以「本地攒日志 + 聚合」替代
|
||||
2. **订阅信息**(订阅编号 / 生效期间 / 额度):**没有免 auth 的接口可直接查询**(作者实测,2026-09-13);登录态的**「钱包」页面**可见——显示订阅编号(形如「订阅 #N」)、状态(生效中 / 已过期 + 过期时间)、总额度与已用 / 剩余:
|
||||
- 订阅编号**推测**与日志 `other.subscription_id` 为同一体系(实现期以真实数据核对)
|
||||
- 对应设计:订阅信息由购买者手工录入(见 `docs/design/purchases表字段设计.md`)
|
||||
Reference in New Issue
Block a user