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.
Files changed (88) hide show
  1. package/.claude/settings.local.json +159 -157
  2. package/cli/bin/foliko.js +12 -12
  3. package/cli/src/commands/chat.js +143 -143
  4. package/cli/src/commands/list.js +93 -93
  5. package/cli/src/index.js +75 -75
  6. package/cli/src/ui/chat-ui.js +201 -201
  7. package/cli/src/utils/ansi.js +40 -40
  8. package/cli/src/utils/markdown.js +292 -292
  9. package/examples/ambient-example.js +194 -194
  10. package/examples/basic.js +115 -115
  11. package/examples/bootstrap.js +121 -121
  12. package/examples/mcp-example.js +56 -56
  13. package/examples/skill-example.js +49 -49
  14. package/examples/test-chat.js +137 -137
  15. package/examples/test-mcp.js +85 -85
  16. package/examples/test-reload.js +59 -59
  17. package/examples/test-telegram.js +50 -50
  18. package/examples/test-tg-bot.js +45 -45
  19. package/examples/test-tg-simple.js +47 -47
  20. package/examples/test-tg.js +62 -62
  21. package/examples/test-think.js +43 -43
  22. package/examples/test-web-plugin.js +103 -103
  23. package/examples/test-weixin-feishu.js +103 -103
  24. package/examples/workflow.js +158 -158
  25. package/package.json +1 -1
  26. package/plugins/ai-plugin.js +102 -102
  27. package/plugins/ambient-agent/EventWatcher.js +113 -113
  28. package/plugins/ambient-agent/ExplorerLoop.js +640 -640
  29. package/plugins/ambient-agent/GoalManager.js +197 -197
  30. package/plugins/ambient-agent/Reflector.js +95 -95
  31. package/plugins/ambient-agent/StateStore.js +90 -90
  32. package/plugins/ambient-agent/constants.js +101 -101
  33. package/plugins/ambient-agent/index.js +579 -579
  34. package/plugins/audit-plugin.js +187 -187
  35. package/plugins/default-plugins.js +662 -662
  36. package/plugins/email/constants.js +64 -64
  37. package/plugins/email/handlers.js +461 -461
  38. package/plugins/email/index.js +278 -278
  39. package/plugins/email/monitor.js +269 -269
  40. package/plugins/email/parser.js +138 -138
  41. package/plugins/email/reply.js +151 -151
  42. package/plugins/email/utils.js +124 -124
  43. package/plugins/feishu-plugin.js +481 -481
  44. package/plugins/file-system-plugin.js +826 -826
  45. package/plugins/install-plugin.js +199 -199
  46. package/plugins/python-executor-plugin.js +367 -367
  47. package/plugins/python-plugin-loader.js +481 -481
  48. package/plugins/rules-plugin.js +294 -294
  49. package/plugins/scheduler-plugin.js +691 -691
  50. package/plugins/session-plugin.js +369 -369
  51. package/plugins/shell-executor-plugin.js +197 -197
  52. package/plugins/storage-plugin.js +240 -240
  53. package/plugins/subagent-plugin.js +845 -845
  54. package/plugins/telegram-plugin.js +482 -482
  55. package/plugins/think-plugin.js +345 -345
  56. package/plugins/tools-plugin.js +196 -196
  57. package/plugins/web-plugin.js +606 -606
  58. package/plugins/weixin-plugin.js +545 -545
  59. package/src/capabilities/index.js +11 -11
  60. package/src/capabilities/skill-manager.js +609 -609
  61. package/src/capabilities/workflow-engine.js +1109 -1109
  62. package/src/core/agent-chat.js +882 -882
  63. package/src/core/agent.js +892 -892
  64. package/src/core/framework.js +465 -465
  65. package/src/core/index.js +19 -19
  66. package/src/core/plugin-base.js +219 -219
  67. package/src/core/plugin-manager.js +863 -863
  68. package/src/core/provider.js +114 -114
  69. package/src/core/sub-agent-config.js +264 -264
  70. package/src/core/system-prompt-builder.js +120 -120
  71. package/src/core/tool-registry.js +517 -517
  72. package/src/core/tool-router.js +297 -297
  73. package/src/executors/executor-base.js +58 -58
  74. package/src/executors/mcp-executor.js +741 -741
  75. package/src/index.js +25 -25
  76. package/src/utils/circuit-breaker.js +301 -301
  77. package/src/utils/error-boundary.js +363 -363
  78. package/src/utils/error.js +374 -374
  79. package/src/utils/event-emitter.js +97 -97
  80. package/src/utils/id.js +133 -133
  81. package/src/utils/index.js +217 -217
  82. package/src/utils/logger.js +181 -181
  83. package/src/utils/plugin-helpers.js +90 -90
  84. package/src/utils/retry.js +122 -122
  85. package/src/utils/sandbox.js +292 -292
  86. package/test/tool-registry-validation.test.js +218 -218
  87. package/website/script.js +136 -136
  88. 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 }