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.
- package/.agent/data/email/processed-emails.json +1 -0
- package/.agent/data/plugins-state.json +5 -1
- package/.agent/data/web/web-config.json +5 -0
- package/.agent/memory/feedback/mnt7jrlt-d67qs7.md +15 -0
- package/.agent/memory/feedback/mnt88ja3-al4fuy.md +9 -0
- package/.agent/plugins/test-plugin.py +108 -0
- package/.agent/sessions/cli_default.json +514 -5298
- package/.claude/settings.local.json +2 -1
- package/SPEC.md +735 -696
- package/output/zen_silence.png +0 -0
- package/package.json +2 -2
- package/plugins/ambient-agent/EventWatcher.js +33 -37
- package/plugins/ambient-agent/ExplorerLoop.js +338 -36
- package/plugins/ambient-agent/GoalManager.js +7 -3
- package/plugins/ambient-agent/StateStore.js +30 -1
- package/plugins/ambient-agent/constants.js +15 -1
- package/plugins/ambient-agent/index.js +26 -33
- package/plugins/coordinator-plugin.js +3 -3
- package/plugins/default-plugins.js +2 -2
- package/plugins/email/index.js +150 -36
- package/plugins/email/monitor.js +79 -5
- package/plugins/email/reply.js +15 -25
- package/plugins/extension-executor-plugin.js +160 -31
- package/plugins/file-system-plugin.js +57 -24
- package/plugins/memory-plugin.js +176 -64
- package/plugins/python-plugin-loader.js +79 -9
- package/plugins/scheduler-plugin.js +64 -24
- package/plugins/think-plugin.js +7 -2
- package/plugins/web-plugin.js +263 -4
- package/skills/ambient-agent/SKILL.md +342 -314
- package/src/core/agent-chat.js +64 -9
- package/src/core/agent.js +118 -59
- package/src/core/tool-registry.js +5 -5
- package/src/executors/mcp-executor.js +188 -26
- package/src/utils/id.js +5 -0
- package/system.md +3480 -0
- package/.agent/data/ambient/goals.json +0 -50
- package/.agent/data/ambient/memories.json +0 -7
- package/.agent/memory/core.md +0 -1
- package/.agent/memory/feedback/mnrsiuoc-e1ru74.md +0 -9
- package/.agent/memory/feedback/mnrt2mmz-98az6n.md +0 -9
- package/.agent/memory/feedback/mnrtqrhm-kxsicz.md +0 -9
- package/.agent/memory/feedback/mnrts8vg-i0ngzp.md +0 -15
- package/.agent/memory/feedback/mnrtt7jt-c0trb2.md +0 -9
- package/.agent/memory/feedback/mnruc2f0-5s52la.md +0 -16
- package/.agent/memory/feedback/mnrumbmx-63sa0v.md +0 -9
- package/.agent/memory/project/mnn93ogy-ypjn27.md +0 -9
- package/.agent/memory/project/mnn98fqy-5nhc1u.md +0 -25
- package/.agent/memory/project/mnrp7p5n-8enm2a.md +0 -31
- package/.agent/memory/project/mnrp9ifb-yynks0.md +0 -40
- package/.agent/memory/project/mnrpb3b8-f617s4.md +0 -25
- package/.agent/memory/project/mnrrmqgg-focprv.md +0 -9
- package/.agent/memory/project/mnrtykbh-6atsor.md +0 -9
- package/.agent/memory/project/mnru9jiu-kgau16.md +0 -35
- package/.agent/memory/reference/mnq3oenw-46haj6.md +0 -63
- package/.agent/memory/reference/mnq5qxm2-mjoooh.md +0 -116
- package/.agent/memory/reference/mnrnvpwo-rcqv9m.md +0 -52
- package/.agent/memory/reference/mnrovxvz-zy9xqm.md +0 -25
- package/.agent/memory/reference/mnroxabj-1b3930.md +0 -68
- package/.agent/memory/reference/mnrpjtlp-mnb9od.md +0 -35
- package/.agent/memory/reference/mnrps1x3-6b8xfm.md +0 -28
- package/.agent/memory/reference/mnrpt9ov-15er5w.md +0 -22
- package/.agent/memory/reference/mnrq82dn-y9tv9e.md +0 -50
- package/.agent/memory/reference/mnrqnr5v-v75drf.md +0 -34
- package/.agent/memory/reference/mnrrfzys-urudaf.md +0 -31
- package/.agent/memory/reference/mnrrocha-t0027n.md +0 -21
- package/.agent/memory/reference/mnrukklc-bxndsb.md +0 -35
- package/.agent/memory/user/mnm67t9m-x8rekk.md +0 -9
- package/.agent/memory/user/mnn5mmqh-w6aktx.md +0 -11
- package/.agent/memory/user/mnnbfhhn-dk1bd1.md +0 -22
- package/.agent/memory/user/mnrt39t8-8eosy0.md +0 -9
- package/foliko-cloud-rising.png +0 -0
- package/foliko-dawn-of-ai.png +0 -0
- package/foliko-mindful-observation.png +0 -0
- package/foliko-stellar-dreams.png +0 -0
- package/foliko-zen-jing.png +0 -0
- package/foliko-zen-kong.png +0 -0
- package/foliko-zen-wu.png +0 -0
- package/zen_karesansui.png +0 -0
package/plugins/email/monitor.js
CHANGED
|
@@ -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.
|
|
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
|
|
package/plugins/email/reply.js
CHANGED
|
@@ -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
|
-
//
|
|
17
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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 (!
|
|
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
|
-
|
|
127
|
-
|
|
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:
|
|
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('
|
|
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
|
-
|
|
362
|
-
|
|
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
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
}
|
|
380
|
-
//
|
|
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
|
|
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
|
-
* 将
|
|
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
|
|
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
|
-
//
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
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
|
-
|
|
875
|
-
|
|
876
|
-
|
|
880
|
+
filePath: filePath,
|
|
881
|
+
markdown: markdown,
|
|
882
|
+
content: markdown
|
|
877
883
|
}
|
|
878
884
|
} catch (e) {
|
|
879
|
-
//
|
|
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
|
-
|
|
890
|
-
|
|
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
|
-
//
|
|
932
|
+
// 返回原始内容
|
|
900
933
|
const truncatedData = maxLength && typeof data === 'string' && data.length > maxLength
|
|
901
934
|
? data.substring(0, maxLength) + '\n\n...(truncated)'
|
|
902
935
|
: data
|