foliko 1.1.67 → 1.1.69
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 +19 -10
- package/.dockerignore +45 -45
- package/.env.example +56 -56
- package/CLAUDE.md +2 -2
- package/README.md +13 -13
- package/SPEC.md +3 -3
- package/cli/src/commands/chat.js +2 -20
- package/cli/src/commands/list.js +7 -6
- package/cli/src/commands/plugin.js +3 -2
- package/cli/src/daemon.js +2 -2
- package/cli/src/ui/chat-ui-old.js +15 -4
- package/cli/src/ui/chat-ui.js +236 -203
- package/cli/src/ui/footer-bar.js +20 -46
- package/cli/src/ui/message-bubble.js +24 -2
- package/cli/src/ui/status-bar.js +177 -0
- package/cli/src/utils/config.js +29 -0
- package/cli/src/utils/plugin-config.js +1 -1
- package/docker-compose.yml +33 -33
- package/docs/features.md +120 -120
- package/docs/quick-reference.md +160 -160
- package/docs/user-manual.md +1391 -1391
- package/examples/ambient-example.js +2 -2
- package/examples/bootstrap.js +3 -3
- package/examples/test-chat.js +1 -1
- package/examples/test-reload.js +1 -1
- package/examples/test-telegram.js +1 -1
- package/examples/test-tg-bot.js +1 -1
- package/examples/test-tg-simple.js +2 -2
- package/examples/test-tg.js +1 -1
- package/examples/test-think.js +1 -1
- package/examples/test-weixin-feishu.js +3 -3
- package/package.json +3 -1
- package/plugins/ambient-agent/index.js +1 -1
- package/plugins/audit-plugin.js +84 -29
- package/plugins/coordinator-plugin.js +14 -12
- package/plugins/data-splitter-plugin.js +323 -0
- package/plugins/default-plugins.js +23 -12
- package/plugins/email/index.js +1 -1
- package/plugins/extension-executor-plugin.js +87 -9
- package/plugins/feishu-plugin.js +118 -16
- package/plugins/file-system-plugin.js +68 -50
- package/plugins/gate-trading.js +10 -10
- package/plugins/install-plugin.js +7 -7
- package/plugins/memory-plugin.js +9 -12
- package/plugins/plugin-manager-plugin.js +12 -14
- package/plugins/python-executor-plugin.js +1 -1
- package/plugins/python-plugin-loader.js +1 -1
- package/plugins/qq-plugin.js +151 -24
- package/plugins/rules-plugin.js +8 -8
- package/plugins/scheduler-plugin.js +24 -20
- package/plugins/session-plugin.js +313 -397
- package/plugins/storage-plugin.js +235 -175
- package/plugins/subagent-plugin.js +17 -13
- package/plugins/telegram-plugin.js +116 -17
- package/plugins/think-plugin.js +64 -60
- package/plugins/tools-plugin.js +8 -8
- package/plugins/web-plugin.js +2 -2
- package/plugins/weixin-plugin.js +107 -24
- package/skills/find-skills/AGENTS.md +2 -2
- package/skills/find-skills/SKILL.md +133 -133
- package/skills/foliko-dev/AGENTS.md +236 -236
- package/skills/foliko-dev/SKILL.md +19 -19
- package/skills/mcp-usage/SKILL.md +200 -200
- package/skills/plugin-guide/SKILL.md +4 -4
- package/skills/python-plugin-dev/SKILL.md +5 -5
- package/skills/skill-guide/SKILL.md +104 -6
- package/skills/subagent-guide/SKILL.md +237 -237
- package/skills/workflow-guide/SKILL.md +646 -646
- package/src/capabilities/skill-manager.js +124 -17
- package/src/capabilities/workflow-engine.js +3 -3
- package/src/core/agent-chat.js +72 -26
- package/src/core/agent.js +17 -27
- package/src/core/branch-summary-auto.js +206 -0
- package/src/core/chat-session.js +45 -169
- package/src/core/command-registry.js +200 -0
- package/src/core/constants.js +198 -0
- package/src/core/context-compressor.js +702 -326
- package/src/core/context-manager.js +0 -1
- package/src/core/enhanced-context-compressor.js +210 -0
- package/src/core/framework.js +260 -84
- package/src/core/jsonl-storage.js +253 -0
- package/src/core/plugin-base.js +7 -5
- package/src/core/plugin-manager.js +15 -10
- package/src/core/provider-registry.js +159 -0
- package/src/core/provider.js +2 -0
- package/src/core/session-entry.js +225 -0
- package/src/core/session-manager.js +701 -0
- package/src/core/storage-manager.js +494 -0
- package/src/core/sub-agent-config.js +1 -1
- package/src/core/subagent.js +16 -135
- package/src/core/token-counter.js +177 -58
- package/src/core/tool-executor.js +2 -70
- package/src/core/ui-extension-context.js +174 -0
- package/src/executors/mcp-executor.js +27 -16
- package/src/utils/chat-queue.js +11 -22
- package/src/utils/data-splitter.js +345 -0
- package/src/utils/logger.js +152 -180
- package/src/utils/message-validator.js +283 -0
- package/src/utils/plugin-helpers.js +2 -2
- package/src/utils/retry.js +168 -22
- package/website_v2/docs/api.html +1 -1
- package/website_v2/docs/configuration.html +2 -2
- package/website_v2/docs/plugin-development.html +4 -4
- package/website_v2/docs/project-structure.html +2 -2
- package/website_v2/docs/skill-development.html +2 -2
- package/website_v2/index.html +1 -1
- package/website_v2/styles/animations.css +7 -7
- package/.agent/agents/backend-dev.md +0 -102
- package/.agent/agents/data-analyst.md +0 -117
- package/.agent/agents/devops.md +0 -115
- package/.agent/agents/frontend-dev.md +0 -94
- package/.agent/agents/network-requester.md +0 -44
- package/.agent/agents/poster-designer.md +0 -52
- package/.agent/agents/product-manager.md +0 -85
- package/.agent/agents/qa-engineer.md +0 -100
- package/.agent/agents/security-engineer.md +0 -99
- package/.agent/agents/team-lead.md +0 -137
- package/.agent/agents/ui-designer.md +0 -116
- package/.agent/data/default.json +0 -58
- package/.agent/data/email/processed-emails.json +0 -1
- package/.agent/data/plugins-state.json +0 -199
- package/.agent/data/scheduler/tasks.json +0 -1
- package/.agent/data/web/web-config.json +0 -5
- package/.agent/data/weixin/images/file_1776188148383jpg +0 -0
- package/.agent/data/weixin/images/file_1776188458326.jpg +0 -0
- package/.agent/data/weixin/images/file_1776188689423.jpg +0 -0
- package/.agent/data/weixin/images/file_1776188813604.jpg +0 -0
- package/.agent/data/weixin/images/file_1776189097450.jpg +0 -0
- package/.agent/data/weixin/videos/file_1776188318431.mp4 +0 -0
- package/.agent/data/weixin.json +0 -6
- package/.agent/mcp_config.json +0 -14
- package/.agent/memory/user/mof6gk94-kneeuh.md +0 -9
- package/.agent/package.json +0 -8
- package/.agent/plugins/marknative/README.md +0 -134
- package/.agent/plugins/marknative/fonts/SegoeUI Emoji.ttf +0 -0
- package/.agent/plugins/marknative/fonts.zip +0 -0
- package/.agent/plugins/marknative/index.js +0 -256
- package/.agent/plugins/marknative/package.json +0 -12
- package/.agent/plugins/test-plugin.py +0 -99
- package/.agent/plugins.json +0 -14
- package/.agent/python-scripts/test_sample.py +0 -24
- package/.agent/sessions/cli_default.json +0 -247
- package/.agent/skills/agent-browser/SKILL.md +0 -311
- package/.agent/skills/agent-browser/TEST_PLAN.md +0 -200
- package/.agent/skills/sysinfo/SKILL.md +0 -38
- package/.agent/skills/sysinfo/system-info.sh +0 -130
- package/.agent/skills/workflow/SKILL.md +0 -324
- package/.agent/test-agent.js +0 -35
- package/.agent/weixin.json +0 -6
- package/.agent/workflows/email-digest.json +0 -50
- package/.agent/workflows/file-backup.json +0 -21
- package/.agent/workflows/get-ip-notify.json +0 -32
- package/.agent/workflows/news-aggregator.json +0 -93
- package/.agent/workflows/news-dashboard-v2.json +0 -94
- package/.agent/workflows/notification-batch.json +0 -32
- package/src/core/session-context.js +0 -346
- package/src/core/session-storage.js +0 -295
package/src/utils/logger.js
CHANGED
|
@@ -1,19 +1,39 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Foliko Logger -
|
|
3
|
-
*
|
|
2
|
+
* Foliko Logger - 基于 Pino 的统一日志系统
|
|
3
|
+
* 支持日志级别、命名空间、格式化输出、文件轮转
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
ERROR: 3,
|
|
11
|
-
NONE: 4,
|
|
12
|
-
};
|
|
6
|
+
const pino = require('pino');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const os = require('os');
|
|
13
10
|
|
|
14
|
-
|
|
11
|
+
// 默认日志目录(在项目 .foliko/logs 下)
|
|
12
|
+
const DEFAULT_LOG_DIR = path.join(process.cwd(), '.foliko', 'logs');
|
|
15
13
|
|
|
16
|
-
//
|
|
14
|
+
// 创建日志目录
|
|
15
|
+
function ensureLogDir(logDir) {
|
|
16
|
+
if (!fs.existsSync(logDir)) {
|
|
17
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
18
|
+
}
|
|
19
|
+
return logDir;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// 获取日志文件路径
|
|
23
|
+
function getLogFilePath(logDir) {
|
|
24
|
+
ensureLogDir(logDir);
|
|
25
|
+
const date = new Date().toISOString().split('T')[0];
|
|
26
|
+
return path.join(logDir, `vb-agent-${date}.log`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// 当前日志文件路径
|
|
30
|
+
let currentLogFile = null;
|
|
31
|
+
let currentFileStream = null;
|
|
32
|
+
|
|
33
|
+
// 根 logger 引用
|
|
34
|
+
let rootLogger = null;
|
|
35
|
+
|
|
36
|
+
// 日志事件发射器
|
|
17
37
|
const logEmitter = {
|
|
18
38
|
_listeners: {},
|
|
19
39
|
on(event, listener) {
|
|
@@ -37,173 +57,129 @@ const logEmitter = {
|
|
|
37
57
|
}
|
|
38
58
|
};
|
|
39
59
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
this.colors = {
|
|
56
|
-
debug: '\x1b[36m', // 青色
|
|
57
|
-
info: '\x1b[32m', // 绿色
|
|
58
|
-
warn: '\x1b[33m', // 黄色
|
|
59
|
-
error: '\x1b[31m', // 红色
|
|
60
|
-
grey: '\x1b[90m', // 暗灰色
|
|
61
|
-
reset: '\x1b[0m',
|
|
62
|
-
dim: '\x1b[2m', // 暗灰色
|
|
63
|
-
};
|
|
60
|
+
// 初始化 logger
|
|
61
|
+
function initLogger(options = {}) {
|
|
62
|
+
const {
|
|
63
|
+
level = process.env.LOG_LEVEL || 'info',
|
|
64
|
+
logDir = DEFAULT_LOG_DIR,
|
|
65
|
+
name = 'vb-agent',
|
|
66
|
+
silent = false, // 是否静默(不输出到控制台)
|
|
67
|
+
} = options;
|
|
68
|
+
|
|
69
|
+
currentLogFile = getLogFilePath(logDir);
|
|
70
|
+
|
|
71
|
+
// 关闭旧的文件流
|
|
72
|
+
if (currentFileStream) {
|
|
73
|
+
currentFileStream.end();
|
|
74
|
+
currentFileStream = null;
|
|
64
75
|
}
|
|
65
76
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
77
|
+
// 如果是静默模式(CLI TUI),只创建文件写入流
|
|
78
|
+
if (silent) {
|
|
79
|
+
// 使用 pino.destination 直接写文件,不经过 stdout
|
|
80
|
+
currentFileStream = pino.destination({
|
|
81
|
+
dest: currentLogFile,
|
|
82
|
+
sync: false, // 异步写入
|
|
83
|
+
});
|
|
72
84
|
|
|
73
|
-
|
|
74
|
-
|
|
85
|
+
rootLogger = pino({
|
|
86
|
+
name,
|
|
87
|
+
level,
|
|
88
|
+
timestamp: pino.stdTimeFunctions.isoTime,
|
|
89
|
+
base: {
|
|
90
|
+
pid: process.pid,
|
|
91
|
+
hostname: os.hostname(),
|
|
92
|
+
},
|
|
93
|
+
}, currentFileStream);
|
|
94
|
+
|
|
95
|
+
return rootLogger;
|
|
75
96
|
}
|
|
76
97
|
|
|
77
|
-
|
|
78
|
-
|
|
98
|
+
// 非静默模式:同时输出到控制台和文件
|
|
99
|
+
currentFileStream = fs.createWriteStream(currentLogFile, { flags: 'a' });
|
|
79
100
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
enableTimestamp: this.enableTimestamp,
|
|
90
|
-
enableColors: this.enableColors,
|
|
91
|
-
});
|
|
92
|
-
}
|
|
101
|
+
rootLogger = pino({
|
|
102
|
+
name,
|
|
103
|
+
level,
|
|
104
|
+
timestamp: pino.stdTimeFunctions.isoTime,
|
|
105
|
+
base: {
|
|
106
|
+
pid: process.pid,
|
|
107
|
+
hostname: os.hostname(),
|
|
108
|
+
},
|
|
109
|
+
}, currentFileStream);
|
|
93
110
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
* @param {string|number} level
|
|
97
|
-
*/
|
|
98
|
-
setLevel(level) {
|
|
99
|
-
if (typeof level === 'string') {
|
|
100
|
-
this.level = LOG_LEVELS[level.toUpperCase()] ?? LOG_LEVELS.INFO;
|
|
101
|
-
} else {
|
|
102
|
-
this.level = level;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
111
|
+
return rootLogger;
|
|
112
|
+
}
|
|
105
113
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const grey = this.colors.grey;
|
|
114
|
+
// 获取根 logger(延迟初始化)
|
|
115
|
+
function getRootLogger() {
|
|
116
|
+
if (!rootLogger) {
|
|
117
|
+
// 默认静默模式(CLI 使用)
|
|
118
|
+
rootLogger = initLogger({ silent: true });
|
|
119
|
+
}
|
|
120
|
+
return rootLogger;
|
|
121
|
+
}
|
|
115
122
|
|
|
116
|
-
|
|
123
|
+
// Logger 类
|
|
124
|
+
class Logger {
|
|
125
|
+
constructor(options = {}) {
|
|
126
|
+
this.namespace = options.namespace || 'app';
|
|
127
|
+
this._logger = getRootLogger().child({ name: this.namespace });
|
|
128
|
+
}
|
|
117
129
|
|
|
118
|
-
|
|
119
|
-
|
|
130
|
+
child(namespace) {
|
|
131
|
+
return new Logger({ namespace: `${this.namespace}:${namespace}` });
|
|
132
|
+
}
|
|
120
133
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
134
|
+
// 事件监听支持
|
|
135
|
+
on(event, listener) {
|
|
136
|
+
logEmitter.on(event, listener);
|
|
137
|
+
}
|
|
126
138
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
} else {
|
|
131
|
-
parts.push(levelName.padEnd(5));
|
|
132
|
-
}
|
|
139
|
+
off(event, listener) {
|
|
140
|
+
logEmitter.off(event, listener);
|
|
141
|
+
}
|
|
133
142
|
|
|
134
|
-
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if (arg instanceof Error) {
|
|
139
|
-
return `${arg.message}\n${arg.stack}`;
|
|
140
|
-
}
|
|
141
|
-
if (typeof arg === 'object') {
|
|
142
|
-
try {
|
|
143
|
-
return JSON.stringify(arg, null, 2);
|
|
144
|
-
} catch {
|
|
145
|
-
return String(arg);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
return String(arg);
|
|
149
|
-
})
|
|
150
|
-
.join(' ');
|
|
151
|
-
|
|
152
|
-
parts.push(`${grey}${message}${reset}`);
|
|
153
|
-
return parts.join(' ');
|
|
143
|
+
trace(...args) {
|
|
144
|
+
const msg = args.join(' ');
|
|
145
|
+
this._logger.trace({ event: 'trace', msg }, msg);
|
|
146
|
+
logEmitter.emit('log', { level: 'TRACE', namespace: this.namespace, message: msg });
|
|
154
147
|
}
|
|
155
148
|
|
|
156
|
-
/**
|
|
157
|
-
* 调试日志
|
|
158
|
-
* @param {...any} args
|
|
159
|
-
*/
|
|
160
149
|
debug(...args) {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
150
|
+
const msg = args.join(' ');
|
|
151
|
+
this._logger.debug({ event: 'debug', msg }, msg);
|
|
152
|
+
logEmitter.emit('log', { level: 'DEBUG', namespace: this.namespace, message: msg });
|
|
165
153
|
}
|
|
166
154
|
|
|
167
|
-
/**
|
|
168
|
-
* 信息日志
|
|
169
|
-
* @param {...any} args
|
|
170
|
-
*/
|
|
171
155
|
info(...args) {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
logEmitter.emit('log', { level: 'INFO', namespace: this.namespace, message: args.join(' '), formatted });
|
|
176
|
-
}
|
|
156
|
+
const msg = args.join(' ');
|
|
157
|
+
this._logger.info({ event: 'info', msg }, msg);
|
|
158
|
+
logEmitter.emit('log', { level: 'INFO', namespace: this.namespace, message: msg });
|
|
177
159
|
}
|
|
178
160
|
|
|
179
|
-
/**
|
|
180
|
-
* 警告日志
|
|
181
|
-
* @param {...any} args
|
|
182
|
-
*/
|
|
183
161
|
warn(...args) {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
logEmitter.emit('log', { level: 'WARN', namespace: this.namespace, message: args.join(' '), formatted });
|
|
188
|
-
}
|
|
162
|
+
const msg = args.join(' ');
|
|
163
|
+
this._logger.warn({ event: 'warn', msg }, msg);
|
|
164
|
+
logEmitter.emit('log', { level: 'WARN', namespace: this.namespace, message: msg });
|
|
189
165
|
}
|
|
190
166
|
|
|
191
|
-
/**
|
|
192
|
-
* 错误日志
|
|
193
|
-
* @param {...any} args
|
|
194
|
-
*/
|
|
195
167
|
error(...args) {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
168
|
+
const msg = args.join(' ');
|
|
169
|
+
this._logger.error({ event: 'error', msg }, msg);
|
|
170
|
+
logEmitter.emit('log', { level: 'ERROR', namespace: this.namespace, message: msg });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
fatal(...args) {
|
|
174
|
+
const msg = args.join(' ');
|
|
175
|
+
this._logger.fatal({ event: 'fatal', msg }, msg);
|
|
176
|
+
logEmitter.emit('log', { level: 'FATAL', namespace: this.namespace, message: msg });
|
|
201
177
|
}
|
|
202
178
|
|
|
203
179
|
/**
|
|
204
|
-
*
|
|
180
|
+
* 性能计时
|
|
205
181
|
* @param {string} label
|
|
206
|
-
* @returns {function}
|
|
182
|
+
* @returns {function} 结束函数,调用后记录耗时
|
|
207
183
|
*/
|
|
208
184
|
timer(label) {
|
|
209
185
|
const start = process.hrtime.bigint();
|
|
@@ -215,41 +191,37 @@ class Logger {
|
|
|
215
191
|
}
|
|
216
192
|
}
|
|
217
193
|
|
|
218
|
-
//
|
|
219
|
-
function
|
|
220
|
-
|
|
221
|
-
getLogger._instance = new Logger({
|
|
222
|
-
namespace: 'foliko',
|
|
223
|
-
level: process.env.LOG_LEVEL ? LOG_LEVELS[process.env.LOG_LEVEL.toUpperCase()] : LOG_LEVELS.INFO,
|
|
224
|
-
});
|
|
225
|
-
}
|
|
226
|
-
return getLogger._instance;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// 一次绑定,消除每次访问 getter 重复 bind 的开销
|
|
230
|
-
const _globalLogger = getLogger();
|
|
231
|
-
|
|
232
|
-
// 允许动态设置全局日志级别
|
|
233
|
-
function setGlobalLogLevel(level) {
|
|
234
|
-
Logger.globalLevel = level;
|
|
235
|
-
if (_globalLogger) {
|
|
236
|
-
_globalLogger._level = level;
|
|
237
|
-
}
|
|
194
|
+
// 创建全局 logger
|
|
195
|
+
function createGlobalLogger() {
|
|
196
|
+
return new Logger({ namespace: 'foliko' });
|
|
238
197
|
}
|
|
239
198
|
|
|
240
199
|
// 导出
|
|
241
200
|
module.exports = {
|
|
242
201
|
Logger,
|
|
243
|
-
logger:
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
202
|
+
logger: createGlobalLogger(),
|
|
203
|
+
setGlobalLogLevel: (level) => {
|
|
204
|
+
if (rootLogger) {
|
|
205
|
+
rootLogger.level = level;
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
setSilent: () => {
|
|
209
|
+
rootLogger = null;
|
|
210
|
+
getRootLogger();
|
|
211
|
+
},
|
|
212
|
+
setConsole: () => {
|
|
213
|
+
rootLogger = null;
|
|
214
|
+
rootLogger = initLogger({ silent: false });
|
|
215
|
+
getRootLogger();
|
|
216
|
+
},
|
|
217
|
+
getLogFilePath: () => currentLogFile,
|
|
218
|
+
initLogger,
|
|
219
|
+
LOG_LEVELS: {
|
|
220
|
+
TRACE: 'trace',
|
|
221
|
+
DEBUG: 'debug',
|
|
222
|
+
INFO: 'info',
|
|
223
|
+
WARN: 'warn',
|
|
224
|
+
ERROR: 'error',
|
|
225
|
+
NONE: 'silent',
|
|
252
226
|
},
|
|
253
|
-
logEmitter,
|
|
254
|
-
LOG_LEVELS,
|
|
255
227
|
};
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 消息验证工具
|
|
3
|
+
* 统一管理 tool-call / tool-result 配对验证
|
|
4
|
+
*
|
|
5
|
+
* 解决的问题:
|
|
6
|
+
* - 消除 4 处重复的消息配对验证逻辑
|
|
7
|
+
* - 统一 tool-call 输入格式验证
|
|
8
|
+
* - 减少消息数组多次遍历
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { logger } = require('./logger');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 收集所有 assistant 消息中的 tool-call ID
|
|
15
|
+
* @param {Array} messages - 消息数组
|
|
16
|
+
* @returns {Set<string>} tool-call ID 集合
|
|
17
|
+
*/
|
|
18
|
+
function collectToolCallIds(messages) {
|
|
19
|
+
const ids = new Set();
|
|
20
|
+
for (const msg of messages) {
|
|
21
|
+
if (msg.role !== 'assistant') continue;
|
|
22
|
+
|
|
23
|
+
// 格式1: msg.content 数组中的 tool-call 块 (AI SDK 格式)
|
|
24
|
+
if (Array.isArray(msg.content)) {
|
|
25
|
+
for (const item of msg.content) {
|
|
26
|
+
if ((item.type === 'tool-call' || item.type === 'tool-use') && item.toolCallId) {
|
|
27
|
+
ids.add(item.toolCallId);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 格式2: msg.tool_calls 数组 (OpenAI 兼容格式)
|
|
33
|
+
if (Array.isArray(msg.tool_calls)) {
|
|
34
|
+
for (const tc of msg.tool_calls) {
|
|
35
|
+
if (tc.id) ids.add(tc.id);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return ids;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 验证消息配对 — 移除 orphaned tool-result(没有对应 tool-call 的)
|
|
44
|
+
* @param {Array} messages - 消息数组(会被修改)
|
|
45
|
+
* @param {Object} [options]
|
|
46
|
+
* @param {boolean} [options.log=true] - 是否记录日志
|
|
47
|
+
* @returns {Array} 过滤后的消息数组
|
|
48
|
+
*/
|
|
49
|
+
function validateMessagesPairing(messages, options = {}) {
|
|
50
|
+
const { log: shouldLog = true } = options;
|
|
51
|
+
const validToolCallIds = collectToolCallIds(messages);
|
|
52
|
+
|
|
53
|
+
let removedToolResultCount = 0;
|
|
54
|
+
|
|
55
|
+
for (const msg of messages) {
|
|
56
|
+
if (msg.role !== 'tool' || !Array.isArray(msg.content)) continue;
|
|
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;
|
|
68
|
+
}
|
|
69
|
+
return true;
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// content 全被删除了,标记整个消息待删除
|
|
73
|
+
if (msg.content.length === 0 && originalLength > 0) {
|
|
74
|
+
msg._orphaned = true;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const originalLen = messages.length;
|
|
79
|
+
const filtered = messages.filter((msg) => !(msg.role === 'tool' && msg._orphaned));
|
|
80
|
+
|
|
81
|
+
if (shouldLog && (removedToolResultCount > 0 || filtered.length !== originalLen)) {
|
|
82
|
+
logger.debug(
|
|
83
|
+
`[validateMessagesPairing] removed ${removedToolResultCount} orphaned tool-results,` +
|
|
84
|
+
` ${originalLen - filtered.length} orphaned tool messages`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return filtered;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 验证 tool-call 格式 — 修复不完整的 JSON 输入
|
|
93
|
+
* @param {Array} messages - 消息数组(会被修改)
|
|
94
|
+
* @param {Object} [options]
|
|
95
|
+
* @param {boolean} [options.log=true] - 是否记录日志
|
|
96
|
+
* @returns {number} 修复/移除的数量
|
|
97
|
+
*/
|
|
98
|
+
function validateToolCalls(messages, options = {}) {
|
|
99
|
+
const { log: shouldLog = true } = options;
|
|
100
|
+
let fixedCount = 0;
|
|
101
|
+
const invalidatedToolCallIds = new Set();
|
|
102
|
+
|
|
103
|
+
for (const msg of messages) {
|
|
104
|
+
if (msg.role !== 'assistant' || !Array.isArray(msg.content)) continue;
|
|
105
|
+
|
|
106
|
+
for (const item of msg.content) {
|
|
107
|
+
if (item.type !== 'tool-call' && item.type !== 'tool-use') continue;
|
|
108
|
+
|
|
109
|
+
const input = item.input;
|
|
110
|
+
if (typeof input !== 'string') continue;
|
|
111
|
+
|
|
112
|
+
const trimmed = input.trim();
|
|
113
|
+
if (trimmed === '{' || trimmed === '' || !trimmed.startsWith('{')) {
|
|
114
|
+
if (item.toolCallId) {
|
|
115
|
+
invalidatedToolCallIds.add(item.toolCallId);
|
|
116
|
+
}
|
|
117
|
+
if (shouldLog) {
|
|
118
|
+
logger.warn(
|
|
119
|
+
`[validateToolCalls] invalid input for "${item.toolName}", toolCallId=${item.toolCallId ||
|
|
120
|
+
'?'}, converting to text`
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
// 把无效的 tool-call 转换成 text 文本
|
|
124
|
+
item.type = 'text';
|
|
125
|
+
item.text = `(工具调用 ${item.toolName} 参数不完整,已跳过)`;
|
|
126
|
+
delete item.toolCallId;
|
|
127
|
+
delete item.toolName;
|
|
128
|
+
delete item.input;
|
|
129
|
+
fixedCount++;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 清理引用了无效 toolCallId 的 tool-result
|
|
135
|
+
if (invalidatedToolCallIds.size > 0) {
|
|
136
|
+
for (const msg of messages) {
|
|
137
|
+
if (msg.role !== 'tool' || !Array.isArray(msg.content)) continue;
|
|
138
|
+
|
|
139
|
+
msg.content = msg.content.filter((item) => {
|
|
140
|
+
if (
|
|
141
|
+
item &&
|
|
142
|
+
(item.type === 'tool-result' || item.type === 'tool_result') &&
|
|
143
|
+
item.toolCallId &&
|
|
144
|
+
invalidatedToolCallIds.has(item.toolCallId)
|
|
145
|
+
) {
|
|
146
|
+
fixedCount++;
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
return true;
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (shouldLog && fixedCount > 0) {
|
|
155
|
+
logger.info(`[validateToolCalls] Fixed ${fixedCount} incomplete tool calls/results`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return fixedCount;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* 一次遍历完成配对验证 + tool-call 格式验证
|
|
163
|
+
* 相比分别调用减少消息数组迭代次数
|
|
164
|
+
* @param {Array} messages - 消息数组(会被修改)
|
|
165
|
+
* @returns {Array} 过滤后的消息数组
|
|
166
|
+
*/
|
|
167
|
+
function validateAll(messages) {
|
|
168
|
+
// 1. 先修复 tool-call 格式(避免无效 ID 干扰后续配对验证)
|
|
169
|
+
validateToolCalls(messages);
|
|
170
|
+
// 2. 再移除 orphaned tool-result
|
|
171
|
+
return validateMessagesPairing(messages);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* 过滤消息,保留配对的 tool-call → tool-result 链条
|
|
176
|
+
* 用于上下文压缩后保留完整的工具调用链路
|
|
177
|
+
* @param {Array} messages - 消息数组
|
|
178
|
+
* @param {number} [keepRecentAssistant=3] - 保留最近几条无工具调用的 assistant 消息
|
|
179
|
+
* @returns {Array} 过滤后的消息数组
|
|
180
|
+
*/
|
|
181
|
+
function filterPairedMessages(messages, keepRecentAssistant = 3) {
|
|
182
|
+
// 第一遍:收集所有 tool-call 及对应的 result
|
|
183
|
+
const assistantToolCalls = new Map(); // toolCallId → assistant index
|
|
184
|
+
const toolResults = new Map(); // toolCallId → [result indices]
|
|
185
|
+
|
|
186
|
+
for (let i = 0; i < messages.length; i++) {
|
|
187
|
+
const msg = messages[i];
|
|
188
|
+
if (msg.role === 'assistant') {
|
|
189
|
+
const content = Array.isArray(msg.content) ? msg.content : [msg.content];
|
|
190
|
+
for (const item of content) {
|
|
191
|
+
if ((item.type === 'tool-call' || item.type === 'tool-use') && item.toolCallId) {
|
|
192
|
+
assistantToolCalls.set(item.toolCallId, i);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (Array.isArray(msg.tool_calls)) {
|
|
196
|
+
for (const tc of msg.tool_calls) {
|
|
197
|
+
if (tc.id) assistantToolCalls.set(tc.id, i);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (msg.role === 'tool' && Array.isArray(msg.content)) {
|
|
202
|
+
for (const item of msg.content) {
|
|
203
|
+
if ((item.type === 'tool-result' || item.type === 'tool_result') && item.toolCallId) {
|
|
204
|
+
if (!toolResults.has(item.toolCallId)) {
|
|
205
|
+
toolResults.set(item.toolCallId, []);
|
|
206
|
+
}
|
|
207
|
+
toolResults.get(item.toolCallId).push(i);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// 第二遍:决定哪些 assistant 消息需要保留
|
|
214
|
+
const assistantIndicesToKeep = new Set();
|
|
215
|
+
for (let i = 0; i < messages.length; i++) {
|
|
216
|
+
const msg = messages[i];
|
|
217
|
+
if (msg.role !== 'assistant') continue;
|
|
218
|
+
|
|
219
|
+
// 检查是否有 tool-call
|
|
220
|
+
let hasToolCall = false;
|
|
221
|
+
const content = Array.isArray(msg.content) ? msg.content : [msg.content];
|
|
222
|
+
for (const item of content) {
|
|
223
|
+
if (item.type === 'tool-call' || item.type === 'tool-use') {
|
|
224
|
+
hasToolCall = true;
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (!hasToolCall && Array.isArray(msg.tool_calls)) {
|
|
229
|
+
hasToolCall = msg.tool_calls.some((tc) => tc.id);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (hasToolCall) {
|
|
233
|
+
assistantIndicesToKeep.add(i);
|
|
234
|
+
} else if (i >= messages.length - keepRecentAssistant) {
|
|
235
|
+
// 保留最近几条非工具调用的 assistant 消息
|
|
236
|
+
assistantIndicesToKeep.add(i);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// 保存有 tool-call 但没有 result 的 assistant(防止切掉半截链条)
|
|
241
|
+
for (const [toolCallId, assistantIdx] of assistantToolCalls) {
|
|
242
|
+
if (!toolResults.has(toolCallId)) {
|
|
243
|
+
assistantIndicesToKeep.add(assistantIdx);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// 第三遍:决定哪些 tool-result 要保留(只保留有对应 assistant 的)
|
|
248
|
+
const indicesToKeep = new Set();
|
|
249
|
+
for (let i = 0; i < messages.length; i++) {
|
|
250
|
+
const msg = messages[i];
|
|
251
|
+
if (msg.role === 'tool') {
|
|
252
|
+
const content = Array.isArray(msg.content) ? msg.content : [msg.content];
|
|
253
|
+
const hasPairedAssistant = content.some(
|
|
254
|
+
(item) =>
|
|
255
|
+
(item.type === 'tool-result' || item.type === 'tool_result') &&
|
|
256
|
+
item.toolCallId &&
|
|
257
|
+
assistantToolCalls.has(item.toolCallId) &&
|
|
258
|
+
assistantIndicesToKeep.has(assistantToolCalls.get(item.toolCallId))
|
|
259
|
+
);
|
|
260
|
+
if (hasPairedAssistant) {
|
|
261
|
+
indicesToKeep.add(i);
|
|
262
|
+
}
|
|
263
|
+
} else if (msg.role === 'assistant') {
|
|
264
|
+
if (assistantIndicesToKeep.has(i)) {
|
|
265
|
+
indicesToKeep.add(i);
|
|
266
|
+
}
|
|
267
|
+
} else {
|
|
268
|
+
// user, system 等角色默认保留
|
|
269
|
+
indicesToKeep.add(i);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return Array.from(indicesToKeep)
|
|
274
|
+
.sort((a, b) => a - b)
|
|
275
|
+
.map((i) => messages[i]);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
module.exports = {
|
|
279
|
+
validateMessagesPairing,
|
|
280
|
+
validateToolCalls,
|
|
281
|
+
validateAll,
|
|
282
|
+
filterPairedMessages,
|
|
283
|
+
};
|
|
@@ -24,8 +24,8 @@ function isDirectory(filePath) {
|
|
|
24
24
|
/**
|
|
25
25
|
* 解析插件路径
|
|
26
26
|
* 支持两种结构:
|
|
27
|
-
* 1. 文件夹结构: .
|
|
28
|
-
* 2. 单文件结构: .
|
|
27
|
+
* 1. 文件夹结构: .foliko/plugins/my-plugin/index.js
|
|
28
|
+
* 2. 单文件结构: .foliko/plugins/my-plugin.js
|
|
29
29
|
* @param {string} pluginsDir - 插件目录
|
|
30
30
|
* @param {string} name - 插件名称
|
|
31
31
|
* @param {Object} options - 选项
|