From 8fe439febab82a8c57b489b48cd8d5106a542cd1 Mon Sep 17 00:00:00 2001 From: Git Hooks Test Date: Sun, 23 Aug 2026 21:35:36 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8DAGC=E8=B5=84=E6=BA=90?= =?UTF-8?q?=E7=94=9F=E6=88=90=E7=9A=84=E5=BA=8F=E5=88=97=E5=B8=A7=E6=8C=81?= =?UTF-8?q?=E4=B9=85=E5=8C=96=E4=B8=8E=E7=94=BB=E5=B8=83=E5=AE=8C=E6=88=90?= =?UTF-8?q?=E4=B8=8A=E4=B8=8B=E6=96=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 本地manifest资产新增imageSequenceFrames和imageSequenceDurationMs,角色动画完成后随账本恢复并写回manifest - 新建视频、音效和背景音乐时创建或复用同名画板项目与素材库目录,并携带projectId、assetFolderId和canvasCompletion - 资源编辑请求指纹纳入generationMode,旧账本继续通过legacy指纹兼容 - 同步更新共享契约、前端资源投影、实施计划文档与定向测试 --- .../src-tauri/src/assets.rs | 2 + .../src-tauri/src/commands.rs | 2 + .../src-tauri/src/project/asset_canvas.rs | 2 + .../src/project/asset_canvas/generation.rs | 4 + .../src/project/asset_canvas_tests.rs | 6 + .../src/project/manifest/recovery_tests.rs | 2 + .../src/project/resource_dependency_graph.rs | 2 + .../src-tauri/src/project/resource_editor.rs | 214 ++++++++++++++++-- .../resourceProjectionModel.ts | 10 + ...案】AI游戏创作智能体App实施计划-2026-06-24.md | 3 + .../shared/src/contracts/gameCreationApp.ts | 8 + .../shared-contracts/src/game_creation_app.rs | 18 ++ 12 files changed, 258 insertions(+), 15 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/assets.rs b/apps/ai-game-creator-shell/src-tauri/src/assets.rs index c6df5a8a7..d15eaadc1 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/assets.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/assets.rs @@ -1638,6 +1638,8 @@ pub(crate) fn register_local_asset_entry( kind: kind.to_string(), media_type: media_type.to_string(), local_path: normalized_path.clone(), + image_sequence_frames: None, + image_sequence_duration_ms: None, source, }); Ok((id, "asset.register")) 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 048986210..d1d5f7a57 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/commands.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/commands.rs @@ -2312,6 +2312,8 @@ mod ui_editor_font_tests { kind: "font".to_string(), media_type: "font/ttf".to_string(), local_path: relative_path.to_string(), + image_sequence_frames: None, + image_sequence_duration_ms: None, source: GameCreationAppAssetSource { kind: GameCreationAppAssetSourceKind::Uploaded, canvas_project_id: None, diff --git a/apps/ai-game-creator-shell/src-tauri/src/project/asset_canvas.rs b/apps/ai-game-creator-shell/src-tauri/src/project/asset_canvas.rs index 94a07ba68..467de49c3 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/project/asset_canvas.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/project/asset_canvas.rs @@ -2762,6 +2762,8 @@ fn commit_asset_canvas_at_internal( kind: asset_kind, media_type: staged.media_type.clone(), local_path: final_relative_path.clone(), + image_sequence_frames: None, + image_sequence_duration_ms: None, source, }; manifest.assets.push(asset.clone()); diff --git a/apps/ai-game-creator-shell/src-tauri/src/project/asset_canvas/generation.rs b/apps/ai-game-creator-shell/src-tauri/src/project/asset_canvas/generation.rs index edc0b8b95..f36ceb74f 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/project/asset_canvas/generation.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/project/asset_canvas/generation.rs @@ -2958,6 +2958,8 @@ mod tests { kind: "illustration".to_string(), media_type: "image/png".to_string(), local_path: "assets/source.png".to_string(), + image_sequence_frames: None, + image_sequence_duration_ms: None, source: GameCreationAppAssetSource { kind: GameCreationAppAssetSourceKind::Uploaded, canvas_project_id: None, @@ -4108,6 +4110,8 @@ mod tests { kind: "illustration".to_string(), media_type: "image/png".to_string(), local_path: "assets/source.png".to_string(), + image_sequence_frames: None, + image_sequence_duration_ms: None, source: GameCreationAppAssetSource { kind: GameCreationAppAssetSourceKind::Canvas, canvas_project_id: None, diff --git a/apps/ai-game-creator-shell/src-tauri/src/project/asset_canvas_tests.rs b/apps/ai-game-creator-shell/src-tauri/src/project/asset_canvas_tests.rs index ab7ec8fc9..be228ae29 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/project/asset_canvas_tests.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/project/asset_canvas_tests.rs @@ -408,6 +408,8 @@ fn refine_preserves_source_and_records_non_destructive_lineage() { kind: "illustration".to_string(), media_type: "image/png".to_string(), local_path: "assets/source.png".to_string(), + image_sequence_frames: None, + image_sequence_duration_ms: None, source: GameCreationAppAssetSource { kind: GameCreationAppAssetSourceKind::Uploaded, canvas_project_id: None, @@ -488,6 +490,8 @@ fn discovers_unique_active_refine_draft_and_rejects_ambiguous_candidates() { kind: "illustration".to_string(), media_type: "image/png".to_string(), local_path: "assets/source.png".to_string(), + image_sequence_frames: None, + image_sequence_duration_ms: None, source: GameCreationAppAssetSource { kind: GameCreationAppAssetSourceKind::Uploaded, canvas_project_id: None, @@ -979,6 +983,8 @@ fn rejects_symlinked_or_hardlinked_media_at_source_staging_and_final_paths() { kind: "illustration".to_string(), media_type: "image/png".to_string(), local_path: "assets/source.png".to_string(), + image_sequence_frames: None, + image_sequence_duration_ms: None, source: GameCreationAppAssetSource { kind: GameCreationAppAssetSourceKind::Uploaded, canvas_project_id: None, diff --git a/apps/ai-game-creator-shell/src-tauri/src/project/manifest/recovery_tests.rs b/apps/ai-game-creator-shell/src-tauri/src/project/manifest/recovery_tests.rs index e72d29970..65d8a1737 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/project/manifest/recovery_tests.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/project/manifest/recovery_tests.rs @@ -183,6 +183,8 @@ fn successful_first_playable_registration_creates_one_initial_version_with_asset kind: "character".to_string(), media_type: "image/png".to_string(), local_path: "assets/player.png".to_string(), + image_sequence_frames: None, + image_sequence_duration_ms: None, source: GameCreationAppAssetSource { kind: GameCreationAppAssetSourceKind::Generated, canvas_project_id: None, diff --git a/apps/ai-game-creator-shell/src-tauri/src/project/resource_dependency_graph.rs b/apps/ai-game-creator-shell/src-tauri/src/project/resource_dependency_graph.rs index 5a4a36fb1..82ec4561c 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/project/resource_dependency_graph.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/project/resource_dependency_graph.rs @@ -633,6 +633,8 @@ mod tests { kind: "test".to_string(), media_type: "image/png".to_string(), local_path: format!("assets/{id}.png"), + image_sequence_frames: None, + image_sequence_duration_ms: None, source: GameCreationAppAssetSource { kind: GameCreationAppAssetSourceKind::Canvas, canvas_project_id: None, 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 c255e681e..6b5fe501c 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 @@ -362,6 +362,10 @@ struct ResourceEditLedger { remote_object_key: Option, #[serde(default)] remote_legacy_public_path: Option, + #[serde(default)] + remote_sequence_frames_json: Option, + #[serde(default)] + remote_sequence_duration_ms: Option, remote_asset_object_id: Option, remote_model: Option, #[serde(default)] @@ -802,6 +806,7 @@ fn resource_edit_request_fingerprint( "projectId": input.expected_project_id, "operationId": input.operation_id, "editKind": input.edit_kind, + "generationMode": input.generation_mode, "sourceResourceId": source.canonical_resource_id, "sourcePath": source.source_path, "sourceSha256": source.source_sha256, @@ -1928,6 +1933,7 @@ fn resource_edit_remote_request( prompt: &str, asset_name: &str, source_reference: Option<&str>, + canvas_context: Option<&ExternalCanvasGenerationContext>, ) -> Result<(&'static str, serde_json::Value), String> { let generation_inputs = serde_json::json!({ "source": RESOURCE_EDIT_QUEUE_SOURCE, @@ -1979,27 +1985,57 @@ fn resource_edit_remote_request( source_reference.ok_or_else(|| "视频编辑缺少稳定源引用".to_string())? ]); } + if let Some(context) = canvas_context { + body["projectId"] = serde_json::json!(context.project_id); + body["assetFolderId"] = serde_json::json!(context.asset_folder_id); + body["canvasCompletion"] = serde_json::json!({ + "title": asset_name, + "placeholder": external_canvas_placeholder("16:9"), + }); + } Ok(("/api/external/v1/editor/videos/generations", body)) } - LocalProjectResourceEditKind::SoundEffect => Ok(( - "/api/external/v1/editor/audios/sound-effects/generations", - serde_json::json!({ + LocalProjectResourceEditKind::SoundEffect => { + let mut body = serde_json::json!({ "prompt": build_resource_edit_audio_prompt(source, prompt, 2_048)?, "model": "eleven_text_to_sound_v2", "loop": false, "assetLabel": asset_name, "generationInputs": generation_inputs, - }), - )), - LocalProjectResourceEditKind::BackgroundMusic => Ok(( - "/api/external/v1/editor/audios/background-music/generations", - serde_json::json!({ + }); + if let Some(context) = canvas_context { + body["projectId"] = serde_json::json!(context.project_id); + body["assetFolderId"] = serde_json::json!(context.asset_folder_id); + body["canvasCompletion"] = serde_json::json!({ + "title": asset_name, + "placeholder": external_canvas_placeholder("1:1"), + }); + } + Ok(( + "/api/external/v1/editor/audios/sound-effects/generations", + body, + )) + } + LocalProjectResourceEditKind::BackgroundMusic => { + let mut body = serde_json::json!({ "gptDescriptionPrompt": build_resource_edit_audio_prompt(source, prompt, 200)?, "makeInstrumental": true, "assetLabel": asset_name, "generationInputs": generation_inputs, - }), - )), + }); + if let Some(context) = canvas_context { + body["projectId"] = serde_json::json!(context.project_id); + body["assetFolderId"] = serde_json::json!(context.asset_folder_id); + body["canvasCompletion"] = serde_json::json!({ + "title": asset_name, + "placeholder": external_canvas_placeholder("1:1"), + }); + } + Ok(( + "/api/external/v1/editor/audios/background-music/generations", + body, + )) + } _ => Err("当前资源类型不是远端媒体派生".to_string()), } } @@ -2208,6 +2244,9 @@ struct ResourceEditRemoteIdentity { resource_id: Option, object_key: Option, legacy_public_path: Option, + sequence_frames: + Option>, + sequence_duration_ms: Option, asset_object_id: Option, model: Option, } @@ -2235,6 +2274,41 @@ fn extract_resource_edit_remote_identity( } else { None }; + let sequence_frames = if edit_kind == LocalProjectResourceEditKind::CharacterAnimation { + data.get("frames") + .and_then(serde_json::Value::as_array) + .map(|frames| { + frames + .iter() + .filter_map(|frame| { + let image_src = json_string_field(frame, "imageSrc")?; + let width = frame.get("width").and_then(serde_json::Value::as_u64)? as u32; + let height = + frame.get("height").and_then(serde_json::Value::as_u64)? as u32; + Some(shared_contracts::game_creation_app::GameCreationAppImageSequenceFrame { + image_src, + object_key: json_string_field(frame, "objectKey"), + asset_object_id: json_string_field(frame, "assetObjectId"), + width, + height, + }) + }) + .collect::>() + }) + } else { + None + }; + let sequence_duration_ms = if edit_kind == LocalProjectResourceEditKind::CharacterAnimation { + data.get("durationSeconds") + .and_then(serde_json::Value::as_u64) + .map(|seconds| seconds.saturating_mul(1_000)) + .or_else(|| { + data.get("imageSequenceDurationMs") + .and_then(serde_json::Value::as_u64) + }) + } else { + None + }; if object_key.is_none() && legacy_public_path.is_none() { return Err("资源编辑结果缺少稳定媒体引用".to_string()); } @@ -2248,6 +2322,8 @@ fn extract_resource_edit_remote_identity( resource_id, object_key, legacy_public_path, + sequence_frames, + sequence_duration_ms, asset_object_id, model, }) @@ -2574,12 +2650,27 @@ async fn prepare_remote_resource_edit( write_resource_edit_ledger(root, ledger)?; } if ledger.endpoint.is_none() || ledger.request_body_json.is_none() { + let canvas_context = + if input.generation_mode == LocalProjectResourceGenerationMode::Create { + Some( + prepare_external_canvas_generation_context( + root, + &client, + &api_base_url, + &api_key, + ) + .await?, + ) + } else { + None + }; let (endpoint, body) = resource_edit_remote_request( input, source, prompt, asset_name, source_reference.as_deref(), + canvas_context.as_ref(), )?; ledger.endpoint = Some(endpoint.to_string()); ledger.request_body_json = Some( @@ -2606,6 +2697,15 @@ async fn prepare_remote_resource_edit( ledger.remote_resource_id = identity.resource_id; ledger.remote_object_key = identity.object_key.clone(); ledger.remote_legacy_public_path = identity.legacy_public_path.clone(); + ledger.remote_sequence_frames_json = identity + .sequence_frames + .as_ref() + .map(|frames| { + serde_json::to_string(frames) + .map_err(|error| format!("序列化角色动画序列帧失败:{error}")) + }) + .transpose()?; + ledger.remote_sequence_duration_ms = identity.sequence_duration_ms; ledger.remote_asset_object_id = identity.asset_object_id; ledger.remote_model = identity.model; update_resource_edit_phase(root, ledger, ResourceEditLedgerPhase::RemoteCompleted)?; @@ -3090,6 +3190,8 @@ pub(crate) fn normalize_local_project_raster_resource_at( kind: source_subtype.to_string(), media_type: verified_media_type, local_path: source_path, + image_sequence_frames: None, + image_sequence_duration_ms: None, source: GameCreationAppAssetSource { kind: GameCreationAppAssetSourceKind::Generated, canvas_project_id: None, @@ -3301,11 +3403,24 @@ fn commit_resource_edit_asset_internal( staged_extension ); let remote = ledger.remote_object_key.is_some() || ledger.remote_legacy_public_path.is_some(); + let image_sequence_frames = ledger + .remote_sequence_frames_json + .as_deref() + .map(|json| { + serde_json::from_str::< + Vec, + >(json) + .map_err(|error| format!("解析角色动画序列帧失败:{error}")) + }) + .transpose()?; + let image_sequence_duration_ms = ledger.remote_sequence_duration_ms; let asset = GameCreationAppAssetManifestEntry { id: asset_id.clone(), kind: source.asset_kind.clone(), media_type: staged_media_type.to_string(), local_path: relative_path.clone(), + image_sequence_frames, + image_sequence_duration_ms, source: GameCreationAppAssetSource { kind: if remote { GameCreationAppAssetSourceKind::Canvas @@ -4293,6 +4408,8 @@ pub(crate) async fn derive_local_project_resource_at( remote_resource_id: None, remote_object_key: None, remote_legacy_public_path: None, + remote_sequence_frames_json: None, + remote_sequence_duration_ms: None, remote_asset_object_id: None, remote_model: None, terminal_failure_code: None, @@ -4579,6 +4696,8 @@ mod tests { remote_resource_id: None, remote_object_key: None, remote_legacy_public_path: None, + remote_sequence_frames_json: None, + remote_sequence_duration_ms: None, remote_asset_object_id: None, remote_model: None, terminal_failure_code: None, @@ -5692,7 +5811,7 @@ mod tests { ); bgm.prompt = prompt.to_string(); let (bgm_endpoint, bgm_body) = - resource_edit_remote_request(&bgm, &source, prompt, "主题音乐编辑版", None) + resource_edit_remote_request(&bgm, &source, prompt, "主题音乐编辑版", None, None) .expect("build BGM request"); assert_eq!( bgm_endpoint, @@ -5719,6 +5838,7 @@ mod tests { prompt, "视频编辑版", Some("stable-video-reference"), + None, ) .expect("build video request"); assert_eq!(video_endpoint, "/api/external/v1/editor/videos/generations"); @@ -5731,7 +5851,7 @@ mod tests { let mut sound_effect = bgm.clone(); sound_effect.edit_kind = LocalProjectResourceEditKind::SoundEffect; let (sound_effect_endpoint, _) = - resource_edit_remote_request(&sound_effect, &source, prompt, "音效编辑版", None) + resource_edit_remote_request(&sound_effect, &source, prompt, "音效编辑版", None, None) .expect("build sound effect request"); assert_eq!( sound_effect_endpoint, @@ -5746,6 +5866,7 @@ mod tests { prompt, "图片编辑版", Some("stable-image-reference"), + None, ) .expect("build image request"); assert_eq!(image_endpoint, "/api/external/v1/editor/images/edits"); @@ -5776,6 +5897,11 @@ mod tests { source_version: None, }; let prompt = "全新的森林场景"; + let canvas_context = ExternalCanvasGenerationContext { + project_id: "project-1".to_string(), + asset_folder_id: "folder-1".to_string(), + canvas_name: "测试画板".to_string(), + }; let mut create_video = input( std::path::Path::new("."), @@ -5784,14 +5910,51 @@ mod tests { "create:video".to_string(), ); create_video.generation_mode = LocalProjectResourceGenerationMode::Create; - let (create_video_endpoint, create_video_body) = - resource_edit_remote_request(&create_video, &source, prompt, "新视频", None) - .expect("build create video request"); + let (create_video_endpoint, create_video_body) = resource_edit_remote_request( + &create_video, + &source, + prompt, + "新视频", + None, + Some(&canvas_context), + ) + .expect("build create video request"); assert_eq!( create_video_endpoint, "/api/external/v1/editor/videos/generations" ); assert!(create_video_body.get("referenceVideoSrcs").is_none()); + assert_eq!( + create_video_body["projectId"], + serde_json::json!("project-1") + ); + assert_eq!( + create_video_body["assetFolderId"], + serde_json::json!("folder-1") + ); + assert!(create_video_body.get("canvasCompletion").is_some()); + + let mut create_sound_effect = create_video.clone(); + create_sound_effect.edit_kind = LocalProjectResourceEditKind::SoundEffect; + let (create_sound_effect_endpoint, create_sound_effect_body) = + resource_edit_remote_request( + &create_sound_effect, + &source, + "清脆的点击声", + "新音效", + None, + Some(&canvas_context), + ) + .expect("build create sound effect request"); + assert_eq!( + create_sound_effect_endpoint, + "/api/external/v1/editor/audios/sound-effects/generations" + ); + assert_eq!( + create_sound_effect_body["projectId"], + serde_json::json!("project-1") + ); + assert!(create_sound_effect_body.get("canvasCompletion").is_some()); let mut animation = create_video.clone(); animation.edit_kind = LocalProjectResourceEditKind::CharacterAnimation; @@ -5802,6 +5965,7 @@ mod tests { "让角色自然呼吸", "角色动画", Some("stable-image-object-key"), + None, ) .expect("build character animation request"); assert_eq!( @@ -5819,6 +5983,16 @@ mod tests { "data": { "previewVideoPath": "/generated-character-drafts/layer/animation/preview.mp4", "objectKey": "generated-character-drafts/layer/animation/frame-01.png", + "durationSeconds": 4, + "frames": [ + { + "imageSrc": "generated-character-drafts/layer/animation/frame-01.png", + "objectKey": "generated-character-drafts/layer/animation/frame-01.png", + "assetObjectId": "animation-frame-object", + "width": 720, + "height": 1280 + } + ], "resource": { "resourceId": "editor-resource-animation", "objectKey": "generated-character-drafts/layer/animation/frame-01.png" @@ -5838,6 +6012,16 @@ mod tests { identity.object_key.as_deref(), Some("generated-character-drafts/layer/animation/frame-01.png") ); + assert_eq!(identity.sequence_duration_ms, Some(4_000)); + assert_eq!(identity.sequence_frames.as_ref().map(Vec::len), Some(1)); + assert_eq!( + identity + .sequence_frames + .as_ref() + .and_then(|frames| frames.first()) + .map(|frame| frame.width), + Some(720) + ); } #[tokio::test] diff --git a/apps/ai-game-creator-shell/src/view/project-development/resourceProjectionModel.ts b/apps/ai-game-creator-shell/src/view/project-development/resourceProjectionModel.ts index 75ac11650..6151887d7 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/resourceProjectionModel.ts +++ b/apps/ai-game-creator-shell/src/view/project-development/resourceProjectionModel.ts @@ -45,6 +45,14 @@ export type ProjectResource = { dependencyDepth: number; content?: string; version?: ProjectVersionResourceSummary; + imageSequenceFrames?: Array<{ + imageSrc: string; + objectKey?: string | null; + assetObjectId?: string | null; + width: number; + height: number; + }> | null; + imageSequenceDurationMs?: number | null; }; const documentExtension = @@ -200,6 +208,8 @@ export function projectResourcesFromReadModels( referenceResourceIds: asset.source.referenceResourceIds ?? [], dependencies: [], dependencyDepth: 0, + imageSequenceFrames: asset.imageSequenceFrames ?? null, + imageSequenceDurationMs: asset.imageSequenceDurationMs ?? null, }); } diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index 480c2e7f0..e9aeb6880 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -4,8 +4,11 @@ - 统一资源编辑命令新增 `generationMode=create|derive`:`create` 用于无源生成视频、音效和背景音乐,`derive` 保持现有“基于已有资源派生”。视频新建请求不再携带 `referenceVideoSrcs`;音效 / 背景音乐新建使用同款文本生成端点,但提示词前缀改为“生成新音频”。 - 图片资源新增“生成动画”派生入口:复用 `/api/external/v1/editor/character-animations/generations`(账号模式自动映射 `/api/editor/character-animations/generations`),以源图片的稳定 objectKey、真实宽高和固定 `seedance2.0-fast / 720p / same / 32帧 / 4秒` 参数提交;完成后下载 `previewVideoPath` 预览视频并以 `character-animation` 资源类型登记为本地 MP4/WebM 媒体,避免只保存首帧。 +- 本地 manifest 资产模型新增 `imageSequenceFrames` 与 `imageSequenceDurationMs`:角色动画完成时把远端 `frames` 与 `durationSeconds` 随账本持久化,恢复或重新提交后仍写回 manifest,前端资源投影同步暴露。 - 资源页顶部新增“生成视频 / 生成音效 / 生成背景音乐”三个入口,图片聚焦态新增“生成动画”按钮;统一资源编辑面板按 `generationMode` 显示“新建媒体资源”或“编辑现有资源”。 +- 新建视频 / 音效 / 背景音乐时,先复用 `prepare_external_canvas_generation_context` 创建或匹配同名画板项目与素材库目录,并在生成请求中携带 `projectId / assetFolderId / canvasCompletion`,结果同时进入平台画布 / 素材库与本地 manifest。 - durable 账本新增 `remoteLegacyPublicPath` 字段(旧账本默认缺省),用于角色动画预览视频的换签下载与恢复;资源提交、轮询、下载、manifest 提交和恢复队列继续复用既有资源编辑事务链。 +- 资源编辑请求指纹纳入 `generationMode`,同一 `operationId` 换生成模式重试时不再按旧账本模式恢复;旧账本继续通过 legacy 指纹兼容。 - 去背景(`/api/editor/images/background-removals`)仍缺 External v1 契约,不在本次补齐范围;本次只补视频 / 角色动画 / 音效 / 背景音乐。 ## 2026-08-19 UI Editor 节点右键菜单 diff --git a/packages/shared/src/contracts/gameCreationApp.ts b/packages/shared/src/contracts/gameCreationApp.ts index bbd343775..0d42cb25f 100644 --- a/packages/shared/src/contracts/gameCreationApp.ts +++ b/packages/shared/src/contracts/gameCreationApp.ts @@ -466,6 +466,14 @@ export interface GameCreationAppAssetManifestEntry { mediaType: string; localPath: string; source: GameCreationAppAssetSource; + imageSequenceFrames?: Array<{ + imageSrc: string; + objectKey?: string | null; + assetObjectId?: string | null; + width: number; + height: number; + }> | null; + imageSequenceDurationMs?: number | null; } export const GAME_CREATION_RESOURCE_LAYOUT_SCHEMA_VERSION = 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 13e6df9b7..3fe71edad 100644 --- a/server-rs/crates/shared-contracts/src/game_creation_app.rs +++ b/server-rs/crates/shared-contracts/src/game_creation_app.rs @@ -478,6 +478,18 @@ pub struct GameCreationAppAssetSource { pub reference_resource_ids: Vec, } +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GameCreationAppImageSequenceFrame { + pub image_src: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub object_key: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub asset_object_id: Option, + pub width: u32, + pub height: u32, +} + #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct GameCreationAppAssetManifestEntry { @@ -486,6 +498,10 @@ pub struct GameCreationAppAssetManifestEntry { pub media_type: String, pub local_path: String, pub source: GameCreationAppAssetSource, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub image_sequence_frames: Option>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub image_sequence_duration_ms: Option, } pub const GAME_CREATION_RESOURCE_LAYOUT_SCHEMA_VERSION: &str = "game-creator-resource-layout.v1"; @@ -1777,6 +1793,8 @@ mod tests { kind: "character".to_string(), media_type: "image".to_string(), local_path: "assets/images/player.png".to_string(), + image_sequence_frames: None, + image_sequence_duration_ms: None, source: GameCreationAppAssetSource { kind: GameCreationAppAssetSourceKind::Canvas, canvas_project_id: Some("canvas-project-1".to_string()),