修复 DirectProject 流式正文逐 delta 脱敏吃掉段尾换行导致 Markdown 结构损坏
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m28s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m58s
Project CI / Backend tests (pull_request) Successful in 4m7s
Project CI / Frontend tests (pull_request) Successful in 2m13s
Project CI / Native shell tests (pull_request) Successful in 6m8s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 9m4s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 9m29s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m45s
Project CI / Repository checks (pull_request) Successful in 2m15s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m28s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m58s
Project CI / Backend tests (pull_request) Successful in 4m7s
Project CI / Frontend tests (pull_request) Successful in 2m13s
Project CI / Native shell tests (pull_request) Successful in 6m8s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 9m4s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 9m29s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m45s
Project CI / Repository checks (pull_request) Successful in 2m15s
- prompt_context.rs:sanitize_error_context 改为 split_inclusive('\n') 逐段处理并原样保留行终止符,「逐段脱敏」与「整段脱敏」同形(CRLF 仍归一成 LF,私钥块整行吞掉的旧口径不变)
- codex_app_server/mod.rs:新增 direct_thread_delta_sanitization_preserves_line_breaks,钉住逐段脱敏 == 整段脱敏,去掉 split_inclusive 即红
- pitfalls.md:记入本次排障(含 chunk 起点路径误判这一同源未修缺陷)
This commit is contained in:
@@ -6006,6 +6006,35 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// 流式脱敏必须保留增量自带的换行:前端把每个 `item.delta` 的脱敏结果**直接拼接**成一条
|
||||
/// 消息再交给 Markdown 渲染,所以「逐段脱敏」必须与「整段脱敏」同形,不能吃掉段尾换行。
|
||||
///
|
||||
/// 回归背景:`sanitize_error_context` 曾用 `lines()` + `join("\n")` 往返,把以换行结尾的段
|
||||
/// 的末尾换行吃掉。真实会话里段落、列表项和表格行会因此并进同一行(表头、`|---|---|` 与
|
||||
/// 数据行挤成一段正文),整条汇报的 Markdown 结构——尤其表格——直接失效。
|
||||
#[test]
|
||||
fn direct_thread_delta_sanitization_preserves_line_breaks() {
|
||||
let root = std::path::Path::new("/workspace/direct-project");
|
||||
let chunks = [
|
||||
"共 4 项验收要求。\n",
|
||||
"\n",
|
||||
"| 素材 | 用途 |\n",
|
||||
"|---|---|\n",
|
||||
"| 飞鸟角色 | 玩家角色 |\n",
|
||||
];
|
||||
let streamed: String = chunks
|
||||
.iter()
|
||||
.map(|chunk| direct_thread_delta_text(root, chunk))
|
||||
.collect();
|
||||
let whole = chunks.concat();
|
||||
assert_eq!(streamed, whole, "逐段脱敏不得吃掉段尾换行");
|
||||
assert_eq!(
|
||||
direct_thread_delta_text(root, &whole),
|
||||
streamed,
|
||||
"逐段脱敏与整段脱敏必须同形"
|
||||
);
|
||||
}
|
||||
|
||||
/// 判据:读取器与单测共用同一个分类函数,这些分支的行为被钉在这里。
|
||||
///
|
||||
/// 参数:method / params / 正文候选 / 安全活动类别 / turnId。
|
||||
|
||||
@@ -623,34 +623,42 @@ const ERROR_REDACTED_KEY: &str = "[redacted-sensitive-field]";
|
||||
/// results. This intentionally does not call `sanitize_prompt_context`: the
|
||||
/// latter is stricter by design and would erase useful HTTP status/code/field
|
||||
/// information whenever a safe error line mentions a credential field.
|
||||
///
|
||||
/// 逐行脱敏,但**保留每个 chunk 末尾的换行**:本函数会被流式增量逐段调用
|
||||
/// (`direct_thread_delta_text` → 前端把各段拼成一条消息再交给 Markdown 渲染)。用
|
||||
/// `lines()` + `join("\n")` 会把「以换行结尾的段」的末尾换行吃掉,拼接后段落、列表项和表格行
|
||||
/// 会并进同一行,整条消息的 Markdown 结构(尤其表格)就作废了。
|
||||
pub(crate) fn sanitize_error_context(value: &str) -> String {
|
||||
let mut sanitized = Vec::new();
|
||||
let mut sanitized = String::with_capacity(value.len());
|
||||
let mut inside_private_key = false;
|
||||
for line in value.lines() {
|
||||
for chunk in value.split_inclusive('\n') {
|
||||
let terminated = chunk.ends_with('\n');
|
||||
let line = chunk.strip_suffix('\n').unwrap_or(chunk);
|
||||
let line = line.strip_suffix('\r').unwrap_or(line);
|
||||
let lower = line.to_ascii_lowercase();
|
||||
if contains_sensitive_cli_flag(&lower) {
|
||||
sanitized.push("[redacted sensitive context]".to_string());
|
||||
continue;
|
||||
}
|
||||
if inside_private_key {
|
||||
sanitized.push_str("[redacted sensitive context]");
|
||||
} else if inside_private_key {
|
||||
if lower.contains("-----end") && lower.contains("private key") {
|
||||
inside_private_key = false;
|
||||
}
|
||||
// 私钥块整行吞掉(连带它的换行),与旧口径一致,不补空行。
|
||||
continue;
|
||||
}
|
||||
if lower.contains("-----begin") && lower.contains("private key") {
|
||||
sanitized.push("[redacted sensitive context]".to_string());
|
||||
} else if lower.contains("-----begin") && lower.contains("private key") {
|
||||
sanitized.push_str("[redacted sensitive context]");
|
||||
inside_private_key = !(lower.contains("-----end") && lower.contains("private key"));
|
||||
continue;
|
||||
} else {
|
||||
let line = redact_secret_tokens(line);
|
||||
let line = redact_error_sensitive_assignments(&line);
|
||||
let line = redact_error_bearer_values(&line);
|
||||
let line = redact_error_config_names(&line);
|
||||
sanitized.push_str(&redact_secret_tokens(&line));
|
||||
}
|
||||
if terminated {
|
||||
sanitized.push('\n');
|
||||
}
|
||||
|
||||
let line = redact_secret_tokens(line);
|
||||
let line = redact_error_sensitive_assignments(&line);
|
||||
let line = redact_error_bearer_values(&line);
|
||||
let line = redact_error_config_names(&line);
|
||||
sanitized.push(redact_secret_tokens(&line));
|
||||
}
|
||||
sanitized.join("\n")
|
||||
sanitized
|
||||
}
|
||||
|
||||
fn error_key_boundary(lower: &str, start: usize, end: usize) -> bool {
|
||||
|
||||
Reference in New Issue
Block a user