From f185d21f55bb4e5143d5983910f17e952ff595da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Wed, 16 Sep 2026 19:07:23 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B5=84=E6=BA=90=E7=BC=96=E8=BE=91=E6=BA=90?= =?UTF-8?q?=E8=BA=AB=E4=BB=BD=E6=94=B9=E7=94=A8=E7=8B=AC=E7=AB=8B=E8=AF=8D?= =?UTF-8?q?=E6=B1=87=E8=A1=A8=EF=BC=8C=E5=8F=AA=E4=BC=A0=E6=AD=A3=E5=BC=8F?= =?UTF-8?q?=20kind=20=E6=88=90=E5=91=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - resource_editor.rs:新增 `ResourceEditSourceKind`,把源身份拆成 `Manifest(GameCreationAppAssetKind)` / `ProjectVersion` / `AgentResultText` - resource_editor.rs:`ResourceEditSourceSnapshot.asset_kind` 由 `String` 收紧为该枚举,项目版本与 Agent 回执不再冒充 manifest kind - resource_editor.rs:`infer_resource_edit_source_asset_kind` 直接返回枚举成员,`art-image` / `svg` / `text` 收口为 `Image` / `Image` / `Document` - resource_editor.rs:栅格源归一化只接受正式枚举成员,`task-artifact` 特判删除,未知值按 `Image` 收口 - resource_editor.rs:账本、HTTP 载荷与提示词边界改用 `as_str()`,manifest 写回改用 `manifest_kind()` - resource_editor.rs:`source_subtype` 字段补充「只接受 manifest 正式 kind」的文档说明,测试 fixture 同步改用枚举成员 --- .../src-tauri/src/project/resource_editor.rs | 176 ++++++++++++------ 1 file changed, 115 insertions(+), 61 deletions(-) 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 5b32ab88a..9046ce21b 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 @@ -180,6 +180,8 @@ pub(crate) struct DeriveLocalProjectResourceInput { pub(crate) source_path: Option, #[serde(default)] pub(crate) source_media_type: Option, + /// 源资源 subtype:只接受 manifest 的正式 `GameCreationAppAssetKind` 成员。 + /// 任务产物、附件、项目版本与 Agent 回执等非 manifest 来源不通过该字段传递。 #[serde(default)] pub(crate) source_subtype: Option, #[serde(default)] @@ -296,6 +298,7 @@ pub(crate) struct NormalizeLocalProjectRasterResourceInput { pub(crate) source_resource_id: String, pub(crate) source_path: String, pub(crate) source_media_type: String, + /// 源资源 subtype:只接受 manifest 的正式 `GameCreationAppAssetKind` 成员。 #[serde(default)] pub(crate) source_subtype: Option, pub(crate) producer_task_id: String, @@ -309,12 +312,43 @@ pub(crate) struct NormalizeLocalProjectRasterResourceResult { pub(crate) manifest: GameCreationAppManifest, } +/// 资源编辑源身份的词汇表。 +/// +/// `Manifest` 只承载 manifest 的正式 `GameCreationAppAssetKind` 成员;项目版本快照与 +/// Agent 回执文本没有 manifest kind,单独成变体,避免它们混进 kind 字段后被静默收口成 +/// `unknown`。 +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum ResourceEditSourceKind { + Manifest(GameCreationAppAssetKind), + ProjectVersion, + AgentResultText, +} + +impl ResourceEditSourceKind { + /// 私有账本与外部接口上下文使用的稳定标签。 + fn as_str(self) -> &'static str { + match self { + Self::Manifest(kind) => kind.as_str(), + Self::ProjectVersion => "project-version", + Self::AgentResultText => "agent-result-derivative", + } + } + + /// 派生资源写回 manifest 时使用的正式 kind;非 manifest 谱系没有 kind。 + fn manifest_kind(self) -> GameCreationAppAssetKind { + match self { + Self::Manifest(kind) => kind, + Self::ProjectVersion | Self::AgentResultText => GameCreationAppAssetKind::Unknown, + } + } +} + #[derive(Clone, Debug)] struct ResourceEditSourceSnapshot { canonical_resource_id: String, source_path: Option, media_type: String, - asset_kind: String, + asset_kind: ResourceEditSourceKind, source_sha256: String, bytes: Option>, generation_mode: LocalProjectResourceGenerationMode, @@ -1125,19 +1159,28 @@ fn infer_resource_edit_source_media_type( Some(media_type.to_string()) } -fn infer_resource_edit_source_asset_kind(edit_kind: &LocalProjectResourceEditKind) -> String { - match edit_kind { - LocalProjectResourceEditKind::ImageReference => "art-image", - LocalProjectResourceEditKind::Svg => "svg", - LocalProjectResourceEditKind::CharacterAnimation => "character-animation", - LocalProjectResourceEditKind::Video => "video", - LocalProjectResourceEditKind::SoundEffect => "sound-effect", - LocalProjectResourceEditKind::BackgroundMusic => "background-music", - LocalProjectResourceEditKind::Text => "text", - LocalProjectResourceEditKind::AgentResult => "agent-result-derivative", - LocalProjectResourceEditKind::Version => "project-version", - } - .to_string() +fn infer_resource_edit_source_asset_kind( + edit_kind: &LocalProjectResourceEditKind, +) -> ResourceEditSourceKind { + let kind = match edit_kind { + LocalProjectResourceEditKind::ImageReference | LocalProjectResourceEditKind::Svg => { + GameCreationAppAssetKind::Image + } + LocalProjectResourceEditKind::CharacterAnimation => { + GameCreationAppAssetKind::CharacterAnimation + } + LocalProjectResourceEditKind::Video => GameCreationAppAssetKind::Video, + LocalProjectResourceEditKind::SoundEffect => GameCreationAppAssetKind::SoundEffect, + LocalProjectResourceEditKind::BackgroundMusic => GameCreationAppAssetKind::BackgroundMusic, + LocalProjectResourceEditKind::Text => GameCreationAppAssetKind::Document, + LocalProjectResourceEditKind::AgentResult => { + return ResourceEditSourceKind::AgentResultText; + } + LocalProjectResourceEditKind::Version => { + return ResourceEditSourceKind::ProjectVersion; + } + }; + ResourceEditSourceKind::Manifest(kind) } fn resolve_agent_result_source( @@ -1183,7 +1226,7 @@ fn resolve_resource_edit_source( canonical_resource_id: format!("version:{version_id}"), source_path: None, media_type: "application/vnd.genarrative.project-version+json".to_string(), - asset_kind: "project-version".to_string(), + asset_kind: ResourceEditSourceKind::ProjectVersion, source_sha256: sha256_hex( &serde_json::to_vec(&version) .map_err(|error| format!("序列化源项目版本失败:{error}"))?, @@ -1208,7 +1251,7 @@ fn resolve_resource_edit_source( canonical_resource_id, source_path: None, media_type: "text/markdown".to_string(), - asset_kind: "agent-result-derivative".to_string(), + asset_kind: ResourceEditSourceKind::AgentResultText, source_sha256: sha256_hex(content.as_bytes()), bytes: None, generation_mode: input.generation_mode, @@ -1302,14 +1345,18 @@ fn resolve_resource_edit_source( .unwrap_or_default(); let asset_kind = source_asset .as_ref() - .map(|asset| asset.kind.clone()) + .map(|asset| ResourceEditSourceKind::Manifest(asset.kind)) .or_else(|| { - input - .source_subtype - .as_deref() - .map(|value| GameCreationAppAssetKind::parse_with_context(value, "resource-edit")) + input.source_subtype.as_deref().map(|value| { + ResourceEditSourceKind::Manifest(GameCreationAppAssetKind::parse_with_context( + value, + "resource-edit.source-subtype", + )) + }) }) - .unwrap_or(GameCreationAppAssetKind::Unknown); + .unwrap_or(ResourceEditSourceKind::Manifest( + GameCreationAppAssetKind::Unknown, + )); let canonical_resource_id = source_asset .as_ref() .map(source_asset_canonical_resource_id) @@ -1337,7 +1384,7 @@ fn resolve_resource_edit_source( canonical_resource_id, source_path: Some(path), media_type, - asset_kind: asset_kind.to_string(), + asset_kind, source_sha256: sha256_hex(text.as_bytes()), bytes: None, generation_mode: input.generation_mode, @@ -1390,7 +1437,7 @@ fn resolve_resource_edit_source( canonical_resource_id, source_path: Some(path), media_type, - asset_kind: asset_kind.to_string(), + asset_kind, source_sha256: sha256_hex(&bytes), bytes: Some(bytes), generation_mode: input.generation_mode, @@ -1823,7 +1870,7 @@ async fn confirm_resource_edit_source( "contentType": source.media_type, "contentLength": bytes.len(), "contentHash": source.source_sha256, - "assetKind": source.asset_kind, + "assetKind": source.asset_kind.as_str(), "accessPolicy": "private", })), ) @@ -1931,7 +1978,7 @@ async fn register_resource_edit_source_image( "width": decoded.width(), "height": decoded.height(), "sourceType": "uploaded", - "assetKind": source.asset_kind, + "assetKind": source.asset_kind.as_str(), "generationInputs": { "source": RESOURCE_EDIT_QUEUE_SOURCE, "localAssetId": local_asset_id, @@ -2044,7 +2091,7 @@ async fn ensure_resource_edit_source_reference( local_asset_id, &source.source_sha256, &source.media_type, - &source.asset_kind, + source.asset_kind.as_str(), )?; let principal_key = external_editor_project_binding_key_sha256(&input.expected_project_id, principal)?; @@ -2266,7 +2313,7 @@ fn build_resource_edit_audio_prompt( .and_then(|asset| asset.source.prompt.as_deref()) .map(str::trim) .filter(|value| !value.is_empty()); - let mut source_context = format!("名称 {source_name},用途 {}", source.asset_kind); + let mut source_context = format!("名称 {source_name},用途 {}", source.asset_kind.as_str()); if let Some(original_prompt) = original_prompt { source_context.push_str(",原始描述 "); source_context.push_str(original_prompt); @@ -3802,23 +3849,24 @@ pub(crate) fn normalize_local_project_raster_resource_at( .map_err(|error| format!("序列化源图片身份失败:{error}"))?; let identity_hash = sha256_hex(&identity_material); let asset_id = format!("normalized-{}", &identity_hash[..24]); - let source_subtype = input + // 该接口只把栅格源登记成 manifest 图片资产:源 subtype 只接受正式枚举成员, + // 未声明或非图片族成员时按图片收口。 + let source_kind = input .source_subtype .as_deref() .map(str::trim) - .filter(|value| !value.is_empty() && *value != "task-artifact") - .unwrap_or("art-image"); + .filter(|value| !value.is_empty()) + .map(|value| GameCreationAppAssetKind::parse_with_context(value, "resource-edit.normalize")) + .filter(|kind| *kind != GameCreationAppAssetKind::Unknown) + .unwrap_or(GameCreationAppAssetKind::Image); let asset = GameCreationAppAssetManifestEntry { id: asset_id.clone(), - kind: GameCreationAppAssetKind::parse_with_context( - source_subtype, - "resource-edit.derivative", - ), + kind: source_kind, media_type: verified_media_type, local_path: source_path, image_sequence_frames: None, image_sequence_duration_ms: None, - category: game_creation_app_asset_category_for_kind(source_subtype), + category: game_creation_app_asset_category_for_kind(source_kind.as_str()), tags: Vec::new(), source: GameCreationAppAssetSource { kind: GameCreationAppAssetSourceKind::Generated, @@ -4059,14 +4107,14 @@ fn commit_resource_edit_asset_internal( .transpose()?; let image_sequence_duration_ms = ledger.remote_sequence_duration_ms; let asset_kind = if input.edit_kind == LocalProjectResourceEditKind::CharacterAnimation { - "character-animation".to_string() + GameCreationAppAssetKind::CharacterAnimation } else { - source.asset_kind.clone() + source.asset_kind.manifest_kind() }; - let category = game_creation_app_asset_category_for_kind(&asset_kind); + let category = game_creation_app_asset_category_for_kind(asset_kind.as_str()); let asset = GameCreationAppAssetManifestEntry { id: asset_id.clone(), - kind: GameCreationAppAssetKind::parse_with_context(&asset_kind, "resource-edit.derivative"), + kind: asset_kind, media_type: staged_media_type.to_string(), local_path: relative_path.clone(), image_sequence_frames, @@ -5174,7 +5222,13 @@ pub(crate) async fn resume_local_project_resource_edit_at( .source_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))); + .or_else(|| { + Some( + infer_resource_edit_source_asset_kind(&ledger.edit_kind) + .as_str() + .to_string(), + ) + }); derive_local_project_resource_at(DeriveLocalProjectResourceInput { project_path: input.project_path, expected_project_id: input.expected_project_id, @@ -5273,7 +5327,7 @@ pub(crate) async fn derive_local_project_resource_at( source_asset_id: source.source_asset.as_ref().map(|asset| asset.id.clone()), source_path: source.source_path.clone(), source_media_type: Some(source.media_type.clone()), - source_asset_kind: Some(source.asset_kind.clone()), + source_asset_kind: Some(source.asset_kind.as_str().to_string()), producer_task_id: input.producer_task_id.clone().or_else(|| { source .source_asset @@ -5990,7 +6044,7 @@ mod tests { source_asset_id: source.source_asset.as_ref().map(|asset| asset.id.clone()), source_path: source.source_path.clone(), source_media_type: Some(source.media_type.clone()), - source_asset_kind: Some(source.asset_kind.clone()), + source_asset_kind: Some(source.asset_kind.as_str().to_string()), producer_task_id: input.producer_task_id.clone().or_else(|| { source .source_asset @@ -6255,7 +6309,7 @@ mod tests { canonical_resource_id: "local-asset:video-1".to_string(), source_path: Some("assets/source-video.mp4".to_string()), media_type: "video/mp4".to_string(), - asset_kind: "video".to_string(), + asset_kind: ResourceEditSourceKind::Manifest(GameCreationAppAssetKind::Video), source_sha256: "a".repeat(64), bytes: Some(vec![1, 2, 3]), generation_mode: LocalProjectResourceGenerationMode::Derive, @@ -6476,7 +6530,7 @@ mod tests { &uploaded.id, &source.source_sha256, &source.media_type, - &source.asset_kind, + source.asset_kind.as_str(), ) .expect("owner A source identity"); let (a_resource_id, a_width, a_height) = @@ -6716,7 +6770,7 @@ mod tests { &source_asset.id, &source.source_sha256, &source.media_type, - &source.asset_kind, + source.asset_kind.as_str(), ) .expect("concurrent source identity"); let binding_key = external_editor_resource_binding_key_sha256( @@ -6804,7 +6858,7 @@ mod tests { &source_asset.id, &source.source_sha256, &source.media_type, - &source.asset_kind, + source.asset_kind.as_str(), ) .expect("character source identity"); write_external_editor_resource_binding_at( @@ -7063,7 +7117,7 @@ mod tests { canonical_resource_id: request.source_resource_id.clone(), source_path: Some("assets/accepted-switch-video.mp4".to_string()), media_type: "video/mp4".to_string(), - asset_kind: "video".to_string(), + asset_kind: ResourceEditSourceKind::Manifest(GameCreationAppAssetKind::Video), source_sha256: "a".repeat(64), bytes: Some(b"\0\0\0\x18ftypisom".to_vec()), generation_mode: request.generation_mode, @@ -7121,7 +7175,7 @@ mod tests { canonical_resource_id: request.source_resource_id.clone(), source_path: Some("assets/video-one.mp4".to_string()), media_type: "video/mp4".to_string(), - asset_kind: "video".to_string(), + asset_kind: ResourceEditSourceKind::Manifest(GameCreationAppAssetKind::Video), source_sha256: "a".repeat(64), bytes: Some(b"\0\0\0\x18ftypisom".to_vec()), generation_mode: LocalProjectResourceGenerationMode::Derive, @@ -7261,7 +7315,7 @@ mod tests { canonical_resource_id: request.source_resource_id.clone(), source_path: Some("assets/legacy-video.mp4".to_string()), media_type: "video/mp4".to_string(), - asset_kind: "video".to_string(), + asset_kind: ResourceEditSourceKind::Manifest(GameCreationAppAssetKind::Video), source_sha256: "a".repeat(64), bytes: Some(b"\0\0\0\x18ftypisom".to_vec()), generation_mode: LocalProjectResourceGenerationMode::Derive, @@ -7434,7 +7488,7 @@ mod tests { canonical_resource_id: request.source_resource_id.clone(), source_path: Some("assets/identity-switch.mp4".to_string()), media_type: "video/mp4".to_string(), - asset_kind: "video".to_string(), + asset_kind: ResourceEditSourceKind::Manifest(GameCreationAppAssetKind::Video), source_sha256: "a".repeat(64), bytes: None, generation_mode: request.generation_mode, @@ -7496,7 +7550,7 @@ mod tests { canonical_resource_id: request.source_resource_id.clone(), source_path: Some("assets/resource-identity-owner.mp4".to_string()), media_type: "video/mp4".to_string(), - asset_kind: "video".to_string(), + asset_kind: ResourceEditSourceKind::Manifest(GameCreationAppAssetKind::Video), source_sha256: "a".repeat(64), bytes: None, generation_mode: request.generation_mode, @@ -7601,7 +7655,7 @@ mod tests { canonical_resource_id: request.source_resource_id.clone(), source_path: Some("assets/resource-owner-lease.mp4".to_string()), media_type: "video/mp4".to_string(), - asset_kind: "video".to_string(), + asset_kind: ResourceEditSourceKind::Manifest(GameCreationAppAssetKind::Video), source_sha256: "a".repeat(64), bytes: None, generation_mode: request.generation_mode, @@ -7798,7 +7852,7 @@ mod tests { canonical_resource_id: "local-asset:video-submission-status".to_string(), source_path: Some("assets/video-submission-status.mp4".to_string()), media_type: "video/mp4".to_string(), - asset_kind: "video".to_string(), + asset_kind: ResourceEditSourceKind::Manifest(GameCreationAppAssetKind::Video), source_sha256: "a".repeat(64), bytes: Some(b"\0\0\0\x18ftypisom".to_vec()), generation_mode: LocalProjectResourceGenerationMode::Derive, @@ -7925,7 +7979,7 @@ mod tests { canonical_resource_id: "local-asset:video-accepted-response".to_string(), source_path: Some("assets/video-accepted-response.mp4".to_string()), media_type: "video/mp4".to_string(), - asset_kind: "video".to_string(), + asset_kind: ResourceEditSourceKind::Manifest(GameCreationAppAssetKind::Video), source_sha256: "a".repeat(64), bytes: Some(b"\0\0\0\x18ftypisom".to_vec()), generation_mode: LocalProjectResourceGenerationMode::Derive, @@ -8091,7 +8145,7 @@ mod tests { canonical_resource_id: request.source_resource_id.clone(), source_path: Some("assets/video-terminal-failure.mp4".to_string()), media_type: "video/mp4".to_string(), - asset_kind: "video".to_string(), + asset_kind: ResourceEditSourceKind::Manifest(GameCreationAppAssetKind::Video), source_sha256: "a".repeat(64), bytes: Some(b"\0\0\0\x18ftypisom".to_vec()), generation_mode: LocalProjectResourceGenerationMode::Derive, @@ -8243,7 +8297,7 @@ mod tests { canonical_resource_id: request.source_resource_id.clone(), source_path: Some("assets/archive-owner-video.mp4".to_string()), media_type: "video/mp4".to_string(), - asset_kind: "video".to_string(), + asset_kind: ResourceEditSourceKind::Manifest(GameCreationAppAssetKind::Video), source_sha256: "a".repeat(64), bytes: Some(b"\0\0\0\x18ftypisom".to_vec()), generation_mode: request.generation_mode, @@ -8302,7 +8356,7 @@ mod tests { canonical_resource_id, source_path: Some("assets/pending-owner.mp4".to_string()), media_type: "video/mp4".to_string(), - asset_kind: "video".to_string(), + asset_kind: ResourceEditSourceKind::Manifest(GameCreationAppAssetKind::Video), source_sha256: "a".repeat(64), bytes: None, generation_mode: LocalProjectResourceGenerationMode::Derive, @@ -8358,7 +8412,7 @@ mod tests { canonical_resource_id: local_request.source_resource_id.clone(), source_path: Some("assets/pending-local.md".to_string()), media_type: "text/markdown".to_string(), - asset_kind: "text".to_string(), + asset_kind: ResourceEditSourceKind::Manifest(GameCreationAppAssetKind::Document), source_sha256: "b".repeat(64), bytes: Some(b"local".to_vec()), generation_mode: local_request.generation_mode, @@ -8448,7 +8502,7 @@ mod tests { canonical_resource_id: "local-asset:audio-1".to_string(), source_path: Some("audio/theme.ogg".to_string()), media_type: "audio/ogg".to_string(), - asset_kind: "background-music".to_string(), + asset_kind: ResourceEditSourceKind::Manifest(GameCreationAppAssetKind::BackgroundMusic), source_sha256: "a".repeat(64), bytes: Some(vec![1]), generation_mode: LocalProjectResourceGenerationMode::Derive, @@ -8546,7 +8600,7 @@ mod tests { canonical_resource_id: "local-asset:media-1".to_string(), source_path: Some("assets/source.mp4".to_string()), media_type: "video/mp4".to_string(), - asset_kind: "video".to_string(), + asset_kind: ResourceEditSourceKind::Manifest(GameCreationAppAssetKind::Video), source_sha256: "a".repeat(64), bytes: Some(b"\0\0\0\x18ftypisom".to_vec()), generation_mode: LocalProjectResourceGenerationMode::Derive,