From c48f0627cf5e56de0ab88f6c95e7c7b5c76f4806 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Sat, 19 Sep 2026 16:56:24 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20ts-rs=20=E5=AF=B9=20enum?= =?UTF-8?q?=20=E7=BA=A7=20deny=5Funknown=5Ffields=20=E7=9A=84=E8=A7=A3?= =?UTF-8?q?=E6=9E=90=E5=91=8A=E8=AD=A6=E5=B9=B6=E4=BF=9D=E7=95=99=E5=A5=91?= =?UTF-8?q?=E7=BA=A6=E4=B8=A5=E6=A0=BC=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 把 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 通过 --- .../src-tauri/Cargo.lock | 7 +- .../src-tauri/Cargo.toml | 5 +- .../shared-memory/decision-log.md | 10 +++ docs/project-memory/shared-memory/pitfalls.md | 8 ++ server-rs/Cargo.lock | 7 +- server-rs/Cargo.toml | 8 +- .../src/model3d/multiview_to_model/request.rs | 7 +- .../model3d_multiview_request_contract.rs | 78 +++++++++++++++++++ 8 files changed, 118 insertions(+), 12 deletions(-) create mode 100644 server-rs/crates/shared-contracts/tests/model3d_multiview_request_contract.rs diff --git a/apps/ai-game-creator-shell/src-tauri/Cargo.lock b/apps/ai-game-creator-shell/src-tauri/Cargo.lock index be749d083..2044029ff 100644 --- a/apps/ai-game-creator-shell/src-tauri/Cargo.lock +++ b/apps/ai-game-creator-shell/src-tauri/Cargo.lock @@ -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", diff --git a/apps/ai-game-creator-shell/src-tauri/Cargo.toml b/apps/ai-game-creator-shell/src-tauri/Cargo.toml index f5cc8b673..682ea0ab5 100644 --- a/apps/ai-game-creator-shell/src-tauri/Cargo.toml +++ b/apps/ai-game-creator-shell/src-tauri/Cargo.toml @@ -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" } diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 1b57435a0..2b891309d 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -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.1,GitHub 最新 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` 不得携带网格尺寸,二者矛盾同样在副作用前失败关闭。 diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index d59e89037..a8c482d06 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -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 宽)点开输入盒右下角的「推理档」,弹层是个**空盒子**:档位文字(默认 / 低 / 中 / 高 / 最高)整片看不见,只剩一个方框。 diff --git a/server-rs/Cargo.lock b/server-rs/Cargo.lock index e74fa84aa..89c3e8184 100644 --- a/server-rs/Cargo.lock +++ b/server-rs/Cargo.lock @@ -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", diff --git a/server-rs/Cargo.toml b/server-rs/Cargo.toml index 574747abf..271dbf419 100644 --- a/server-rs/Cargo.toml +++ b/server-rs/Cargo.toml @@ -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.0,main 版本号还是 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" diff --git a/server-rs/crates/shared-contracts/src/model3d/multiview_to_model/request.rs b/server-rs/crates/shared-contracts/src/model3d/multiview_to_model/request.rs index d91603a4f..33717b182 100644 --- a/server-rs/crates/shared-contracts/src/model3d/multiview_to_model/request.rs +++ b/server-rs/crates/shared-contracts/src/model3d/multiview_to_model/request.rs @@ -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)] diff --git a/server-rs/crates/shared-contracts/tests/model3d_multiview_request_contract.rs b/server-rs/crates/shared-contracts/tests/model3d_multiview_request_contract.rs new file mode 100644 index 000000000..b3802bcac --- /dev/null +++ b/server-rs/crates/shared-contracts/tests/model3d_multiview_request_contract.rs @@ -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::(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::(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::(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}" + ); +}