agent-Specialization/easyagent/src/tools/search_workspace.js

141 lines
5.2 KiB
JavaScript

'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 };