优化: UI/UX遗留批(审计/AI命令层/前端组件,会话前基线收尾)

- AuditLog +298(筛选/详情/i18n)+ audit 后端 record/mod

- AI 命令层:generate_image +81 / fetch_url / fetch_search / skills / tool_registry / tools/file / provider / conversation

- 前端组件:AiChat/TopBar/ConversationSidebar/GitChanges/ApprovalPopup/Dashboard/ProjectDetail 等 30+ + composables + i18n

- 诊断文档: aichat历史会话实证诊断-2026-08-04 + project_soft_delete 测试
This commit is contained in:
lxy
2026-08-05 22:15:34 +08:00
parent 28de5d6143
commit e68d283651
49 changed files with 1257 additions and 138 deletions
+28 -10
View File
@@ -150,13 +150,13 @@ pub fn register(
// 虽 1MB 字节上限挡住极端情况,但万行级源码全量进 LLM context 仍易撑爆。
// 改:无 offset 默认返回前 500 行 + has_more 提示翻页(对齐 read 工具常规用法)。
let line_count = content.lines().count();
let (result, offset_used, has_more) = if let Some(offset) = args["offset"].as_u64() {
let (result, offset_used, has_more, limit) = if let Some(offset) = args["offset"].as_u64() {
let lines: Vec<&str> = content.lines().collect();
let skip = offset as usize;
let limit = args["limit"].as_u64().unwrap_or(200).min(2000) as usize;
let page: Vec<&str> = lines.into_iter().skip(skip).take(limit).collect();
let more = (skip + page.len()) < line_count;
(page.join("\n"), Some(skip), more)
(page.join("\n"), Some(skip), more, limit)
} else {
// 无 offset: 尊重 LLM 传入的 limit(对齐有 offset 分支语义),
// 未传 limit 默认 500 行(大文件翻页友好,避免一次灌入全量)。
@@ -168,13 +168,26 @@ pub fn register(
.min(2000) as usize;
let page: Vec<&str> = content.lines().take(limit).collect();
let more = line_count > page.len();
(page.join("\n"), None, more)
(page.join("\n"), None, more, limit)
};
// 截断明文化:治模型把 has_more 当"被压缩/损坏"反复重读同文件(实证 read_file 同文件重复读 6 次)。
// returned_lines=本次返回行数,consumed=累计已读(offset+本次),truncated_info 明确剩余行数+续读方式。
let returned_lines = result.lines().count();
let consumed = offset_used.unwrap_or(0) + returned_lines;
let truncated_info = if has_more {
Some(format!(
"文件共 {line_count} 行,本次返回 {returned_lines} 行(offset={}, limit={limit}),剩余 {} 行未读。继续读取请用 offset={} 参数(可配合 limit={limit})",
offset_used.map(|o| o.to_string()).unwrap_or_else(|| "0".to_string()),
line_count - consumed,
consumed,
))
} else { None };
Ok(serde_json::json!({
"path": path, "content": result, "size": metadata.len(), "file_hash": file_hash, "lines": line_count,
"offset": offset_used,
"returned_lines": result.lines().count(),
"has_more": has_more,
"returned_lines": returned_lines,
"truncated": has_more,
"truncated_info": truncated_info,
}))
}
);
@@ -1034,7 +1047,7 @@ pub fn register(
registry,
dummy: Arc<()>,
"run_command",
"在指定工作目录执行 shell 命令,返回 stdout/stderr/exit_code。仅用于命令执行场景:跑测试套件、构建项目、运行二进制/脚本验证行为。读取文件用 read_file,编辑文件用 patch_file/write_file,列目录用 list_directory,搜索文件名用 search_files——不要用本工具完成这些操作。高风险,须人工批准。命令需自包含(非交互式,避免需用户输入的程序)。默认超时 60 秒。",
"在指定工作目录执行 shell 命令,返回 stdout/stderr/exit_code。仅用于命令执行场景:跑测试套件、构建项目、运行二进制/脚本验证行为。读取文件用 read_file,编辑文件用 patch_file/write_file,列目录用 list_directory,搜索文件名用 search_files——不要用本工具完成这些操作。高风险,须人工批准。命令需自包含(非交互式,避免需用户输入的程序)。默认超时 60 秒。返回含 succeeded 布尔(按 exit_code==0 判定)。注意:部分命令非零退出属正常语义(如 git diff 无差异返回 1),此时以 stdout/stderr 内容为准,勿仅凭 exit_code≠0 判失败。",
RiskLevel::High,
schema: object_schema(vec![
("command", "string", true),
@@ -1108,6 +1121,7 @@ pub fn register(
let (stderr, stderr_trunc) = truncate_output(&result.stderr, MAX_OUT);
Ok(serde_json::json!({
"succeeded": result.exit_code == Some(0),
"command": command,
"working_dir": working_dir,
"exit_code": result.exit_code,
@@ -1289,11 +1303,15 @@ mod tests {
let win_start = char_start.saturating_sub(context_chars);
let win_end = (char_start + context_chars).min(total_chars);
let window: String = line.chars().skip(win_start).take(win_end - win_start).collect();
// 结构:window 前可加前导 …(仅 win_start>0,match 行首不加),窗口标记附加在 window 后。
// 修复:原 format 在 window 前有字面量 …(±N字符窗口)…,致 match 行首(win_start=0)
// 也以 … 开头,违反「行首不前导 …」语义。
let lead = if win_start > 0 { "" } else { "" };
format!(
"{}…(±{}字符窗口)…{}",
if win_start > 0 { "" } else { "" },
context_chars,
window
"{lead}{window}…(±{context_chars}字符窗口)…",
lead = lead,
window = window,
context_chars = context_chars
)
}