From b3b568385ce4fdc82bfd99d159c84d405facf7ca Mon Sep 17 00:00:00 2001 From: kdletters <61648117+kdletters@users.noreply.github.com> Date: Tue, 6 Oct 2026 01:44:11 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BB=A7=E7=BB=AD=E6=94=B6=E6=95=9B=E6=B5=8F?= =?UTF-8?q?=E8=A7=88=E5=99=A8=E4=B8=8E=E7=B4=A0=E6=9D=90=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 保留浏览器进程收割和清理阶段正文 保留 External Editor 凭据解析与保存错误 区分生成任务清单读取失败与未初始化状态 --- .../src-tauri/src/asset_generation_tasks.rs | 16 ++-- .../src-tauri/src/assets.rs | 14 +-- .../src-tauri/src/browser/process.rs | 87 ++++++++++++++----- ...€�里程碑】AGC错误具体文本展示-2026-10-04.md | 1 + 4 files changed, 84 insertions(+), 34 deletions(-) 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 44d7dc7a1..faec7fda0 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 @@ -198,15 +198,21 @@ fn trim_ledger(tasks: &mut Vec) { /// /// 只用来做**精确落点**的交叉核对,所以按同一个口径归一化两侧的路径串(去空白、反斜杠折成 /// 正斜杠);不做模糊匹配、不按素材名猜,避免把另一次生成的产物算到这条任务上。 -fn registered_asset_ids_by_local_path(root: &Path) -> Option> { - let manifest = read_existing_manifest_for_project(root).ok()?; - Some( +fn registered_asset_ids_by_local_path( + root: &Path, +) -> Result>, String> { + let manifest = match read_existing_manifest_for_project(root) { + Ok(manifest) => manifest, + Err(error) if error == "本地项目尚未初始化" => return Ok(None), + Err(error) => return Err(format!("读取生成任务核对用项目清单失败:{error}")), + }; + Ok(Some( manifest .assets .iter() .map(|asset| (normalize_local_path(&asset.local_path), asset.id.clone())) .collect(), - ) + )) } fn normalize_local_path(path: &str) -> String { @@ -278,7 +284,7 @@ pub(crate) fn list_local_project_asset_generation_tasks( ) -> Result, String> { let _guard = lock_ledger()?; let mut tasks = read_ledger(root)?; - let registered = registered_asset_ids_by_local_path(root); + let registered = registered_asset_ids_by_local_path(root)?; if repair_interrupted_tasks(&mut tasks, ®istered) { write_ledger(root, &tasks)?; drop(_guard); 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 1e8ec4712..f3d94ad20 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/assets.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/assets.rs @@ -217,8 +217,10 @@ fn private_external_editor_api_credentials_from_file_at( "本机陶泥儿开发者 Key 配置", PRIVATE_EXTERNAL_EDITOR_API_KEY_MAX_BYTES, )?; - let parsed = serde_json::from_str::(&content) - .map_err(|_| "本机陶泥儿开发者 Key 配置格式无效,请重新登录客户端后重试".to_string())?; + let parsed = + serde_json::from_str::(&content).map_err(|error| { + format!("本机陶泥儿开发者 Key 配置格式无效:{error};请重新登录客户端后重试") + })?; let api_key = normalize_external_editor_api_key(&parsed.api_key)?; let api_base_url = normalize_external_editor_api_base_url( parsed @@ -506,9 +508,9 @@ pub(crate) async fn ensure_private_external_editor_api_credentials( if let Some(credentials) = private_external_editor_api_credentials_from_file_at(&path)? { return Ok(credentials); } - prepare_private_external_editor_api_credentials_parent_dir_at(&path).map_err(|_| { + prepare_private_external_editor_api_credentials_parent_dir_at(&path).map_err(|error| { format!( - "{PRIVATE_EXTERNAL_EDITOR_CREDENTIAL_STORAGE_PREPARATION_FAILURE}: 本机开发者凭据存储目录未安全初始化;未创建远端凭据" + "{PRIVATE_EXTERNAL_EDITOR_CREDENTIAL_STORAGE_PREPARATION_FAILURE}: 本机开发者凭据存储目录未安全初始化:{error};未创建远端凭据" ) })?; // Another local client may have completed the atomic write while this call @@ -518,9 +520,9 @@ pub(crate) async fn ensure_private_external_editor_api_credentials( } let credentials = create_private_external_editor_api_credentials_from_platform_session().await?; - if write_private_external_editor_api_credentials_at(&path, &credentials).is_err() { + if let Err(error) = write_private_external_editor_api_credentials_at(&path, &credentials) { return Err(format!( - "{PRIVATE_EXTERNAL_EDITOR_CREDENTIAL_PERSISTENCE_FAILURE}: 本机开发者凭据已创建但未能安全保存;请在账户开发者凭据页面撤销后重试" + "{PRIVATE_EXTERNAL_EDITOR_CREDENTIAL_PERSISTENCE_FAILURE}: 本机开发者凭据已创建但未能安全保存:{error};请在账户开发者凭据页面撤销后重试" )); } Ok(credentials) diff --git a/apps/ai-game-creator-shell/src-tauri/src/browser/process.rs b/apps/ai-game-creator-shell/src-tauri/src/browser/process.rs index 66862b26e..81682e790 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/browser/process.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/browser/process.rs @@ -392,20 +392,46 @@ struct BrowserProcessGuard { job: Option, } +struct BrowserReapOutcome { + exited: bool, + detail: Option, +} + impl BrowserProcessGuard { /// 整树收割:Windows 由 Job 终止全树,再 kill+wait root 兜底; /// Unix 只终止 root,子进程依赖 root 死亡后的级联退出。 /// 返回是否已确认 root 进程退出。 - async fn reap(&mut self) -> bool { + async fn reap(&mut self) -> BrowserReapOutcome { + let mut details = Vec::new(); #[cfg(windows)] if let Some(job) = &self.job { - let _ = job.terminate(); + if let Err(error) = job.terminate() { + details.push(format!("browser Job 终止失败:{error}")); + } + } + match tokio::time::timeout(BROWSER_CLOSE_TIMEOUT, self.child.kill()).await { + Ok(Ok(())) => {} + Ok(Err(error)) if error.kind() != std::io::ErrorKind::NotFound => { + details.push(format!("browser root 终止失败:{error}")); + } + Ok(Err(_)) => {} + Err(_) => details.push("browser root 终止超时".to_string()), + } + let exited = match tokio::time::timeout(BROWSER_CLOSE_TIMEOUT, self.child.wait()).await { + Ok(Ok(_)) => true, + Ok(Err(error)) => { + details.push(format!("等待 browser root 退出失败:{error}")); + false + } + Err(_) => { + details.push("等待 browser root 退出超时".to_string()); + false + } + }; + BrowserReapOutcome { + exited, + detail: (!details.is_empty()).then(|| details.join(";")), } - let _ = tokio::time::timeout(BROWSER_CLOSE_TIMEOUT, self.child.kill()).await; - matches!( - tokio::time::timeout(BROWSER_CLOSE_TIMEOUT, self.child.wait()).await, - Ok(Ok(_)) - ) } async fn confirm_reaped(&mut self) -> Result<(), String> { @@ -418,7 +444,7 @@ impl BrowserProcessGuard { loop { if job .is_empty() - .map_err(|_| "browser-tree-reap-unconfirmed".to_string())? + .map_err(|error| format!("browser-tree-reap-unconfirmed:{error}"))? { return Ok(()); } @@ -434,7 +460,8 @@ impl BrowserProcessGuard { // 配置目录,且 browser-cleanup-unconfirmed 永远不会暴露。 match self.child.inner.try_wait() { Ok(Some(_)) => Ok(()), - _ => Err("browser-tree-reap-unconfirmed".into()), + Ok(None) => Err("browser-tree-reap-unconfirmed:root 进程仍在运行".into()), + Err(error) => Err(format!("browser-tree-reap-unconfirmed:{error}")), } } } @@ -474,26 +501,28 @@ impl OwnedBrowser { { return Ok(()); } - let subprocess_exited = self.process.reap().await; - if !subprocess_exited { + let reap = self.process.reap().await; + if !reap.exited { // 进程退出未确认:保留目录与 owner.json,留待下次启动或 // 预检时由跨会话清扫收割,而不是删掉证据让清扫失明。 if let Some(temp) = self.temp.take() { std::mem::forget(temp); } - return Err( - "browser-cleanup-unconfirmed: stage=shutdown subprocess-exited=false cleanup-confirmed=false recovery-retried=false" - .into(), - ); + return Err(format!( + "browser-cleanup-unconfirmed: stage=shutdown subprocess-exited=false cleanup-confirmed=false recovery-retried=false{}", + reap.detail + .as_deref() + .map(|detail| format!(" detail={detail}")) + .unwrap_or_default() + )); } - if self.process.confirm_reaped().await.is_err() { + if let Err(error) = self.process.confirm_reaped().await { if let Some(temp) = self.temp.take() { std::mem::forget(temp); } - return Err( - "browser-cleanup-unconfirmed: stage=shutdown subprocess-exited=true cleanup-confirmed=false recovery-retried=false" - .into(), - ); + return Err(format!( + "browser-cleanup-unconfirmed: stage=shutdown subprocess-exited=true cleanup-confirmed=false recovery-retried=false detail={error}" + )); } Ok(()) } @@ -513,8 +542,8 @@ async fn cleanup_failed_launch( error: OwnedBrowserLaunchError, tree_control_confirmed: bool, ) -> BrowserLaunchFailure { - let subprocess_exited = process.reap().await; - let tree_reaped = if tree_control_confirmed && subprocess_exited { + let reap = process.reap().await; + let tree_reaped = if tree_control_confirmed && reap.exited { #[cfg(windows)] { process.job.is_none() || process.confirm_reaped().await.is_ok() @@ -534,7 +563,19 @@ async fn cleanup_failed_launch( let _ = temp.keep(); false }; - BrowserLaunchFailure::from_launch_error(error, subprocess_exited, cleanup_confirmed) + let mut failure = + BrowserLaunchFailure::from_launch_error(error, reap.exited, cleanup_confirmed); + if let Some(detail) = reap.detail { + let detail = crate::sanitize_diagnostic_message(&detail, None) + .chars() + .take(1200) + .collect::(); + failure.detail = Some(match failure.detail.take() { + Some(existing) => format!("{existing};清理:{detail}"), + None => format!("清理:{detail}"), + }); + } + failure } /// 自拉浏览器并完成 CDP 连接。Windows 先把 root 放入 KILL_ON_JOB_CLOSE diff --git a/docs/project-memory/plans/【里程碑】AGC错误具体文本展示-2026-10-04.md b/docs/project-memory/plans/【里程碑】AGC错误具体文本展示-2026-10-04.md index 88c96a8ad..12ba2f1d2 100644 --- a/docs/project-memory/plans/【里程碑】AGC错误具体文本展示-2026-10-04.md +++ b/docs/project-memory/plans/【里程碑】AGC错误具体文本展示-2026-10-04.md @@ -92,3 +92,4 @@ DirectProject 回合失败在确认不是客户端内部不可归类故障时, - 对话模型选择器读取 native 配置原先把 IPC/文件异常 `.catch(() => null)` 后只显示“读取客户端配置失败”;现通过统一可见错误脱敏出口保留具体正文,并保留配置不可读时不猜测模型路由的安全行为。 - 策划工作区事件订阅失败仍有一处直接静默结束,导致文件刷新失联却没有提示;现把订阅 IPC 正文写入工作区错误状态,继续保留手动刷新与已有清单读取错误出口。 - 新一轮原生扫出 app-server 进程与上下文漏损:Codex 执行器核验/OAuth 保存/回合身份的 JoinError、图片 base64 解码、Direct 上下文 canonicalize,以及进程树归属、Job、等待、终止和 Drop 清理错误原先只剩固定码;现保留 JoinError、解码器、OS/Job、等待和终止阶段正文,并为进程树 Drop 清理失败写入诊断日志。 +- 继续扫出浏览器收割的 Job/root kill/wait 错误原先压成 `bool`、External Editor 本机凭据 JSON/目录创建/保存错误丢底层正文,以及生成任务中断收口把项目清单读取失败当成“状态未知”;现分别保留浏览器清理阶段正文、凭据解析/存储正文,并让非“项目尚未初始化”的清单读取失败直接返回具体错误。