diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_error.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_error.rs index 3f67baed8..891fff007 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_error.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_error.rs @@ -23,6 +23,12 @@ pub(crate) const AGENT_RUNTIME_ERROR_MAX_DETAIL_CHARS: usize = 8 * 1024; pub(crate) const AGENT_RUNTIME_ERROR_APP_LOG_DETAIL_CHARS: usize = 1_200; pub(crate) const AGENT_RUNTIME_ERROR_APP_LOG_METADATA_CHARS: usize = 200; +/// 身份行里 public summary 的字符预算。 +/// +/// 身份行必须短小:`summary` 由调用方给,`direct_tool_bridge` 传的是工具错误原文, +/// 所以这里与 sidecar 侧的摘要预算同口径(320 字符)截断,并再脱敏一次。 +pub(crate) const AGENT_RUNTIME_ERROR_APP_LOG_SUMMARY_CHARS: usize = 320; + static ERROR_EVENT_SEQUENCE: AtomicU64 = AtomicU64::new(1); #[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] @@ -128,8 +134,10 @@ pub(crate) fn persist_agent_runtime_error( /// 把统一错误事件投影成 AppData `diagnostics/application.log` 里的两行。 /// -/// 传进来的 `detail` 已经是 sidecar 用的脱敏文本;这里只再按应用日志的预算截一次, -/// 让日志与项目内 sidecar 是同一份诊断,不再单独拼一套字段。 +/// 传进来的 `detail` 是 sidecar 用的脱敏文本,`metadata` 则从未脱敏过。两行落到 `app_log!` +/// 时都会先按应用日志预算(1200 / 200 字符)再脱敏、再截断:`app_log!` 还会把同一行写到 +/// stderr,那里没有 `sanitize_diagnostic_message` 兜底,所以每个调用方给的外来文本 +/// (`summary` 按 320 字符预算)都在这里过一遍脱敏。 /// /// 拆成「身份行 + 详情行」是因为整行只要出现凭据标记就会被 /// [`crate::sanitize_diagnostic_message`] 整体替换成脱敏占位,详情行可能因此消失; @@ -159,7 +167,11 @@ pub(crate) fn agent_runtime_error_app_log_lines( let stage = single_line_log_field(stage); let code = single_line_log_field(code); let detail_ref = single_line_log_field(detail_ref); - let public_text = single_line_log_field(public_text); + let public_text = single_line_log_field(&redact_agent_runtime_error( + root, + public_text, + AGENT_RUNTIME_ERROR_APP_LOG_SUMMARY_CHARS, + )); let elapsed_ms = elapsed_ms .map(|value| value.to_string()) .unwrap_or_else(|| "none".to_string()); diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index fc72b0b57..5c2046cdc 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -9177,7 +9177,7 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在 ## 2026-09-21 统一错误事件额外投影到 AppData 应用日志 - 背景:`direct-codex-failure:v2 ... 详情:.agent/runtime/errors/error--9.json`(例如 `stage=code-generation code=turn-idle-timeout`)里的诊断正文只落在项目目录,而“报告问题”只上传 AppData `diagnostics/application.log`;用户提交上来的失败消息因此只有一个指向项目文件的引用,团队复现不到 stderr 摘要、退出状态这些真因。 -- 决策:统一错误写入边界 `agent/runtime_error.rs` 在落 `.agent/runtime/errors/.json` 之前先把同一份已脱敏诊断投影成应用日志两行——`agent.runtime.error`(身份行:eventId / source / stage / code / retryable / clientTurnId / elapsedMs / detailRef / summary)与 `agent.runtime.error.detail`(详情行:hint / detail / metadata)。字段仍只从 sidecar 那份 diagnosis 来,不新增第二份来源;detail / metadata 先按应用日志预算(1200 / 200 字符)脱敏截断,自由文本先压平换行(`app_log!` 同时写 stderr,那里没人压行)。 +- 决策:统一错误写入边界 `agent/runtime_error.rs` 在落 `.agent/runtime/errors/.json` 之前先把同一份已脱敏诊断投影成应用日志两行——`agent.runtime.error`(身份行:eventId / source / stage / code / retryable / clientTurnId / elapsedMs / detailRef / summary)与 `agent.runtime.error.detail`(详情行:hint / detail / metadata)。字段仍只从 sidecar 那份 diagnosis 来,不新增第二份来源;落盘前 detail / metadata 按应用日志预算(1200 / 200 字符)脱敏截断,身份行的 `summary` 也按 320 字符预算脱敏截断(`direct_tool_bridge` 会把它当自由文本传工具错误原文,而 `app_log!` 同时写 stderr,那里没有 `sanitize_diagnostic_message` 兜底),自由文本先压平换行。 - 决策(两行而不是一行):整行一旦命中 `sanitize_diagnostic_message` 的凭据标记(token / bearer / authorization / credential / api key)会被整体替换成 ``;拆开后详情行即使被吃掉,身份行仍能定位 eventId 与 detailRef。 - 边界:进程内错误报告事件池(`error_report`)与项目内 `.agent/runtime/errors/.json` sidecar 是两套东西——本次只把 sidecar 的同一份诊断作为**应用日志行**落盘,不进事件池、不改报告上传协议、不改 `read_agent_runtime_error_detail` 详情入口。 - 影响面:只改 `apps/ai-game-creator-shell/src-tauri/src/agent/runtime_error.rs` 这一处写入边界(覆盖 direct-codex / agc-tools / agent-runtime 三类来源);sidecar schema、对话投影、前端详情入口与错误报告协议均未改动。 diff --git a/docs/technical/【技术方案】AGC错误报告与诊断上传-2026-08-31.md b/docs/technical/【技术方案】AGC错误报告与诊断上传-2026-08-31.md index d13a14f72..1eeed278b 100644 --- a/docs/technical/【技术方案】AGC错误报告与诊断上传-2026-08-31.md +++ b/docs/technical/【技术方案】AGC错误报告与诊断上传-2026-08-31.md @@ -13,7 +13,7 @@ AI Game Creator Shell 采用 IDEA 风格的当前进程错误报告:错误事 - 指纹计算可使用调用方的 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` 只读取应用级日志。 -- 这里有两套互不相干的东西,不要互相代入:**错误报告事件池**是进程内 `error_report` 的结构化事件(本次变更不动它,仍然只在内存里、提交时才生成 `events.jsonl`);**统一 Agent Runtime 错误事件**是项目内 sidecar `.agent/runtime/errors/.json`,既不进事件池也不进报告包。因为报告包里的日志附件只有 AppData 应用日志,所以 sidecar 的同一份已脱敏诊断再作为**日志行**(不是报告事件)投影成两行:`agent.runtime.error`(身份行:eventId / source / stage / code / retryable / clientTurnId / elapsedMs / detailRef / summary)与 `agent.runtime.error.detail`(详情行:hint / detail / metadata)。两行都由 `agent/runtime_error.rs` 从同一份 diagnosis 生成,detail 与 metadata 各按应用日志预算截断,不新增字段来源;详情行可能因整行凭据标记被整体脱敏,身份行保证事件仍可定位。 +- 这里有两套互不相干的东西,不要互相代入:**错误报告事件池**是进程内 `error_report` 的结构化事件(本次变更不动它,仍然只在内存里、提交时才生成 `events.jsonl`);**统一 Agent Runtime 错误事件**是项目内 sidecar `.agent/runtime/errors/.json`,既不进事件池也不进报告包。因为报告包里的日志附件只有 AppData 应用日志,所以 sidecar 的同一份已脱敏诊断再作为**日志行**(不是报告事件)投影成两行:`agent.runtime.error`(身份行:eventId / source / stage / code / retryable / clientTurnId / elapsedMs / detailRef / summary)与 `agent.runtime.error.detail`(详情行:hint / detail / metadata)。两行都由 `agent/runtime_error.rs` 从同一份 diagnosis 生成,不新增字段来源;落到日志前 detail / metadata 各按应用日志预算(1200 / 200 字符)脱敏截断,身份行里的 `summary` 也按 320 字符预算脱敏截断(该字段由调用方给,`direct_tool_bridge` 会传工具错误原文,而 `app_log!` 同时把整行写 stderr,那里没有 `sanitize_diagnostic_message` 兜底)。详情行可能因整行凭据标记被整体脱敏,身份行保证事件仍可定位。 - 报告面板只由自动诊断通知中的“查看并报告”打开,不提供聊天命令、崩溃页按钮或其他手动入口;默认选中当前快照中的全部事件,用户可取消不想提交的事件。允许填写最多 2,000 字中文描述并取消日志附件;本版本不支持截图或任意文件附件。 - 报告面板读取当前错误快照失败时,必须明确显示“错误事件暂不可用,请关闭后重试”,不能把失败误显示为“当前没有待报告的错误”。 - 通知中的“查看并报告”打开面板时必须保留该次通知快照;最新快照读取瞬时失败时使用这份 fallback 继续展示和提交,不能因先清空通知而丢失用户刚看到的事件。