foliko 1.0.75 → 1.0.76
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/.claude/settings.local.json +159 -157
- package/cli/bin/foliko.js +12 -12
- package/cli/src/commands/chat.js +143 -143
- package/cli/src/commands/list.js +93 -93
- package/cli/src/index.js +75 -75
- package/cli/src/ui/chat-ui.js +201 -201
- package/cli/src/utils/ansi.js +40 -40
- package/cli/src/utils/markdown.js +292 -292
- package/examples/ambient-example.js +194 -194
- package/examples/basic.js +115 -115
- package/examples/bootstrap.js +121 -121
- package/examples/mcp-example.js +56 -56
- package/examples/skill-example.js +49 -49
- package/examples/test-chat.js +137 -137
- package/examples/test-mcp.js +85 -85
- package/examples/test-reload.js +59 -59
- package/examples/test-telegram.js +50 -50
- package/examples/test-tg-bot.js +45 -45
- package/examples/test-tg-simple.js +47 -47
- package/examples/test-tg.js +62 -62
- package/examples/test-think.js +43 -43
- package/examples/test-web-plugin.js +103 -103
- package/examples/test-weixin-feishu.js +103 -103
- package/examples/workflow.js +158 -158
- package/package.json +1 -1
- package/plugins/ai-plugin.js +102 -102
- package/plugins/ambient-agent/EventWatcher.js +113 -113
- package/plugins/ambient-agent/ExplorerLoop.js +640 -640
- package/plugins/ambient-agent/GoalManager.js +197 -197
- package/plugins/ambient-agent/Reflector.js +95 -95
- package/plugins/ambient-agent/StateStore.js +90 -90
- package/plugins/ambient-agent/constants.js +101 -101
- package/plugins/ambient-agent/index.js +579 -579
- package/plugins/audit-plugin.js +187 -187
- package/plugins/default-plugins.js +662 -662
- package/plugins/email/constants.js +64 -64
- package/plugins/email/handlers.js +461 -461
- package/plugins/email/index.js +278 -278
- package/plugins/email/monitor.js +269 -269
- package/plugins/email/parser.js +138 -138
- package/plugins/email/reply.js +151 -151
- package/plugins/email/utils.js +124 -124
- package/plugins/feishu-plugin.js +481 -481
- package/plugins/file-system-plugin.js +826 -826
- package/plugins/install-plugin.js +199 -199
- package/plugins/python-executor-plugin.js +367 -367
- package/plugins/python-plugin-loader.js +481 -481
- package/plugins/rules-plugin.js +294 -294
- package/plugins/scheduler-plugin.js +691 -691
- package/plugins/session-plugin.js +369 -369
- package/plugins/shell-executor-plugin.js +197 -197
- package/plugins/storage-plugin.js +240 -240
- package/plugins/subagent-plugin.js +845 -845
- package/plugins/telegram-plugin.js +482 -482
- package/plugins/think-plugin.js +345 -345
- package/plugins/tools-plugin.js +196 -196
- package/plugins/web-plugin.js +606 -606
- package/plugins/weixin-plugin.js +545 -545
- package/src/capabilities/index.js +11 -11
- package/src/capabilities/skill-manager.js +609 -609
- package/src/capabilities/workflow-engine.js +1109 -1109
- package/src/core/agent-chat.js +882 -882
- package/src/core/agent.js +892 -892
- package/src/core/framework.js +465 -465
- package/src/core/index.js +19 -19
- package/src/core/plugin-base.js +219 -219
- package/src/core/plugin-manager.js +863 -863
- package/src/core/provider.js +114 -114
- package/src/core/sub-agent-config.js +264 -264
- package/src/core/system-prompt-builder.js +120 -120
- package/src/core/tool-registry.js +517 -517
- package/src/core/tool-router.js +297 -297
- package/src/executors/executor-base.js +58 -58
- package/src/executors/mcp-executor.js +741 -741
- package/src/index.js +25 -25
- package/src/utils/circuit-breaker.js +301 -301
- package/src/utils/error-boundary.js +363 -363
- package/src/utils/error.js +374 -374
- package/src/utils/event-emitter.js +97 -97
- package/src/utils/id.js +133 -133
- package/src/utils/index.js +217 -217
- package/src/utils/logger.js +181 -181
- package/src/utils/plugin-helpers.js +90 -90
- package/src/utils/retry.js +122 -122
- package/src/utils/sandbox.js +292 -292
- package/test/tool-registry-validation.test.js +218 -218
- package/website/script.js +136 -136
- package/foliko-1.0.75.tgz +0 -0
|
@@ -1,826 +1,826 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* FileSystem 插件
|
|
3
|
-
* 提供文件系统操作的常用工具
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
const { Plugin } = require('../src/core/plugin-base')
|
|
7
|
-
|
|
8
|
-
class FileSystemPlugin extends Plugin {
|
|
9
|
-
constructor(config = {}) {
|
|
10
|
-
super()
|
|
11
|
-
this.name = 'file-system'
|
|
12
|
-
this.version = '1.0.0'
|
|
13
|
-
this.description = '文件系统工具插件'
|
|
14
|
-
this.priority = 5
|
|
15
|
-
this.system = true
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
install(framework) {
|
|
19
|
-
const { z } = require('zod')
|
|
20
|
-
const fs = require('fs')
|
|
21
|
-
const path = require('path')
|
|
22
|
-
const { exec } = require('child_process')
|
|
23
|
-
|
|
24
|
-
// 路径安全验证:防止路径穿越攻击
|
|
25
|
-
const validatePath = (filePath, allowOutsideCwd = false) => {
|
|
26
|
-
const resolved = path.resolve(filePath)
|
|
27
|
-
const cwd = process.cwd()
|
|
28
|
-
|
|
29
|
-
// 允许绝对路径且在允许列表中的路径(如果有的话)
|
|
30
|
-
if (allowOutsideCwd) {
|
|
31
|
-
return { valid: true, resolved }
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// 检查是否在 cwd 目录下
|
|
35
|
-
if (!resolved.startsWith(cwd)) {
|
|
36
|
-
return { valid: false, error: `路径不允许访问: ${filePath}` }
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return { valid: true, resolved }
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// 读取目录
|
|
43
|
-
framework.registerTool({
|
|
44
|
-
name: 'read_directory',
|
|
45
|
-
description: '读取目录内容,列出目录中的文件和子目录',
|
|
46
|
-
inputSchema: z.object({
|
|
47
|
-
path: z.string().optional().describe('目录路径'),
|
|
48
|
-
dirPath: z.string().optional().describe('目录路径(同path)'),
|
|
49
|
-
recursive: z.boolean().optional().describe('是否递归')
|
|
50
|
-
}),
|
|
51
|
-
execute: async (args, framework) => {
|
|
52
|
-
const dirPath = args.path || args.dirPath || '.'
|
|
53
|
-
const recursive = args.recursive || false
|
|
54
|
-
try {
|
|
55
|
-
const items = []
|
|
56
|
-
const readDir = (currentPath, depth = 0) => {
|
|
57
|
-
if (depth > 3 && recursive) return
|
|
58
|
-
const entries = fs.readdirSync(currentPath, { withFileTypes: true })
|
|
59
|
-
for (const entry of entries) {
|
|
60
|
-
const fullPath = path.join(currentPath, entry.name)
|
|
61
|
-
const relativePath = path.relative(process.cwd(), fullPath)
|
|
62
|
-
if (relativePath.includes('node_modules') || relativePath.includes('.git')) {
|
|
63
|
-
continue
|
|
64
|
-
}
|
|
65
|
-
items.push({
|
|
66
|
-
name: entry.name,
|
|
67
|
-
path: relativePath,
|
|
68
|
-
type: entry.isDirectory() ? 'directory' : 'file',
|
|
69
|
-
size: entry.isFile() ? fs.statSync(fullPath).size : null
|
|
70
|
-
})
|
|
71
|
-
if (entry.isDirectory() && recursive && depth < 3) {
|
|
72
|
-
readDir(fullPath, depth + 1)
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
readDir(dirPath)
|
|
77
|
-
return { success: true, dirPath, items: items.slice(0, 100), total: items.length }
|
|
78
|
-
} catch (error) {
|
|
79
|
-
return { success: false, error: error.message }
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
// 创建目录
|
|
85
|
-
framework.registerTool({
|
|
86
|
-
name: 'create_directory',
|
|
87
|
-
description: '创建新目录',
|
|
88
|
-
inputSchema: z.object({
|
|
89
|
-
path: z.string().optional().describe('目录路径'),
|
|
90
|
-
dirPath: z.string().optional().describe('目录路径(同path)')
|
|
91
|
-
}),
|
|
92
|
-
execute: async (args, framework) => {
|
|
93
|
-
const dirPath = args.path || args.dirPath
|
|
94
|
-
try {
|
|
95
|
-
fs.mkdirSync(dirPath, { recursive: true })
|
|
96
|
-
return { success: true, message: `目录已创建: ${dirPath}` }
|
|
97
|
-
} catch (error) {
|
|
98
|
-
return { success: false, error: error.message }
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
// 读取文件
|
|
104
|
-
framework.registerTool({
|
|
105
|
-
name: 'read_file',
|
|
106
|
-
description: '读取文件内容。path 是必填参数。',
|
|
107
|
-
inputSchema: z.object({
|
|
108
|
-
path: z.string().describe('文件路径(必须)'),
|
|
109
|
-
lines: z.number().optional().describe('只读取前 N 行')
|
|
110
|
-
}),
|
|
111
|
-
execute: async (args, framework) => {
|
|
112
|
-
const { path: filePath, lines } = args
|
|
113
|
-
if (!filePath) {
|
|
114
|
-
return { success: false, error: 'path 是必填参数' }
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// 路径安全验证
|
|
118
|
-
const pathCheck = validatePath(filePath)
|
|
119
|
-
if (!pathCheck.valid) {
|
|
120
|
-
return { success: false, error: pathCheck.error }
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
try {
|
|
124
|
-
if (!fs.existsSync(pathCheck.resolved)) {
|
|
125
|
-
return { success: false, error: '文件不存在' }
|
|
126
|
-
}
|
|
127
|
-
const stat = fs.statSync(pathCheck.resolved)
|
|
128
|
-
if (stat.size > 1024 * 1024) {
|
|
129
|
-
return { success: false, error: '文件太大,超过 1MB' }
|
|
130
|
-
}
|
|
131
|
-
let content
|
|
132
|
-
if (lines) {
|
|
133
|
-
const fileContent = fs.readFileSync(pathCheck.resolved, 'utf8')
|
|
134
|
-
const allLines = fileContent.split('\n')
|
|
135
|
-
content = allLines.slice(0, lines).join('\n')
|
|
136
|
-
} else {
|
|
137
|
-
content = fs.readFileSync(pathCheck.resolved, 'utf8')
|
|
138
|
-
}
|
|
139
|
-
return {
|
|
140
|
-
success: true,
|
|
141
|
-
filePath: pathCheck.resolved,
|
|
142
|
-
content,
|
|
143
|
-
size: stat.size,
|
|
144
|
-
lines: lines ? null : content.split('\n').length
|
|
145
|
-
}
|
|
146
|
-
} catch (error) {
|
|
147
|
-
return { success: false, error: error.message }
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
})
|
|
151
|
-
|
|
152
|
-
// 写入文件
|
|
153
|
-
framework.registerTool({
|
|
154
|
-
name: 'write_file',
|
|
155
|
-
description: '创建或写入文件内容。content 是要写入的文本内容。',
|
|
156
|
-
inputSchema: z.object({
|
|
157
|
-
path: z.string().describe('文件路径(必须)'),
|
|
158
|
-
content: z.string().describe('文件内容(必须)')
|
|
159
|
-
}),
|
|
160
|
-
execute: async (args, framework) => {
|
|
161
|
-
const { path: filePath, content } = args
|
|
162
|
-
if (!filePath || !content) {
|
|
163
|
-
return { success: false, error: 'path 和 content 都是必填参数' }
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// 路径安全验证
|
|
167
|
-
const pathCheck = validatePath(filePath)
|
|
168
|
-
if (!pathCheck.valid) {
|
|
169
|
-
return { success: false, error: pathCheck.error }
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
try {
|
|
173
|
-
const dir = path.dirname(pathCheck.resolved)
|
|
174
|
-
if (!fs.existsSync(dir)) {
|
|
175
|
-
fs.mkdirSync(dir, { recursive: true })
|
|
176
|
-
}
|
|
177
|
-
fs.writeFileSync(pathCheck.resolved, content, 'utf8')
|
|
178
|
-
return { success: true, message: `文件已写入: ${pathCheck.resolved}`, filePath: pathCheck.resolved, size: content.length }
|
|
179
|
-
} catch (error) {
|
|
180
|
-
return { success: false, error: error.message }
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
})
|
|
184
|
-
|
|
185
|
-
// 删除文件
|
|
186
|
-
framework.registerTool({
|
|
187
|
-
name: 'delete_file',
|
|
188
|
-
description: '删除文件或目录',
|
|
189
|
-
inputSchema: z.object({
|
|
190
|
-
path: z.string().optional().describe('文件或目录路径'),
|
|
191
|
-
targetPath: z.string().optional().describe('文件或目录路径(同path)'),
|
|
192
|
-
recursive: z.boolean().optional().describe('递归删除目录')
|
|
193
|
-
}),
|
|
194
|
-
execute: async (args, framework) => {
|
|
195
|
-
const targetPath = args.path || args.targetPath
|
|
196
|
-
const recursive = args.recursive || false
|
|
197
|
-
|
|
198
|
-
// 路径安全验证
|
|
199
|
-
const pathCheck = validatePath(targetPath)
|
|
200
|
-
if (!pathCheck.valid) {
|
|
201
|
-
return { success: false, error: pathCheck.error }
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
try {
|
|
205
|
-
if (!fs.existsSync(pathCheck.resolved)) {
|
|
206
|
-
return { success: false, error: '目标不存在' }
|
|
207
|
-
}
|
|
208
|
-
const stat = fs.statSync(pathCheck.resolved)
|
|
209
|
-
if (stat.isDirectory()) {
|
|
210
|
-
if (recursive) {
|
|
211
|
-
fs.rmSync(pathCheck.resolved, { recursive: true, force: true })
|
|
212
|
-
} else {
|
|
213
|
-
fs.rmdirSync(pathCheck.resolved)
|
|
214
|
-
}
|
|
215
|
-
} else {
|
|
216
|
-
fs.unlinkSync(pathCheck.resolved)
|
|
217
|
-
}
|
|
218
|
-
return { success: true, message: `已删除: ${pathCheck.resolved}` }
|
|
219
|
-
} catch (error) {
|
|
220
|
-
return { success: false, error: error.message }
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
})
|
|
224
|
-
|
|
225
|
-
// 修改文件(替换文本)
|
|
226
|
-
framework.registerTool({
|
|
227
|
-
name: 'modify_file',
|
|
228
|
-
description: '修改文件内容(替换文本),支持精确匹配和正则',
|
|
229
|
-
inputSchema: z.object({
|
|
230
|
-
filePath: z.string().describe('文件路径(必须)'),
|
|
231
|
-
find: z.string().min(1).describe('要查找的文本(必填,不能为空)'),
|
|
232
|
-
replace: z.string().describe('替换后的文本'),
|
|
233
|
-
replaceAll: z.boolean().optional().describe('是否替换所有匹配,默认 true'),
|
|
234
|
-
useRegex: z.boolean().optional().describe('是否使用正则表达式匹配,默认 false'),
|
|
235
|
-
backup: z.boolean().default(false).describe('修改前是否创建备份,默认 false')
|
|
236
|
-
}),
|
|
237
|
-
execute: async (args, framework) => {
|
|
238
|
-
const filePath = args.filePath || args.path
|
|
239
|
-
const find = args.find
|
|
240
|
-
const replace = args.replace
|
|
241
|
-
const replaceAll = args.replaceAll !== false // 默认 true
|
|
242
|
-
const useRegex = args.useRegex === true
|
|
243
|
-
const backup = args.backup !== false // 默认 true
|
|
244
|
-
|
|
245
|
-
// 校验 filePath
|
|
246
|
-
if (!filePath || typeof filePath !== 'string' || filePath.trim() === '') {
|
|
247
|
-
return { success: false, error: 'filePath 是必填参数,不能为空' }
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// 校验 find
|
|
251
|
-
if (!find || typeof find !== 'string' || find.trim() === '') {
|
|
252
|
-
return { success: false, error: 'find 是必填参数,不能为空字符串' }
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// 路径安全验证
|
|
256
|
-
const pathCheck = validatePath(filePath)
|
|
257
|
-
if (!pathCheck.valid) {
|
|
258
|
-
return { success: false, error: pathCheck.error }
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
try {
|
|
262
|
-
if (!fs.existsSync(pathCheck.resolved)) {
|
|
263
|
-
return { success: false, error: '文件不存在' }
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// 检查是否是二进制文件
|
|
267
|
-
const stats = fs.statSync(pathCheck.resolved)
|
|
268
|
-
if (stats.size > 10 * 1024 * 1024) {
|
|
269
|
-
return { success: false, error: '文件超过 10MB,不支持修改' }
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
let content = fs.readFileSync(pathCheck.resolved, 'utf8')
|
|
273
|
-
let count = 0
|
|
274
|
-
const matches = []
|
|
275
|
-
|
|
276
|
-
if (useRegex) {
|
|
277
|
-
// 正则表达式模式
|
|
278
|
-
const flags = replaceAll ? 'g' : ''
|
|
279
|
-
const regex = new RegExp(find, flags)
|
|
280
|
-
|
|
281
|
-
let match
|
|
282
|
-
while ((match = regex.exec(content)) !== null) {
|
|
283
|
-
matches.push({
|
|
284
|
-
index: match.index,
|
|
285
|
-
length: match[0].length,
|
|
286
|
-
text: match[0]
|
|
287
|
-
})
|
|
288
|
-
if (!replaceAll) break
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
if (matches.length > 0) {
|
|
292
|
-
count = replaceAll ? matches.length : 1
|
|
293
|
-
content = content.replace(regex, replace)
|
|
294
|
-
}
|
|
295
|
-
} else {
|
|
296
|
-
// 精确字符串匹配
|
|
297
|
-
const escapedFind = find.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
298
|
-
const flags = replaceAll ? 'g' : ''
|
|
299
|
-
const regex = new RegExp(escapedFind, flags)
|
|
300
|
-
|
|
301
|
-
let match
|
|
302
|
-
while ((match = regex.exec(content)) !== null) {
|
|
303
|
-
matches.push({
|
|
304
|
-
index: match.index,
|
|
305
|
-
length: match[0].length,
|
|
306
|
-
text: match[0]
|
|
307
|
-
})
|
|
308
|
-
if (!replaceAll) break
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
if (matches.length > 0) {
|
|
312
|
-
count = replaceAll ? matches.length : 1
|
|
313
|
-
content = content.replace(regex, replace)
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
if (count === 0) {
|
|
318
|
-
return {
|
|
319
|
-
success: false,
|
|
320
|
-
error: `未找到匹配文本: "${find.substring(0, 50)}${find.length > 50 ? '...' : ''}"`,
|
|
321
|
-
filePath
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// 创建备份
|
|
326
|
-
if (backup) {
|
|
327
|
-
const backupPath = pathCheck.resolved + '.bak'
|
|
328
|
-
fs.writeFileSync(backupPath, fs.readFileSync(pathCheck.resolved), 'utf8')
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
// 原子写入:先写临时文件,再 rename
|
|
332
|
-
const tempPath = pathCheck.resolved + '.tmp.' + Date.now()
|
|
333
|
-
fs.writeFileSync(tempPath, content, 'utf8')
|
|
334
|
-
|
|
335
|
-
// 验证写入内容
|
|
336
|
-
const verifyContent = fs.readFileSync(tempPath, 'utf8')
|
|
337
|
-
if (verifyContent !== content) {
|
|
338
|
-
fs.unlinkSync(tempPath)
|
|
339
|
-
return { success: false, error: '写入验证失败,内容不匹配' }
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
fs.renameSync(tempPath, pathCheck.resolved)
|
|
343
|
-
|
|
344
|
-
return {
|
|
345
|
-
success: true,
|
|
346
|
-
message: `文件已修改: ${pathCheck.resolved}`,
|
|
347
|
-
filePath: pathCheck.resolved,
|
|
348
|
-
replacements: count,
|
|
349
|
-
matches: matches.slice(0, 5), // 最多返回5个匹配位置
|
|
350
|
-
backupCreated: backup
|
|
351
|
-
}
|
|
352
|
-
} catch (error) {
|
|
353
|
-
return { success: false, error: error.message }
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
})
|
|
357
|
-
|
|
358
|
-
// 搜索文件
|
|
359
|
-
framework.registerTool({
|
|
360
|
-
name: 'search_file',
|
|
361
|
-
description: '在文件或目录中搜索文本,支持精确匹配和正则表达式',
|
|
362
|
-
inputSchema: z.object({
|
|
363
|
-
pattern: z.string().describe('搜索模式(关键词或正则表达式)'),
|
|
364
|
-
path: z.string().optional().describe('搜索目录路径'),
|
|
365
|
-
file: z.string().optional().describe('搜索指定文件(与 path 二选一)'),
|
|
366
|
-
fileType: z.string().optional().describe('文件类型过滤,如 .js、.py'),
|
|
367
|
-
maxResults: z.number().optional().describe('最大结果数,默认 100'),
|
|
368
|
-
maxResultsPerFile: z.number().optional().describe('每个文件最大结果数,默认 50'),
|
|
369
|
-
contextLines: z.number().optional().describe('匹配行的上下文行数,默认 0'),
|
|
370
|
-
caseSensitive: z.boolean().optional().describe('是否大小写敏感,默认 false'),
|
|
371
|
-
useRegex: z.boolean().optional().describe('是否使用正则表达式,默认 false'),
|
|
372
|
-
excludeDirs: z.array(z.string()).optional().describe('排除的目录名,默认 ["node_modules", ".git", "dist", "build"]')
|
|
373
|
-
}),
|
|
374
|
-
execute: async (args, framework) => {
|
|
375
|
-
const pattern = args.pattern
|
|
376
|
-
const dirPath = args.path || process.cwd()
|
|
377
|
-
const targetFile = args.file
|
|
378
|
-
const fileType = args.fileType
|
|
379
|
-
const maxResults = args.maxResults || 100
|
|
380
|
-
const maxResultsPerFile = args.maxResultsPerFile || 50
|
|
381
|
-
const contextLines = args.contextLines || 0
|
|
382
|
-
const caseSensitive = args.caseSensitive === true
|
|
383
|
-
const useRegex = args.useRegex === true
|
|
384
|
-
const excludeDirs = args.excludeDirs || ['node_modules', '.git', 'dist', 'build', '.claude']
|
|
385
|
-
|
|
386
|
-
if (!pattern || typeof pattern !== 'string' || pattern.trim() === '') {
|
|
387
|
-
return { success: false, error: 'pattern 是必填参数,不能为空' }
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
try {
|
|
391
|
-
const results = []
|
|
392
|
-
let regex
|
|
393
|
-
|
|
394
|
-
if (useRegex) {
|
|
395
|
-
// 直接使用正则表达式
|
|
396
|
-
try {
|
|
397
|
-
regex = new RegExp(pattern, caseSensitive ? 'g' : 'gi')
|
|
398
|
-
} catch (e) {
|
|
399
|
-
return { success: false, error: `无效的正则表达式: ${e.message}` }
|
|
400
|
-
}
|
|
401
|
-
} else {
|
|
402
|
-
// 转义特殊字符作为精确匹配
|
|
403
|
-
const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
404
|
-
regex = new RegExp(escaped, caseSensitive ? 'g' : 'gi')
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
const binaryExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.ico', '.exe', '.dll', '.so', '.zip', '.tar', '.gz', '.rar', '.7z', '.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.mp3', '.mp4', '.avi', '.mkv', '.wav', '.flac']
|
|
408
|
-
|
|
409
|
-
const searchInContent = (content, filePath) => {
|
|
410
|
-
const matches = []
|
|
411
|
-
const lines = content.split('\n')
|
|
412
|
-
let fileResultsCount = 0
|
|
413
|
-
|
|
414
|
-
for (let i = 0; i < lines.length; i++) {
|
|
415
|
-
if (fileResultsCount >= maxResultsPerFile) break
|
|
416
|
-
const line = lines[i]
|
|
417
|
-
regex.lastIndex = 0 // 重置 regex 状态
|
|
418
|
-
|
|
419
|
-
if (regex.test(line)) {
|
|
420
|
-
// 找到所有匹配的位置
|
|
421
|
-
const lineMatches = []
|
|
422
|
-
regex.lastIndex = 0
|
|
423
|
-
let match
|
|
424
|
-
while ((match = regex.exec(line)) !== null) {
|
|
425
|
-
lineMatches.push({
|
|
426
|
-
column: match.index,
|
|
427
|
-
length: match[0].length,
|
|
428
|
-
text: match[0]
|
|
429
|
-
})
|
|
430
|
-
if (!regex.global) break
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
const matchInfo = {
|
|
434
|
-
file: filePath,
|
|
435
|
-
line: i + 1,
|
|
436
|
-
content: line.substring(0, 300),
|
|
437
|
-
matches: lineMatches
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
if (contextLines > 0) {
|
|
441
|
-
const start = Math.max(0, i - contextLines)
|
|
442
|
-
const end = Math.min(lines.length, i + contextLines + 1)
|
|
443
|
-
matchInfo.context = lines.slice(start, end).map((l, idx) => ({
|
|
444
|
-
line: start + idx + 1,
|
|
445
|
-
content: l
|
|
446
|
-
}))
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
matches.push(matchInfo)
|
|
450
|
-
fileResultsCount++
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
return matches
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
// 搜索单个文件
|
|
458
|
-
if (targetFile) {
|
|
459
|
-
const fullPath = path.resolve(targetFile)
|
|
460
|
-
if (!fs.existsSync(fullPath)) {
|
|
461
|
-
return { success: false, error: `文件不存在: ${targetFile}` }
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
const ext = path.extname(fullPath).toLowerCase()
|
|
465
|
-
if (binaryExtensions.includes(ext)) {
|
|
466
|
-
return { success: false, error: `不支持搜索二进制文件: ${ext}` }
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
try {
|
|
470
|
-
const content = fs.readFileSync(fullPath, 'utf8')
|
|
471
|
-
const fileMatches = searchInContent(content, targetFile)
|
|
472
|
-
results.push(...fileMatches)
|
|
473
|
-
} catch (e) {
|
|
474
|
-
return { success: false, error: `读取文件失败: ${e.message}` }
|
|
475
|
-
}
|
|
476
|
-
} else {
|
|
477
|
-
// 搜索目录
|
|
478
|
-
const searchDir = (currentPath, depth = 0) => {
|
|
479
|
-
if (depth > 10 || results.length >= maxResults) return
|
|
480
|
-
|
|
481
|
-
let entries
|
|
482
|
-
try {
|
|
483
|
-
entries = fs.readdirSync(currentPath, { withFileTypes: true })
|
|
484
|
-
} catch (e) {
|
|
485
|
-
return // 跳过无法读取的目录
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
for (const entry of entries) {
|
|
489
|
-
if (results.length >= maxResults) break
|
|
490
|
-
|
|
491
|
-
const fullPath = path.join(currentPath, entry.name)
|
|
492
|
-
const relativePath = path.relative(process.cwd(), fullPath)
|
|
493
|
-
|
|
494
|
-
// 检查是否在排除目录中
|
|
495
|
-
const shouldExclude = excludeDirs.some(exclude =>
|
|
496
|
-
relativePath.includes(exclude)
|
|
497
|
-
)
|
|
498
|
-
if (shouldExclude) continue
|
|
499
|
-
|
|
500
|
-
if (entry.isDirectory()) {
|
|
501
|
-
searchDir(fullPath, depth + 1)
|
|
502
|
-
} else if (entry.isFile()) {
|
|
503
|
-
// 文件类型过滤
|
|
504
|
-
if (fileType && !entry.name.endsWith(fileType)) continue
|
|
505
|
-
|
|
506
|
-
const ext = path.extname(entry.name).toLowerCase()
|
|
507
|
-
if (binaryExtensions.includes(ext)) continue
|
|
508
|
-
|
|
509
|
-
try {
|
|
510
|
-
const content = fs.readFileSync(fullPath, 'utf8')
|
|
511
|
-
const fileMatches = searchInContent(content, relativePath)
|
|
512
|
-
|
|
513
|
-
for (const match of fileMatches) {
|
|
514
|
-
if (results.length >= maxResults) break
|
|
515
|
-
results.push(match)
|
|
516
|
-
}
|
|
517
|
-
} catch (e) {
|
|
518
|
-
// 跳过无法读取的文件
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
searchDir(dirPath)
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
// 计算统计信息
|
|
528
|
-
const filesWithMatches = new Set(results.map(r => r.file)).size
|
|
529
|
-
const totalMatches = results.reduce((sum, r) => sum + (r.matches?.length || 1), 0)
|
|
530
|
-
|
|
531
|
-
return {
|
|
532
|
-
success: true,
|
|
533
|
-
pattern,
|
|
534
|
-
results: results.slice(0, maxResults),
|
|
535
|
-
total: results.length,
|
|
536
|
-
stats: {
|
|
537
|
-
filesWithMatches,
|
|
538
|
-
totalMatches,
|
|
539
|
-
searchPath: targetFile || dirPath
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
} catch (error) {
|
|
543
|
-
return { success: false, error: error.message }
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
})
|
|
547
|
-
|
|
548
|
-
// 执行命令
|
|
549
|
-
framework.registerTool({
|
|
550
|
-
name: 'execute_command',
|
|
551
|
-
description: '执行终端命令',
|
|
552
|
-
inputSchema: z.object({
|
|
553
|
-
cmd: z.string().optional().describe('要执行的命令'),
|
|
554
|
-
command: z.string().optional().describe('要执行的命令(同cmd)'),
|
|
555
|
-
run: z.string().optional().describe('要执行的命令(同cmd)'),
|
|
556
|
-
cwd: z.string().optional().describe('工作目录'),
|
|
557
|
-
timeout: z.number().optional().describe('超时时间(ms)')
|
|
558
|
-
}),
|
|
559
|
-
execute: async (args, framework) => {
|
|
560
|
-
const command = args.cmd || args.command || args.run
|
|
561
|
-
const cwd = args.cwd || process.cwd()
|
|
562
|
-
const timeout = Math.min(args.timeout || 30000, 120000) // 最多 2 分钟
|
|
563
|
-
|
|
564
|
-
// 验证命令:检查危险的 shell 模式
|
|
565
|
-
const dangerousPatterns = [
|
|
566
|
-
/;\s*rm\s+/i, // ; rm -rf
|
|
567
|
-
/\|\s*rm\s+/i, // | rm -rf
|
|
568
|
-
/&&\s*rm\s+/i, // && rm -rf
|
|
569
|
-
/;\s*del\s+/i, // ; del
|
|
570
|
-
/\|\s*del\s+/i, // | del
|
|
571
|
-
/&\s*rm\s+/i, // & rm -rf
|
|
572
|
-
/;\s*format\s+/i, // ; format
|
|
573
|
-
/&\s*format\s+/i, // & format
|
|
574
|
-
/\$\(/i, // $(command substitution)
|
|
575
|
-
/`[^`]+`/i, // `command substitution`
|
|
576
|
-
/>\s*\/dev\//i, // redirect to /dev/
|
|
577
|
-
/<\s*\/dev\//i, // read from /dev/
|
|
578
|
-
]
|
|
579
|
-
|
|
580
|
-
for (const pattern of dangerousPatterns) {
|
|
581
|
-
if (pattern.test(command)) {
|
|
582
|
-
return {
|
|
583
|
-
success: false,
|
|
584
|
-
error: `命令包含可疑模式,已拒绝执行: ${pattern.toString()}`
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
// 验证 cwd 路径
|
|
590
|
-
const resolvedCwd = path.resolve(cwd)
|
|
591
|
-
if (!fs.existsSync(resolvedCwd)) {
|
|
592
|
-
return { success: false, error: `工作目录不存在: ${resolvedCwd}` }
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
return new Promise((resolve) => {
|
|
596
|
-
const startTime = Date.now()
|
|
597
|
-
exec(command, { cwd: resolvedCwd, timeout, shell: true }, (error, stdout, stderr) => {
|
|
598
|
-
const duration = Date.now() - startTime
|
|
599
|
-
if (error) {
|
|
600
|
-
resolve({ success: false, command, error: error.message, stderr: stderr.substring(0, 1000), duration })
|
|
601
|
-
} else {
|
|
602
|
-
resolve({
|
|
603
|
-
success: true,
|
|
604
|
-
command,
|
|
605
|
-
stdout: stdout.substring(0, 10000),
|
|
606
|
-
stderr: stderr.substring(0, 1000),
|
|
607
|
-
duration
|
|
608
|
-
})
|
|
609
|
-
}
|
|
610
|
-
})
|
|
611
|
-
})
|
|
612
|
-
}
|
|
613
|
-
})
|
|
614
|
-
|
|
615
|
-
// 执行 Bash 命令
|
|
616
|
-
framework.registerTool({
|
|
617
|
-
name: 'bash',
|
|
618
|
-
description: '执行 Bash 命令(支持 Linux/macOS/Windows Git Bash)',
|
|
619
|
-
inputSchema: z.object({
|
|
620
|
-
cmd: z.string().optional().describe('要执行的 bash 命令'),
|
|
621
|
-
command: z.string().optional().describe('要执行的 bash 命令(同cmd)'),
|
|
622
|
-
cwd: z.string().optional().describe('工作目录'),
|
|
623
|
-
timeout: z.number().optional().describe('超时时间(ms)')
|
|
624
|
-
}),
|
|
625
|
-
execute: async (args, framework) => {
|
|
626
|
-
const command = args.cmd || args.command
|
|
627
|
-
const cwd = args.cwd || process.cwd()
|
|
628
|
-
const timeout = Math.min(args.timeout || 30000, 120000)
|
|
629
|
-
|
|
630
|
-
if (!command) {
|
|
631
|
-
return { success: false, error: 'cmd/command 是必填参数' }
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
// 验证命令:检查危险的 shell 模式
|
|
635
|
-
const dangerousPatterns = [
|
|
636
|
-
/;\s*rm\s+-rf/i,
|
|
637
|
-
/\|\s*rm\s+/i,
|
|
638
|
-
/&&\s*rm\s+/i,
|
|
639
|
-
/;\s*del\s+/i,
|
|
640
|
-
/\$\(/i,
|
|
641
|
-
/`[^`]+`/i,
|
|
642
|
-
/>\s*\/dev\//i,
|
|
643
|
-
/<\s*\/dev\//i,
|
|
644
|
-
]
|
|
645
|
-
|
|
646
|
-
for (const pattern of dangerousPatterns) {
|
|
647
|
-
if (pattern.test(command)) {
|
|
648
|
-
return {
|
|
649
|
-
success: false,
|
|
650
|
-
error: `命令包含可疑模式,已拒绝执行: ${pattern.toString()}`
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
// 验证 cwd 路径
|
|
656
|
-
const resolvedCwd = path.resolve(cwd)
|
|
657
|
-
if (!fs.existsSync(resolvedCwd)) {
|
|
658
|
-
return { success: false, error: `工作目录不存在: ${resolvedCwd}` }
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
// 确定 bash 路径
|
|
662
|
-
let bashPath = 'bash'
|
|
663
|
-
const isWindows = process.platform === 'win32'
|
|
664
|
-
|
|
665
|
-
if (isWindows) {
|
|
666
|
-
// Windows 下尝试查找 Git Bash
|
|
667
|
-
const possiblePaths = [
|
|
668
|
-
'C:\\Program Files\\Git\\bin\\bash.exe',
|
|
669
|
-
'C:\\Program Files (x86)\\Git\\bin\\bash.exe',
|
|
670
|
-
process.env.GIT_BASH_PATH || 'bash'
|
|
671
|
-
]
|
|
672
|
-
for (const p of possiblePaths) {
|
|
673
|
-
if (fs.existsSync(p)) {
|
|
674
|
-
bashPath = p
|
|
675
|
-
break
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
return new Promise((resolve) => {
|
|
681
|
-
const startTime = Date.now()
|
|
682
|
-
exec(
|
|
683
|
-
`"${bashPath}" -c ${JSON.stringify(command)}`,
|
|
684
|
-
{ cwd: resolvedCwd, timeout, shell: false },
|
|
685
|
-
(error, stdout, stderr) => {
|
|
686
|
-
const duration = Date.now() - startTime
|
|
687
|
-
if (error) {
|
|
688
|
-
resolve({
|
|
689
|
-
success: false,
|
|
690
|
-
command,
|
|
691
|
-
error: error.message,
|
|
692
|
-
stderr: stderr.substring(0, 2000),
|
|
693
|
-
stdout: stdout.substring(0, 2000),
|
|
694
|
-
duration
|
|
695
|
-
})
|
|
696
|
-
} else {
|
|
697
|
-
resolve({
|
|
698
|
-
success: true,
|
|
699
|
-
command,
|
|
700
|
-
stdout: stdout.substring(0, 10000),
|
|
701
|
-
stderr: stderr.substring(0, 1000),
|
|
702
|
-
duration
|
|
703
|
-
})
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
)
|
|
707
|
-
})
|
|
708
|
-
}
|
|
709
|
-
})
|
|
710
|
-
|
|
711
|
-
// 获取北京时间
|
|
712
|
-
framework.registerTool({
|
|
713
|
-
name: 'get_time',
|
|
714
|
-
description: '获取当前的时间,需要获取日期和时间的时候需要调用该接口',
|
|
715
|
-
inputSchema: z.object({}),
|
|
716
|
-
execute: async (args, framework) => {
|
|
717
|
-
const now = new Date()
|
|
718
|
-
const beijingTime = new Date(now.getTime() + (8 * 60 * 60 * 1000))
|
|
719
|
-
return {
|
|
720
|
-
success: true,
|
|
721
|
-
beijingTime: beijingTime.toISOString().replace('T', ' ').substring(0, 19),
|
|
722
|
-
timestamp: now.getTime(),
|
|
723
|
-
timezone: 'Asia/Shanghai (UTC+8)',
|
|
724
|
-
formatted: {
|
|
725
|
-
year: beijingTime.getUTCFullYear(),
|
|
726
|
-
month: String(beijingTime.getUTCMonth() + 1).padStart(2, '0'),
|
|
727
|
-
day: String(beijingTime.getUTCDate()).padStart(2, '0'),
|
|
728
|
-
hour: String(beijingTime.getUTCHours()).padStart(2, '0'),
|
|
729
|
-
minute: String(beijingTime.getUTCMinutes()).padStart(2, '0'),
|
|
730
|
-
second: String(beijingTime.getUTCMinutes()).padStart(2, '0')
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
})
|
|
735
|
-
|
|
736
|
-
// HTTP 请求
|
|
737
|
-
framework.registerTool({
|
|
738
|
-
name: 'fetch',
|
|
739
|
-
description: '发送 HTTP 请求获取远程数据。如果访问失败或超时,LLM 应自动使用 proxy: true 重试(代理会绕过网络限制)。',
|
|
740
|
-
inputSchema: z.object({
|
|
741
|
-
url: z.string().describe('请求 URL'),
|
|
742
|
-
method: z.enum(['GET', 'POST', 'PUT', 'DELETE', 'PATCH']).optional().describe('请求方法,默认 GET'),
|
|
743
|
-
headers: z.record(z.string()).optional().describe('请求头'),
|
|
744
|
-
body: z.string().optional().describe('请求体(POST/PUT/PATCH 时使用)'),
|
|
745
|
-
timeout: z.number().optional().describe('超时时间(ms),默认 30000'),
|
|
746
|
-
proxy: z.boolean().optional().describe('是否使用代理,默认 false。访问失败时可自动设为 true 重试')
|
|
747
|
-
}),
|
|
748
|
-
execute: async (args, framework) => {
|
|
749
|
-
const { url, method = 'GET', headers = {}, body, timeout = 30000, proxy = false } = args
|
|
750
|
-
try {
|
|
751
|
-
const controller = new AbortController()
|
|
752
|
-
const timeoutId = setTimeout(() => controller.abort(), timeout)
|
|
753
|
-
const fetchUrl = proxy ? `https://fcdn.foliko.com?url=${encodeURIComponent(url)}` : url
|
|
754
|
-
|
|
755
|
-
const response = await fetch(fetchUrl, {
|
|
756
|
-
method,
|
|
757
|
-
headers,
|
|
758
|
-
body: body || undefined,
|
|
759
|
-
signal: controller.signal
|
|
760
|
-
})
|
|
761
|
-
|
|
762
|
-
clearTimeout(timeoutId)
|
|
763
|
-
|
|
764
|
-
const text = await response.text()
|
|
765
|
-
let data
|
|
766
|
-
try {
|
|
767
|
-
data = JSON.parse(text)
|
|
768
|
-
} catch {
|
|
769
|
-
data = text
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
return {
|
|
773
|
-
success: true,
|
|
774
|
-
status: response.status,
|
|
775
|
-
statusText: response.statusText,
|
|
776
|
-
headers: Object.fromEntries(response.headers.entries()),
|
|
777
|
-
body: data,
|
|
778
|
-
usedProxy: proxy
|
|
779
|
-
}
|
|
780
|
-
} catch (error) {
|
|
781
|
-
return {
|
|
782
|
-
success: false,
|
|
783
|
-
error: error.message,
|
|
784
|
-
url,
|
|
785
|
-
method,
|
|
786
|
-
hint: '如果访问失败,可尝试设置 proxy: true'
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
})
|
|
791
|
-
|
|
792
|
-
// 发送通知
|
|
793
|
-
framework.registerTool({
|
|
794
|
-
name: 'notification_send',
|
|
795
|
-
description: '发送系统通知,仅发送给当前聊天会话,通知会显示给用户或在下次对话时呈现',
|
|
796
|
-
inputSchema: z.object({
|
|
797
|
-
title: z.string().describe('通知标题'),
|
|
798
|
-
message: z.string().describe('通知内容'),
|
|
799
|
-
source: z.string().optional().describe('通知来源标识,默认 系统消息')
|
|
800
|
-
}),
|
|
801
|
-
execute: async (args, framework) => {
|
|
802
|
-
const { title, message, source = '系统消息' } = args
|
|
803
|
-
try {
|
|
804
|
-
// 获取当前执行上下文中的 sessionId,只发送到当前会话
|
|
805
|
-
const ctx = framework.getExecutionContext()
|
|
806
|
-
const sessionId = ctx?.sessionId || null
|
|
807
|
-
|
|
808
|
-
framework.emit('notification', {
|
|
809
|
-
title,
|
|
810
|
-
message,
|
|
811
|
-
source,
|
|
812
|
-
sessionId,
|
|
813
|
-
timestamp: new Date().toISOString()
|
|
814
|
-
})
|
|
815
|
-
return { success: true, message: '通知已发送' }
|
|
816
|
-
} catch (error) {
|
|
817
|
-
return { success: false, error: error.message }
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
})
|
|
821
|
-
|
|
822
|
-
return this
|
|
823
|
-
}
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
module.exports = { FileSystemPlugin }
|
|
1
|
+
/**
|
|
2
|
+
* FileSystem 插件
|
|
3
|
+
* 提供文件系统操作的常用工具
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { Plugin } = require('../src/core/plugin-base')
|
|
7
|
+
|
|
8
|
+
class FileSystemPlugin extends Plugin {
|
|
9
|
+
constructor(config = {}) {
|
|
10
|
+
super()
|
|
11
|
+
this.name = 'file-system'
|
|
12
|
+
this.version = '1.0.0'
|
|
13
|
+
this.description = '文件系统工具插件'
|
|
14
|
+
this.priority = 5
|
|
15
|
+
this.system = true
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
install(framework) {
|
|
19
|
+
const { z } = require('zod')
|
|
20
|
+
const fs = require('fs')
|
|
21
|
+
const path = require('path')
|
|
22
|
+
const { exec } = require('child_process')
|
|
23
|
+
|
|
24
|
+
// 路径安全验证:防止路径穿越攻击
|
|
25
|
+
const validatePath = (filePath, allowOutsideCwd = false) => {
|
|
26
|
+
const resolved = path.resolve(filePath)
|
|
27
|
+
const cwd = process.cwd()
|
|
28
|
+
|
|
29
|
+
// 允许绝对路径且在允许列表中的路径(如果有的话)
|
|
30
|
+
if (allowOutsideCwd) {
|
|
31
|
+
return { valid: true, resolved }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 检查是否在 cwd 目录下
|
|
35
|
+
if (!resolved.startsWith(cwd)) {
|
|
36
|
+
return { valid: false, error: `路径不允许访问: ${filePath}` }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return { valid: true, resolved }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 读取目录
|
|
43
|
+
framework.registerTool({
|
|
44
|
+
name: 'read_directory',
|
|
45
|
+
description: '读取目录内容,列出目录中的文件和子目录',
|
|
46
|
+
inputSchema: z.object({
|
|
47
|
+
path: z.string().optional().describe('目录路径'),
|
|
48
|
+
dirPath: z.string().optional().describe('目录路径(同path)'),
|
|
49
|
+
recursive: z.boolean().optional().describe('是否递归')
|
|
50
|
+
}),
|
|
51
|
+
execute: async (args, framework) => {
|
|
52
|
+
const dirPath = args.path || args.dirPath || '.'
|
|
53
|
+
const recursive = args.recursive || false
|
|
54
|
+
try {
|
|
55
|
+
const items = []
|
|
56
|
+
const readDir = (currentPath, depth = 0) => {
|
|
57
|
+
if (depth > 3 && recursive) return
|
|
58
|
+
const entries = fs.readdirSync(currentPath, { withFileTypes: true })
|
|
59
|
+
for (const entry of entries) {
|
|
60
|
+
const fullPath = path.join(currentPath, entry.name)
|
|
61
|
+
const relativePath = path.relative(process.cwd(), fullPath)
|
|
62
|
+
if (relativePath.includes('node_modules') || relativePath.includes('.git')) {
|
|
63
|
+
continue
|
|
64
|
+
}
|
|
65
|
+
items.push({
|
|
66
|
+
name: entry.name,
|
|
67
|
+
path: relativePath,
|
|
68
|
+
type: entry.isDirectory() ? 'directory' : 'file',
|
|
69
|
+
size: entry.isFile() ? fs.statSync(fullPath).size : null
|
|
70
|
+
})
|
|
71
|
+
if (entry.isDirectory() && recursive && depth < 3) {
|
|
72
|
+
readDir(fullPath, depth + 1)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
readDir(dirPath)
|
|
77
|
+
return { success: true, dirPath, items: items.slice(0, 100), total: items.length }
|
|
78
|
+
} catch (error) {
|
|
79
|
+
return { success: false, error: error.message }
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
// 创建目录
|
|
85
|
+
framework.registerTool({
|
|
86
|
+
name: 'create_directory',
|
|
87
|
+
description: '创建新目录',
|
|
88
|
+
inputSchema: z.object({
|
|
89
|
+
path: z.string().optional().describe('目录路径'),
|
|
90
|
+
dirPath: z.string().optional().describe('目录路径(同path)')
|
|
91
|
+
}),
|
|
92
|
+
execute: async (args, framework) => {
|
|
93
|
+
const dirPath = args.path || args.dirPath
|
|
94
|
+
try {
|
|
95
|
+
fs.mkdirSync(dirPath, { recursive: true })
|
|
96
|
+
return { success: true, message: `目录已创建: ${dirPath}` }
|
|
97
|
+
} catch (error) {
|
|
98
|
+
return { success: false, error: error.message }
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
// 读取文件
|
|
104
|
+
framework.registerTool({
|
|
105
|
+
name: 'read_file',
|
|
106
|
+
description: '读取文件内容。path 是必填参数。',
|
|
107
|
+
inputSchema: z.object({
|
|
108
|
+
path: z.string().describe('文件路径(必须)'),
|
|
109
|
+
lines: z.number().optional().describe('只读取前 N 行')
|
|
110
|
+
}),
|
|
111
|
+
execute: async (args, framework) => {
|
|
112
|
+
const { path: filePath, lines } = args
|
|
113
|
+
if (!filePath) {
|
|
114
|
+
return { success: false, error: 'path 是必填参数' }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 路径安全验证
|
|
118
|
+
const pathCheck = validatePath(filePath)
|
|
119
|
+
if (!pathCheck.valid) {
|
|
120
|
+
return { success: false, error: pathCheck.error }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
if (!fs.existsSync(pathCheck.resolved)) {
|
|
125
|
+
return { success: false, error: '文件不存在' }
|
|
126
|
+
}
|
|
127
|
+
const stat = fs.statSync(pathCheck.resolved)
|
|
128
|
+
if (stat.size > 1024 * 1024) {
|
|
129
|
+
return { success: false, error: '文件太大,超过 1MB' }
|
|
130
|
+
}
|
|
131
|
+
let content
|
|
132
|
+
if (lines) {
|
|
133
|
+
const fileContent = fs.readFileSync(pathCheck.resolved, 'utf8')
|
|
134
|
+
const allLines = fileContent.split('\n')
|
|
135
|
+
content = allLines.slice(0, lines).join('\n')
|
|
136
|
+
} else {
|
|
137
|
+
content = fs.readFileSync(pathCheck.resolved, 'utf8')
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
success: true,
|
|
141
|
+
filePath: pathCheck.resolved,
|
|
142
|
+
content,
|
|
143
|
+
size: stat.size,
|
|
144
|
+
lines: lines ? null : content.split('\n').length
|
|
145
|
+
}
|
|
146
|
+
} catch (error) {
|
|
147
|
+
return { success: false, error: error.message }
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
// 写入文件
|
|
153
|
+
framework.registerTool({
|
|
154
|
+
name: 'write_file',
|
|
155
|
+
description: '创建或写入文件内容。content 是要写入的文本内容。',
|
|
156
|
+
inputSchema: z.object({
|
|
157
|
+
path: z.string().describe('文件路径(必须)'),
|
|
158
|
+
content: z.string().describe('文件内容(必须)')
|
|
159
|
+
}),
|
|
160
|
+
execute: async (args, framework) => {
|
|
161
|
+
const { path: filePath, content } = args
|
|
162
|
+
if (!filePath || !content) {
|
|
163
|
+
return { success: false, error: 'path 和 content 都是必填参数' }
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 路径安全验证
|
|
167
|
+
const pathCheck = validatePath(filePath)
|
|
168
|
+
if (!pathCheck.valid) {
|
|
169
|
+
return { success: false, error: pathCheck.error }
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
const dir = path.dirname(pathCheck.resolved)
|
|
174
|
+
if (!fs.existsSync(dir)) {
|
|
175
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
176
|
+
}
|
|
177
|
+
fs.writeFileSync(pathCheck.resolved, content, 'utf8')
|
|
178
|
+
return { success: true, message: `文件已写入: ${pathCheck.resolved}`, filePath: pathCheck.resolved, size: content.length }
|
|
179
|
+
} catch (error) {
|
|
180
|
+
return { success: false, error: error.message }
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
// 删除文件
|
|
186
|
+
framework.registerTool({
|
|
187
|
+
name: 'delete_file',
|
|
188
|
+
description: '删除文件或目录',
|
|
189
|
+
inputSchema: z.object({
|
|
190
|
+
path: z.string().optional().describe('文件或目录路径'),
|
|
191
|
+
targetPath: z.string().optional().describe('文件或目录路径(同path)'),
|
|
192
|
+
recursive: z.boolean().optional().describe('递归删除目录')
|
|
193
|
+
}),
|
|
194
|
+
execute: async (args, framework) => {
|
|
195
|
+
const targetPath = args.path || args.targetPath
|
|
196
|
+
const recursive = args.recursive || false
|
|
197
|
+
|
|
198
|
+
// 路径安全验证
|
|
199
|
+
const pathCheck = validatePath(targetPath)
|
|
200
|
+
if (!pathCheck.valid) {
|
|
201
|
+
return { success: false, error: pathCheck.error }
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
if (!fs.existsSync(pathCheck.resolved)) {
|
|
206
|
+
return { success: false, error: '目标不存在' }
|
|
207
|
+
}
|
|
208
|
+
const stat = fs.statSync(pathCheck.resolved)
|
|
209
|
+
if (stat.isDirectory()) {
|
|
210
|
+
if (recursive) {
|
|
211
|
+
fs.rmSync(pathCheck.resolved, { recursive: true, force: true })
|
|
212
|
+
} else {
|
|
213
|
+
fs.rmdirSync(pathCheck.resolved)
|
|
214
|
+
}
|
|
215
|
+
} else {
|
|
216
|
+
fs.unlinkSync(pathCheck.resolved)
|
|
217
|
+
}
|
|
218
|
+
return { success: true, message: `已删除: ${pathCheck.resolved}` }
|
|
219
|
+
} catch (error) {
|
|
220
|
+
return { success: false, error: error.message }
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
// 修改文件(替换文本)
|
|
226
|
+
framework.registerTool({
|
|
227
|
+
name: 'modify_file',
|
|
228
|
+
description: '修改文件内容(替换文本),支持精确匹配和正则',
|
|
229
|
+
inputSchema: z.object({
|
|
230
|
+
filePath: z.string().describe('文件路径(必须)'),
|
|
231
|
+
find: z.string().min(1).describe('要查找的文本(必填,不能为空)'),
|
|
232
|
+
replace: z.string().describe('替换后的文本'),
|
|
233
|
+
replaceAll: z.boolean().optional().describe('是否替换所有匹配,默认 true'),
|
|
234
|
+
useRegex: z.boolean().optional().describe('是否使用正则表达式匹配,默认 false'),
|
|
235
|
+
backup: z.boolean().default(false).describe('修改前是否创建备份,默认 false')
|
|
236
|
+
}),
|
|
237
|
+
execute: async (args, framework) => {
|
|
238
|
+
const filePath = args.filePath || args.path
|
|
239
|
+
const find = args.find
|
|
240
|
+
const replace = args.replace
|
|
241
|
+
const replaceAll = args.replaceAll !== false // 默认 true
|
|
242
|
+
const useRegex = args.useRegex === true
|
|
243
|
+
const backup = args.backup !== false // 默认 true
|
|
244
|
+
|
|
245
|
+
// 校验 filePath
|
|
246
|
+
if (!filePath || typeof filePath !== 'string' || filePath.trim() === '') {
|
|
247
|
+
return { success: false, error: 'filePath 是必填参数,不能为空' }
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// 校验 find
|
|
251
|
+
if (!find || typeof find !== 'string' || find.trim() === '') {
|
|
252
|
+
return { success: false, error: 'find 是必填参数,不能为空字符串' }
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// 路径安全验证
|
|
256
|
+
const pathCheck = validatePath(filePath)
|
|
257
|
+
if (!pathCheck.valid) {
|
|
258
|
+
return { success: false, error: pathCheck.error }
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
try {
|
|
262
|
+
if (!fs.existsSync(pathCheck.resolved)) {
|
|
263
|
+
return { success: false, error: '文件不存在' }
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// 检查是否是二进制文件
|
|
267
|
+
const stats = fs.statSync(pathCheck.resolved)
|
|
268
|
+
if (stats.size > 10 * 1024 * 1024) {
|
|
269
|
+
return { success: false, error: '文件超过 10MB,不支持修改' }
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
let content = fs.readFileSync(pathCheck.resolved, 'utf8')
|
|
273
|
+
let count = 0
|
|
274
|
+
const matches = []
|
|
275
|
+
|
|
276
|
+
if (useRegex) {
|
|
277
|
+
// 正则表达式模式
|
|
278
|
+
const flags = replaceAll ? 'g' : ''
|
|
279
|
+
const regex = new RegExp(find, flags)
|
|
280
|
+
|
|
281
|
+
let match
|
|
282
|
+
while ((match = regex.exec(content)) !== null) {
|
|
283
|
+
matches.push({
|
|
284
|
+
index: match.index,
|
|
285
|
+
length: match[0].length,
|
|
286
|
+
text: match[0]
|
|
287
|
+
})
|
|
288
|
+
if (!replaceAll) break
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (matches.length > 0) {
|
|
292
|
+
count = replaceAll ? matches.length : 1
|
|
293
|
+
content = content.replace(regex, replace)
|
|
294
|
+
}
|
|
295
|
+
} else {
|
|
296
|
+
// 精确字符串匹配
|
|
297
|
+
const escapedFind = find.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
298
|
+
const flags = replaceAll ? 'g' : ''
|
|
299
|
+
const regex = new RegExp(escapedFind, flags)
|
|
300
|
+
|
|
301
|
+
let match
|
|
302
|
+
while ((match = regex.exec(content)) !== null) {
|
|
303
|
+
matches.push({
|
|
304
|
+
index: match.index,
|
|
305
|
+
length: match[0].length,
|
|
306
|
+
text: match[0]
|
|
307
|
+
})
|
|
308
|
+
if (!replaceAll) break
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (matches.length > 0) {
|
|
312
|
+
count = replaceAll ? matches.length : 1
|
|
313
|
+
content = content.replace(regex, replace)
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (count === 0) {
|
|
318
|
+
return {
|
|
319
|
+
success: false,
|
|
320
|
+
error: `未找到匹配文本: "${find.substring(0, 50)}${find.length > 50 ? '...' : ''}"`,
|
|
321
|
+
filePath
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// 创建备份
|
|
326
|
+
if (backup) {
|
|
327
|
+
const backupPath = pathCheck.resolved + '.bak'
|
|
328
|
+
fs.writeFileSync(backupPath, fs.readFileSync(pathCheck.resolved), 'utf8')
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// 原子写入:先写临时文件,再 rename
|
|
332
|
+
const tempPath = pathCheck.resolved + '.tmp.' + Date.now()
|
|
333
|
+
fs.writeFileSync(tempPath, content, 'utf8')
|
|
334
|
+
|
|
335
|
+
// 验证写入内容
|
|
336
|
+
const verifyContent = fs.readFileSync(tempPath, 'utf8')
|
|
337
|
+
if (verifyContent !== content) {
|
|
338
|
+
fs.unlinkSync(tempPath)
|
|
339
|
+
return { success: false, error: '写入验证失败,内容不匹配' }
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
fs.renameSync(tempPath, pathCheck.resolved)
|
|
343
|
+
|
|
344
|
+
return {
|
|
345
|
+
success: true,
|
|
346
|
+
message: `文件已修改: ${pathCheck.resolved}`,
|
|
347
|
+
filePath: pathCheck.resolved,
|
|
348
|
+
replacements: count,
|
|
349
|
+
matches: matches.slice(0, 5), // 最多返回5个匹配位置
|
|
350
|
+
backupCreated: backup
|
|
351
|
+
}
|
|
352
|
+
} catch (error) {
|
|
353
|
+
return { success: false, error: error.message }
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
// 搜索文件
|
|
359
|
+
framework.registerTool({
|
|
360
|
+
name: 'search_file',
|
|
361
|
+
description: '在文件或目录中搜索文本,支持精确匹配和正则表达式',
|
|
362
|
+
inputSchema: z.object({
|
|
363
|
+
pattern: z.string().describe('搜索模式(关键词或正则表达式)'),
|
|
364
|
+
path: z.string().optional().describe('搜索目录路径'),
|
|
365
|
+
file: z.string().optional().describe('搜索指定文件(与 path 二选一)'),
|
|
366
|
+
fileType: z.string().optional().describe('文件类型过滤,如 .js、.py'),
|
|
367
|
+
maxResults: z.number().optional().describe('最大结果数,默认 100'),
|
|
368
|
+
maxResultsPerFile: z.number().optional().describe('每个文件最大结果数,默认 50'),
|
|
369
|
+
contextLines: z.number().optional().describe('匹配行的上下文行数,默认 0'),
|
|
370
|
+
caseSensitive: z.boolean().optional().describe('是否大小写敏感,默认 false'),
|
|
371
|
+
useRegex: z.boolean().optional().describe('是否使用正则表达式,默认 false'),
|
|
372
|
+
excludeDirs: z.array(z.string()).optional().describe('排除的目录名,默认 ["node_modules", ".git", "dist", "build"]')
|
|
373
|
+
}),
|
|
374
|
+
execute: async (args, framework) => {
|
|
375
|
+
const pattern = args.pattern
|
|
376
|
+
const dirPath = args.path || process.cwd()
|
|
377
|
+
const targetFile = args.file
|
|
378
|
+
const fileType = args.fileType
|
|
379
|
+
const maxResults = args.maxResults || 100
|
|
380
|
+
const maxResultsPerFile = args.maxResultsPerFile || 50
|
|
381
|
+
const contextLines = args.contextLines || 0
|
|
382
|
+
const caseSensitive = args.caseSensitive === true
|
|
383
|
+
const useRegex = args.useRegex === true
|
|
384
|
+
const excludeDirs = args.excludeDirs || ['node_modules', '.git', 'dist', 'build', '.claude']
|
|
385
|
+
|
|
386
|
+
if (!pattern || typeof pattern !== 'string' || pattern.trim() === '') {
|
|
387
|
+
return { success: false, error: 'pattern 是必填参数,不能为空' }
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
try {
|
|
391
|
+
const results = []
|
|
392
|
+
let regex
|
|
393
|
+
|
|
394
|
+
if (useRegex) {
|
|
395
|
+
// 直接使用正则表达式
|
|
396
|
+
try {
|
|
397
|
+
regex = new RegExp(pattern, caseSensitive ? 'g' : 'gi')
|
|
398
|
+
} catch (e) {
|
|
399
|
+
return { success: false, error: `无效的正则表达式: ${e.message}` }
|
|
400
|
+
}
|
|
401
|
+
} else {
|
|
402
|
+
// 转义特殊字符作为精确匹配
|
|
403
|
+
const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
404
|
+
regex = new RegExp(escaped, caseSensitive ? 'g' : 'gi')
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const binaryExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.ico', '.exe', '.dll', '.so', '.zip', '.tar', '.gz', '.rar', '.7z', '.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.mp3', '.mp4', '.avi', '.mkv', '.wav', '.flac']
|
|
408
|
+
|
|
409
|
+
const searchInContent = (content, filePath) => {
|
|
410
|
+
const matches = []
|
|
411
|
+
const lines = content.split('\n')
|
|
412
|
+
let fileResultsCount = 0
|
|
413
|
+
|
|
414
|
+
for (let i = 0; i < lines.length; i++) {
|
|
415
|
+
if (fileResultsCount >= maxResultsPerFile) break
|
|
416
|
+
const line = lines[i]
|
|
417
|
+
regex.lastIndex = 0 // 重置 regex 状态
|
|
418
|
+
|
|
419
|
+
if (regex.test(line)) {
|
|
420
|
+
// 找到所有匹配的位置
|
|
421
|
+
const lineMatches = []
|
|
422
|
+
regex.lastIndex = 0
|
|
423
|
+
let match
|
|
424
|
+
while ((match = regex.exec(line)) !== null) {
|
|
425
|
+
lineMatches.push({
|
|
426
|
+
column: match.index,
|
|
427
|
+
length: match[0].length,
|
|
428
|
+
text: match[0]
|
|
429
|
+
})
|
|
430
|
+
if (!regex.global) break
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const matchInfo = {
|
|
434
|
+
file: filePath,
|
|
435
|
+
line: i + 1,
|
|
436
|
+
content: line.substring(0, 300),
|
|
437
|
+
matches: lineMatches
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (contextLines > 0) {
|
|
441
|
+
const start = Math.max(0, i - contextLines)
|
|
442
|
+
const end = Math.min(lines.length, i + contextLines + 1)
|
|
443
|
+
matchInfo.context = lines.slice(start, end).map((l, idx) => ({
|
|
444
|
+
line: start + idx + 1,
|
|
445
|
+
content: l
|
|
446
|
+
}))
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
matches.push(matchInfo)
|
|
450
|
+
fileResultsCount++
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return matches
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// 搜索单个文件
|
|
458
|
+
if (targetFile) {
|
|
459
|
+
const fullPath = path.resolve(targetFile)
|
|
460
|
+
if (!fs.existsSync(fullPath)) {
|
|
461
|
+
return { success: false, error: `文件不存在: ${targetFile}` }
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const ext = path.extname(fullPath).toLowerCase()
|
|
465
|
+
if (binaryExtensions.includes(ext)) {
|
|
466
|
+
return { success: false, error: `不支持搜索二进制文件: ${ext}` }
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
try {
|
|
470
|
+
const content = fs.readFileSync(fullPath, 'utf8')
|
|
471
|
+
const fileMatches = searchInContent(content, targetFile)
|
|
472
|
+
results.push(...fileMatches)
|
|
473
|
+
} catch (e) {
|
|
474
|
+
return { success: false, error: `读取文件失败: ${e.message}` }
|
|
475
|
+
}
|
|
476
|
+
} else {
|
|
477
|
+
// 搜索目录
|
|
478
|
+
const searchDir = (currentPath, depth = 0) => {
|
|
479
|
+
if (depth > 10 || results.length >= maxResults) return
|
|
480
|
+
|
|
481
|
+
let entries
|
|
482
|
+
try {
|
|
483
|
+
entries = fs.readdirSync(currentPath, { withFileTypes: true })
|
|
484
|
+
} catch (e) {
|
|
485
|
+
return // 跳过无法读取的目录
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
for (const entry of entries) {
|
|
489
|
+
if (results.length >= maxResults) break
|
|
490
|
+
|
|
491
|
+
const fullPath = path.join(currentPath, entry.name)
|
|
492
|
+
const relativePath = path.relative(process.cwd(), fullPath)
|
|
493
|
+
|
|
494
|
+
// 检查是否在排除目录中
|
|
495
|
+
const shouldExclude = excludeDirs.some(exclude =>
|
|
496
|
+
relativePath.includes(exclude)
|
|
497
|
+
)
|
|
498
|
+
if (shouldExclude) continue
|
|
499
|
+
|
|
500
|
+
if (entry.isDirectory()) {
|
|
501
|
+
searchDir(fullPath, depth + 1)
|
|
502
|
+
} else if (entry.isFile()) {
|
|
503
|
+
// 文件类型过滤
|
|
504
|
+
if (fileType && !entry.name.endsWith(fileType)) continue
|
|
505
|
+
|
|
506
|
+
const ext = path.extname(entry.name).toLowerCase()
|
|
507
|
+
if (binaryExtensions.includes(ext)) continue
|
|
508
|
+
|
|
509
|
+
try {
|
|
510
|
+
const content = fs.readFileSync(fullPath, 'utf8')
|
|
511
|
+
const fileMatches = searchInContent(content, relativePath)
|
|
512
|
+
|
|
513
|
+
for (const match of fileMatches) {
|
|
514
|
+
if (results.length >= maxResults) break
|
|
515
|
+
results.push(match)
|
|
516
|
+
}
|
|
517
|
+
} catch (e) {
|
|
518
|
+
// 跳过无法读取的文件
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
searchDir(dirPath)
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// 计算统计信息
|
|
528
|
+
const filesWithMatches = new Set(results.map(r => r.file)).size
|
|
529
|
+
const totalMatches = results.reduce((sum, r) => sum + (r.matches?.length || 1), 0)
|
|
530
|
+
|
|
531
|
+
return {
|
|
532
|
+
success: true,
|
|
533
|
+
pattern,
|
|
534
|
+
results: results.slice(0, maxResults),
|
|
535
|
+
total: results.length,
|
|
536
|
+
stats: {
|
|
537
|
+
filesWithMatches,
|
|
538
|
+
totalMatches,
|
|
539
|
+
searchPath: targetFile || dirPath
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
} catch (error) {
|
|
543
|
+
return { success: false, error: error.message }
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
})
|
|
547
|
+
|
|
548
|
+
// 执行命令
|
|
549
|
+
framework.registerTool({
|
|
550
|
+
name: 'execute_command',
|
|
551
|
+
description: '执行终端命令',
|
|
552
|
+
inputSchema: z.object({
|
|
553
|
+
cmd: z.string().optional().describe('要执行的命令'),
|
|
554
|
+
command: z.string().optional().describe('要执行的命令(同cmd)'),
|
|
555
|
+
run: z.string().optional().describe('要执行的命令(同cmd)'),
|
|
556
|
+
cwd: z.string().optional().describe('工作目录'),
|
|
557
|
+
timeout: z.number().optional().describe('超时时间(ms)')
|
|
558
|
+
}),
|
|
559
|
+
execute: async (args, framework) => {
|
|
560
|
+
const command = args.cmd || args.command || args.run
|
|
561
|
+
const cwd = args.cwd || process.cwd()
|
|
562
|
+
const timeout = Math.min(args.timeout || 30000, 120000) // 最多 2 分钟
|
|
563
|
+
|
|
564
|
+
// 验证命令:检查危险的 shell 模式
|
|
565
|
+
const dangerousPatterns = [
|
|
566
|
+
/;\s*rm\s+/i, // ; rm -rf
|
|
567
|
+
/\|\s*rm\s+/i, // | rm -rf
|
|
568
|
+
/&&\s*rm\s+/i, // && rm -rf
|
|
569
|
+
/;\s*del\s+/i, // ; del
|
|
570
|
+
/\|\s*del\s+/i, // | del
|
|
571
|
+
/&\s*rm\s+/i, // & rm -rf
|
|
572
|
+
/;\s*format\s+/i, // ; format
|
|
573
|
+
/&\s*format\s+/i, // & format
|
|
574
|
+
/\$\(/i, // $(command substitution)
|
|
575
|
+
/`[^`]+`/i, // `command substitution`
|
|
576
|
+
/>\s*\/dev\//i, // redirect to /dev/
|
|
577
|
+
/<\s*\/dev\//i, // read from /dev/
|
|
578
|
+
]
|
|
579
|
+
|
|
580
|
+
for (const pattern of dangerousPatterns) {
|
|
581
|
+
if (pattern.test(command)) {
|
|
582
|
+
return {
|
|
583
|
+
success: false,
|
|
584
|
+
error: `命令包含可疑模式,已拒绝执行: ${pattern.toString()}`
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// 验证 cwd 路径
|
|
590
|
+
const resolvedCwd = path.resolve(cwd)
|
|
591
|
+
if (!fs.existsSync(resolvedCwd)) {
|
|
592
|
+
return { success: false, error: `工作目录不存在: ${resolvedCwd}` }
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
return new Promise((resolve) => {
|
|
596
|
+
const startTime = Date.now()
|
|
597
|
+
exec(command, { cwd: resolvedCwd, timeout, shell: true }, (error, stdout, stderr) => {
|
|
598
|
+
const duration = Date.now() - startTime
|
|
599
|
+
if (error) {
|
|
600
|
+
resolve({ success: false, command, error: error.message, stderr: stderr.substring(0, 1000), duration })
|
|
601
|
+
} else {
|
|
602
|
+
resolve({
|
|
603
|
+
success: true,
|
|
604
|
+
command,
|
|
605
|
+
stdout: stdout.substring(0, 10000),
|
|
606
|
+
stderr: stderr.substring(0, 1000),
|
|
607
|
+
duration
|
|
608
|
+
})
|
|
609
|
+
}
|
|
610
|
+
})
|
|
611
|
+
})
|
|
612
|
+
}
|
|
613
|
+
})
|
|
614
|
+
|
|
615
|
+
// 执行 Bash 命令
|
|
616
|
+
framework.registerTool({
|
|
617
|
+
name: 'bash',
|
|
618
|
+
description: '执行 Bash 命令(支持 Linux/macOS/Windows Git Bash)',
|
|
619
|
+
inputSchema: z.object({
|
|
620
|
+
cmd: z.string().optional().describe('要执行的 bash 命令'),
|
|
621
|
+
command: z.string().optional().describe('要执行的 bash 命令(同cmd)'),
|
|
622
|
+
cwd: z.string().optional().describe('工作目录'),
|
|
623
|
+
timeout: z.number().optional().describe('超时时间(ms)')
|
|
624
|
+
}),
|
|
625
|
+
execute: async (args, framework) => {
|
|
626
|
+
const command = args.cmd || args.command
|
|
627
|
+
const cwd = args.cwd || process.cwd()
|
|
628
|
+
const timeout = Math.min(args.timeout || 30000, 120000)
|
|
629
|
+
|
|
630
|
+
if (!command) {
|
|
631
|
+
return { success: false, error: 'cmd/command 是必填参数' }
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// 验证命令:检查危险的 shell 模式
|
|
635
|
+
const dangerousPatterns = [
|
|
636
|
+
/;\s*rm\s+-rf/i,
|
|
637
|
+
/\|\s*rm\s+/i,
|
|
638
|
+
/&&\s*rm\s+/i,
|
|
639
|
+
/;\s*del\s+/i,
|
|
640
|
+
/\$\(/i,
|
|
641
|
+
/`[^`]+`/i,
|
|
642
|
+
/>\s*\/dev\//i,
|
|
643
|
+
/<\s*\/dev\//i,
|
|
644
|
+
]
|
|
645
|
+
|
|
646
|
+
for (const pattern of dangerousPatterns) {
|
|
647
|
+
if (pattern.test(command)) {
|
|
648
|
+
return {
|
|
649
|
+
success: false,
|
|
650
|
+
error: `命令包含可疑模式,已拒绝执行: ${pattern.toString()}`
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// 验证 cwd 路径
|
|
656
|
+
const resolvedCwd = path.resolve(cwd)
|
|
657
|
+
if (!fs.existsSync(resolvedCwd)) {
|
|
658
|
+
return { success: false, error: `工作目录不存在: ${resolvedCwd}` }
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// 确定 bash 路径
|
|
662
|
+
let bashPath = 'bash'
|
|
663
|
+
const isWindows = process.platform === 'win32'
|
|
664
|
+
|
|
665
|
+
if (isWindows) {
|
|
666
|
+
// Windows 下尝试查找 Git Bash
|
|
667
|
+
const possiblePaths = [
|
|
668
|
+
'C:\\Program Files\\Git\\bin\\bash.exe',
|
|
669
|
+
'C:\\Program Files (x86)\\Git\\bin\\bash.exe',
|
|
670
|
+
process.env.GIT_BASH_PATH || 'bash'
|
|
671
|
+
]
|
|
672
|
+
for (const p of possiblePaths) {
|
|
673
|
+
if (fs.existsSync(p)) {
|
|
674
|
+
bashPath = p
|
|
675
|
+
break
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
return new Promise((resolve) => {
|
|
681
|
+
const startTime = Date.now()
|
|
682
|
+
exec(
|
|
683
|
+
`"${bashPath}" -c ${JSON.stringify(command)}`,
|
|
684
|
+
{ cwd: resolvedCwd, timeout, shell: false },
|
|
685
|
+
(error, stdout, stderr) => {
|
|
686
|
+
const duration = Date.now() - startTime
|
|
687
|
+
if (error) {
|
|
688
|
+
resolve({
|
|
689
|
+
success: false,
|
|
690
|
+
command,
|
|
691
|
+
error: error.message,
|
|
692
|
+
stderr: stderr.substring(0, 2000),
|
|
693
|
+
stdout: stdout.substring(0, 2000),
|
|
694
|
+
duration
|
|
695
|
+
})
|
|
696
|
+
} else {
|
|
697
|
+
resolve({
|
|
698
|
+
success: true,
|
|
699
|
+
command,
|
|
700
|
+
stdout: stdout.substring(0, 10000),
|
|
701
|
+
stderr: stderr.substring(0, 1000),
|
|
702
|
+
duration
|
|
703
|
+
})
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
)
|
|
707
|
+
})
|
|
708
|
+
}
|
|
709
|
+
})
|
|
710
|
+
|
|
711
|
+
// 获取北京时间
|
|
712
|
+
framework.registerTool({
|
|
713
|
+
name: 'get_time',
|
|
714
|
+
description: '获取当前的时间,需要获取日期和时间的时候需要调用该接口',
|
|
715
|
+
inputSchema: z.object({}),
|
|
716
|
+
execute: async (args, framework) => {
|
|
717
|
+
const now = new Date()
|
|
718
|
+
const beijingTime = new Date(now.getTime() + (8 * 60 * 60 * 1000))
|
|
719
|
+
return {
|
|
720
|
+
success: true,
|
|
721
|
+
beijingTime: beijingTime.toISOString().replace('T', ' ').substring(0, 19),
|
|
722
|
+
timestamp: now.getTime(),
|
|
723
|
+
timezone: 'Asia/Shanghai (UTC+8)',
|
|
724
|
+
formatted: {
|
|
725
|
+
year: beijingTime.getUTCFullYear(),
|
|
726
|
+
month: String(beijingTime.getUTCMonth() + 1).padStart(2, '0'),
|
|
727
|
+
day: String(beijingTime.getUTCDate()).padStart(2, '0'),
|
|
728
|
+
hour: String(beijingTime.getUTCHours()).padStart(2, '0'),
|
|
729
|
+
minute: String(beijingTime.getUTCMinutes()).padStart(2, '0'),
|
|
730
|
+
second: String(beijingTime.getUTCMinutes()).padStart(2, '0')
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
})
|
|
735
|
+
|
|
736
|
+
// HTTP 请求
|
|
737
|
+
framework.registerTool({
|
|
738
|
+
name: 'fetch',
|
|
739
|
+
description: '发送 HTTP 请求获取远程数据。如果访问失败或超时,LLM 应自动使用 proxy: true 重试(代理会绕过网络限制)。',
|
|
740
|
+
inputSchema: z.object({
|
|
741
|
+
url: z.string().describe('请求 URL'),
|
|
742
|
+
method: z.enum(['GET', 'POST', 'PUT', 'DELETE', 'PATCH']).optional().describe('请求方法,默认 GET'),
|
|
743
|
+
headers: z.record(z.string()).optional().describe('请求头'),
|
|
744
|
+
body: z.string().optional().describe('请求体(POST/PUT/PATCH 时使用)'),
|
|
745
|
+
timeout: z.number().optional().describe('超时时间(ms),默认 30000'),
|
|
746
|
+
proxy: z.boolean().optional().describe('是否使用代理,默认 false。访问失败时可自动设为 true 重试')
|
|
747
|
+
}),
|
|
748
|
+
execute: async (args, framework) => {
|
|
749
|
+
const { url, method = 'GET', headers = {}, body, timeout = 30000, proxy = false } = args
|
|
750
|
+
try {
|
|
751
|
+
const controller = new AbortController()
|
|
752
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout)
|
|
753
|
+
const fetchUrl = proxy ? `https://fcdn.foliko.com?url=${encodeURIComponent(url)}` : url
|
|
754
|
+
|
|
755
|
+
const response = await fetch(fetchUrl, {
|
|
756
|
+
method,
|
|
757
|
+
headers,
|
|
758
|
+
body: body || undefined,
|
|
759
|
+
signal: controller.signal
|
|
760
|
+
})
|
|
761
|
+
|
|
762
|
+
clearTimeout(timeoutId)
|
|
763
|
+
|
|
764
|
+
const text = await response.text()
|
|
765
|
+
let data
|
|
766
|
+
try {
|
|
767
|
+
data = JSON.parse(text)
|
|
768
|
+
} catch {
|
|
769
|
+
data = text
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
return {
|
|
773
|
+
success: true,
|
|
774
|
+
status: response.status,
|
|
775
|
+
statusText: response.statusText,
|
|
776
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
777
|
+
body: data,
|
|
778
|
+
usedProxy: proxy
|
|
779
|
+
}
|
|
780
|
+
} catch (error) {
|
|
781
|
+
return {
|
|
782
|
+
success: false,
|
|
783
|
+
error: error.message,
|
|
784
|
+
url,
|
|
785
|
+
method,
|
|
786
|
+
hint: '如果访问失败,可尝试设置 proxy: true'
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
})
|
|
791
|
+
|
|
792
|
+
// 发送通知
|
|
793
|
+
framework.registerTool({
|
|
794
|
+
name: 'notification_send',
|
|
795
|
+
description: '发送系统通知,仅发送给当前聊天会话,通知会显示给用户或在下次对话时呈现',
|
|
796
|
+
inputSchema: z.object({
|
|
797
|
+
title: z.string().describe('通知标题'),
|
|
798
|
+
message: z.string().describe('通知内容'),
|
|
799
|
+
source: z.string().optional().describe('通知来源标识,默认 系统消息')
|
|
800
|
+
}),
|
|
801
|
+
execute: async (args, framework) => {
|
|
802
|
+
const { title, message, source = '系统消息' } = args
|
|
803
|
+
try {
|
|
804
|
+
// 获取当前执行上下文中的 sessionId,只发送到当前会话
|
|
805
|
+
const ctx = framework.getExecutionContext()
|
|
806
|
+
const sessionId = ctx?.sessionId || null
|
|
807
|
+
|
|
808
|
+
framework.emit('notification', {
|
|
809
|
+
title,
|
|
810
|
+
message,
|
|
811
|
+
source,
|
|
812
|
+
sessionId,
|
|
813
|
+
timestamp: new Date().toISOString()
|
|
814
|
+
})
|
|
815
|
+
return { success: true, message: '通知已发送' }
|
|
816
|
+
} catch (error) {
|
|
817
|
+
return { success: false, error: error.message }
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
})
|
|
821
|
+
|
|
822
|
+
return this
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
module.exports = { FileSystemPlugin }
|