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.
Files changed (157) hide show
  1. package/.claude/settings.local.json +19 -10
  2. package/.dockerignore +45 -45
  3. package/.env.example +56 -56
  4. package/CLAUDE.md +2 -2
  5. package/README.md +13 -13
  6. package/SPEC.md +3 -3
  7. package/cli/src/commands/chat.js +2 -20
  8. package/cli/src/commands/list.js +7 -6
  9. package/cli/src/commands/plugin.js +3 -2
  10. package/cli/src/daemon.js +2 -2
  11. package/cli/src/ui/chat-ui-old.js +15 -4
  12. package/cli/src/ui/chat-ui.js +236 -203
  13. package/cli/src/ui/footer-bar.js +20 -46
  14. package/cli/src/ui/message-bubble.js +24 -2
  15. package/cli/src/ui/status-bar.js +177 -0
  16. package/cli/src/utils/config.js +29 -0
  17. package/cli/src/utils/plugin-config.js +1 -1
  18. package/docker-compose.yml +33 -33
  19. package/docs/features.md +120 -120
  20. package/docs/quick-reference.md +160 -160
  21. package/docs/user-manual.md +1391 -1391
  22. package/examples/ambient-example.js +2 -2
  23. package/examples/bootstrap.js +3 -3
  24. package/examples/test-chat.js +1 -1
  25. package/examples/test-reload.js +1 -1
  26. package/examples/test-telegram.js +1 -1
  27. package/examples/test-tg-bot.js +1 -1
  28. package/examples/test-tg-simple.js +2 -2
  29. package/examples/test-tg.js +1 -1
  30. package/examples/test-think.js +1 -1
  31. package/examples/test-weixin-feishu.js +3 -3
  32. package/package.json +3 -1
  33. package/plugins/ambient-agent/index.js +1 -1
  34. package/plugins/audit-plugin.js +84 -29
  35. package/plugins/coordinator-plugin.js +14 -12
  36. package/plugins/data-splitter-plugin.js +323 -0
  37. package/plugins/default-plugins.js +23 -12
  38. package/plugins/email/index.js +1 -1
  39. package/plugins/extension-executor-plugin.js +87 -9
  40. package/plugins/feishu-plugin.js +118 -16
  41. package/plugins/file-system-plugin.js +68 -50
  42. package/plugins/gate-trading.js +10 -10
  43. package/plugins/install-plugin.js +7 -7
  44. package/plugins/memory-plugin.js +9 -12
  45. package/plugins/plugin-manager-plugin.js +12 -14
  46. package/plugins/python-executor-plugin.js +1 -1
  47. package/plugins/python-plugin-loader.js +1 -1
  48. package/plugins/qq-plugin.js +151 -24
  49. package/plugins/rules-plugin.js +8 -8
  50. package/plugins/scheduler-plugin.js +24 -20
  51. package/plugins/session-plugin.js +313 -397
  52. package/plugins/storage-plugin.js +235 -175
  53. package/plugins/subagent-plugin.js +17 -13
  54. package/plugins/telegram-plugin.js +116 -17
  55. package/plugins/think-plugin.js +64 -60
  56. package/plugins/tools-plugin.js +8 -8
  57. package/plugins/web-plugin.js +2 -2
  58. package/plugins/weixin-plugin.js +107 -24
  59. package/skills/find-skills/AGENTS.md +2 -2
  60. package/skills/find-skills/SKILL.md +133 -133
  61. package/skills/foliko-dev/AGENTS.md +236 -236
  62. package/skills/foliko-dev/SKILL.md +19 -19
  63. package/skills/mcp-usage/SKILL.md +200 -200
  64. package/skills/plugin-guide/SKILL.md +4 -4
  65. package/skills/python-plugin-dev/SKILL.md +5 -5
  66. package/skills/skill-guide/SKILL.md +104 -6
  67. package/skills/subagent-guide/SKILL.md +237 -237
  68. package/skills/workflow-guide/SKILL.md +646 -646
  69. package/src/capabilities/skill-manager.js +124 -17
  70. package/src/capabilities/workflow-engine.js +3 -3
  71. package/src/core/agent-chat.js +72 -26
  72. package/src/core/agent.js +17 -27
  73. package/src/core/branch-summary-auto.js +206 -0
  74. package/src/core/chat-session.js +45 -169
  75. package/src/core/command-registry.js +200 -0
  76. package/src/core/constants.js +198 -0
  77. package/src/core/context-compressor.js +702 -326
  78. package/src/core/context-manager.js +0 -1
  79. package/src/core/enhanced-context-compressor.js +210 -0
  80. package/src/core/framework.js +260 -84
  81. package/src/core/jsonl-storage.js +253 -0
  82. package/src/core/plugin-base.js +7 -5
  83. package/src/core/plugin-manager.js +15 -10
  84. package/src/core/provider-registry.js +159 -0
  85. package/src/core/provider.js +2 -0
  86. package/src/core/session-entry.js +225 -0
  87. package/src/core/session-manager.js +701 -0
  88. package/src/core/storage-manager.js +494 -0
  89. package/src/core/sub-agent-config.js +1 -1
  90. package/src/core/subagent.js +16 -135
  91. package/src/core/token-counter.js +177 -58
  92. package/src/core/tool-executor.js +2 -70
  93. package/src/core/ui-extension-context.js +174 -0
  94. package/src/executors/mcp-executor.js +27 -16
  95. package/src/utils/chat-queue.js +11 -22
  96. package/src/utils/data-splitter.js +345 -0
  97. package/src/utils/logger.js +152 -180
  98. package/src/utils/message-validator.js +283 -0
  99. package/src/utils/plugin-helpers.js +2 -2
  100. package/src/utils/retry.js +168 -22
  101. package/website_v2/docs/api.html +1 -1
  102. package/website_v2/docs/configuration.html +2 -2
  103. package/website_v2/docs/plugin-development.html +4 -4
  104. package/website_v2/docs/project-structure.html +2 -2
  105. package/website_v2/docs/skill-development.html +2 -2
  106. package/website_v2/index.html +1 -1
  107. package/website_v2/styles/animations.css +7 -7
  108. package/.agent/agents/backend-dev.md +0 -102
  109. package/.agent/agents/data-analyst.md +0 -117
  110. package/.agent/agents/devops.md +0 -115
  111. package/.agent/agents/frontend-dev.md +0 -94
  112. package/.agent/agents/network-requester.md +0 -44
  113. package/.agent/agents/poster-designer.md +0 -52
  114. package/.agent/agents/product-manager.md +0 -85
  115. package/.agent/agents/qa-engineer.md +0 -100
  116. package/.agent/agents/security-engineer.md +0 -99
  117. package/.agent/agents/team-lead.md +0 -137
  118. package/.agent/agents/ui-designer.md +0 -116
  119. package/.agent/data/default.json +0 -58
  120. package/.agent/data/email/processed-emails.json +0 -1
  121. package/.agent/data/plugins-state.json +0 -199
  122. package/.agent/data/scheduler/tasks.json +0 -1
  123. package/.agent/data/web/web-config.json +0 -5
  124. package/.agent/data/weixin/images/file_1776188148383jpg +0 -0
  125. package/.agent/data/weixin/images/file_1776188458326.jpg +0 -0
  126. package/.agent/data/weixin/images/file_1776188689423.jpg +0 -0
  127. package/.agent/data/weixin/images/file_1776188813604.jpg +0 -0
  128. package/.agent/data/weixin/images/file_1776189097450.jpg +0 -0
  129. package/.agent/data/weixin/videos/file_1776188318431.mp4 +0 -0
  130. package/.agent/data/weixin.json +0 -6
  131. package/.agent/mcp_config.json +0 -14
  132. package/.agent/memory/user/mof6gk94-kneeuh.md +0 -9
  133. package/.agent/package.json +0 -8
  134. package/.agent/plugins/marknative/README.md +0 -134
  135. package/.agent/plugins/marknative/fonts/SegoeUI Emoji.ttf +0 -0
  136. package/.agent/plugins/marknative/fonts.zip +0 -0
  137. package/.agent/plugins/marknative/index.js +0 -256
  138. package/.agent/plugins/marknative/package.json +0 -12
  139. package/.agent/plugins/test-plugin.py +0 -99
  140. package/.agent/plugins.json +0 -14
  141. package/.agent/python-scripts/test_sample.py +0 -24
  142. package/.agent/sessions/cli_default.json +0 -247
  143. package/.agent/skills/agent-browser/SKILL.md +0 -311
  144. package/.agent/skills/agent-browser/TEST_PLAN.md +0 -200
  145. package/.agent/skills/sysinfo/SKILL.md +0 -38
  146. package/.agent/skills/sysinfo/system-info.sh +0 -130
  147. package/.agent/skills/workflow/SKILL.md +0 -324
  148. package/.agent/test-agent.js +0 -35
  149. package/.agent/weixin.json +0 -6
  150. package/.agent/workflows/email-digest.json +0 -50
  151. package/.agent/workflows/file-backup.json +0 -21
  152. package/.agent/workflows/get-ip-notify.json +0 -32
  153. package/.agent/workflows/news-aggregator.json +0 -93
  154. package/.agent/workflows/news-dashboard-v2.json +0 -94
  155. package/.agent/workflows/notification-batch.json +0 -32
  156. package/src/core/session-context.js +0 -346
  157. package/src/core/session-storage.js +0 -295
@@ -1,19 +1,39 @@
1
1
  /**
2
- * Foliko Logger - 统一日志系统
3
- * 支持日志级别、命名空间、格式化输出
2
+ * Foliko Logger - 基于 Pino 的统一日志系统
3
+ * 支持日志级别、命名空间、格式化输出、文件轮转
4
4
  */
5
5
 
6
- const LOG_LEVELS = {
7
- DEBUG: 0,
8
- INFO: 1,
9
- WARN: 2,
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
- const LEVEL_NAMES = ['DEBUG', 'INFO', 'WARN', 'ERROR'];
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
- class Logger {
41
- /**
42
- * @param {Object} options
43
- * @param {string} options.namespace - 日志命名空间
44
- * @param {number} options.level - 日志级别 (0-4)
45
- * @param {boolean} options.enableTimestamp - 是否显示时间戳
46
- * @param {boolean} options.enableColors - 是否使用颜色
47
- */
48
- constructor(options = {}) {
49
- this.namespace = options.namespace || 'app';
50
- this._level = options.level ?? LOG_LEVELS.INFO;
51
- this.enableTimestamp = options.enableTimestamp !== false;
52
- this.enableColors = options.enableColors !== false;
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
- get level() {
70
- return Logger.globalLevel ?? this._level;
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
- set level(v) {
74
- this._level = v;
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
- static globalLevel = null;
78
- static globalLevelOverride = false;
98
+ // 非静默模式:同时输出到控制台和文件
99
+ currentFileStream = fs.createWriteStream(currentLogFile, { flags: 'a' });
79
100
 
80
- /**
81
- * 创建子日志器
82
- * @param {string} namespace
83
- * @returns {Logger}
84
- */
85
- child(namespace) {
86
- return new Logger({
87
- namespace: `${this.namespace}:${namespace}`,
88
- level: this.level,
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
- * @private
109
- */
110
- _format(level, levelName, args) {
111
- const color = this.colors[levelName.toLowerCase()] || '';
112
- const reset = this.colors.reset;
113
- const dim = this.colors.dim;
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
- const parts = [];
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
- parts.push(`${dim}[${this.namespace}]${reset}`);
130
+ child(namespace) {
131
+ return new Logger({ namespace: `${this.namespace}:${namespace}` });
132
+ }
120
133
 
121
- // 时间戳
122
- if (this.enableTimestamp) {
123
- const timestamp = new Date().toISOString().split('T')[1].slice(0, -1);
124
- parts.push(`${dim}${timestamp}${reset}`);
125
- }
134
+ // 事件监听支持
135
+ on(event, listener) {
136
+ logEmitter.on(event, listener);
137
+ }
126
138
 
127
- // 级别
128
- if (this.enableColors) {
129
- parts.push(`${color}${levelName.padEnd(5)}${reset}`);
130
- } else {
131
- parts.push(levelName.padEnd(5));
132
- }
139
+ off(event, listener) {
140
+ logEmitter.off(event, listener);
141
+ }
133
142
 
134
- // 消息(根据级别着色)
135
- const msgColor = this.enableColors ? color : '';
136
- const message = args
137
- .map((arg) => {
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
- if (this.level <= LOG_LEVELS.DEBUG) {
162
- const formatted = this._format('debug', 'DEBUG', args);
163
- logEmitter.emit('log', { level: 'DEBUG', namespace: this.namespace, message: args.join(' '), formatted });
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
- if (this.level <= LOG_LEVELS.INFO) {
173
- const formatted = this._format('info', 'INFO', args);
174
- console.log(formatted);
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
- if (this.level <= LOG_LEVELS.WARN) {
185
- const formatted = this._format('warn', 'WARN', args);
186
- console.warn(formatted);
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
- if (this.level <= LOG_LEVELS.ERROR) {
197
- const formatted = this._format('error', 'ERROR', args);
198
- console.error(formatted);
199
- logEmitter.emit('log', { level: 'ERROR', namespace: this.namespace, message: args.join(' '), formatted });
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 getLogger() {
220
- if (!getLogger._instance) {
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
- child: _globalLogger.child.bind(_globalLogger),
245
- debug: _globalLogger.debug.bind(_globalLogger),
246
- info: _globalLogger.info.bind(_globalLogger),
247
- warn: _globalLogger.warn.bind(_globalLogger),
248
- error: _globalLogger.error.bind(_globalLogger),
249
- timer: _globalLogger.timer.bind(_globalLogger),
250
- setLevel: (level) => _globalLogger.setLevel(level),
251
- setGlobalLogLevel,
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. 文件夹结构: .agent/plugins/my-plugin/index.js
28
- * 2. 单文件结构: .agent/plugins/my-plugin.js
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 - 选项