From b4fd8d9b8b24a78abd137a64df90b07787fee7fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Tue, 15 Sep 2026 17:37:28 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=B6=E7=B4=A7=20AGC=20manifest=20=E8=B5=84?= =?UTF-8?q?=E6=BA=90=20kind=20=E5=AD=97=E6=AE=B5=20=E5=B0=86=20manifest=20?= =?UTF-8?q?=E6=9D=A1=E7=9B=AE=20kind=20=E6=94=B9=E4=B8=BA=20Rust=20?= =?UTF-8?q?=E6=9E=9A=E4=B8=BE=E5=B9=B6=E6=8A=8A=E6=9C=AA=E7=9F=A5=E5=80=BC?= =?UTF-8?q?=E6=94=B6=E5=8F=A3=E4=B8=BA=20Unknown=20=E4=BF=AE=E6=AD=A3?= =?UTF-8?q?=E5=AD=97=E4=BD=93=E4=B8=8A=E4=BC=A0=E4=B8=8E=E5=9B=BE=E9=9B=86?= =?UTF-8?q?=E5=88=87=E7=89=87=E7=9A=84=E6=AD=A3=E5=BC=8F=E8=B5=84=E6=BA=90?= =?UTF-8?q?=20kind=20=E4=BF=9D=E6=8C=81=20source.kind=20=E4=B8=8E=20genera?= =?UTF-8?q?tionKind=20=E7=AD=89=E5=85=B6=E4=BB=96=E5=AD=97=E7=AC=A6?= =?UTF-8?q?=E4=B8=B2=E5=AD=97=E6=AE=B5=E4=B8=8D=E5=8F=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src-tauri/src/agent/direct_runtime.rs | 4 +-- .../src-tauri/src/agent/direct_tool_bridge.rs | 4 +-- .../src/agent/generation/canvas_generation.rs | 6 ++-- .../src/agent/generation/prompt_context.rs | 2 +- .../src-tauri/src/assets.rs | 33 +++++++++-------- .../src-tauri/src/commands.rs | 4 +-- .../src-tauri/src/main.rs | 4 +-- .../src-tauri/src/project/resource_editor.rs | 24 ++++++++----- .../src-tauri/src/ui_editor/workflow.rs | 2 +- ...】AGC资源kindRust枚举与ts-rs绑定-2026-09-15.md | 5 +++ .../shared-contracts/src/game_creation_app.rs | 6 ++-- .../src/game_creation_app/asset_kind.rs | 35 +++++++++++++++++++ 12 files changed, 90 insertions(+), 39 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime.rs index 2e4cdad61..973c39caf 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime.rs @@ -2020,7 +2020,7 @@ fn direct_taonier_art_asset_identity( local_asset_id: asset.id.clone(), source_sha256: validated.content_sha256, media_type: asset.media_type.clone(), - canonical_asset_kind: asset.kind.clone(), + canonical_asset_kind: asset.kind.to_string(), resource_id: asset .source .resource_id @@ -2546,7 +2546,7 @@ async fn recover_direct_taonier_spritesheet_read_only_at( &source_asset.id, &format!("{:x}", Sha256::digest(&source_bytes)), &source_asset.media_type, - &source_asset.kind, + source_asset.kind.as_str(), )?; let principal = external_editor_binding_principal(&access)?; let Some(project_binding) = diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tool_bridge.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tool_bridge.rs index 8832c1267..20109edf4 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tool_bridge.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tool_bridge.rs @@ -1169,7 +1169,7 @@ fn bridge_registered_resource( // (落盘值 + 按 kind 派生 + 读时自愈),这里走 Rust 的同构实现。 // 直接透传落盘 `asset.category` 会让 `kind:"ui"` 的资产在 UI 显示「UI 交互」、 // 在 Agent 侧读到 `unclassified`(真机 55 条分歧)。 - "category": game_creation_app_asset_effective_category(&asset.kind, asset.category), + "category": game_creation_app_asset_effective_category(asset.kind.as_str(), asset.category), "tags": asset.tags, "canvasProjectId": asset.source.canvas_project_id, "resourceId": asset.source.resource_id, @@ -1828,7 +1828,7 @@ async fn bridge_create_or_derive_resource( source_asset_id: source_asset.as_ref().map(|asset| asset.id.clone()), source_path: source_asset.as_ref().map(|asset| asset.local_path.clone()), source_media_type: source_asset.as_ref().map(|asset| asset.media_type.clone()), - source_subtype: source_asset.as_ref().map(|asset| asset.kind.clone()), + source_subtype: source_asset.as_ref().map(|asset| asset.kind.to_string()), producer_task_id: source_asset .as_ref() .and_then(|asset| asset.source.task_id.clone()), diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/generation/canvas_generation.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/generation/canvas_generation.rs index 99177a4f4..598274053 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/generation/canvas_generation.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/generation/canvas_generation.rs @@ -1649,7 +1649,7 @@ async fn canonical_art_spec_reference_at( &source.id, &format!("{:x}", Sha256::digest(&bytes)), &source.media_type, - &source.kind, + source.kind.as_str(), )?; if let Some(binding) = read_external_editor_resource_binding_at( root, @@ -6801,7 +6801,7 @@ fn register_platform_art_slice_manifest_entries_at( .iter_mut() .find(|asset| asset.local_path == registration.local_path) { - existing.kind = "art-spritesheet-slice".to_string(); + existing.kind = GameCreationAppAssetKind::Icon; existing.media_type = registration.media_type.clone(); existing.image_sequence_frames = None; existing.image_sequence_duration_ms = None; @@ -6815,7 +6815,7 @@ fn register_platform_art_slice_manifest_entries_at( ); manifest.assets.push(GameCreationAppAssetManifestEntry { id: id.clone(), - kind: "art-spritesheet-slice".to_string(), + kind: GameCreationAppAssetKind::Icon, media_type: registration.media_type.clone(), local_path: registration.local_path.clone(), image_sequence_frames: None, diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/generation/prompt_context.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/generation/prompt_context.rs index 9d1f102c9..122641101 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/generation/prompt_context.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/generation/prompt_context.rs @@ -98,7 +98,7 @@ pub(crate) fn render_local_asset_prompt_context(root: &Path) -> Result bool { /// 分类时才生效,`kind` 本身错时自愈只会把错值放大。 /// /// 判不出内容类型时返回中性的 `asset`(派生 `unclassified` → 「待归类」),**不猜具体类型**。 -fn uploaded_asset_kind(file_name: &str, media_type: &str) -> &'static str { +fn uploaded_asset_kind(file_name: &str, media_type: &str) -> GameCreationAppAssetKind { let media_type = media_type.trim().to_ascii_lowercase(); let extension = Path::new(file_name) .extension() @@ -492,20 +492,20 @@ fn uploaded_asset_kind(file_name: &str, media_type: &str) -> &'static str { "mp3" | "wav" | "ogg" | "m4a" | "aac" | "flac" | "opus" ) { - "audio" + GameCreationAppAssetKind::Audio } else if media_type.starts_with("video/") || matches!(extension, "mp4" | "webm" | "mov") { - "video" + GameCreationAppAssetKind::Video } else if media_type.starts_with("font/") || matches!(extension, "ttf" | "otf" | "woff" | "woff2") { - "document" + GameCreationAppAssetKind::Font } else if media_type.starts_with("image/") || matches!( extension, "png" | "jpg" | "jpeg" | "webp" | "gif" | "svg" | "avif" | "bmp" ) { - "image" + GameCreationAppAssetKind::Image } else if matches!(media_type.as_str(), "text/html" | "text/css") || media_type.contains("javascript") || media_type.contains("typescript") @@ -514,7 +514,7 @@ fn uploaded_asset_kind(file_name: &str, media_type: &str) -> &'static str { "html" | "htm" | "css" | "js" | "mjs" | "cjs" | "jsx" | "ts" | "tsx" ) { - "code" + GameCreationAppAssetKind::Code } else if media_type.starts_with("text/") || matches!(media_type.as_str(), "application/json" | "application/xml") || matches!( @@ -532,9 +532,9 @@ fn uploaded_asset_kind(file_name: &str, media_type: &str) -> &'static str { | "xml" ) { - "document" + GameCreationAppAssetKind::Document } else { - "asset" + GameCreationAppAssetKind::Unknown } } @@ -563,7 +563,7 @@ pub(crate) fn upload_local_asset_at( register_local_asset_entry( root, &relative_path, - uploaded_asset_kind(file_name, media_type), + uploaded_asset_kind(file_name, media_type).as_str(), media_type, "upload", GameCreationAppAssetSource { @@ -1879,7 +1879,10 @@ pub(crate) fn register_local_asset_entry( let normalized_path = normalize_relative_path(local_path)?; let absolute_path = resolve_local_project_path(root, &normalized_path)?; let manifest_path = root.join(".agent/manifest.json"); - let kind = if kind.is_empty() { "asset" } else { kind }; + let kind = GameCreationAppAssetKind::parse_with_context( + if kind.is_empty() { "unknown" } else { kind }, + "register_local_asset_entry", + ); let media_type = if media_type.is_empty() { "application/octet-stream" } else { @@ -1899,8 +1902,8 @@ pub(crate) fn register_local_asset_entry( // 时才触发),于是这个资产永远停在错误栏目。 // kind 没变时刻意不动 category——落盘分类是权威值,同 kind 重登记不得抹掉它。 if existing.kind != kind { - existing.kind = kind.to_string(); - existing.category = game_creation_app_asset_category_for_kind(kind); + existing.kind = kind; + existing.category = game_creation_app_asset_category_for_kind(kind.as_str()); } existing.media_type = media_type.to_string(); existing.source = source; @@ -1913,12 +1916,12 @@ pub(crate) fn register_local_asset_entry( ); manifest.assets.push(GameCreationAppAssetManifestEntry { id: id.clone(), - kind: kind.to_string(), + kind, media_type: media_type.to_string(), local_path: normalized_path.clone(), image_sequence_frames: None, image_sequence_duration_ms: None, - category: game_creation_app_asset_category_for_kind(kind), + category: game_creation_app_asset_category_for_kind(kind.as_str()), tags: Vec::new(), source, }); @@ -1931,7 +1934,7 @@ pub(crate) fn register_local_asset_entry( "recordType": record_type, "assetId": id.clone(), "localPath": normalized_path.clone(), - "kind": kind, + "kind": kind.as_str(), "mediaType": media_type, "source": source_for_record, }), diff --git a/apps/ai-game-creator-shell/src-tauri/src/commands.rs b/apps/ai-game-creator-shell/src-tauri/src/commands.rs index 989f16689..faf059c48 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/commands.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/commands.rs @@ -4036,7 +4036,7 @@ pub(crate) fn import_local_project_assets_for_agent( imported.push(ImportedAsset { id: existing.id.clone(), local_path: existing.local_path.clone(), - asset_kind: Some(existing.kind.clone()), + asset_kind: Some(existing.kind.to_string()), }); continue; } @@ -4234,7 +4234,7 @@ pub(crate) async fn import_account_editor_assets_for_agent( imported.push(ImportedAsset { id: existing.id.clone(), local_path: existing.local_path.clone(), - asset_kind: Some(existing.kind.clone()), + asset_kind: Some(existing.kind.to_string()), }); continue; } diff --git a/apps/ai-game-creator-shell/src-tauri/src/main.rs b/apps/ai-game-creator-shell/src-tauri/src/main.rs index 4d5030c2a..2284da2a5 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/main.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/main.rs @@ -33,8 +33,8 @@ use shared_contracts::game_creation_app::{ GameCreationAgentCapabilityDescriptor, GameCreationAgentPassPlanTrace, GameCreationAgentRepairRouteTrace, GameCreationAgentRunStep, GameCreationAgentRunTaskGraphTrace, GameCreationAgentRunTrace, GameCreationAgentToolCallTrace, - GameCreationAppAgentGroup, GameCreationAppAssetManifestEntry, GameCreationAppAssetSource, - GameCreationAppAssetSourceKind, GameCreationAppCommandRunState, + GameCreationAppAgentGroup, GameCreationAppAssetKind, GameCreationAppAssetManifestEntry, + GameCreationAppAssetSource, GameCreationAppAssetSourceKind, GameCreationAppCommandRunState, GameCreationAppCommandRunStatus, GameCreationAppLimitedRunCommandDescriptor, GameCreationAppManifest, GameCreationAppPermission, GameCreationAppPreviewState, GameCreationAppPreviewStatus, GameCreationAppTaskState, GameCreationAppTaskStatus, diff --git a/apps/ai-game-creator-shell/src-tauri/src/project/resource_editor.rs b/apps/ai-game-creator-shell/src-tauri/src/project/resource_editor.rs index b1f292976..d7a8a58a7 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/project/resource_editor.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/project/resource_editor.rs @@ -1303,8 +1303,13 @@ fn resolve_resource_edit_source( let asset_kind = source_asset .as_ref() .map(|asset| asset.kind.clone()) - .or_else(|| input.source_subtype.clone()) - .unwrap_or_else(|| "asset".to_string()); + .or_else(|| { + input + .source_subtype + .as_deref() + .map(|value| GameCreationAppAssetKind::parse_with_context(value, "resource-edit")) + }) + .unwrap_or(GameCreationAppAssetKind::Unknown); let canonical_resource_id = source_asset .as_ref() .map(source_asset_canonical_resource_id) @@ -1332,7 +1337,7 @@ fn resolve_resource_edit_source( canonical_resource_id, source_path: Some(path), media_type, - asset_kind, + asset_kind: asset_kind.to_string(), source_sha256: sha256_hex(text.as_bytes()), bytes: None, generation_mode: input.generation_mode, @@ -1363,7 +1368,7 @@ fn resolve_resource_edit_source( if matches!( input.edit_kind, LocalProjectResourceEditKind::SoundEffect | LocalProjectResourceEditKind::BackgroundMusic - ) && resource_edit_audio_kind(&asset_kind, &path) != input.edit_kind + ) && resource_edit_audio_kind(asset_kind.as_str(), &path) != input.edit_kind { return Err("音频资源的音效/BGM 编辑类型与源资源用途不一致".to_string()); } @@ -1385,7 +1390,7 @@ fn resolve_resource_edit_source( canonical_resource_id, source_path: Some(path), media_type, - asset_kind, + asset_kind: asset_kind.to_string(), source_sha256: sha256_hex(&bytes), bytes: Some(bytes), generation_mode: input.generation_mode, @@ -3805,7 +3810,10 @@ pub(crate) fn normalize_local_project_raster_resource_at( .unwrap_or("art-image"); let asset = GameCreationAppAssetManifestEntry { id: asset_id.clone(), - kind: source_subtype.to_string(), + kind: GameCreationAppAssetKind::parse_with_context( + source_subtype, + "resource-edit.derivative", + ), media_type: verified_media_type, local_path: source_path, image_sequence_frames: None, @@ -4058,7 +4066,7 @@ fn commit_resource_edit_asset_internal( let category = game_creation_app_asset_category_for_kind(&asset_kind); let asset = GameCreationAppAssetManifestEntry { id: asset_id.clone(), - kind: asset_kind, + kind: GameCreationAppAssetKind::parse_with_context(&asset_kind, "resource-edit.derivative"), media_type: staged_media_type.to_string(), local_path: relative_path.clone(), image_sequence_frames, @@ -5165,7 +5173,7 @@ pub(crate) async fn resume_local_project_resource_edit_at( let source_subtype = ledger .source_asset_kind .clone() - .or_else(|| source_asset.as_ref().map(|asset| asset.kind.clone())) + .or_else(|| source_asset.as_ref().map(|asset| asset.kind.to_string())) .or_else(|| Some(infer_resource_edit_source_asset_kind(&ledger.edit_kind))); derive_local_project_resource_at(DeriveLocalProjectResourceInput { project_path: input.project_path, diff --git a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/workflow.rs b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/workflow.rs index 22de573c6..3eccc2876 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/workflow.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/workflow.rs @@ -880,7 +880,7 @@ fn install_page_component_assets( .and_then(|name| name.to_str()) .unwrap_or("UI 素材") .to_string(), - asset_type: asset.kind.clone(), + asset_type: asset.kind.to_string(), }; sprite.path = asset.local_path.clone(); match state.sprite_assets.get(&asset_id) { diff --git a/docs/project-memory/plans/【实施计划】AGC资源kindRust枚举与ts-rs绑定-2026-09-15.md b/docs/project-memory/plans/【实施计划】AGC资源kindRust枚举与ts-rs绑定-2026-09-15.md index 7f76005c8..8d90b2e62 100644 --- a/docs/project-memory/plans/【实施计划】AGC资源kindRust枚举与ts-rs绑定-2026-09-15.md +++ b/docs/project-memory/plans/【实施计划】AGC资源kindRust枚举与ts-rs绑定-2026-09-15.md @@ -16,6 +16,11 @@ - shell 内生成的 GameCreationApp kind TypeScript 文件; - 对应 Rust/TS 定向测试与文档 todo 证据。 +本切片已落地: + +- `GameCreationAppAssetManifestEntry.kind` 改为 `GameCreationAppAssetKind`;manifest 反序列化未知值收口为 `Unknown`。 +- 上传内容推断的字体写入 `Font`,未知上传类型写入 `Unknown`;图集切片 manifest 写入 `Icon`。 + 明确不修改: - manifest 结构体的 `kind: String` 字段及所有调用方(留给下一个小提交); diff --git a/server-rs/crates/shared-contracts/src/game_creation_app.rs b/server-rs/crates/shared-contracts/src/game_creation_app.rs index bd528afb7..39e622dc1 100644 --- a/server-rs/crates/shared-contracts/src/game_creation_app.rs +++ b/server-rs/crates/shared-contracts/src/game_creation_app.rs @@ -496,7 +496,7 @@ pub struct GameCreationAppImageSequenceFrame { #[serde(rename_all = "camelCase")] pub struct GameCreationAppAssetManifestEntry { pub id: String, - pub kind: String, + pub kind: GameCreationAppAssetKind, pub media_type: String, pub local_path: String, pub source: GameCreationAppAssetSource, @@ -558,7 +558,7 @@ impl<'de> Deserialize<'de> for GameCreationAppAssetManifestEntry { }; Ok(Self { id: wire.id, - kind: wire.kind, + kind: GameCreationAppAssetKind::parse_with_context(&wire.kind, "manifest.asset.kind"), media_type: wire.media_type, local_path: wire.local_path, source: wire.source, @@ -2084,7 +2084,7 @@ mod tests { }); manifest.assets.push(GameCreationAppAssetManifestEntry { id: "asset-player".to_string(), - kind: "character".to_string(), + kind: GameCreationAppAssetKind::Character, media_type: "image".to_string(), local_path: "assets/images/player.png".to_string(), image_sequence_frames: None, diff --git a/server-rs/crates/shared-contracts/src/game_creation_app/asset_kind.rs b/server-rs/crates/shared-contracts/src/game_creation_app/asset_kind.rs index fe103682b..5638a299a 100644 --- a/server-rs/crates/shared-contracts/src/game_creation_app/asset_kind.rs +++ b/server-rs/crates/shared-contracts/src/game_creation_app/asset_kind.rs @@ -1,5 +1,6 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer, de}; use std::fmt; +use std::ops::Deref; use std::str::FromStr; /// GameCreationApp manifest 资源 kind 的唯一 Rust 类型。 @@ -101,6 +102,8 @@ impl GameCreationAppAssetKind { /// 解析外部 manifest/API 字符串;未知值收口到 `Unknown` 并留下可检索的原始值与上下文。 pub fn parse_with_context(value: &str, context: &'static str) -> Self { + #[cfg(not(feature = "kind-observability"))] + let _ = context; let parsed = Self::from_str_lossy(value); if parsed == Self::Unknown && value.trim() != Self::Unknown.as_str() { #[cfg(feature = "kind-observability")] @@ -128,6 +131,38 @@ impl fmt::Display for GameCreationAppAssetKind { } } +impl AsRef for GameCreationAppAssetKind { + fn as_ref(&self) -> &str { + self.as_str() + } +} + +impl Deref for GameCreationAppAssetKind { + type Target = str; + + fn deref(&self) -> &Self::Target { + self.as_str() + } +} + +impl PartialEq for GameCreationAppAssetKind { + fn eq(&self, other: &str) -> bool { + self.as_str() == other + } +} + +impl PartialEq<&str> for GameCreationAppAssetKind { + fn eq(&self, other: &&str) -> bool { + self.as_str() == *other + } +} + +impl PartialEq for GameCreationAppAssetKind { + fn eq(&self, other: &String) -> bool { + self.as_str() == other + } +} + impl Serialize for GameCreationAppAssetKind { fn serialize(&self, serializer: S) -> Result where