diff --git a/CHANGELOG.md b/CHANGELOG.md index ffeaecf..7e0ff0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,33 @@ All notable changes are recorded here. Versions follow [SemVer](https://semver.org/). +## v0.7.23 (2026-09-14) + +自 v0.7.22 起累计 76 个 PR(#161–#237),是本项目迄今最大的一次版本。⚠️ **数据库 schema 12 → 14**:含一次性数据修复(`keys.used` 按账本重算)与 4 条新索引(迁移在启动时执行;dev 库 79.6 MB / 36.6 万行,升级前请先备份 `data/aitokenpool.db`)。 + +- **OpenAI 流式「无 usage ⇒ 计费 0」(#187 ec158bd)** — 该协议下上游 usage 是 **opt-in**(`stream_options.include_usage`),而网关把客户端请求体原样转发、从不设该开关 ⇒ 整条流按 0 token 结算:余额不动、`usage_records` 无行、`transactions` 无行;`month_calls = COUNT(*) FROM usage_records` 使同一次调用在运营视图里也一并消失。修法:同一 helper 为 `openai_chat` 出站体注入 `stream_options.include_usage`(客户端自己设过则保留),anthropic(`message_start`)/ responses(`response.completed`)本就必报、不动;**仍无 usage 时 fail closed 报错**,且区分「从未上报」与「上报 0」。**刻意不估算 token** —— 缺数据不等于数据为 0。 +- **DeepSeek 缓存命中 token 的三种拼写(#188 73fae8c / #189 9bd2dec)** — 同一规则在四处实现,流式认 3 种拼写而非流式只认 2 种 ⇒ 计费面超收(未配命中价 5.5×、已配 2.9×)、高峰价下命中的单位价错 30×;同时非流式 openai→anthropic 的响应翻译漏掉命中 token。两处补齐后四个读者一致(余下只认 1 种者皆为协议原生、流式/非流式同口径)。 +- **模型价格校验漏掉 `cache_hit_input_per_m`(#190 5fcc68e)** — `validate_common` 查了 6 个价格字段里的 5 个,独漏命中价的非高峰兄弟;该字段可为负则一路无钳制进结算(`balance-pts` 负数变加法、`earn` 负数变扣减,实测 -8 点)。补齐 ≥0 校验。 +- **流式响应不再被总超时切断(#210 acc6f49)** — 出站客户端 `.timeout(120s)` 是**整个请求含响应体**的截止时间,而流式转发共用它 ⇒ 持续出数据的流到 120 s 被硬切(实测每 30 s 一帧的假上游只收到 3 帧、`[DONE]` 永不到达),长回答与推理模型常态超过 120 s。改为两条明确策略:非流式保留 `.timeout(120s)`;流式用 `.connect_timeout(120s).read_timeout(120s)` 无总截止 —— 有进展就不罚,静默 120 s 仍判死。`read_timeout` 此前全树从未设置(流式只有总上限、没有本该有的停滞检测)。 +- **上游响应体读取失败不再伪造 200(#211 e885cd6)** — `resp.bytes().await.unwrap_or_default()` 把读失败折叠成空 body,再让状态行单独决定结果 ⇒ 2xx 而 body 从未到达的请求被当作 `200 OK` + `0` 字节交给客户端。 +- **跨协议翻译五连修(#205 45a26bd / #206 b46f113 / #207 254eacf / #208 03a9aca / #209 bbc74ea)** — ① 完成信号在流式与整体两个翻译器里各推一次且结论不同,改为同源;② `openai_sse_to_openai_responses` 在 `[DONE]` 发 `event: response.completed` + `data: {}`(既无 `type` 也无 `response`),且 `finish_reason` 分支也会发 ⇒ 正常上游产生两次终态,改为恰好一次完整终态;③ 流式 `openai_chat → responses` 把消息项硬编码 `output_index: 0`、工具项也从 0 起 ⇒ 文本 + 工具调用两项同号,按 `output_index` 取键的客户端必然错位,改为每项唯一序号且保序;④ Responses 客户端的工具往返由 `function_call` / `function_call_output` 两个**无 `role`** 的项承载,而转换器只认 `role` + `content` ⇒ 工具项被直接丢弃;⑤ `tool_choice` 只在 `anthropic → openai_chat` 一条路径翻译,其余 4 条跨协议路径全部丢失。 +- **认证路径不再占住异步 worker 与共享 DB 锁(#212 ce68a9f / #213 fa6a794 / #214 a2d5fe1 / #223 174879a)** — ① 验证码写入分两步且绑定字面量 `"+10 minutes"` 作 `expires_at`(列里就存了这串字),重发限流也在另一把锁里另查,三条语句改为同一临界区内一次写清;② 密码 KDF(argon2 ~0.24 s)在进程级 DB 互斥锁内执行,一个未认证请求即可卡住所有 DB 路径(改后锁可用次数 0/0/1/0 → 158/159/525/670);③ 邮件发送是阻塞 I/O(3 次重试 × 15 s 超时),却在三个未认证 handler 里就地调用;④ 哈希/校验已挪出锁,但仍**内联跑在异步 worker 上**,改为阻塞池。 +- **认证边界两处(#185 223ad55 / #186 f16fe47)** — 登录邮箱大小写归一(`Admin@X` 与 `admin@x` 曾是两个账号);`resend_code` 曾对**任意**地址发信用码,改为镜像 `forgot_password` 的守卫、按存在性判断(带阳性对照)。 +- **共享上架校验补齐(#199 ad100dd / #201 928dd24 / #202 ad817cd)** — ① `POST /api/sharings` 曾原样落库调用者给的 `provider`/`model`,而每次调用都按 `(provider, model)` 查价、查不到即回落 `(0.0, 0.0)` ⇒ 不可计价的组合被平台**免费代理**;② 同理补上「能路由」(plan 必须在 config `[[plans]]` 中可解析出端点),否则上架即失败;③ `mask_upstream_key` 按**字节**切片(`&key[..2]` / `&key[len-4..]`),多字节字符跨边界即 panic。 +- **`keys.used` 单位修正 + 一次性数据修复(#217 164b8d3,schema 12 → 13)** — `keys.quota` 是点数(表单标题即「声明额度(点数)」),而 `used` 累计的是 **token** 数,共享页把两者相除画进度条并都标「点」⇒ 「已用 1000000 / 5000 点」、条常年钉在 100%。改为累计 `pts`(同单位),并**从账本一次性重算历史值**(`used = SUM(transactions.pts WHERE type='consume')`,门禁 `schema_version < 13`)。同提交把 `schema_version` 读数改为 `MAX(version)`:该表无唯一约束,真库里已堆 22 行,单行读会让每次启动都追加一行并使每个 `v < N` 门禁**永远为真**。 +- **月聚合查询性能(#234 93b2ebd / #236 83ecf4e,schema 13 → 14)** — 12 处 `strftime('%Y-%m', time) = strftime('%Y-%m','now')` 把索引列包进函数 ⇒ 索引不可用、退化为全表扫(dev NAS 实测单查询 3.8 s / 4.5 s,表现为「刷新页面十几秒像被登出」)。根因**不是** NAS 吞吐(4 KB 热读 1.9 µs,与本地盘同级)而是「一次查询摸多少页」。改为**闭**区间范围谓词(闭上界必须写,否则会带进未来月行——已加断言)+ 4 条新索引:`transactions(user_id,time,type,pts)`、`transactions(time,type,pts)`(前者给带 user_id 的聚合,后者给全库聚合)与 `usage_records(time)`、`usage_records(user_id,time)`(该表此前**零索引**)。实测 3845 ms → 119 ms、4525 ms → 84 ms。 +- **部署产物不再提供非法主密钥默认值(#235 7852277 / #237 bc4b111)** — `docker-compose.yml` 的 `ATP_MASTER_KEY` 默认值是 `dev-master-key-请替换`(18 字符、含 `-` 与中文):`crypto::parse_master_key` 拒绝它,`Crypto::from_config` 只记一行日志后**落穿到随机主密钥** ⇒ 重启后已加密的上游 key 全部不可解密、所有上游调用 503 —— 一次**静默全量故障**,且看起来像 key 问题而不是配置问题。改为 `${ATP_MASTER_KEY:?…}`:缺失即 compose 报错退出并打印生成命令(**刻意不给合法默认值** —— 那是把「重启即坏」换成「所有部署共用一把公开主密钥」),并修正同文件那句「未设置时使用示例默认值」的注释(该值根本解析不了,注释承诺的行为并不存在)。新增 `cargo test` 门禁:随包产物里的主密钥默认值必须「要么不存在、要么恰好 64 位 hex」,按形状(`Required` / `Default` / `Literal` / `NotALiteral`)分别判定,因此文件自己的快速开始示例不会被误读为默认值。 +- **模型目录按官方改名(#233 11c5324)** — DeepSeek 官方现名 `deepseek-flash`(V4.1-Flash),旧名 `deepseek-v4-flash` / `deepseek-v4-flash-vision-exp` 已退役(请求仍应答,但上游按 Flash 现行价承接计费);`deepseek-v4-pro` 不变。影响面不只是展示:`POST /api/sharings` 以「`models` 表里存在该 (provider, model)`」判定可计价,旧名不在表里会让上架被拒。示例目录 14 → 13 条、vision 并入新名。**⚠️ 部署侧须同步改自己的 `config.toml`** —— `seed_models` 是**全量同步**(config 为准,缺席即删行)。 +- **钱包 / 交易口径统一(#191 562c8f6 / #192 0c25117 / #193 9fead29 / #195 f55cfd2 / #196 f6eed10 / #197 6997241 / #215 1febe59 / #218 a9596f9 / #219 0feae18 / #220 1d5fcdd)** — 交易方向**只由 `type` 决定**(6 个 writer 全存正数 `pts`,不再从符号推方向;后端、前端、运营计数器三处各自的副本同时收口);仪表盘 `net` 改按**当月**(此前是近 7 天合计,却显示在当月分类型行之上);「点数」列的筛选/排序/CSV 导出改按**渲染出的带符号值**(其余消费者仍按原值);Key 列筛选与显示取同一个值;周趋势的「周」定义去重(周一起、闭区间);7 天序列补齐空日(sparkline 的 x 轴此前是「序号」而非时间);四个 token 列可按真实值排序(此前表头按钮点了不动)。 +- **赠送过期进入账本(#194 e3f85d2)** — `gift.rs::expire_past_gifts` 把过期点数标掉并重算 `gift_balance`:**点数真离账却没有任何 `transactions` 行**,是唯一「移动余额而无账本行」的路径 ⇒ 账本与账户不可对账。改为清扫前先 `SUM` 即将过期额、仅 >0 时插**一行** `expire`(幂等、只记真正过期的部分),钱包汇总/趋势、运营 `month_out`、前端类型与筛选项同步补上(`expire` 计入支出)。 +- **交易表与表格交互(#226 9ecfc67 / #227 26d448b / #228 6673389 / #229 d1faab6 / #230 2be0d54 / #231 dfa7318 / #232 e7bfc6f)** — ① 列数(`` / 空行 colspan / 错误行 colspan / 行模板 ``)在四处手写、失配无声(colspan 短了只是画窄),新增门禁;② 行内按钮按**筛选后**的下标标记、处理器却按缓存记录取数 ⇒ 搜索后操作错行;③ 管理员模型表单的提交按钮被**嵌套 `withLoading`** 吃掉(外层守卫直接返回,请求从不发出);④ CSV 导出的时间列与单元格不同口径(一处本地精确时间、一处库内 UTC 串,时区与精度两张脸);⑤ 交易类型筛选曾有两套控件两份状态(列筛选一出值,顶部 tab 即成死控件、tab 高亮是说谎的指示器);⑥ 「模型」/「Key」列的无值兜底显示**本地化类型名**,而这两列是服务端筛选且服务端没有语言包 ⇒ 按屏幕上的文案筛 0 行(改为语言中性占位符 `—`,服务端两列 LIKE 搜同一文案);⑦ 列筛选曾服务端 (`tx_where`) 施加后**本地再筛一遍** ⇒ 输入 `"deepseek "` 表格空而「共 N 条」与汇总卡仍有数、输入 `%` 服务端全中而本地 0 行、CSV 导出 0 行(改为声明 `serverFilter: true`、三个消费者同变 pass-through)。 +- **i18n 与用户可见文案(#161 b4b3fbd / #162 d8031b9 / #163 4bcddca / #164 d8724c2 / #165 37d5b1c / #166 c14a590 / #167 8e40d79 / #168 8cff75c / #169 edd21a7 / #177 92946b7 / #182 75aa3b3 / #183 6f8b599 / #184 75979ad / #200 3d9abcd / #224 4907a1c)** — ① 卡模式(`@media max-width:560px`,thead 隐藏、`td::before` 打印 `data-label`)的 53 处标签全是中文硬编码、英文模式仍显示中文(实测 27 个),改为渲染时持语言键(复用已有列头键,未发明新词);② 后端错误到英文的映射表按**首个**子串命中返回,两条更具体的被通用词遮蔽(`验证码不存在或已过期` / `验证码错误次数过多` 永不可达)⇒ 英文下显示错误文案,改为最长匹配;③ `aria-label` 与搜索清除提示、英文文案里的代码标识与货币声明(英文管理页表头曾断言货币);④ 侧栏每个导航项都标了数字快捷键,而 keydown 守卫把范围硬编码为 `1`–`7`,`ops` 加入后第 8 项失效;⑤ `ui/js/api.js` 是请求咽喉、也是唯一自建错误文案却直接用 CJK 字面量的地方(门禁此前只看 3 个文件,`api.js` 在射程外);⑥ 交易列筛选状态存的是**本地化串**、比较时又按当前语言重新本地化 ⇒ 切语言后表被清空,改为存 `{value,label}`;⑦ 两条指向 2026-08-22 文档扁平化时已删除材料的死指针、两条陈旧/虚假的静态 `data-i18n` 兜底、一处已无数据来源的「方案备注」提示。 +- **接口约定从散文搬进门禁(#170 fcff194 / #171 0de4bde / #172 fa67974 / #173 b7becb5 / #225 669d03f / #226 9ecfc67 / #236 83ecf4e / #237 bc4b111)** — 语言包不变量、插值占位符、`ui/js/data.js` 兜底目录 vs 唯一真源 `config/config.example.toml`(含本次改名后的重新对齐)、表格列数与活状态行形状、月聚合查询计划(`perf_gate`:日期函数再包住时间列即失败)、主密钥默认值。同时修掉一条会因 CI 机器负载而非产品缺陷失败的探针(用**最大值**统计量判「阻塞期间健康检查仍被服务」,改为按提示样本判定)。 +- **文档与代码对齐(#178 3b1c0b0 / #179 a366162 / #180 d2c7431 / #181 9db4872 / #198 81df12d / #203 cabe4c6 / #216 aeb56c8 / #221 425c1b7)** — 去掉已过时的「跨协议流式 → 400」声明、`gift.rs` 模块头里虚构的 `dashboard` 触发者、过期的门禁计数注释、硬编码的模式/事务类型/API 清单与跨文件行号引用。 +- **运营与共享视图(#174 7d9b56e / #175 940850e / #176 4830248)** — `/api/ops/runtime` 新增 `version`(与 `/healthz` 同源)与 uptime,界面可直接确认线上构建版本(此前必须手 curl);运营页把早就返回却从未渲染的 `total_txs` 卡片补上;共享页补「本月新增」第四张卡(后端 `sharing_row` 增返 `created_at`)。 +- **前端 live 容器清单改从 DOM 推导(1e64d98)** — `ui/js/app.js` 里两份手写花名册(`bindLiveRetry` 13 项、`KBD_TABLE_IDS`)都漏了后加的 `#model-body` ⇒ 它的「重试」按钮点了没反应(冒烟证据:渲染器自己声明的 `retryFn` 参数从未被使用)、行也无法用键盘激活。改为由 DOM 推导,新增表格自动纳入。 +- **质量门禁**:`cargo test` 249/249(v0.7.22 时 148)+ `cargo fmt --check` + `cargo clippy --all-targets -- -D warnings` + `node --check` ×4 + i18n ZH/EN 键集精确相等(786×2)+ 新增 `perf_gate` / `deploy_gate` / `catalog_gate` / `table_gate` 四个纯测试模块(零新依赖)。 + ## v0.7.22 (2026-09-11) - **补齐全局 reset 层并让表单/按钮几何对齐原型(rant 2026-09-11T22:01:43,PR #159 f46882c)** — 纯前端改动(`ui/` 内,后端契约与部署结构不变,数据库仍留 NAS、不启用 WAL): diff --git a/Cargo.lock b/Cargo.lock index 6075ad8..b069e49 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -51,7 +51,7 @@ dependencies = [ [[package]] name = "aitokenpool" -version = "0.7.22" +version = "0.7.23" dependencies = [ "aes-gcm", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index f00a84e..72110c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aitokenpool" -version = "0.7.22" +version = "0.7.23" edition = "2021" description = "AI Token 共享池 — 企业 key 池 + 公共共享市场" license = "MIT" diff --git a/ui/index.html b/ui/index.html index 469a1ce..6f9ebef 100644 --- a/ui/index.html +++ b/ui/index.html @@ -5,7 +5,7 @@ AITokenPool - + @@ -842,9 +842,9 @@

使用模型

- - - - + + + +