新增: 工具工作流补全(diff_files工具 + 技能清单注入 + 工作流进度共享 + human端到端测试 + 知识库MCP工具)
This commit is contained in:
@@ -259,6 +259,46 @@ pub fn register(
|
||||
}
|
||||
);
|
||||
|
||||
// ── diff_files (Low 只读, 任意两文件对比) ──
|
||||
// 补 git_diff 缺口:git_diff 只比仓库内版本,本工具对比任意两文件(绝对路径),
|
||||
// 复用 generate_diff(LCS unified diff)生成差异,只读无副作用。
|
||||
declare_tool!(
|
||||
registry,
|
||||
allowed_dirs: Arc<RwLock<AllowedDirs>>,
|
||||
"diff_files",
|
||||
"对比任意两个文件的差异,返回 unified diff(+/- 行前缀,基于 LCS)。参数 path_a/path_b 为两文件绝对路径,均须在授权目录内。只读无副作用,用于两文件/两版本内容对比(git_diff 仅仓库内,本工具补任意两文件缺口)",
|
||||
RiskLevel::Low,
|
||||
schema: object_schema(vec![("path_a", "string", true), ("path_b", "string", true)]),
|
||||
args => {
|
||||
let snap = allowed_dirs.read().await.clone();
|
||||
let a_resolved = resolve_workspace_path_with_allowed(
|
||||
args["path_a"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 path_a 参数"))?,
|
||||
&snap,
|
||||
)?;
|
||||
let b_resolved = resolve_workspace_path_with_allowed(
|
||||
args["path_b"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 path_b 参数"))?,
|
||||
&snap,
|
||||
)?;
|
||||
let a_path = a_resolved.to_str().ok_or_else(|| anyhow::anyhow!("path_a 含非法字符"))?.to_string();
|
||||
let b_path = b_resolved.to_str().ok_or_else(|| anyhow::anyhow!("path_b 含非法字符"))?.to_string();
|
||||
if a_path == b_path {
|
||||
anyhow::bail!("两个路径相同,无需对比: {}", a_path);
|
||||
}
|
||||
// 读取两文件文本(1MB 上限 + 二进制拦截,解码对齐 read_file 的 UTF-16 BOM 优先)
|
||||
let (content_a, size_a) = read_text_file(&a_path).await?;
|
||||
let (content_b, size_b) = read_text_file(&b_path).await?;
|
||||
let diff = generate_diff(&content_a, &content_b);
|
||||
Ok(serde_json::json!({
|
||||
"path_a": a_path,
|
||||
"path_b": b_path,
|
||||
"size_a": size_a,
|
||||
"size_b": size_b,
|
||||
"identical": content_a == content_b,
|
||||
"diff": diff,
|
||||
}))
|
||||
}
|
||||
);
|
||||
|
||||
// ── list_directory (Low 只读) ──
|
||||
declare_tool!(
|
||||
registry,
|
||||
@@ -1232,6 +1272,38 @@ pub fn register(
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// diff_files 辅助 — 读取文本文件内容(两文件对比共用)
|
||||
// ============================================================
|
||||
|
||||
/// 读取文本文件内容,返回 (内容, 字节数)。
|
||||
///
|
||||
/// diff_files 两文件对比共用:1MB 上限 + 二进制拦截 + UTF-16 BOM 优先解码
|
||||
/// (复用 decode_bytes_to_string,对齐 read_file/patch_file 的读取语义,单真相源)。
|
||||
async fn read_text_file(path: &str) -> anyhow::Result<(String, u64)> {
|
||||
use tokio::fs::File;
|
||||
use tokio::io::AsyncReadExt;
|
||||
let mut file = File::open(path).await
|
||||
.map_err(|e| {
|
||||
if e.kind() == std::io::ErrorKind::NotFound {
|
||||
anyhow::anyhow!("文件不存在: {}", path)
|
||||
} else {
|
||||
anyhow::anyhow!("无法访问文件 {}: {}", path, e)
|
||||
}
|
||||
})?;
|
||||
let metadata = file.metadata().await
|
||||
.map_err(|e| anyhow::anyhow!("读取元数据失败 {}: {}", path, e))?;
|
||||
if metadata.len() > 1_048_576 {
|
||||
anyhow::bail!("文件超过 1MB 限制 ({} 字节)", metadata.len());
|
||||
}
|
||||
let mut raw_bytes = Vec::with_capacity(metadata.len() as usize);
|
||||
file.read_to_end(&mut raw_bytes).await
|
||||
.map_err(|e| anyhow::anyhow!("读取文件失败: {}", e))?;
|
||||
let content = decode_bytes_to_string(&raw_bytes)
|
||||
.map_err(|_| anyhow::anyhow!("文件非 UTF-8 文本(疑似二进制),无法对比: {}", path))?;
|
||||
Ok((content, metadata.len()))
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// patch_file 失败提示辅助 — old_text 不匹配时给 LLM 相近锚点
|
||||
// ============================================================
|
||||
|
||||
Reference in New Issue
Block a user