统一 UI 编辑器文档资产类型

新增 ui-design-doc 与 application/json 常量

创建、桥接、工作流和持久化统一使用严格资产类型
This commit is contained in:
2026-09-14 20:33:46 +08:00
parent 9cd1a94369
commit ee7d00c0b1
4 changed files with 34 additions and 22 deletions
@@ -2144,7 +2144,10 @@ pub(crate) fn create_ui_design_resource(
let next_index = manifest
.assets
.iter()
.filter(|asset| asset.kind == "UI")
.filter(|asset| {
asset.kind == crate::ui_editor::persistence::UI_DESIGN_DOC_ASSET_KIND
&& asset.media_type == crate::ui_editor::persistence::UI_DESIGN_DOC_MEDIA_TYPE
})
.count()
+ 1;
let resource_name = format!("UI 设计 {next_index}");
@@ -2178,8 +2181,8 @@ pub(crate) fn create_ui_design_resource(
let asset = match register_local_asset_at(
root,
&relative_path,
"UI",
"application/json",
crate::ui_editor::persistence::UI_DESIGN_DOC_ASSET_KIND,
crate::ui_editor::persistence::UI_DESIGN_DOC_MEDIA_TYPE,
"generated",
GameCreationAppAssetSource {
kind: GameCreationAppAssetSourceKind::Generated,
@@ -19,6 +19,8 @@ use std::time::{SystemTime, UNIX_EPOCH};
use typed_floats::tf32::StrictlyPositiveFinite;
const UI_DESIGN_STATE_SCHEMA_VERSION: &str = "game-creator-ui-design-state.v1";
pub(crate) const UI_DESIGN_DOC_ASSET_KIND: &str = "ui-design-doc";
pub(crate) const UI_DESIGN_DOC_MEDIA_TYPE: &str = "application/json";
const UI_DESIGN_STATE_MAX_BYTES: usize = 2 * 1024 * 1024;
const UI_DESIGN_CODE_MAX_BYTES: usize = UI_DESIGN_STATE_MAX_BYTES * 8;
const UI_DESIGN_STATE_MAX_IMAGES: usize = 4;
@@ -321,7 +323,7 @@ fn ui_design_asset(
.into_iter()
.find(|asset| asset.id == asset_id)
.ok_or_else(|| "UI 设计资源不存在".to_string())?;
if asset.kind != "UI" || asset.media_type != "application/json" {
if asset.kind != UI_DESIGN_DOC_ASSET_KIND || asset.media_type != UI_DESIGN_DOC_MEDIA_TYPE {
return Err("目标资源不是 UI 设计 JSON 资产".to_string());
}
normalize_relative_path(&asset.local_path)?;
@@ -815,8 +817,8 @@ mod tests {
let asset = register_local_asset_at(
directory.path(),
relative_path,
"UI",
"application/json",
UI_DESIGN_DOC_ASSET_KIND,
UI_DESIGN_DOC_MEDIA_TYPE,
"test",
GameCreationAppAssetSource {
kind: GameCreationAppAssetSourceKind::Generated,
@@ -1,4 +1,7 @@
use crate::ui_editor::persistence::initialize_ui_design_state_with_source_image_at;
use crate::ui_editor::persistence::{
initialize_ui_design_state_with_source_image_at, UI_DESIGN_DOC_ASSET_KIND,
UI_DESIGN_DOC_MEDIA_TYPE,
};
use crate::{
acquire_project_write_lock, advance_agent_runtime_project_revision_locked,
enforce_project_permission_policy, read_existing_manifest_for_project,
@@ -89,8 +92,8 @@ pub(crate) fn ensure_ui_design_resource_for_prototype(
}
if let Some(asset) = manifest.assets.iter().find(|asset| {
asset.kind == "UI"
&& asset.media_type == "application/json"
asset.kind == UI_DESIGN_DOC_ASSET_KIND
&& asset.media_type == UI_DESIGN_DOC_MEDIA_TYPE
&& asset.source.reference_resource_ids.iter().any(|reference| {
source_reference_ids
.iter()
@@ -118,8 +121,8 @@ pub(crate) fn ensure_ui_design_resource_for_prototype(
let asset = match register_local_asset_at(
root,
&relative_path,
"UI",
"application/json",
UI_DESIGN_DOC_ASSET_KIND,
UI_DESIGN_DOC_MEDIA_TYPE,
"generated",
GameCreationAppAssetSource {
kind: GameCreationAppAssetSourceKind::Generated,
@@ -193,7 +196,9 @@ fn next_ui_design_path(
let mut index = manifest
.assets
.iter()
.filter(|asset| asset.kind == "UI")
.filter(|asset| {
asset.kind == UI_DESIGN_DOC_ASSET_KIND && asset.media_type == UI_DESIGN_DOC_MEDIA_TYPE
})
.count()
+ 1;
loop {
@@ -281,11 +286,9 @@ mod tests {
};
let first = ensure_ui_design_resource_for_prototype(input.clone()).expect("bridge");
assert!(first.created);
assert_eq!(first.asset.kind, "UI");
assert_eq!(first.asset.kind, UI_DESIGN_DOC_ASSET_KIND);
// 写侧 → 分类的端到端断言:这条路径走的是与
// `workflow.rs` / `persistence.rs` 完全相同的 `register_local_asset_at(..., "UI", ...)`
// 别名表漏掉大写 `UI` 时,这里会落 unclassified(真机 8 条 UI 资产的表现),
// 且派生值本身就是 unclassified,读时自愈也救不回来。
// 写侧与 persistence/workflow 共用 UI 文档 kind/media 常量
assert_eq!(
first.asset.category,
GameCreationAppAssetCategory::UiInteraction
@@ -322,7 +325,10 @@ mod tests {
.manifest
.assets
.iter()
.filter(|asset| asset.kind == "UI")
.filter(|asset| {
asset.kind == UI_DESIGN_DOC_ASSET_KIND
&& asset.media_type == UI_DESIGN_DOC_MEDIA_TYPE
})
.count(),
1
);
@@ -7,6 +7,7 @@ use crate::ui_editor::layout::node::{Node, StageStatus};
use crate::ui_editor::persistence::{
initialize_ui_design_state_at, load_ui_design_state_at, save_ui_design_state_at,
LoadUiDesignStateInput, SaveUiDesignStateInput, SaveUiDesignStateResult,
UI_DESIGN_DOC_ASSET_KIND, UI_DESIGN_DOC_MEDIA_TYPE,
};
use crate::ui_editor::resource::font::FontAsset;
use crate::ui_editor::resource::sprite::{SpriteAsset, SpriteAssetMetadata, SpriteBorder};
@@ -683,8 +684,8 @@ fn find_page_ui_resource(
let Some(asset) = matches.into_iter().next() else {
return Ok(None);
};
if asset.kind != "UI"
|| asset.media_type != "application/json"
if asset.kind != UI_DESIGN_DOC_ASSET_KIND
|| asset.media_type != UI_DESIGN_DOC_MEDIA_TYPE
|| asset.local_path != workflow_relative_path(source, &page.page_id)
{
return Err(format!("页面 {} 的 UI workflow 资源身份冲突", page.page_id));
@@ -748,8 +749,8 @@ fn ensure_page_ui_resource(
let registered = register_local_asset_at(
root,
&relative_path,
"UI",
"application/json",
UI_DESIGN_DOC_ASSET_KIND,
UI_DESIGN_DOC_MEDIA_TYPE,
"ui-workflow",
GameCreationAppAssetSource {
kind: GameCreationAppAssetSourceKind::Generated,
@@ -1179,7 +1180,7 @@ fn update_page_manifest_stage(root: &Path, asset_id: &str, stage: &str) -> Resul
.iter_mut()
.find(|asset| asset.id == asset_id)
.ok_or_else(|| format!("UI workflow 资源 {} 未登记", asset_id))?;
if asset.kind != "UI" || asset.media_type != "application/json" {
if asset.kind != UI_DESIGN_DOC_ASSET_KIND || asset.media_type != UI_DESIGN_DOC_MEDIA_TYPE {
return Err(format!("UI workflow 资源 {} 类型不匹配", asset_id));
}
let next_kind = format!("ui-workflow.{stage}");