'use strict'; const fs = require('fs'); const path = require('path'); const fg = require('fast-glob'); const { execSync } = require('child_process'); const { getContainerContext, resolveContainerPath, execContainerAction } = require('./container_bridge'); function resolvePath(workspace, p) { if (!p) return workspace; if (path.isAbsolute(p)) return p; return path.join(workspace, p); } function hasRg() { try { execSync('rg --version', { stdio: 'ignore' }); return true; } catch (_) { return false; } } async function searchWorkspaceTool(workspace, args) { const root = resolvePath(workspace, args.root || '.'); const mode = args.mode; const query = args.query || ''; const useRegex = !!args.use_regex; const caseSensitive = !!args.case_sensitive; const maxResults = args.max_results || 20; const maxMatchesPerFile = args.max_matches_per_file || 3; const includeGlob = Array.isArray(args.include_glob) && args.include_glob.length ? args.include_glob : ['**/*']; const excludeGlob = Array.isArray(args.exclude_glob) ? args.exclude_glob : []; const maxFileSize = args.max_file_size || null; const ctx = getContainerContext(); if (ctx) { const resolved = resolveContainerPath(workspace, args.root || '.', ctx); if (resolved.error) return { success: false, error: resolved.error }; const resp = execContainerAction(ctx, 'search_workspace', { mode, query, use_regex: useRegex, case_sensitive: caseSensitive, max_results: maxResults, max_matches_per_file: maxMatchesPerFile, include_glob: includeGlob, exclude_glob: excludeGlob, max_file_size: maxFileSize, root: resolved.containerPath, }, null, resolved.containerPath); if (!resp.success) return { success: false, error: resp.error || '搜索失败' }; return resp; } if (mode === 'file') { const files = await fg(includeGlob, { cwd: root, dot: true, ignore: excludeGlob, onlyFiles: true, absolute: true }); const matcher = useRegex ? new RegExp(query, caseSensitive ? '' : 'i') : null; const matches = []; for (const file of files) { const name = path.basename(file); const hay = caseSensitive ? name : name.toLowerCase(); const needle = caseSensitive ? query : query.toLowerCase(); const ok = useRegex ? matcher.test(name) : hay.includes(needle); if (ok) { matches.push(file); if (matches.length >= maxResults) break; } } return { success: true, mode: 'file', root, query, matches }; } if (mode === 'content') { if (hasRg()) { const argsList = ['--line-number', '--no-heading', '--color=never']; if (!useRegex) argsList.push('--fixed-strings'); if (!caseSensitive) argsList.push('-i'); if (maxMatchesPerFile) argsList.push(`--max-count=${maxMatchesPerFile}`); if (maxFileSize) argsList.push(`--max-filesize=${maxFileSize}`); for (const g of includeGlob) argsList.push('-g', g); for (const g of excludeGlob) argsList.push('-g', `!${g}`); argsList.push(query); argsList.push(root); let output = ''; try { output = execSync(`rg ${argsList.map((a) => JSON.stringify(a)).join(' ')}`, { maxBuffer: 10 * 1024 * 1024 }).toString(); } catch (err) { output = err.stdout ? err.stdout.toString() : ''; } const lines = output.split(/\r?\n/).filter(Boolean); const files = new Map(); for (const line of lines) { const [filePath, lineNum, ...rest] = line.split(':'); const snippet = rest.join(':').trim(); if (!files.has(filePath)) files.set(filePath, []); if (files.get(filePath).length < maxMatchesPerFile) { files.get(filePath).push({ line: Number(lineNum), snippet }); } if (files.size >= maxResults) break; } const results = Array.from(files.entries()).map(([file, matches]) => ({ file, matches })); return { success: true, mode: 'content', root, query, results }; } // fallback const files = await fg(includeGlob, { cwd: root, dot: true, ignore: excludeGlob, onlyFiles: true, absolute: true }); const matcher = useRegex ? new RegExp(query, caseSensitive ? '' : 'i') : null; const results = []; for (const file of files) { if (maxFileSize) { try { const stat = fs.statSync(file); if (stat.size > maxFileSize) continue; } catch (_) {} } const content = fs.readFileSync(file, 'utf8'); const lines = content.split(/\r?\n/); const matches = []; for (let i = 0; i < lines.length; i++) { const line = lines[i]; const hay = caseSensitive ? line : line.toLowerCase(); const needle = caseSensitive ? query : query.toLowerCase(); const ok = useRegex ? matcher.test(line) : hay.includes(needle); if (ok) { matches.push({ line: i + 1, snippet: line.trim() }); if (matches.length >= maxMatchesPerFile) break; } } if (matches.length) { results.push({ file, matches }); if (results.length >= maxResults) break; } } return { success: true, mode: 'content', root, query, results }; } return { success: false, error: '未知 search_workspace mode' }; } module.exports = { searchWorkspaceTool };