176 lines
7.3 KiB
JavaScript
176 lines
7.3 KiB
JavaScript
import { exec } from "child_process";
|
||
import { promisify } from "util";
|
||
import { readdirSync, readFileSync, statSync } from "fs";
|
||
import { join, extname, relative } from "path";
|
||
const execAsync = promisify(exec);
|
||
export const searchCodeTool = {
|
||
name: "search_code",
|
||
description: "在项目中搜索代码。支持正则表达式、文本搜索、文件名搜索。返回匹配的文件、行号和上下文。类似 grep/ripgrep。",
|
||
inputSchema: {
|
||
type: "object",
|
||
properties: {
|
||
project_path: {
|
||
type: "string",
|
||
description: "项目根目录(绝对路径)",
|
||
},
|
||
query: {
|
||
type: "string",
|
||
description: "搜索内容(支持正则表达式)",
|
||
},
|
||
mode: {
|
||
type: "string",
|
||
description: "搜索模式:content=代码内容(默认)、filename=文件名、symbol=函数/类名",
|
||
enum: ["content", "filename", "symbol"],
|
||
},
|
||
includes: {
|
||
type: "string",
|
||
description: "文件过滤 glob(如 *.ts, *.py)",
|
||
},
|
||
case_sensitive: {
|
||
type: "boolean",
|
||
description: "是否区分大小写(默认 false)",
|
||
},
|
||
max_results: {
|
||
type: "number",
|
||
description: "最大结果数(默认 50)",
|
||
},
|
||
},
|
||
required: ["project_path", "query"],
|
||
},
|
||
};
|
||
const SKIP_DIRS = new Set([
|
||
"node_modules", ".git", "__pycache__", "dist", "build", ".next",
|
||
".nuxt", "coverage", ".cache", ".tsbuildinfo", "vendor",
|
||
]);
|
||
const TEXT_EXTS = new Set([
|
||
".ts", ".tsx", ".js", ".jsx", ".py", ".java", ".go", ".rs", ".c", ".cpp", ".h",
|
||
".css", ".scss", ".less", ".html", ".vue", ".svelte", ".json", ".yaml", ".yml",
|
||
".toml", ".md", ".txt", ".sh", ".bat", ".ps1", ".sql", ".graphql", ".prisma",
|
||
".env", ".gitignore", ".eslintrc", ".prettierrc",
|
||
]);
|
||
function searchInDir(dir, rootDir, regex, mode, includeExt, results, maxResults, depth = 0) {
|
||
if (depth > 10 || results.length >= maxResults)
|
||
return;
|
||
try {
|
||
const entries = readdirSync(dir);
|
||
for (const entry of entries) {
|
||
if (results.length >= maxResults)
|
||
break;
|
||
if (SKIP_DIRS.has(entry) || entry.startsWith("."))
|
||
continue;
|
||
const fullPath = join(dir, entry);
|
||
try {
|
||
const stat = statSync(fullPath);
|
||
if (stat.isDirectory()) {
|
||
searchInDir(fullPath, rootDir, regex, mode, includeExt, results, maxResults, depth + 1);
|
||
}
|
||
else if (stat.isFile()) {
|
||
const ext = extname(entry).toLowerCase();
|
||
// 文件名搜索
|
||
if (mode === "filename") {
|
||
if (regex.test(entry)) {
|
||
results.push({ file: relative(rootDir, fullPath), line: 0, content: entry });
|
||
}
|
||
continue;
|
||
}
|
||
// 内容搜索 - 只搜索文本文件
|
||
if (!TEXT_EXTS.has(ext) && ext !== "")
|
||
continue;
|
||
if (includeExt && !entry.endsWith(includeExt.replace("*", "")))
|
||
continue;
|
||
if (stat.size > 1024 * 512)
|
||
continue; // 跳过 >512KB 的文件
|
||
const content = readFileSync(fullPath, "utf-8");
|
||
const lines = content.split("\n");
|
||
for (let i = 0; i < lines.length; i++) {
|
||
if (results.length >= maxResults)
|
||
break;
|
||
if (mode === "symbol") {
|
||
// 只匹配函数/类/接口定义
|
||
const line = lines[i];
|
||
if (/(?:function|class|interface|def|const|let|var|export)\s/.test(line) && regex.test(line)) {
|
||
results.push({
|
||
file: relative(rootDir, fullPath),
|
||
line: i + 1,
|
||
content: line.trim(),
|
||
context_before: i > 0 ? lines[i - 1].trim() : undefined,
|
||
context_after: i < lines.length - 1 ? lines[i + 1].trim() : undefined,
|
||
});
|
||
}
|
||
}
|
||
else {
|
||
if (regex.test(lines[i])) {
|
||
results.push({
|
||
file: relative(rootDir, fullPath),
|
||
line: i + 1,
|
||
content: lines[i].trim(),
|
||
context_before: i > 0 ? lines[i - 1].trim() : undefined,
|
||
context_after: i < lines.length - 1 ? lines[i + 1].trim() : undefined,
|
||
});
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
catch { }
|
||
}
|
||
}
|
||
catch { }
|
||
}
|
||
export async function executeSearchCode(args) {
|
||
const { project_path, query, mode = "content", includes, case_sensitive = false, max_results = 50 } = args;
|
||
const flags = case_sensitive ? "" : "i";
|
||
let regex;
|
||
try {
|
||
regex = new RegExp(query, flags);
|
||
}
|
||
catch {
|
||
regex = new RegExp(query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), flags);
|
||
}
|
||
const results = [];
|
||
const includeExt = includes ? includes.replace("*", "") : null;
|
||
searchInDir(project_path, project_path, regex, mode, includeExt, results, max_results);
|
||
if (results.length === 0) {
|
||
return `# 搜索结果\n\n🔍 "${query}" (${mode})\n📂 ${project_path}\n\n_未找到匹配结果_`;
|
||
}
|
||
// 按文件分组
|
||
const grouped = {};
|
||
for (const r of results) {
|
||
(grouped[r.file] || (grouped[r.file] = [])).push(r);
|
||
}
|
||
const fileCount = Object.keys(grouped).length;
|
||
const output = [
|
||
`# 搜索结果`,
|
||
``,
|
||
`🔍 "${query}" | 模式: ${mode}${includes ? ` | 过滤: ${includes}` : ""}`,
|
||
`📂 ${project_path}`,
|
||
`📊 ${results.length} 个匹配,${fileCount} 个文件`,
|
||
``,
|
||
];
|
||
for (const [file, matches] of Object.entries(grouped)) {
|
||
output.push(`## 📄 ${file} (${matches.length} 处)`);
|
||
for (const m of matches) {
|
||
if (mode === "filename") {
|
||
output.push(`- ${m.content}`);
|
||
}
|
||
else {
|
||
output.push(`**行 ${m.line}:**`);
|
||
if (m.context_before)
|
||
output.push(`\`\`\`\n ${m.context_before}`);
|
||
else
|
||
output.push("```");
|
||
output.push(`→ ${m.content}`);
|
||
if (m.context_after)
|
||
output.push(` ${m.context_after}\n\`\`\``);
|
||
else
|
||
output.push("```");
|
||
}
|
||
}
|
||
output.push(``);
|
||
}
|
||
if (results.length >= max_results) {
|
||
output.push(`\n⚠️ 结果已截断(最大 ${max_results} 条),可增大 max_results 或缩小搜索范围`);
|
||
}
|
||
return output.join("\n");
|
||
}
|
||
//# sourceMappingURL=searchCode.js.map
|