diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/mod.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/mod.rs index be81367bf..659caf036 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/mod.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/mod.rs @@ -2162,7 +2162,8 @@ fn direct_codex_error_feedback_prompt(error: &str, attempt: usize) -> String { /// ([`DirectTurnError::is_reportable`],宿主 / 环境事实)。 /// /// 返回给用户看的那行 `direct-codex-failure:v2 ...` 文本:它是这一轮(或这次拒绝)的收口说明, -/// 事件载荷、横幅与项目历史共用同一份,命令边界也只序列化它一次。 +/// 事件载荷与横幅共用同一份,命令边界也只序列化它一次。诊断文件的引用**不进**这份文案: +/// 界面不再展开详情,线索只留在宿主侧(`.agent/runtime/errors`、应用日志与错误上报池)。 pub(crate) fn record_direct_codex_failure( root: &Path, failure: &DirectTurnError, @@ -2211,7 +2212,8 @@ pub(crate) fn record_direct_codex_failure( }; // 诊断 code 仍走共享的 runtime_error 分类(它是跨链路的持久化标签,不是流程判据)。 let error_code = classify_direct_codex_error(&detail); - let unified_detail_ref = persist_agent_runtime_error( + // sidecar 照写,但引用不进用户可见文案。 + let _ = persist_agent_runtime_error( root, client_turn_id, "direct-codex", @@ -2226,10 +2228,9 @@ pub(crate) fn record_direct_codex_failure( "legacyDiagnosticWritten": diagnostic_written, }), ) - .ok() - .map(|event| event.detail_ref); - format!( - "direct-codex-failure:v2 stage={} code={} retryable={} summary={};建议:{};{}{}", + .ok(); + let public_text = format!( + "direct-codex-failure:v2 stage={} code={} retryable={} summary={};建议:{};{}", stage.id(), error_code, retryable, @@ -2238,18 +2239,19 @@ pub(crate) fn record_direct_codex_failure( .unwrap_or("未提供可安全展示的详细原因"), recovery_hint, diagnostics_suffix, - unified_detail_ref - .map(|path| format!(";详情:{path}")) - .unwrap_or_default(), - ) + ); + // 失败也进错误上报池:命令接单化之后前端 catch 只剩"接单被拒",池不能只靠前端填。 + // 两条通道同时上报也不会变成两条——池按 fingerprint 合并同一份文案。 + let _ = crate::error_report::report_agent_runtime_error("direct-codex", &public_text); + public_text } -/// 命令边界的错误文本:可留痕的调用级拒绝在这里补一份运行错误诊断(返回串因此带 `详情:` 引用), +/// 命令边界的错误文本:可留痕的调用级拒绝在这里补一份运行错误诊断(文案里不带诊断引用), /// 其余只输出 [`DirectTurnError`] 的 `Display`。 /// /// 分层改成 typed 之前,这几条"宿主 / 环境事实"是在回合失败通道里被写进诊断的;分层之后它们不再 -/// 进那条通道,留痕与界面的 `详情:` 展开都在这里补回来。GUI 命令与 CLI 边界共用这一份,禁止在各自 -/// 边界再写一套判据;回合级失败已在上游写过诊断,这里直接放行。 +/// 进那条通道,留痕在这里补回来。GUI 命令与 CLI 边界共用这一份,禁止在各自边界再写一套判据; +/// 回合级失败已在上游写过诊断,这里直接放行。 pub(crate) fn direct_turn_error_boundary_text( root: &Path, client_turn_id: Option<&str>, @@ -2261,19 +2263,6 @@ pub(crate) fn direct_turn_error_boundary_text( record_direct_codex_failure(root, &failure, client_turn_id) } -fn persist_direct_codex_failure_context( - root: &Path, - client_turn_id: &str, - error: &str, -) -> Result<(), String> { - let item = direct_project_local_message_item( - "assistant", - error, - Some(&format!("direct-codex:{client_turn_id}:failure")), - )?; - append_direct_project_history_item_at(root, &item) -} - fn direct_taonier_art_generation_runtime_context( root: &Path, output_path: &str, @@ -4649,12 +4638,9 @@ async fn run_direct_game_creator_turn_at_with_creation_type_and_emitter( &failure, turn_emitter.map(|emitter| emitter.turn_id()), ); - if let Some(emitter) = turn_emitter { - // Persist the safe terminal projection so the next DirectProject - // turn can answer a diagnostic question from evidence instead of - // guessing or starting another playtest. - let _ = persist_direct_codex_failure_context(root, emitter.turn_id(), &error); - } + // TODO(Direct 命令接单化):失败说明本轮不写进项目历史——重进项目只会看到那条没有回复的 + // 用户消息,原因只在当轮界面与宿主诊断里。以后要给"进历史但不喂模型"的条目留一条通道 + // (见 `docs/adr/【ADR】DirectProject命令接单化-2026-09-23.md` 的备选方案第 3 条)。 if let Some(emitter) = turn_emitter { // 失败说明也是这一回合的内容:按出现顺序追加到回合流末尾, // 这样"流里已经是完整内容"这一点对失败回合同样成立。 @@ -7936,8 +7922,8 @@ mod tests { ); } - /// 可留痕的调用级拒绝(宿主 / 环境事实)在命令边界补一份运行错误诊断,返回串带 `详情:` 引用: - /// 这是分层改 typed 之前的行为,前端横幅的展开逻辑按这个引用工作。 + /// 可留痕的调用级拒绝(宿主 / 环境事实)在命令边界补一份运行错误诊断,但返回串不再带引用: + /// 界面不展开详情,线索只留在 `.agent/runtime/errors`、应用日志与错误上报池。 #[test] fn reportable_call_rejection_writes_a_diagnostic_at_the_boundary() { let parent = tempfile::tempdir().expect("temp dir"); @@ -7952,7 +7938,8 @@ mod tests { }, ); - assert!(text.contains("详情:.agent/runtime/errors/"), "{text}"); + assert!(text.contains("direct-codex-failure:v2"), "{text}"); + assert!(!text.contains("详情:"), "{text}"); let entries = std::fs::read_dir(root.join(".agent/runtime/errors")) .expect("runtime error directory") .filter_map(Result::ok) diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/user_input.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/user_input.rs index 65c44bb2a..98fcec77c 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/user_input.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/user_input.rs @@ -31,8 +31,8 @@ pub(crate) fn normalize_direct_client_turn_id( /// DirectProject 聊天命令:对外仍然是 `Result`。 /// /// 字符串只在这里生成一次;前端拿到的仍是"一句给用户看的话",而 Rust 侧从命令入口到宿主出口全程 -/// 只传 typed 错误。可留痕的调用级拒绝(宿主 / 环境事实)在这里补一份运行错误诊断,返回串因此带上 -/// `详情:` 引用——界面横幅的展开逻辑按这个引用工作。 +/// 只传 typed 错误。可留痕的调用级拒绝(宿主 / 环境事实)在这里补一份运行错误诊断,但返回串不再 +/// 带诊断引用——界面不展开详情,线索只在宿主侧。 /// // TODO(Direct 命令接单化,未实施):现在这个命令 await 整轮,于是"命令边界"承担了不属于它的角色—— // 回合失败的文案要靠这条 Err 回到界面,认证失败重试也只能挂在它上面。目标形状(草案见 diff --git a/apps/ai-game-creator-shell/src-tauri/src/commands.rs b/apps/ai-game-creator-shell/src-tauri/src/commands.rs index b5c09bb9d..896eb1773 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/commands.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/commands.rs @@ -5873,38 +5873,6 @@ pub(crate) async fn read_direct_project_conversation( .map_err(|error| format!("读取 DirectProject 历史后台任务失败:{error}"))? } -#[tauri::command] -pub(crate) async fn read_agent_runtime_error_detail( - project_path: String, - detail_ref: String, -) -> Result { - tauri::async_runtime::spawn_blocking(move || { - let root = Path::new(project_path.trim()); - enforce_project_permission_policy(root, "conversation.read")?; - let relative = detail_ref.trim(); - let Some(file_name) = relative.strip_prefix(".agent/runtime/errors/") else { - return Err("错误诊断引用不在项目错误目录内".to_string()); - }; - if file_name.is_empty() - || file_name.contains(['/', '\\']) - || file_name.contains("..") - || !file_name.ends_with(".json") - { - return Err("错误诊断引用格式无效".to_string()); - } - let path = root.join(relative); - prepare_game_creator_private_path_for_read(&path, false, "统一错误诊断")?; - let bytes = std::fs::read(&path).map_err(|error| format!("读取错误诊断失败:{error}"))?; - if bytes.len() > 16 * 1024 { - return Err("错误诊断超过读取上限".to_string()); - } - let text = String::from_utf8(bytes).map_err(|_| "错误诊断不是 UTF-8 文本".to_string())?; - Ok(redact_agent_runtime_error(root, &text, 16 * 1024)) - }) - .await - .map_err(|error| format!("读取统一错误诊断后台任务失败:{error}"))? -} - #[tauri::command] pub(crate) fn list_game_creator_direct_active_turns( ) -> Result, String> { diff --git a/apps/ai-game-creator-shell/src-tauri/src/main.rs b/apps/ai-game-creator-shell/src-tauri/src/main.rs index 9d5f1ae18..166b53646 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/main.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/main.rs @@ -2761,7 +2761,6 @@ fn main() { archive_game_creator_agent_session, read_local_conversation, read_direct_project_conversation, - read_agent_runtime_error_detail, list_game_creator_direct_active_turns, subscribe_direct_project_thread, consume_direct_project_thread, diff --git a/apps/ai-game-creator-shell/src/view/project-development/chat/controller/useDirectProjectChatController.ts b/apps/ai-game-creator-shell/src/view/project-development/chat/controller/useDirectProjectChatController.ts index dfcf38435..2fb54cb81 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/chat/controller/useDirectProjectChatController.ts +++ b/apps/ai-game-creator-shell/src/view/project-development/chat/controller/useDirectProjectChatController.ts @@ -610,22 +610,10 @@ export function useDirectProjectChatController({ } void captureAgentRuntimeError(error, DIRECT_CODEX_AGENT_ID); const message = error instanceof Error ? error.message : String(error); - let persistedDetail = ''; - const detailRef = message.match( - /详情:(\.agent\/runtime\/errors\/[^\s;]+)/, - )?.[1]; - if (detailRef) { - try { - persistedDetail = await invoke( - 'read_agent_runtime_error_detail', - { projectPath: nextProjectPath, detailRef }, - ); - } catch { - persistedDetail = ''; - } - } + // 不再展开诊断详情:文案里没有引用,前端也不去读那份文件。线索留在 + // `.agent/runtime/errors`、应用日志与错误上报池里,界面只显示这一句话。 const visibleMessage = projectRuntimeVisibleError( - persistedDetail ? `${message}\n\n${persistedDetail}` : message, + message, '陶泥儿智能创作', true, );