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