@saber2pr/ai-agent 0.0.44 → 0.0.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/core/agent-graph.js +1 -4
- package/lib/tools/filesystem/index.js +161 -116
- package/lib/tools/ts-lsp/index.js +20 -17
- package/package.json +1 -1
package/lib/core/agent-graph.js
CHANGED
|
@@ -426,12 +426,9 @@ class McpGraphAgent {
|
|
|
426
426
|
3. **禁止空操作**:如果你认为任务已完成或不需要调用工具,请不要输出任何 Action 结构。严禁使用 "None"、"null" 或空字符串作为工具名称。
|
|
427
427
|
|
|
428
428
|
# 🎯 核心指令
|
|
429
|
-
1.
|
|
429
|
+
1. **任务终结判定**:当你已经读取了用户要求的文件、回答了问题或完成了代码编写时,必须立即提供最终回复。
|
|
430
430
|
2. **回复格式**:任务完成时,请以 "Final Answer:" 开头进行总结,此时不再调用任何工具。
|
|
431
431
|
|
|
432
|
-
# 📝 审计任务专项
|
|
433
|
-
1. 避免在同一个文件上陷入无限循环尝试。
|
|
434
|
-
2. 优先通过 \`list_directory\` 了解全局,再深入具体文件。
|
|
435
432
|
{extraPrompt}`;
|
|
436
433
|
// 2. 核心逻辑:处理消息上下文
|
|
437
434
|
let inputMessages;
|
|
@@ -4,7 +4,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.getFilesystemTools = void 0;
|
|
7
|
-
/* eslint-disable no-continue */
|
|
8
7
|
const promises_1 = __importDefault(require("fs/promises"));
|
|
9
8
|
const minimatch_1 = require("minimatch");
|
|
10
9
|
const path_1 = __importDefault(require("path"));
|
|
@@ -27,25 +26,9 @@ const WriteFileArgsSchema = zod_1.z.object({
|
|
|
27
26
|
path: zod_1.z.string(),
|
|
28
27
|
content: zod_1.z.string(),
|
|
29
28
|
});
|
|
30
|
-
const EditOperation = zod_1.z.object({
|
|
31
|
-
oldText: zod_1.z.string().describe('Text to search for - must match exactly'),
|
|
32
|
-
newText: zod_1.z.string().describe('Text to replace with'),
|
|
33
|
-
});
|
|
34
|
-
const EditFileArgsSchema = zod_1.z.object({
|
|
35
|
-
path: zod_1.z.string(),
|
|
36
|
-
edits: zod_1.z.array(EditOperation),
|
|
37
|
-
dryRun: zod_1.z
|
|
38
|
-
.boolean()
|
|
39
|
-
.optional()
|
|
40
|
-
.default(false)
|
|
41
|
-
.describe('Preview changes using git-style diff format'),
|
|
42
|
-
});
|
|
43
29
|
const CreateDirectoryArgsSchema = zod_1.z.object({
|
|
44
30
|
path: zod_1.z.string(),
|
|
45
31
|
});
|
|
46
|
-
const ListDirectoryArgsSchema = zod_1.z.object({
|
|
47
|
-
path: zod_1.z.string(),
|
|
48
|
-
});
|
|
49
32
|
const ListDirectoryWithSizesArgsSchema = zod_1.z.object({
|
|
50
33
|
path: zod_1.z.string(),
|
|
51
34
|
sortBy: zod_1.z
|
|
@@ -57,6 +40,7 @@ const ListDirectoryWithSizesArgsSchema = zod_1.z.object({
|
|
|
57
40
|
const DirectoryTreeArgsSchema = zod_1.z.object({
|
|
58
41
|
path: zod_1.z.string(),
|
|
59
42
|
excludePatterns: zod_1.z.array(zod_1.z.string()).optional().default([]),
|
|
43
|
+
depth: zod_1.z.number().optional().default(2).describe('递归深度,默认 2 层。增加深度会消耗更多 Token'),
|
|
60
44
|
});
|
|
61
45
|
const MoveFileArgsSchema = zod_1.z.object({
|
|
62
46
|
source: zod_1.z.string(),
|
|
@@ -71,9 +55,19 @@ const GetFileInfoArgsSchema = zod_1.z.object({
|
|
|
71
55
|
path: zod_1.z.string(),
|
|
72
56
|
});
|
|
73
57
|
const GrepSearchArgsSchema = zod_1.z.object({
|
|
74
|
-
path: zod_1.z.string().describe('
|
|
75
|
-
query: zod_1.z.string().describe('
|
|
76
|
-
includePattern: zod_1.z.string().optional().default('**/*').describe('
|
|
58
|
+
path: zod_1.z.string().describe('搜索的起始目录路径'),
|
|
59
|
+
query: zod_1.z.string().describe('要搜索的文本关键字'),
|
|
60
|
+
includePattern: zod_1.z.string().optional().default('**/*').describe('匹配模式,例如 "**/*.ts"'),
|
|
61
|
+
maxFiles: zod_1.z.number().optional().default(100).describe('最大扫描文件数,防止大型项目超时'),
|
|
62
|
+
});
|
|
63
|
+
const PatchEditArgsSchema = zod_1.z.object({
|
|
64
|
+
path: zod_1.z.string().describe('文件路径'),
|
|
65
|
+
patches: zod_1.z.array(zod_1.z.object({
|
|
66
|
+
startLine: zod_1.z.number().describe('起始行号(包含)'),
|
|
67
|
+
endLine: zod_1.z.number().describe('结束行号(包含)'),
|
|
68
|
+
replacement: zod_1.z.string().describe('要插入的新代码内容'),
|
|
69
|
+
originalSnippet: zod_1.z.string().optional().describe('可选:该行范围内的原始代码片段,用于二次校验防止行号偏移'),
|
|
70
|
+
})).describe('补丁列表。注意:若有多个补丁,建议从文件尾部向头部执行,或确保行号不重叠'),
|
|
77
71
|
});
|
|
78
72
|
const getFilesystemTools = (targetDir) => {
|
|
79
73
|
(0, lib_1.setAllowedDirectories)([targetDir]);
|
|
@@ -97,44 +91,48 @@ const getFilesystemTools = (targetDir) => {
|
|
|
97
91
|
};
|
|
98
92
|
const readTextFileTool = (0, createTool_1.createTool)({
|
|
99
93
|
name: 'read_text_file',
|
|
100
|
-
description: '
|
|
101
|
-
'Handles various text encodings and provides detailed error messages ' +
|
|
102
|
-
'if the file cannot be read. Use this tool when you need to examine ' +
|
|
103
|
-
"the contents of a single file. Use the 'head' parameter to read only " +
|
|
104
|
-
"the first N lines of a file, or the 'tail' parameter to read only " +
|
|
105
|
-
'the last N lines of a file. Operates on the file as text regardless of extension.',
|
|
94
|
+
description: '读取文件全文。若超过100行则禁止使用,必须改用 read_file_range。支持 head/tail 参数。',
|
|
106
95
|
parameters: ReadTextFileArgsSchema,
|
|
107
96
|
handler: readTextFileHandler,
|
|
108
97
|
});
|
|
109
98
|
const readMultipleFilesTool = (0, createTool_1.createTool)({
|
|
110
99
|
name: 'read_multiple_files',
|
|
111
|
-
description: '
|
|
112
|
-
'
|
|
113
|
-
|
|
114
|
-
"path as a reference. Failed reads for individual files won't stop " +
|
|
115
|
-
'the entire operation. Only works within allowed directories.',
|
|
100
|
+
description: '同时读取多个文件的内容。当你需要对比多个文件或分析跨文件关联时使用。' +
|
|
101
|
+
'注意:为了防止 Token 溢出,本工具一次最多读取 10 个文件,且每个文件仅展示前 6000 字符。' +
|
|
102
|
+
'若需查看完整大文件或特定逻辑,请改用 read_file_range。',
|
|
116
103
|
parameters: ReadMultipleFilesArgsSchema,
|
|
117
104
|
handler: async (args) => {
|
|
118
|
-
|
|
105
|
+
// 保护 1:文件数量限制 (防止 AI 一次传入几十个文件)
|
|
106
|
+
const MAX_FILES = 10;
|
|
107
|
+
const pathsToRead = args.paths.slice(0, MAX_FILES);
|
|
108
|
+
const isTruncatedByCount = args.paths.length > MAX_FILES;
|
|
109
|
+
// 保护 2:单文件字符数限制 (防止读入超大型二进制或日志文件)
|
|
110
|
+
const MAX_CHARS_PER_FILE = 6000;
|
|
111
|
+
const results = await Promise.all(pathsToRead.map(async (filePath) => {
|
|
119
112
|
try {
|
|
113
|
+
// 沿用你现有的路径验证逻辑
|
|
120
114
|
const validPath = await (0, lib_1.validatePath)(targetDir, filePath);
|
|
121
115
|
const content = await (0, lib_1.readFileContent)(validPath);
|
|
116
|
+
if (content.length > MAX_CHARS_PER_FILE) {
|
|
117
|
+
return `${filePath} (内容已截断):\n${content.substring(0, MAX_CHARS_PER_FILE)}\n\n[... 内容过长,仅展示前 ${MAX_CHARS_PER_FILE} 字符。若需查看后续内容,请使用 read_file_range 指定行号读取 ...]`;
|
|
118
|
+
}
|
|
122
119
|
return `${filePath}:\n${content}\n`;
|
|
123
120
|
}
|
|
124
121
|
catch (error) {
|
|
125
122
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
126
|
-
return `${filePath}:
|
|
123
|
+
return `${filePath}: 读取失败 - ${errorMessage}`;
|
|
127
124
|
}
|
|
128
125
|
}));
|
|
129
|
-
|
|
126
|
+
let text = results.join('\n---\n');
|
|
127
|
+
if (isTruncatedByCount) {
|
|
128
|
+
text += `\n\n⚠️ 注意:一次请求最多处理 ${MAX_FILES} 个文件。剩余 ${args.paths.length - MAX_FILES} 个文件未读取,请分批请求。`;
|
|
129
|
+
}
|
|
130
130
|
return text;
|
|
131
131
|
},
|
|
132
132
|
});
|
|
133
133
|
const writeFileTool = (0, createTool_1.createTool)({
|
|
134
134
|
name: 'write_file',
|
|
135
|
-
description: '
|
|
136
|
-
'Use with caution as it will overwrite existing files without warning. ' +
|
|
137
|
-
'Handles text content with proper encoding. Only works within allowed directories.',
|
|
135
|
+
description: '仅用于创建新文件。严禁用于修改现有源代码。',
|
|
138
136
|
parameters: WriteFileArgsSchema,
|
|
139
137
|
handler: async (args) => {
|
|
140
138
|
const validPath = await (0, lib_1.validatePath)(targetDir, args.path);
|
|
@@ -143,18 +141,6 @@ const getFilesystemTools = (targetDir) => {
|
|
|
143
141
|
return text;
|
|
144
142
|
},
|
|
145
143
|
});
|
|
146
|
-
const editFileTool = (0, createTool_1.createTool)({
|
|
147
|
-
name: 'edit_file',
|
|
148
|
-
description: 'Make line-based edits to a text file. Each edit replaces exact line sequences ' +
|
|
149
|
-
'with new content. Returns a git-style diff showing the changes made. ' +
|
|
150
|
-
'Only works within allowed directories.',
|
|
151
|
-
parameters: EditFileArgsSchema,
|
|
152
|
-
handler: async (args) => {
|
|
153
|
-
const validPath = await (0, lib_1.validatePath)(targetDir, args.path);
|
|
154
|
-
const result = await (0, lib_1.applyFileEdits)(validPath, args.edits, args.dryRun);
|
|
155
|
-
return result;
|
|
156
|
-
},
|
|
157
|
-
});
|
|
158
144
|
const createDirectoryTool = (0, createTool_1.createTool)({
|
|
159
145
|
name: 'create_directory',
|
|
160
146
|
description: 'Create a new directory or ensure a directory exists. Can create multiple ' +
|
|
@@ -169,24 +155,8 @@ const getFilesystemTools = (targetDir) => {
|
|
|
169
155
|
return text;
|
|
170
156
|
},
|
|
171
157
|
});
|
|
172
|
-
const listDirectoryTool = (0, createTool_1.createTool)({
|
|
173
|
-
name: 'list_directory',
|
|
174
|
-
description: 'Get a detailed listing of all files and directories in a specified path. ' +
|
|
175
|
-
'Results clearly distinguish between files and directories with [FILE] and [DIR] ' +
|
|
176
|
-
'prefixes. This tool is essential for understanding directory structure and ' +
|
|
177
|
-
'finding specific files within a directory. Only works within allowed directories.',
|
|
178
|
-
parameters: ListDirectoryArgsSchema,
|
|
179
|
-
handler: async (args) => {
|
|
180
|
-
const validPath = await (0, lib_1.validatePath)(targetDir, args.path);
|
|
181
|
-
const entries = await promises_1.default.readdir(validPath, { withFileTypes: true });
|
|
182
|
-
const formatted = entries
|
|
183
|
-
.map(entry => `${entry.isDirectory() ? '[DIR]' : '[FILE]'} ${entry.name}`)
|
|
184
|
-
.join('\n');
|
|
185
|
-
return formatted;
|
|
186
|
-
},
|
|
187
|
-
});
|
|
188
158
|
const listDirectoryWithSizesTool = (0, createTool_1.createTool)({
|
|
189
|
-
name: '
|
|
159
|
+
name: 'list_directory',
|
|
190
160
|
description: 'Get a detailed listing of all files and directories in a specified path, including sizes. ' +
|
|
191
161
|
'Results clearly distinguish between files and directories with [FILE] and [DIR] ' +
|
|
192
162
|
'prefixes. This tool is useful for understanding directory structure and ' +
|
|
@@ -241,46 +211,36 @@ const getFilesystemTools = (targetDir) => {
|
|
|
241
211
|
});
|
|
242
212
|
const directoryTreeTool = (0, createTool_1.createTool)({
|
|
243
213
|
name: 'directory_tree',
|
|
244
|
-
description: '
|
|
245
|
-
|
|
246
|
-
'Files have no children array, while directories always have a children array (which may be empty). ' +
|
|
247
|
-
'The output is formatted with 2-space indentation for readability. Only works within allowed directories.',
|
|
214
|
+
description: '获取目录的递归树状 JSON 结构。' +
|
|
215
|
+
'默认仅展示 2 层深度以节省 Token。如果需要看更深层级,请调大 depth 参数。',
|
|
248
216
|
parameters: DirectoryTreeArgsSchema,
|
|
249
217
|
handler: async (args) => {
|
|
250
218
|
const rootPath = args.path;
|
|
251
|
-
async function buildTree(currentPath, excludePatterns = []) {
|
|
219
|
+
async function buildTree(currentPath, currentDepth, maxDepth, excludePatterns = []) {
|
|
220
|
+
if (currentDepth > maxDepth)
|
|
221
|
+
return []; // 深度限制
|
|
252
222
|
const validPath = await (0, lib_1.validatePath)(targetDir, currentPath);
|
|
253
223
|
const entries = await promises_1.default.readdir(validPath, { withFileTypes: true });
|
|
254
224
|
const result = [];
|
|
255
225
|
for (const entry of entries) {
|
|
256
226
|
const relativePath = path_1.default.relative(rootPath, path_1.default.join(currentPath, entry.name));
|
|
257
|
-
const shouldExclude = excludePatterns.some(pattern => {
|
|
258
|
-
if (pattern.includes('*')) {
|
|
259
|
-
return (0, minimatch_1.minimatch)(relativePath, pattern, { dot: true });
|
|
260
|
-
}
|
|
261
|
-
// For files: match exact name or as part of path
|
|
262
|
-
// For directories: match as directory path
|
|
263
|
-
return ((0, minimatch_1.minimatch)(relativePath, pattern, { dot: true }) ||
|
|
264
|
-
(0, minimatch_1.minimatch)(relativePath, `**/${pattern}`, { dot: true }) ||
|
|
265
|
-
(0, minimatch_1.minimatch)(relativePath, `**/${pattern}/**`, { dot: true }));
|
|
266
|
-
});
|
|
227
|
+
const shouldExclude = excludePatterns.some(pattern => (0, minimatch_1.minimatch)(relativePath, pattern, { dot: true }));
|
|
267
228
|
if (shouldExclude)
|
|
268
229
|
continue;
|
|
269
230
|
const entryData = {
|
|
270
231
|
name: entry.name,
|
|
271
232
|
type: entry.isDirectory() ? 'directory' : 'file',
|
|
272
233
|
};
|
|
273
|
-
if (entry.isDirectory()) {
|
|
234
|
+
if (entry.isDirectory() && currentDepth < maxDepth) {
|
|
274
235
|
const subPath = path_1.default.join(currentPath, entry.name);
|
|
275
|
-
entryData.children = await buildTree(subPath, excludePatterns);
|
|
236
|
+
entryData.children = await buildTree(subPath, currentDepth + 1, maxDepth, excludePatterns);
|
|
276
237
|
}
|
|
277
238
|
result.push(entryData);
|
|
278
239
|
}
|
|
279
240
|
return result;
|
|
280
241
|
}
|
|
281
|
-
const treeData = await buildTree(rootPath, args.excludePatterns);
|
|
282
|
-
|
|
283
|
-
return text;
|
|
242
|
+
const treeData = await buildTree(rootPath, 1, args.depth, args.excludePatterns);
|
|
243
|
+
return JSON.stringify(treeData, null, 2);
|
|
284
244
|
},
|
|
285
245
|
});
|
|
286
246
|
const moveFileTool = (0, createTool_1.createTool)({
|
|
@@ -301,7 +261,8 @@ const getFilesystemTools = (targetDir) => {
|
|
|
301
261
|
const searchFilesTool = (0, createTool_1.createTool)({
|
|
302
262
|
name: 'search_files',
|
|
303
263
|
description: 'Search for files matching a specific pattern in a specified path. ' +
|
|
304
|
-
'Returns a list of files that match the pattern. Only works within allowed directories.'
|
|
264
|
+
'Returns a list of files that match the pattern. Only works within allowed directories.' +
|
|
265
|
+
'Used only for filename search',
|
|
305
266
|
parameters: SearchFilesArgsSchema,
|
|
306
267
|
handler: async (args) => {
|
|
307
268
|
const validPath = await (0, lib_1.validatePath)(targetDir, args.path);
|
|
@@ -314,68 +275,152 @@ const getFilesystemTools = (targetDir) => {
|
|
|
314
275
|
});
|
|
315
276
|
const getFileInfoTool = (0, createTool_1.createTool)({
|
|
316
277
|
name: 'get_file_info',
|
|
317
|
-
description: '
|
|
318
|
-
'Only works within allowed directories.',
|
|
278
|
+
description: '查看文件元数据(大小、行数、修改时间)。读取大文件前务必先调用此工具。',
|
|
319
279
|
parameters: GetFileInfoArgsSchema,
|
|
320
280
|
handler: async (args) => {
|
|
321
281
|
const validPath = await (0, lib_1.validatePath)(targetDir, args.path);
|
|
322
|
-
const
|
|
323
|
-
|
|
282
|
+
const stats = await promises_1.default.stat(validPath);
|
|
283
|
+
// 计算行数:读取内容并按换行符分割
|
|
284
|
+
// 注意:对于极大的文件,这种方式可能稍慢,但对普通源代码文件非常有效
|
|
285
|
+
const content = await promises_1.default.readFile(validPath, 'utf-8');
|
|
286
|
+
const lineCount = content.split('\n').length;
|
|
287
|
+
const info = {
|
|
288
|
+
size: `${(stats.size / 1024).toFixed(2)} KB`,
|
|
289
|
+
lineCount: lineCount, // 新增行号字段
|
|
290
|
+
mtime: stats.mtime.toLocaleString(),
|
|
291
|
+
type: path_1.default.extname(validPath) || 'unknown'
|
|
292
|
+
};
|
|
293
|
+
return Object.entries(info)
|
|
324
294
|
.map(([key, value]) => `${key}: ${value}`)
|
|
325
295
|
.join('\n');
|
|
326
|
-
return text;
|
|
327
296
|
},
|
|
328
297
|
});
|
|
329
298
|
const grepSearchTool = (0, createTool_1.createTool)({
|
|
330
299
|
name: 'grep_search',
|
|
331
|
-
description: '
|
|
332
|
-
'
|
|
333
|
-
'
|
|
300
|
+
description: '在指定目录的文件内容中搜索关键字。' +
|
|
301
|
+
'该工具会返回包含关键字的文件路径及匹配行的预览。' +
|
|
302
|
+
'请尽量通过 includePattern 缩小搜索范围。',
|
|
334
303
|
parameters: GrepSearchArgsSchema,
|
|
335
304
|
handler: async (args) => {
|
|
336
305
|
const startPath = await (0, lib_1.validatePath)(targetDir, args.path);
|
|
337
|
-
|
|
338
|
-
|
|
306
|
+
const allFiles = await (0, lib_1.searchFilesWithValidation)(targetDir, startPath, args.includePattern, [targetDir], { excludePatterns: ['node_modules', 'dist', '.git', 'build'] });
|
|
307
|
+
// 限制扫描文件数,防止爆炸
|
|
308
|
+
const filesToScan = allFiles.slice(0, args.maxFiles);
|
|
339
309
|
const matches = [];
|
|
340
|
-
// 2. 并发读取文件内容进行关键词匹配 (限制并发数为 10,防止内存或句柄爆炸)
|
|
341
310
|
const concurrencyLimit = 10;
|
|
342
|
-
for (let i = 0; i <
|
|
343
|
-
const chunk =
|
|
311
|
+
for (let i = 0; i < filesToScan.length; i += concurrencyLimit) {
|
|
312
|
+
const chunk = filesToScan.slice(i, i + concurrencyLimit);
|
|
344
313
|
await Promise.all(chunk.map(async (filePath) => {
|
|
345
314
|
try {
|
|
346
|
-
// 注意:这里确保只搜索文件,不搜索目录
|
|
347
315
|
const stats = await promises_1.default.stat(filePath);
|
|
348
316
|
if (!stats.isFile())
|
|
349
317
|
return;
|
|
350
318
|
const content = await (0, lib_1.readFileContent)(filePath);
|
|
351
319
|
if (content.includes(args.query)) {
|
|
352
|
-
|
|
353
|
-
|
|
320
|
+
const relativePath = path_1.default.relative(targetDir, filePath);
|
|
321
|
+
// 找到匹配的那一行(预览用)
|
|
322
|
+
const lines = content.split('\n');
|
|
323
|
+
const matchLineIndex = lines.findIndex(l => l.includes(args.query));
|
|
324
|
+
matches.push(`${relativePath} (Line ${matchLineIndex + 1}: "${lines[matchLineIndex].trim().substring(0, 100)}")`);
|
|
354
325
|
}
|
|
355
326
|
}
|
|
356
|
-
catch {
|
|
357
|
-
// 忽略读取失败的文件(如二进制文件或无权限文件)
|
|
358
|
-
}
|
|
327
|
+
catch { /* 忽略错误文件 */ }
|
|
359
328
|
}));
|
|
360
329
|
}
|
|
361
|
-
|
|
362
|
-
?
|
|
363
|
-
:
|
|
330
|
+
let response = matches.length > 0
|
|
331
|
+
? `找到关键词 "${args.query}" 的位置如下:\n${matches.join('\n')}`
|
|
332
|
+
: `未找到包含 "${args.query}" 的内容。`;
|
|
333
|
+
if (allFiles.length > args.maxFiles) {
|
|
334
|
+
response += `\n\n注意:搜索已达到限制,仅扫描了前 ${args.maxFiles} 个文件。若未找到结果,请提供更精确的 path 或 includePattern。`;
|
|
335
|
+
}
|
|
336
|
+
return response;
|
|
337
|
+
},
|
|
338
|
+
});
|
|
339
|
+
const readFileRangeTool = (0, createTool_1.createTool)({
|
|
340
|
+
name: 'read_file_range',
|
|
341
|
+
description: '精准读取指定行范围(包含行号前缀)。修改代码前或根据报错定位时必用。',
|
|
342
|
+
parameters: zod_1.z.object({
|
|
343
|
+
path: zod_1.z.string().describe('相对于目标目录的文件路径'),
|
|
344
|
+
startLine: zod_1.z.number().describe('起始行号(从 1 开始计)'),
|
|
345
|
+
endLine: zod_1.z.number().describe('结束行号'),
|
|
346
|
+
}),
|
|
347
|
+
handler: async (args) => {
|
|
348
|
+
// 1. 验证路径安全(沿用你代码中的 validatePath 逻辑)
|
|
349
|
+
const validPath = await (0, lib_1.validatePath)(targetDir, args.path);
|
|
350
|
+
try {
|
|
351
|
+
const content = await promises_1.default.readFile(validPath, 'utf-8');
|
|
352
|
+
const lines = content.split('\n');
|
|
353
|
+
const totalLines = lines.length;
|
|
354
|
+
// 2. 边界保护:确保行号不越界
|
|
355
|
+
const start = Math.max(1, args.startLine);
|
|
356
|
+
const end = Math.min(totalLines, args.endLine);
|
|
357
|
+
if (start > totalLines) {
|
|
358
|
+
return `错误:文件仅有 ${totalLines} 行,起始行号 ${start} 超出范围。`;
|
|
359
|
+
}
|
|
360
|
+
if (start > end) {
|
|
361
|
+
return `错误:起始行号 ${start} 不能大于结束行号 ${end}。`;
|
|
362
|
+
}
|
|
363
|
+
// 3. 截取并添加行号索引(核心:增强 AI 的位置感)
|
|
364
|
+
const selectedLines = lines.slice(start - 1, end);
|
|
365
|
+
const formattedContent = selectedLines
|
|
366
|
+
.map((line, index) => `${start + index}| ${line}`)
|
|
367
|
+
.join('\n');
|
|
368
|
+
return `[文件: ${args.path} | 第 ${start} 至 ${end} 行 / 共 ${totalLines} 行]\n${formattedContent}`;
|
|
369
|
+
}
|
|
370
|
+
catch (error) {
|
|
371
|
+
return `读取文件范围失败: ${error.message}`;
|
|
372
|
+
}
|
|
373
|
+
},
|
|
374
|
+
});
|
|
375
|
+
const editFileTool = (0, createTool_1.createTool)({
|
|
376
|
+
name: 'edit_file',
|
|
377
|
+
description: '基于行号范围替换代码。修改逻辑的唯一工具。调用前须通过 read_file_range 获取最新行号。支持删除(空内容)或单行替换。',
|
|
378
|
+
parameters: PatchEditArgsSchema,
|
|
379
|
+
handler: async (args) => {
|
|
380
|
+
const validPath = await (0, lib_1.validatePath)(targetDir, args.path);
|
|
381
|
+
try {
|
|
382
|
+
const content = await promises_1.default.readFile(validPath, 'utf-8');
|
|
383
|
+
let lines = content.split('\n');
|
|
384
|
+
// 按起始行号从大到小排序,这样修改前面的行不会影响后面待修改行的索引
|
|
385
|
+
const sortedPatches = [...args.patches].sort((a, b) => b.startLine - a.startLine);
|
|
386
|
+
for (const patch of sortedPatches) {
|
|
387
|
+
// 校验行号合法性
|
|
388
|
+
if (patch.startLine < 1 || patch.endLine > lines.length || patch.startLine > patch.endLine) {
|
|
389
|
+
return `错误:行号范围 ${patch.startLine}-${patch.endLine} 超出文件实际范围 (1-${lines.length})`;
|
|
390
|
+
}
|
|
391
|
+
// 可选:二次校验(防止 AI 记忆了错误的行号)
|
|
392
|
+
if (patch.originalSnippet) {
|
|
393
|
+
const currentText = lines.slice(patch.startLine - 1, patch.endLine).join('\n');
|
|
394
|
+
// 模糊对比,如果差异太大则报错
|
|
395
|
+
if (!currentText.includes(patch.originalSnippet.trim()) && currentText.trim().length > 0) {
|
|
396
|
+
return `警告:第 ${patch.startLine} 行的内容已发生变动,与你预想的代码不符。请重新读取文件获取最新行号。`;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
// 执行替换:splice(开始索引, 删除数量, 替换内容)
|
|
400
|
+
// 索引需要减 1
|
|
401
|
+
lines.splice(patch.startLine - 1, (patch.endLine - patch.startLine) + 1, patch.replacement);
|
|
402
|
+
}
|
|
403
|
+
await promises_1.default.writeFile(validPath, lines.join('\n'), 'utf-8');
|
|
404
|
+
return `成功通过行号更新了 ${args.path} 的 ${args.patches.length} 处代码。`;
|
|
405
|
+
}
|
|
406
|
+
catch (error) {
|
|
407
|
+
return `Patch 失败: ${error.message}`;
|
|
408
|
+
}
|
|
364
409
|
},
|
|
365
410
|
});
|
|
366
411
|
return [
|
|
412
|
+
readFileRangeTool,
|
|
413
|
+
editFileTool,
|
|
414
|
+
directoryTreeTool,
|
|
415
|
+
listDirectoryWithSizesTool,
|
|
416
|
+
grepSearchTool,
|
|
417
|
+
getFileInfoTool,
|
|
367
418
|
readTextFileTool,
|
|
368
419
|
readMultipleFilesTool,
|
|
420
|
+
searchFilesTool,
|
|
369
421
|
writeFileTool,
|
|
370
|
-
editFileTool,
|
|
371
422
|
createDirectoryTool,
|
|
372
|
-
listDirectoryTool,
|
|
373
|
-
listDirectoryWithSizesTool,
|
|
374
|
-
directoryTreeTool,
|
|
375
423
|
moveFileTool,
|
|
376
|
-
searchFilesTool,
|
|
377
|
-
getFileInfoTool,
|
|
378
|
-
grepSearchTool,
|
|
379
424
|
];
|
|
380
425
|
};
|
|
381
426
|
exports.getFilesystemTools = getFilesystemTools;
|
|
@@ -8,8 +8,19 @@ const getTsLspTools = (targetDir) => {
|
|
|
8
8
|
const engine = new ts_context_mcp_1.PromptEngine(targetDir);
|
|
9
9
|
return [
|
|
10
10
|
(0, createTool_1.createTool)({
|
|
11
|
-
name:
|
|
12
|
-
description:
|
|
11
|
+
name: 'get_method_body',
|
|
12
|
+
description: '【仅限TS/JS】通过方法名提取代码块。比行号读取更抗干扰,参考逻辑时首选。',
|
|
13
|
+
parameters: zod_1.z.object({
|
|
14
|
+
filePath: zod_1.z.string().describe('文件相对路径'),
|
|
15
|
+
methodName: zod_1.z.string().describe('方法名或函数名'),
|
|
16
|
+
}),
|
|
17
|
+
handler: async ({ filePath, methodName }) => {
|
|
18
|
+
return engine.getMethodImplementation(filePath, methodName);
|
|
19
|
+
},
|
|
20
|
+
}),
|
|
21
|
+
(0, createTool_1.createTool)({
|
|
22
|
+
name: 'get_repo_map',
|
|
23
|
+
description: '获取项目全局文件结构及导出清单,用于快速定位代码',
|
|
13
24
|
parameters: zod_1.z.object({}),
|
|
14
25
|
handler: async () => {
|
|
15
26
|
engine.refresh();
|
|
@@ -17,18 +28,18 @@ const getTsLspTools = (targetDir) => {
|
|
|
17
28
|
},
|
|
18
29
|
}),
|
|
19
30
|
(0, createTool_1.createTool)({
|
|
20
|
-
name:
|
|
21
|
-
description:
|
|
22
|
-
parameters: zod_1.z.object({ filePath: zod_1.z.string().describe(
|
|
31
|
+
name: 'analyze_deps',
|
|
32
|
+
description: '分析指定文件的依赖关系,支持 tsconfig 路径别名解析',
|
|
33
|
+
parameters: zod_1.z.object({ filePath: zod_1.z.string().describe('文件相对路径') }),
|
|
23
34
|
handler: async ({ filePath }) => engine.getDeps(filePath),
|
|
24
35
|
}),
|
|
25
36
|
(0, createTool_1.createTool)({
|
|
26
|
-
name:
|
|
27
|
-
description:
|
|
28
|
-
parameters: zod_1.z.object({ filePath: zod_1.z.string().describe(
|
|
37
|
+
name: 'read_skeleton',
|
|
38
|
+
description: '提取文件的结构定义(接口、类、方法签名),忽略具体实现以节省 Token',
|
|
39
|
+
parameters: zod_1.z.object({ filePath: zod_1.z.string().describe('文件相对路径') }),
|
|
29
40
|
handler: async (args) => {
|
|
30
41
|
const pathArg = args === null || args === void 0 ? void 0 : args.filePath;
|
|
31
|
-
if (typeof pathArg !==
|
|
42
|
+
if (typeof pathArg !== 'string' || pathArg.trim() === '') {
|
|
32
43
|
return `Error: 参数 'filePath' 无效。收到的是: ${JSON.stringify(pathArg)}`;
|
|
33
44
|
}
|
|
34
45
|
try {
|
|
@@ -41,14 +52,6 @@ const getTsLspTools = (targetDir) => {
|
|
|
41
52
|
}
|
|
42
53
|
},
|
|
43
54
|
}),
|
|
44
|
-
(0, createTool_1.createTool)({
|
|
45
|
-
name: "get_method_body",
|
|
46
|
-
description: "获取指定文件内某个方法或函数的完整实现代码",
|
|
47
|
-
parameters: zod_1.z.object({ filePath: zod_1.z.string().describe("文件相对路径"), methodName: zod_1.z.string().describe("方法名或函数名") }),
|
|
48
|
-
handler: async ({ filePath, methodName }) => {
|
|
49
|
-
return engine.getMethodImplementation(filePath, methodName);
|
|
50
|
-
},
|
|
51
|
-
}),
|
|
52
55
|
];
|
|
53
56
|
};
|
|
54
57
|
exports.getTsLspTools = getTsLspTools;
|