From 1ca3416bb6430dcbb5ec56ad002eede7750a92c4 Mon Sep 17 00:00:00 2001 From: argszero Date: Wed, 30 Sep 2026 17:00:32 +0800 Subject: [PATCH] chore(release): v0.7.29 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Release v0.7.29 — a version bump plus the CHANGELOG entry for the 41 commits merged since v0.7.28 (#295–#335). No production logic changes. The release PR now has to touch five files instead of three: #303 and #305 gave the copies in docker-compose.yml / Dockerfile / CHANGELOG.md an executor in src/deploy_gate.rs, so a bump that misses any of them turns `cargo test` red (this is the deliberate cost of those rules). - Cargo.toml / Cargo.lock: 0.7.28 -> 0.7.29 - docker-compose.yml: header comment and `image: aitokenpool:` - Dockerfile: header comment, `docker build -t aitokenpool:` and the `docker run` example - CHANGELOG.md: the `## v0.7.29` entry - src/deploy_gate.rs: the version-scanner self-check asserted against the literal "0.7.28"; it now uses the derived `declared_version()`, so the release number is not left behind as another stale copy in the test. Verified: `cargo test` 462 passed / 0 failed, `cargo fmt --check` rc=0, `cargo clippy --all-targets -- -D warnings` rc=0. A/B with the file bytes restored afterwards: bumping the manifest alone reddens exactly the two version rules and names all five copy sites; a stale CHANGELOG heading reddens only the heading rule; a builder image below the declared rust-version reddens only the >= rule. --- CHANGELOG.md | 17 +++++++++++++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- Dockerfile | 6 +++--- docker-compose.yml | 4 ++-- src/deploy_gate.rs | 4 +++- 6 files changed, 27 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 48b432b..29c9831 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)` 标注必须**相等**、`FROM rust:` 只须 **≥**(用更新的工具链构建是合法的)。代价是刻意的:**发行 PR 从此还要改这两个文件**,漏改当场变红。③镜像 tag 取自 git ref、二进制版本取自 `Cargo.toml`,两者无人对照 ⇒ 打一个与清单不符的 tag(或给旧版本补 tag)会发布一个「自己的二进制都不认的版本」的镜像,而 README 教大家从 `ghcr.io/…:latest` 拉;`docker-publish.yml` 增加一步(只对 `refs/tags/` 生效)fail-closed。④`CHANGELOG.md` 的最新 `## v` 标题是第四个载体、此前毫无执行者(它在 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 查询)。 diff --git a/Cargo.lock b/Cargo.lock index b33664e..be171fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -51,7 +51,7 @@ dependencies = [ [[package]] name = "aitokenpool" -version = "0.7.28" +version = "0.7.29" dependencies = [ "aes-gcm", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index eccbcf6..880835b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aitokenpool" -version = "0.7.28" +version = "0.7.29" edition = "2021" description = "AI Token 共享池 — 企业 key 池 + 公共共享市场" license = "MIT" diff --git a/Dockerfile b/Dockerfile index 3552988..c06acc8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/docker-compose.yml b/docker-compose.yml index 12db8fd..a0fbb8d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,4 @@ -# AITokenPool — Docker Compose 一键部署(v0.7.28) +# AITokenPool — Docker Compose 一键部署(v0.7.29) # # 快速开始: # export ATP_MASTER_KEY=$(openssl rand -hex 32) # 必填,见下方说明 @@ -16,7 +16,7 @@ services: aitokenpool: build: . - image: aitokenpool:0.7.28 + image: aitokenpool:0.7.29 ports: - "8080:8080" volumes: diff --git a/src/deploy_gate.rs b/src/deploy_gate.rs index d6b8a5b..e51050d 100644 --- a/src/deploy_gate.rs +++ b/src/deploy_gate.rs @@ -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())), "依赖段不得被当成期望来源" );