diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_delivery.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_delivery.rs index a65bf0679..88ce1af67 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_delivery.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_delivery.rs @@ -58,11 +58,12 @@ pub(super) async fn update_plan( } *explanation = truncate_agent_runtime_text(explanation, 1200); } - let value = serde_json::to_value(plan).map_err(|_| "direct-plan-invalid")?; + let value = + serde_json::to_value(plan).map_err(|error| format!("direct-plan-invalid:{error}"))?; let session = Arc::clone(session); tokio::task::spawn_blocking(move || session.update_plan(value)) .await - .map_err(|_| "direct-plan-worker-exited")? + .map_err(|error| format!("direct-plan-worker-exited:{error}"))? } #[derive(Clone, Debug, Deserialize, Serialize)] @@ -404,7 +405,7 @@ pub(super) async fn requires_new_web_contract( let kind = creation_type.map(str::to_string); tokio::task::spawn_blocking(move || initial_requirement_at(&root, kind.as_deref(), cli)) .await - .map_err(|_| "delivery-initial-check-exited")? + .map_err(|error| format!("delivery-initial-check-exited:{error}"))? } fn mark_initial_delivered(root: &Path, ledger: &ExecutionLedger) -> Result<(), String> { @@ -451,7 +452,7 @@ pub(super) async fn register_contract( let value = serde_json::to_value(contract).map_err(|_| "delivery-contract-invalid")?; let frozen = session.freeze_contract(value)?; Ok(json!({"status":"frozen","contract":frozen,"next":"只推进已登记范围;使用托管构建/验证和固定场景提供真实证据"})) - }).await.map_err(|_| "delivery-contract-worker-exited")? + }).await.map_err(|error| format!("delivery-contract-worker-exited:{error}"))? } fn artifact_hash(root: &Path, relative: &str) -> Result { @@ -467,7 +468,7 @@ fn artifact_hash(root: &Path, relative: &str) -> Result { loop { let count = file .read(&mut buffer) - .map_err(|_| "delivery-artifact-read")?; + .map_err(|error| format!("delivery-artifact-read:{error}"))?; if count == 0 { break; } @@ -650,7 +651,7 @@ pub(super) async fn status(root: &Path, session: &Arc) -> Resu let session = Arc::clone(session); tokio::task::spawn_blocking(move || { let ledger = session.snapshot()?; let assessment = assess(&root,&ledger)?; Ok(json!({"phase":ledger.phase,"contract":ledger.contract,"plan":ledger.plan,"assessment":assessment,"writeRecoveryRequired":ledger.last_failed_write_revision.is_some(),"usedPasses":ledger.used_passes,"maxRuns":ledger.max_runs,"usedExecutionMs":ledger.used_execution_ms,"maxExecutionMs":ledger.max_execution_ms,"inFlight":ledger.active.len(),"report":ledger.terminal_report})) - }).await.map_err(|_| "delivery-status-worker-exited")? + }).await.map_err(|error| format!("delivery-status-worker-exited:{error}"))? } pub(super) async fn try_seal(root: &Path, session: &Arc) -> Result { @@ -667,7 +668,7 @@ pub(super) async fn try_seal(root: &Path, session: &Arc) -> Re session.begin_sealing(ledger.revision) }) .await - .map_err(|_| "delivery-seal-worker-exited")? + .map_err(|error| format!("delivery-seal-worker-exited:{error}"))? } pub(super) async fn finish_sealing( @@ -693,7 +694,7 @@ pub(super) async fn finish_sealing( session.complete(report.clone())?; if mark_initial_delivered(&root,&ledger).is_err() { app_log!("首次交付标记未写入,后续保持更严格的新项目验收要求"); } Ok(Some(report)) - }).await.map_err(|_| "delivery-finalize-worker-exited")? + }).await.map_err(|error| format!("delivery-finalize-worker-exited:{error}"))? } /// 回合末的宿主复核:返回要交付的答复,或者一个"还没完,按这份证据继续修"的要求。 @@ -722,9 +723,10 @@ pub(super) async fn review_reply( let review_session = Arc::clone(session); let review = tokio::task::spawn_blocking(move || review_session.record_delivery_review()) .await - .map_err(|_| "delivery-review-worker-exited")??; + .map_err(|error| format!("delivery-review-worker-exited:{error}"))??; let assessment = status(root, session).await?; - let detail = serde_json::to_string(&assessment).map_err(|_| "delivery-review-invalid")?; + let detail = serde_json::to_string(&assessment) + .map_err(|error| format!("delivery-review-invalid:{error}"))?; if review >= ledger.max_runs { let missing = assessment .pointer("/assessment/checks") @@ -751,7 +753,7 @@ pub(super) async fn review_reply( let saved = report.clone(); tokio::task::spawn_blocking(move || session.interrupt(saved)) .await - .map_err(|_| "delivery-review-worker-exited")??; + .map_err(|error| format!("delivery-review-worker-exited:{error}"))??; return Ok(Some(report)); } Err(TurnError::ReviewRequired { diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_execution.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_execution.rs index 573653eb4..b8c20cc82 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_execution.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_execution.rs @@ -128,11 +128,11 @@ struct CodexExecutorIdentity { fn executor_digest(path: &Path) -> Result { use std::io::Read; - let mut file = - File::open(path).map_err(|_| "direct-execution-executor: 无法读取已选择的执行器")?; + let mut file = File::open(path) + .map_err(|error| format!("direct-execution-executor: 无法读取已选择的执行器:{error}"))?; let before = file .metadata() - .map_err(|_| "direct-execution-executor: 执行器身份不可用")?; + .map_err(|error| format!("direct-execution-executor: 执行器身份不可用:{error}"))?; if !before.is_file() || before.len() > 512 * 1024 * 1024 { return Err("direct-execution-executor: 执行器类型或大小无效".into()); } @@ -142,7 +142,7 @@ fn executor_digest(path: &Path) -> Result { loop { let read = file .read(&mut buffer) - .map_err(|_| "direct-execution-executor: 执行器读取失败")?; + .map_err(|error| format!("direct-execution-executor: 执行器读取失败:{error}"))?; if read == 0 { break; } @@ -154,7 +154,7 @@ fn executor_digest(path: &Path) -> Result { } let after = file .metadata() - .map_err(|_| "direct-execution-executor: 执行器身份不可用")?; + .map_err(|error| format!("direct-execution-executor: 执行器身份不可用:{error}"))?; if bytes != before.len() || after.len() != before.len() || before.modified().ok() != after.modified().ok() @@ -446,7 +446,7 @@ pub(super) async fn begin( ) }) .await - .map_err(|_| "direct-execution-start: 宿主状态初始化退出")??; + .map_err(|error| format!("direct-execution-start: 宿主状态初始化退出:{error}"))??; let root = session.root.clone(); { let mut registry = sessions() @@ -515,7 +515,7 @@ pub(super) fn open_with_analytics_at( config.validate()?; let root = root .canonicalize() - .map_err(|_| "direct-execution-project: 项目不可用")?; + .map_err(|error| format!("direct-execution-project: 项目不可用:{error}"))?; if turn.is_empty() || turn.len() > 512 || request_hash.len() != 64 { return Err("direct-execution-identity: 回合身份无效".into()); } @@ -523,17 +523,17 @@ pub(super) fn open_with_analytics_at( return Err("direct-execution-host: 权威状态不得写入模型项目目录".into()); } crate::ensure_game_creator_private_directory_tree(host, "宿主执行状态目录") - .map_err(|_| "direct-execution-host: 无法准备私有执行状态目录")?; + .map_err(|error| format!("direct-execution-host: 无法准备私有执行状态目录:{error}"))?; let host = host .canonicalize() - .map_err(|_| "direct-execution-host: 状态目录不可用")?; + .map_err(|error| format!("direct-execution-host: 状态目录不可用:{error}"))?; if host.starts_with(&root) { return Err("direct-execution-host: 权威状态不得写入模型项目目录".into()); } let project_key = hash(root.to_string_lossy().as_bytes()); let directory = host.join(&project_key); crate::ensure_game_creator_private_directory_tree(&directory, "宿主项目执行状态") - .map_err(|_| "direct-execution-host: 无法准备私有项目执行状态")?; + .map_err(|error| format!("direct-execution-host: 无法准备私有项目执行状态:{error}"))?; let key = hash(turn.as_bytes()); let lock_path = directory.join(format!("{key}.lock")); if lock_path.exists() { @@ -546,7 +546,7 @@ pub(super) fn open_with_analytics_at( .create(true) .truncate(false) .open(&lock_path) - .map_err(|_| "direct-execution-owner: 无法打开执行归属锁")?; + .map_err(|error| format!("direct-execution-owner: 无法打开执行归属锁:{error}"))?; owner .try_lock() .map_err(|_| "direct-execution-active: 同一回合仍由其他执行器持有")?; diff --git a/docs/adr/【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md b/docs/adr/【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md index d30d636a6..ac6ac1aab 100644 --- a/docs/adr/【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md +++ b/docs/adr/【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md @@ -119,6 +119,7 @@ DirectProject 已经解决过同一类问题([`【ADR】DirectProject命令接 - WebView 普通错误出口遇到 Tauri 的结构化对象时,优先展示 `message/detail/reason/code`,再有界序列化对象;不得把对象转成 `[object Object]` 后回落“未提供具体错误正文”。 - Direct 工具桥图片读取和真实试玩验证证据的 OS 读取/哈希错误保留底层原因;路径越界、私有路径和安全拒绝仍使用稳定安全码。 - `agc_read_project_context` 的安全拒绝继续只给稳定码,但受控文件打开/读取/复核的 OS 错误要附在错误码后,不能把 `file-read-failed` 当成完整原因。 +- Direct 交付复核/执行账本的 worker join、产物读取、执行器摘要和宿主目录/锁 I/O 错误也保留底层原因;稳定 delivery/direct-execution 分类码只负责分流。 - 浏览器启动/DevTools 握手与 Codex model-catalog 子进程失败保留 stderr、退出状态、解析错误和阶段;稳定机器码仍用于分类,但不能单独成为用户可见正文。 - Node/npm 环境探测和 Web scaffold 构建持续排空并保存有界 stdout/stderr 尾部,失败返回退出状态、超时阶段和脱敏正文;环境预检回执同时下发 `code` 与 `diagnostic`。 - HTTP 409 只有明确包含泥点不足事实时才映射为 `paidCreditsInsufficient`;Claude Code 的普通 409 冲突保留为 `upstreamFailed`。 diff --git a/docs/project-memory/plans/【里程碑】AGC错误具体文本展示-2026-10-04.md b/docs/project-memory/plans/【里程碑】AGC错误具体文本展示-2026-10-04.md index 2df3632d6..a8feaef46 100644 --- a/docs/project-memory/plans/【里程碑】AGC错误具体文本展示-2026-10-04.md +++ b/docs/project-memory/plans/【里程碑】AGC错误具体文本展示-2026-10-04.md @@ -22,6 +22,7 @@ DirectProject 回合失败在确认不是客户端内部不可归类故障时, - WebView 结构化对象错误保留 `message/detail/reason/code` 或有界 JSON 正文,避免 `[object Object]` 触发普通错误兜底。 - Direct 工具桥图片与真实试玩证据读取/哈希错误保留 OS 正文,安全边界拒绝继续使用稳定码。 - `agc_read_project_context` 保留 `file-read-failed` 等稳定码,同时在能取得时附带文件打开/读取/复核的 OS 正文。 +- Direct 交付复核和执行账本的 worker、产物读取、执行器摘要及宿主目录/锁失败保留底层原因,分类码不再单独承载正文。 - Transport / Stream / IPC / host process / memory exhaustion 的回归测试与错误事件证据。 ## 不做