foliko 2.0.4 → 2.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +4 -1
- package/.cli_default_systemPrompt.md +291 -0
- package/CLAUDE.md +3 -0
- package/README.md +20 -3
- package/docs/architecture.md +34 -2
- package/docs/extensions.md +199 -0
- package/docs/migration.md +100 -0
- package/docs/public-api.md +1180 -6
- package/docs/usage.md +122 -30
- package/package.json +1 -1
- package/plugins/core/audit/index.js +1 -1
- package/plugins/core/default/bootstrap.js +65 -19
- package/plugins/core/python-loader/index.js +43 -25
- package/plugins/core/skill-manager/PROMPT.md +6 -0
- package/plugins/core/skill-manager/index.js +538 -93
- package/plugins/core/sub-agent/PROMPT.md +10 -0
- package/plugins/core/sub-agent/index.js +36 -3
- package/plugins/core/think/index.js +1 -0
- package/plugins/core/workflow/index.js +106 -22
- package/plugins/executors/data-splitter/PROMPT.md +13 -0
- package/plugins/executors/data-splitter/index.js +5 -4
- package/plugins/executors/extension/extension-registry.js +145 -0
- package/plugins/executors/extension/index.js +405 -437
- package/plugins/executors/extension/prompt-builder.js +359 -0
- package/plugins/executors/extension/skill-helper.js +143 -0
- package/plugins/messaging/feishu/index.js +5 -3
- package/plugins/messaging/qq/index.js +6 -4
- package/plugins/messaging/telegram/index.js +6 -3
- package/plugins/messaging/weixin/index.js +5 -3
- package/plugins/tools/PROMPT.md +26 -0
- package/plugins/tools/index.js +6 -5
- package/skills/foliko/AGENTS.md +196 -43
- package/skills/foliko/SKILL.md +157 -28
- package/skills/mcp/SKILL.md +77 -118
- package/skills/plugins/SKILL.md +89 -3
- package/skills/python/SKILL.md +57 -39
- package/skills/skill-guide/SKILL.md +42 -34
- package/skills/workflows/SKILL.md +224 -9
- package/skills/workflows/workflow-troubleshooting/SKILL.md +221 -281
- package/src/agent/chat.js +48 -27
- package/src/agent/main.js +34 -13
- package/src/agent/prompt-registry.js +133 -87
- package/src/agent/prompts/PROMPT.md +3 -0
- package/src/agent/sub.js +1 -1
- package/src/cli/ui/chat-ui-old.js +5 -2
- package/src/cli/ui/chat-ui.js +5 -2
- package/src/common/constants.js +12 -0
- package/src/common/error-capture.js +91 -0
- package/src/common/logger.js +2 -2
- package/src/context/compressor.js +6 -2
- package/src/executors/mcp-executor.js +105 -125
- package/src/framework/framework.js +632 -6
- package/src/index.js +4 -0
- package/src/plugin/base.js +913 -10
- package/src/plugin/manager.js +29 -8
- package/src/tool/schema.js +32 -9
- package/tests/core/plugin-prompts.test.js +13 -13
- package/tests/core/prompt-registry.test.js +59 -51
- package/website/index.html +821 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 全局错误捕获 — 把 uncaughtException 和 unhandledRejection 的完整堆栈落盘
|
|
5
|
+
* 下次出现 Cannot read properties of undefined (reading 'delete') 这类异步错误,
|
|
6
|
+
* 用户直接看 .foliko/error.log 即可拿到带文件名/行号的完整堆栈,无需复现。
|
|
7
|
+
*
|
|
8
|
+
* 设计要点:
|
|
9
|
+
* - 静默写文件,不污染 console(避免和 TUI / CLI 抢输出)
|
|
10
|
+
* - 同时保留原始 process 监听器(若用户已注册),不会覆盖
|
|
11
|
+
* - 一次安装,重复 require 不会重复绑定
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
|
|
17
|
+
const INSTALLED = Symbol.for('foliko.error-capture.installed');
|
|
18
|
+
const PROJECT_LOG = path.join(process.cwd(), '.foliko', 'error.log');
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 获取当前活跃的 sessionId (兼容 AsyncLocalStorage)
|
|
22
|
+
*/
|
|
23
|
+
function getActiveSessionId() {
|
|
24
|
+
try {
|
|
25
|
+
const { requestContext, sessionContext } = require('./context-helpers');
|
|
26
|
+
if (typeof sessionContext?.getStore === 'function') {
|
|
27
|
+
const store = sessionContext.getStore();
|
|
28
|
+
if (store?.sessionId) return store.sessionId;
|
|
29
|
+
}
|
|
30
|
+
} catch (_) { /* 未安装 context 系统时跳过 */ }
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 把一个事件载荷写为单条记录到 .foliko/error.log
|
|
36
|
+
*/
|
|
37
|
+
function dumpEvent(kind, error) {
|
|
38
|
+
let record;
|
|
39
|
+
try {
|
|
40
|
+
const ts = new Date().toISOString();
|
|
41
|
+
const sid = getActiveSessionId();
|
|
42
|
+
const cwd = process.cwd();
|
|
43
|
+
const node = process.version;
|
|
44
|
+
const pid = process.pid;
|
|
45
|
+
|
|
46
|
+
const errObj = error instanceof Error ? error : new Error(String(error));
|
|
47
|
+
const stack = errObj.stack || `(no stack; value=${String(error)})`;
|
|
48
|
+
|
|
49
|
+
record =
|
|
50
|
+
`\n${'='.repeat(72)}\n` +
|
|
51
|
+
`[${ts}] [pid=${pid}] [node=${node}] [cwd=${cwd}] [sessionId=${sid || '(none)'}] kind=${kind}\n` +
|
|
52
|
+
`error.name=${errObj.name || 'Error'} error.message=${errObj.message || '(no message)'}\n` +
|
|
53
|
+
`stack:\n${stack}\n`;
|
|
54
|
+
} catch (innerErr) {
|
|
55
|
+
record = `\n[${new Date().toISOString()}] dump failed: ${innerErr?.message || innerErr}\n`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
fs.mkdirSync(path.dirname(PROJECT_LOG), { recursive: true });
|
|
60
|
+
fs.appendFileSync(PROJECT_LOG, record, 'utf8');
|
|
61
|
+
} catch (_) {
|
|
62
|
+
// 连日志都写不了就别折腾了
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 安装全局错误捕获。重复调用安全(no-op)。
|
|
68
|
+
*/
|
|
69
|
+
function install(options = {}) {
|
|
70
|
+
const logFile = options.logFile || PROJECT_LOG;
|
|
71
|
+
|
|
72
|
+
// 用 global symbol 标记,避免重复 require 时再次 process.on
|
|
73
|
+
if (process[INSTALLED]) return process[INSTALLED];
|
|
74
|
+
if (!process[INSTALLED]) process[INSTALLED] = new Set();
|
|
75
|
+
if (process[INSTALLED].has('core')) return 'core';
|
|
76
|
+
process[INSTALLED].add('core');
|
|
77
|
+
|
|
78
|
+
// 捕获 unhandledRejection —— 这正是用户报的 Promise 拒绝
|
|
79
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
80
|
+
dumpEvent('unhandledRejection', reason);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// 捕获 uncaughtException —— 同步抛错兜底
|
|
84
|
+
process.on('uncaughtException', (err, origin) => {
|
|
85
|
+
dumpEvent(`uncaughtException(${origin})`, err);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
return 'core';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
module.exports = { install, dumpEvent, PROJECT_LOG };
|
package/src/common/logger.js
CHANGED
|
@@ -23,7 +23,7 @@ function ensureLogDir(logDir) {
|
|
|
23
23
|
function getLogFilePath(logDir) {
|
|
24
24
|
ensureLogDir(logDir);
|
|
25
25
|
const date = new Date().toISOString().split('T')[0];
|
|
26
|
-
return path.join(logDir, `
|
|
26
|
+
return path.join(logDir, `foliko-${date}.log`);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
// 当前日志文件路径
|
|
@@ -62,7 +62,7 @@ function initLogger(options = {}) {
|
|
|
62
62
|
const {
|
|
63
63
|
level = process.env.LOG_LEVEL || 'info',
|
|
64
64
|
logDir = DEFAULT_LOG_DIR,
|
|
65
|
-
name = '
|
|
65
|
+
name = 'foliko',
|
|
66
66
|
silent = false, // 是否静默(不输出到控制台)
|
|
67
67
|
} = options;
|
|
68
68
|
|
|
@@ -19,6 +19,10 @@ const {
|
|
|
19
19
|
TURN_PREFIX_SUMMARIZATION_PROMPT, BRANCH_SUMMARY_PREAMBLE, BRANCH_SUMMARY_PROMPT,
|
|
20
20
|
DEFAULT_COMPACTION_SETTINGS, MODEL_CONTEXT_LIMITS, COMPRESSION_TIMEOUT_MS,
|
|
21
21
|
} = require('./compaction-prompts');
|
|
22
|
+
const {
|
|
23
|
+
KEEP_RECENT_MESSAGES,
|
|
24
|
+
COMPRESSION_MESSAGE_THRESHOLD,
|
|
25
|
+
} = require('../common/constants');
|
|
22
26
|
|
|
23
27
|
// ─── Helper classes ───────────────────────────────────────────────────
|
|
24
28
|
|
|
@@ -184,8 +188,8 @@ class ContextCompressor {
|
|
|
184
188
|
|
|
185
189
|
this.model = config.model || 'deepseek-chat';
|
|
186
190
|
this._maxContextTokens = config.maxContextTokens || this._getDefaultContextLimit();
|
|
187
|
-
this._keepRecentMessages = config.keepRecentMessages ||
|
|
188
|
-
this._compressionMessageThreshold = config.compressionMessageThreshold ||
|
|
191
|
+
this._keepRecentMessages = config.keepRecentMessages || KEEP_RECENT_MESSAGES;
|
|
192
|
+
this._compressionMessageThreshold = config.compressionMessageThreshold || COMPRESSION_MESSAGE_THRESHOLD;
|
|
189
193
|
this._enableSmartCompress = config.enableSmartCompress !== false;
|
|
190
194
|
this._compactionSettings = config.compactionSettings || DEFAULT_COMPACTION_SETTINGS;
|
|
191
195
|
|
|
@@ -14,7 +14,7 @@ const { StdioClientTransport } = require('@modelcontextprotocol/sdk/client/stdio
|
|
|
14
14
|
const { zodSchemaToMarkdown } = require('@chnak/zod-to-markdown');
|
|
15
15
|
const { logger } = require('../common/logger');
|
|
16
16
|
const { MCPClientWrapper } = require('./mcp-client');
|
|
17
|
-
const { extractSchema
|
|
17
|
+
const { extractSchema } = require('./mcp-desc');
|
|
18
18
|
const { jsonSchemaToZod } = require('../tool/schema');
|
|
19
19
|
|
|
20
20
|
const log = logger.child('MCPExecutor');
|
|
@@ -53,120 +53,6 @@ class MCPExecutorPlugin extends Plugin {
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
framework.registerTool({
|
|
57
|
-
name: 'mcp_tool_schema',
|
|
58
|
-
description: '查询工具的调用示例,直接复制 example 或 fullExample 字段使用',
|
|
59
|
-
inputSchema: z.object({
|
|
60
|
-
server: z.string().describe('MCP 服务器名称'),
|
|
61
|
-
tool: z.string().describe('工具名称'),
|
|
62
|
-
}),
|
|
63
|
-
execute: async (args) => {
|
|
64
|
-
const { server, tool } = args;
|
|
65
|
-
const clientInfo = this._clients.get(server);
|
|
66
|
-
if (!clientInfo) return { success: false, error: `MCP server '${server}' not found` };
|
|
67
|
-
|
|
68
|
-
const toolInfo = clientInfo.tools.find((t) => t.name === tool);
|
|
69
|
-
if (!toolInfo) return { success: false, error: `Tool '${tool}' not found` };
|
|
70
|
-
|
|
71
|
-
const schema = extractSchema(toolInfo.inputSchema);
|
|
72
|
-
const props = schema.properties || {};
|
|
73
|
-
const required = schema.required || [];
|
|
74
|
-
|
|
75
|
-
const generateExample = (prop, depth = 0) => {
|
|
76
|
-
if (!prop) return null;
|
|
77
|
-
const type = prop.type || (prop.properties ? 'object' : 'string');
|
|
78
|
-
if (type === 'string') return prop.description?.split('.')[0] || `${prop.title || '值'}`;
|
|
79
|
-
if (type === 'number' || type === 'integer') return prop.default || 0;
|
|
80
|
-
if (type === 'boolean') return false;
|
|
81
|
-
if (type === 'array') {
|
|
82
|
-
if (prop.items) {
|
|
83
|
-
const itemExample = generateExample(prop.items, depth + 1);
|
|
84
|
-
return depth === 0 ? [itemExample] : itemExample;
|
|
85
|
-
}
|
|
86
|
-
return ['示例'];
|
|
87
|
-
}
|
|
88
|
-
if (type === 'object' || prop.properties) {
|
|
89
|
-
const obj = {};
|
|
90
|
-
for (const [key, val] of Object.entries(prop.properties || {})) {
|
|
91
|
-
obj[key] = generateExample(val, depth + 1);
|
|
92
|
-
}
|
|
93
|
-
return obj;
|
|
94
|
-
}
|
|
95
|
-
return null;
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
const exampleArgs = {};
|
|
99
|
-
for (const key of required) {
|
|
100
|
-
const prop = props[key];
|
|
101
|
-
if (prop) exampleArgs[key] = generateExample(prop);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return {
|
|
105
|
-
success: true,
|
|
106
|
-
data: {
|
|
107
|
-
name: toolInfo.name, description: toolInfo.description, required,
|
|
108
|
-
parameters: Object.fromEntries(
|
|
109
|
-
required.map((key) => [key, {
|
|
110
|
-
type: props[key]?.type || 'string',
|
|
111
|
-
description: props[key]?.description || '',
|
|
112
|
-
example: generateExample(props[key]),
|
|
113
|
-
}])
|
|
114
|
-
),
|
|
115
|
-
example: JSON.stringify(exampleArgs, null, 2),
|
|
116
|
-
fullExample: `mcp_call({ server: "${server}", tool: "${tool}", args_json: ${JSON.stringify(JSON.stringify(exampleArgs))} })`,
|
|
117
|
-
},
|
|
118
|
-
};
|
|
119
|
-
},
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
framework.registerTool({
|
|
123
|
-
name: 'mcp_call',
|
|
124
|
-
description: '调用 MCP 服务器工具。args_json 是包含实际参数的 JSON 字符串',
|
|
125
|
-
inputSchema: z.object({
|
|
126
|
-
server: z.string().describe('MCP 服务器名称'),
|
|
127
|
-
tool: z.string().describe('工具名称'),
|
|
128
|
-
args_json: z.string().describe('参数 JSON 字符串'),
|
|
129
|
-
}),
|
|
130
|
-
execute: async (args) => {
|
|
131
|
-
const { server, tool, args_json } = args;
|
|
132
|
-
const clientInfo = this._clients.get(server);
|
|
133
|
-
if (!clientInfo) return { success: false, error: `MCP server '${server}' not found` };
|
|
134
|
-
|
|
135
|
-
let finalArgs = {};
|
|
136
|
-
if (args_json) {
|
|
137
|
-
try { finalArgs = JSON.parse(args_json); }
|
|
138
|
-
catch (e) { return { success: false, error: `args_json 格式错误: ${args_json}` }; }
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
if (!finalArgs || Object.keys(finalArgs).length === 0) {
|
|
142
|
-
const toolInfo = clientInfo.tools.find((t) => t.name === tool);
|
|
143
|
-
if (toolInfo) {
|
|
144
|
-
const schema = extractSchema(toolInfo.inputSchema);
|
|
145
|
-
const required = schema.required || [];
|
|
146
|
-
return { success: false, error: `参数为空!必须提供: ${required.join(', ')}`, hint: `正确格式: {"${required[0]}": "具体值"}` };
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
try {
|
|
151
|
-
const mcpTool = clientInfo.toolObjects?.[tool];
|
|
152
|
-
if (!mcpTool) return { success: false, error: `Tool '${tool}' not found on server '${server}'` };
|
|
153
|
-
|
|
154
|
-
const mcpToolName = `${server}:${tool}`;
|
|
155
|
-
framework.emit('tool:call', { name: mcpToolName, args: finalArgs, source: 'mcp' });
|
|
156
|
-
framework.emit('tool-call', { name: mcpToolName, args: finalArgs, source: 'mcp' });
|
|
157
|
-
|
|
158
|
-
const execResult = await mcpTool.execute(finalArgs);
|
|
159
|
-
|
|
160
|
-
framework.emit('tool:result', { name: mcpToolName, args: finalArgs, result: execResult, source: 'mcp' });
|
|
161
|
-
return { success: true, data: execResult };
|
|
162
|
-
} catch (err) {
|
|
163
|
-
log.error(` Tool '${tool}' failed:`, err.message);
|
|
164
|
-
framework.emit('tool:error', { name: `${server}:${tool}`, args: finalArgs, error: err.message, source: 'mcp' });
|
|
165
|
-
return { success: false, error: err.message };
|
|
166
|
-
}
|
|
167
|
-
},
|
|
168
|
-
});
|
|
169
|
-
|
|
170
56
|
framework.registerTool({
|
|
171
57
|
name: 'mcp_list_servers',
|
|
172
58
|
description: '列出已连接的 MCP 服务器及其工具',
|
|
@@ -235,9 +121,12 @@ class MCPExecutorPlugin extends Plugin {
|
|
|
235
121
|
if (!clientInfo) return { success: false, error: `MCP server '${server}' not found` };
|
|
236
122
|
try { clientInfo.client.close?.(); } catch (e) { /* ignore */ }
|
|
237
123
|
this._clients.delete(server);
|
|
238
|
-
|
|
239
|
-
|
|
124
|
+
// 注销该服务器的所有工具(this.tools 中的 key 就是 MCP 原始工具名)
|
|
125
|
+
for (const t of clientInfo.tools) {
|
|
126
|
+
if (this.tools[t.name]) delete this.tools[t.name];
|
|
240
127
|
}
|
|
128
|
+
// 同时注销对应的扩展(mcp:<server>)
|
|
129
|
+
this._unregisterExtension(server);
|
|
241
130
|
await this._saveMCPServerEnabled(server, false);
|
|
242
131
|
this._refreshAllAgentsMCPPrompt(this._framework);
|
|
243
132
|
return { success: true, data: `MCP 服务器 '${server}' 已关闭` };
|
|
@@ -343,15 +232,23 @@ class MCPExecutorPlugin extends Plugin {
|
|
|
343
232
|
toolObjects[toolName] = tool;
|
|
344
233
|
toolsInfo.push({ name: toolName, description: tool.description || '', inputSchema: tool.inputSchema || {} });
|
|
345
234
|
|
|
346
|
-
|
|
347
|
-
this.tools[
|
|
348
|
-
name:
|
|
349
|
-
description:
|
|
235
|
+
// 直接调用底层 MCP 工具(不再走 mcp_call 中转)
|
|
236
|
+
this.tools[toolName] = {
|
|
237
|
+
name: toolName,
|
|
238
|
+
description: tool.description || '',
|
|
350
239
|
inputSchema: tool.inputSchema || {},
|
|
351
240
|
execute: async (args) => {
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
241
|
+
try {
|
|
242
|
+
const mcpToolName = `${name}:${toolName}`;
|
|
243
|
+
framework?.emit?.('tool:call', { name: mcpToolName, args: args || {}, source: 'mcp' });
|
|
244
|
+
const execResult = await tool.execute(args || {});
|
|
245
|
+
framework?.emit?.('tool:result', { name: mcpToolName, args: args || {}, result: execResult, source: 'mcp' });
|
|
246
|
+
return { success: true, data: execResult };
|
|
247
|
+
} catch (err) {
|
|
248
|
+
log.error(` Tool '${toolName}' failed:`, err.message);
|
|
249
|
+
framework?.emit?.('tool:error', { name: `${name}:${toolName}`, args: args || {}, error: err.message, source: 'mcp' });
|
|
250
|
+
return { success: false, error: err.message };
|
|
251
|
+
}
|
|
355
252
|
},
|
|
356
253
|
};
|
|
357
254
|
}
|
|
@@ -359,10 +256,89 @@ class MCPExecutorPlugin extends Plugin {
|
|
|
359
256
|
|
|
360
257
|
this._clients.set(name, { client, tools: toolsInfo, toolObjects, enabled: config.enabled !== false });
|
|
361
258
|
|
|
259
|
+
// 把整个 MCP 服务器注册为扩展(mcp:<servername>),让 ext_call / ext_skill 统一调度
|
|
260
|
+
this._registerAsExtension(name, toolsInfo, toolObjects);
|
|
261
|
+
|
|
262
|
+
// 触发 mcp 工具变化事件,通知 extension-executor 刷新
|
|
263
|
+
this._framework?.emit?.('mcp:tools-changed', { name, toolsCount: toolsInfo.length });
|
|
264
|
+
|
|
362
265
|
log.debug(` Added server '${name}' with ${toolsInfo.length} tools`);
|
|
363
266
|
return { success: true, name, toolsCount: toolsInfo.length };
|
|
364
267
|
}
|
|
365
268
|
|
|
269
|
+
/**
|
|
270
|
+
* 把 MCP 服务器注册为扩展插件(mcp:<servername>)
|
|
271
|
+
* 使其能通过 ext_call({plugin:'mcp:<server>', tool:'xxx', args:{...}}) 调用
|
|
272
|
+
* 通过 ext_skill({plugin:'mcp:<server>'}) 获取参数说明
|
|
273
|
+
*/
|
|
274
|
+
_registerAsExtension(serverName, toolsInfo, toolObjects) {
|
|
275
|
+
const extExecutor = this._framework?.pluginManager?.get('extension-executor');
|
|
276
|
+
if (!extExecutor || typeof extExecutor.registerTool !== 'function') return;
|
|
277
|
+
|
|
278
|
+
const extName = `mcp:${serverName}`;
|
|
279
|
+
// 优先从 server config 取 description,否则用 server name
|
|
280
|
+
const serverConfig = this._serverConfigs.get(serverName) || {};
|
|
281
|
+
const extDescription = serverConfig.description
|
|
282
|
+
? serverConfig.description
|
|
283
|
+
: `MCP 服务器 ${serverName}`;
|
|
284
|
+
const pluginInfo = {
|
|
285
|
+
name: extName,
|
|
286
|
+
description: extDescription,
|
|
287
|
+
version: '1.0.0',
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
for (const toolInfo of toolsInfo) {
|
|
291
|
+
const mcpTool = toolObjects[toolInfo.name];
|
|
292
|
+
if (!mcpTool) continue;
|
|
293
|
+
|
|
294
|
+
// 把 JSON schema 转换为 Zod schema(用于 AI SDK 的工具参数定义)
|
|
295
|
+
// 同时保留原始 JSON schema 作为 _rawSchema,让 ext_skill 能渲染出清晰的参数说明
|
|
296
|
+
const rawSchema = extractSchema(toolInfo.inputSchema);
|
|
297
|
+
let inputSchema = rawSchema;
|
|
298
|
+
try {
|
|
299
|
+
if (rawSchema && rawSchema.properties) {
|
|
300
|
+
const zodSchema = this._jsonSchemaToZod(rawSchema);
|
|
301
|
+
if (zodSchema) inputSchema = zodSchema;
|
|
302
|
+
}
|
|
303
|
+
} catch (e) { /* fallback: 保留原始 schema */ }
|
|
304
|
+
|
|
305
|
+
extExecutor.registerTool(extName, pluginInfo, {
|
|
306
|
+
name: toolInfo.name,
|
|
307
|
+
description: toolInfo.description || '',
|
|
308
|
+
inputSchema,
|
|
309
|
+
_rawSchema: rawSchema, // 原始 JSON schema,用于 ext_skill 渲染
|
|
310
|
+
execute: async (args, framework) => {
|
|
311
|
+
try {
|
|
312
|
+
const finalArgs = args || {};
|
|
313
|
+
const mcpToolName = `${serverName}:${toolInfo.name}`;
|
|
314
|
+
framework?.emit?.('tool:call', { name: mcpToolName, args: finalArgs, source: 'mcp' });
|
|
315
|
+
const result = await mcpTool.execute(finalArgs);
|
|
316
|
+
framework?.emit?.('tool:result', { name: mcpToolName, args: finalArgs, result, source: 'mcp' });
|
|
317
|
+
return { success: true, data: result };
|
|
318
|
+
} catch (err) {
|
|
319
|
+
log.error(` Tool '${toolInfo.name}' failed:`, err.message);
|
|
320
|
+
return { success: false, error: err.message };
|
|
321
|
+
}
|
|
322
|
+
},
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* 注销 MCP 服务器对应的扩展
|
|
329
|
+
*/
|
|
330
|
+
_unregisterExtension(serverName) {
|
|
331
|
+
const extExecutor = this._framework?.pluginManager?.get('extension-executor');
|
|
332
|
+
if (!extExecutor || typeof extExecutor._unregisterTool !== 'function') return;
|
|
333
|
+
|
|
334
|
+
const extName = `mcp:${serverName}`;
|
|
335
|
+
const clientInfo = this._clients.get(serverName);
|
|
336
|
+
const toolNames = clientInfo?.tools?.map((t) => t.name) || [];
|
|
337
|
+
for (const toolName of toolNames) {
|
|
338
|
+
extExecutor._unregisterTool(extName, toolName);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
366
342
|
async removeServer(name) {
|
|
367
343
|
const clientInfo = this._clients.get(name);
|
|
368
344
|
if (clientInfo) {
|
|
@@ -373,9 +349,13 @@ class MCPExecutorPlugin extends Plugin {
|
|
|
373
349
|
}
|
|
374
350
|
} catch (e) { /* ignore */ }
|
|
375
351
|
for (const toolName of Object.keys(this.tools)) {
|
|
376
|
-
if (toolName
|
|
352
|
+
if (this.tools[toolName]?.description?.includes?.(`[${name}]`)) {
|
|
353
|
+
delete this.tools[toolName];
|
|
354
|
+
}
|
|
377
355
|
}
|
|
378
356
|
this._clients.delete(name);
|
|
357
|
+
this._unregisterExtension(name);
|
|
358
|
+
this._framework?.emit?.('mcp:tools-changed', { name, removed: true });
|
|
379
359
|
}
|
|
380
360
|
}
|
|
381
361
|
|