修复 ts-rs 对 enum 级 deny_unknown_fields 的解析告警并保留契约严格性

- 把 server-rs 与 apps/ai-game-creator-shell/src-tauri 两个 workspace 的 ts-rs 从 registry 固定到上游修复 commit fd8679ef20f866f747c046b30bd29c15e3d7a003(PR #491:已发布版 12.0.1 的 EnumAttr 不识别 deny_unknown_fields,只有 StructAttr 会静默忽略它),两个 Cargo.lock 同步改成 git 源
- 说明固定原因与退出条件:crates.io 最新仍是 12.0.1、GitHub 最新 tag 仍是 v12.0.0,等发布 > 12.0.1 后改回 registry 版本并删除 commit 固定
- 明确不使用 ts-rs 的 no-serde-warnings feature 消警:它会连同 deserialize_with 这类真实未支持的 serde 属性一起静默,掩盖 Rust 与 TS 绑定的漂移
- 保留 shared-contracts 的 Model3dMultiviewInputs enum 级 deny_unknown_fields(该属性一直由 serde 生效,告警只来自 ts-rs 的属性解析),注释改为写明不要为消警删属性、也不要改用 no-serde-warnings
- 新增契约测试 tests/model3d_multiview_request_contract.rs:钉住 views/taskId 变体内部与请求体顶层拒绝未知字段,并覆盖 kind 与 camelCase 字段名解析
- 记录排障经验与决策:pitfalls.md 说明无 span 的告警不代表属性失效,decision-log.md 记录固定 commit、影响面与回退条件
- 验证:cargo check -p shared-contracts 与该 crate 的 cargo test 不再出现 deny_unknown_fields 告警且全绿;cargo test --locked -p shared-contracts model3d 加 prettier 后 packages/shared 的 model3d 绑定零 diff;AGC 的 cargo check --locked --bin genarrative-ai-game-creator-shell 不再出现该告警(AGC 侧仍保留 1 条既有的 deserialize_with 告警,与本改动无关);npm run check:encoding、npm run check:doc-index、git diff --check、cargo fmt --all --check 通过
This commit is contained in:
2026-09-19 16:56:24 +08:00
parent 092bca7444
commit c48f0627cf
8 changed files with 118 additions and 12 deletions
+3 -4
View File
@@ -6227,8 +6227,7 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "ts-rs"
version = "12.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "756050066659291d47a554a9f558125db17428b073c5ffce1daf5dcb0f7231d8"
source = "git+https://github.com/Aleph-Alpha/ts-rs?rev=fd8679ef20f866f747c046b30bd29c15e3d7a003#fd8679ef20f866f747c046b30bd29c15e3d7a003"
dependencies = [
"thiserror 2.0.18",
"ts-rs-macros",
@@ -6237,9 +6236,9 @@ dependencies = [
[[package]]
name = "ts-rs-macros"
version = "12.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38d90eea51bc7988ef9e674bf80a85ba6804739e535e9cab48e4bb34a8b652aa"
source = "git+https://github.com/Aleph-Alpha/ts-rs?rev=fd8679ef20f866f747c046b30bd29c15e3d7a003#fd8679ef20f866f747c046b30bd29c15e3d7a003"
dependencies = [
"indexmap 2.14.0",
"proc-macro2",
"quote",
"syn 2.0.118",
@@ -21,7 +21,10 @@ shared-contracts = { path = "../../../server-rs/crates/shared-contracts", defaul
tauri-build = { version = "2.6.2", features = [] }
[dependencies]
ts-rs = "12.0.1"
# 与 server-rs 同一口径:已发布的 ts-rs 12.0.1 解析不了 enum 级 `serde(deny_unknown_fields)`
# (见 agent/direct_codex_user_item/model.rs),上游修复 #491 未发版,故固定到该 commit;
# 等 crates.io 发布 > 12.0.1 后改回 registry 版本。不要改用 `no-serde-warnings` 关告警。
ts-rs = { git = "https://github.com/Aleph-Alpha/ts-rs", rev = "fd8679ef20f866f747c046b30bd29c15e3d7a003" }
typed_floats = { version = "1.0.7", features = ["serde"] }
nalgebra = { version = "0.35.0", features = ["serde-serialize"] }
agent-runtime-core = { path = "../../../server-rs/crates/agent-runtime-core" }
@@ -2,6 +2,16 @@
> 用途:记录已经确认、会影响后续开发的长期技术/产品/协作决策。短期讨论不要写在这里。
> 当前口径:历史条目的旧路径、旧版本和已退役对象只用于追溯,不构成现行实现依据;如与当前代码或 `docs/README.md` 冲突,以当前代码和最新专题文档为准。
## 2026-09-19 `ts-rs` 固定到上游 #491 修复 commit,保留 enum 级 `serde(deny_unknown_fields)`
- 背景:`shared-contracts``Model3dMultiviewInputs` 用内部 tag 枚举 + enum 级 `#[serde(deny_unknown_fields)]` 严格拒绝未知字段,但每次编译都会打印一条没有 span 的 `ts-rs failed to parse this attribute` 告警。查明 ts-rs 12.0.1 只在 `StructAttr` 里静默忽略 `deny_unknown_fields``EnumAttr` 没有该分支;上游 PR #491 已修复但**尚未发版**crates.io 最新仍是 12.0.1GitHub 最新 tag 仍是 v12.0.0)。
- 决策一:保留 enum 级 `deny_unknown_fields`,serde 侧严格性不动;新增契约用例 `server-rs/crates/shared-contracts/tests/model3d_multiview_request_contract.rs` 钉住“变体里的未知字段必须被拒绝”,防止后续为消警改成非严格模式。
- 决策二:两个 workspace`server-rs``apps/ai-game-creator-shell/src-tauri`)的 `ts-rs` 固定到上游修复 commit `fd8679ef20f866f747c046b30bd29c15e3d7a003`,而不是启用 `no-serde-warnings`:后者会把 `deserialize_with` 等**真实**未支持的 serde 属性一并静默,掩盖 Rust/TS 类型漂移。
- 退出条件:等 crates.io 发布 > 12.0.1 后改回 `ts-rs = "12.0.1"`(或更高 registry 版本)并删除 commit 固定;届时重新跑一次 `npm run contracts:model3d:generate` + prettier 确认绑定零 diff。
- 影响面:两个 workspace 的 `Cargo.lock``ts-rs` / `ts-rs-macros` 从 registry 源改成 git 源(新增 `indexmap` 依赖);CI 与本地构建需要能访问 GitHub(仓库已因 `tripo3d-sdk` 具备该前提)。
- 验证:`cargo check -p shared-contracts``cargo check --locked --bin genarrative-ai-game-creator-shell` 均无 `deny_unknown_fields` 告警;`cargo test -p shared-contracts`101+2+3+2 全绿)后 `npm run contracts:model3d:generate` + prettier 使 `packages/shared/src/contracts/model3d/` 零 diff`npm run check:encoding``git diff --check``rustfmt --check` 通过。AGC 侧保留 1 条既有的 `deserialize_with` 告警(`ui_editor/component/image.rs`)。
- 关联文档:[踩坑与排障记录](pitfalls.md)、[Tripo 3D模型Provider集成](../../technical/【技术方案】Tripo 3D模型Provider集成-2026-09-18.md)。
## 2026-09-17 图集切分模式改为显式声明
- 决策:`sliceMode` 在图标图集生成入口成为必填字段且不保留任何默认值。省略、`null` 或空字符串必须在引用解析、定价、入队和 provider / OSS 副作用之前返回 `400``field=sliceMode`);`grid` 必须同时提供 `gridX`/`gridY``connected-components` 不得携带网格尺寸,二者矛盾同样在副作用前失败关闭。
@@ -4,6 +4,14 @@
Vite 默认监听应用根下的 Rust `src-tauri/target`,构建产物较多时会创建大量 Windows 文件监听器。AGC 配置通过 `server.watch.ignored: ['**/src-tauri/target/**']` 排除此目录,不关闭业务源码、CSS、共享组件监听或 HMR。排查时区分后端就绪、Vite 扫描和原生窗口首绘;监听目录回归不能代替实机首绘测量,验证入口见本地开发运维文档。
## 2026-09-19 ts-rs 的 “failed to parse serde attribute” 不代表 `deny_unknown_fields` 失效,不要靠删属性或全局静默消警
- **现象**:编译 `shared-contracts`(以及 AGC 壳)稳定打印一条**没有 span**的告警:`warning: failed to parse serde attribute / | deny_unknown_fields | = note: ts-rs failed to parse this attribute. It will be ignored.`。因为不带文件名和行号,容易被当成“这条属性没生效”,进而被顺手删掉。
- **成因**:告警只来自 ts-rs 宏对 serde 属性的**解析**`StructAttr` 里有 `"deny_unknown_fields" | "default"` 的静默分支,`EnumAttr` 没有,所以只有 enum 级 `#[serde(deny_unknown_fields)]` 会告警。**serde 侧该属性一直生效**:内部 tag 枚举的 struct 变体会真的拒绝未知字段(契约用例见 `server-rs/crates/shared-contracts/tests/model3d_multiview_request_contract.rs`)。TS 绑定本来就无法表达“拒绝未知字段”,告警与绑定内容无关。
- **处理(现行口径)**:保留属性,禁止用 `no-serde-warnings` feature 消警(它会连同 `deserialize_with` 这类**真实**未支持的 serde 属性一起静默)。两个 workspace 的 `ts-rs` 固定到上游修复 commit `fd8679ef20f866f747c046b30bd29c15e3d7a003`PR #491`EnumAttr` 增加 `"deny_unknown_fields" | "default"` 分支),等 crates.io 发布 > 12.0.1 后改回 registry 版本并删掉固定。
- **验证**`cargo check -p shared-contracts`server-rs)与 `cargo check --locked --bin genarrative-ai-game-creator-shell`AGC)都不再出现 `deny_unknown_fields` 告警;`cargo test -p shared-contracts` 全绿且 `npm run contracts:model3d:generate` + `npx prettier --write packages/shared/src/contracts/model3d` 后绑定零 diff(固定 commit 不改变生成结果)。AGC 侧仍会打印 1 条 `deserialize_with = "deserialize_fill_amount"` 告警(`ui_editor/component/image.rs`,字段类型仍是 `f32`,属既有无害告警)。
- **关联**`server-rs/Cargo.toml``apps/ai-game-creator-shell/src-tauri/Cargo.toml`(两处 `ts-rs` 固定)、`server-rs/crates/shared-contracts/src/model3d/multiview_to_model/request.rs`
## 2026-09-17 AGC 输入盒的「推理档」弹层被祖先裁切:要放开裁切而不是挪弹层
- **现象**:窄窗口下(视口 ≤1000px 时右侧对话面板只有 280px 宽)点开输入盒右下角的「推理档」,弹层是个**空盒子**:档位文字(默认 / 低 / 中 / 高 / 最高)整片看不见,只剩一个方框。
+3 -4
View File
@@ -6314,8 +6314,7 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "ts-rs"
version = "12.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "756050066659291d47a554a9f558125db17428b073c5ffce1daf5dcb0f7231d8"
source = "git+https://github.com/Aleph-Alpha/ts-rs?rev=fd8679ef20f866f747c046b30bd29c15e3d7a003#fd8679ef20f866f747c046b30bd29c15e3d7a003"
dependencies = [
"thiserror 2.0.18",
"ts-rs-macros",
@@ -6324,9 +6323,9 @@ dependencies = [
[[package]]
name = "ts-rs-macros"
version = "12.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38d90eea51bc7988ef9e674bf80a85ba6804739e535e9cab48e4bb34a8b652aa"
source = "git+https://github.com/Aleph-Alpha/ts-rs?rev=fd8679ef20f866f747c046b30bd29c15e3d7a003#fd8679ef20f866f747c046b30bd29c15e3d7a003"
dependencies = [
"indexmap 2.14.0",
"proc-macro2",
"quote",
"syn 2.0.118",
+7 -1
View File
@@ -117,7 +117,13 @@ tokio = "1"
tokio-stream = "0.1"
tokio-tungstenite = "0.27"
tripo3d-sdk = { git = "https://github.com/VAST-AI-Research/tripo-rust-sdk.git", rev = "1986be88627b04b1fa582a795bf6ac15ae6e0c6d" }
ts-rs = "12.0.1"
# 已发布到 crates.io 的 ts-rs 12.0.1 解析不了 enum 级 `serde(deny_unknown_fields)`
# 编译时会打印一条没有 span 的 “failed to parse serde attribute” 告警(该属性仍由 serde 生效)。
# 上游修复 #491 尚未发版(最新 tag 仍为 v12.0.0main 版本号还是 12.0.1),因此固定到修复 commit;
# 等 crates.io 发布 > 12.0.1 后改回 registry 版本并删除本固定。不要改用
# `no-serde-warnings` 关告警:那会连同真实未支持的 serde 属性一起静默。
# 严格性契约测试见 shared-contracts/tests/model3d_multiview_request_contract.rs。
ts-rs = { git = "https://github.com/Aleph-Alpha/ts-rs", rev = "fd8679ef20f866f747c046b30bd29c15e3d7a003" }
tower = "0.5"
tower-http = "0.6"
tracing = "0.1"
@@ -8,8 +8,11 @@ use crate::model3d::common::{
use super::MODEL3D_TS_EXPORT_DIR;
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
// 注:ts-rs 12 不支持 enum 级 deny_unknown_fields,构建时会打印一条
// “failed to parse serde attribute” 告警;该属性仍由 serde 生效,生成的 TS 绑定不受影响
// 注:该属性由 serde 生效,JSON 侧会拒绝这些变体里的未知字段
// (见 tests/model3d_multiview_request_contract.rs);TS 绑定无法表达这份严格性
// 已发布版 ts-rs 12.0.1 的 EnumAttr 解析不了它,会打印一条没有 span 的
// “failed to parse serde attribute” 告警,因此 workspace 固定到上游修复 commit#491)。
// 不要为了消警删掉该属性,也不要改用 `no-serde-warnings` 一并静默其它未支持的属性。
#[serde(tag = "kind", rename_all = "camelCase", deny_unknown_fields)]
#[derive(ts_rs::TS)]
#[ts(export, export_to = MODEL3D_TS_EXPORT_DIR)]
@@ -0,0 +1,78 @@
use serde_json::json;
use shared_contracts::model3d::common::Model3dModelVersion;
use shared_contracts::model3d::multiview_to_model::{
Model3dMultiviewInputs, Model3dMultiviewToModelRequest,
};
#[test]
fn multiview_inputs_accepts_declared_fields_and_camel_case_tag() {
let inputs: Model3dMultiviewInputs = serde_json::from_value(json!({
"kind": "views",
"front": "front.png",
"left": "left.png"
}))
.expect("已声明字段应可反序列化");
match inputs {
Model3dMultiviewInputs::Views { front, left, .. } => {
assert_eq!(front, "front.png");
assert_eq!(left.as_deref(), Some("left.png"));
}
Model3dMultiviewInputs::TaskId { .. } => panic!("kind=views 不应解成 taskId 变体"),
}
let task: Model3dMultiviewInputs = serde_json::from_value(json!({
"kind": "taskId",
"taskId": "tripo-task-1"
}))
.expect("taskId 变体使用 camelCase 字段名");
assert!(matches!(task, Model3dMultiviewInputs::TaskId { .. }));
}
#[test]
fn multiview_inputs_reject_unknown_fields_inside_variants() {
// enum 级 `deny_unknown_fields` 由 serde 生效,ts-rs 12 无法在 TS 绑定里表达;
// 这条用例钉住该严格性,防止为消除 ts-rs 告警而把它改成非严格模式。
let error = serde_json::from_value::<Model3dMultiviewInputs>(json!({
"kind": "views",
"front": "front.png",
"left": "left.png",
"back": "back.png",
"right": "right.png",
"unexpected": true
}))
.expect_err("变体里出现未知字段应被拒绝");
assert!(
error.to_string().contains("unknown field"),
"未知字段错误应指出 unknown field,实际为:{error}"
);
let error = serde_json::from_value::<Model3dMultiviewInputs>(json!({
"kind": "taskId",
"taskId": "tripo-task-1",
"unexpected": true
}))
.expect_err("taskId 变体里出现未知字段应被拒绝");
assert!(
error.to_string().contains("unknown field"),
"未知字段错误应指出 unknown field,实际为:{error}"
);
}
#[test]
fn multiview_request_rejects_unknown_top_level_fields() {
let error = serde_json::from_value::<Model3dMultiviewToModelRequest>(json!({
"inputs": { "kind": "taskId", "taskId": "tripo-task-1" },
"model": Model3dModelVersion::H31.as_str(),
"unexpected": true
}))
.expect_err("请求体顶层出现未知字段应被拒绝");
assert!(
error.to_string().contains("unknown field"),
"未知字段错误应指出 unknown field,实际为:{error}"
);
}