foliko 2.0.2 → 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 (78) 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 +1 -0
  8. package/plugins/core/coordinator/index.js +10 -5
  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 +1 -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/session/entry.js +1 -11
  61. package/src/storage/manager.js +1 -12
  62. package/src/tool/executor.js +2 -1
  63. package/src/tool/router.js +5 -5
  64. package/src/utils/data-splitter.js +2 -1
  65. package/src/utils/index.js +150 -0
  66. package/src/utils/message-validator.js +21 -17
  67. package/src/utils/plugin-helpers.js +19 -5
  68. package/subagent.md +2 -2
  69. package/tests/core/plugin-prompts.test.js +219 -0
  70. package/tests/core/prompt-registry.test.js +209 -0
  71. package/src/cli/utils/plugin-config.js +0 -50
  72. package/src/config/plugin-config.js +0 -50
  73. /package/skills/{ambient-agent → ambient}/SKILL.md +0 -0
  74. /package/skills/{foliko-dev → foliko}/AGENTS.md +0 -0
  75. /package/skills/{foliko-dev → foliko}/SKILL.md +0 -0
  76. /package/skills/{mcp-usage → mcp}/SKILL.md +0 -0
  77. /package/skills/{plugin-guide → plugins-guide}/SKILL.md +0 -0
  78. /package/skills/{python-plugin-dev → python}/SKILL.md +0 -0
@@ -6,60 +6,6 @@
6
6
  */
7
7
 
8
8
  const path = require('path');
9
- const fs = require('fs');
10
-
11
- /**
12
- * Load agent config from .foliko/ directory
13
- * @param {Framework} framework
14
- * @param {string} agentDir
15
- * @returns {Object}
16
- */
17
- function loadAgentConfig(framework, agentDir = '.foliko') {
18
- const cwd = framework.getCwd ? framework.getCwd() : process.cwd();
19
- const folikoDir = path.resolve(cwd, agentDir);
20
- const config = {
21
- plugins: [],
22
- skills: [],
23
- ai: {},
24
- };
25
-
26
- // Load plugins.json
27
- const pluginsPath = path.join(folikoDir, 'plugins.json');
28
- if (fs.existsSync(pluginsPath)) {
29
- try {
30
- const pluginsConfig = JSON.parse(fs.readFileSync(pluginsPath, 'utf-8'));
31
- if (Array.isArray(pluginsConfig)) {
32
- config.plugins = pluginsConfig;
33
- } else if (pluginsConfig.plugins) {
34
- config.plugins = pluginsConfig.plugins;
35
- }
36
- if (pluginsConfig.ai) config.ai = { ...config.ai, ...pluginsConfig.ai };
37
- if (pluginsConfig.skillsDirs) config.skillsDirs = pluginsConfig.skillsDirs;
38
- } catch (err) {
39
- framework.logger?.warn?.('Failed to load plugins.json:', err.message);
40
- }
41
- }
42
-
43
- // Load agent configs from agents/ directory
44
- const agentsDir = path.join(folikoDir, 'agents');
45
- if (fs.existsSync(agentsDir)) {
46
- try {
47
- const files = fs.readdirSync(agentsDir).filter(f => f.endsWith('.json'));
48
- config.agentFiles = files.map(f => path.join(agentsDir, f));
49
- } catch { /* ignore */ }
50
- }
51
-
52
- // Load skills config
53
- const skillsDir = path.join(folikoDir, 'skills');
54
- if (fs.existsSync(skillsDir)) {
55
- try {
56
- config.skillsDirs = config.skillsDirs || [];
57
- config.skillsDirs.push(skillsDir);
58
- } catch { /* ignore */ }
59
- }
60
-
61
- return config;
62
- }
63
9
 
64
10
  /**
65
11
  * Load default plugins based on config
@@ -75,4 +21,4 @@ async function loadDefaultPlugins(framework, config = {}) {
75
21
  return framework.pluginManager.getAll().map(p => p.name);
76
22
  }
77
23
 
78
- module.exports = { loadAgentConfig, loadDefaultPlugins };
24
+ module.exports = { loadDefaultPlugins };
package/src/index.js CHANGED
@@ -15,12 +15,12 @@ require('dotenv').config({ override: true });
15
15
  // Framework
16
16
  // ============================================================
17
17
  const { Framework } = require('./framework/framework');
18
- const { bootstrap, ready, destroy } = require('./framework/lifecycle');
18
+ const { bootstrap, ready, destroy, setCwd, rescanProject, reloadAll } = require('./framework/lifecycle');
19
19
 
20
20
  // ============================================================
21
21
  // Agent
22
22
  // ============================================================
23
- const { createAgent, MainAgent, SubAgent, WorkerAgent } = require('./agent');
23
+ const { createAgent, MainAgent, SubAgent, WorkerAgent, PromptRegistry } = require('./agent');
24
24
 
25
25
  // ============================================================
26
26
  // Plugin
@@ -70,6 +70,9 @@ exports.createAgent = createAgent;
70
70
 
71
71
  exports.ready = ready;
72
72
  exports.destroy = destroy;
73
+ exports.setCwd = setCwd;
74
+ exports.rescanProject = rescanProject;
75
+ exports.reloadAll = reloadAll;
73
76
 
74
77
  // --- 上下文管理 ---
75
78
 
@@ -113,6 +116,11 @@ exports.withPython = (config = {}) => {
113
116
  exports.z = require('zod');
114
117
  exports.LLM = llm;
115
118
 
119
+ // --- 常量 ---
120
+ exports.PROMPT_SLOT = require('./common/constants').PROMPT_SLOT;
121
+ exports.PROMPT_PRIORITY = require('./common/constants').PROMPT_PRIORITY;
122
+ exports.PROMPT_PRIORITY_TO_SLOT = require('./common/constants').PROMPT_PRIORITY_TO_SLOT;
123
+
116
124
  // --- 框架核心类(用于扩展) ---
117
125
 
118
126
  exports.Framework = Framework;
@@ -123,6 +131,7 @@ exports.ToolRegistry = ToolRegistry;
123
131
  exports.ToolExecutor = ToolExecutor;
124
132
  exports.Plugin = Plugin;
125
133
  exports.PluginManager = PluginManager;
134
+ exports.PromptRegistry = PromptRegistry;
126
135
  exports.EventEmitter = EventEmitter;
127
136
  exports.Logger = Logger;
128
137
 
@@ -20,10 +20,30 @@ class Plugin {
20
20
 
21
21
  _framework = null;
22
22
  _subAgents = [];
23
- _promptParts = [];
24
- _deferredPromptParts = null;
25
23
  tools = {};
26
24
 
25
+ /**
26
+ * 声明式 prompt parts(推荐使用,替代 start() 中的 registerPromptPart 调用)
27
+ *
28
+ * 用法:
29
+ * prompts = [
30
+ * {
31
+ * name: 'my-part',
32
+ * slot: 'BEHAVIOR', // 可选,PROMPT_SLOT 中的一个;默认 CUSTOM
33
+ * order: 50, // 可选,覆盖 slot 默认 order
34
+ * description: '描述', // 可选
35
+ * provider() { // 'this' 绑定到 plugin 实例;不要用箭头函数
36
+ * return this._buildSomething();
37
+ * },
38
+ * },
39
+ * ];
40
+ *
41
+ * 生命周期:
42
+ * - Plugin.start() 时自动注册到 framework.prompts
43
+ * - Plugin.uninstall() / Plugin.reload() 时自动清理(clearOwner(this.name))
44
+ */
45
+ prompts = [];
46
+
27
47
  install(framework) {
28
48
  this._framework = framework;
29
49
  return this;
@@ -56,9 +76,51 @@ class Plugin {
56
76
 
57
77
  start(framework) {
58
78
  this._autoRegisterSubAgents();
79
+ this._autoRegisterPrompts();
59
80
  return this;
60
81
  }
61
82
 
83
+ /**
84
+ * 自动注册 this.prompts 声明的所有 part 到 framework.prompts
85
+ * - 失败一个不影响其他
86
+ * - 'this' 在 provider 中绑定到 plugin 实例(必须用普通 function,箭头函数的 this 是词法的)
87
+ */
88
+ _autoRegisterPrompts() {
89
+ const reg = this._framework?.prompts;
90
+ if (!reg) return;
91
+ if (!Array.isArray(this.prompts) || this.prompts.length === 0) return;
92
+
93
+ for (const entry of this.prompts) {
94
+ if (!entry || !entry.name || typeof entry.provider !== 'function') {
95
+ log.warn(`[${this.name}] Invalid prompt entry: missing name or provider`);
96
+ continue;
97
+ }
98
+ try {
99
+ // bind 让 provider 中的 this 指向 plugin 实例
100
+ const boundProvider = entry.provider.bind(this);
101
+ reg.register(this.name, entry.name, boundProvider, {
102
+ slot: entry.slot,
103
+ order: entry.order,
104
+ description: entry.description,
105
+ });
106
+ } catch (err) {
107
+ log.error(`[${this.name}] Failed to register prompt part "${entry.name}":`, err.message);
108
+ }
109
+ }
110
+ }
111
+
112
+ /**
113
+ * 清理当前 plugin 在 framework.prompts 中注册的所有 part
114
+ */
115
+ _cleanupPrompts() {
116
+ const reg = this._framework?.prompts;
117
+ if (!reg) return;
118
+ const removed = reg.clearOwner(this.name);
119
+ if (removed > 0) {
120
+ log.debug(`[${this.name}] cleared ${removed} prompt part(s)`);
121
+ }
122
+ }
123
+
62
124
  _autoRegisterSubAgents() {
63
125
  const agentsToRegister = this._deferredSubAgents || this.agents;
64
126
  if (!agentsToRegister || !Array.isArray(agentsToRegister) || agentsToRegister.length === 0) {
@@ -139,15 +201,10 @@ class Plugin {
139
201
  reload(framework) {
140
202
  this._framework = framework;
141
203
  this._deferredSubAgents = null;
142
- this._deferredPromptParts = null;
143
204
 
144
- for (const part of this._promptParts) {
145
- try {
146
- framework._mainAgent?.registerPromptPart(part.name, part.priority, part.provider);
147
- } catch (err) {
148
- log.error(` Failed to re-register prompt part ${part.name}:`, err.message);
149
- }
150
- }
205
+ // 先清理旧的,再重新注册 declarative prompts
206
+ this._cleanupPrompts();
207
+ this._autoRegisterPrompts();
151
208
 
152
209
  this._autoRegisterSubAgents();
153
210
  }
@@ -157,15 +214,8 @@ class Plugin {
157
214
  }
158
215
 
159
216
  uninstall(framework) {
160
- for (const part of this._promptParts) {
161
- try {
162
- framework._mainAgent?.unregisterPromptPart(part.name);
163
- } catch (err) {
164
- // ignore
165
- }
166
- }
167
- this._promptParts = [];
168
- this._deferredPromptParts = null;
217
+ // 清理 framework.prompts 中该插件注册的所有 part
218
+ this._cleanupPrompts();
169
219
 
170
220
  for (const plugin of this._subAgents) {
171
221
  try {
@@ -178,42 +228,6 @@ class Plugin {
178
228
  this._framework = null;
179
229
  }
180
230
 
181
- registerPromptPart(name, priority = 100, provider) {
182
- if (!this._framework) return;
183
-
184
- this._promptParts.push({ name, priority, provider });
185
-
186
- const mainAgent = this._framework.getMainAgent();
187
- if (mainAgent) {
188
- mainAgent.registerPromptPart(name, priority, provider);
189
- this._framework.syncPromptPartsToSubagents();
190
- return;
191
- }
192
-
193
- if (!this._deferredPromptParts) {
194
- this._deferredPromptParts = [];
195
- }
196
- this._deferredPromptParts.push({ name, priority, provider });
197
-
198
- const alreadyListening = this._framework._events?.['framework:ready']?.length > 0;
199
- this._framework.once('framework:ready', () => {
200
- setTimeout(() => {
201
- if (this._deferredPromptParts) {
202
- for (const part of this._deferredPromptParts) {
203
- try {
204
- const mainAgent = this._framework.getMainAgent();
205
- mainAgent?.registerPromptPart(part.name, part.priority, part.provider);
206
- } catch (err) {
207
- log.error(` Failed to register prompt part ${part.name}:`, err.message);
208
- }
209
- }
210
- this._deferredPromptParts = null;
211
- this._framework.syncPromptPartsToSubagents();
212
- }
213
- }, 100);
214
- });
215
- }
216
-
217
231
  getInfo() {
218
232
  return {
219
233
  name: this.name,
@@ -5,66 +5,10 @@
5
5
  */
6
6
 
7
7
  const path = require('path');
8
- const fs = require('fs');
8
+ const { resolvePluginPath, scanPluginNames } = require('../utils/plugin-helpers');
9
9
  const { logger } = require('../common/logger');
10
10
  const log = logger.child('plugin-loader');
11
11
 
12
- /**
13
- * Resolve plugin path from name
14
- * @param {string} pluginName
15
- * @param {Object} options
16
- * @returns {string|null}
17
- */
18
- function resolvePluginPath(pluginName, options = {}) {
19
- const { pluginDirs = [], searchPaths = [] } = options;
20
-
21
- // Check explicit paths first
22
- const allPaths = [...pluginDirs, ...searchPaths];
23
- for (const dir of allPaths) {
24
- const fullPath = path.resolve(dir, pluginName);
25
- if (fs.existsSync(fullPath)) {
26
- return fullPath;
27
- }
28
- const indexJs = path.join(fullPath, 'index.js');
29
- if (fs.existsSync(indexJs)) {
30
- return fullPath;
31
- }
32
- }
33
-
34
- // Check node_modules
35
- try {
36
- const resolved = require.resolve(pluginName);
37
- return resolved;
38
- } catch {
39
- return null;
40
- }
41
- }
42
-
43
- /**
44
- * Scan directory for plugin names
45
- * @param {string} dir
46
- * @returns {string[]}
47
- */
48
- function scanPluginNames(dir) {
49
- if (!fs.existsSync(dir)) return [];
50
-
51
- try {
52
- return fs.readdirSync(dir)
53
- .filter(f => {
54
- const fullPath = path.join(dir, f);
55
- const stat = fs.statSync(fullPath);
56
- if (stat.isDirectory()) {
57
- return fs.existsSync(path.join(fullPath, 'index.js'));
58
- }
59
- return f.endsWith('.js');
60
- })
61
- .map(f => f.replace(/\.js$/, ''));
62
- } catch (err) {
63
- log.warn(`Failed to scan plugin directory ${dir}: ${err.message}`);
64
- return [];
65
- }
66
- }
67
-
68
12
  /**
69
13
  * Load a plugin from a file path
70
14
  * @param {string} pluginPath
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  const { randomUUID } = require('crypto');
7
+ const { generateEntryId } = require('../common/id');
7
8
 
8
9
  /**
9
10
  * Entry types for session tree entries
@@ -21,17 +22,6 @@ const EntryTypes = {
21
22
  LEAF: 'leaf'
22
23
  };
23
24
 
24
- /**
25
- * Generate a unique short ID (8 hex chars, collision-checked)
26
- */
27
- function generateEntryId(byId) {
28
- for (let i = 0; i < 100; i++) {
29
- const id = randomUUID().slice(0, 8);
30
- if (!byId.has(id)) return id;
31
- }
32
- return randomUUID();
33
- }
34
-
35
25
  /**
36
26
  * Generate UUID v7 for session IDs
37
27
  */
@@ -5,18 +5,7 @@
5
5
 
6
6
  const fs = require('fs');
7
7
  const path = require('path');
8
- const { randomUUID } = require('crypto');
9
-
10
- /**
11
- * Generate unique entry ID
12
- */
13
- function generateEntryId(existingIds) {
14
- for (let i = 0; i < 100; i++) {
15
- const id = randomUUID().slice(0, 8);
16
- if (!existingIds.has(id)) return id;
17
- }
18
- return randomUUID();
19
- }
8
+ const { generateEntryId } = require('../common/id');
20
9
 
21
10
  class StorageEntry {
22
11
  constructor(key, value, timestamp) {
@@ -71,7 +71,8 @@ class ToolExecutor extends EventEmitter {
71
71
  }
72
72
 
73
73
  validateToolCalls(messages) {
74
- return validateToolCalls(messages);
74
+ const registeredTools = new Set(this._tools.keys());
75
+ return validateToolCalls(messages, { availableTools: registeredTools });
75
76
  }
76
77
 
77
78
  getStats() {
@@ -23,7 +23,7 @@ const INTENT_PATTERNS = {
23
23
  '写代码', '开发', '编程', '创建插件', '编写函数',
24
24
  'code', 'develop', 'plugin', '编程',
25
25
  ],
26
- tools: ['loadSkill', 'read_file', 'write_file', 'execute_command', 'shell'],
26
+ tools: ['skill_load', 'read_file', 'write_file', 'execute_command', 'shell_exec'],
27
27
  },
28
28
  system_info: {
29
29
  keywords: [
@@ -34,19 +34,19 @@ const INTENT_PATTERNS = {
34
34
  },
35
35
  data_analysis: {
36
36
  keywords: ['分析', '数据', '统计', '查询', 'analyze', 'data', 'statistics', 'query'],
37
- tools: ['execute_command', 'python', 'search_file'],
37
+ tools: ['execute_command', 'py_execute', 'search_file'],
38
38
  },
39
39
  plugin_management: {
40
40
  keywords: ['插件', '重载', '加载插件', 'plugin', 'reload', 'load plugin'],
41
- tools: ['reload_plugins', 'list_plugins', 'loadSkill'],
41
+ tools: ['reload_plugins', 'list_plugins', 'skill_load'],
42
42
  },
43
43
  shell_command: {
44
44
  keywords: ['命令', '终端', 'shell', '执行', 'command', 'terminal', 'bash', 'cmd'],
45
- tools: ['shell', 'powershell', 'execute_command'],
45
+ tools: ['shell_exec', 'powershell', 'execute_command'],
46
46
  },
47
47
  skill_usage: {
48
48
  keywords: ['技能', 'skill', '使用技能', '加载技能'],
49
- tools: ['loadSkill'],
49
+ tools: ['skill_load'],
50
50
  },
51
51
  scheduling: {
52
52
  keywords: [
@@ -168,13 +168,14 @@ class DataSplitter {
168
168
  const processChunk = async (chunk) => {
169
169
  const chunkIndex = chunk.index;
170
170
 
171
- // 为当前块创建子 Agent
171
+ // 为当前块创建子 Agent(隐藏,不在指令系统中显示)
172
172
  const subagent = this.framework.createSubAgent({
173
173
  name: `${agentName}-chunk-${chunkIndex}`,
174
174
  role: agentRole,
175
175
  systemPrompt: `你是${agentRole},负责处理大数据中的第 ${chunkIndex + 1}/${totalChunks} 块。`,
176
176
  maxRetries: this.maxRetries,
177
177
  disableTools: true, // 分拆处理只做文本分析,不需要额外工具
178
+ hidden: true, // 隐藏子Agent,不在指令系统中显示
178
179
  });
179
180
 
180
181
  try {
@@ -47,6 +47,80 @@ const {
47
47
  combineBoundaries,
48
48
  } = require('../common/errors');
49
49
 
50
+ /**
51
+ * 解析 YAML frontmatter
52
+ * @param {string} content
53
+ * @returns {Object|null}
54
+ */
55
+ function parseFrontmatter(content) {
56
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
57
+ if (!match) return null;
58
+
59
+ const frontmatter = {};
60
+ const lines = match[1].split('\n');
61
+ let currentKey = null;
62
+
63
+ for (const line of lines) {
64
+ // 处理 metadata 嵌套
65
+ if (currentKey === 'metadata') {
66
+ if (line.match(/^ {2}[a-zA-Z]/)) {
67
+ const colonIndex = line.indexOf(':');
68
+ if (colonIndex > 0) {
69
+ const key = line.substring(2, colonIndex).trim();
70
+ const value = line
71
+ .substring(colonIndex + 1)
72
+ .trim()
73
+ .replace(/^["']|["']$/g, '');
74
+ frontmatter[currentKey][key] = value;
75
+ continue;
76
+ }
77
+ } else if (line.match(/^[a-zA-Z]/)) {
78
+ currentKey = null;
79
+ continue;
80
+ }
81
+ }
82
+
83
+ const colonIndex = line.indexOf(':');
84
+ if (colonIndex === -1) continue;
85
+
86
+ const key = line.substring(0, colonIndex).trim();
87
+ let value = line.substring(colonIndex + 1).trim();
88
+
89
+ // 移除引号
90
+ if (
91
+ (value.startsWith('"') && value.endsWith('"')) ||
92
+ (value.startsWith("'") && value.endsWith("'"))
93
+ ) {
94
+ value = value.slice(1, -1);
95
+ }
96
+
97
+ if (key === 'metadata') {
98
+ currentKey = 'metadata';
99
+ frontmatter.metadata = {};
100
+ } else if (key === 'allowed-tools' || key === 'skills' || key === 'tools' || key === 'tags') {
101
+ // 去除首尾空白和方括号
102
+ value = value.trim().replace(/^\[|\]$/g, '');
103
+ frontmatter[key] = value
104
+ .split(',')
105
+ .map((v) => v.trim().replace(/^["']|["']$/g, ''))
106
+ .filter(Boolean);
107
+ } else {
108
+ frontmatter[key] = value;
109
+ }
110
+ }
111
+
112
+ return frontmatter;
113
+ }
114
+
115
+ /**
116
+ * 移除 frontmatter,只保留正文内容
117
+ * @param {string} content
118
+ * @returns {string}
119
+ */
120
+ function stripFrontmatter(content) {
121
+ return content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, '');
122
+ }
123
+
50
124
  /**
51
125
  * 清理 LLM 回复中的思考标记
52
126
  * @param {string} text
@@ -530,6 +604,8 @@ module.exports = {
530
604
  // 工具函数
531
605
  cleanResponse,
532
606
  safeJsonParse,
607
+ parseFrontmatter,
608
+ stripFrontmatter,
533
609
  deepClone,
534
610
  debounce,
535
611
  throttle,
@@ -539,4 +615,78 @@ module.exports = {
539
615
 
540
616
  // Edit/Diff
541
617
  editDiff: require('../common/diff'),
618
+
619
+ // 插件发布/安装相关
620
+ DEFAULT_REPO: 'https://github.com/chnak/foliko-plugins.git',
621
+
622
+ IGNORE_PATTERNS: [
623
+ 'node_modules',
624
+ '.git',
625
+ '.env',
626
+ '.DS_Store',
627
+ 'Thumbs.db',
628
+ '*.log',
629
+ '*.lock',
630
+ '*.bak',
631
+ '.claude',
632
+ '.foliko',
633
+ 'examples',
634
+ 'dist',
635
+ 'build',
636
+ 'coverage',
637
+ 'tests',
638
+ '__tests__',
639
+ '*.test.js',
640
+ '*.spec.js',
641
+ 'package-lock.json',
642
+ 'yarn.lock',
643
+ 'pnpm-lock.yaml',
644
+ ],
645
+
646
+ shouldIgnore(name) {
647
+ return this.IGNORE_PATTERNS.some((pattern) => {
648
+ if (pattern.startsWith('*.')) {
649
+ return name.endsWith(pattern.slice(1));
650
+ }
651
+ return name === pattern;
652
+ });
653
+ },
654
+
655
+ copyDirRecursive(src, dest) {
656
+ const fs = require('fs');
657
+ if (!fs.existsSync(src)) return;
658
+ fs.mkdirSync(dest, { recursive: true });
659
+ const entries = fs.readdirSync(src, { withFileTypes: true });
660
+ for (const entry of entries) {
661
+ const srcPath = path.join(src, entry.name);
662
+ const destPath = path.join(dest, entry.name);
663
+ if (this.shouldIgnore(entry.name)) continue;
664
+ if (entry.isDirectory()) {
665
+ this.copyDirRecursive(srcPath, destPath);
666
+ } else {
667
+ fs.copyFileSync(srcPath, destPath);
668
+ }
669
+ }
670
+ },
671
+
672
+ parseGitUrl(url) {
673
+ const patterns = [
674
+ /^https:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?$/,
675
+ /^git@github\.com:([^/]+)\/([^/]+?)(?:\.git)?$/,
676
+ ];
677
+ for (const pattern of patterns) {
678
+ const match = url.match(pattern);
679
+ if (match) return { owner: match[1], repo: match[2] };
680
+ }
681
+ return null;
682
+ },
683
+
684
+ gitCommand(args, cwd) {
685
+ const { execSync } = require('child_process');
686
+ try {
687
+ return execSync(`git ${args}`, { cwd, encoding: 'utf-8', stdio: 'pipe' });
688
+ } catch (err) {
689
+ return err.stdout || err.stderr || '';
690
+ }
691
+ },
542
692
  };
@@ -53,24 +53,28 @@ function validateMessagesPairing(messages, options = {}) {
53
53
  let removedToolResultCount = 0;
54
54
 
55
55
  for (const msg of messages) {
56
- if (msg.role !== 'tool' || !Array.isArray(msg.content)) continue;
56
+ if (msg.role !== 'tool') continue;
57
57
 
58
- const originalLength = msg.content.length;
59
- msg.content = msg.content.filter((item) => {
60
- if (
61
- item &&
62
- (item.type === 'tool-result' || item.type === 'tool_result') &&
63
- item.toolCallId &&
64
- !validToolCallIds.has(item.toolCallId)
65
- ) {
66
- removedToolResultCount++;
67
- return false;
58
+ if (Array.isArray(msg.content)) {
59
+ const originalLength = msg.content.length;
60
+ msg.content = msg.content.filter((item) => {
61
+ if (
62
+ item &&
63
+ (item.type === 'tool-result' || item.type === 'tool_result') &&
64
+ item.toolCallId &&
65
+ !validToolCallIds.has(item.toolCallId)
66
+ ) {
67
+ removedToolResultCount++;
68
+ return false;
69
+ }
70
+ return true;
71
+ });
72
+ // content 全被删除了,标记整个消息待删除
73
+ if (msg.content.length === 0 && originalLength > 0) {
74
+ msg._orphaned = true;
68
75
  }
69
- return true;
70
- });
71
-
72
- // content 全被删除了,标记整个消息待删除
73
- if (msg.content.length === 0 && originalLength > 0) {
76
+ } else if (msg.tool_call_id && !validToolCallIds.has(msg.tool_call_id)) {
77
+ // OpenAI 兼容格式:整条消息无对应 tool_calls,标记为 orphaned
74
78
  msg._orphaned = true;
75
79
  }
76
80
  }
@@ -280,4 +284,4 @@ module.exports = {
280
284
  validateToolCalls,
281
285
  validateAll,
282
286
  filterPairedMessages,
283
- };
287
+ };