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 fcaa5b22d..5c505e70d 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 @@ -7,7 +7,7 @@ use std::collections::{BTreeMap, BTreeSet}; use std::fs; -use std::io::{BufRead, BufReader, Write}; +use std::io::{BufRead, BufReader, Read, Write}; use std::path::{Component, Path, PathBuf}; use std::process::{Child, ChildStdin, Command, Stdio}; use std::sync::atomic::{AtomicBool, AtomicU8, Ordering}; @@ -43,6 +43,7 @@ const RPC_TIMEOUT: Duration = Duration::from_secs(10); const EDITOR_RPC_TIMEOUT: Duration = Duration::from_secs(90); const MAX_MANIFEST_BYTES: u64 = 1024 * 1024; const MAX_RPC_BYTES: usize = 2 * 1024 * 1024; +const MAX_PLUGIN_STDERR_BYTES: usize = 64 * 1024; const KNOWN_PERMISSIONS: &[&str] = &[ "events.subscribe", @@ -163,6 +164,7 @@ struct RunningPlugin { lines: Option>>, pending: PendingRpc, registrations: Arc>, + stderr_summary: Arc>, next_request_id: u64, editor_context: Option, active: Arc, @@ -578,7 +580,7 @@ fn spawn_plugin(manifest: &PluginManifest, root: &Path) -> Result Result stderr, + None => { + let _ = child.kill(); + let _ = child.wait(); + return Err("插件 stderr 不可用".to_string()); + } + }; + let stderr_summary = Arc::new(Mutex::new(String::new())); + let stderr_summary_writer = Arc::clone(&stderr_summary); + thread::spawn(move || { + let mut bytes = Vec::new(); + let result = stderr + .take((MAX_PLUGIN_STDERR_BYTES + 1) as u64) + .read_to_end(&mut bytes); + let summary = match result { + Ok(_) if bytes.len() > MAX_PLUGIN_STDERR_BYTES => { + "插件 stderr 超过大小上限".to_string() + } + Ok(_) => crate::sanitize_diagnostic_message(&String::from_utf8_lossy(&bytes), None) + .chars() + .take(1200) + .collect(), + Err(error) => format!("读取插件 stderr 失败:{error}"), + }; + if let Ok(mut stored) = stderr_summary_writer.lock() { + *stored = summary; + } + }); #[cfg(windows)] let job = match crate::process_session::WindowsProcessJob::assign_std(&child) { Ok(job) => job, @@ -651,6 +682,7 @@ fn spawn_plugin(manifest: &PluginManifest, root: &Path) -> Result Some(if let Some(code) = status.code() { - format!("插件进程已退出:exitCode={code}") + format!( + "插件进程已退出:exitCode={code}{}", + running + .stderr_summary + .lock() + .ok() + .filter(|summary| !summary.is_empty()) + .map(|summary| format!(";stderr={summary}")) + .unwrap_or_default() + ) } else { - "插件进程已被信号终止".to_string() + format!( + "插件进程已被信号终止{}", + running + .stderr_summary + .lock() + .ok() + .filter(|summary| !summary.is_empty()) + .map(|summary| format!(";stderr={summary}")) + .unwrap_or_default() + ) }), Ok(None) => None, Err(error) => Some(format!("读取插件进程状态失败:{error}")), diff --git a/docs/project-memory/plans/【里程碑】AGC错误具体文本展示-2026-10-04.md b/docs/project-memory/plans/【里程碑】AGC错误具体文本展示-2026-10-04.md index 7e91b6880..833ecec18 100644 --- a/docs/project-memory/plans/【里程碑】AGC错误具体文本展示-2026-10-04.md +++ b/docs/project-memory/plans/【里程碑】AGC错误具体文本展示-2026-10-04.md @@ -100,3 +100,4 @@ DirectProject 回合失败在确认不是客户端内部不可归类故障时, - 发布构建预检原先把 `package.json` 元数据/读取/解析错误当成“没有 build”,把 `package-lock.json` 读取/解析错误当成“没有锁定版本”;现分别区分 NotFound 与真实读盘/JSON 错误,保留相对工作区路径和底层正文。 - 发布尝试账本和工作区偏好文件原先把损坏、权限、类型和 JSON 解析失败当成空账本/默认偏好;现只有文件不存在才使用默认,其他读取错误沿发布幂等和建项目录命令返回具体正文。 - 插件主机扫描工作区、读取进程状态、停止插件和禁用插件时原先吞掉目录项/`try_wait`/kill/wait 错误;现保留插件状态错误并阻止“停止失败却显示已禁用”,正常主动 kill 的非零退出码不再误报。 +- 插件进程 stderr 原先使用 `Stdio::null()`,即使进程退出也只能显示退出码;现以有界脱敏 sidecar 保留插件 stderr,并在退出状态中附带正文。