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.
Files changed (59) hide show
  1. package/.claude/settings.local.json +4 -1
  2. package/.cli_default_systemPrompt.md +291 -0
  3. package/CLAUDE.md +3 -0
  4. package/README.md +20 -3
  5. package/docs/architecture.md +34 -2
  6. package/docs/extensions.md +199 -0
  7. package/docs/migration.md +100 -0
  8. package/docs/public-api.md +1180 -6
  9. package/docs/usage.md +122 -30
  10. package/package.json +1 -1
  11. package/plugins/core/audit/index.js +1 -1
  12. package/plugins/core/default/bootstrap.js +65 -19
  13. package/plugins/core/python-loader/index.js +43 -25
  14. package/plugins/core/skill-manager/PROMPT.md +6 -0
  15. package/plugins/core/skill-manager/index.js +538 -93
  16. package/plugins/core/sub-agent/PROMPT.md +10 -0
  17. package/plugins/core/sub-agent/index.js +36 -3
  18. package/plugins/core/think/index.js +1 -0
  19. package/plugins/core/workflow/index.js +106 -22
  20. package/plugins/executors/data-splitter/PROMPT.md +13 -0
  21. package/plugins/executors/data-splitter/index.js +5 -4
  22. package/plugins/executors/extension/extension-registry.js +145 -0
  23. package/plugins/executors/extension/index.js +405 -437
  24. package/plugins/executors/extension/prompt-builder.js +359 -0
  25. package/plugins/executors/extension/skill-helper.js +143 -0
  26. package/plugins/messaging/feishu/index.js +5 -3
  27. package/plugins/messaging/qq/index.js +6 -4
  28. package/plugins/messaging/telegram/index.js +6 -3
  29. package/plugins/messaging/weixin/index.js +5 -3
  30. package/plugins/tools/PROMPT.md +26 -0
  31. package/plugins/tools/index.js +6 -5
  32. package/skills/foliko/AGENTS.md +196 -43
  33. package/skills/foliko/SKILL.md +157 -28
  34. package/skills/mcp/SKILL.md +77 -118
  35. package/skills/plugins/SKILL.md +89 -3
  36. package/skills/python/SKILL.md +57 -39
  37. package/skills/skill-guide/SKILL.md +42 -34
  38. package/skills/workflows/SKILL.md +224 -9
  39. package/skills/workflows/workflow-troubleshooting/SKILL.md +221 -281
  40. package/src/agent/chat.js +48 -27
  41. package/src/agent/main.js +34 -13
  42. package/src/agent/prompt-registry.js +133 -87
  43. package/src/agent/prompts/PROMPT.md +3 -0
  44. package/src/agent/sub.js +1 -1
  45. package/src/cli/ui/chat-ui-old.js +5 -2
  46. package/src/cli/ui/chat-ui.js +5 -2
  47. package/src/common/constants.js +12 -0
  48. package/src/common/error-capture.js +91 -0
  49. package/src/common/logger.js +2 -2
  50. package/src/context/compressor.js +6 -2
  51. package/src/executors/mcp-executor.js +105 -125
  52. package/src/framework/framework.js +632 -6
  53. package/src/index.js +4 -0
  54. package/src/plugin/base.js +913 -10
  55. package/src/plugin/manager.js +29 -8
  56. package/src/tool/schema.js +32 -9
  57. package/tests/core/plugin-prompts.test.js +13 -13
  58. package/tests/core/prompt-registry.test.js +59 -51
  59. 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 };
@@ -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, `vb-agent-${date}.log`);
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 = 'vb-agent',
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 || 20;
188
- this._compressionMessageThreshold = config.compressionMessageThreshold || 200;
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, buildMCPServersDescription, bindMcpParamsDesc } = require('./mcp-desc');
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
- for (const key of Object.keys(this.tools)) {
239
- if (key.startsWith(`${server}_`)) delete this.tools[key];
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
- const registeredName = `${name}_${toolName}`;
347
- this.tools[registeredName] = {
348
- name: registeredName,
349
- description: `[${name}] ${tool.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
- return await this._framework.executeTool('mcp_call', {
353
- server: name, tool: toolName, args_json: JSON.stringify(args || {}),
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.startsWith(name + '_')) delete this.tools[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