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
|