From cecc26d024f8ad7d86817d80c7db1c492b21dc2b Mon Sep 17 00:00:00 2001 From: kdletters <61648117+kdletters@users.noreply.github.com> Date: Mon, 5 Oct 2026 18:35:52 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=A0=E6=92=AD=E6=8F=92=E4=BB=B6=20IPC=20?= =?UTF-8?q?=E5=92=8C=E5=AA=92=E4=BD=93=E8=A7=A3=E6=9E=90=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 插件 stdout 读取错误送达挂起 RPC 保留 UTF-8 图片和账本 JSON 解析原因 同步错误传播约定文档 --- .../src/agent/generation/canvas_generation.rs | 2 +- .../src-tauri/src/plugin_host.rs | 34 ++++++++++++++----- .../src-tauri/src/project/resource_editor.rs | 20 +++++------ ...GC命令错误结构化与错误报告口径-2026-10-01.md | 1 + ...€�里程碑】AGC错误具体文本展示-2026-10-04.md | 1 + 5 files changed, 39 insertions(+), 19 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/generation/canvas_generation.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/generation/canvas_generation.rs index 9d846db13..745cbd6c7 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/generation/canvas_generation.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/generation/canvas_generation.rs @@ -1968,7 +1968,7 @@ fn read_validated_platform_art_reference_at( } let bytes = fs::read(&source_path).map_err(|error| format!("读取参考素材失败:{error}"))?; let decoded = image::load_from_memory(&bytes) - .map_err(|_| format!("参考素材不是可解析图片:{}", source.local_path))?; + .map_err(|error| format!("参考素材不是可解析图片:{}:{error}", source.local_path))?; Ok((bytes, decoded)) } diff --git a/apps/ai-game-creator-shell/src-tauri/src/plugin_host.rs b/apps/ai-game-creator-shell/src-tauri/src/plugin_host.rs index 7694d53f3..4dd4e1b1e 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/plugin_host.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/plugin_host.rs @@ -160,7 +160,7 @@ struct RunningPlugin { #[cfg(windows)] _job: crate::process_session::WindowsProcessJob, stdin: Arc>, - lines: Option>, + lines: Option>>, pending: PendingRpc, registrations: Arc>, next_request_id: u64, @@ -627,9 +627,18 @@ fn spawn_plugin(manifest: &PluginManifest, root: &Path) -> Result { + if sender.send(Ok(line)).is_err() { + break; + } + } + Ok(None) => break, + Err(error) => { + let _ = sender.send(Err(error)); + break; + } } } }); @@ -652,7 +661,7 @@ fn read_bounded_rpc_line(reader: &mut impl BufRead) -> Result, St loop { let buffer = reader .fill_buf() - .map_err(|_| "读取插件输出失败".to_string())?; + .map_err(|error| format!("读取插件输出失败:{error}"))?; if buffer.is_empty() { return if bytes.is_empty() { Ok(None) @@ -670,7 +679,7 @@ fn read_bounded_rpc_line(reader: &mut impl BufRead) -> Result, St if newline.is_some() { return String::from_utf8(bytes) .map(Some) - .map_err(|_| "插件输出不是 UTF-8".to_string()); + .map_err(|error| format!("插件输出不是 UTF-8:{error}")); } } } @@ -1437,10 +1446,18 @@ impl PluginHost { let manifest = record.manifest.clone(); let root = root.to_path_buf(); thread::spawn(move || { - while let Ok(line) = lines.recv() { + let mut terminal_error = None; + while let Ok(line_result) = lines.recv() { if !active.load(Ordering::SeqCst) { break; } + let line = match line_result { + Ok(line) => line, + Err(error) => { + terminal_error = Some(error); + break; + } + }; let Ok(envelope) = serde_json::from_str::(&line) else { continue; }; @@ -1495,8 +1512,9 @@ impl PluginHost { } if let Ok(mut waiting) = pending.lock() { let remaining = std::mem::take(&mut *waiting); + let terminal_error = terminal_error.unwrap_or_else(|| "插件进程已退出".to_string()); for (_, sender) in remaining { - let _ = sender.send(Err("插件进程已退出".to_string())); + let _ = sender.send(Err(terminal_error.clone())); } } }); 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 189ad10fb..6e3c05987 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 @@ -1454,8 +1454,8 @@ fn resolve_resource_edit_source( RESOURCE_EDIT_TEXT_MAX_BYTES, "源文本资源", )?; - let text = - String::from_utf8(bytes).map_err(|_| "文本资源必须使用 UTF-8 编码".to_string())?; + let text = String::from_utf8(bytes) + .map_err(|error| format!("文本资源必须使用 UTF-8 编码:{error}"))?; return Ok(ResourceEditSourceSnapshot { canonical_resource_id, source_path: Some(path), @@ -1507,7 +1507,7 @@ fn resolve_resource_edit_source( let (source_width, source_height) = if input.edit_kind == LocalProjectResourceEditKind::CharacterAnimation { let decoded = image::load_from_memory(&bytes) - .map_err(|_| "生成角色动画前无法解析源图片尺寸".to_string())?; + .map_err(|error| format!("生成角色动画前无法解析源图片尺寸:{error}"))?; (Some(decoded.width()), Some(decoded.height())) } else { (None, None) @@ -2029,8 +2029,8 @@ async fn register_resource_edit_source_image( .bytes .as_deref() .ok_or_else(|| "登记源图片缺少文件内容".to_string())?; - let decoded = - image::load_from_memory(bytes).map_err(|_| "登记源图片前无法解析图片尺寸".to_string())?; + let decoded = image::load_from_memory(bytes) + .map_err(|error| format!("登记源图片前无法解析图片尺寸:{error}"))?; let endpoint = format!( "/api/external/v1/editor/projects/{}/resources", percent_encode_query_component(remote_project_id) @@ -2313,7 +2313,7 @@ async fn ensure_resource_edit_source_reference( .as_deref() .ok_or_else(|| "登记源图片缺少文件内容".to_string())?; let decoded = image::load_from_memory(bytes) - .map_err(|_| "登记源图片前无法解析图片尺寸".to_string())?; + .map_err(|error| format!("登记源图片前无法解析图片尺寸:{error}"))?; let resource_id = if let Some(resource_id) = ledger.source_remote_resource_id.clone() { resource_id } else { @@ -2755,7 +2755,7 @@ async fn submit_resource_edit_remote( .as_deref() .ok_or_else(|| "资源编辑账本缺少请求正文".to_string())?; let mut body_value = serde_json::from_str::(body) - .map_err(|_| "资源编辑账本请求正文无效".to_string())?; + .map_err(|error| format!("资源编辑账本请求正文无效:{error}"))?; if editor_api_mode() == EditorApiMode::PlatformAccount { if let Some(inputs) = body_value.as_object_mut().and_then(|body| { body.entry("generationInputs") @@ -3165,7 +3165,7 @@ fn validate_downloaded_media( return Err("抠图结果必须是带透明通道的 PNG".to_string()); } let decoded = image::load_from_memory_with_format(bytes, image::ImageFormat::Png) - .map_err(|_| "抠图结果不是有效的 PNG".to_string())?; + .map_err(|error| format!("抠图结果不是有效的 PNG:{error}"))?; if !decoded.color().has_alpha() { return Err("抠图结果缺少透明通道".to_string()); } @@ -4745,7 +4745,7 @@ fn write_resource_edit_result_binding( | LocalProjectResourceEditKind::BackgroundRemoval ) { let decoded = image::load_from_memory(&bytes) - .map_err(|_| "派生图片 binding 无法解析尺寸".to_string())?; + .map_err(|error| format!("派生图片 binding 无法解析尺寸:{error}"))?; (Some(decoded.width()), Some(decoded.height())) } else { (None, None) @@ -5878,7 +5878,7 @@ pub(crate) async fn derive_local_project_resource_typed( Err(error) => Err(error), Ok(bytes) => (|| { let content = std::str::from_utf8(&bytes) - .map_err(|_| "派生文本不是 UTF-8".to_string()) + .map_err(|error| format!("派生文本不是 UTF-8:{error}")) .map_err(ResourceEditError::Other)?; let (media_type, extension) = validate_text_derivative( &input.edit_kind, diff --git a/docs/adr/【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md b/docs/adr/【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md index aa6692527..9e814a1d7 100644 --- a/docs/adr/【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md +++ b/docs/adr/【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md @@ -112,6 +112,7 @@ DirectProject 已经解决过同一类问题([`【ADR】DirectProject命令接 - Codex CLI / Claude sidecar 的 JSONL/JSON 解析失败、失败终态、缺失终态、超时和空回执都保留解析错误、失败事件正文与有界安全片段;`LlmError` 没有 detail 字段的超时/空回执由 Direct `ModelCallFailed.detail` 补回,不再把这些协议事实统一压成单一类别。 - 账户、模型目录、External Editor/资源编辑、发布、素材上传、错误报告与客户端受控工具桥共用同一原则:网络错误保留底层因链,JSON/协议解析保留 serde 原因,HTTP 错误保留状态码与安全正文;只有响应完全没有正文时才说明“未提供 error/code/message”,不得退成“无法连接/格式无效/服务器未返回错误信息”。 - 素材直传、参考图直传、资源编辑上传/轮询、模型目录初始化和 Direct MCP stdio 的 `send`/读写/序列化失败,不得用 `map_err(|_| 固定句)` 丢掉 `reqwest`、serde 或 OS/IPC 正文;固定句只能作为事实前缀,底层原因必须接在后面。 +- 插件 stdout RPC 读取线程不得吞掉 `fill_buf`、UTF-8 或大小上限错误后伪装成“插件进程已退出”;读线程通过挂起 RPC 的错误通道传播真实原因。 - 浏览器启动/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 9d346443f..b2a498180 100644 --- a/docs/project-memory/plans/【里程碑】AGC错误具体文本展示-2026-10-04.md +++ b/docs/project-memory/plans/【里程碑】AGC错误具体文本展示-2026-10-04.md @@ -15,6 +15,7 @@ DirectProject 回合失败在确认不是客户端内部不可归类故障时, - Rust 侧错误文本脱敏:只替换敏感值、URL、绝对路径和私钥内容,保留 HTTP 状态、错误码、字段名和可行动描述。 - app-server `codexErrorInfo` 只有嵌套机器字段时,保留经有界脱敏的结构化值;`error` 是字符串或未知 JSON 形状时也保留正文;`fields=codexErrorInfo` 不能成为唯一正文。 - 素材/参考图/资源编辑上传、资源编辑轮询、模型目录初始化和 Direct MCP stdio 的网络、JSON、写入、读写失败保留底层错误正文;不能由 `map_err(|_| 固定句)` 把它们压成无因的连接或失败提示。 +- 插件 stdout RPC 的 `fill_buf`、UTF-8 和响应大小错误通过挂起 RPC 传播,不能在读取线程里吞掉后统一说“插件进程已退出”。 - Transport / Stream / IPC / host process / memory exhaustion 的回归测试与错误事件证据。 ## 不做