foliko 1.1.6 → 1.1.7

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 (79) hide show
  1. package/.agent/data/email/processed-emails.json +1 -0
  2. package/.agent/data/plugins-state.json +5 -1
  3. package/.agent/data/web/web-config.json +5 -0
  4. package/.agent/memory/feedback/mnt7jrlt-d67qs7.md +15 -0
  5. package/.agent/memory/feedback/mnt88ja3-al4fuy.md +9 -0
  6. package/.agent/plugins/test-plugin.py +108 -0
  7. package/.agent/sessions/cli_default.json +514 -5298
  8. package/.claude/settings.local.json +2 -1
  9. package/SPEC.md +735 -696
  10. package/output/zen_silence.png +0 -0
  11. package/package.json +2 -2
  12. package/plugins/ambient-agent/EventWatcher.js +33 -37
  13. package/plugins/ambient-agent/ExplorerLoop.js +338 -36
  14. package/plugins/ambient-agent/GoalManager.js +7 -3
  15. package/plugins/ambient-agent/StateStore.js +30 -1
  16. package/plugins/ambient-agent/constants.js +15 -1
  17. package/plugins/ambient-agent/index.js +26 -33
  18. package/plugins/coordinator-plugin.js +3 -3
  19. package/plugins/default-plugins.js +2 -2
  20. package/plugins/email/index.js +150 -36
  21. package/plugins/email/monitor.js +79 -5
  22. package/plugins/email/reply.js +15 -25
  23. package/plugins/extension-executor-plugin.js +160 -31
  24. package/plugins/file-system-plugin.js +57 -24
  25. package/plugins/memory-plugin.js +176 -64
  26. package/plugins/python-plugin-loader.js +79 -9
  27. package/plugins/scheduler-plugin.js +64 -24
  28. package/plugins/think-plugin.js +7 -2
  29. package/plugins/web-plugin.js +263 -4
  30. package/skills/ambient-agent/SKILL.md +342 -314
  31. package/src/core/agent-chat.js +64 -9
  32. package/src/core/agent.js +118 -59
  33. package/src/core/tool-registry.js +5 -5
  34. package/src/executors/mcp-executor.js +188 -26
  35. package/src/utils/id.js +5 -0
  36. package/system.md +3480 -0
  37. package/.agent/data/ambient/goals.json +0 -50
  38. package/.agent/data/ambient/memories.json +0 -7
  39. package/.agent/memory/core.md +0 -1
  40. package/.agent/memory/feedback/mnrsiuoc-e1ru74.md +0 -9
  41. package/.agent/memory/feedback/mnrt2mmz-98az6n.md +0 -9
  42. package/.agent/memory/feedback/mnrtqrhm-kxsicz.md +0 -9
  43. package/.agent/memory/feedback/mnrts8vg-i0ngzp.md +0 -15
  44. package/.agent/memory/feedback/mnrtt7jt-c0trb2.md +0 -9
  45. package/.agent/memory/feedback/mnruc2f0-5s52la.md +0 -16
  46. package/.agent/memory/feedback/mnrumbmx-63sa0v.md +0 -9
  47. package/.agent/memory/project/mnn93ogy-ypjn27.md +0 -9
  48. package/.agent/memory/project/mnn98fqy-5nhc1u.md +0 -25
  49. package/.agent/memory/project/mnrp7p5n-8enm2a.md +0 -31
  50. package/.agent/memory/project/mnrp9ifb-yynks0.md +0 -40
  51. package/.agent/memory/project/mnrpb3b8-f617s4.md +0 -25
  52. package/.agent/memory/project/mnrrmqgg-focprv.md +0 -9
  53. package/.agent/memory/project/mnrtykbh-6atsor.md +0 -9
  54. package/.agent/memory/project/mnru9jiu-kgau16.md +0 -35
  55. package/.agent/memory/reference/mnq3oenw-46haj6.md +0 -63
  56. package/.agent/memory/reference/mnq5qxm2-mjoooh.md +0 -116
  57. package/.agent/memory/reference/mnrnvpwo-rcqv9m.md +0 -52
  58. package/.agent/memory/reference/mnrovxvz-zy9xqm.md +0 -25
  59. package/.agent/memory/reference/mnroxabj-1b3930.md +0 -68
  60. package/.agent/memory/reference/mnrpjtlp-mnb9od.md +0 -35
  61. package/.agent/memory/reference/mnrps1x3-6b8xfm.md +0 -28
  62. package/.agent/memory/reference/mnrpt9ov-15er5w.md +0 -22
  63. package/.agent/memory/reference/mnrq82dn-y9tv9e.md +0 -50
  64. package/.agent/memory/reference/mnrqnr5v-v75drf.md +0 -34
  65. package/.agent/memory/reference/mnrrfzys-urudaf.md +0 -31
  66. package/.agent/memory/reference/mnrrocha-t0027n.md +0 -21
  67. package/.agent/memory/reference/mnrukklc-bxndsb.md +0 -35
  68. package/.agent/memory/user/mnm67t9m-x8rekk.md +0 -9
  69. package/.agent/memory/user/mnn5mmqh-w6aktx.md +0 -11
  70. package/.agent/memory/user/mnnbfhhn-dk1bd1.md +0 -22
  71. package/.agent/memory/user/mnrt39t8-8eosy0.md +0 -9
  72. package/foliko-cloud-rising.png +0 -0
  73. package/foliko-dawn-of-ai.png +0 -0
  74. package/foliko-mindful-observation.png +0 -0
  75. package/foliko-stellar-dreams.png +0 -0
  76. package/foliko-zen-jing.png +0 -0
  77. package/foliko-zen-kong.png +0 -0
  78. package/foliko-zen-wu.png +0 -0
  79. package/zen_karesansui.png +0 -0
@@ -22,6 +22,70 @@ function createImapConfig(config = {}) {
22
22
  }
23
23
  }
24
24
 
25
+ /**
26
+ * 标记所有现有未读邮件为已读(启动监控前调用)
27
+ * @param {Object} emailPlugin - EmailPlugin 实例
28
+ * @returns {Promise<number>} 标记的邮件数量
29
+ */
30
+ async function markAllExistingUnreadAsRead(emailPlugin) {
31
+ const config = emailPlugin._watchConfig
32
+ if (!config) return 0
33
+
34
+ const imapConfig = createImapConfig(config)
35
+ const box = config.box || 'INBOX'
36
+
37
+ return new Promise((resolve, reject) => {
38
+ const imap = new Imap(imapConfig)
39
+
40
+ imap.on('ready', () => {
41
+ imap.openBox(box, false, (err, boxData) => {
42
+ if (err) {
43
+ imap.end()
44
+ return reject(err)
45
+ }
46
+
47
+ // 搜索所有 UNSEEN 邮件
48
+ imap.search(['UNSEEN'], (err, results) => {
49
+ if (err) {
50
+ imap.end()
51
+ return reject(err)
52
+ }
53
+
54
+ if (!results || results.length === 0) {
55
+ imap.end()
56
+ emailPlugin._log.info(` 没有现有未读邮件需要标记`)
57
+ return resolve(0)
58
+ }
59
+
60
+ emailPlugin._log.info(` 发现 ${results.length} 封现有未读邮件,开始标记为已读...`)
61
+
62
+ // 批量标记为已读
63
+ if (results.length > 0) {
64
+ imap.setFlags(results, '\\Seen', (err) => {
65
+ if (err) {
66
+ emailPlugin._log.warn(` 标记已读失败: ${err.message}`)
67
+ } else {
68
+ emailPlugin._log.info(` 已标记 ${results.length} 封邮件为已读`)
69
+ }
70
+ imap.end()
71
+ resolve(results.length)
72
+ })
73
+ } else {
74
+ imap.end()
75
+ resolve(0)
76
+ }
77
+ })
78
+ })
79
+ })
80
+
81
+ imap.on('error', (err) => {
82
+ reject(err)
83
+ })
84
+
85
+ imap.connect()
86
+ })
87
+ }
88
+
25
89
  /**
26
90
  * 处理邮件监控
27
91
  * @param {Object} emailPlugin - EmailPlugin 实例
@@ -52,7 +116,7 @@ function handleEmailWatch(emailPlugin, args) {
52
116
  * @param {Object} config - 监控配置
53
117
  * @returns {Object} 操作结果
54
118
  */
55
- function startEmailWatch(emailPlugin, config) {
119
+ async function startEmailWatch(emailPlugin, config) {
56
120
  if (emailPlugin._watchEnabled) {
57
121
  return { success: false, error: 'Email watch is already running' }
58
122
  }
@@ -72,6 +136,13 @@ function startEmailWatch(emailPlugin, config) {
72
136
 
73
137
  emailPlugin._watchEnabled = true
74
138
 
139
+ // 启动监控前,先标记所有现有未读邮件为已读
140
+ try {
141
+ await markAllExistingUnreadAsRead(emailPlugin)
142
+ } catch (err) {
143
+ emailPlugin._log.warn(' 标记现有邮件已读失败,继续监控:', err.message)
144
+ }
145
+
75
146
  // 立即执行一次检查
76
147
  emailPlugin._checkNewEmails().catch(err => {
77
148
  emailPlugin._log.error('Initial check failed:', err.message)
@@ -143,7 +214,7 @@ async function checkNewEmails(emailPlugin) {
143
214
  if (!emailPlugin._watchConfig) return
144
215
 
145
216
  const callId = generateCallId()
146
- emailPlugin._log.debug(`[${callId}] _checkNewEmails called, _checkInProgress=${emailPlugin._checkInProgress}`)
217
+ //emailPlugin._log.info(`[Email] 开始检查新邮件...`)
147
218
 
148
219
  // 防止并发检查
149
220
  if (emailPlugin._checkInProgress) {
@@ -177,6 +248,7 @@ async function checkNewEmails(emailPlugin) {
177
248
  }
178
249
 
179
250
  if (!results || results.length === 0) {
251
+ emailPlugin._log.info(`[Email] 没有未读邮件`)
180
252
  cleanup()
181
253
  return resolve({ success: true, newEmails: 0 })
182
254
  }
@@ -187,7 +259,12 @@ async function checkNewEmails(emailPlugin) {
187
259
  )
188
260
 
189
261
  if (newEmails.length > 0) {
262
+ emailPlugin._log.info(`[Email] 发现 ${newEmails.length} 封未读邮件,开始处理...`)
190
263
  const latestUid = Math.max(...newEmails)
264
+
265
+ // 先更新 _lastSeenUid,防止重复检测
266
+ emailPlugin._lastSeenUid = latestUid
267
+
191
268
  // imap.fetch() 需要 UIDs 数组,传单个数字会导致获取从该 UID 到末尾的所有邮件
192
269
  const f = imap.fetch([latestUid], { bodies: '' })
193
270
 
@@ -221,9 +298,6 @@ async function checkNewEmails(emailPlugin) {
221
298
  msg.on('end', () => {
222
299
  const checkDone = () => {
223
300
  if (bodyParsed) {
224
- // 更新最后看到的 UID
225
- emailPlugin._lastSeenUid = latestUid
226
-
227
301
  // 发送事件通知
228
302
  emailPlugin._emitEmailReceived(email)
229
303
 
@@ -13,38 +13,25 @@ const { EMAIL_DEFAULTS } = require('./constants')
13
13
  async function handleAutoReply(emailPlugin, args) {
14
14
  let { to, subject, body, from, _event, prompt, messageId, inReplyTo } = args
15
15
 
16
- // 如果没有直接参数,尝试从 _event 提取
17
- // 支持多种事件结构:{ email, timestamp } { data: { email } } 或 { data: { ... } }
18
- if (!to && !subject && !body && _event) {
16
+ // 优先使用传入的参数,如果没有则尝试从 _event 提取(兼容旧方式)
17
+ if ((!to || !subject || !body) && _event) {
19
18
  // 兼容多种事件结构
20
19
  const email = _event.email || _event.data?.email || _event.data || {}
21
- from = email.from?.text || email.from || ''
22
- to = email.to?.text || email.to || ''
23
- subject = email.subject || ''
24
- body = email.text || email.body || ''
25
- // 优先使用 _event 中的 uid/messageId
26
- if (!messageId) {
27
- messageId = email.messageId || email.uid || _event.data?.messageId || _event.data?.uid
28
- }
29
- }
30
-
31
- // 如果 _event 没有有效数据,不允许自动读取所有邮件
32
- if (!_event || (!_event.email && !_event.data)) {
33
- return {
34
- success: false,
35
- error: '缺少邮件数据:_event 中没有有效的邮件信息,无法自动回复。请确认是否在收到新邮件事件时调用此工具。'
36
- }
20
+ to = to || email.from?.text || email.from || ''
21
+ subject = subject || email.subject || ''
22
+ body = body || email.text || email.body || ''
23
+ messageId = messageId || email.messageId || email.uid
37
24
  }
38
25
 
39
26
  // 优先使用传入的 inReplyTo,否则用 messageId
40
27
  const replyTo = inReplyTo || messageId
41
28
 
42
29
  // 检查必要参数
43
- if (!from && !to) {
44
- return { success: false, error: '缺少收件人地址' }
30
+ if (!to) {
31
+ return { success: false, error: '缺少收件人地址(to)' }
45
32
  }
46
33
  if (!body) {
47
- return { success: false, error: '缺少邮件内容' }
34
+ return { success: false, error: '缺少邮件内容(body)' }
48
35
  }
49
36
 
50
37
  try {
@@ -114,20 +101,23 @@ ${body}
114
101
  const msgId = messageId || emailData.messageId || emailData.uid
115
102
  if (msgId) {
116
103
  emailPlugin._processedEmails.add(msgId)
104
+ emailPlugin._saveProcessedEmails()
117
105
  emailPlugin._log.info(`邮件已处理: ${msgId}`)
118
106
 
119
107
  // 24小时后从已处理列表中移除
120
108
  setTimeout(() => {
121
109
  emailPlugin._processedEmails.delete(msgId)
110
+ emailPlugin._saveProcessedEmails()
122
111
  }, EMAIL_DEFAULTS.processedEmailTTL)
123
112
  }
124
113
 
125
114
  // 自动标记原邮件为已读
126
- const uid = emailData.uid || msgId
127
- if (uid) {
115
+ // emailData.uid 是数字 UID,emailData.messageId 是字符串
116
+ const emailUid = emailData.uid
117
+ if (emailUid) {
128
118
  try {
129
119
  const { markAsRead } = require('./handlers')
130
- await markAsRead(emailPlugin, { uid: uid }) // 使用 uid 而非 messageId
120
+ await markAsRead(emailPlugin, { uid: emailUid }) // 使用数字 uid
131
121
  } catch (err) {
132
122
  emailPlugin._log.warn(`自动标记已读失败: ${err.message}`)
133
123
  }
@@ -123,11 +123,14 @@ class ExtensionExecutorPlugin extends Plugin {
123
123
  this._scanPluginTools(plugin);
124
124
  }
125
125
 
126
+ // 获取 MCP executor 引用(用于 ext_call 统一路由)
127
+ this._mcpExecutor = framework.pluginManager?.get('mcp') || null;
128
+
126
129
  framework.registerTool({
127
130
  name: 'ext_call',
128
- description: '调用扩展插件的工具',
131
+ description: '调用扩展插件的工具(包括 MCP 服务器工具)',
129
132
  inputSchema: z.object({
130
- plugin: z.string().describe('扩展插件名称'),
133
+ plugin: z.string().describe('插件名称(如 email, gate-trading, mcp)'),
131
134
  tool: z.string().describe('工具名称'),
132
135
  args: z.record(z.any()).optional().describe('工具参数'),
133
136
  }),
@@ -135,6 +138,16 @@ class ExtensionExecutorPlugin extends Plugin {
135
138
  const { plugin, tool, args: toolArgs = {} } = args;
136
139
  log.info(` ext_call: plugin=${plugin}, tool=${tool}, args=${JSON.stringify(toolArgs)}`);
137
140
 
141
+ // MCP 服务器工具(已注册为 server_toolname 格式,如 github_search)
142
+ if (plugin === 'mcp' && this._mcpExecutor) {
143
+ const mcpToolDef = this._mcpExecutor.tools?.[tool];
144
+ if (mcpToolDef && mcpToolDef.execute) {
145
+ log.info(` ext_call [MCP]: tool=${tool}`);
146
+ return await mcpToolDef.execute(toolArgs || {});
147
+ }
148
+ return { success: false, error: `MCP tool '${tool}' not found` };
149
+ }
150
+
138
151
  const ext = this._extensions.get(plugin);
139
152
  if (!ext) {
140
153
  // 触发错误事件
@@ -337,7 +350,7 @@ class ExtensionExecutorPlugin extends Plugin {
337
350
  const existingPrompt = agent._originalPrompt || '';
338
351
 
339
352
  // 如果已有 [Extensions] 部分,替换它
340
- const extStartIdx = existingPrompt.indexOf('[Extensions]');
353
+ const extStartIdx = existingPrompt.indexOf('Extensions');
341
354
  if (extStartIdx !== -1) {
342
355
  const extEndMarker = '\n\n【';
343
356
  let extEndIdx = existingPrompt.indexOf(extEndMarker, extStartIdx);
@@ -358,48 +371,162 @@ class ExtensionExecutorPlugin extends Plugin {
358
371
  }
359
372
 
360
373
  _buildExtensionsDescription() {
361
- if (this._extensions.size === 0) {
362
- return '';
363
- }
374
+ let desc = '';
375
+
376
+ // 1. 扩展插件工具(包括 MCP,已通过 this.tools 注册为 server_toolname 格式)
377
+ if (this._extensions.size > 0 || (this._mcpExecutor && Object.keys(this._mcpExecutor.tools || {}).length > 0)) {
378
+ desc += '## 【Extensions】扩展插件工具\n\n';
379
+ desc += '你可以通过 `ext_call` 工具调用以下扩展插件的功能。\n\n';
380
+
381
+ for (const [name, ext] of this._extensions) {
382
+ desc += `### ${ext.name || name}\n\n`;
383
+ desc += `${ext.description || ''}\n\n`;
384
+ for (const tool of ext.tools) {
385
+ desc += `- **${tool.name}**: ${tool.description || '无描述'}\n`;
386
+ // 添加参数描述
387
+ if (tool.inputSchema) {
388
+ try {
389
+ if (typeof tool.inputSchema.toJSON === 'function') {
390
+ desc += `**参数:**\n\n`;
391
+ desc += zodSchemaToMarkdown(tool.inputSchema) + '\n\n';
392
+ } else if (tool.inputSchema.properties) {
393
+ // JSON Schema 格式
394
+ desc += `**参数:**\n\n`;
395
+ desc += zodSchemaToMarkdown(tool.inputSchema) + '\n\n';
396
+ }
397
+ } catch (e) {
398
+ // 忽略转换错误
399
+ }
400
+ }
401
+ }
402
+ desc += '\n';
403
+ }
364
404
 
365
- let desc = '[Extensions]\n\n';
366
- desc += '你可以通过 `ext_call` 工具调用以下扩展插件的功能。\n\n';
367
-
368
- for (const [name, ext] of this._extensions) {
369
- desc += `### ${ext.name || name}\n\n`;
370
- desc += `${ext.description || ''}\n\n`;
371
- for (const tool of ext.tools) {
372
- desc += `- **${tool.name}**: ${tool.description || '无描述'}\n`;
373
- // 添加参数描述
374
- if (tool.inputSchema) {
375
- try {
376
- if (typeof tool.inputSchema.toJSON === 'function') {
377
- desc += `**参数:**\n\n`;
378
- desc += zodSchemaToMarkdown(tool.inputSchema) + '\n\n';
379
- } else if (tool.inputSchema.properties) {
380
- // JSON Schema 格式
381
- desc += `**参数:**\n\n`;
382
- desc += this._schemaToMarkdown(tool.inputSchema) + '\n\n';
405
+ // MCP 服务器工具(已注册为 server_toolname 格式)
406
+ if (this._mcpExecutor && Object.keys(this._mcpExecutor.tools || {}).length > 0) {
407
+ desc += '### mcp (MCP 服务器工具)\n\n';
408
+ desc += 'MCP 服务器工具已注册为 `服务器_工具名` 格式(如 github_search)。\n\n';
409
+ for (const [toolName, toolDef] of Object.entries(this._mcpExecutor.tools)) {
410
+ desc += `- **${toolName}**: ${toolDef.description || '无描述'}\n`;
411
+ // 添加参数描述
412
+ if (toolDef.inputSchema) {
413
+ try {
414
+ const schemaResult = this._convertSchemaToMarkdown(toolDef.inputSchema);
415
+ if (schemaResult) {
416
+ desc += `**参数:**\n\n`;
417
+ desc += schemaResult + '\n\n';
418
+ }
419
+ } catch (e) {
420
+ // 忽略转换错误
383
421
  }
384
- } catch (e) {
385
- // 忽略转换错误
386
422
  }
387
423
  }
424
+ desc += '\n';
388
425
  }
389
- desc += '\n';
390
426
  }
391
427
 
392
- desc += '**调用格式:**\n';
428
+ if (!desc) {
429
+ return '';
430
+ }
431
+
432
+ desc += '**统一调用格式:**\n';
393
433
  desc += '```\next_call({ plugin: "插件名", tool: "工具名", args: {参数} })\n';
394
434
  desc += '```\n';
395
435
  return desc.trim();
396
436
  }
397
437
 
398
438
  /**
399
- * 将 JSON Schema 转换为 Markdown
439
+ * 将 Schema 转换为 Markdown 格式的参数描述
440
+ * 支持 Zod schema 和 JSON Schema
441
+ */
442
+ _convertSchemaToMarkdown(inputSchema) {
443
+ if (!inputSchema) return null;
444
+
445
+ // 提取实际 schema(处理 { jsonSchema: {...} } 或 { inputSchema: {...} } 格式)
446
+ let schema = inputSchema;
447
+ if (inputSchema.jsonSchema) schema = inputSchema.jsonSchema;
448
+ if (inputSchema.inputSchema) schema = inputSchema.inputSchema;
449
+
450
+ // 检查是否是 Zod schema
451
+ const isZodSchema = typeof schema.shape === 'function' || (schema._def && schema._def.typeName);
452
+ if (isZodSchema) {
453
+ // Zod schema 直接转换
454
+ return zodSchemaToMarkdown(schema);
455
+ }
456
+
457
+ // JSON Schema 转换为 Zod 再转换
458
+ if (schema.properties) {
459
+ const zodSchema = this._jsonSchemaToZod(schema);
460
+ if (zodSchema) {
461
+ return zodSchemaToMarkdown(zodSchema);
462
+ }
463
+ }
464
+
465
+ // 如果都没有,回退到简单格式
466
+ return this._schemaToMarkdown(schema);
467
+ }
468
+
469
+ /**
470
+ * 将 JSON Schema 转换为 Zod schema
471
+ */
472
+ _jsonSchemaToZod(jsonSchema) {
473
+ if (!jsonSchema || !jsonSchema.properties) return null;
474
+
475
+ try {
476
+ const shape = {};
477
+ const properties = jsonSchema.properties;
478
+ const required = jsonSchema.required || [];
479
+
480
+ for (const [key, prop] of Object.entries(properties)) {
481
+ shape[key] = this._jsonSchemaPropToZod(prop, required.includes(key));
482
+ }
483
+
484
+ return z.object(shape);
485
+ } catch (e) {
486
+ return null;
487
+ }
488
+ }
489
+
490
+ /**
491
+ * 将 JSON Schema 属性转换为 Zod 类型
492
+ */
493
+ _jsonSchemaPropToZod(prop, isRequired) {
494
+ if (prop.enum) {
495
+ let zodType = z.string().enum(prop.enum);
496
+ if (prop.nullable) zodType = zodType.nullable();
497
+ return isRequired ? zodType : zodType.optional();
498
+ }
499
+
500
+ const type = prop.type || 'string';
501
+ switch (type) {
502
+ case 'string':
503
+ return isRequired ? z.string() : z.string().optional();
504
+ case 'number':
505
+ case 'integer':
506
+ return isRequired ? z.number() : z.number().optional();
507
+ case 'boolean':
508
+ return isRequired ? z.boolean() : z.boolean().optional();
509
+ case 'array':
510
+ return isRequired ? z.array(z.any()) : z.array(z.any()).optional();
511
+ case 'object':
512
+ if (prop.properties) {
513
+ const nested = {};
514
+ for (const [k, v] of Object.entries(prop.properties)) {
515
+ nested[k] = this._jsonSchemaPropToZod(v, prop.required?.includes(k) || false);
516
+ }
517
+ return isRequired ? z.object(nested) : z.object(nested).optional();
518
+ }
519
+ return isRequired ? z.record(z.any()) : z.record(z.any()).optional();
520
+ default:
521
+ return isRequired ? z.any() : z.any().optional();
522
+ }
523
+ }
524
+
525
+ /**
526
+ * 将 JSON Schema 转换为 Markdown(简单格式)
400
527
  */
401
528
  _schemaToMarkdown(schema) {
402
- if (!schema || !schema.properties) return '';
529
+ if (!schema || !schema.properties) return null;
403
530
 
404
531
  const props = schema.properties || {};
405
532
  const required = schema.required || [];
@@ -411,7 +538,7 @@ class ExtensionExecutorPlugin extends Plugin {
411
538
  const descText = prop.description || '';
412
539
  md += `- \`${key}\`${isRequired ? ' (必填)' : ''}: ${type} ${descText}\n`;
413
540
  }
414
- return md;
541
+ return md || null;
415
542
  }
416
543
 
417
544
  getExtensions() {
@@ -431,6 +558,8 @@ class ExtensionExecutorPlugin extends Plugin {
431
558
  for (const { instance: plugin } of plugins) {
432
559
  this._scanPluginTools(plugin);
433
560
  }
561
+ // 重新获取 MCP executor 引用
562
+ this._mcpExecutor = framework.pluginManager?.get('mcp') || null;
434
563
  // 刷新所有 Agent 的扩展提示词
435
564
  this._refreshAllAgentsExtPrompt(framework);
436
565
  }
@@ -818,7 +818,7 @@ class FileSystemPlugin extends Plugin {
818
818
  // HTTP 请求
819
819
  framework.registerTool({
820
820
  name: 'fetch',
821
- description: '发送 HTTP 请求获取远程数据。如果访问失败或超时,LLM 应自动使用 proxy: true 重试(代理会绕过网络限制)。',
821
+ description: '发送 HTTP 请求获取远程数据。HTML 内容会保存到临时文件并返回路径;非 HTML 内容可选择转换为 Markdown。注意:如果访问失败或超时,LLM 应自动使用 proxy: true 重试。',
822
822
  inputSchema: z.object({
823
823
  url: z.string().describe('请求 URL'),
824
824
  method: z.enum(['GET', 'POST', 'PUT', 'DELETE', 'PATCH']).optional().describe('请求方法,默认 GET'),
@@ -831,6 +831,10 @@ class FileSystemPlugin extends Plugin {
831
831
  }),
832
832
  execute: async (args, framework) => {
833
833
  const { url, method = 'GET', headers = {}, body, timeout = 30000, proxy = false, toMarkdown = true, maxLength } = args
834
+ const fs = require('fs')
835
+ const path = require('path')
836
+ const os = require('os')
837
+
834
838
  try {
835
839
  const controller = new AbortController()
836
840
  const timeoutId = setTimeout(() => controller.abort(), timeout)
@@ -853,50 +857,79 @@ class FileSystemPlugin extends Plugin {
853
857
  data = text
854
858
  }
855
859
 
856
- // 如果 toMarkdown 为 true 且响应是 HTML,则转换为 Markdown
857
- if (toMarkdown && typeof data === 'string') {
858
- // 检测是否为 HTML 内容
859
- const isHtml = /<[a-z][\s\S]*>/i.test(data) || data.trim().startsWith('<')
860
- if (isHtml) {
860
+ // 检测是否为 HTML 内容
861
+ const isHtml = typeof data === 'string' && (/<[a-z][\s\S]*>/i.test(data) || data.trim().startsWith('<'))
862
+
863
+ // HTML 内容统一保存到临时文件
864
+ if (isHtml) {
865
+ const tmpDir = os.tmpdir()
866
+ const filename = `fetch_${Date.now()}_${Math.random().toString(36).slice(2, 8)}.html`
867
+ const filePath = path.join(tmpDir, filename)
868
+ fs.writeFileSync(filePath, data, 'utf-8')
869
+
870
+ // toMarkdown 为 true 时返回 markdown
871
+ if (toMarkdown) {
861
872
  try {
862
-
863
873
  const markdown = NodeHtmlMarkdown.translate(data)
864
- // 限制返回长度
865
- const truncatedMarkdown = maxLength && markdown.length > maxLength
866
- ? markdown.substring(0, maxLength) + '\n\n...(truncated)'
867
- : markdown
868
874
  return {
869
875
  success: true,
870
876
  status: response.status,
871
877
  statusText: response.statusText,
872
878
  headers: Object.fromEntries(response.headers.entries()),
873
879
  usedProxy: proxy,
874
- markdown: truncatedMarkdown,
875
- originalLength: markdown.length,
876
- truncated: maxLength && markdown.length > maxLength
880
+ filePath: filePath,
881
+ markdown: markdown,
882
+ content: markdown
877
883
  }
878
884
  } catch (e) {
879
- // 转换失败时返回原始 body
880
- const truncatedBody = maxLength && data.length > maxLength
881
- ? data.substring(0, maxLength) + '\n\n...(truncated)'
882
- : data
885
+ // 转换失败时只返回路径
883
886
  return {
884
887
  success: true,
885
888
  status: response.status,
886
889
  statusText: response.statusText,
887
890
  headers: Object.fromEntries(response.headers.entries()),
888
891
  usedProxy: proxy,
889
- markdown: null,
890
- body: truncatedBody,
891
- originalLength: data.length,
892
- truncated: maxLength && data.length > maxLength,
893
- markdownError: e.message
892
+ filePath: filePath,
893
+ content: `[HTML已保存到文件: ${filePath}]`
894
894
  }
895
895
  }
896
896
  }
897
+
898
+ return {
899
+ success: true,
900
+ status: response.status,
901
+ statusText: response.statusText,
902
+ headers: Object.fromEntries(response.headers.entries()),
903
+ usedProxy: proxy,
904
+ filePath: filePath,
905
+ content: `[HTML已保存到文件: ${filePath}]`
906
+ }
907
+ }
908
+
909
+ // 非 HTML 内容
910
+ if (toMarkdown && typeof data === 'string') {
911
+ try {
912
+ const markdown = NodeHtmlMarkdown.translate(data)
913
+ const truncatedMarkdown = maxLength && markdown.length > maxLength
914
+ ? markdown.substring(0, maxLength) + '\n\n...(truncated)'
915
+ : markdown
916
+ return {
917
+ success: true,
918
+ status: response.status,
919
+ statusText: response.statusText,
920
+ headers: Object.fromEntries(response.headers.entries()),
921
+ usedProxy: proxy,
922
+ markdown: truncatedMarkdown,
923
+ content: truncatedMarkdown,
924
+ originalLength: markdown.length,
925
+ truncated: maxLength && markdown.length > maxLength
926
+ }
927
+ } catch (e) {
928
+ // 转换失败时返回原始 body
929
+ }
897
930
  }
898
931
 
899
- // toMarkdown 为 false 或非 HTML 内容时,返回原始内容
932
+ // 返回原始内容
900
933
  const truncatedData = maxLength && typeof data === 'string' && data.length > maxLength
901
934
  ? data.substring(0, maxLength) + '\n\n...(truncated)'
902
935
  : data