Merge pull request '统一错误事件同步留痕到 AppData 应用日志' (#453) from feat/log-the-dianosis into master
Project CI / AI game creator shell Rust crates (push) Successful in 2m57s
Project CI / AI game creator shell Rust smoke (push) Successful in 3m46s
Project CI / Backend tests (push) Successful in 5m16s
Project CI / AI game creator shell Rust lane 2/2 (push) Successful in 8m57s
Project CI / Native shell tests (push) Successful in 6m36s
Project CI / AI game creator shell Rust lane 1/2 (push) Successful in 10m24s
Project CI / Repository checks (push) Successful in 2m37s
Project CI / Frontend tests (push) Successful in 3m54s
Project CI / AI game creator shell web tests (push) Successful in 2m15s
Project CI / AI game creator shell Rust crates (push) Successful in 2m57s
Project CI / AI game creator shell Rust smoke (push) Successful in 3m46s
Project CI / Backend tests (push) Successful in 5m16s
Project CI / AI game creator shell Rust lane 2/2 (push) Successful in 8m57s
Project CI / Native shell tests (push) Successful in 6m36s
Project CI / AI game creator shell Rust lane 1/2 (push) Successful in 10m24s
Project CI / Repository checks (push) Successful in 2m37s
Project CI / Frontend tests (push) Successful in 3m54s
Project CI / AI game creator shell web tests (push) Successful in 2m15s
Reviewed-on: #453
This commit was merged in pull request #453.
This commit is contained in:
@@ -2,7 +2,8 @@
|
||||
//!
|
||||
//! Every caller supplies a safe public summary and a private detail. This
|
||||
//! module is the only persistence boundary for the latter: it redacts project
|
||||
//! paths and credentials before writing a bounded diagnostic sidecar.
|
||||
//! paths and credentials before writing a bounded diagnostic sidecar, and
|
||||
//! projects the same bounded diagnosis into the AppData application log.
|
||||
|
||||
use super::{redact_agent_runtime_error, write_agent_runtime_json_sidecar_with_max_bytes};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -14,6 +15,20 @@ use std::time::{SystemTime, UNIX_EPOCH};
|
||||
pub(crate) const AGENT_RUNTIME_ERROR_SCHEMA_VERSION: &str = "agent-runtime-error.v1";
|
||||
pub(crate) const AGENT_RUNTIME_ERROR_MAX_DETAIL_CHARS: usize = 8 * 1024;
|
||||
|
||||
/// 应用日志里 detail / metadata 的字符预算。
|
||||
///
|
||||
/// `application.log` 的每一行在落盘前还会被 `sanitize_diagnostic_message` 截到 2 KiB,
|
||||
/// 这里的预算留出身份字段与中文摘要的位置,保证被截掉的是诊断正文的尾部,而不是
|
||||
/// `eventId`、`code` 或 `detailRef`。
|
||||
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)]
|
||||
@@ -71,6 +86,27 @@ pub(crate) fn persist_agent_runtime_error(
|
||||
"detail": safe_detail,
|
||||
"metadata": metadata,
|
||||
});
|
||||
// 统一错误事件的项目内 sidecar 只在项目目录可见:用户提交错误报告时上传的是 AppData
|
||||
// 应用日志,诊断包拿不到 detail。这里先把同一份已脱敏诊断留进应用日志,再落项目文件,
|
||||
// 于是 sidecar 写失败也仍然留下可提交的诊断。
|
||||
let app_log_lines = agent_runtime_error_app_log_lines(
|
||||
root,
|
||||
&event_id,
|
||||
client_turn_id,
|
||||
source,
|
||||
stage,
|
||||
code,
|
||||
retryable,
|
||||
public_text,
|
||||
recovery_hint,
|
||||
&detail_ref,
|
||||
&safe_detail,
|
||||
elapsed_ms,
|
||||
&metadata,
|
||||
);
|
||||
for line in app_log_lines {
|
||||
app_log!("{line}");
|
||||
}
|
||||
write_agent_runtime_json_sidecar_with_max_bytes(
|
||||
root,
|
||||
&detail_ref,
|
||||
@@ -96,6 +132,78 @@ pub(crate) fn persist_agent_runtime_error(
|
||||
})
|
||||
}
|
||||
|
||||
/// 把统一错误事件投影成 AppData `diagnostics/application.log` 里的两行。
|
||||
///
|
||||
/// 传进来的 `detail` 是 sidecar 用的脱敏文本,`metadata` 则从未脱敏过。两行落到 `app_log!`
|
||||
/// 时都会先按应用日志预算(1200 / 200 字符)再脱敏、再截断:`app_log!` 还会把同一行写到
|
||||
/// stderr,那里没有 `sanitize_diagnostic_message` 兜底,所以每个调用方给的外来文本
|
||||
/// (`summary` 按 320 字符预算)都在这里过一遍脱敏。
|
||||
///
|
||||
/// 已经脱敏过的 `detail` 也照走同一遍流水线,不按「调用方已脱敏」走短路:截断会把
|
||||
/// `[redacted-secret]` 这类标记切开,而且这里是 `pub(crate)` 边界,不假设未来调用方一定先脱敏。
|
||||
///
|
||||
/// 拆成「身份行 + 详情行」是因为整行只要出现凭据标记就会被
|
||||
/// [`crate::sanitize_diagnostic_message`] 整体替换成脱敏占位。因此身份行**只放程序生成或
|
||||
/// 调用方常量字段**(eventId / source / stage / code / retryable / clientTurnId / elapsedMs /
|
||||
/// detailRef),`summary`、`hint` 这些自由文本全部放详情行:自由文本里一个裸词
|
||||
/// (例如 `credential`)就能让整行被替换,放错了就会把事件定位信息一起吃掉。
|
||||
///
|
||||
/// 单行口径在这里落地:`app_log!` 同时把这行写到 stderr,那里没有人替我们压行,
|
||||
/// 所以每个字段(含 `source` / `stage` / `code` / `detailRef` 这些调用方给的标识)都先
|
||||
/// 过一遍 [`single_line_log_field`],不假设调用方一定给单行文本。
|
||||
pub(crate) fn agent_runtime_error_app_log_lines(
|
||||
root: &Path,
|
||||
event_id: &str,
|
||||
client_turn_id: Option<&str>,
|
||||
source: &str,
|
||||
stage: &str,
|
||||
code: &str,
|
||||
retryable: bool,
|
||||
public_text: &str,
|
||||
recovery_hint: &str,
|
||||
detail_ref: &str,
|
||||
detail: &str,
|
||||
elapsed_ms: Option<u64>,
|
||||
metadata: &Value,
|
||||
) -> [String; 2] {
|
||||
let event_id = single_line_log_field(event_id);
|
||||
let client_turn_id = single_line_log_field(client_turn_id.unwrap_or("none"));
|
||||
let source = single_line_log_field(source);
|
||||
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(&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());
|
||||
let identity = format!(
|
||||
"agent.runtime.error eventId={event_id} source={source} stage={stage} code={code} retryable={retryable} clientTurnId={client_turn_id} elapsedMs={elapsed_ms} detailRef={detail_ref}"
|
||||
);
|
||||
let detail = redact_agent_runtime_error(root, detail, AGENT_RUNTIME_ERROR_APP_LOG_DETAIL_CHARS);
|
||||
let metadata = redact_agent_runtime_error(
|
||||
root,
|
||||
&metadata.to_string(),
|
||||
AGENT_RUNTIME_ERROR_APP_LOG_METADATA_CHARS,
|
||||
);
|
||||
let detail_line = format!(
|
||||
"agent.runtime.error.detail eventId={event_id} hint={} summary={} detail={} metadata={}",
|
||||
single_line_log_field(recovery_hint),
|
||||
single_line_log_field(&public_text),
|
||||
single_line_log_field(&detail),
|
||||
single_line_log_field(&metadata),
|
||||
);
|
||||
[identity, detail_line]
|
||||
}
|
||||
|
||||
/// 应用日志是逐行读取的:落到日志里的自由文本必须先压平换行。
|
||||
fn single_line_log_field(value: &str) -> String {
|
||||
value.replace(['\r', '\n'], " ")
|
||||
}
|
||||
|
||||
pub(crate) fn classify_direct_codex_error(error: &str) -> &'static str {
|
||||
let normalized = error.to_ascii_lowercase();
|
||||
if normalized.contains("等待 turn/completed 超时") {
|
||||
@@ -153,6 +261,91 @@ mod tests {
|
||||
assert!(!text.contains("token=secret"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_event_app_log_lines_keep_identity_and_redact_detail() {
|
||||
let parent = tempfile::tempdir().expect("temp root");
|
||||
let root = parent.path().join("project");
|
||||
crate::project::init_local_game_project_at(&root, "runtime-error", "错误事件")
|
||||
.expect("init project");
|
||||
// 生产路径传进来的是已经脱敏的 safe_detail(8 KiB 口径),这里按同一口径造输入;
|
||||
// metadata 在生产里从未脱敏,仍按原文传。
|
||||
let safe_detail = redact_agent_runtime_error(
|
||||
&root,
|
||||
"C:\\Users\\private\\project https://provider.example/a?token=secret\n第二行诊断",
|
||||
AGENT_RUNTIME_ERROR_MAX_DETAIL_CHARS,
|
||||
);
|
||||
let lines = agent_runtime_error_app_log_lines(
|
||||
&root,
|
||||
"error-1-1",
|
||||
Some("turn-123"),
|
||||
"direct-codex",
|
||||
"code-generation",
|
||||
"turn-idle-timeout",
|
||||
true,
|
||||
"本轮没有收到完成事件\n附带换行",
|
||||
"查看诊断后重试",
|
||||
".agent/runtime/errors/error-1-1.json",
|
||||
&safe_detail,
|
||||
Some(1200),
|
||||
&serde_json::json!({"authorization": "Bearer secret"}),
|
||||
);
|
||||
for line in &lines {
|
||||
assert!(!line.contains('\n'), "{line}");
|
||||
}
|
||||
// 落盘边界按真实口径核验:应用日志逐行脱敏、逐行截断,两行不会先拼在一起。
|
||||
let persisted = lines
|
||||
.iter()
|
||||
.map(|line| crate::sanitize_diagnostic_message(line, None))
|
||||
.collect::<Vec<_>>();
|
||||
for line in &persisted {
|
||||
assert!(!line.contains("token=secret"), "{line}");
|
||||
assert!(line.chars().count() <= 2_048, "{line}");
|
||||
}
|
||||
let identity = &persisted[0];
|
||||
assert!(identity.contains("eventId=error-1-1"), "{identity}");
|
||||
assert!(identity.contains("code=turn-idle-timeout"), "{identity}");
|
||||
assert!(
|
||||
identity.contains("detailRef=.agent/runtime/errors/error-1-1.json"),
|
||||
"{identity}"
|
||||
);
|
||||
// 身份行只放程序生成或调用方常量字段:自由文本放错行会让「整行命中标记」把它吃掉。
|
||||
assert!(!identity.contains("summary="), "{identity}");
|
||||
let detail = &persisted[1];
|
||||
assert!(
|
||||
detail.contains("summary=本轮没有收到完成事件 附带换行"),
|
||||
"{detail}"
|
||||
);
|
||||
assert!(
|
||||
detail.contains("detail=<absolute-path> <redacted-url> 第二行诊断"),
|
||||
"{detail}"
|
||||
);
|
||||
assert!(!detail.contains("Bearer secret"), "{detail}");
|
||||
|
||||
// 自由文本里出现裸标记词时,被整行替换的只能是详情行:身份行仍可定位事件。
|
||||
let marked = agent_runtime_error_app_log_lines(
|
||||
&root,
|
||||
"error-3-1",
|
||||
None,
|
||||
"agc-tools",
|
||||
"tool-execution",
|
||||
"tool-error",
|
||||
true,
|
||||
"credential rotation failed",
|
||||
"查看项目错误诊断后处理",
|
||||
".agent/runtime/errors/error-3-1.json",
|
||||
&safe_detail,
|
||||
None,
|
||||
&serde_json::json!({"tool": "agc_tools"}),
|
||||
);
|
||||
let identity = crate::sanitize_diagnostic_message(&marked[0], None);
|
||||
assert!(identity.contains("eventId=error-3-1"), "{identity}");
|
||||
assert!(identity.contains("code=tool-error"), "{identity}");
|
||||
assert_eq!(
|
||||
crate::sanitize_diagnostic_message(&marked[1], None),
|
||||
"<sensitive diagnostic details redacted>"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn timeout_and_tool_errors_have_distinct_codes() {
|
||||
assert_eq!(
|
||||
|
||||
@@ -9294,6 +9294,15 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
|
||||
- 验证:定向 `projectResourceLiveIntegration`(32 条,三条断言面板留在失败态的用例按新口径改写为「重开面板再重试,身份不变」)、`resourceCanvasQuickEditDraft`(10 条)、`resourceCanvasFloatingDismiss`(18 条)全绿;`npm --prefix apps/ai-game-creator-shell run typecheck` 通过。
|
||||
- 合并前复核(2026-09-21):合并 master 后按**仓库根**跑全量 `npx vitest run`,**373 个测试文件全过、4512 通过 / 34 跳过 / 0 失败**;PR #419 显示 `No Conflicts`。真实客户端观感与远程 CI 未复验(后者按用户要求不追,runner/镜像问题见 Issue #431)。
|
||||
|
||||
## 2026-09-21 统一错误事件额外投影到 AppData 应用日志
|
||||
|
||||
- 背景:`direct-codex-failure:v2 ... 详情:.agent/runtime/errors/error-<id>-9.json`(例如 `stage=code-generation code=turn-idle-timeout`)里的诊断正文只落在项目目录,而“报告问题”只上传 AppData `diagnostics/application.log`;用户提交上来的失败消息因此只有一个指向项目文件的引用,团队复现不到 stderr 摘要、退出状态这些真因。
|
||||
- 决策:统一错误写入边界 `agent/runtime_error.rs` 在落 `.agent/runtime/errors/<eventId>.json` 之前先把同一份已脱敏诊断投影成应用日志两行——`agent.runtime.error`(身份行:eventId / source / stage / code / retryable / clientTurnId / elapsedMs / detailRef)与 `agent.runtime.error.detail`(详情行:hint / summary / detail / metadata)。字段仍只从 sidecar 那份 diagnosis 来,不新增第二份来源;落盘前 summary 按 320 字符、detail / metadata 按(1200 / 200 字符)预算脱敏截断(`direct_tool_bridge` 会把它当自由文本传工具错误原文,而 `app_log!` 同时写 stderr,那里没有 `sanitize_diagnostic_message` 兜底),自由文本先压平换行。
|
||||
- 决策(两行而不是一行):整行一旦命中 `sanitize_diagnostic_message` 的凭据标记(token / bearer / authorization / credential / api key)会被整体替换成 `<sensitive diagnostic details redacted>`;拆开后详情行即使被吃掉,身份行仍能定位 eventId 与 detailRef。后续复核发现:裸词标记(例如 `credential rotation failed`)脱敏消不掉,summary 留在身份行时仍会连 eventId 一起被替换,因此 summary / hint 等自由文本一律只放详情行,身份行只留程序生成与调用方常量字段。
|
||||
- 边界:进程内错误报告事件池(`error_report`)与项目内 `.agent/runtime/errors/<eventId>.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、对话投影、前端详情入口与错误报告协议均未改动。
|
||||
- 验证:`cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml runtime_error`(7 passed,含新增 `error_event_app_log_lines_keep_identity_and_redact_detail`)与 `-- direct_runtime direct_tool_bridge`(131 passed)全绿;`cargo fmt --check`、`npm run check:encoding`、`git diff --check` 通过。
|
||||
|
||||
## 2026-09-21 画布生成面板:重开草稿、重试身份与提交载荷全部收敛到一份 canonical content
|
||||
|
||||
- 背景:DirectProject composer 收口到 `content[]` 之后,栏目画布的图片类生成浮层仍留着第二套形状——面板里 `prompt` + `references` 两份 state,重开草稿 `ResourceCanvasAssetGenerationPanelDraft` 与重试身份 `boundRequestRef` 也是 `prompt + 参考身份`,`directCodexContentToLegacyContentDto` 因此在面板渲染、关闭草稿、重试判据、宿主入队四个边界各算一遍;同时草稿用「正文 + 引用列表」两条线重建编辑器输入,`@显示名` 与引用 chip 会重复成 `@素材-a@素材-a`。
|
||||
|
||||
@@ -5938,3 +5938,7 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/`
|
||||
- **原因**:校验器对每个 `input_text` 单独执行 `trim().is_empty()` 并立即拒绝,混淆了结构化片段合法性和整条消息是否有实际内容。
|
||||
- **处理(现行口径)**:`input_text` 允许空字符串、空格和换行,校验过程保持全部片段的原文、分段与顺序,不做合并或删除;遍历完整条消息后,只在既没有非空白文字、也没有任意非文本 part(素材引用 / 运行画面引用 / Skill 引用 / 附件引用)时返回“聊天内容不能为空”。各类引用仍逐个执行原有校验,消息带正文也不能绕过非法引用。
|
||||
- **关联**:`apps/ai-game-creator-shell/src-tauri/src/agent/direct_codex_user_item/validation.rs`、`docs/【功能说明】AGC聊天素材引用-2026-09-08.md`。
|
||||
|
||||
## 2026-09-21 应用日志整行凭据脱敏会吃掉整条结构化诊断
|
||||
|
||||
`append_application_log_line` 在落盘前对整行做 `sanitize_diagnostic_message`:行内只要出现 `token=`、`bearer `、`authorization`、`credential`、`api key` / `apikey` / `api_key` 这类标记,**整行**就被换成 `<sensitive diagnostic details redacted>`,只留下时间戳与 `RUST module:` 前缀;同时每行还会被截到 2048 字符。于是把“身份字段 + 诊断正文”拼成一行 `app_log!` 时,正文里一个凭据词就可能让整条记录连 `eventId`、`code` 一起消失(2026-09-21 加统一错误事件的日志投影时按两行落:身份行只放程序生成与调用方常量字段,summary / hint / detail 等自由文本一律只放详情行,且自由文本先自行压平换行——裸词标记脱敏消不掉,自由文本放错行会把 eventId、code 一起带走)。
|
||||
|
||||
@@ -13,6 +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/<eventId>.json`,既不进事件池也不进报告包。因为报告包里的日志附件只有 AppData 应用日志,所以 sidecar 的同一份已脱敏诊断再作为**日志行**(不是报告事件)投影成两行:`agent.runtime.error`(身份行:eventId / source / stage / code / retryable / clientTurnId / elapsedMs / detailRef,全部是程序生成或调用方常量)与 `agent.runtime.error.detail`(详情行:hint / summary / detail / metadata,自由文本只出现在这里)。两行都由 `agent/runtime_error.rs` 从同一份 diagnosis 生成,不新增字段来源;落到日志前 summary 按 320 字符、detail / metadata 按(1200 / 200 字符)预算脱敏截断(summary 由调用方给,`direct_tool_bridge` 会传工具错误原文,而 `app_log!` 同时把整行写 stderr,那里没有 `sanitize_diagnostic_message` 兜底)。`sanitize_diagnostic_message` 命中凭据标记时替换的是**整行**,自由文本因此只放详情行:详情行被吃掉也不影响身份行定位事件。
|
||||
- 报告面板只由自动诊断通知中的“查看并报告”打开,不提供聊天命令、崩溃页按钮或其他手动入口;默认选中当前快照中的全部事件,用户可取消不想提交的事件。允许填写最多 2,000 字中文描述并取消日志附件;本版本不支持截图或任意文件附件。
|
||||
- 报告面板读取当前错误快照失败时,必须明确显示“错误事件暂不可用,请关闭后重试”,不能把失败误显示为“当前没有待报告的错误”。
|
||||
- 通知中的“查看并报告”打开面板时必须保留该次通知快照;最新快照读取瞬时失败时使用这份 fallback 继续展示和提交,不能因先清空通知而丢失用户刚看到的事件。
|
||||
|
||||
@@ -1839,3 +1839,9 @@ Direct 回合的所有权属于进程内项目身份锁,不属于当前页面
|
||||
- 用户级配额:跨节点的用户总量配额与计费口径未定;当前用单项目 2 GiB 上限 + 清单引用回收保证常驻占用有界,用户级总量只能靠项目数间接约束。
|
||||
- 目标 bucket 的生命周期规则(例如转低频/过期删除)需要在部署环境确认后单独收口;功能本身已不再依赖它来控制增长。
|
||||
- 大项目(素材数量多、单文件大)的首轮全量上传耗时与带宽占用未实测;单次预算 512 MiB 会把超出部分留到下一次同步,但并发上限与断点续传仍未引入。
|
||||
|
||||
## 2026-09-21 统一错误事件同时落到 AppData 应用日志
|
||||
|
||||
`AgentRuntimeErrorEvent` 把失败投影到用户消息、运行面板和项目内 `.agent/runtime/errors/<eventId>.json` 时,同一份已脱敏诊断还要投影成 AppData `diagnostics/application.log` 的两行:`agent.runtime.error`(身份行:`eventId / source / stage / code / retryable / clientTurnId / elapsedMs / detailRef`)与 `agent.runtime.error.detail`(详情行:`hint / summary / detail / metadata`)。原因是项目内 sidecar 只在项目目录可见,而“报告问题”只上传应用级日志:没有这两行时,用户提交的失败消息里只剩一个 `详情:.agent/runtime/errors/...json` 路径,团队拿不到诊断正文。
|
||||
|
||||
口径:两行都由 `agent/runtime_error.rs` 从同一份 diagnosis 生成,字段不退化成第二份来源;`summary` 按 320 字符、`detail` 与 `metadata` 按(1200 / 200 字符)预算先脱敏再截断,落盘前还会被 `sanitize_diagnostic_message` 二次脱敏并按行截断,因此自由文本字段在行内先压平换行。拆两行是因为整行一旦出现凭据标记会被整体替换成脱敏占位:所以**自由文本(summary / hint / detail)只放详情行**,身份行只留程序生成与调用方常量字段,详情行被整体脱敏时事件仍能按 eventId / detailRef 定位。写日志先于写 sidecar:sidecar 失败不能连日志一起丢。
|
||||
|
||||
Reference in New Issue
Block a user