foliko 2.0.1 → 2.0.3

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/README.md +6 -6
  2. package/docs/public-api.md +91 -0
  3. package/docs/system-prompt.md +219 -0
  4. package/docs/usage.md +6 -6
  5. package/package.json +1 -1
  6. package/plugins/ambient/ExplorerLoop.js +1 -0
  7. package/plugins/ambient/index.js +5 -0
  8. package/plugins/core/coordinator/index.js +11 -6
  9. package/plugins/core/default/bootstrap.js +21 -3
  10. package/plugins/core/default/config.js +12 -3
  11. package/plugins/core/python-loader/index.js +3 -3
  12. package/plugins/core/scheduler/index.js +26 -2
  13. package/plugins/core/skill-manager/index.js +198 -151
  14. package/plugins/core/sub-agent/index.js +34 -15
  15. package/plugins/core/think/index.js +1 -0
  16. package/plugins/core/workflow/index.js +5 -4
  17. package/plugins/executors/data-splitter/index.js +14 -1
  18. package/plugins/executors/extension/index.js +51 -37
  19. package/plugins/executors/python/index.js +3 -3
  20. package/plugins/executors/shell/index.js +6 -4
  21. package/plugins/io/web/index.js +2 -1
  22. package/plugins/memory/index.js +29 -74
  23. package/plugins/messaging/email/handlers.js +1 -19
  24. package/plugins/messaging/email/monitor.js +2 -17
  25. package/plugins/messaging/email/reply.js +2 -1
  26. package/plugins/messaging/email/utils.js +20 -1
  27. package/plugins/messaging/feishu/index.js +1 -1
  28. package/plugins/messaging/qq/index.js +1 -1
  29. package/plugins/messaging/telegram/index.js +5 -1
  30. package/plugins/messaging/weixin/index.js +1 -1
  31. package/plugins/plugin-manager/index.js +1 -33
  32. package/plugins/tools/index.js +14 -1
  33. package/skills/plugins/SKILL.md +316 -0
  34. package/skills/skill-guide/SKILL.md +5 -5
  35. package/skills/{subagent-guide → subagents}/SKILL.md +1 -1
  36. package/skills/{workflow-guide → workflows}/SKILL.md +3 -3
  37. package/skills/{workflow-troubleshooting → workflows/workflow-troubleshooting}/DEBUGGING.md +197 -197
  38. package/skills/{workflow-troubleshooting → workflows/workflow-troubleshooting}/SKILL.md +391 -391
  39. package/src/agent/base.js +5 -20
  40. package/src/agent/index.js +20 -8
  41. package/src/agent/main.js +100 -23
  42. package/src/agent/prompt-registry.js +296 -0
  43. package/src/agent/sub-config.js +1 -78
  44. package/src/agent/sub.js +19 -24
  45. package/src/cli/commands/plugin.js +1 -60
  46. package/src/cli/ui/chat-ui-old.js +1 -1
  47. package/src/cli/ui/chat-ui.js +1 -1
  48. package/src/cli/ui/components/agent-mention-provider.js +1 -1
  49. package/src/common/constants.js +42 -0
  50. package/src/common/id.js +13 -0
  51. package/src/common/queue.js +2 -2
  52. package/src/context/compressor.js +17 -9
  53. package/src/framework/framework.js +185 -0
  54. package/src/framework/index.js +1 -2
  55. package/src/framework/lifecycle.js +102 -1
  56. package/src/framework/loader.js +1 -55
  57. package/src/index.js +11 -2
  58. package/src/plugin/base.js +69 -55
  59. package/src/plugin/loader.js +1 -57
  60. package/src/plugin/manager.js +13 -4
  61. package/src/session/entry.js +1 -11
  62. package/src/storage/manager.js +1 -12
  63. package/src/tool/executor.js +2 -1
  64. package/src/tool/router.js +5 -5
  65. package/src/utils/data-splitter.js +2 -1
  66. package/src/utils/index.js +150 -0
  67. package/src/utils/message-validator.js +21 -17
  68. package/src/utils/plugin-helpers.js +19 -5
  69. package/subagent.md +2 -2
  70. package/tests/core/plugin-prompts.test.js +219 -0
  71. package/tests/core/prompt-registry.test.js +209 -0
  72. package/src/cli/utils/plugin-config.js +0 -50
  73. package/src/config/plugin-config.js +0 -50
  74. /package/skills/{ambient-agent → ambient}/SKILL.md +0 -0
  75. /package/skills/{foliko-dev → foliko}/AGENTS.md +0 -0
  76. /package/skills/{foliko-dev → foliko}/SKILL.md +0 -0
  77. /package/skills/{mcp-usage → mcp}/SKILL.md +0 -0
  78. /package/skills/{plugin-guide → plugins-guide}/SKILL.md +0 -0
  79. /package/skills/{python-plugin-dev → python}/SKILL.md +0 -0
package/src/agent/sub.js CHANGED
@@ -27,10 +27,10 @@ class SubAgent extends BaseAgent {
27
27
  this.baseURL = config.baseURL;
28
28
  this.providerOptions = config.providerOptions || {};
29
29
 
30
- this.defaulTools = ['ext_skill', 'loadSkill', 'ext_call', 'subagent_call', 'mcp_call', 'mcp_tool_schema'];
30
+ this.defaulTools = ['ext_skill', 'skill_load', 'ext_call', 'subagent_call', 'mcp_call', 'mcp_tool_schema'];
31
31
  this.bindTools = {
32
32
  read: 'read_file', write: 'write_file', edit: 'modify_file',
33
- glob: 'read_directory', grep: 'search_file', bash: 'bash',
33
+ glob: 'read_directory', grep: 'search_file', bash: 'shell_exec',
34
34
  };
35
35
 
36
36
  this._customSystemPrompt = config.systemPrompt || null;
@@ -39,24 +39,13 @@ class SubAgent extends BaseAgent {
39
39
  this.maxRetries = config.maxRetries ?? 2;
40
40
  this.retryDelay = config.retryDelay ?? 5000;
41
41
  this.disableTools = config.disableTools ?? false;
42
- this._promptParts = [];
42
+ this.hidden = config.hidden || false; // 隐藏子Agent,不在指令系统中显示
43
43
  this._isSubagent = true;
44
44
 
45
45
  this.providerOptions.deepseek = this.providerOptions.deepseek || {};
46
46
  this.providerOptions.deepseek.thinking = { type: 'disabled' };
47
47
 
48
- if (this.framework) {
49
- this.framework.once('framework:ready', () => {
50
- const extExecutor = this.framework?.pluginManager?.get('extension-executor');
51
- extExecutor?._refreshAllAgentsExtPrompt?.(this.framework);
52
- });
53
- }
54
- }
55
-
56
- registerPromptPart(name, priority = 100, provider) {
57
- this._promptParts.push({ name, priority, provider });
58
- this._promptParts.sort((a, b) => a.priority - b.priority);
59
- return this;
48
+ // 扩展工具 prompt 由 _syncPromptParts 同步到 promptRegistry,无需显式刷新
60
49
  }
61
50
 
62
51
  _validateMessagesPairing(messages) { return validateAll(messages); }
@@ -74,7 +63,15 @@ class SubAgent extends BaseAgent {
74
63
 
75
64
  _getAIProvider() {
76
65
  const { createAI } = require('../llm/provider');
77
- return createAI({ provider: this.provider, model: this.model, apiKey: this.apiKey, baseURL: this.baseURL });
66
+ // 优先从 framework ai 插件拿最新配置(避免构造时拿到 stale apiKey
67
+ const aiPlugin = this.framework?.pluginManager?.get('ai');
68
+ const aiConfig = aiPlugin ? aiPlugin.getConfig() : {};
69
+ return createAI({
70
+ provider: aiConfig.provider || this.provider,
71
+ model: aiConfig.model || this.model,
72
+ apiKey: aiConfig.apiKey || this.apiKey,
73
+ baseURL: aiConfig.baseURL || this.baseURL,
74
+ });
78
75
  }
79
76
 
80
77
  _buildAITools() {
@@ -111,15 +108,13 @@ class SubAgent extends BaseAgent {
111
108
  const mainPrompt = mainAgent.getOriginalPrompt();
112
109
  if (mainPrompt) { lines.push('## 主Agent的系统提示词'); lines.push(mainPrompt); lines.push(''); }
113
110
  }
114
- const subagentManager = this.framework?.pluginManager?.get('subagent-manager');
115
- if (subagentManager?._buildDescription) lines.push(subagentManager._buildDescription());
116
111
  lines.push('');
117
- const extExecutor = this.framework?.pluginManager?.get('extension-executor');
118
- const extDesc = extExecutor?._buildExtensionsDescription?.();
119
- if (extDesc) { lines.push(extDesc); lines.push(''); }
120
- for (const part of this._promptParts) {
112
+ // 渲染通过 promptRegistry 注册的 prompt part(按 slot/order 排序)
113
+ // 扩展工具描述、子Agent 描述等都由 _syncPromptParts 同步到 promptRegistry,
114
+ // 无需再单独硬编码调用 _buildExtensionsDescription / _buildDescription
115
+ for (const part of (this.promptRegistry?.list() || [])) {
121
116
  try {
122
- const content = typeof part.provider === 'function' ? part.provider() : '';
117
+ const content = part.get();
123
118
  if (content?.trim()) lines.push('', content.trim());
124
119
  } catch (err) {
125
120
  logger.warn(`[SubAgent:${this.name}] Prompt part '${part.name}' failed:`, err.message);
@@ -127,7 +122,7 @@ class SubAgent extends BaseAgent {
127
122
  }
128
123
  lines.push('', '## 核心职责', `- **专注本职**:只完成与 ${this.role} 相关的任务`, '- **不越界**:不要代替其他子Agent完成任务', '- **返回结果**:返回简洁明确的操作结果', '');
129
124
  lines.push('当你被调用时,你应该:', '1. 仔细理解任务要求', '2. 如果任务不在职责范围内,返回"此任务不在我的职责范围内"', '3. 使用你的工具集完成任务', '4. 返回完整结果');
130
- lines.push('', '## 禁止事项', '- 处理本职外的任务', '- 代替其他子Agent执行操作', '- 在回复中透露内部实现细节', '- 不先调用 ext_skill/loadSkill 就直接使用 ext_call');
125
+ lines.push('', '## 禁止事项', '- 处理本职外的任务', '- 代替其他子Agent执行操作', '- 在回复中透露内部实现细节', '- 不先调用 ext_skill/skill_load 就直接使用 ext_call');
131
126
  return lines.join('\n');
132
127
  }
133
128
 
@@ -5,70 +5,11 @@
5
5
 
6
6
  const fs = require('fs');
7
7
  const path = require('path');
8
- const { execSync } = require('child_process');
9
8
  const https = require('https');
10
9
  const http = require('http');
11
- const { DEFAULT_REPO, shouldIgnore } = require('../utils/plugin-config');
10
+ const { DEFAULT_REPO, shouldIgnore, copyDirRecursive, parseGitUrl, gitCommand } = require('../../utils');
12
11
  const { AGENT_DIR_NAME } = require('../utils/config');
13
12
 
14
- /**
15
- * 递归复制目录(带过滤)
16
- */
17
- function copyDirRecursive(src, dest, ignorePatterns = []) {
18
- if (!fs.existsSync(src)) return;
19
-
20
- fs.mkdirSync(dest, { recursive: true });
21
-
22
- const entries = fs.readdirSync(src, { withFileTypes: true });
23
-
24
- for (const entry of entries) {
25
- const srcPath = path.join(src, entry.name);
26
- const destPath = path.join(dest, entry.name);
27
-
28
- // 检查是否忽略
29
- if (shouldIgnore(entry.name)) {
30
- console.log(` Ignoring: ${entry.name}`);
31
- continue;
32
- }
33
-
34
- if (entry.isDirectory()) {
35
- copyDirRecursive(srcPath, destPath, ignorePatterns);
36
- } else {
37
- fs.copyFileSync(srcPath, destPath);
38
- }
39
- }
40
- }
41
-
42
- /**
43
- * 解析 Git URL 获取信息
44
- */
45
- function parseGitUrl(url) {
46
- // 支持多种格式
47
- const patterns = [
48
- /^https:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?$/,
49
- /^git@github\.com:([^/]+)\/([^/]+?)(?:\.git)?$/,
50
- ];
51
-
52
- for (const pattern of patterns) {
53
- const match = url.match(pattern);
54
- if (match) {
55
- return { owner: match[1], repo: match[2] };
56
- }
57
- }
58
- return null;
59
- }
60
-
61
- /**
62
- * 执行 Git 命令
63
- */
64
- function gitCommand(args, cwd) {
65
- try {
66
- return execSync(`git ${args}`, { cwd, encoding: 'utf-8', stdio: 'pipe' });
67
- } catch (err) {
68
- return err.stdout || err.stderr || '';
69
- }
70
- }
71
-
72
13
  /**
73
14
  * 下载文件
74
15
  */
@@ -168,7 +168,7 @@ class ChatUIOld {
168
168
 
169
169
  async _reloadSkills() {
170
170
  try {
171
- const result = await this.agent.framework.executeTool('reloadSkills', {});
171
+ const result = await this.agent.framework.executeTool('skill_reload', {});
172
172
  if (result.success !== false) {
173
173
  console.log(colored('[提示] 技能已重载', CYAN));
174
174
  } else {
@@ -321,7 +321,7 @@ class ChatUI {
321
321
 
322
322
  if (cmd === '/reload') {
323
323
  try {
324
- const result = await this.agent.framework.executeTool('reloadSkills', {});
324
+ const result = await this.agent.framework.executeTool('skill_reload', {});
325
325
  if (result.success !== false) {
326
326
  this.create_message(`${colored('[提示]', CYAN)} 技能已重载\n`,chalk.dim('● '))
327
327
  } else {
@@ -78,7 +78,7 @@ class AgentMentionProvider {
78
78
 
79
79
  const mainAgent = this.framework._mainAgent;
80
80
  return this.framework._agents
81
- .filter(agent => agent !== mainAgent && agent.name)
81
+ .filter(agent => agent !== mainAgent && agent.name && !agent.hidden)
82
82
  .map(agent => ({
83
83
  name: agent.name.replace(/^subagent_/, '').replace(/^session_/, ''),
84
84
  description: agent._role || agent.description || agent.name,
@@ -79,6 +79,7 @@ const DEFAULT_RETRY_DELAY_MS = 2000;
79
79
  // System Prompt 优先级
80
80
  // ============================================================================
81
81
 
82
+ // 旧的数字优先级(保留以兼容旧代码)
82
83
  const PROMPT_PRIORITY = {
83
84
  DATETIME: 50,
84
85
  ORIGINAL_PROMPT: 80,
@@ -93,6 +94,45 @@ const PROMPT_PRIORITY = {
93
94
  TOOL_CORE_RULES: 150,
94
95
  };
95
96
 
97
+ /**
98
+ * 新的语义槽位(推荐使用)
99
+ * order 越小越靠前;同 slot 内可用 PromptPart.order 进一步排序
100
+ */
101
+ const PROMPT_SLOT = {
102
+ /** 角色身份:用户/系统设定的 systemPrompt */
103
+ IDENTITY: { order: 10, label: '角色身份' },
104
+ /** 运行环境:时间、cwd、平台等 */
105
+ ENVIRONMENT: { order: 20, label: '运行环境' },
106
+ /** 用户指令:sharedPrompt、运行时 metadata */
107
+ USER: { order: 30, label: '用户指令' },
108
+ /** 可用能力:tools、skills、subagents、extensions */
109
+ CAPABILITY: { order: 40, label: '可用能力' },
110
+ /** 行为规则:tool-core-rules、data-splitter、memory-context */
111
+ BEHAVIOR: { order: 50, label: '行为规则' },
112
+ /** 上下文摘要:自动压缩、消息历史 */
113
+ CONTEXT: { order: 60, label: '上下文摘要' },
114
+ /** 自定义兜底 */
115
+ CUSTOM: { order: 999, label: '自定义' },
116
+ };
117
+
118
+ /**
119
+ * 旧 PROMPT_PRIORITY 名字 → 新 PROMPT_SLOT 的映射
120
+ * 仅用于过渡期,旧代码迁移时按此映射
121
+ */
122
+ const PROMPT_PRIORITY_TO_SLOT = {
123
+ DATETIME: 'ENVIRONMENT',
124
+ ORIGINAL_PROMPT: 'IDENTITY',
125
+ SHARED_PROMPT: 'USER',
126
+ METADATA: 'USER',
127
+ CAPABILITIES: 'CAPABILITY',
128
+ TOOLS: 'CAPABILITY',
129
+ SKILLS: 'CAPABILITY',
130
+ SUB_AGENTS: 'CAPABILITY',
131
+ MCP_TOOLS: 'CAPABILITY',
132
+ EXTENSION_TOOLS: 'CAPABILITY',
133
+ TOOL_CORE_RULES: 'BEHAVIOR',
134
+ };
135
+
96
136
  // ============================================================================
97
137
  // 熔断器
98
138
  // ============================================================================
@@ -182,6 +222,8 @@ module.exports = {
182
222
 
183
223
  // Priority
184
224
  PROMPT_PRIORITY,
225
+ PROMPT_SLOT,
226
+ PROMPT_PRIORITY_TO_SLOT,
185
227
 
186
228
  // Circuit Breaker
187
229
  DEFAULT_CIRCUIT_FAILURE_THRESHOLD,
package/src/common/id.js CHANGED
@@ -135,4 +135,17 @@ module.exports = {
135
135
  const random = crypto.randomBytes(8).toString('base64url');
136
136
  return `id_${Date.now()}_${random}`;
137
137
  },
138
+ /**
139
+ * 生成唯一的 entry ID (8位 UUID 片段,避免冲突)
140
+ * @param {Set|string[]} existingIds - 已存在的 ID 集合
141
+ * @returns {string}
142
+ */
143
+ generateEntryId: (existingIds) => {
144
+ const byId = existingIds instanceof Set ? existingIds : new Set(existingIds);
145
+ for (let i = 0; i < 100; i++) {
146
+ const id = uuid().slice(0, 8);
147
+ if (!byId.has(id)) return id;
148
+ }
149
+ return uuid();
150
+ },
138
151
  };
@@ -122,12 +122,12 @@ class ChatQueueManager extends EventEmitter {
122
122
  await this.sleep(calculateDelay(attempt, { baseDelay: this.retryDelay }));
123
123
  continue;
124
124
  }
125
- throw lastError;
125
+ // 不 throw — 直接跳出循环,由后面的 friendlyError 统一处理
126
+ break;
126
127
  }
127
128
 
128
129
  return result;
129
130
  } catch (error) {
130
-
131
131
  lastError = error;
132
132
  if (attempt < this.retryAttempts && this.isRetryableError(error)) {
133
133
  await this.sleep(calculateDelay(attempt, { baseDelay: this.retryDelay }));
@@ -326,15 +326,23 @@ class ContextCompressor {
326
326
  let removedItems = 0;
327
327
  let removedMsgs = 0;
328
328
  for (const msg of messages) {
329
- if (msg.role !== 'tool' || !Array.isArray(msg.content)) continue;
330
- const originalLength = msg.content.length;
331
- msg.content = msg.content.filter((item) => {
332
- if (item && (item.type === 'tool-result' || item.type === 'tool_result') && item.toolCallId && !validToolCallIds.has(item.toolCallId)) {
333
- removedItems++; return false;
334
- }
335
- return true;
336
- });
337
- if (msg.content.length === 0 && originalLength > 0) msg._orphaned = true;
329
+ if (msg.role !== 'tool') continue;
330
+
331
+ if (Array.isArray(msg.content)) {
332
+ // AI SDK 数组格式:按 toolCallId 过滤
333
+ const originalLength = msg.content.length;
334
+ msg.content = msg.content.filter((item) => {
335
+ if (item && (item.type === 'tool-result' || item.type === 'tool_result') && item.toolCallId && !validToolCallIds.has(item.toolCallId)) {
336
+ removedItems++; return false;
337
+ }
338
+ return true;
339
+ });
340
+ if (msg.content.length === 0 && originalLength > 0) msg._orphaned = true;
341
+ } else if (msg.tool_call_id && !validToolCallIds.has(msg.tool_call_id)) {
342
+ // OpenAI 兼容格式:整条消息是 tool result,无对应 tool_calls 时标记为 orphaned
343
+ msg._orphaned = true;
344
+ removedMsgs++;
345
+ }
338
346
  }
339
347
 
340
348
  for (let i = messages.length - 1; i >= 0; i--) {
@@ -12,10 +12,13 @@ const { AsyncLocalStorage } = require('async_hooks');
12
12
  const { EventEmitter } = require('../common/events');
13
13
  const { PluginManager } = require('../plugin/manager');
14
14
  const { ToolRegistry } = require('../tool/registry');
15
+ const { PromptRegistry } = require('../agent/prompt-registry');
15
16
  const { ContextManager } = require('../context/manager');
16
17
  const { createFrameworkLogger } = require('../common/logger');
17
18
  const { AGENT_DIR, HOME_AGENT_DIR_NAME } = require('../common/constants');
18
19
  const { SessionTTL } = require('../session/ttl');
20
+ const { Plugin } = require('../plugin/base');
21
+ const { z } = require('zod');
19
22
 
20
23
  // Add framework node_modules to search path
21
24
  const frameworkNodeModules = path.join(__dirname, '..', '..', 'node_modules');
@@ -28,6 +31,53 @@ const asyncLocalStorage = new AsyncLocalStorage();
28
31
  const requestStorage = new AsyncLocalStorage();
29
32
  const sessionStorage = new AsyncLocalStorage();
30
33
 
34
+ /**
35
+ * 将各种形式的插件定义转换为 Plugin 实例
36
+ * 支持:
37
+ * - Plugin 实例:原样返回
38
+ * - Plugin 子类(class extends Plugin):实例化
39
+ * - function(foliko) 风格:包装为 install
40
+ * - 普通对象 { name, install, start, ... }:拷贝字段并绑定生命周期方法
41
+ * @param {Plugin|Function|Object} pluginDef
42
+ * @returns {Plugin}
43
+ */
44
+ function createPluginInstance(pluginDef) {
45
+ if (pluginDef instanceof Plugin) return pluginDef;
46
+
47
+ if (typeof pluginDef === 'function') {
48
+ if (pluginDef.prototype instanceof Plugin) {
49
+ return new pluginDef();
50
+ }
51
+ // function(foliko) 风格
52
+ const instance = new Plugin();
53
+ instance.name = pluginDef.name || 'function-plugin';
54
+ const fn = pluginDef;
55
+ instance.install = function (framework) {
56
+ this._framework = framework;
57
+ return fn(framework);
58
+ };
59
+ return instance;
60
+ }
61
+
62
+ if (!pluginDef || typeof pluginDef !== 'object') {
63
+ throw new Error('Plugin definition must be a Plugin instance, subclass, function, or plain object');
64
+ }
65
+
66
+ const instance = new Plugin();
67
+ // 显式拷贝已知字段,避免复制任意属性
68
+ const fields = ['name', 'version', 'description', 'priority', 'enabled', 'system', 'tools', 'config', 'agents'];
69
+ for (const field of fields) {
70
+ if (pluginDef[field] !== undefined) instance[field] = pluginDef[field];
71
+ }
72
+ // 绑定生命周期方法(覆盖原型默认值)
73
+ for (const method of ['install', 'start', 'reload', 'uninstall']) {
74
+ if (typeof pluginDef[method] === 'function') {
75
+ instance[method] = pluginDef[method];
76
+ }
77
+ }
78
+ return instance;
79
+ }
80
+
31
81
  class Framework extends EventEmitter {
32
82
  constructor(config = {}) {
33
83
  super();
@@ -41,6 +91,7 @@ class Framework extends EventEmitter {
41
91
  // Subsystems
42
92
  this.toolRegistry = new ToolRegistry();
43
93
  this.pluginManager = new PluginManager(this);
94
+ this.promptRegistry = new PromptRegistry();
44
95
 
45
96
  // Agent management
46
97
  this._agents = [];
@@ -75,6 +126,7 @@ class Framework extends EventEmitter {
75
126
  // Event forwarding
76
127
  this._setupToolEventForwarding();
77
128
  this._setupAgentEventForwarding();
129
+ this._setupPromptRegistryAutoRefresh();
78
130
  this._registerBuiltinTools();
79
131
  }
80
132
 
@@ -107,6 +159,24 @@ class Framework extends EventEmitter {
107
159
  });
108
160
  }
109
161
 
162
+ /**
163
+ * promptRegistry 变更时自动失效所有 Agent 的提示词缓存
164
+ * 插件重载、skill 重载、扩展工具变更等都会触发刷新
165
+ * 系统提示词在下次 chat 时自动重建(懒刷新,不浪费性能)
166
+ */
167
+ _setupPromptRegistryAutoRefresh() {
168
+ // 仅标记脏缓存(不调 invalidateAll,避免与 promptRegistry 事件循环)
169
+ const invalidate = () => {
170
+ for (const agent of this._agents) {
171
+ agent._systemPromptDirty = true;
172
+ }
173
+ };
174
+ const REFRESH_EVENTS = ['register', 'unregister', 'clear-owner'];
175
+ for (const event of REFRESH_EVENTS) {
176
+ this.promptRegistry.on(event, invalidate);
177
+ }
178
+ }
179
+
110
180
  registerPlugin(plugin) { this.pluginManager.register(plugin); return this; }
111
181
  registerProvider(name, config) { const { registerProvider } = require('../llm/provider'); registerProvider(name, config); return this; }
112
182
  getProviderRegistry() { const { getProviderRegistry } = require('../llm/provider'); return getProviderRegistry(); }
@@ -143,6 +213,121 @@ class Framework extends EventEmitter {
143
213
  }
144
214
  getTools() { return this.toolRegistry.getAll(); }
145
215
 
216
+ // tools 对象别名
217
+ get tools() {
218
+ const self = this;
219
+ return {
220
+ register: (toolDef) => self.registerTool(toolDef),
221
+ tool: (toolDef) => self.tool(toolDef),
222
+ execute: (name, args) => self.executeTool(name, args),
223
+ list: () => self.toolRegistry.getNames(),
224
+ get: (name) => self.toolRegistry.get(name),
225
+ has: (name) => self.toolRegistry.has(name),
226
+ };
227
+ }
228
+
229
+ // Extension 工具注册
230
+ registerExtensionTools(pluginName, tools) {
231
+ const extExecutor = this.pluginManager.get('extension-executor');
232
+ if (!extExecutor) {
233
+ this.logger.warn(`[Framework] extension-executor not found, cannot register extension tools for "${pluginName}"`);
234
+ return this;
235
+ }
236
+ for (const [toolName, toolDef] of Object.entries(tools)) {
237
+ if (!toolDef || typeof toolDef !== 'object') continue;
238
+ extExecutor.registerTool(pluginName, {
239
+ name: pluginName,
240
+ description: toolDef.description || '',
241
+ version: toolDef.version || '1.0.0',
242
+ }, {
243
+ name: toolName,
244
+ description: toolDef.description || '',
245
+ inputSchema: toolDef.inputSchema,
246
+ execute: toolDef.execute,
247
+ });
248
+ }
249
+ return this;
250
+ }
251
+
252
+ // 常用类型和工具(方便 function(foliko) 插件使用)
253
+ get Plugin() { return Plugin; }
254
+ get z() { return z; }
255
+
256
+ // prompts 对象别名(系统提示词注册表)
257
+ get prompts() {
258
+ return this.promptRegistry;
259
+ }
260
+
261
+ // plugins 对象别名
262
+ get plugins() {
263
+ const self = this;
264
+ return {
265
+ get: (name) => self.pluginManager.get(name) || null,
266
+ list: () => self.pluginManager.getAll().map(p => p.instance?.name || p.name),
267
+ has: (name) => self.pluginManager.has(name),
268
+ /**
269
+ * 从插件定义创建 Plugin 实例
270
+ * 支持 Plugin 实例 / class / function(foliko) / 普通对象
271
+ */
272
+ create: (pluginDef) => createPluginInstance(pluginDef),
273
+ /**
274
+ * 注册新插件(默认会自动安装 + 启动)
275
+ * @param {Plugin|Function|Object} pluginDef 插件定义
276
+ * @param {Object} [options] 传给 PluginManager
277
+ * @param {boolean} [options.autoLoad=true] 是否自动调用 install + start
278
+ * @returns {Promise<Plugin>}
279
+ */
280
+ register: async (pluginDef, options = {}) => {
281
+ const autoLoad = options.autoLoad !== false;
282
+ const plugin = createPluginInstance(pluginDef);
283
+ self.pluginManager.register(plugin, options);
284
+ if (autoLoad) {
285
+ await self.pluginManager.load(plugin, options);
286
+ }
287
+ return plugin;
288
+ },
289
+ };
290
+ }
291
+
292
+ // 快捷获取插件实例
293
+ plugin(name) {
294
+ return this.pluginManager.get(name) || null;
295
+ }
296
+
297
+ // Extension 对象别名
298
+ get extension() {
299
+ const self = this;
300
+ return {
301
+ tool: (pluginName, tools) => self.registerExtensionTools(pluginName, tools),
302
+ execute: (plugin, tool, args) => self.executeExtension(plugin, tool, args),
303
+ list: () => {
304
+ const extExecutor = self.pluginManager.get('extension-executor');
305
+ if (!extExecutor) return [];
306
+ return extExecutor.getExtensions?.() || [];
307
+ },
308
+ listPlugins: () => {
309
+ const extExecutor = self.pluginManager.get('extension-executor');
310
+ if (!extExecutor) return [];
311
+ return extExecutor.getExtensionNames?.() || [];
312
+ },
313
+ listTools: (pluginName) => {
314
+ const extExecutor = self.pluginManager.get('extension-executor');
315
+ if (!extExecutor) return [];
316
+ return extExecutor.getExtensionTools?.(pluginName) || [];
317
+ },
318
+ getTool: (pluginName, toolName) => {
319
+ const extExecutor = self.pluginManager.get('extension-executor');
320
+ if (!extExecutor) return null;
321
+ return extExecutor.getExtensionTool?.(pluginName, toolName) || null;
322
+ },
323
+ };
324
+ }
325
+
326
+ // 执行 extension 工具
327
+ async executeExtension(plugin, tool, args = {}) {
328
+ return this.executeTool('ext_call', { plugin, tool, args });
329
+ }
330
+
146
331
  getMainAgent() { return this._mainAgent; }
147
332
  getSystemPrompt() { return this._mainAgent?._buildSystemPrompt?.() || ''; }
148
333
  getPluginInstance(name) { return this.pluginManager.get(name) || null; }
@@ -3,7 +3,7 @@
3
3
  const { Framework } = require('./framework');
4
4
  const { bootstrap, ready, destroy, setCwd, rescanProject } = require('./lifecycle');
5
5
  const { FrameworkRegistry } = require('./registry');
6
- const { loadAgentConfig, loadDefaultPlugins } = require('./loader');
6
+ const { loadDefaultPlugins } = require('./loader');
7
7
 
8
8
  module.exports = {
9
9
  Framework,
@@ -13,6 +13,5 @@ module.exports = {
13
13
  destroy,
14
14
  setCwd,
15
15
  rescanProject,
16
- loadAgentConfig,
17
16
  loadDefaultPlugins,
18
17
  };
@@ -114,6 +114,15 @@ async function setCwd(framework, newCwd, options = {}) {
114
114
 
115
115
  framework.pluginManager._setStateFile(resolved);
116
116
 
117
+ // 加载新目录配置,更新 skillsDirs 确保 skill-manager 能扫描到新项目技能
118
+ const { loadAgentConfig } = require('../../plugins/core/default');
119
+ const agentConfig = loadAgentConfig(framework, framework._agentDir || '.foliko');
120
+ const skillManager = framework.pluginManager.get('skill-manager');
121
+ if (skillManager && Array.isArray(agentConfig.skillsDirs)) {
122
+ skillManager.setSearchDirs(agentConfig.skillsDirs);
123
+ } else {
124
+ }
125
+
117
126
  const count = framework._sessionContexts.size;
118
127
  for (const sid of Array.from(framework._sessionContexts.keys())) {
119
128
  framework.destroySessionContext(sid);
@@ -200,4 +209,96 @@ async function rescanProject(framework, options = {}) {
200
209
  return result;
201
210
  }
202
211
 
203
- module.exports = { bootstrap, ready, destroy, setCwd, rescanProject };
212
+ /**
213
+ * Reload all - 完全重新加载所有插件和 skill
214
+ *
215
+ * 与 rescanProject 的区别:rescanProject 保留 SYSTEM_PLUGINS(ext-exec、skill-manager)
216
+ * 的旧实例,可能残留旧 state。reloadAll 显式按顺序:
217
+ * 1. 重载 ext-exec(先清空 _extensions)
218
+ * 2. 卸载非系统插件
219
+ * 3. 重载 defaults plugin
220
+ * 4. bootstrapDefaults(加载所有 default 插件,包括新的 ext-exec 和 skill-manager)
221
+ * 5. 重载 skill-manager(最后一步,让新 skill 的 commands 注册到新 ext-exec)
222
+ *
223
+ * 推荐用法:切换 WORK_DIR 后调用
224
+ * await foliko.setCwd(fw, newCwd);
225
+ * await foliko.reloadAll(fw);
226
+ *
227
+ * @param {Framework} framework
228
+ * @param {Object} options
229
+ * @param {boolean} [options.reloadExtExec=true] 是否重载 ext-exec(默认 true,彻底清空旧 state)
230
+ * @param {boolean} [options.reloadSkillManager=true] 是否重载 skill-manager(默认 true,扫描新 skill)
231
+ * @param {Set<string>} [options.keepLoaded] 额外保留的插件名集合
232
+ */
233
+ async function reloadAll(framework, options = {}) {
234
+ const { bootstrapDefaults, DefaultPlugins, loadAgentConfig } = require('../../plugins/core/default');
235
+ const { SYSTEM_PLUGINS } = require('../common/constants');
236
+ const keepSet = new Set(options.keepLoaded || Array.from(SYSTEM_PLUGINS));
237
+
238
+ console.log('[foliko] reloadAll: starting full reload');
239
+
240
+ // 1. 重载 ext-exec(先清空 _extensions)
241
+ if (options.reloadExtExec !== false && framework.pluginManager.has('extension-executor')) {
242
+ try {
243
+ await framework.pluginManager.reload('extension-executor');
244
+ console.log('[foliko] reloadAll: ext-executor reloaded (state cleared)');
245
+ } catch (err) {
246
+ console.log('[foliko] reloadAll: ext-executor reload failed:', err.message);
247
+ }
248
+ }
249
+
250
+ // 2. 卸载非系统插件
251
+ try {
252
+ const unloadList = await framework.pluginManager.unloadExcept(keepSet);
253
+ console.log(`[foliko] reloadAll: unloaded ${unloadList.length} non-system plugins`);
254
+ } catch (err) {
255
+ console.log('[foliko] reloadAll: unloadExcept failed:', err.message);
256
+ }
257
+
258
+ // 3. 重载 defaults plugin
259
+ let defaultsPlugin = framework.pluginManager.get('defaults');
260
+ if (!defaultsPlugin) {
261
+ const dp = new DefaultPlugins({ agentDir: framework._agentDir });
262
+ try { await framework.loadPlugin(dp); defaultsPlugin = dp; }
263
+ catch (err) { console.log('[foliko] reloadAll: load defaults failed:', err.message); }
264
+ } else {
265
+ try { await defaultsPlugin.reload(framework); }
266
+ catch (err) { console.log('[foliko] reloadAll: reload defaults failed:', err.message); }
267
+ }
268
+
269
+ const agentConfig = defaultsPlugin
270
+ ? (defaultsPlugin.getConfig() || loadAgentConfig(framework, framework._agentDir))
271
+ : loadAgentConfig(framework, framework._agentDir);
272
+
273
+ // 4. bootstrapDefaults(加载所有 default 插件)
274
+ try {
275
+ await bootstrapDefaults(framework, { _config: agentConfig, _skipConfigLoad: true });
276
+ console.log('[foliko] reloadAll: bootstrapDefaults done');
277
+ } catch (err) {
278
+ console.log('[foliko] reloadAll: bootstrapDefaults failed:', err.message);
279
+ }
280
+
281
+ // 5. 重载 skill-manager(最后一步,让新 skill 的 commands 注册到新 ext-exec)
282
+ if (options.reloadSkillManager !== false) {
283
+ const skillManager = framework.pluginManager.get('skill-manager');
284
+ if (skillManager) {
285
+ if (Array.isArray(agentConfig.skillsDirs)) skillManager.setSearchDirs(agentConfig.skillsDirs);
286
+ try {
287
+ await skillManager.reload(framework);
288
+ const sm = skillManager;
289
+ const extExec = framework.pluginManager.get('extension-executor');
290
+ const skillTools = extExec?._extensions?.get('skill')?.tools || [];
291
+ console.log(
292
+ `[foliko] reloadAll: skill-manager reloaded, ${sm._skills.size} skills, ${Object.keys(sm.tools).length} tools in sm, ${skillTools.length} tools in ext-exec`
293
+ );
294
+ } catch (err) {
295
+ console.log('[foliko] reloadAll: skill-manager reload failed:', err.message);
296
+ }
297
+ }
298
+ }
299
+
300
+ framework.emit('reload:complete', { framework });
301
+ return framework;
302
+ }
303
+
304
+ module.exports = { bootstrap, ready, destroy, setCwd, rescanProject, reloadAll };