diff --git a/apps/ai-game-creator-shell/scripts/check-config.mjs b/apps/ai-game-creator-shell/scripts/check-config.mjs index b167a6f44..535579f25 100644 --- a/apps/ai-game-creator-shell/scripts/check-config.mjs +++ b/apps/ai-game-creator-shell/scripts/check-config.mjs @@ -112,7 +112,9 @@ const allowedUncalledTauriCommands = [ 'check_ui_editor_font_glyph_coverage', 'create_ui_design_resource', // 图片类生成的同步变体:GUI 已改为 `start_local_project_asset_generation` + 项目内任务账本 - // (提交即返回、后台生成),这条命令保留给 Rust 侧与既有单测,不再有前端调用方。 + // (提交即返回、后台生成)。这条命令**没有生产调用方**,只有 Rust 集成测试 + // (`src/tests/project.rs`)与 `commands.rs` 单测在调;待后续批次删除,或改为转调 + // `start_local_project_asset_generation`。 'generate_local_project_asset', 'open_game_creator_launcher_window', 'open_game_creator_workspace_window', diff --git a/apps/ai-game-creator-shell/src-tauri/src/asset_generation_tasks.rs b/apps/ai-game-creator-shell/src-tauri/src/asset_generation_tasks.rs index 1c662207f..91a9eae42 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/asset_generation_tasks.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/asset_generation_tasks.rs @@ -14,10 +14,12 @@ //! //! 账本落在**项目内** `.agent/runtime/asset-generation-tasks/tasks.json`(复用既有 agent runtime //! sidecar 读写原语:临时文件 + rename 替换),所以重开项目后仍能看到历史任务。进程重启时 -//! 还在 `queued` / `running` 的记录不可能再有人推进,读账本时按「上次运行中断」收口为失败, -//! 不假装它还在跑。 +//! 还在 `queued` / `running` 的记录不可能再有人推进,读账本时按「上次运行中断」收口;收口结论 +//! **要跟 manifest 交叉核对**:能在清单里找到这次请求的目标素材就按已完成收口(生成通道是先写 +//! manifest 再返回的,所以「素材已登记、账本还停在 running」的窗口里崩溃是真会发生的),核不了 +//! 就不把话说死,不假装它还在跑、也不谎报「生成未完成」。 -use std::collections::BTreeSet; +use std::collections::{BTreeMap, BTreeSet}; use std::path::{Path, PathBuf}; use std::sync::{Mutex, OnceLock}; use std::time::{SystemTime, UNIX_EPOCH}; @@ -27,7 +29,7 @@ use crate::agent::{ write_agent_runtime_json_sidecar_with_max_bytes, PlatformArtAssetGenerationOptions, }; use crate::commands::prepare_local_project_asset_generation; -use crate::project::enforce_project_permission_policy; +use crate::project::{enforce_project_permission_policy, read_existing_manifest_for_project}; pub(crate) const ASSET_GENERATION_TASK_SCHEMA_VERSION: &str = "agc-asset-generation-task.v1"; pub(crate) const ASSET_GENERATION_TASK_LEDGER_RELATIVE_PATH: &str = @@ -45,8 +47,21 @@ pub(crate) const ASSET_GENERATION_TASK_STATUS_FAILED: &str = "failed"; const ASSET_GENERATION_TASK_PHASE_QUEUED: &str = "排队中。"; const ASSET_GENERATION_TASK_PHASE_RUNNING: &str = "正在生成。"; const ASSET_GENERATION_TASK_PHASE_COMPLETED: &str = "生成已完成。"; -const ASSET_GENERATION_TASK_PHASE_INTERRUPTED: &str = "上次运行中断,生成未完成。"; -const ASSET_GENERATION_TASK_INTERRUPTED_ERROR: &str = "应用退出时生成任务仍在进行"; +/// 中断收口:清单里已经能查到这次请求的目标素材 → 按事实收口为已完成。 +const ASSET_GENERATION_TASK_PHASE_INTERRUPTED_SETTLED: &str = + "上次运行中断,但目标素材已登记(已完成)。"; +/// 中断收口:请求指定了精确落点,而清单里没有该素材 → 这次写入确实没落地。 +const ASSET_GENERATION_TASK_PHASE_INTERRUPTED_INCOMPLETE: &str = + "上次运行中断,目标素材未登记,生成未完成。"; +/// 中断收口:没有可核对的目标标识(或清单读不到)→ 不下结论。 +const ASSET_GENERATION_TASK_PHASE_INTERRUPTED_UNKNOWN: &str = + "上次运行中断,状态未知(可能已完成)。"; +const ASSET_GENERATION_TASK_INTERRUPTED_SETTLED_ERROR: &str = + "应用退出时生成任务仍在进行,已按清单确认目标素材登记"; +const ASSET_GENERATION_TASK_INTERRUPTED_INCOMPLETE_ERROR: &str = + "应用退出时生成任务仍在进行,目标素材未登记"; +const ASSET_GENERATION_TASK_INTERRUPTED_UNKNOWN_ERROR: &str = + "应用退出时生成任务仍在进行,未能在清单里确认结果"; /// 一条生成任务的权威记录。字段名与前端一一对应(camelCase)。 #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] @@ -59,6 +74,13 @@ pub(crate) struct AssetGenerationTaskRecord { pub(crate) status: String, /// 阶段文案:**由后端拥有**,前端只渲染。 pub(crate) phase_detail: String, + /// 这次请求指定的精确落点(`outputPath`)。 + /// + /// 它是中断收口时**唯一可核对的目标标识**:有它就能拿 manifest 的 `localPath` 做精确匹配, + /// 判出「素材已登记 → 已完成」还是「落点没有素材 → 未落地」。没有它(绝大多数入口不指定 + /// 落点)就只能报「状态未知」。旧账本没有这个字段,按 `None` 读。 + #[serde(default)] + pub(crate) output_path: Option, pub(crate) created_at_millis: u64, pub(crate) started_at_millis: Option, pub(crate) finished_at_millis: Option, @@ -145,11 +167,45 @@ fn trim_ledger(tasks: &mut Vec) { tasks.drain(0..overflow); } -/// 进程重启后把没人推进的记录收口为失败,并把这次修复写回账本。 +/// manifest 的 `localPath` → 资产 id。 +/// +/// 只用来做**精确落点**的交叉核对,所以按同一个口径归一化两侧的路径串(去空白、反斜杠折成 +/// 正斜杠);不做模糊匹配、不按素材名猜,避免把另一次生成的产物算到这条任务上。 +fn registered_asset_ids_by_local_path(root: &Path) -> Option> { + let manifest = read_existing_manifest_for_project(root).ok()?; + Some( + manifest + .assets + .iter() + .map(|asset| (normalize_local_path(&asset.local_path), asset.id.clone())) + .collect(), + ) +} + +fn normalize_local_path(path: &str) -> String { + path.trim().replace('\\', "/") +} + +/// 中断收口用到的 manifest 交叉核对口径。 +/// +/// `None` = 这次读不到清单(项目还没初始化 / 读盘失败),此时任何记录都不能下结论。 +type RegisteredAssetIdsByLocalPath = Option>; + +/// 进程重启后把没人推进的记录收口,并把这次修复写回账本。 /// /// 判据是「本进程的 live 集合里没有它」:本进程派发的任务在 `start` 里先登记 live 再落账本, /// 所以账本里非终态且不 live 的记录只可能来自上一次运行。 -fn repair_interrupted_tasks(tasks: &mut [AssetGenerationTaskRecord]) -> bool { +/// +/// **收口结论要跟清单核对**:生成通道是先写 manifest 再返回的,所以「素材已经登记、账本还停在 +/// `running`」的窗口里崩溃是真会发生的;只看 live 集合会把这种任务谎报成「生成未完成」。三种结论: +/// +/// - 请求指定了精确落点、且清单里已有该落点 → 按事实收口为**已完成**(带上 assetId); +/// - 请求指定了精确落点、清单里没有 → 这次写入确实没落地,收口为失败并说明「目标素材未登记」; +/// - 没有精确落点(或清单读不到)→ 收口为失败但**不下结论**,文案是「状态未知(可能已完成)」。 +fn repair_interrupted_tasks( + tasks: &mut [AssetGenerationTaskRecord], + registered: &RegisteredAssetIdsByLocalPath, +) -> bool { let live = live_task_ids() .lock() .map(|ids| ids.clone()) @@ -159,22 +215,44 @@ fn repair_interrupted_tasks(tasks: &mut [AssetGenerationTaskRecord]) -> bool { if task.is_terminal() || live.contains(&task.task_id) { continue; } - task.status = ASSET_GENERATION_TASK_STATUS_FAILED.to_string(); - task.phase_detail = ASSET_GENERATION_TASK_PHASE_INTERRUPTED.to_string(); - task.error = Some(ASSET_GENERATION_TASK_INTERRUPTED_ERROR.to_string()); + let registered_asset = task + .output_path + .as_deref() + .map(normalize_local_path) + .filter(|path| !path.is_empty()) + .and_then(|path| registered.as_ref().map(|assets| assets.get(&path).cloned())); + match registered_asset { + Some(Some(asset_id)) => { + task.status = ASSET_GENERATION_TASK_STATUS_COMPLETED.to_string(); + task.phase_detail = ASSET_GENERATION_TASK_PHASE_INTERRUPTED_SETTLED.to_string(); + task.asset_id = Some(asset_id); + task.error = Some(ASSET_GENERATION_TASK_INTERRUPTED_SETTLED_ERROR.to_string()); + } + Some(None) => { + task.status = ASSET_GENERATION_TASK_STATUS_FAILED.to_string(); + task.phase_detail = ASSET_GENERATION_TASK_PHASE_INTERRUPTED_INCOMPLETE.to_string(); + task.error = Some(ASSET_GENERATION_TASK_INTERRUPTED_INCOMPLETE_ERROR.to_string()); + } + None => { + task.status = ASSET_GENERATION_TASK_STATUS_FAILED.to_string(); + task.phase_detail = ASSET_GENERATION_TASK_PHASE_INTERRUPTED_UNKNOWN.to_string(); + task.error = Some(ASSET_GENERATION_TASK_INTERRUPTED_UNKNOWN_ERROR.to_string()); + } + } task.finished_at_millis = Some(now_millis()); repaired = true; } repaired } -/// 读账本:顺带把「上次运行中断」的任务收口为失败。 +/// 读账本:顺带把「上次运行中断」的任务收口(能核对的按事实收口,核不了的只报状态未知)。 pub(crate) fn list_local_project_asset_generation_tasks( root: &Path, ) -> Result, String> { let _guard = lock_ledger()?; let mut tasks = read_ledger(root)?; - if repair_interrupted_tasks(&mut tasks) { + let registered = registered_asset_ids_by_local_path(root); + if repair_interrupted_tasks(&mut tasks, ®istered) { write_ledger(root, &tasks)?; } Ok(tasks) @@ -221,6 +299,7 @@ pub(crate) fn begin_local_project_asset_generation_task( task_id: &str, task_kind: &str, asset_name: &str, + output_path: Option<&str>, ) -> Result { let record = AssetGenerationTaskRecord { task_id: task_id.to_string(), @@ -229,6 +308,7 @@ pub(crate) fn begin_local_project_asset_generation_task( asset_name: asset_name.to_string(), status: ASSET_GENERATION_TASK_STATUS_QUEUED.to_string(), phase_detail: ASSET_GENERATION_TASK_PHASE_QUEUED.to_string(), + output_path: output_path.map(str::to_string), created_at_millis: now_millis(), started_at_millis: None, finished_at_millis: None, @@ -331,6 +411,7 @@ pub(crate) async fn start_local_project_asset_generation( &task_id, &asset_kind, &asset_label, + request.options.output_path.as_deref(), )?; // 先登记 live 再派发:`list` 只把「非终态且不 live」的记录判为上次运行的残留。 if let Ok(mut ids) = live_task_ids().lock() { @@ -356,13 +437,22 @@ pub(crate) fn list_local_project_asset_generations( return Err("项目路径不能为空".to_string()); } let root = Path::new(project_path); - enforce_project_permission_policy(root, "asset.read")?; + // 与相邻的 manifest 读命令同口径:合法的读 command id 是 `asset.list`(`asset.read` 不在 + // 契约的 command 列表里,写进 `denied_commands` 也不可能命中 → 门禁恒不生效)。 + enforce_project_permission_policy(root, "asset.list")?; list_local_project_asset_generation_tasks(root) } #[cfg(test)] mod asset_generation_task_tests { use super::*; + use crate::assets::register_local_asset_at; + use crate::project::{ + init_local_game_project_at, read_manifest_for_project, write_project_permission_policy_at, + }; + use shared_contracts::game_creation_app::{ + GameCreationAppAssetSource, GameCreationAppAssetSourceKind, + }; fn temp_project_root(label: &str) -> PathBuf { let nonce = SystemTime::now() @@ -377,9 +467,76 @@ mod asset_generation_task_tests { root } + /// 已初始化的项目:中断收口要读 manifest 做交叉核对,所以这几条用例必须有真清单。 + fn initialized_project_root(label: &str) -> PathBuf { + let root = temp_project_root(label); + init_local_game_project_at(&root, "project-1", "生成任务账本测试").expect("init project"); + root + } + + /// 登记一个素材到 manifest 并返回它的资产 id(中断收口按 `localPath` 精确匹配)。 + fn register_fixture_asset(root: &Path, local_path: &str) -> String { + let absolute_path = root.join(local_path); + if let Some(parent) = absolute_path.parent() { + std::fs::create_dir_all(parent).expect("create fixture asset parent"); + } + std::fs::write(&absolute_path, b"png-bytes").expect("write fixture asset"); + register_local_asset_at( + root, + local_path, + "icon-spec", + "image/png", + "generated", + GameCreationAppAssetSource { + kind: GameCreationAppAssetSourceKind::Generated, + canvas_project_id: None, + resource_id: None, + asset_object_id: None, + task_id: Some("fixture-generation".to_string()), + prompt: None, + model: None, + generation_route: None, + generation_kind: None, + reference_resource_ids: Vec::new(), + }, + ) + .expect("register fixture asset"); + read_manifest_for_project(root) + .expect("read manifest after registration") + .assets + .iter() + .find(|asset| asset.local_path == local_path) + .expect("registered asset is present") + .id + .clone() + } + fn begin(root: &Path, task_id: &str) -> AssetGenerationTaskRecord { - begin_local_project_asset_generation_task(root, "project-1", task_id, "image", "AI 图") - .expect("begin task") + begin_local_project_asset_generation_task( + root, + "project-1", + task_id, + "image", + "AI 图", + None, + ) + .expect("begin task") + } + + fn begin_with_output( + root: &Path, + task_id: &str, + output_path: &str, + ) -> AssetGenerationTaskRecord { + begin_local_project_asset_generation_task( + root, + "project-1", + task_id, + "icon-spec", + "图标规范", + Some(output_path), + ) + .expect("begin task with output path") } #[test] @@ -412,19 +569,20 @@ mod asset_generation_task_tests { } #[test] - fn restarting_the_process_settles_unfinished_tasks_as_interrupted() { - let root = temp_project_root("interrupted"); + fn restarting_the_process_reports_unknown_state_when_the_result_cannot_be_checked() { + // 项目没初始化 → 读不到 manifest → 没有可核对的目标标识:不许断言「生成未完成」。 + let root = temp_project_root("interrupted-unknown"); begin(&root, "task-interrupted"); // 模拟「上一条进程留下的非终态记录」:live 集合里没有它(本测试进程从未 start 过它)。 let listed = list_local_project_asset_generation_tasks(&root).expect("list"); assert_eq!(listed[0].status, ASSET_GENERATION_TASK_STATUS_FAILED); assert_eq!( listed[0].phase_detail, - ASSET_GENERATION_TASK_PHASE_INTERRUPTED + ASSET_GENERATION_TASK_PHASE_INTERRUPTED_UNKNOWN ); assert_eq!( listed[0].error.as_deref(), - Some(ASSET_GENERATION_TASK_INTERRUPTED_ERROR) + Some(ASSET_GENERATION_TASK_INTERRUPTED_UNKNOWN_ERROR) ); assert!(listed[0].finished_at_millis.is_some()); // 修复要写回账本,第二次读到的仍是同一条终态记录。 @@ -433,6 +591,76 @@ mod asset_generation_task_tests { std::fs::remove_dir_all(&root).ok(); } + #[test] + fn interrupted_task_whose_output_is_already_registered_settles_as_completed() { + // 生成通道先写 manifest 再返回,所以「素材已登记、账本还停在 running」的窗口里崩溃是 + // 真会发生的:这种任务必须按事实收口为已完成,而不是谎报「生成未完成」。 + let root = initialized_project_root("interrupted-settled"); + let asset_id = register_fixture_asset(&root, "assets/art-spec.png"); + begin_with_output(&root, "task-settled", "assets/art-spec.png"); + + let listed = list_local_project_asset_generation_tasks(&root).expect("list"); + assert_eq!(listed[0].status, ASSET_GENERATION_TASK_STATUS_COMPLETED); + assert_eq!( + listed[0].phase_detail, + ASSET_GENERATION_TASK_PHASE_INTERRUPTED_SETTLED + ); + assert_eq!(listed[0].asset_id.as_deref(), Some(asset_id.as_str())); + assert!(listed[0].finished_at_millis.is_some()); + std::fs::remove_dir_all(&root).ok(); + } + + #[test] + fn interrupted_task_whose_output_was_never_registered_is_reported_as_incomplete() { + let root = initialized_project_root("interrupted-incomplete"); + begin_with_output(&root, "task-incomplete", "assets/never-written.png"); + + let listed = list_local_project_asset_generation_tasks(&root).expect("list"); + assert_eq!(listed[0].status, ASSET_GENERATION_TASK_STATUS_FAILED); + assert_eq!( + listed[0].phase_detail, + ASSET_GENERATION_TASK_PHASE_INTERRUPTED_INCOMPLETE + ); + assert_eq!( + listed[0].error.as_deref(), + Some(ASSET_GENERATION_TASK_INTERRUPTED_INCOMPLETE_ERROR) + ); + assert!(listed[0].asset_id.is_none()); + std::fs::remove_dir_all(&root).ok(); + } + + #[test] + fn interrupted_task_without_an_output_slot_never_claims_the_generation_failed() { + // 自动落点(绝大多数入口):清单里无法精确定位这次请求的产物 → 只报状态未知。 + let root = initialized_project_root("interrupted-no-output"); + register_fixture_asset(&root, "assets/art-spec.png"); + begin(&root, "task-no-output"); + + let listed = list_local_project_asset_generation_tasks(&root).expect("list"); + assert_eq!(listed[0].status, ASSET_GENERATION_TASK_STATUS_FAILED); + assert_eq!( + listed[0].phase_detail, + ASSET_GENERATION_TASK_PHASE_INTERRUPTED_UNKNOWN + ); + assert!(listed[0].asset_id.is_none()); + std::fs::remove_dir_all(&root).ok(); + } + + #[test] + fn permission_policy_can_deny_the_generation_task_ledger_read() { + // 读门禁必须用契约里真实存在的 command id(`asset.list`)。写成 `asset.read` 这种不在 + // 命令表里的名字时,`denied_commands` 永远不可能命中 → 门禁恒不生效。 + let root = initialized_project_root("denied-read"); + let mut policy = crate::ProjectPermissionPolicy::default(); + policy.denied_commands.push("asset.list".to_string()); + write_project_permission_policy_at(&root, policy).expect("write permission policy"); + + let error = list_local_project_asset_generations(root.to_string_lossy().into_owned()) + .expect_err("denied ledger read must fail closed"); + assert_eq!(error, "项目权限策略拒绝执行:asset.list"); + std::fs::remove_dir_all(&root).ok(); + } + #[test] fn live_tasks_are_not_settled_while_they_are_still_running() { let root = temp_project_root("live"); @@ -446,6 +674,10 @@ mod asset_generation_task_tests { remove_live_task_id("task-live"); let settled = list_local_project_asset_generation_tasks(&root).expect("list settled"); assert_eq!(settled[0].status, ASSET_GENERATION_TASK_STATUS_FAILED); + assert_eq!( + settled[0].phase_detail, + ASSET_GENERATION_TASK_PHASE_INTERRUPTED_UNKNOWN + ); std::fs::remove_dir_all(&root).ok(); } @@ -477,6 +709,7 @@ mod asset_generation_task_tests { "task-dup", "image", "AI 图", + None, ) .expect_err("duplicate in-flight task"); assert_eq!(error, "生成任务 id 已在进行中:task-dup"); diff --git a/apps/ai-game-creator-shell/src/features/resource-canvas/resourceCanvasAssetGenerationQueue.ts b/apps/ai-game-creator-shell/src/features/resource-canvas/resourceCanvasAssetGenerationQueue.ts index 594e498ea..c2c17179a 100644 --- a/apps/ai-game-creator-shell/src/features/resource-canvas/resourceCanvasAssetGenerationQueue.ts +++ b/apps/ai-game-creator-shell/src/features/resource-canvas/resourceCanvasAssetGenerationQueue.ts @@ -66,8 +66,10 @@ export type ResourceCanvasAssetGenerationQueue = { * 入队并推进。 * * resolve = 该任务已终态(成功);reject = 失败(含后端拒绝入参)。面板据此保留草稿可重试。 - * 排在别的在途任务后面的提交会先在本地队列里等,**不发 IPC**(远端图片类生成共用 - * single-flight 输出槽,并发提交会在远端 POST 之前被拒)。 + * 排在别的在途任务后面的提交会先在本地队列里等,**不发 IPC**——这是本批的**提交节流**: + * AGC 本地 durable 输出槽已按精确动作指纹分槽,不同 prompt / 素材名可以同时在途,所以排队 + * 不再是「远端会拒绝并发」的被迫行为;真并行派发需要并发收口设计(配对读 + manifest CAS + + * 聚焦意图互不覆盖),留待下一批。 */ submit(task: ResourceCanvasAssetGenerationTask): Promise; /** 推进已有队列(宿主挂载 / 重开项目恢复任务时调一次)。 */ @@ -92,8 +94,8 @@ type SettlementWaiter = { /** * 本地排队 + 后端任务账本的驱动器。 * - * 顺序上只有一条规则:**下一条必须等上一条终态**;上一条结束(成功或失败)后由同一个循环 - * 自动补发。派发之后不再由前端猜进度:状态与阶段文案一律来自 + * 顺序上只有一条规则:**下一条必须等上一条终态**(本批的提交节流,见 `submit`);上一条结束 + * (成功或失败)后由同一个循环自动补发。派发之后不再由前端猜进度:状态与阶段文案一律来自 * `list_local_project_asset_generations` 返回的后端记录。 */ export function createResourceCanvasAssetGenerationQueue( diff --git a/apps/ai-game-creator-shell/src/features/resource-canvas/resourceCanvasAssetGenerationTaskModel.ts b/apps/ai-game-creator-shell/src/features/resource-canvas/resourceCanvasAssetGenerationTaskModel.ts index 19a8c5afe..58a7000ab 100644 --- a/apps/ai-game-creator-shell/src/features/resource-canvas/resourceCanvasAssetGenerationTaskModel.ts +++ b/apps/ai-game-creator-shell/src/features/resource-canvas/resourceCanvasAssetGenerationTaskModel.ts @@ -253,13 +253,14 @@ export function mergeLocalProjectAssetGenerationRecord( } /** - * 本地排队的派发判据:**同一时刻只允许一条任务在途**。 + * 本地排队的派发判据:**同一时刻只派发一条任务**(本批的提交节流)。 * - * 远端图片类生成共用 single-flight 输出槽,第二条并发请求会在远端 POST 之前被拒;所以第二条 - * 必须停在本地队列里(`dispatched === false`),等前一条终态后再补发。 + * AGC 本地 durable 输出槽已按**精确动作指纹**分槽,不同 prompt / 素材名可以同时在途,所以这条 + * 判据**不再是「远端会拒绝并发」的被迫排队**;本批前端仍按「同一时刻只派发一条」排队,作为提交 + * 节流。真并行派发需要并发收口设计(配对读 + manifest CAS + 聚焦意图互不覆盖),留待下一批。 * * 「在途」的判据是 `dispatched && 未终态`,而不是 `status === 'running'`:已派发但后端记录还 - * 没读回来的那一段(状态仍是 `queued`)同样占着输出槽,漏掉这一档就会并发发出第二条。 + * 没读回来的那一段(状态仍是 `queued`)同样算在途,漏掉这一档就会提前放出第二条。 */ export function nextResourceCanvasAssetGenerationDispatch( tasks: readonly ResourceCanvasAssetGenerationTask[], diff --git a/apps/ai-game-creator-shell/src/features/resource-canvas/resourceCanvasBottomToolbarModel.ts b/apps/ai-game-creator-shell/src/features/resource-canvas/resourceCanvasBottomToolbarModel.ts index 43fe029f7..f0fd3e9c7 100644 --- a/apps/ai-game-creator-shell/src/features/resource-canvas/resourceCanvasBottomToolbarModel.ts +++ b/apps/ai-game-creator-shell/src/features/resource-canvas/resourceCanvasBottomToolbarModel.ts @@ -21,7 +21,7 @@ import type { ResourceCanvasGenerationKind } from './resourceCanvasGenerationMod */ /** - * `generate_local_project_asset` 放行的无源生成类型。 + * `start_local_project_asset_generation` 放行的无源生成类型。 * * 与 Rust `PLATFORM_ART_ASSET_GENERATION_KINDS` 一一对应(`publication-material` 属宣发 * 素材,本轮不做,因此不在这里)。`spec` / `icon-spec` 都会在 Rust 侧收口到 `icon-spec`, @@ -72,7 +72,7 @@ type ResourceCanvasBottomToolActionBase = { imageSize: string; }; -/** 图片类生成入口:走 `generate_local_project_asset`。 */ +/** 图片类生成入口:走 `start_local_project_asset_generation`(提交即返回、后台生成)。 */ export type ResourceCanvasAssetToolAction = ResourceCanvasBottomToolActionBase & { route: 'asset';