feat(core): 新增 glob 工具,支持文件模式匹配
- 新增 glob 工具,支持 **/*.ts 等 glob 模式匹配文件 - 结果按修改时间排序,限制返回 100 个结果 - 自动忽略 node_modules、.git 等目录 - 更新 Plan Agent 提示词使用 glob 替代 search_files - 在 Plan Agent 工具列表中启用 glob
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
- Fast file pattern matching tool that works with any codebase size
|
||||
- Supports glob patterns like "**/*.js" or "src/**/*.ts"
|
||||
- Returns matching file paths sorted by modification time
|
||||
- Use this tool when you need to find files by name patterns
|
||||
- When you are doing an open ended search that may require multiple rounds of globbing and grepping, use the Task tool instead
|
||||
- You can call multiple tools in a single response. It is always better to speculatively perform multiple searches in parallel if they are potentially useful.
|
||||
@@ -0,0 +1,208 @@
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
import type { ToolResult } from '../../types/index.js';
|
||||
import type { ToolWithMetadata } from '../types.js';
|
||||
import { loadDescription } from '../load_description.js';
|
||||
import { getPermissionManager } from '../../permission/index.js';
|
||||
|
||||
/**
|
||||
* 简单的 glob 模式匹配
|
||||
* 支持 ** (任意目录) 和 * (任意字符)
|
||||
*/
|
||||
function globToRegex(pattern: string): RegExp {
|
||||
// 转义正则特殊字符,但保留 * 和 **
|
||||
let regexStr = pattern
|
||||
.replace(/[.+^${}()|[\]\\]/g, '\\$&') // 转义特殊字符
|
||||
.replace(/\*\*/g, '{{GLOBSTAR}}') // 临时替换 **
|
||||
.replace(/\*/g, '[^/]*') // * 匹配非 / 字符
|
||||
.replace(/\?/g, '[^/]') // ? 匹配单个非 / 字符
|
||||
.replace(/\{\{GLOBSTAR\}\}/g, '.*'); // ** 匹配任意字符包括 /
|
||||
|
||||
return new RegExp(`^${regexStr}$`, 'i');
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否应该忽略的目录/文件
|
||||
*/
|
||||
function shouldIgnore(name: string): boolean {
|
||||
const ignorePatterns = [
|
||||
'node_modules',
|
||||
'.git',
|
||||
'.svn',
|
||||
'.hg',
|
||||
'dist',
|
||||
'build',
|
||||
'coverage',
|
||||
'.cache',
|
||||
'.vscode',
|
||||
'.idea',
|
||||
'__pycache__',
|
||||
'.pytest_cache',
|
||||
'.mypy_cache',
|
||||
'venv',
|
||||
'.venv',
|
||||
'target', // Rust
|
||||
'vendor', // Go
|
||||
];
|
||||
return name.startsWith('.') || ignorePatterns.includes(name);
|
||||
}
|
||||
|
||||
interface FileInfo {
|
||||
path: string;
|
||||
mtime: number;
|
||||
}
|
||||
|
||||
export const globTool: ToolWithMetadata = {
|
||||
name: 'glob',
|
||||
description: loadDescription('glob'),
|
||||
metadata: {
|
||||
name: 'glob',
|
||||
category: 'filesystem',
|
||||
description: '使用 glob 模式匹配文件',
|
||||
keywords: ['glob', 'pattern', 'match', 'file', 'search', '模式', '匹配', '文件'],
|
||||
deferLoading: false, // 常用工具,不延迟加载
|
||||
},
|
||||
parameters: {
|
||||
pattern: {
|
||||
type: 'string',
|
||||
description: '要匹配的 glob 模式(如 "**/*.ts" 或 "src/**/*.js")',
|
||||
required: true,
|
||||
},
|
||||
path: {
|
||||
type: 'string',
|
||||
description:
|
||||
'搜索的目录。如果不指定,使用当前工作目录。重要:省略此字段使用默认目录,不要输入 "undefined" 或 "null"。',
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
|
||||
const pattern = params.pattern as string;
|
||||
const searchPath = params.path as string | undefined;
|
||||
const cwd = process.cwd();
|
||||
|
||||
// 解析搜索目录
|
||||
let searchDir: string;
|
||||
if (searchPath) {
|
||||
searchDir = path.isAbsolute(searchPath) ? searchPath : path.resolve(cwd, searchPath);
|
||||
} else {
|
||||
searchDir = cwd;
|
||||
}
|
||||
|
||||
// 权限检查
|
||||
const permissionManager = getPermissionManager();
|
||||
const permResult = await permissionManager.checkFilePermission({
|
||||
operation: 'search',
|
||||
path: searchDir,
|
||||
workdir: cwd,
|
||||
});
|
||||
|
||||
if (!permResult.allowed) {
|
||||
if (permResult.needsConfirmation) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `需要用户确认: 搜索目录 ${searchDir}\n原因: ${permResult.reason || '需要权限确认'}`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `权限被拒绝: ${permResult.reason || '不允许搜索此目录'}`,
|
||||
};
|
||||
}
|
||||
|
||||
const limit = 100;
|
||||
const files: FileInfo[] = [];
|
||||
let truncated = false;
|
||||
|
||||
// 编译 glob 模式
|
||||
const regex = globToRegex(pattern);
|
||||
|
||||
async function searchRecursive(dir: string, relativePath: string = ''): Promise<void> {
|
||||
if (files.length >= limit) {
|
||||
truncated = true;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const entries = await fs.readdir(dir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
if (files.length >= limit) {
|
||||
truncated = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const entryRelPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
// 忽略特定目录
|
||||
if (!shouldIgnore(entry.name)) {
|
||||
await searchRecursive(fullPath, entryRelPath);
|
||||
}
|
||||
} else {
|
||||
// 检查文件是否匹配模式
|
||||
if (regex.test(entryRelPath)) {
|
||||
try {
|
||||
const stat = await fs.stat(fullPath);
|
||||
files.push({
|
||||
path: fullPath,
|
||||
mtime: stat.mtimeMs,
|
||||
});
|
||||
} catch {
|
||||
// 忽略无法访问的文件
|
||||
files.push({
|
||||
path: fullPath,
|
||||
mtime: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// 忽略权限错误等
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// 检查搜索目录是否存在
|
||||
const dirStat = await fs.stat(searchDir);
|
||||
if (!dirStat.isDirectory()) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `路径不是目录: ${searchDir}`,
|
||||
};
|
||||
}
|
||||
|
||||
await searchRecursive(searchDir);
|
||||
|
||||
// 按修改时间排序(最新的在前)
|
||||
files.sort((a, b) => b.mtime - a.mtime);
|
||||
|
||||
const output: string[] = [];
|
||||
|
||||
if (files.length === 0) {
|
||||
output.push('No files found');
|
||||
} else {
|
||||
output.push(...files.map((f) => f.path));
|
||||
if (truncated) {
|
||||
output.push('');
|
||||
output.push('(Results are truncated. Consider using a more specific path or pattern.)');
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: output.join('\n'),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -10,6 +10,7 @@ export { createDirectoryTool } from './create_directory.js';
|
||||
|
||||
// 搜索
|
||||
export { searchFilesTool } from './search_files.js';
|
||||
export { globTool } from './glob.js';
|
||||
export { grepContentTool } from './grep_content.js';
|
||||
|
||||
// 文件信息
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
listDirTool,
|
||||
createDirectoryTool,
|
||||
searchFilesTool,
|
||||
globTool,
|
||||
grepContentTool,
|
||||
getFileInfoTool,
|
||||
moveFileTool,
|
||||
@@ -88,6 +89,7 @@ const allToolsWithMetadata: ToolWithMetadata[] = [
|
||||
listDirTool,
|
||||
createDirectoryTool,
|
||||
searchFilesTool,
|
||||
globTool,
|
||||
grepContentTool,
|
||||
getFileInfoTool,
|
||||
moveFileTool,
|
||||
|
||||
@@ -17,6 +17,7 @@ const TOOL_CATEGORY_MAP: Record<string, string> = {
|
||||
list_directory: 'filesystem',
|
||||
create_directory: 'filesystem',
|
||||
search_files: 'filesystem',
|
||||
glob: 'filesystem',
|
||||
grep_content: 'filesystem',
|
||||
get_file_info: 'filesystem',
|
||||
move_file: 'filesystem',
|
||||
|
||||
Reference in New Issue
Block a user