foliko 2.0.5 → 2.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +4 -1
- package/.cli_default_systemPrompt.md +291 -0
- package/.editorconfig +56 -56
- package/.lintstagedrc +7 -7
- package/.prettierignore +29 -29
- package/.prettierrc +11 -11
- package/CLAUDE.md +3 -0
- package/Dockerfile +63 -63
- package/README.md +20 -3
- package/docs/architecture.md +34 -2
- package/docs/extensions.md +199 -0
- package/docs/migration.md +100 -0
- package/docs/public-api.md +280 -24
- package/docs/usage.md +122 -30
- package/install.ps1 +129 -129
- package/install.sh +121 -121
- package/package.json +1 -1
- package/plugins/core/audit/index.js +1 -1
- package/plugins/core/default/bootstrap.js +44 -19
- package/plugins/core/python-loader/index.js +43 -25
- package/plugins/core/skill-manager/PROMPT.md +6 -0
- package/plugins/core/skill-manager/index.js +402 -115
- package/plugins/core/sub-agent/PROMPT.md +10 -0
- package/plugins/core/sub-agent/index.js +36 -3
- package/plugins/core/think/index.js +1 -0
- package/plugins/core/workflow/index.js +82 -22
- package/plugins/executors/data-splitter/PROMPT.md +13 -0
- package/plugins/executors/data-splitter/index.js +5 -4
- package/plugins/executors/extension/extension-registry.js +145 -0
- package/plugins/executors/extension/index.js +405 -437
- package/plugins/executors/extension/prompt-builder.js +359 -0
- package/plugins/executors/extension/skill-helper.js +143 -0
- package/plugins/messaging/feishu/index.js +5 -3
- package/plugins/messaging/qq/index.js +6 -4
- package/plugins/messaging/telegram/index.js +6 -3
- package/plugins/messaging/weixin/index.js +5 -3
- package/plugins/tools/PROMPT.md +26 -0
- package/plugins/tools/index.js +6 -5
- package/skills/find-skills/SKILL.md +133 -133
- package/skills/foliko/AGENTS.md +196 -43
- package/skills/foliko/SKILL.md +157 -28
- package/skills/mcp/SKILL.md +77 -118
- package/skills/plugins/SKILL.md +89 -3
- package/skills/python/SKILL.md +57 -39
- package/skills/skill-guide/SKILL.md +42 -34
- package/skills/workflows/SKILL.md +224 -9
- package/skills/workflows/workflow-troubleshooting/SKILL.md +221 -281
- package/src/agent/chat.js +48 -27
- package/src/agent/main.js +34 -13
- package/src/agent/prompt-registry.js +56 -16
- package/src/agent/prompts/PROMPT.md +3 -0
- package/src/agent/sub.js +1 -1
- package/src/cli/ui/chat-ui-old.js +5 -2
- package/src/cli/ui/chat-ui.js +5 -2
- package/src/common/constants.js +12 -0
- package/src/common/error-capture.js +91 -0
- package/src/common/logger.js +2 -2
- package/src/context/compressor.js +6 -2
- package/src/executors/mcp-executor.js +105 -125
- package/src/framework/framework.js +23 -11
- package/src/index.js +4 -0
- package/src/plugin/base.js +908 -5
- package/src/plugin/manager.js +29 -8
- package/src/tool/schema.js +32 -9
- package/website/index.html +821 -0
package/src/agent/chat.js
CHANGED
|
@@ -16,6 +16,7 @@ const {
|
|
|
16
16
|
ToolLoopAgent,
|
|
17
17
|
isLoopFinished,
|
|
18
18
|
} = require('ai');
|
|
19
|
+
const { z } = require('zod');
|
|
19
20
|
const { autoSplitToolResult } = require('../../plugins/executors/data-splitter');
|
|
20
21
|
const { cleanResponse } = require('../utils');
|
|
21
22
|
const { ChatQueueManager } = require('../common/queue');
|
|
@@ -32,6 +33,8 @@ const {
|
|
|
32
33
|
DEFAULT_MAX_CONCURRENT,
|
|
33
34
|
DEFAULT_RETRY_ATTEMPTS,
|
|
34
35
|
DEFAULT_RETRY_DELAY_MS,
|
|
36
|
+
SIMPLE_COMPRESS_TOKEN_RATIO,
|
|
37
|
+
SMART_COMPRESS_TOKEN_RATIO,
|
|
35
38
|
} = require('../common/constants');
|
|
36
39
|
|
|
37
40
|
class AgentChatHandler extends EventEmitter {
|
|
@@ -135,26 +138,22 @@ class AgentChatHandler extends EventEmitter {
|
|
|
135
138
|
this._setupEventForwarding();
|
|
136
139
|
|
|
137
140
|
// 转发 queueManager 事件到 ChatSession(让 SessionScope 能收到 stream:chunk 和 queue:completed 等)
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
this.
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
this.queueManager.on('queue:failed',
|
|
145
|
-
this._chatSession.emit('queue:failed', data);
|
|
146
|
-
});
|
|
141
|
+
// 保存引用以便 destroy 时能正确移除
|
|
142
|
+
this._onStreamChunk = (data) => this._chatSession.emit('stream:chunk', data);
|
|
143
|
+
this._onQueueCompleted = (data) => this._chatSession.emit('queue:completed', data);
|
|
144
|
+
this._onQueueFailed = (data) => this._chatSession.emit('queue:failed', data);
|
|
145
|
+
this.queueManager.on('stream:chunk', this._onStreamChunk);
|
|
146
|
+
this.queueManager.on('queue:completed', this._onQueueCompleted);
|
|
147
|
+
this.queueManager.on('queue:failed', this._onQueueFailed);
|
|
147
148
|
|
|
148
149
|
// 转发 message:complete 和 message:error 到 ChatSession(让 SessionScope 能收到)
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
this.
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
this.on('message:error',
|
|
156
|
-
this._chatSession.emit('message:error', data);
|
|
157
|
-
});
|
|
150
|
+
// 保存引用以便 destroy 时能正确移除
|
|
151
|
+
this._onMessageStart = (data) => this._chatSession.emit('message:start', data);
|
|
152
|
+
this._onMessageComplete = (data) => this._chatSession.emit('message:complete', data);
|
|
153
|
+
this._onMessageError = (data) => this._chatSession.emit('message:error', data);
|
|
154
|
+
this.on('message:start', this._onMessageStart);
|
|
155
|
+
this.on('message:complete', this._onMessageComplete);
|
|
156
|
+
this.on('message:error', this._onMessageError);
|
|
158
157
|
|
|
159
158
|
// 设置消息处理器(使用 bind 保持 this 引用)
|
|
160
159
|
this._chatSession.setMessageProcessor(this._processMessage.bind(this));
|
|
@@ -476,7 +475,7 @@ class AgentChatHandler extends EventEmitter {
|
|
|
476
475
|
throw new Error('AI 客户端未配置');
|
|
477
476
|
}
|
|
478
477
|
this._validateToolCalls(messages);
|
|
479
|
-
const systemPrompt = framework.
|
|
478
|
+
const systemPrompt = framework.prompts.build();
|
|
480
479
|
//await fs.promises.writeFile(`.${sessionId}_systemPrompt.md`, systemPrompt); // 调试用:保存系统提示词
|
|
481
480
|
const tools = this._getAITools(aiTool);
|
|
482
481
|
const agent = new ToolLoopAgent({
|
|
@@ -628,6 +627,14 @@ class AgentChatHandler extends EventEmitter {
|
|
|
628
627
|
* 销毁
|
|
629
628
|
*/
|
|
630
629
|
destroy() {
|
|
630
|
+
// Remove listeners from queueManager
|
|
631
|
+
this.queueManager.off('stream:chunk', this._onStreamChunk);
|
|
632
|
+
this.queueManager.off('queue:completed', this._onQueueCompleted);
|
|
633
|
+
this.queueManager.off('queue:failed', this._onQueueFailed);
|
|
634
|
+
// Remove listeners from this (message forwarding)
|
|
635
|
+
this.off('message:start', this._onMessageStart);
|
|
636
|
+
this.off('message:complete', this._onMessageComplete);
|
|
637
|
+
this.off('message:error', this._onMessageError);
|
|
631
638
|
if (this._chatSession) {
|
|
632
639
|
this._chatSession.removeAllListeners();
|
|
633
640
|
}
|
|
@@ -659,7 +666,7 @@ class AgentChatHandler extends EventEmitter {
|
|
|
659
666
|
throw new Error('AI 客户端未配置');
|
|
660
667
|
}
|
|
661
668
|
this._validateToolCalls(messages);
|
|
662
|
-
const systemPrompt = framework.
|
|
669
|
+
const systemPrompt = framework.prompts.build();
|
|
663
670
|
const tools = this._getAITools(aiTool);
|
|
664
671
|
|
|
665
672
|
// DeepSeek thinking mode: 处理参数
|
|
@@ -763,7 +770,7 @@ class AgentChatHandler extends EventEmitter {
|
|
|
763
770
|
const toolsTokens = this._getCachedToolsTokens();
|
|
764
771
|
const systemPromptTokens = this._getCachedSystemPromptTokens();
|
|
765
772
|
const totalTokens = cachedMsgTokens + toolsTokens + systemPromptTokens;
|
|
766
|
-
const limit = this._maxContextTokens *
|
|
773
|
+
const limit = this._maxContextTokens * SIMPLE_COMPRESS_TOKEN_RATIO;
|
|
767
774
|
|
|
768
775
|
// logger.info(`BEFORE: messages=${messages.length}, tokens=${totalTokens}/${limit}`);
|
|
769
776
|
|
|
@@ -924,8 +931,8 @@ class AgentChatHandler extends EventEmitter {
|
|
|
924
931
|
name: toolName,
|
|
925
932
|
description: toolDef.description || '',
|
|
926
933
|
execute: async (args) => {
|
|
927
|
-
//
|
|
928
|
-
const cleanedArgs = this._cleanToolArgs(args);
|
|
934
|
+
// 清理参数并验证类型
|
|
935
|
+
const cleanedArgs = this._cleanToolArgs(args, toolDef.inputSchema);
|
|
929
936
|
|
|
930
937
|
// 执行工具
|
|
931
938
|
this.emit('tool-call', { name: toolName, args: cleanedArgs });
|
|
@@ -954,7 +961,7 @@ class AgentChatHandler extends EventEmitter {
|
|
|
954
961
|
} catch (err) {
|
|
955
962
|
this.emit('tool-error', { name: toolName, args: cleanedArgs, error: err.message });
|
|
956
963
|
// 返回错误信息字符串,而不是抛出异常
|
|
957
|
-
return { success: false,error: err.message}
|
|
964
|
+
return { success: false, error: err.message }
|
|
958
965
|
}
|
|
959
966
|
},
|
|
960
967
|
};
|
|
@@ -976,10 +983,13 @@ class AgentChatHandler extends EventEmitter {
|
|
|
976
983
|
}
|
|
977
984
|
|
|
978
985
|
/**
|
|
979
|
-
*
|
|
986
|
+
* 清理工具参数并根据 inputSchema 验证类型
|
|
980
987
|
* @private
|
|
988
|
+
* @param {Object} args - 工具参数
|
|
989
|
+
* @param {Object} inputSchema - Zod schema (optional)
|
|
990
|
+
* @returns {Object} 清理后的参数
|
|
981
991
|
*/
|
|
982
|
-
_cleanToolArgs(args) {
|
|
992
|
+
_cleanToolArgs(args, inputSchema) {
|
|
983
993
|
if (!args || typeof args !== 'object') {
|
|
984
994
|
return {};
|
|
985
995
|
}
|
|
@@ -991,6 +1001,17 @@ class AgentChatHandler extends EventEmitter {
|
|
|
991
1001
|
}
|
|
992
1002
|
cleaned[key] = value;
|
|
993
1003
|
}
|
|
1004
|
+
|
|
1005
|
+
// Validate against inputSchema if provided and is a Zod schema
|
|
1006
|
+
if (inputSchema && inputSchema._def && inputSchema.parse) {
|
|
1007
|
+
try {
|
|
1008
|
+
return inputSchema.parse(cleaned);
|
|
1009
|
+
} catch (err) {
|
|
1010
|
+
// If Zod validation fails, return cleaned args (backward compat)
|
|
1011
|
+
logger.warn(`[Tool] Args validation failed: ${err.message}`);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
|
|
994
1015
|
return cleaned;
|
|
995
1016
|
}
|
|
996
1017
|
|
|
@@ -1002,7 +1023,7 @@ class AgentChatHandler extends EventEmitter {
|
|
|
1002
1023
|
return async ({ stepNumber, messages: inputMessages }) => {
|
|
1003
1024
|
try {
|
|
1004
1025
|
const tokenCount = this._countMessagesTokens(inputMessages);
|
|
1005
|
-
const tokenLimit = this._maxContextTokens *
|
|
1026
|
+
const tokenLimit = this._maxContextTokens * SMART_COMPRESS_TOKEN_RATIO;
|
|
1006
1027
|
|
|
1007
1028
|
// 超过限制时,保留三分之一的消息
|
|
1008
1029
|
if (tokenCount > tokenLimit) {
|
package/src/agent/main.js
CHANGED
|
@@ -99,7 +99,29 @@ class MainAgent extends BaseAgent {
|
|
|
99
99
|
}
|
|
100
100
|
return metaParts.join('\n');
|
|
101
101
|
}, 'USER');
|
|
102
|
-
|
|
102
|
+
|
|
103
|
+
// capabilities 从文件加载(动态内容通过 interpolate 注入)
|
|
104
|
+
const capsProvider = () => {
|
|
105
|
+
const list = this._buildCapabilitiesList();
|
|
106
|
+
return list ? `## 系统能力\n\n${list}` : '';
|
|
107
|
+
};
|
|
108
|
+
registerBoth('capabilities', PROMPT_PRIORITY.CAPABILITIES, capsProvider, 'CAPABILITY');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
_buildCapabilitiesList() {
|
|
112
|
+
const plugins = this.framework.pluginManager.getAll();
|
|
113
|
+
if (!plugins || plugins.length === 0) return '';
|
|
114
|
+
const keyPlugins = plugins.filter(p => p.name !== 'defaults' && p.name !== 'agent');
|
|
115
|
+
if (keyPlugins.length === 0) return '';
|
|
116
|
+
|
|
117
|
+
const lines = [];
|
|
118
|
+
keyPlugins.forEach((plugin) => {
|
|
119
|
+
const name = plugin.instance?.name || plugin.name || 'unknown';
|
|
120
|
+
const description = plugin.instance?.description || '无描述';
|
|
121
|
+
lines.push(`- **${name}**:${description}`);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
return lines.join('\n');
|
|
103
125
|
}
|
|
104
126
|
|
|
105
127
|
_getMetadataValue(key) {
|
|
@@ -156,27 +178,26 @@ class MainAgent extends BaseAgent {
|
|
|
156
178
|
_buildSystemPrompt() {
|
|
157
179
|
if (!this._systemPromptDirty && this._systemPromptCache !== null) return this._systemPromptCache;
|
|
158
180
|
|
|
159
|
-
//
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
// 2. 全局注册表(skills, tools, extensions 等共享部分,跳过 _main 的 original-prompt)
|
|
163
|
-
const reg = this.framework?.promptRegistry;
|
|
164
|
-
if (reg && reg.count() > 0) {
|
|
181
|
+
// 统一走 framework.prompts.build():自动包含 _originalPrompt + 注册表所有 parts
|
|
182
|
+
// (PromptRegistry.build() 已处理两段拼接,PromptPart 自带 _dirty 缓存)
|
|
183
|
+
if (this.framework?.prompts?.build) {
|
|
165
184
|
try {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
185
|
+
const text = this.framework.prompts.build();
|
|
186
|
+
if (text) {
|
|
187
|
+
this._systemPromptCache = text;
|
|
188
|
+
this._systemPromptDirty = false;
|
|
189
|
+
return this._systemPromptCache;
|
|
170
190
|
}
|
|
171
191
|
} catch (err) {
|
|
172
|
-
//
|
|
192
|
+
// 降级到旧 builder
|
|
173
193
|
this._systemPromptCache = this._systemPromptBuilder.build();
|
|
174
194
|
this._systemPromptDirty = false;
|
|
175
195
|
return this._systemPromptCache;
|
|
176
196
|
}
|
|
177
197
|
}
|
|
178
198
|
|
|
179
|
-
|
|
199
|
+
// 没有 framework 时:只返回 _originalPrompt
|
|
200
|
+
this._systemPromptCache = this._originalPrompt || '';
|
|
180
201
|
this._systemPromptDirty = false;
|
|
181
202
|
return this._systemPromptCache;
|
|
182
203
|
}
|
|
@@ -57,12 +57,18 @@ class PromptPart {
|
|
|
57
57
|
* PromptRegistry 主类
|
|
58
58
|
*/
|
|
59
59
|
class PromptRegistry extends EventEmitter {
|
|
60
|
-
constructor() {
|
|
60
|
+
constructor(framework = null) {
|
|
61
61
|
super();
|
|
62
62
|
// key = "owner::name"
|
|
63
63
|
this._parts = new Map();
|
|
64
64
|
// 用于 build 时检测循环依赖
|
|
65
65
|
this._building = false;
|
|
66
|
+
// 关联 framework(用于 build 时自动追加 main agent 的 originalPrompt)
|
|
67
|
+
this._framework = framework;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
setFramework(framework) {
|
|
71
|
+
this._framework = framework;
|
|
66
72
|
}
|
|
67
73
|
|
|
68
74
|
// ==================== 注册/注销 ====================
|
|
@@ -99,10 +105,12 @@ class PromptRegistry extends EventEmitter {
|
|
|
99
105
|
* 注册一个 prompt part(从文件读取,支持变量插值)
|
|
100
106
|
* @param {string} owner 拥有者标识
|
|
101
107
|
* @param {string} filePath 文件路径
|
|
102
|
-
* @param {Object} [vars]
|
|
108
|
+
* @param {Object|Function} [vars] 变量对象或函数,模板中用 {{varName}} 引用
|
|
109
|
+
* - Object: 静态变量,注册时替换
|
|
110
|
+
* - Function: 动态变量,每次渲染时调用函数获取最新值
|
|
103
111
|
* @param {Object} [options] 同 register 选项
|
|
104
112
|
*/
|
|
105
|
-
registerFile(owner, filePath, vars =
|
|
113
|
+
registerFile(owner, filePath, vars = null, options = {}) {
|
|
106
114
|
if (!owner || typeof owner !== 'string') {
|
|
107
115
|
throw new Error('PromptRegistry.registerFile: owner is required');
|
|
108
116
|
}
|
|
@@ -115,20 +123,24 @@ class PromptRegistry extends EventEmitter {
|
|
|
115
123
|
if (!fs.existsSync(fullPath)) {
|
|
116
124
|
throw new Error(`PromptRegistry.registerFile: file not found: ${fullPath}`);
|
|
117
125
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
//
|
|
121
|
-
|
|
122
|
-
content =
|
|
123
|
-
|
|
124
|
-
|
|
126
|
+
const rawContent = fs.readFileSync(fullPath, 'utf-8');
|
|
127
|
+
|
|
128
|
+
// 转为 provider,渲染时动态插值
|
|
129
|
+
const provider = () => {
|
|
130
|
+
let content = rawContent;
|
|
131
|
+
if (vars) {
|
|
132
|
+
const varsObj = typeof vars === 'function' ? vars() : vars;
|
|
133
|
+
if (varsObj && typeof varsObj === 'object') {
|
|
134
|
+
content = content.replace(/\{\{(\w+)\}\}/g, (match, varName) => {
|
|
135
|
+
if (Object.prototype.hasOwnProperty.call(varsObj, varName)) {
|
|
136
|
+
return String(varsObj[varName]);
|
|
137
|
+
}
|
|
138
|
+
return match; // 未找到的变量保留原样
|
|
139
|
+
});
|
|
125
140
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// 转为 provider
|
|
131
|
-
const provider = () => content;
|
|
141
|
+
}
|
|
142
|
+
return content;
|
|
143
|
+
};
|
|
132
144
|
|
|
133
145
|
// 从文件名生成 name
|
|
134
146
|
const name = options.name || path.basename(filePath, path.extname(filePath));
|
|
@@ -246,6 +258,7 @@ class PromptRegistry extends EventEmitter {
|
|
|
246
258
|
|
|
247
259
|
/**
|
|
248
260
|
* 渲染完整 prompt(按 priority 拼接)
|
|
261
|
+
* 自动包含 main agent 的 originalPrompt(作为首段)
|
|
249
262
|
*/
|
|
250
263
|
build() {
|
|
251
264
|
return this.buildForAgent(null);
|
|
@@ -253,6 +266,9 @@ class PromptRegistry extends EventEmitter {
|
|
|
253
266
|
|
|
254
267
|
/**
|
|
255
268
|
* 渲染适用于指定 agent 的 prompt
|
|
269
|
+
* - main agent (agentId 为 null 或 'main'):自动在首段追加 `_originalPrompt`
|
|
270
|
+
* - 其他 agent:不追加(subagent 自己有 _customSystemPrompt)
|
|
271
|
+
*
|
|
256
272
|
* @param {string|null} agentId agent ID
|
|
257
273
|
*/
|
|
258
274
|
buildForAgent(agentId = null) {
|
|
@@ -262,6 +278,17 @@ class PromptRegistry extends EventEmitter {
|
|
|
262
278
|
this._building = true;
|
|
263
279
|
try {
|
|
264
280
|
const sections = [];
|
|
281
|
+
|
|
282
|
+
// 1. main agent 的 originalPrompt 作为首段(per-Agent,identity)
|
|
283
|
+
const isMain = agentId === null || agentId === 'main';
|
|
284
|
+
if (isMain) {
|
|
285
|
+
const originalPrompt = this._getMainAgentOriginalPrompt();
|
|
286
|
+
if (originalPrompt && originalPrompt.trim()) {
|
|
287
|
+
sections.push(originalPrompt.trim());
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// 2. 全局注册表的所有 parts
|
|
265
292
|
for (const part of this.listForAgent(agentId)) {
|
|
266
293
|
const content = part.get();
|
|
267
294
|
if (content && content.trim()) sections.push(content.trim());
|
|
@@ -272,6 +299,19 @@ class PromptRegistry extends EventEmitter {
|
|
|
272
299
|
}
|
|
273
300
|
}
|
|
274
301
|
|
|
302
|
+
/**
|
|
303
|
+
* 获取 main agent 的 originalPrompt
|
|
304
|
+
*/
|
|
305
|
+
_getMainAgentOriginalPrompt() {
|
|
306
|
+
const main = this._framework?.getMainAgent?.();
|
|
307
|
+
if (!main) return '';
|
|
308
|
+
// 优先用 _originalPrompt(per-Agent 私有),回退到 getOriginalPrompt()
|
|
309
|
+
if (typeof main.getOriginalPrompt === 'function') {
|
|
310
|
+
return main.getOriginalPrompt() || main._originalPrompt || '';
|
|
311
|
+
}
|
|
312
|
+
return main._originalPrompt || '';
|
|
313
|
+
}
|
|
314
|
+
|
|
275
315
|
/**
|
|
276
316
|
* build() 的别名
|
|
277
317
|
*/
|
package/src/agent/sub.js
CHANGED
|
@@ -27,7 +27,7 @@ class SubAgent extends BaseAgent {
|
|
|
27
27
|
this.baseURL = config.baseURL;
|
|
28
28
|
this.providerOptions = config.providerOptions || {};
|
|
29
29
|
|
|
30
|
-
this.defaulTools = ['ext_skill', 'skill_load', 'ext_call', 'subagent_call'
|
|
30
|
+
this.defaulTools = ['ext_skill', 'skill_load', 'ext_call', 'subagent_call'];
|
|
31
31
|
this.bindTools = {
|
|
32
32
|
read: 'read_file', write: 'write_file', edit: 'modify_file',
|
|
33
33
|
glob: 'read_directory', grep: 'search_file', bash: 'shell_exec',
|
|
@@ -129,10 +129,13 @@ class ChatUIOld {
|
|
|
129
129
|
if (trimmed.startsWith('/') && trimmed.includes(':')) {
|
|
130
130
|
const cmdName = trimmed.slice(1).split(' ')[0];
|
|
131
131
|
const cmdArgs = trimmed.slice(cmdName.length + 2);
|
|
132
|
+
const [skillName, ...rest] = cmdName.split(':');
|
|
133
|
+
const commandName = rest.join(':');
|
|
132
134
|
try {
|
|
135
|
+
// 新格式:plugin = `skill:<name>`, tool = command name
|
|
133
136
|
const result = await this.agent.framework.executeTool('ext_call', {
|
|
134
|
-
plugin:
|
|
135
|
-
tool:
|
|
137
|
+
plugin: `skill:${skillName}`,
|
|
138
|
+
tool: commandName,
|
|
136
139
|
args: { command: cmdArgs }
|
|
137
140
|
});
|
|
138
141
|
if (result.success !== false) {
|
package/src/cli/ui/chat-ui.js
CHANGED
|
@@ -374,10 +374,13 @@ class ChatUI {
|
|
|
374
374
|
if (trimmed.startsWith('/') && cmdName) {
|
|
375
375
|
// 检查 skill 命令 (格式: skillname:cmdname)
|
|
376
376
|
if (cmdName.includes(':')) {
|
|
377
|
+
const [skillName, ...rest] = cmdName.split(':');
|
|
378
|
+
const commandName = rest.join(':');
|
|
377
379
|
try {
|
|
380
|
+
// 新格式:plugin = `skill:<name>`, tool = command name
|
|
378
381
|
const result = await this.agent.framework.executeTool('ext_call', {
|
|
379
|
-
plugin:
|
|
380
|
-
tool:
|
|
382
|
+
plugin: `skill:${skillName}`,
|
|
383
|
+
tool: commandName,
|
|
381
384
|
args: { command: cmdArgs }
|
|
382
385
|
});
|
|
383
386
|
if (result.success !== false) {
|
package/src/common/constants.js
CHANGED
|
@@ -53,6 +53,9 @@ const DEFAULT_MAX_MESSAGES_PER_SESSION = 1000;
|
|
|
53
53
|
/** 保留最近的 N 条消息 */
|
|
54
54
|
const KEEP_RECENT_MESSAGES = 20;
|
|
55
55
|
|
|
56
|
+
/** 压缩消息数量阈值 */
|
|
57
|
+
const COMPRESSION_MESSAGE_THRESHOLD = 200;
|
|
58
|
+
|
|
56
59
|
/** 智能压缩启用 */
|
|
57
60
|
const ENABLE_SMART_COMPRESS = true;
|
|
58
61
|
|
|
@@ -191,6 +194,12 @@ const TOKENS_PER_TOOL = 500;
|
|
|
191
194
|
/** 每消息固定开销 token */
|
|
192
195
|
const TOKENS_PER_MESSAGE_OVERHEAD = 4;
|
|
193
196
|
|
|
197
|
+
/** 简单压缩模式 token 限制比例(50%) */
|
|
198
|
+
const SIMPLE_COMPRESS_TOKEN_RATIO = 0.5;
|
|
199
|
+
|
|
200
|
+
/** 智能压缩模式 token 限制比例(75%) */
|
|
201
|
+
const SMART_COMPRESS_TOKEN_RATIO = 0.75;
|
|
202
|
+
|
|
194
203
|
// ============================================================================
|
|
195
204
|
// 导出
|
|
196
205
|
// ============================================================================
|
|
@@ -211,6 +220,7 @@ module.exports = {
|
|
|
211
220
|
|
|
212
221
|
// Compression
|
|
213
222
|
KEEP_RECENT_MESSAGES,
|
|
223
|
+
COMPRESSION_MESSAGE_THRESHOLD,
|
|
214
224
|
ENABLE_SMART_COMPRESS,
|
|
215
225
|
COMPRESSION_TIMEOUT_MS,
|
|
216
226
|
MAX_TOOL_RESULT_SIZE,
|
|
@@ -244,4 +254,6 @@ module.exports = {
|
|
|
244
254
|
TOKENS_PER_CHAR,
|
|
245
255
|
TOKENS_PER_TOOL,
|
|
246
256
|
TOKENS_PER_MESSAGE_OVERHEAD,
|
|
257
|
+
SIMPLE_COMPRESS_TOKEN_RATIO,
|
|
258
|
+
SMART_COMPRESS_TOKEN_RATIO,
|
|
247
259
|
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 全局错误捕获 — 把 uncaughtException 和 unhandledRejection 的完整堆栈落盘
|
|
5
|
+
* 下次出现 Cannot read properties of undefined (reading 'delete') 这类异步错误,
|
|
6
|
+
* 用户直接看 .foliko/error.log 即可拿到带文件名/行号的完整堆栈,无需复现。
|
|
7
|
+
*
|
|
8
|
+
* 设计要点:
|
|
9
|
+
* - 静默写文件,不污染 console(避免和 TUI / CLI 抢输出)
|
|
10
|
+
* - 同时保留原始 process 监听器(若用户已注册),不会覆盖
|
|
11
|
+
* - 一次安装,重复 require 不会重复绑定
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
|
|
17
|
+
const INSTALLED = Symbol.for('foliko.error-capture.installed');
|
|
18
|
+
const PROJECT_LOG = path.join(process.cwd(), '.foliko', 'error.log');
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 获取当前活跃的 sessionId (兼容 AsyncLocalStorage)
|
|
22
|
+
*/
|
|
23
|
+
function getActiveSessionId() {
|
|
24
|
+
try {
|
|
25
|
+
const { requestContext, sessionContext } = require('./context-helpers');
|
|
26
|
+
if (typeof sessionContext?.getStore === 'function') {
|
|
27
|
+
const store = sessionContext.getStore();
|
|
28
|
+
if (store?.sessionId) return store.sessionId;
|
|
29
|
+
}
|
|
30
|
+
} catch (_) { /* 未安装 context 系统时跳过 */ }
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 把一个事件载荷写为单条记录到 .foliko/error.log
|
|
36
|
+
*/
|
|
37
|
+
function dumpEvent(kind, error) {
|
|
38
|
+
let record;
|
|
39
|
+
try {
|
|
40
|
+
const ts = new Date().toISOString();
|
|
41
|
+
const sid = getActiveSessionId();
|
|
42
|
+
const cwd = process.cwd();
|
|
43
|
+
const node = process.version;
|
|
44
|
+
const pid = process.pid;
|
|
45
|
+
|
|
46
|
+
const errObj = error instanceof Error ? error : new Error(String(error));
|
|
47
|
+
const stack = errObj.stack || `(no stack; value=${String(error)})`;
|
|
48
|
+
|
|
49
|
+
record =
|
|
50
|
+
`\n${'='.repeat(72)}\n` +
|
|
51
|
+
`[${ts}] [pid=${pid}] [node=${node}] [cwd=${cwd}] [sessionId=${sid || '(none)'}] kind=${kind}\n` +
|
|
52
|
+
`error.name=${errObj.name || 'Error'} error.message=${errObj.message || '(no message)'}\n` +
|
|
53
|
+
`stack:\n${stack}\n`;
|
|
54
|
+
} catch (innerErr) {
|
|
55
|
+
record = `\n[${new Date().toISOString()}] dump failed: ${innerErr?.message || innerErr}\n`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
fs.mkdirSync(path.dirname(PROJECT_LOG), { recursive: true });
|
|
60
|
+
fs.appendFileSync(PROJECT_LOG, record, 'utf8');
|
|
61
|
+
} catch (_) {
|
|
62
|
+
// 连日志都写不了就别折腾了
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 安装全局错误捕获。重复调用安全(no-op)。
|
|
68
|
+
*/
|
|
69
|
+
function install(options = {}) {
|
|
70
|
+
const logFile = options.logFile || PROJECT_LOG;
|
|
71
|
+
|
|
72
|
+
// 用 global symbol 标记,避免重复 require 时再次 process.on
|
|
73
|
+
if (process[INSTALLED]) return process[INSTALLED];
|
|
74
|
+
if (!process[INSTALLED]) process[INSTALLED] = new Set();
|
|
75
|
+
if (process[INSTALLED].has('core')) return 'core';
|
|
76
|
+
process[INSTALLED].add('core');
|
|
77
|
+
|
|
78
|
+
// 捕获 unhandledRejection —— 这正是用户报的 Promise 拒绝
|
|
79
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
80
|
+
dumpEvent('unhandledRejection', reason);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// 捕获 uncaughtException —— 同步抛错兜底
|
|
84
|
+
process.on('uncaughtException', (err, origin) => {
|
|
85
|
+
dumpEvent(`uncaughtException(${origin})`, err);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
return 'core';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
module.exports = { install, dumpEvent, PROJECT_LOG };
|
package/src/common/logger.js
CHANGED
|
@@ -23,7 +23,7 @@ function ensureLogDir(logDir) {
|
|
|
23
23
|
function getLogFilePath(logDir) {
|
|
24
24
|
ensureLogDir(logDir);
|
|
25
25
|
const date = new Date().toISOString().split('T')[0];
|
|
26
|
-
return path.join(logDir, `
|
|
26
|
+
return path.join(logDir, `foliko-${date}.log`);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
// 当前日志文件路径
|
|
@@ -62,7 +62,7 @@ function initLogger(options = {}) {
|
|
|
62
62
|
const {
|
|
63
63
|
level = process.env.LOG_LEVEL || 'info',
|
|
64
64
|
logDir = DEFAULT_LOG_DIR,
|
|
65
|
-
name = '
|
|
65
|
+
name = 'foliko',
|
|
66
66
|
silent = false, // 是否静默(不输出到控制台)
|
|
67
67
|
} = options;
|
|
68
68
|
|
|
@@ -19,6 +19,10 @@ const {
|
|
|
19
19
|
TURN_PREFIX_SUMMARIZATION_PROMPT, BRANCH_SUMMARY_PREAMBLE, BRANCH_SUMMARY_PROMPT,
|
|
20
20
|
DEFAULT_COMPACTION_SETTINGS, MODEL_CONTEXT_LIMITS, COMPRESSION_TIMEOUT_MS,
|
|
21
21
|
} = require('./compaction-prompts');
|
|
22
|
+
const {
|
|
23
|
+
KEEP_RECENT_MESSAGES,
|
|
24
|
+
COMPRESSION_MESSAGE_THRESHOLD,
|
|
25
|
+
} = require('../common/constants');
|
|
22
26
|
|
|
23
27
|
// ─── Helper classes ───────────────────────────────────────────────────
|
|
24
28
|
|
|
@@ -184,8 +188,8 @@ class ContextCompressor {
|
|
|
184
188
|
|
|
185
189
|
this.model = config.model || 'deepseek-chat';
|
|
186
190
|
this._maxContextTokens = config.maxContextTokens || this._getDefaultContextLimit();
|
|
187
|
-
this._keepRecentMessages = config.keepRecentMessages ||
|
|
188
|
-
this._compressionMessageThreshold = config.compressionMessageThreshold ||
|
|
191
|
+
this._keepRecentMessages = config.keepRecentMessages || KEEP_RECENT_MESSAGES;
|
|
192
|
+
this._compressionMessageThreshold = config.compressionMessageThreshold || COMPRESSION_MESSAGE_THRESHOLD;
|
|
189
193
|
this._enableSmartCompress = config.enableSmartCompress !== false;
|
|
190
194
|
this._compactionSettings = config.compactionSettings || DEFAULT_COMPACTION_SETTINGS;
|
|
191
195
|
|