修复: read_file 可靠性 + obscura 渲染(压缩豁免 + UTF-16 闭环 + stdout 救回)

read_file 压缩豁免(AI 定向读代码不折叠)+ UTF-16 BOM 闭环(read_file/read_symbol/patch_file);
obscura 非零退出码救回 stdout + fetch markdown 空壳 fallback scrape --eval + 探测日志。
This commit is contained in:
lxy
2026-08-02 02:21:31 +08:00
parent 57d6a2d066
commit 023377ab24
3 changed files with 716 additions and 76 deletions
+88 -6
View File
@@ -380,8 +380,16 @@ pub fn should_summarize_tool_result(
/// - 错误行(在头尾区间外的)额外插入,标注位置。
/// - 中间大段省略为 `... (省略 N 行) ...`。
///
/// `tool_name` 仅用于摘要头注释,不参与内容判断。空 content 返回空字符串。
/// `tool_name` 参与内容判断:`read_file` 是 AI 定向读代码的工具,压缩其 content
/// 等于直接阉割 AI 的代码分析能力(实测 limit=100 读 28KB,被折叠为首尾各 5 行),
/// 故对 `read_file` 一律豁免压缩(原样返回)。其余工具仍走压缩。
/// 空内容一律返回空字符串。
pub fn extract_key_info(content: &str, tool_name: &str) -> String {
// read_file 豁免:AI 定向读代码,压缩 content 违背用户/AI 意图。
// read_file handler 自带 limit 硬上限 2000 行(file.rs:156/168),无爆 prompt 风险。
if tool_name == "read_file" {
return content.to_string();
}
// JSON 感知压缩:识别对象中的大数组/大字符串并截断
if let Ok(mut val) = serde_json::from_str::<serde_json::Value>(content) {
if let Some(obj) = val.as_object_mut() {
@@ -1120,11 +1128,12 @@ mod tests {
#[test]
fn extract_key_info_single_huge_line_no_newline_compressed() {
// 单行超大内容(50KB)原本逃逸压缩,现按字符数截断保留头尾。
// 注:read_file 已豁免压缩(BUG-260801),此处用 run_command 验证通用压缩路径。
let content = "x".repeat(50_000);
let result = extract_key_info(&content, "read_file");
let result = extract_key_info(&content, "run_command");
assert!(result.len() < content.len(), "单行超长应压缩: {} >= {}", result.len(), content.len());
assert!(result.contains("已压缩"), "应含压缩标记");
assert!(result.starts_with("[工具 read_file"), "应以工具名开头");
assert!(result.starts_with("[工具 run_command"), "应以工具名开头");
assert!(result.contains("原始 50000 字符"), "应报告原始字符数");
assert!(result.contains("(截断)"), "应含截断标记");
}
@@ -1133,9 +1142,10 @@ mod tests {
fn extract_key_info_json_huge_string_field_truncated() {
// JSON 对象中大字符串字段(单行少行)逃逸压缩。
// 如 `{"path":"src/main.rs","content":"单行超大文本..."}`。
// 注:read_file 已豁免压缩(BUG-260801),此处用 run_command 验证通用压缩路径。
let large = "z".repeat(10_000);
let content = format!("{{\"path\":\"src/main.rs\",\"content\":\"{}\"}}", large);
let result = extract_key_info(&content, "read_file");
let result = extract_key_info(&content, "run_command");
assert!(result.len() < content.len(), "JSON 大字符串字段应压缩: {} >= {}", result.len(), content.len());
assert!(result.contains("_truncated"), "应含 _truncated 标记");
assert!(result.contains("src/main.rs"), "应保留 path 字段");
@@ -1154,9 +1164,10 @@ mod tests {
#[test]
fn extract_key_info_eleven_lines_triggers_compression() {
// 边界:行数 == 11(刚超 kept_boundary=10)→ 触发压缩,含标记
// 注:read_file 已豁免压缩(BUG-260801),此处用 run_command 验证通用压缩路径。
let lines: Vec<String> = (1..=11).map(|i| format!("line {}", i)).collect();
let content = lines.join("\n");
let result = extract_key_info(&content, "read_file");
let result = extract_key_info(&content, "run_command");
assert!(result.contains("已压缩"), "11 行应触发压缩");
assert!(result.contains("line 1"), "保留首行");
assert!(result.contains("line 11"), "保留末行");
@@ -1167,6 +1178,7 @@ mod tests {
// 边界:错误行恰在头部区间内(idx < head_end)→ 不重复插入(头部已含)
// 错误行在尾部区间内(idx >= tail_start)→ 不重复插入(尾部已含)
// 错误行在中间区间 → 标注 [行 N] 插入
// 注:read_file 已豁免压缩(BUG-260801),此处用 run_command 验证通用压缩路径。
let mut lines: Vec<String> = (1..=20).map(|i| format!("norm {}", i)).collect();
// idx=2(头部区间 [0,5))错误行 → 头部已含,不在 error_lines(扫描跳过 head/tail)
lines[2] = "error in head zone".to_string();
@@ -1175,7 +1187,7 @@ mod tests {
// idx=10(中间)错误行 → 标注插入
lines[10] = "error in middle".to_string();
let content = lines.join("\n");
let result = extract_key_info(&content, "read_file");
let result = extract_key_info(&content, "run_command");
// 中间错误行被标注插入(原始行号 11)
assert!(result.contains("[行 11] error in middle"), "中间错误行应标注插入: {}", result);
// 头/尾错误行原样保留(无 [行 N] 标注)
@@ -1327,4 +1339,74 @@ mod tests {
assert_eq!(extract_pending_tc_id("文件内容"), None);
assert_eq!(extract_pending_tc_id(""), None);
}
// ── read_file 豁免压缩(BUG-260801: AI 定向读代码不应被折叠) ──
//
// 根因:extract_key_info 的 JSON 分支对 content 字段(文件内容)行数 > 10 即折叠中间为
// "(压缩中间内容)"。read_file limit=100 读 28KB(100 行)→ 触发 → AI 只拿到首尾各 5 行,
// 代码分析被阉割。修法:tool_name=="read_file" 一律原样返回(豁免)。
// read_file handler 自带 limit 硬上限 2000 行(file.rs),无爆 prompt 风险。
#[test]
fn extract_key_info_read_file_exempt_from_compression() {
// read_file 返回 JSON,content 字段 100 行(>10 行阈值)→ 其他工具会折叠,
// read_file 应豁免:原样返回,无 "(压缩中间内容)" / "_truncated"。
let content_str: String = (1..=100).map(|i| format!("line {}", i)).collect::<Vec<_>>().join("\n");
// 模拟 read_file 真实返回结构
let content = serde_json::json!({
"path": "/some/file.rs",
"content": content_str,
"lines": 100,
"returned_lines": 100
})
.to_string();
let result = extract_key_info(&content, "read_file");
assert_eq!(result, content, "read_file 大结果应原样返回(豁免压缩)");
assert!(!result.contains("压缩中间内容"), "read_file 不应折叠中间");
assert!(!result.contains("_truncated"), "read_file 不应打 _truncated 标记");
// content 字段 100 行完整保留
let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
assert_eq!(parsed["content"].as_str().unwrap().lines().count(), 100);
}
#[test]
fn extract_key_info_other_tools_still_compressed_when_large() {
// 对照:同样 100 行 content,run_command 应仍被压缩(中间折叠)。
let content_str: Vec<String> = (1..=100).map(|i| format!("line {}", i)).collect();
let content = serde_json::json!({
"stdout": content_str.join("\n"),
"exit_code": 0
})
.to_string();
let result = extract_key_info(&content, "run_command");
assert!(
result.contains("压缩中间内容"),
"run_command 大结果仍应压缩: {}",
result
);
assert!(result.contains("_truncated"), "run_command 应打 _truncated 标记");
}
#[test]
fn extract_key_info_read_file_short_content_unchanged() {
// read_file 小结果同样原样(豁免对大小一致生效)。
let content = serde_json::json!({
"path": "/x.rs",
"content": "only one line",
"lines": 1
})
.to_string();
let result = extract_key_info(&content, "read_file");
assert_eq!(result, content, "read_file 小结果原样返回");
}
#[test]
fn extract_key_info_read_file_non_json_plaintext_exempt() {
// read_file 纯文本大结果(理论上 read_file 总返回 JSON,但兜底:非 JSON 也豁免)。
let lines: Vec<String> = (1..=50).map(|i| format!("line {}", i)).collect();
let content = lines.join("\n");
let result = extract_key_info(&content, "read_file");
assert_eq!(result, content, "read_file 纯文本应豁免压缩");
assert!(!result.contains("输出已压缩"), "read_file 纯文本不应压缩");
}
}