From beda813a59125ff517b09aabd0fbfc10841869d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Wed, 2 Sep 2026 20:01:04 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8A=A0=E5=BC=BA=E9=94=99=E8=AF=AF=E6=8A=A5?= =?UTF-8?q?=E5=91=8A=E5=87=AD=E6=8D=AE=E8=84=B1=E6=95=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 使用 Unicode 空白归一化匹配凭据标记 补充 NBSP 和 EM SPACE 回归测试并同步技术文档 --- ...术方案】AGC错误报告与诊断上传-2026-08-31.md | 2 +- .../crates/api-server/src/error_reports.rs | 23 ++++++++++++++++--- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/docs/technical/【技术方案】AGC错误报告与诊断上传-2026-08-31.md b/docs/technical/【技术方案】AGC错误报告与诊断上传-2026-08-31.md index 9b6b9d2dc..31b747b01 100644 --- a/docs/technical/【技术方案】AGC错误报告与诊断上传-2026-08-31.md +++ b/docs/technical/【技术方案】AGC错误报告与诊断上传-2026-08-31.md @@ -19,7 +19,7 @@ AI Game Creator Shell 采用 IDEA 风格的当前进程错误报告:错误事 ## HTTP 与存储 - 登录态客户端使用 `POST /api/error-reports`,请求 DTO 位于 `shared-contracts::error_reports`。 -- api-server 对请求体设置 24 MiB 上限,并校验 schemaVersion、submissionId、事件/日志数量和 20 MiB 压缩包上限;事件字段、用户说明和日志名/内容均做长度限制与凭据脱敏,归档使用 `events.jsonl`(每行一个事件)。结构化事件只保存在当前进程内,用户提交时才生成 `events.jsonl`,不在磁盘单独持久化。submissionId 提供重放幂等;客户端对同一批事件复用稳定 submissionId,服务端提交成功后立即反馈成功,本地事件 ack 独立重试,不因 ack 失败误报提交失败;SpacetimeDB procedure 使用服务 identity 门禁,`user_id` 由 api-server 注入并在 module 校验,时间戳仍使用 `ctx.timestamp`。创建 procedure 额外限制单 identity 每小时 100 次提交。 +- api-server 对请求体设置 24 MiB 上限,并校验 schemaVersion、submissionId、事件/日志数量和 20 MiB 压缩包上限;事件字段、用户说明和日志名/内容均做长度限制与凭据脱敏(匹配归一化覆盖 Unicode 空白字符),归档使用 `events.jsonl`(每行一个事件)。结构化事件只保存在当前进程内,用户提交时才生成 `events.jsonl`,不在磁盘单独持久化。submissionId 提供重放幂等;客户端对同一批事件复用稳定 submissionId,服务端提交成功后立即反馈成功,本地事件 ack 独立重试,不因 ack 失败误报提交失败;SpacetimeDB procedure 使用服务 identity 门禁,`user_id` 由 api-server 注入并在 module 校验,时间戳仍使用 `ctx.timestamp`。创建 procedure 额外限制单 identity 每小时 100 次提交。 - 归档构建只在请求生命周期内使用受 20 MiB 上限约束的内存 `Vec`,随后直接 PUT 到私有 OSS;服务端不写本地报告文件,也不保留本地索引。OSS 上传失败不写入数据库,调用方可稍后重新提交。 - 归档对象使用固定私有 OSS key:`agc/error-reports/v1/{batchId}.zip`;key 只由报告 UUID 决定,不包含时间戳。上传成功后才写入 SpacetimeDB `error_report` 元数据表;`userId + submissionId` 由唯一幂等键保证重放返回已有记录。完整事件、说明和日志只存在 OSS ZIP。 - `agc` 是服务端专用私有前缀;公共直传票据、通用 object-key 规范化和 legacy 公开路径均拒绝该前缀。归档内同名日志会自动加数字后缀,读取本机诊断日志时拒绝符号链接/非普通文件。 diff --git a/server-rs/crates/api-server/src/error_reports.rs b/server-rs/crates/api-server/src/error_reports.rs index 7613dda39..bb634179f 100644 --- a/server-rs/crates/api-server/src/error_reports.rs +++ b/server-rs/crates/api-server/src/error_reports.rs @@ -645,10 +645,10 @@ fn sanitize_report_text(v: &str) -> String { sanitize_report_text_with_limit(v, MAX_EVENT_FIELD_CHARS) } fn sanitize_report_text_with_limit(v: &str, max: usize) -> String { - let l = v.to_ascii_lowercase(); - let normalized = l + let normalized = v + .to_lowercase() .chars() - .filter(|c| !c.is_ascii_whitespace()) + .filter(|c| !c.is_whitespace()) .collect::(); if [ "authorization:", @@ -679,6 +679,23 @@ fn sanitize_report_text_with_limit(v: &str, max: usize) -> String { .take(max) .collect() } + +#[cfg(test)] +mod tests { + use super::sanitize_report_text_with_limit; + + #[test] + fn redacts_credentials_separated_by_unicode_whitespace() { + for value in [ + "token\u{00a0}=\u{2003}secret", + "Authorization:\u{2003}Bearer\u{00a0}secret", + "api-key\u{00a0}:\u{2003}secret", + ] { + assert_eq!(sanitize_report_text_with_limit(value, 512), "[REDACTED]"); + } + } +} + fn sanitize_log_name(v: &str) -> String { let s = v .rsplit(['/', '\\'])