From 3e12db1706d2343d74b7638b3c1459c004253038 Mon Sep 17 00:00:00 2001 From: skycurtain Date: Wed, 7 Oct 2026 12:53:37 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E5=90=8C=E6=AD=A5=E5=BC=95=E6=93=8E?= =?UTF-8?q?=E8=AE=BE=E8=AE=A1=E5=AE=9A=E7=A8=BF=EF=BC=8CAPI=20=E6=97=B6?= =?UTF-8?q?=E5=88=BB=E6=94=B9=E4=B8=BA=20Unix=20=E7=A7=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 独立 apps/engine、setTimeout 轮次、engine_status 快照、不为 CFW 预留; 公共约定 §4 响应时刻统一 Unix 秒;契约 engine 增 sync_interval_ms。 --- README.md | 1 + docs/design/API契约-公共约定.md | 7 +- docs/design/API契约-管理面.md | 6 +- docs/design/同步引擎设计.md | 203 ++++++++++++++++++++++++++++++++ docs/design/技术选型.md | 2 +- 5 files changed, 213 insertions(+), 6 deletions(-) create mode 100644 docs/design/同步引擎设计.md diff --git a/README.md b/README.md index f8f3e75..57d002c 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,7 @@ | [`docs/design/API契约-管理面.md`](docs/design/API契约-管理面.md) | 管理面 16 端点:引导与会话三态、引擎状态、总览聚合、key 登记四态、下钻明细、支付、成员(令牌明文仅一次)、审计;5 项裁定(经审通过)+ 4 项遗留(key 详情已裁决:实现期定) | 定稿(2026-10-02) | | [`docs/design/API契约-成员视图.md`](docs/design/API契约-成员视图.md) | 成员视图 3 端点:令牌验证与页头、单页聚合(含支付参数卡判定)、日志明细(DTO 与管理面同构);4 项裁定(经审通过)+ 1 项遗留 | 定稿(2026-10-02) | | [`docs/design/骨架搭建指南.md`](docs/design/骨架搭建指南.md) | monorepo 骨架 + 在线文档页的一次性执行指南(供作者或任一 AI 执行者):前置、步骤、验收清单、红线、依据文档索引 | 草案(待审阅) | +| [`docs/design/同步引擎设计.md`](docs/design/同步引擎设计.md) | 独立 `apps/engine` 进程:手写 `setTimeout` 轮次、失败只记录、`engine_status` 快照供 `/api/admin/engine`;周期可配、**不为 CFW 预留** | 定稿(2026-10-05) | | [`docs/references/NewAPI-接口范围.md`](docs/references/NewAPI-接口范围.md) | 计费平台在 NewAPI 中可用的接口参考:鉴权档位、请求与返回结构、字段语义、实测行为、源码出处(**唯一上游参考**) | 事实依据 | - 命名与分区:**目录用英文、文档名用中文**;`docs/design/`(我们定的方案与决策)· `docs/references/`(平台可用接口参考——实证事实,不做设计决策) diff --git a/docs/design/API契约-公共约定.md b/docs/design/API契约-公共约定.md index cbf7d13..8bd0159 100644 --- a/docs/design/API契约-公共约定.md +++ b/docs/design/API契约-公共约定.md @@ -56,9 +56,10 @@ ## 4. 时间口径 -- **传输格式:ISO 8601 UTC 字符串**(如 `2026-10-02T14:22:05Z`)——自描述;存储层维持 UTC Unix 秒(表设计约定),DTO 层转换;北京时间展示由前端完成(共性 §3.3) -- **时间范围参数**:`from` / `to`,秒级 ISO 8601 UTC,**左闭右开 `[from, to)`**——快捷项边界无歧义、无跨桶重复计数 -- 快捷项(最近 7 / 30 天、本订阅期间、自定义起止)是前端预置逻辑:前端算出 `from/to` 再请求,后端无快捷参数;「本订阅期间」的期间数据来源归后两篇各自定义 +- **响应时刻字段:Unix 秒整数**(UTC 时刻,如 `1780000000`)——无时区歧义;本地/北京时间由展示层转换 +- **请求时间参数**(`from` / `to` 等):ISO 8601 UTC 字符串(便于试调);范围 **左闭右开 `[from, to)`** +- 存储层维持 timestamptz / 上游 Unix 秒;DTO 输出统一 `toUnixSec` +- 快捷项(最近 7 / 30 天、本订阅期间、自定义起止)是前端预置逻辑:前端算出 `from/to` 再请求,后端无快捷参数 ## 5. 数值口径(对齐共性 §3.2) diff --git a/docs/design/API契约-管理面.md b/docs/design/API契约-管理面.md index a5a2f18..98d6fa1 100644 --- a/docs/design/API契约-管理面.md +++ b/docs/design/API契约-管理面.md @@ -67,6 +67,7 @@ ```json { "running": true, + "sync_interval_ms": 300000, "last_round": { "started_at": "2026-10-02T08:00:00Z", "keys_total": 18, "succeeded": 16, "failed": 2, @@ -75,7 +76,8 @@ } ``` -`failed_api_key_ids` 支撑 Key 管理「最近轮次失败」筛选与健康带失败数跳转;`last_round` 为 `null`(引擎尚未跑过)合法。**状态存储与字段细节归同步引擎设计(待立稿),两稿以本端点字段为衔接对齐点。** +`sync_interval_ms`:当前同步周期(毫秒),**只读观测**,经 env 配置、无修改接口。 +`failed_api_key_ids` 支撑 Key 管理「最近轮次失败」筛选与健康带失败数跳转;`last_round` 为 `null`(引擎尚未跑过)合法。状态存储为 `engine_status` 单行快照(《同步引擎设计》§5)。 ## 4. 全员总览 @@ -264,5 +266,5 @@ 1. **管理面日志明细的界面落点**——**已裁决:实现期定**(先做「跳 Key 管理定位到该行」,详情视图待真实数据再定); 2. **审计日志表结构**——归技术稿,以本文 §9 DTO 字段为对齐锚点; -3. **`GET /api/admin/engine` 的状态存储**——归同步引擎设计(待立稿),两稿衔接对齐; +3. **`GET /api/admin/engine` 的状态存储**——已对齐《同步引擎设计》§5(`engine_status` 单行快照 + 只读 `sync_interval_ms`); 4. **令牌生成细节**(随机源 / 长度 / 前缀)——归技术稿。 diff --git a/docs/design/同步引擎设计.md b/docs/design/同步引擎设计.md new file mode 100644 index 0000000..a27b145 --- /dev/null +++ b/docs/design/同步引擎设计.md @@ -0,0 +1,203 @@ +# 同步引擎设计 + +> 母体形态下同步引擎的进程形态、调度与轮次语义:拉什么、写什么、失败怎么办、状态如何暴露。**范围声明(2026-10-05 作者裁决)**:① 本阶段**不考虑 Cloudflare Workers,也不为 CFW 做准备**;② 引擎**独立于 HTTP 服务进程**(`apps/engine`),避免后台 CPU 与 API 抢事件循环;③ 调度用**手写 `setTimeout` 循环**(不引入 cron 库),周期**可配置**。字段与去重的权威仍在七篇表设计;本文定「引擎怎么转」。 + +| 项目 | 内容 | +|------|------| +| 状态 | 定稿(2026-10-05) | +| 关联 | 《api_keys表字段设计》§4 写入时机 · 《usage_logs表字段设计》§3.1 去重 · 《qpu_history表字段设计》§4 采样 · 《API契约-管理面》§3 `engine` 端点 · 《技术选型》§4 monorepo(本文增补 `apps/engine`)· `docs/references/NewAPI-接口范围.md` | +| 产出 | 进程形态 · 调度与轮次语义 · 失败与观测 · 与 `/api/admin/engine` 的状态衔接 · 配置与验收 | + +--- + +## 1. 进程形态(与后端分离) + +同步引擎是**独立入口进程**(`apps/engine`),与 `apps/server`(Hono)**分进程、可同机部署**: + +```text +apps/server ──► HTTP(/api/admin · /api/member)──► packages/db +apps/engine ──► 定时 syncRound() ──► packages/db + │ + └────────► packages/core/upstream(NewAPI) +``` + +| 约定 | 说明 | +|------|------| +| 共享 | `packages/core`(上游客户端、dedupe、折算)· `packages/db`(仓储)——**业务能力仍一份** | +| 不共享 | 进程内存、事件循环、调度器;HTTP 与引擎**互不调用** | +| 不调自家 API | 引擎只打上游 NewAPI + 写库;不走 `/api/admin/*` | +| 部署 | 开发:两个命令(`dev:server` / `dev:engine`);生产 compose 两个 service(均连同一 PostgreSQL) | + +**为何分进程(2026-10-05 复审)**:一轮可能解析上千条日志并做指纹/加密/批量写,若与 API 同进程会争事件循环,查询延迟与「引擎是否在跑」绑死;定时器漂移本身对本业务无害,**互扰才是拆分动因**。 + +## 2. 调度(方案 A:手写 `setTimeout`) + +不引入 `croner` / `node-cron` 等库;用标准定时器实现「一轮结束 → 约下一枪」。 + +```text +loop(): + if inFlight: log warn; schedule(); return + inFlight = true + try: + await syncRound() + finally: + inFlight = false + schedule() // 本轮结束后再等 interval + +schedule(): + setTimeout(loop, SYNC_INTERVAL_MS) +``` + +| 项 | 约定 | +|----|------| +| 周期 | **`SYNC_INTERVAL_MS`**,默认 `300000`(5 分钟);由 env 配置,非法值启动失败 | +| 语义 | **结束后 + interval**(周期 = 间隔 + 上轮耗时);不追求整点 cron | +| 防堆积 | 上一轮未结束则跳过 / 不叠加(`inFlight`);**禁止**裸 `setInterval(async …)` | +| 启动 | `apps/engine` 进程起来后立即跑一轮,再进循环 | +| 停止 | 收到退出信号:不强制中断本轮(可等完);不保证优雅排空 | +| 错过 | 不追补「错过的时刻」;下一轮全量拉最近 1000 条即补偿(上游窗口语义) | + +> 与交互文案「约 5 分钟 / ≤5 分钟」对齐:实际最坏接近 `interval + 单轮耗时`;若单轮通常远小于 interval,观感仍约为 5 分钟。需要「整点对齐」时再改为 `nextSlot = ceil(now / interval) * interval`(非本阶段)。 + +## 3. 一轮 `syncRound()` 做什么 + +```text +1. 选 key:api_keys.status = active 全量(id 升序,串行) +2. 对每个 key: + a. 解密 key_ciphertext → sk 明文 + b. GET /api/usage/token + 成功 → UPDATE 上游字段全组 + usage_synced_at + 失败 → 记入本轮 failed,不写 api_keys + c. GET /api/log/token + 成功 → 逐行 dedupe_key → INSERT ON CONFLICT DO NOTHING + → UPDATE log_synced_at + 失败 → 记入本轮 failed,不写 log_synced_at + d. (建议)每 key 之间 setImmediate/短 sleep 让出,降低单轮 CPU 连续占用 +3. QPU 采样(每轮一次,与 key 无关): + GET /api/status → quota_per_unit + 与 qpu_history 最新值相同 → 不写;变化 → INSERT(effective_from = 本轮时刻) +4. 收束 last_round(§5),打一行轮次日志 +``` + +### 3.1 与表设计的对齐(勿另立标准) + +| 行为 | 权威出处 | +|------|----------| +| 仅写成功结果;失败不写行 | 《api_keys表字段设计》§4 | +| `dedupe_key` 规则(type=2 / 非 2 / 空 request_id) | 《usage_logs表字段设计》§3.1 | +| `ON CONFLICT (dedupe_key) DO NOTHING` | 同文写入时机 | +| QPU **值变化才追加** | 《qpu_history表字段设计》§4 | +| 登记后**异步补一轮**,不阻塞 201 | 《api_keys表字段设计》§4 | + +### 3.2 幂等与重试 + +- 重复拉取 → `dedupe_key` 合并,零副作用。 +- 单 key 失败不影响其他 key;不写「半行」已成功的 key 会保留。 +- 不做单 key 积极重试;**周期即重试**。 + +### 3.3 登记后补跑(已裁决:方案 A) + +- Key 登记(`POST /api/admin/keys`)成功后,**不**在 HTTP 请求里同步拉日志,**也不**引入秒级唤醒/优先队列。 +- **最迟一个 `SYNC_INTERVAL_MS` 内**由下一轮自然覆盖(独立进程下避免 IPC;与交互文案「约 5 分钟内完成」一致)。 +- 与《api_keys表字段设计》§4「首轮日志同步不阻塞登记响应」不冲突:登记 201 立即返回;首轮日志异步、且上限为一个调度周期。 + +## 4. 失败语义(R2:记录、不决策) + +| 情况 | 引擎行为 | 可见 | +|------|----------|------| +| 401 / 403 / 5xx / 超时 | 该 key 本轮失败;不写 api_keys / log_synced_at | `log_synced_at` 停滞;`last_round.failed_api_key_ids` | +| 登记阶段 422/502 | 不属引擎 | 登记接口已处理 | + +**不做**:自动停用、自动摘除、自动告警(R4 延后)。 + +运行日志(stdout)建议**中文描述**(与平台文案基调一致),例如: + +```text +同步轮次完成:key=18 成功=16 失败=2 耗时=812ms +同步失败:key_id=3 原因=401 +``` + +访问日志、启动日志与错误日志同样使用中文表述(实现已统一)。 + +## 5. 引擎状态与 `/api/admin/engine` 衔接 + +契约响应形态不变。**存储:`engine_status` 单行快照表**(已裁决);engine 每轮写入,server 的 `/api/admin/engine` 只读该表(不 import engine 包、不走 IPC)。 + +```text +engine_status(单行) + running: boolean + sync_interval_ms: number // 只读观测:当前生效周期 + last_round: { started_at, keys_total, succeeded, failed, failed_api_key_ids } | null + updated_at +``` + +- 与《API契约-管理面》§3 字段一一对应(`last_round` 为 `null` 合法);`sync_interval_ms` 为**只读观测**,便于界面展示,**不提供修改接口**。 +- `GET /api/admin/keys?recent_failed=true` 读 `failed_api_key_ids`。 +- **不做**完整 `engine_rounds` 历史表;表名/列名实现期可微调,语义不变。 + +## 6. 代码落点 + +| 逻辑 | 位置 | 约束 | +|------|------|------| +| 上游 HTTP | `packages/core/upstream.ts`(补 `fetchLogs`) | Web 标准 API | +| dedupe / 折算 | `packages/core` | 已有 | +| SQL | `packages/db`(engine 写入用仓储) | 方言不外泄 | +| `syncRound` + 定时循环 + 进程入口 | **`apps/engine`**(如 `src/loop.ts`、`src/sync-round.ts`、`src/index.ts`) | 独立 `package.json` | +| `/api/admin/engine` 读状态 | `apps/server` 读 `engine_status` | 不 import engine 包 | + +Monorepo 增补: + +```text +apps/ + server/ # HTTP + engine/ # 同步循环(本设计) + web/ +``` + +## 7. 配置(env,由 `apps/engine` 读取) + +| 键 | 默认 | 含义 | +|----|------|------| +| `SYNC_INTERVAL_MS` | `300000` | 轮询间隔毫秒(**可配置**;默认 5 分钟)。**仅 env、仅启动时读**;管理面/API **不提供修改**(运维旋钮而非业务口径) | +| 复用 | `DATABASE_URL` · `SK_KEY_ENCRYPTION_SECRET` · `NEWAPI_BASE` | 与 server 相同语义 | + +`server` **不读** `SYNC_*`。 + +## 8. 非目标 + +1. **CFW / Cron Triggers / D1**——不预留; +2. **cron 库 / cron 表达式**——本阶段方案 A,只用毫秒间隔; +3. **经 server / API / 管理面修改同步周期**——interval 只走 env;状态快照中的 `sync_interval_ms` 仅只读观测; +4. **R3 备份 / R4 告警**; +5. **多 key 并行、分布式锁、多 engine 实例**——单 engine 实例部署约定(文档写明即可); +6. **轮次历史表、调用自家 HTTP API、进程间 IPC**——协作只经 PostgreSQL。 + +## 9. 验收清单(实现后) + +- [ ] 只启 `dev:engine`(或 compose 仅 engine+db):日志入库、`log_synced_at` 前进 +- [ ] 只启 `dev:server`:HTTP 正常,无轮询日志 +- [ ] 同一数据重复轮次不产生重复行 +- [ ] 某 key 401:其余 key 正常;`engine_status.last_round.failed_api_key_ids` 含之 +- [ ] QPU 不变无新行,变化 +1 +- [ ] `GET /api/admin/engine` 与契约一致(跨进程读到 engine 写的状态) +- [ ] `SYNC_INTERVAL_MS` 可调;**不启动 `apps/engine` 即不轮询**(独立进程,无 `SYNC_ENABLED` 开关) +- [ ] 无定时器叠加(人为拉长一轮耗时,验证 `inFlight`) + +## 10. 裁决记录 + +### 2026-10-05 · 进程与调度形态 + +- 独立 `apps/engine`,与 `apps/server` 分进程;不为 CFW 预留。 +- 调度方案 A:手写 `setTimeout`,一轮结束再约下一枪;`SYNC_INTERVAL_MS` 可配置。 + +### 2026-10-05 · 补跑与状态(作者认可推荐项) + +1. **登记后补跑 = 方案 A**:最迟一个 interval 内由下一轮覆盖;无秒级唤醒/优先队列。 +2. **`engine_status` 单行快照表**:engine 写、server 读;表名实现期可微调;可含只读 `sync_interval_ms`。 +3. 周期文案:默认 5 分钟、可配置(`SYNC_INTERVAL_MS`)。 + +### 2026-10-05 · 周期修改通道 + +**不通过 server / API 修改 interval**——仅 env、启动时读;管理面无改配。协作只经 PostgreSQL,无 IPC/内嵌 HTTP。 + +实现落点:`syncRound` + 调度循环在 `apps/engine`(见 §6)。 diff --git a/docs/design/技术选型.md b/docs/design/技术选型.md index da94bab..10b2dfc 100644 --- a/docs/design/技术选型.md +++ b/docs/design/技术选型.md @@ -19,7 +19,7 @@ | 运行时与工具链 | **Bun**(runtime + 包管理 + 测试 + 打包一体化) | | HTTP | **Hono** | | 数据访问 | **Drizzle ORM** + postgres.js + **PostgreSQL** | -| 调度 | 进程内定时(细则归同步引擎设计) | +| 调度 | **独立进程 `apps/engine`**:手写 `setTimeout` 循环(细则见《同步引擎设计》) | | 加密 | WebCrypto(Bun 原生) | | 前端 | **Vue 3 + antdv-next + Vite** | | 部署 | **Docker**(倾向,§6) |