foliko 2.0.2 → 2.0.4
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/README.md +6 -6
- package/docs/public-api.md +91 -0
- package/docs/system-prompt.md +219 -0
- package/docs/usage.md +6 -6
- package/package.json +1 -1
- package/plugins/ambient/ExplorerLoop.js +1 -0
- package/plugins/ambient/index.js +1 -0
- package/plugins/core/coordinator/index.js +10 -5
- package/plugins/core/default/bootstrap.js +21 -3
- package/plugins/core/default/config.js +12 -3
- package/plugins/core/python-loader/index.js +3 -3
- package/plugins/core/scheduler/index.js +26 -2
- package/plugins/core/skill-manager/index.js +198 -151
- package/plugins/core/sub-agent/index.js +34 -15
- package/plugins/core/think/index.js +1 -0
- package/plugins/core/workflow/index.js +5 -4
- package/plugins/executors/data-splitter/index.js +14 -1
- package/plugins/executors/extension/index.js +52 -38
- package/plugins/executors/python/index.js +3 -3
- package/plugins/executors/shell/index.js +6 -4
- package/plugins/io/web/index.js +2 -1
- package/plugins/memory/index.js +29 -74
- package/plugins/messaging/email/handlers.js +1 -19
- package/plugins/messaging/email/monitor.js +2 -17
- package/plugins/messaging/email/reply.js +2 -1
- package/plugins/messaging/email/utils.js +20 -1
- package/plugins/messaging/feishu/index.js +1 -1
- package/plugins/messaging/qq/index.js +1 -1
- package/plugins/messaging/telegram/index.js +1 -1
- package/plugins/messaging/weixin/index.js +1 -1
- package/plugins/plugin-manager/index.js +1 -33
- package/plugins/tools/index.js +14 -1
- package/skills/plugins/SKILL.md +316 -0
- package/skills/skill-guide/SKILL.md +5 -5
- package/skills/{subagent-guide → subagents}/SKILL.md +1 -1
- package/skills/{workflow-guide → workflows}/SKILL.md +3 -3
- package/skills/{workflow-troubleshooting → workflows/workflow-troubleshooting}/DEBUGGING.md +197 -197
- package/skills/{workflow-troubleshooting → workflows/workflow-troubleshooting}/SKILL.md +391 -391
- package/src/agent/base.js +5 -20
- package/src/agent/chat.js +67 -5
- package/src/agent/index.js +20 -8
- package/src/agent/main.js +100 -23
- package/src/agent/prompt-registry.js +296 -0
- package/src/agent/sub-config.js +1 -78
- package/src/agent/sub.js +20 -25
- package/src/cli/commands/plugin.js +1 -60
- package/src/cli/ui/chat-ui-old.js +1 -1
- package/src/cli/ui/chat-ui.js +21 -17
- package/src/cli/ui/components/agent-mention-provider.js +1 -1
- package/src/common/constants.js +42 -0
- package/src/common/id.js +13 -0
- package/src/common/queue.js +3 -3
- package/src/context/compressor.js +17 -9
- package/src/framework/framework.js +218 -0
- package/src/framework/index.js +1 -2
- package/src/framework/lifecycle.js +102 -1
- package/src/framework/loader.js +1 -55
- package/src/index.js +11 -2
- package/src/plugin/base.js +69 -55
- package/src/plugin/loader.js +1 -57
- package/src/session/chat.js +1 -1
- package/src/session/entry.js +1 -11
- package/src/storage/manager.js +1 -12
- package/src/tool/executor.js +3 -2
- package/src/tool/router.js +5 -5
- package/src/utils/data-splitter.js +2 -1
- package/src/utils/index.js +150 -0
- package/src/utils/message-validator.js +21 -17
- package/src/utils/plugin-helpers.js +19 -5
- package/subagent.md +2 -2
- package/tests/core/plugin-prompts.test.js +219 -0
- package/tests/core/prompt-registry.test.js +209 -0
- package/src/cli/utils/plugin-config.js +0 -50
- package/src/config/plugin-config.js +0 -50
- /package/skills/{ambient-agent → ambient}/SKILL.md +0 -0
- /package/skills/{foliko-dev → foliko}/AGENTS.md +0 -0
- /package/skills/{foliko-dev → foliko}/SKILL.md +0 -0
- /package/skills/{mcp-usage → mcp}/SKILL.md +0 -0
- /package/skills/{plugin-guide → plugins-guide}/SKILL.md +0 -0
- /package/skills/{python-plugin-dev → python}/SKILL.md +0 -0
package/src/plugin/base.js
CHANGED
|
@@ -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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
161
|
-
|
|
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,
|
package/src/plugin/loader.js
CHANGED
|
@@ -5,66 +5,10 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
const path = require('path');
|
|
8
|
-
const
|
|
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
|
package/src/session/chat.js
CHANGED
|
@@ -225,7 +225,7 @@ class ChatSession extends EventEmitter {
|
|
|
225
225
|
messageStore.messages = messages;
|
|
226
226
|
messageStore.historyLoaded = true;
|
|
227
227
|
messageStore._hasMoreHistory = allMessages.length > MAX_INITIAL_LOAD;
|
|
228
|
-
logger.debug(`Loaded ${messages.length} msgs for session ${sessionId} (total: ${allMessages.length})`);
|
|
228
|
+
//logger.debug(`Loaded ${messages.length} msgs for session ${sessionId} (total: ${allMessages.length})`);
|
|
229
229
|
}
|
|
230
230
|
}
|
|
231
231
|
}
|
package/src/session/entry.js
CHANGED
|
@@ -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
|
*/
|
package/src/storage/manager.js
CHANGED
|
@@ -5,18 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
const fs = require('fs');
|
|
7
7
|
const path = require('path');
|
|
8
|
-
const {
|
|
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) {
|
package/src/tool/executor.js
CHANGED
|
@@ -35,7 +35,7 @@ class ToolExecutor extends EventEmitter {
|
|
|
35
35
|
return;
|
|
36
36
|
}
|
|
37
37
|
this._tools.set(tool.name, tool);
|
|
38
|
-
logger.debug('ToolExecutor', `Registered tool: ${tool.name}`);
|
|
38
|
+
//logger.debug('ToolExecutor', `Registered tool: ${tool.name}`);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
unregisterTool(name) {
|
|
@@ -71,7 +71,8 @@ class ToolExecutor extends EventEmitter {
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
validateToolCalls(messages) {
|
|
74
|
-
|
|
74
|
+
const registeredTools = new Set(this._tools.keys());
|
|
75
|
+
return validateToolCalls(messages, { availableTools: registeredTools });
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
getStats() {
|
package/src/tool/router.js
CHANGED
|
@@ -23,7 +23,7 @@ const INTENT_PATTERNS = {
|
|
|
23
23
|
'写代码', '开发', '编程', '创建插件', '编写函数',
|
|
24
24
|
'code', 'develop', 'plugin', '编程',
|
|
25
25
|
],
|
|
26
|
-
tools: ['
|
|
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', '
|
|
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', '
|
|
41
|
+
tools: ['reload_plugins', 'list_plugins', 'skill_load'],
|
|
42
42
|
},
|
|
43
43
|
shell_command: {
|
|
44
44
|
keywords: ['命令', '终端', 'shell', '执行', 'command', 'terminal', 'bash', 'cmd'],
|
|
45
|
-
tools: ['
|
|
45
|
+
tools: ['shell_exec', 'powershell', 'execute_command'],
|
|
46
46
|
},
|
|
47
47
|
skill_usage: {
|
|
48
48
|
keywords: ['技能', 'skill', '使用技能', '加载技能'],
|
|
49
|
-
tools: ['
|
|
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 {
|
package/src/utils/index.js
CHANGED
|
@@ -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'
|
|
56
|
+
if (msg.role !== 'tool') continue;
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
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
|
+
};
|
|
@@ -70,15 +70,20 @@ function resolvePluginPath(pluginsDir, name, options = {}) {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
/**
|
|
73
|
-
*
|
|
73
|
+
* 扫描插件目录,返回所有插件名称(支持嵌套目录)
|
|
74
74
|
* @param {string} pluginsDir - 插件目录
|
|
75
|
+
* @param {string} baseDir - 基础目录(用于计算相对路径)
|
|
75
76
|
* @returns {string[]} 插件名称列表
|
|
76
77
|
*/
|
|
77
|
-
function scanPluginNames(pluginsDir) {
|
|
78
|
+
function scanPluginNames(pluginsDir, baseDir = null) {
|
|
78
79
|
if (!fs.existsSync(pluginsDir)) {
|
|
79
80
|
return [];
|
|
80
81
|
}
|
|
81
82
|
|
|
83
|
+
if (baseDir === null) {
|
|
84
|
+
baseDir = pluginsDir;
|
|
85
|
+
}
|
|
86
|
+
|
|
82
87
|
// 忽略的目录/文件(无意义或系统文件)
|
|
83
88
|
const IGNORE_PATTERNS = new Set([
|
|
84
89
|
'__pycache__',
|
|
@@ -105,13 +110,22 @@ function scanPluginNames(pluginsDir) {
|
|
|
105
110
|
const isDir = entry.isSymbolicLink() ? isDirectory(fullPath) : entry.isDirectory();
|
|
106
111
|
|
|
107
112
|
if (isDir) {
|
|
108
|
-
|
|
109
|
-
|
|
113
|
+
const indexPath = path.join(fullPath, 'index.js');
|
|
114
|
+
if (fs.existsSync(indexPath)) {
|
|
115
|
+
// 这是一个插件目录
|
|
116
|
+
const relativePath = path.relative(baseDir, fullPath);
|
|
117
|
+
names.add(relativePath.replace(/\\/g, '/'));
|
|
118
|
+
} else {
|
|
119
|
+
// 不是插件目录,递归扫描子目录
|
|
120
|
+
const subNames = scanPluginNames(fullPath, baseDir);
|
|
121
|
+
subNames.forEach(n => names.add(n));
|
|
122
|
+
}
|
|
110
123
|
} else if (entry.isFile() && entry.name.endsWith('.js')) {
|
|
111
124
|
// 单文件插件(排除与文件夹同名的)
|
|
112
125
|
const baseName = entry.name.replace(/\.js$/, '');
|
|
113
126
|
if (!names.has(baseName)) {
|
|
114
|
-
|
|
127
|
+
const relativePath = path.relative(baseDir, fullPath);
|
|
128
|
+
names.add(relativePath.replace(/\\/g, '/').replace(/\.js$/, ''));
|
|
115
129
|
}
|
|
116
130
|
}
|
|
117
131
|
}
|
package/subagent.md
CHANGED
|
@@ -111,7 +111,7 @@
|
|
|
111
111
|
## 【Extensions】扩展插件
|
|
112
112
|
|
|
113
113
|
**使用流程(必须按顺序执行):**
|
|
114
|
-
1. 调用 `ext_skill({ plugin: "<plugin_name>" })` 或 `
|
|
114
|
+
1. 调用 `ext_skill({ plugin: "<plugin_name>" })` 或 `skill_load({ skill: "<skill_name>" })` 获取目标扩展的详细工具参数
|
|
115
115
|
2. 根据返回的参数定义,使用 `ext_call({ plugin, tool, args })` 调用扩展
|
|
116
116
|
|
|
117
117
|
> **警告**:禁止在未执行第1步获取参数的情况下直接调用 `ext_call`!
|
|
@@ -167,4 +167,4 @@ MCP (Model Context Protocol) 执行器
|
|
|
167
167
|
**工具:** `designmd_search_design_kits`, `designmd_get_design_kit`, `designmd_download_design_kit`, `designmd_upload_design_kit`, `designmd_delete_design_kit`, `designmd_list_popular_kits`, `designmd_list_tags`
|
|
168
168
|
|
|
169
169
|
## 禁止事项
|
|
170
|
-
- 不先调用 `ext_skill/
|
|
170
|
+
- 不先调用 `ext_skill/skill_load` 获取参数就直接使用 `ext_call`
|