Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)** — ① 列数(`<thead>` / 空行 colspan / 错误行 colspan / 行模板 `<td>`)在四处手写、失配无声(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):
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "aitokenpool"
version = "0.7.22"
version = "0.7.23"
edition = "2021"
description = "AI Token 共享池 — 企业 key 池 + 公共共享市场"
license = "MIT"
Expand Down
10 changes: 5 additions & 5 deletions ui/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AITokenPool</title>
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Cdefs%3E%3ClinearGradient id='g' x1='0' y1='0' x2='1' y2='1'%3E%3Cstop offset='0' stop-color='%234ecdc4'/%3E%3Cstop offset='1' stop-color='%232a9d8f'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='64' height='64' rx='14' fill='url(%23g)'/%3E%3Ctext x='32' y='43' font-family='-apple-system,Segoe UI,Arial,sans-serif' font-size='30' font-weight='800' text-anchor='middle' fill='%23062021'%3EAT%3C/text%3E%3C/svg%3E">
<link rel="stylesheet" href="css/style.css?v=20260912-4">
<link rel="stylesheet" href="css/style.css?v=20260914-10">
</head>
<body>

Expand Down Expand Up @@ -842,9 +842,9 @@ <h3 id="chat-title">使用模型</h3>
</div>
</div>

<script src="js/api.js?v=20260912-5"></script>
<script src="js/data.js?v=20260914-1"></script>
<script src="js/i18n.js?v=20260914-4"></script>
<script src="js/app.js?v=20260914-9"></script>
<script src="js/api.js?v=20260914-10"></script>
<script src="js/data.js?v=20260914-10"></script>
<script src="js/i18n.js?v=20260914-10"></script>
<script src="js/app.js?v=20260914-10"></script>
</body>
</html>
Loading