From 35ec04913826b5878eca430bbbcc36b12e33f47f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Tue, 1 Sep 2026 21:43:03 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E9=BD=90=20AGC=20Runtime=20=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E9=87=87=E9=9B=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rust Runtime 终态失败、预算耗尽和启动确认失败统一进入错误队列 Direct Codex 与专业 Agent 前台裸 invoke 补充采集并统一上下文去重 归一化 Codex app-server 错误类别并新增回归测试与技术方案 --- .../src-tauri/src/agent/runtime_state.rs | 3 + .../src-tauri/src/error_report/mod.rs | 2 +- .../src-tauri/src/error_report/queue.rs | 56 ++++++++++++++++++- apps/ai-game-creator-shell/src/App.tsx | 8 ++- .../src/services/errorReporting.ts | 8 +++ .../tests/errorReporting.test.ts | 16 ++++++ ...术方案】AGC错误报告与诊断上传-2026-08-31.md | 4 +- 7 files changed, 90 insertions(+), 7 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_state.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_state.rs index ff040770c..a866769af 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_state.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_state.rs @@ -1410,6 +1410,7 @@ pub(crate) fn fail_game_creator_agent_runtime_turn_at( state.next_step = "等待开发者处理失败".to_string(); let public_error = redact_agent_runtime_error(root, error, 500); state.error = Some(public_error.clone()); + let _ = crate::error_report::report_agent_runtime_error(&state.agent_id, error); // The public terminal message is deliberately committed before the // remaining Runtime projections. Even if a task/event/state write is the // failing subsystem, the user still receives one stable failure outcome. @@ -1469,6 +1470,7 @@ pub(crate) fn fail_game_creator_agent_runtime_budget_at( state.next_step = "调整任务范围后重试".to_string(); let public_error = redact_agent_runtime_error(root, error, 500); state.error = Some(public_error.clone()); + let _ = crate::error_report::report_agent_runtime_error(&state.agent_id, error); let public_status_result = append_game_creator_agent_runtime_terminal_public_message_at(root, &state, &public_error); write_non_terminal_isolated_child_cancel_tombstones_for_parent_at( @@ -3779,6 +3781,7 @@ pub(crate) fn fail_game_creator_agent_runtime_public_start_status_at( error: &str, ) -> Result<(), String> { let error = redact_agent_runtime_project_paths(root, error, 500); + let _ = crate::error_report::report_agent_runtime_error(&record.agent_id, &error); let failed_task = AgentRuntimeTaskRecord { status: "failed".to_string(), phase: "public-status-write-failed".to_string(), diff --git a/apps/ai-game-creator-shell/src-tauri/src/error_report/mod.rs b/apps/ai-game-creator-shell/src-tauri/src/error_report/mod.rs index 845ab6ede..1d3db7c14 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/error_report/mod.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/error_report/mod.rs @@ -5,4 +5,4 @@ mod sanitize; pub use commands::{ack_error_reports, get_pending_error_reports, report_client_error}; pub use notifications::initialize_notifications; -pub use queue::report_diagnostic_error; +pub use queue::{report_agent_runtime_error, report_diagnostic_error}; diff --git a/apps/ai-game-creator-shell/src-tauri/src/error_report/queue.rs b/apps/ai-game-creator-shell/src-tauri/src/error_report/queue.rs index 42c28093c..ea6736c47 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/error_report/queue.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/error_report/queue.rs @@ -48,6 +48,33 @@ fn now() -> String { format!("{seconds}") } +fn stable_codex_app_server_error_message(message: &str) -> Option { + for marker in ["kind=codex-app-server-", "codex-app-server-error:"] { + let Some(start) = message.find(marker) else { + continue; + }; + let kind_start = start + marker.len(); + let kind = message[kind_start..] + .chars() + .take_while(|character| character.is_ascii_lowercase() || *character == '-') + .collect::(); + if !kind.is_empty() { + return Some(format!("codex-app-server-error:{kind}")); + } + } + None +} + +pub fn report_agent_runtime_error(agent_id: &str, error: &str) -> Option { + report_diagnostic_error( + "agent-runtime", + error, + None, + Some("agent-runtime"), + Some(agent_id), + ) +} + pub fn report_diagnostic_error( source: &str, message: &str, @@ -71,10 +98,12 @@ pub fn report_diagnostic_error( }) }); let source = sanitize(source, MAX_SOURCE_CHARS); - let message = sanitize(message, MAX_MESSAGE_CHARS); - let stack = stack.map(|value| sanitize(value, MAX_STACK_CHARS)); let action = action.map(|value| sanitize(value, MAX_ACTION_CHARS)); let page = page.map(|value| sanitize(value, MAX_PAGE_CHARS)); + let sanitized_message = sanitize(message, MAX_MESSAGE_CHARS); + let message = + stable_codex_app_server_error_message(&sanitized_message).unwrap_or(sanitized_message); + let stack = stack.map(|value| sanitize(value, MAX_STACK_CHARS)); if message.trim().is_empty() { return None; } @@ -168,7 +197,7 @@ pub(crate) fn reset_for_tests() { #[cfg(test)] mod tests { - use super::{report_diagnostic_error, reset_for_tests, snapshot}; + use super::{report_agent_runtime_error, report_diagnostic_error, reset_for_tests, snapshot}; #[test] fn merges_events_by_call_site_without_storing_second_queue() { @@ -199,4 +228,25 @@ mod tests { assert!(report_diagnostic_error("test", "\n", None, None, None).is_none()); assert!(snapshot().is_empty()); } + + #[test] + fn canonicalizes_codex_app_server_public_summaries_and_deduplicates_frontend_capture() { + reset_for_tests(); + let runtime = report_agent_runtime_error( + "project-supervisor", + "runtime 调用 LLM 失败:kind=codex-app-server-other fingerprint=deadbeef chars=42", + ) + .expect("runtime event"); + let frontend = report_diagnostic_error( + "agent-runtime", + "codex-app-server-error:other", + None, + Some("agent-runtime"), + Some("project-supervisor"), + ) + .expect("frontend event"); + assert_eq!(runtime.event_id, frontend.event_id); + assert_eq!(snapshot()[0].count, 2); + assert_eq!(snapshot()[0].message, "codex-app-server-error:other"); + } } diff --git a/apps/ai-game-creator-shell/src/App.tsx b/apps/ai-game-creator-shell/src/App.tsx index e1a4f2cfe..3c6c9a316 100644 --- a/apps/ai-game-creator-shell/src/App.tsx +++ b/apps/ai-game-creator-shell/src/App.tsx @@ -237,7 +237,10 @@ import { ProjectSupervisorView } from './features/project-workspace/ProjectSuper import { ProjectWorkspaceChatPane } from './features/project-workspace/ProjectWorkspaceChatPane'; import { SupervisorChatOnlyView } from './features/project-workspace/SupervisorChatOnlyView'; import { RuntimeConfigDialog } from './features/runtime-config/RuntimeConfigDialog'; -import { invokeDiagnostic } from './services/errorReporting'; +import { + captureAgentRuntimeError, + invokeDiagnostic, +} from './services/errorReporting'; import type { HomeCreationType } from './view/home'; import { type ProjectAgentResultSummary, @@ -3388,6 +3391,7 @@ export function App({ ); setCommandLog((current) => [...current, 'conversation.write']); } catch (error) { + void captureAgentRuntimeError(error, agent.id); if (agentConversationLoadVersionRef.current !== saveVersion) { return; } @@ -5541,6 +5545,7 @@ export function App({ } return; } + void captureAgentRuntimeError(error, PROJECT_SUPERVISOR_AGENT_ID); const message = error instanceof Error ? error.message : String(error); const visibleMessage = projectRuntimeVisibleError( @@ -5746,6 +5751,7 @@ export function App({ ); } } catch (error) { + void captureAgentRuntimeError(error, PROJECT_SUPERVISOR_AGENT_ID); if (localProjectPathRef.current !== nextProjectPath) { return; } diff --git a/apps/ai-game-creator-shell/src/services/errorReporting.ts b/apps/ai-game-creator-shell/src/services/errorReporting.ts index 93944b478..5f8ed9bed 100644 --- a/apps/ai-game-creator-shell/src/services/errorReporting.ts +++ b/apps/ai-game-creator-shell/src/services/errorReporting.ts @@ -75,6 +75,14 @@ export async function captureClientError( }).catch(() => undefined); } +export function captureAgentRuntimeError(error: unknown, agentId: string) { + return captureClientError(error, { + source: 'agent-runtime', + action: 'agent-runtime', + page: agentId, + }); +} + function formatConsoleArgument(value: unknown) { if (value instanceof Error) return value.stack || value.message; if (typeof value === 'string') return value; diff --git a/apps/ai-game-creator-shell/tests/errorReporting.test.ts b/apps/ai-game-creator-shell/tests/errorReporting.test.ts index 6211c57b7..7554032b0 100644 --- a/apps/ai-game-creator-shell/tests/errorReporting.test.ts +++ b/apps/ai-game-creator-shell/tests/errorReporting.test.ts @@ -81,6 +81,7 @@ import { invoke } from '@tauri-apps/api/core'; import { fetchClientHttp } from '../src/services/clientHttp'; import { + captureAgentRuntimeError, captureClientError, getPendingClientErrorEvents, installWebviewLogBridge, @@ -110,6 +111,21 @@ describe('客户端错误报告池', () => { expect((await getPendingClientErrorEvents())[0]?.count).toBe(2); }); + it('使用统一上下文采集 Agent Runtime 错误', async () => { + await captureAgentRuntimeError( + new Error('kind=codex-app-server-other fingerprint=dynamic chars=12'), + 'project-supervisor', + ); + + expect(invoke).toHaveBeenCalledWith('report_client_error', { + source: 'agent-runtime', + message: 'kind=codex-app-server-other fingerprint=dynamic chars=12', + stack: expect.any(String), + action: 'agent-runtime', + page: 'project-supervisor', + }); + }); + it('限制当前进程错误池最多保留 100 条', async () => { for (let index = 0; index < 101; index += 1) { await captureClientError(new Error(`错误 ${index}`), { source: 'test' }); diff --git a/docs/technical/【技术方案】AGC错误报告与诊断上传-2026-08-31.md b/docs/technical/【技术方案】AGC错误报告与诊断上传-2026-08-31.md index 23213fb77..ad49e6c68 100644 --- a/docs/technical/【技术方案】AGC错误报告与诊断上传-2026-08-31.md +++ b/docs/technical/【技术方案】AGC错误报告与诊断上传-2026-08-31.md @@ -6,13 +6,13 @@ AI Game Creator Shell 采用 IDEA 风格的当前进程错误报告:错误事 ## 客户端 -- 捕获 React render error、`window.onerror`、`unhandledrejection` 以及显式标记的 Tauri/API/Agent 错误。 +- 捕获 React render error、`window.onerror`、`unhandledrejection` 以及显式标记的 Tauri/API/Agent 错误。Agent Runtime 的终态失败、预算耗尽和启动确认失败由 Rust 失败投影统一入池;Direct Codex 与专业 Agent 的前台裸 Tauri invoke catch 作为补充入口,重复事件由同一 fingerprint 合并,主动取消和“同一 turn 已在运行”不作为错误采集。 - 事件字段包括 eventId、fingerprint、source、message、stack、时间和次数;重复事件合并。不再携带 severity、errorCode、page、action、requestId 等无法稳定关联的字段。 - 指纹计算可使用调用方的 page/action 及脱敏后的首个调用点作为进程内区分输入,但这些上下文不会作为事件字段上传;消息与 stack 在入池前统一脱敏,WebCrypto 失败时降级为稳定可读指纹,采集本身不得产生新的未处理拒绝。 - 客户端 API 自动采集只覆盖网络错误、408 和 5xx;预期的 4xx 登录/鉴权失败不进入错误报告池。 - Rust 侧通过 `app_log!` 将普通文本日志同时输出到 stderr 和 AppData `diagnostics/application.log`,超出 256 KiB 滚动到 `application.previous.log`;WebView 的 console 输出通过 `append_application_log` 镜像到同一 raw log,并在客户端桥接处再次脱敏;`read_diagnostic_logs` 只读取应用级日志。 - 报告面板只由自动诊断通知中的“查看并报告”打开,不提供聊天命令、崩溃页按钮或其他手动入口;默认选中最新事件,其他事件可勾选。允许填写最多 2,000 字中文描述并取消日志附件;本版本不支持截图或任意文件附件。 -- Rust 是结构化错误队列的唯一真相源:`src-tauri/src/error_report/` 负责脱敏、调用点指纹、eventId、计数、100 条上限、5 秒聚合和 ack 生命周期;WebView 仅通过 Tauri bridge 上报、读取快照并保存短暂 React 展示状态,不维护第二份事件 Map。Rust emit 只作为无状态唤醒,携带单调递增的 generation,不携带错误正文或事件 ID。 +- Rust 是结构化错误队列的唯一真相源:`src-tauri/src/error_report/` 负责脱敏、调用点指纹、eventId、计数、100 条上限、5 秒聚合和 ack 生命周期;WebView 仅通过 Tauri bridge 上报、读取快照并保存短暂 React 展示状态,不维护第二份事件 Map。Runtime 错误上报统一使用 `source=agent-runtime`、`action=agent-runtime` 和 Agent ID 作为 page;包含 `kind=codex-app-server-*` 或 `codex-app-server-error:*` 的消息在入池前归一为稳定类别(例如 `codex-app-server-error:other`),不把 public summary 中的动态 fingerprint/长度作为分桶输入。Rust emit 只作为无状态唤醒,携带单调递增的 generation,不携带错误正文或事件 ID。 - 错误事件先在当前进程内存池按 fingerprint 合并,经过 5 秒聚合后只发出一次非阻塞存在性唤醒;WebView 挂载、收到唤醒、重新获得焦点或恢复可见时都查询完整未 ack 快照,并在桥接暂时失败时做有限退避重试。通知支持“查看并报告”和“忽略”,同一 fingerprint 仅在新增时唤醒一次。通知不直接打开阻塞式报告面板。忽略只关闭当前 UI,不删除事件;提交成功后由 bridge ack/delete 选中事件。 - 上传失败只在当前进程显示失败并允许用户再次提交,不跨重启恢复事件池,不后台自动重试。