Merge pull request '修复 DirectProject 流式正文逐 delta 脱敏吃掉段尾换行导致 Markdown 结构损坏' (#533) from fix/direct-delta-markdown into master
Project CI / AI game creator shell Rust crates (push) Successful in 1m27s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m55s
Project CI / Backend tests (push) Successful in 4m6s
Project CI / Frontend tests (push) Successful in 2m10s
Project CI / Native shell tests (push) Successful in 6m4s
Project CI / AI game creator shell Rust lane 2/2 (push) Successful in 8m53s
Project CI / AI game creator shell Rust lane 1/2 (push) Successful in 9m22s
Project CI / AI game creator shell web tests (push) Successful in 1m44s
Project CI / Repository checks (push) Successful in 2m13s
Project CI / AI game creator shell Rust crates (push) Successful in 1m27s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m55s
Project CI / Backend tests (push) Successful in 4m6s
Project CI / Frontend tests (push) Successful in 2m10s
Project CI / Native shell tests (push) Successful in 6m4s
Project CI / AI game creator shell Rust lane 2/2 (push) Successful in 8m53s
Project CI / AI game creator shell Rust lane 1/2 (push) Successful in 9m22s
Project CI / AI game creator shell web tests (push) Successful in 1m44s
Project CI / Repository checks (push) Successful in 2m13s
Reviewed-on: #533
This commit was merged in pull request #533.
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 {
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
# 踩坑与排障记录
|
||||
|
||||
## 2026-09-29 逐 delta 脱敏会吃掉段尾换行:DirectProject 汇报的 Markdown 表格整块失效
|
||||
|
||||
- **现象**:AGC 项目对话里 agent 汇报正文渲染错位——段落并进同一行、序号项挤成一段、Markdown 表格整块不渲染(表头 `| 素材 | 用途 | 路径 |`、`|---|---|---|` 与数据行以纯文本连在同一行里);同一段文本里的路径还会出现 `game<absolute-path>ame.js`、`assets<absolute-path>anvas-generated/...` 这种被切坏的占位符。重开项目读历史时同一段话渲染正常,很容易被当成渲染器偶发或模型写坏了 Markdown。
|
||||
- **原因**:两处叠加。① `#384`(`30c377d0f`)把 DirectProject 流式正文改成**每个 delta 各自脱敏**(`direct_codex_thread_delta_event` → `direct_thread_delta_text` → `sanitize_detail_text`),而 `sanitize_error_context` 用 `value.lines()` 遍历再 `join("\n")` 回填:**以换行结尾的段,末尾换行被吃掉**(`"a\n"` → `"a"`、`"\n"` → `""`)。前端 `appendLiveText` 是裸拼接,于是段落空行、列表项换行、表格行换行全部消失。本机 `gameagent-cfc64e7f` 的真实汇报按「每行一个 delta」拼接实测:1445 字符 / 38 个换行 → 1435 字符 / **0 个换行**,渲染成 0 个 `table`、0 个 `li`、0 个 `h2`(整条消息只剩一个段落)。② 同一函数里 `redact_absolute_path_tokens` 把 `index == 0` 当文本边界:delta 从 `/` 开头时,片段被误判成绝对路径(`"assets" + "/c" + "anvas-generated"` → `assets<absolute-path>anvas-generated`)。它既是内容损坏,又让流式正文**比完成态更长**(2 字符 → `<absolute-path>` 15 字符),而 `mergeDirectChatEntry` 的正文合并规则是「取更长的一份」,于是 `item.completed` 送来的正确快照再也覆盖不回界面——只有重开项目(读 `project.jsonl` 原文经整段脱敏)才恢复。
|
||||
- **处理(本次)**:`sanitize_error_context` 改为 `split_inclusive('\n')` 逐段处理、原样保留行终止符(CRLF 仍归一成 LF,私钥块整行吞掉的旧口径不变)。逐段脱敏因此与整段脱敏同形,Markdown 结构不再依赖 delta 的切法。
|
||||
- **仍未修**:`/` 开头的 delta 片段被判成绝对路径(上面 ②)。要修必须把「上一个字符」带进边界判定(正文增量可从 `direct_project_history` 的累计文本取,思考增量目前没有累计器),属于跨 delta 状态的取舍,本次未一并改。
|
||||
- **判据/取证**:`cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml --bin genarrative-ai-game-creator-shell direct_thread_delta`——新增 `direct_thread_delta_sanitization_preserves_line_breaks` 钉住「逐段脱敏 == 整段脱敏」,去掉 `split_inclusive` 即红;真实文本的回归用渲染侧夹具复核(修复前 table/li/h2 全 0,修复后与整段脱敏一致:1 个 table / 3 个 th / 4 个 h2)。
|
||||
- **关联**:`apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/mod.rs`、`.../agent/generation/prompt_context.rs`、`.../agent/direct_thread_wire.rs`、`apps/ai-game-creator-shell/src/view/project-development/chat/conversation/directThreadChat.ts`、`#384`。
|
||||
|
||||
## 2026-09-29 Game Agent 读工具被项目相对路径规则拦住
|
||||
|
||||
- 项目外读取不能只依赖末段 `O_NOFOLLOW`:父目录符号链接可隐藏 `.ssh` 等受保护名字。文件读取和目录列表在访问前逐段检查原始路径,拒绝符号链接与 Windows 重解析点;目录扫描对子目录再次检查。系统临时目录若含平台别名(例如 macOS `/var`),普通读取测试使用临时目录的 canonical 路径,不能通过 canonicalize 待读路径来抹掉待检测链接。
|
||||
|
||||
Reference in New Issue
Block a user