foliko 1.0.81 → 1.0.83

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