diff --git a/apps/ai-game-creator-shell/src-tauri/src/commands/desktop.rs b/apps/ai-game-creator-shell/src-tauri/src/commands/desktop.rs index 22f2a3a5d..634f3b7f3 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/commands/desktop.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/commands/desktop.rs @@ -562,7 +562,9 @@ pub(crate) async fn derive_local_project_resource( ) -> Result { let root = Path::new(input.project_path.trim()); enforce_project_permission_policy(root, "asset.register")?; - derive_local_project_resource_at(input).await + derive_local_project_resource_at(input) + .await + .map_err(resource_edit_command_error) } #[tauri::command] @@ -580,7 +582,15 @@ pub(crate) async fn resume_local_project_resource_edit( ) -> Result { let root = Path::new(input.project_path.trim()); enforce_project_permission_policy(root, "asset.register")?; - resume_local_project_resource_edit_at(input).await + resume_local_project_resource_edit_at(input) + .await + .map_err(resource_edit_command_error) +} + +/// 桌面命令面给自己的资源编辑失败加前缀:typed 错误只带平台原文(或「服务器未返回错误信息」), +/// 前缀是使用者的事——工具面用的是各工具自己的前缀,这里是桌面命令。 +fn resource_edit_command_error(cause: String) -> String { + format!("资源编辑生成失败:{cause}") } #[tauri::command] diff --git a/apps/ai-game-creator-shell/src-tauri/src/project/resource_editor/background_removal_tests.rs b/apps/ai-game-creator-shell/src-tauri/src/project/resource_editor/background_removal_tests.rs index 3f1c306cf..5f96cf170 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/project/resource_editor/background_removal_tests.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/project/resource_editor/background_removal_tests.rs @@ -714,7 +714,7 @@ async fn background_removal_remote_failure_keeps_manifest_without_result() { server_message: None } ); - assert_eq!(error.to_user_msg(), "资源编辑生成失败"); + assert_eq!(error.to_user_msg(), "服务器未返回错误信息"); let manifest = read_existing_manifest_for_project(fixture.root()).expect("read unchanged manifest"); assert_eq!(manifest.assets.len(), 1); diff --git a/apps/ai-game-creator-shell/src-tauri/src/project/resource_editor/error.rs b/apps/ai-game-creator-shell/src-tauri/src/project/resource_editor/error.rs index ce7b61668..208bc8feb 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/project/resource_editor/error.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/project/resource_editor/error.rs @@ -17,13 +17,16 @@ pub(crate) enum ResourceEditError { } impl ResourceEditError { - /// 面向用户与模型的文案。只有这一层知道「远端生成失败」该怎么说, - /// 上层(工具层)原样透传,不再叠自己的前缀。 + /// 面向用户与模型的文案。 + /// + /// 这里只给事实:平台原文,或平台没给时的「服务器未返回错误信息」。「谁失败了」这类前缀由使用者 + /// 自己加(工具面用各工具自己的前缀,桌面命令面用「资源编辑生成失败」),这一层不替它们定调—— + /// 前缀写死在这里,非工具侧的调用方就只能看到一句没有信息量的总结。 pub(crate) fn to_user_msg(&self) -> String { match self { Self::RemoteGenerationFailed { server_message } => match server_message { - Some(server_message) => format!("资源编辑生成失败:{server_message}"), - None => "资源编辑生成失败".to_string(), + Some(server_message) => server_message.clone(), + None => "服务器未返回错误信息".to_string(), }, Self::Other(message) => message.clone(), } @@ -35,3 +38,26 @@ impl std::fmt::Display for ResourceEditError { formatter.write_str(&self.to_user_msg()) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn remote_generation_failure_keeps_platform_text_verbatim() { + let error = ResourceEditError::RemoteGenerationFailed { + server_message: Some("provider detail".to_string()), + }; + + assert_eq!(error.to_user_msg(), "provider detail"); + } + + #[test] + fn remote_generation_failure_without_message_blames_the_server() { + let error = ResourceEditError::RemoteGenerationFailed { + server_message: None, + }; + + assert_eq!(error.to_user_msg(), "服务器未返回错误信息"); + } +} diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 35df7a8bd..d0b2f7494 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -9,6 +9,7 @@ - 决策(原文边界):平台原文只进当次错误文案,仍不进账本(`terminal_failure_code` 的写入边界与既有断言不变)。平台 `user_visible_external_generation_error` 已对四种 kind 做 sanitize,图片/视频两种原样透出——与同 wire 的 `canvas_generation.rs` 口径一致,要收边界应改服务端。 - 决策(命名与落点):尚未 typed 化的变体叫 `Other`,不叫 `Message`(后者分不清是「已渲染文案」还是「原始消息」);新错误单独放 `project/resource_editor/error.rs`,不再往主文件里塞类型定义。 - 决策(工具层承载):`RemoteGenerationFailed` 不能到工具层又被压回一句字符串。共用载体放 `agent/tool/error.rs` 的 `RemoteResourceEditFailure { serverMessage }`(两个工具共用的文案只写一份),`CreateOrDeriveResourceError` / `RemoveBackgroundError` 各加 `RemoteGenerationFailed(RemoteResourceEditFailure)` 变体;翻译用显式 `from_resource_edit_error`,不用 `impl From`,远端终态进 `RemoteGenerationFailed`、其余 `Other` 仍落回各工具原有的「失败:<文案>」变体。这样诊断 sidecar 的 `error` 字段(typed enum 整体序列化)天然带上平台原文,LLM 侧拿到的 `message` 也带上。 +- 决策(前缀归属,2026-10-02 追加):叶子错误只给事实,不给「谁失败了」的总结前缀。`RemoteResourceEditFailure::to_user_msg` 与 `ResourceEditError::to_user_msg` 都只返回平台 `error` 原文;平台没给时统一说「服务器未返回错误信息」,不再说「资源编辑生成失败」这种没有信息量的总结。前缀由使用者自己加:`agc_create_or_derive_resource` 用「生成或派生资源失败:」、`agc_remove_background` 用「抠图失败:」、桌面命令面用「资源编辑生成失败:」。同一份 typed 错误因此可以同时服务工具面(前缀各随其工具)与桌面面(保留原有文案)。 - 决策(凭据作用域的 error 类型):`with_direct_editor_api_credentials` 原本把操作限定成 `Result<_, String>`,会把 typed 错误提前压掉。新增 `with_direct_editor_api_credentials_as(operation, credentials_error)` 保留调用方 error 类型,凭据解析失败由调用方显式翻译(这里传 `ResourceEditError::Other`),旧名保持 `String` 语义、零改动。 - 决策(future 装箱):`handle_direct_tool_bridge` 的状态机在调试测试线程的默认栈上已经贴着上限,资源编辑 arm 直接内联会顶穿(`bridge_write_file_waits_on_the_blocking_pool_instead_of_a_runtime_worker` 栈溢出)。桥里四个资源编辑 await 点用 `Box::pin` 只留指针进外层状态机;这是体积问题,不是错误用 `Box`。 - 改动范围:`apps/ai-game-creator-shell/src-tauri/src/project/resource_editor.rs`、`project/resource_editor/error.rs`(新)、`agent/tool/error.rs`、`agent/tool/create_or_derive_resource/error.rs`、`agent/tool/remove_background/error.rs`、`agent/direct_tool_bridge.rs`、`assets.rs`、`docs/project-memory/shared-memory/pitfalls.md`。 diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index 8ff81704d..58ce5cff1 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -340,7 +340,7 @@ Copy Artifact 插件在**非 SYSTEM 认证**下按「认证用户」判权:只 - 现象:「生成背景音乐」再次提交 0.1 秒就失败,卡片只有 `remote-terminal-failed: 远端资源编辑已明确失败,不允许再次请求`,既没有原因也没有下一步。 - 原因:上一次同 `operationId` 的请求被平台确定性拒绝(HTTP 400 或任务 `failed`)后,账本落到 `remote-failed`,之后所有重试都在 `ensure_resource_edit_phase_resumable` 失败关闭;唯一出口是「待恢复资源编辑」里的移出恢复队列,但终态文案没有指向它。 -- 处理:终态文案带出稳定失败码,并明确「先在待恢复资源编辑中把它移出恢复队列」;上游失败原文仍不写入账本(只存分类码),首次失败的原始拒绝说明继续由当次错误文案承担——轮询终态这条路由此改成把平台 `error` 原文装进 `ResourceEditError::RemoteGenerationFailed` 原样带出(2026-10-02),工具层再用 `RemoteResourceEditFailure` 原样传给模型与用户卡片,不再压成「资源编辑生成失败」一句;第一句失败文案不再带 `remote-terminal-failed:` 前缀。 +- 处理:终态文案带出稳定失败码,并明确「先在待恢复资源编辑中把它移出恢复队列」;上游失败原文仍不写入账本(只存分类码),首次失败的原始拒绝说明继续由当次错误文案承担——轮询终态这条路由此改成把平台 `error` 原文装进 `ResourceEditError::RemoteGenerationFailed` 原样带出(2026-10-02),工具层经 `RemoteResourceEditFailure` 转发时只保留原文、平台没给就说「服务器未返回错误信息」,前缀由使用它的工具/命令自己加(不再统一压成「资源编辑生成失败」一句,也不再多一层无信息前缀);第一句失败文案不再带 `remote-terminal-failed:` 前缀。 - 验证:`remote_failed_status_is_terminal_and_can_only_be_archived` 断言失败文案带出平台 `error` 原文、同时账本序列化不含原文;`background_removal_remote_failure_keeps_manifest_without_result` 覆盖平台没给 `error` 时的兜底文案;`submission_bad_request_is_terminal_while_gateway_failure_requires_reconciliation` 等资源编辑用例继续通过。 - 关联:`apps/ai-game-creator-shell/src-tauri/src/project/resource_editor.rs`。