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
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,23 @@

All notable changes are recorded here. Versions follow [SemVer](https://semver.org/).

## v0.7.29 (2026-09-30)

自 v0.7.28 起累计 41 个提交(#295–#335)。**无 schema 变更(仍 15)**;**有一处 config 默认值变更** —— 邮件端口的两个值载体由 `587` 改为 **`465`**(见下),既有 `config.toml` 若显式写了 `smtp_port = 587`,请同步改成 `465`;省略该键则自动取新默认值。本版三条主线:**发行链上「声明有消费者、没有执行者」的四个载体一次性补齐**;**「元素的外观由它自己的类声明」这一族 CSS 层叠缺陷**;以及两个新功能(共享页内编辑、SIGTERM 优雅停机)。

- **发行链的四个版本事实载体,第一次各有执行者(#302 / #303 / #304 / #305 / #306)** — 四处都在陈述同一个事实,而其中三处此前**没有任何断言**(同一形状本仓已经见过两次:R76 的 MSRV、R80 的发行 tag)。①`Cargo.toml` 声明 `rust-version = "1.86"`、`CONTRIBUTING.md` 印成契约,而 CI 只装 `stable`(1.98)⇒ 声明零执行者(已被咬过:clippy 会建议只有 1.87 才稳定的 API);新增独立 `msrv` job,装清单声明的工具链、**断言装到的 `rustc` 就是那个版本**(否则「抬高声明、job 仍测旧版」会静默重演),再 `cargo build --locked`。②`docker-compose.yml` / `Dockerfile` 各抄了一份发行版本号与 `rust-version`,那句副本生于 `a51b3ba`(v0.6.6)后一次未改 ⇒ 陈旧 **22 个发行版**:README 首推的 `docker compose up -d --build` 建出的镜像标着 `aitokenpool:0.6.6`,而里面的二进制自报 `0.7.28`(`/healthz` 取自 `env!("CARGO_PKG_VERSION")`)。改值,并在既有 `deploy_gate.rs` 里让副本有执行者:期望值**从 `Cargo.toml` 派生**(只在 `[package]` 段取值 —— 依赖表里的 `version = "0.7"` 不得顶替),`aitokenpool:<v>` 与 `(v<v>)` 标注必须**相等**、`FROM rust:<v>` 只须 **≥**(用更新的工具链构建是合法的)。代价是刻意的:**发行 PR 从此还要改这两个文件**,漏改当场变红。③镜像 tag 取自 git ref、二进制版本取自 `Cargo.toml`,两者无人对照 ⇒ 打一个与清单不符的 tag(或给旧版本补 tag)会发布一个「自己的二进制都不认的版本」的镜像,而 README 教大家从 `ghcr.io/…:latest` 拉;`docker-publish.yml` 增加一步(只对 `refs/tags/` 生效)fail-closed。④`CHANGELOG.md` 的最新 `## v<semver>` 标题是第四个载体、此前毫无执行者(它在 23/23 个 tag 上一直正确 ⇒ 正是「有消费者、无执行者」的形状):`deploy_gate.rs` 追加第三条规则,期望值仍**派生**、`include_str!` 保持零新依赖。⑤`include_str!` 是**编译期**读文件 ⇒ 目标不在 Docker 构建上下文里 `docker build` 直接失败;上下文由 `Dockerfile` 的 `COPY` 源与 `.dockerignore` 共同定义,而这条属性同样没有执行者 —— `src/**` 的 47 处内嵌里发布期可达的**只有 1 处**(`main.rs` → `config/config.example.toml`,`COPY config ./config` 正是为它加的),其余**全部**靠 `#[cfg(test)]` 保命(其中 7 处真的落在上下文之外),而 **CI 从不 `docker build`** ⇒ 平时全绿、只在打 tag 或 `docker compose up -d --build` 那刻才炸;`deploy_gate.rs` 第四条规则判「每处内嵌要么目标在上下文里,要么该站点发布期不可达」,名册**从 `main.rs` 派生**、语料**运行期走盘** `src/**/*.rs`。
- **门禁自身的两条盲区(#307 / #311)** — ①交易表排序校验的三条 400 经由一个接收 `String` 的闭包产出,`"error"` 的值是**裸标识符** ⇒ 词法提取器只认字面量、**静默读不到**,于是「后端每条用户可见错误都必须能被 `ERR_MAP` 命中」那条门禁在这三条上**恒真**(en 界面显示中文,而 CI 全绿;它们生于 #284,晚于门禁 35 个 PR)。把信封写回调用点(与仓内 60 余处同形)、登记三条,并新增一条 **fail-closed** 规则把盲区从「逐条补」改成「默认不允许存在读不到的写形态」(语料从文件系统派生、与提取器**共用**位置函数)。诚实边界已写进 `ui/README.md`:这是 **API 契约/一致性**修复 —— 前端发不出非法 `sort`/`dir`,只有直连 API 才够得到那三条 400。②仓库散文用**文件名**引用会话仪器(jsdom 探针、Python 编译门禁),而那些仪器**从未进仓**(CI 没有 JS 运行器)⇒ 62 处死链,其中一处就在写下「仪器在仓外,不写文件名」这句话的文件里、隔 3 行。保留陈述、删文件名(62 处一处一行),并新增 `citation_gate.rs` 让这条约定有执行者。
- **CSS:元素的外观由它自己的类声明(#329 / #330 / #331 / #332)** — 同一族层叠缺陷的两张脸:`.form label { display:flex; flex-direction:column }`(特指度 0,1,1)压过组件层(0,1,0)⇒ 带组件类的 label 要的布局全部失效 —— `.check-line`(勾选项竖排)与 `.chip`(星期 chip 从圆饼变竖排);`.card h3`(0,1,1)压过 `.wallet-hero-label`(0,1,0)⇒ **整个类一条声明都没生效**(钱包页余额标题一直按普通卡片标题渲染,`margin-bottom: 0` 把大数字拉近说明的那条也从未生效)。修法都是给泛化规则加 `:not([class])`,并新建 `layout_gate.rs`:R1/R2 判「带类的 label 必须自己声明布局」,R3 把「**整个类**都被压死」变成可推导(比较特指度与源序、追踪 markup 嵌套 —— 不追踪会因一条 `.page-head p` 产生 7 个假死类)。反向的 R4(「有名无元素」)删掉四条**今天仍会生效**的孤儿选择器(`.demo-hint` / `.login-divider` +两伪元素 / `.wallet-note` / `@media` 里的 `.sidebar #toggle-mode-btn`);`.delta` 因原型真会产出 `class="delta ok"`、属设计基线词汇,刻意保留。JS 侧同样没有 `dead_code`:`ui/js/app.js` 两枚顶层声明丢了全部读者(`DAY_LABELS`、`nowTime()`)无人报,删掉它们并新增 `js_gate.rs`(顶层声明必须在 `ui/index.html` + `ui/js/*.js` 里以标识符边界出现 ≥2 次)。⭐ 读者语料**刻意不含 `ui/README.md` 与 CSS** —— 实测把 README 计入,就会对这两个恰好被 README 提到的名字失明(1 → 2);「提到名字」不是「读点」。
- **「一个事实一个来源」的显示面续批(#295 / #296 / #297 / #298 / #299 / #300 / #301 / #308 / #312 / #320 / #321 / #323)** — ①市场上架页的「高峰计价」开关只读**一个**字段,而计费引擎按**三个**字段逐项判定 ⇒ 只配高峰输出价的模型高峰时段按高峰收、界面零披露;改为触发器读「任一高峰字段」、各槽取**自己**字段的高峰或空闲价。②键盘导航把「当前行」记成**下标** ⇒ 重绘换 `innerHTML` 后下标指向一个从未高亮过的行、切视图后仍操作旧表;改为 `kbdEnter()` 只对**自己带 `.row-active` 的行**动手(重绘自动失效),`switchView()` 在守卫接受目的地后调 `kbdClear()` —— 两个消费者都是**状态变更**(市场行 → 真花钱的调用;共享行 → PATCH)。③`switchView()` 拒绝一个视图时是**裸 `return`** ⇒ boot 的 hash 指向被拒视图(非 admin 落 `#/admin`,而普通使用即可达成:登出不清 hash)会让用户落在**没人渲染过**的空白仪表盘、此后零请求、不自愈;改为返回结局、`enterApp()` 按身份回落到 `homeView()`。④运营卡用 `COUNT(*)`(**每个**状态)却叫「N 个在售 key」,而同屏上方那张卡数 `status='on'` ⇒ 同一词、同一屏、两个数;改**词**不改数(`total` 必须继续数全部状态,否则那行自己的「全部停用」pill 会结构性恒绿)。⑤网关:`keys` 行只按 `model` 路由,计费却按 `(provider, model)` 查目录 ⇒ 目录里那条被改名或删除后(admin 表单可编辑 `provider`/`model`、config 启动同步可收缩),一个 `status='on'` 的 key 就变成「可路由但不可计价」,而结算兜底把它按 **0** 计费 ⇒ 一次真实调用静默变免费、且 `pts = 0` 不降余额 ⇒ 无限次白嫖。`sharing::create` 早就堵了创建那扇门,本次堵**同一不变量的第二扇门**:`forward` / `forward_stream` 在选 key 之前逐个计价、不可计价即 503 拒绝(绝不发给上游),结算兜底从静默 0 改成响亮的 `log::error!` —— 「缺数据」不是「数据为 0」。⑥四个 `#[allow(dead_code)]` 挂在**结构体**上会连将来新增的字段一起静默(实测六个里四个什么都没抑制),删掉并**按字段**收窄剩下的;⚠️ CI 看不见这一类,唯一证人是一次**非 test** 构建(`clippy --all-targets` 会编译测试目标,而 parse 测试逐字段断言)。⑦仪表盘的绿柱画的是 `income` 序列(`earn + topup + gift`),却挂着「共享收益」的名字,而**同屏**那张收益卡用**同一个名字**指另一个事实(仅 `earn`)⇒ 充值过的用户看到绿柱 5000 而卡片写 0;改名字不改数据(交易页早在 #133 就叫它「收入」,那个更窄的名字晚了 22 个 PR,且那一版注释还自称与交易页一致)。⑧市场详情行把「没有这个数」的展示哨兵 `—` 当数值填进占位符 ⇒ 全部渲染成「成功率 —%」(两类行:游客表无该字段、活行被硬编码 `success: null`);拆键(带占位符的与不带的),哨兵永不进占位符。⑨管理页文案说「删除模型后调用按 0 计费」,而 #299 已把这条状态改成**发往上游之前就 503 拒绝** ⇒ 改词不改行为(复用拒绝语本身已有的措辞,零新键)。⑩设置页「生成新 API Key」的提示仍宣称「完整 key 仅此一次」,而该事实早在 v1.22.1 就已被改写(`GET /api/api-keys` 现在返回 owner 可见的 `full_key`)⇒ 改值,键数不变。⑪共享列表的 Key 列在**服务端做好的脱敏串**上又加工一遍(前 3 后 4;`key.length <= 8` 那个分支与另一分支逐字相同、自原型起从未被数据走到;服务端给整串掩码时印 11 个星号)⇒ 改为原样打印(设置页 API Key 表原本就是同规),门禁的掩码函数名与字面量**全从 `sharing.rs` 派生**。⑫行内二次确认的三条出口(再次点击 / 3 秒超时 / Esc)当初只有 Esc 那条接了收尾 ⇒ ①点击出口不还原标签:删除**失败**时按钮一直念「确认删除?」,再点一次把那句话记成「原标签」,此后连超时都还原不回来(整会话不自愈)②点击与超时都不摘 `document` 级 keydown ⇒ 每次攒一条死闭包,此后任意一次 Esc 逐个改写早已不在的按钮(实测 1 → 2 → 3 条)。三条出口改为走同一个 `disarm()`,且「原标签 / 那条监听」记在**节点上**(两次调用拿不到彼此的闭包)。
- **邮件端口的**值**被留在协议切换之前(#328)** — `63fc76fd` 把发信从 STARTTLS 换成隐式 TLS(`SmtpTransport::relay`),提交信息写着「端口改 465」,**diff 只改了散文**:两个值载体(`config/config.example.toml` 的示例草稿与 `config.rs::default_smtp_port()`)仍然是 `587`。而 `ensure_config` 会把示例复制给每个新用户、`[mail]` 省略 `smtp_port` 则走 serde 默认 ⇒ 587 配上隐式 TLS:客户端在 STARTTLS 端口上发起 TLS 握手,Gmail 回明文,rustls 报 `InvalidContentType`,验证码**发不出去**(注册 502)。两个载体都改成 `465`,并新增 `smtp_port_gate.rs`:期望端口**从 `mail.rs` 的传输模式派生**(lettre 的 `SUBMISSIONS_PORT` / `SUBMISSION_PORT`),零硬编码 ⇒ 下次换协议必须让两个载体一起跟。
- **文档与注释的校准(#309 / #310 / #313–#319 / #322 / #324–#327)** — 一整批「文档/注释说的事情代码已经不再做」:i18n 计数不再复述(改指机制,因为**门禁已拥有那些数**,复述即同一事实的第二个载体);`.stat` / `.bar-label` 两条「线上别名」其实是 `75dcb2c`(#154)亲手删掉的历史名(全树只剩那句注释本身);登录页两节还在描述 #153 删掉的四个装饰选择器;「最近使用」一节仍写 `getRecentIds`/`saveRecentIds` 与「id 数组」(真名 `getRecentKeys`/`saveRecentKeys`、存的是身份串 `provider/model`);运营卡仍用 #269 之前的名字;`tr` 其实是 `T`;`/healthz` 的模块头把响应写死成 `0.2.0`(写下那天是对的,之后陈旧 26 个发行版);`inlineForm` 的校验契约与同文件另一节自相矛盾;`perf_gate` 头部陈旧的文件数(那个数**已由断言拥有**);四条没跟上后续功能的能力清单;被粘贴重复的一句话。**本仓对这一轴的定案一贯是「修数据、不上门禁」** —— 词法门禁能钉住一句话的形状,钉不住它的真假。

**新增功能**:①**共享页内编辑**(#333 `eb2c8ca` 端点 + #334 `c701584` 前端)—— `PATCH /api/sharings/:id` 从「只收 `status` 一个字段」扩成与 `POST` 同一套字段的**部分更新**(省略即不动),列表每行多一个编辑入口,复用**同一张**行内表单、一份 `sharePayload`、一份校验、一份时段编码;创建与更新的守卫合并为 `validate_listing`(规则不会再像 C2052 那样分叉,测试断言两条路径返回**同一个错误正文**而不只是同一个状态码)。硬约束:服务端只给脱敏成品,所以 `key` 是**三态**(省略/空白=保留原密文、新值=替换),掩码只进 `placeholder`、每个程序化写点只写空串(掩码落进 `value` 会被当成新 key 加密进去,原 key 永久失效且界面无提示)。②**SIGTERM 优雅停机**(#335 `6bee283`)—— `main.rs` 改走 `with_graceful_shutdown`:收到 SIGTERM/SIGINT 后停止接受新连接、等待在途请求,上限 `DRAIN_LIMIT_SECS`(8s),超限强制退出并打一行日志(流式/长连接被上限截断是有界的必然代价,常量注释直说,不假装无损)。上限只是一半:docker 自己按 `stop_grace_period` 计时并 SIGKILL,所以那两个数必须有序 —— `docker-compose.yml` 声明 `stop_grace_period: 12s`,`shutdown_gate.rs` 保证它**严格大于**应用排水上限(改一个忘另一个会当场变红,而不是静默把排水掐短)。配套新增 `docs/deployment.md`:四步上线清单(预拉 → 更新 → 健康校验 → 失败回滚,每步带判据)、两条验收测量、以及不可违反的硬约束(不得改 `start-first`、不得搬 SQLite 文件、不得触碰 `ATP_MASTER_KEY`);它**指向**那两个常量而不复述数字(第三份副本=第三个漂移点)。

**新增门禁**(全部从被测算代码**派生**,不写快照):`deploy_gate` 第二/三/四条规则(产物版本副本、CHANGELOG 标题、Docker 构建上下文)、`docker-publish.yml` 的 tag 守卫、CI 的 `msrv` job、`citation_gate.rs`、`js_gate.rs`、`layout_gate.rs`(R1–R4)、`smtp_port_gate.rs`、`readme_gate.rs`(中英两版必须陈述同一批语言无关的结构位)、`shutdown_gate.rs`,以及 `state_gate` / `i18n_pack` 新增的 `the_marketplace_peak_trigger_names_the_fields_it_displays`、`the_keyboard_row_action_follows_the_highlight_not_the_index`、`the_refused_view_switch_never_leaves_an_unrendered_view`、`the_ops_key_rows_are_named_the_way_their_own_card_names_them`、`the_router_requires_a_price_before_it_reaches_upstream`、`the_dashboard_trend_names_its_series_the_way_the_transactions_trend_names_it`、`a_placeholder_is_never_filled_by_a_display_sentinel`、`every_error_key_site_has_a_readable_write_form`、`the_sharing_list_prints_the_mask_the_server_made`、`the_inline_confirm_disarms_on_every_exit`、`the_share_edit_form_never_submits_the_stored_key_mask`。

`cargo test` 376 → **462**(新增 86 条门禁与回归测试)。**无 UI 资产令牌变更**:本版每个前端 PR 都自行推过自己的 `?v=` 格,`ui/index.html` 结束在 `app.js` `?v=20260930-2`、`i18n.js` `?v=20260930-1`、`api.js` `?v=20260929-1`、`style.css` `?v=20260924-5`、`data.js` `?v=20260914-11`。**无 schema 变更(仍 15)**;唯一 config 变更是上文的邮件端口默认值。

## v0.7.28 (2026-09-25)

自 v0.7.27 起累计 20 个 PR(#274–#293)。**无 schema 变更(仍 15)、无 config 变更**。本版主线仍是那条已被反复证实的规律:**一个事实只能有一个真源,显示口径必须等于消费口径** —— 本批把它推进到「**状态**」与「**边界**」两类载体上:越界的状态(跨身份、跨请求、跨渲染)与越界的口径(页 vs 数据集、控件 vs 查询)。
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.28"
version = "0.7.29"
edition = "2021"
description = "AI Token 共享池 — 企业 key 池 + 公共共享市场"
license = "MIT"
Expand Down
6 changes: 3 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# AITokenPool — multi-stage release image (v0.7.28)
# AITokenPool — multi-stage release image (v0.7.29)
#
# Build: docker build -t aitokenpool:0.7.28 .
# Build: docker build -t aitokenpool:0.7.29 .
# Run: docker run -p 8080:8080 -v "$PWD/atp-data:/data" \
# -e ATP_MASTER_KEY="$(openssl rand -hex 32)" \
# aitokenpool:0.7.28
# aitokenpool:0.7.29
#
# Notes:
# - unified data dir (rant 2026-08-19T20:53:23): ATP_DATA_DIR=/data holds
Expand Down
4 changes: 2 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# AITokenPool — Docker Compose 一键部署(v0.7.28)
# AITokenPool — Docker Compose 一键部署(v0.7.29)
#
# 快速开始:
# export ATP_MASTER_KEY=$(openssl rand -hex 32) # 必填,见下方说明
Expand All @@ -16,7 +16,7 @@
services:
aitokenpool:
build: .
image: aitokenpool:0.7.28
image: aitokenpool:0.7.29
ports:
- "8080:8080"
volumes:
Expand Down
4 changes: 3 additions & 1 deletion src/deploy_gate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -499,10 +499,12 @@ fn the_version_scanners_actually_see_the_copies_they_guard() {
"`rust-version` 应是 x.y:{msrv:?}"
);
// 同名字段按段收窄:依赖表里的 `version = "0.7"` 不得顶替 `[package]` 的版本。
// (产物里「依赖名 + 发行版本号」这一组合不得出现 —— 取的是**派生**的 `ver`,不是当时
// 的发行号字面量:写死会让每次发行都留下一份陈旧副本,正是本门禁存在的理由。)
assert!(
!FILES
.iter()
.any(|(_, s)| s.contains("axum") && s.contains("0.7.28")),
.any(|(_, s)| s.contains("axum") && s.contains(ver.as_str())),
"依赖段不得被当成期望来源"
);

Expand Down
Loading