Files
bigwo/dev-assistant-mcp/dist/tools/searchCode.js
2026-03-12 12:47:56 +08:00

176 lines
7.3 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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