From 5c031f259064fcd2295a515b6052a100794b2da1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Mon, 17 Aug 2026 18:47:34 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E5=85=85=20UI=20=E6=96=87=E6=9C=AC?= =?UTF-8?q?=E5=AD=97=E4=BD=93=E5=BC=95=E7=94=A8=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 拒绝 Text 组件引用不存在的 font_assets 条目 新增无效字体引用持久化回归测试 同步 UI State 内部引用校验技术方案 --- .../src-tauri/src/ui_editor/persistence.rs | 87 +++++++++++++++++-- ...案】AI游戏创作智能体App实施计划-2026-06-24.md | 2 +- 2 files changed, 81 insertions(+), 8 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/persistence.rs b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/persistence.rs index 9f39a8114..ad0da4f96 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/persistence.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/persistence.rs @@ -532,13 +532,24 @@ fn validate_node( )); } for component in &node.components { - if let Component::Image(image) = component { - if image - .target_graphic - .as_ref() - .is_some_and(|id| !state.sprite_assets.contains_key(id)) - { - return Err("Image 组件引用了不存在的独立素材".to_string()); + match component { + Component::Image(image) => { + if image + .target_graphic + .as_ref() + .is_some_and(|id| !state.sprite_assets.contains_key(id)) + { + return Err("Image 组件引用了不存在的独立素材".to_string()); + } + } + Component::Text(text) => { + if text + .font + .as_ref() + .is_some_and(|id| !state.font_assets.contains_key(id)) + { + return Err("Text 组件引用了不存在的字体素材".to_string()); + } } } } @@ -774,4 +785,66 @@ mod tests { }) .is_err()); } + + #[test] + fn rejects_text_component_with_missing_font_asset() { + let (directory, asset_id) = fixture(); + let state: State = serde_json::from_value(serde_json::json!({ + "ui_trees": [{ + "src_ui_design": "page", + "root": { + "id": "root", + "transform": { + "anchor_min": [0.0, 0.0], + "anchor_max": [1.0, 1.0], + "offset_min": [0.0, 0.0], + "offset_max": [0.0, 0.0] + }, + "metadata": { + "name": "根节点", + "description": "", + "layout_status": "Pending", + "components_status": "Pending", + "allow_llm_edit_layout": false, + "allow_llm_edit_component": false, + "source": "System" + }, + "components": [{ + "Text": { + "content": "标题", + "font": "missing-font", + "font_style": "Normal", + "font_sizing": {"Fixed": 14}, + "color": [255, 255, 255, 255], + "alignment": "UpperLeft", + "horizontal_overflow": "Wrap", + "vertical_overflow": "Truncate", + "line_spacing": 1.0 + } + }], + "children": [] + } + }], + "ui_design_images": { + "page": { + "metadata": { + "name": "主界面", + "description": "", + "role": "Page", + "slave_to": null + }, + "path": "assets/page.png", + "pixel_size": [1280.0, 720.0], + "pixels_per_unit": 1.0 + } + }, + "sprite_assets": {}, + "font_assets": {} + })) + .expect("deserialize state with missing font reference"); + + let error = save_ui_design_state_at(input(directory.path(), &asset_id, 0, state)) + .expect_err("missing Text font reference must be rejected"); + assert!(error.contains("Text 组件引用了不存在的字体素材")); + } } diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index 9ed60b9fa..d4ff922f2 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -1076,7 +1076,7 @@ game-project/ ## 2026-08-17 UI Editor 项目资源 State 持久化 -- UI 编辑器复用现有 manifest `kind: "UI"`、`mediaType: "application/json"` 资源,不增加平行 asset kind。资源文件固定为严格 `game-creator-ui-design-state.v1` JSON envelope:`projectId`、`assetId`、每资源 `revision` 和 Rust 唯一源 `State`;旧空对象、未知字段、身份错配、超限、无效内部引用和不安全相对路径均失败关闭。 +- UI 编辑器复用现有 manifest `kind: "UI"`、`mediaType: "application/json"` 资源,不增加平行 asset kind。资源文件固定为严格 `game-creator-ui-design-state.v1` JSON envelope:`projectId`、`assetId`、每资源 `revision` 和 Rust 唯一源 `State`;旧空对象、未知字段、身份错配、超限、无效内部引用和不安全相对路径均失败关闭。内部引用校验同时覆盖 `Image.target_graphic -> sprite_assets` 与 `Text.font -> font_assets`,可选引用非空时必须命中同一 State 内已登记资源。 - Tauri 专用 load/save command 只接受项目路径、期望项目 ID、manifest asset ID 和(保存时)资源 revision;Rust 按 manifest 解析受控本地路径并在项目写锁内做 CAS。相同 `State` 返回 unchanged 且不推进 project revision;不同内容安装并回读一致后才推进 revision,后续推进失败返回 `reconciliation-required`,不伪装为完整保存。 - UI State 原子安装保留最近一个有效 `.previous` 恢复副本。主文件损坏时仅在恢复副本通过同一严格校验后恢复;恢复安装与保存共用项目写锁,并在持锁后重新读取主文件,已有并发保存的有效新版本时直接返回而不安装旧副本。任一候选均不可信则停在加载错误,前端禁编辑和保存。新建 UI 资源先登记并安装合法 envelope,任一步失败补偿 manifest/文件,避免把空 JSON 留给资源卡。 - 图片路径只需是安全项目相对路径,不要求外部图片仍存在或已登记为 manifest asset;缺失媒体只导致 preview 占位。`imageOrder`、当前选择、缩放、面板开关和 preview URL 不写入 State,加载后由 State 派生。保存冻结提交快照,保存期间的新编辑继续保持 dirty;AI state lock 和加载期间禁保存。