foliko 2.0.10 → 2.0.12
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/.editorconfig +56 -56
- package/.lintstagedrc +7 -7
- package/.prettierignore +29 -29
- package/.prettierrc +11 -11
- package/Dockerfile +63 -63
- package/install.ps1 +129 -129
- package/install.sh +121 -121
- package/package.json +1 -1
- package/plugins/core/workflow/context.js +941 -941
- package/plugins/core/workflow/engine.js +66 -66
- package/plugins/core/workflow/index.js +10 -2
- package/plugins/core/workflow/js-runner.js +318 -318
- package/plugins/core/workflow/json-runner.js +323 -323
- package/plugins/core/workflow/stages/choice.js +74 -74
- package/plugins/core/workflow/stages/each.js +123 -123
- package/plugins/core/workflow/stages/parallel.js +69 -69
- package/skills/find-skills/SKILL.md +133 -133
- package/src/agent/chat.js +227 -54
- package/src/session/chat.js +18 -2
- package/src/utils/message-validator.js +194 -30
- package/weixin_o9cq80zgZqKPA2-s59PN43GdDy1w@im.wechat.jsonl +0 -303
- package/weixin_o9cq80zgZqKPA2-s59PN43GdDy1w@im.wechat.jsonl.bak.1783160606497 +0 -303
|
@@ -114,17 +114,21 @@ function validateToolCalls(messages, options = {}) {
|
|
|
114
114
|
if (typeof input !== 'string') continue;
|
|
115
115
|
|
|
116
116
|
const trimmed = input.trim();
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
117
|
+
// 只有真正无法解析为 JSON 的才标记为 invalid
|
|
118
|
+
let invalid = false;
|
|
119
|
+
if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
|
|
120
|
+
try { JSON.parse(trimmed); } catch { invalid = true; }
|
|
121
|
+
} else if (trimmed !== '') {
|
|
122
|
+
invalid = true;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (invalid) {
|
|
126
|
+
if (item.toolCallId) invalidatedToolCallIds.add(item.toolCallId);
|
|
121
127
|
if (shouldLog) {
|
|
122
128
|
logger.warn(
|
|
123
|
-
`[validateToolCalls] invalid input for "${item.toolName}", toolCallId=${item.toolCallId ||
|
|
124
|
-
'?'}, converting to text`
|
|
129
|
+
`[validateToolCalls] invalid input for "${item.toolName}", toolCallId=${item.toolCallId || '?'}, converting to text`
|
|
125
130
|
);
|
|
126
131
|
}
|
|
127
|
-
// 把无效的 tool-call 转换成 text 文本
|
|
128
132
|
item.type = 'text';
|
|
129
133
|
item.text = `(工具调用 ${item.toolName} 参数不完整,已跳过)`;
|
|
130
134
|
delete item.toolCallId;
|
|
@@ -155,43 +159,148 @@ function validateToolCalls(messages, options = {}) {
|
|
|
155
159
|
}
|
|
156
160
|
}
|
|
157
161
|
|
|
158
|
-
if (shouldLog && fixedCount > 0) {
|
|
159
|
-
// logger.info(`[validateToolCalls] Fixed ${fixedCount} incomplete tool calls/results`);
|
|
160
|
-
}
|
|
162
|
+
if (shouldLog && fixedCount > 0) {}
|
|
161
163
|
|
|
162
164
|
return fixedCount;
|
|
163
165
|
}
|
|
164
166
|
|
|
165
167
|
/**
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
+
* 单次遍历完成所有验证和规范化:
|
|
169
|
+
* 1. 修复 tool-call 格式(不完整 JSON → text)
|
|
170
|
+
* 2. 收集有效 toolCallId(content[] 和 tool_calls[])
|
|
171
|
+
* 3. 规范化 error-text/error-json → text/json
|
|
172
|
+
* 4. 移除 orphaned tool-result(没有对应 tool-call 的)
|
|
173
|
+
* 5. 移除整个 orphaned tool 消息(content 被清空的)
|
|
174
|
+
*
|
|
175
|
+
* 比分别调用减少 3-4 次消息数组遍历
|
|
168
176
|
* @param {Array} messages - 消息数组(会被修改)
|
|
169
177
|
* @returns {Array} 过滤后的消息数组
|
|
170
178
|
*/
|
|
171
179
|
function validateAll(messages) {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
180
|
+
const VALID_OUTPUT_TYPES = new Set(['text', 'json', 'execution-denied', 'content']);
|
|
181
|
+
const validToolCallIds = new Set();
|
|
182
|
+
const invalidatedIds = new Set();
|
|
183
|
+
|
|
184
|
+
// 第一遍:收集有效 ID + 修复格式 + 规范化 error 类型
|
|
185
|
+
for (const msg of messages) {
|
|
186
|
+
if (msg.role === 'assistant') {
|
|
187
|
+
const content = msg.content;
|
|
188
|
+
if (Array.isArray(content)) {
|
|
189
|
+
for (const item of content) {
|
|
190
|
+
if (item && (item.type === 'tool-call' || item.type === 'tool-use')) {
|
|
191
|
+
const input = item.input;
|
|
192
|
+
// 检查 input 是否为不完整的 JSON:空字符串、或只有 { 或 }
|
|
193
|
+
// 有效 input 应能被 JSON.parse(即使是 {})
|
|
194
|
+
let invalid = false;
|
|
195
|
+
if (typeof input === 'string') {
|
|
196
|
+
const trimmed = input.trim();
|
|
197
|
+
// 有效 input:能解析为 JSON 的字符串(包括 '{}')
|
|
198
|
+
if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
|
|
199
|
+
try { JSON.parse(trimmed); } catch { invalid = true; }
|
|
200
|
+
} else if (trimmed !== '') {
|
|
201
|
+
// 非空且不是 {} 开头:无效
|
|
202
|
+
invalid = true;
|
|
203
|
+
}
|
|
204
|
+
// 空字符串 '' 单独放过(无参数工具是合法的)
|
|
205
|
+
} else if (input === undefined || input === null) {
|
|
206
|
+
// input 为 null/undefined 时:只有当工具 schema 要求必填时才应该报错
|
|
207
|
+
// 但我们无法在此判断工具是否必填,统一放行让后续验证处理
|
|
208
|
+
// invalid = true;
|
|
209
|
+
}
|
|
210
|
+
if (invalid) {
|
|
211
|
+
logger.debug(`[validateAll] INVALID detected: toolName=${item.toolName}, input=${JSON.stringify(input)}, typeof=${typeof input}`);
|
|
212
|
+
// 无效输入 → 转 text,标记为 invalid
|
|
213
|
+
item.type = 'text';
|
|
214
|
+
item.text = `(工具调用 ${item.toolName} 参数不完整,已跳过)`;
|
|
215
|
+
if (item.toolCallId) invalidatedIds.add(item.toolCallId);
|
|
216
|
+
delete item.toolCallId;
|
|
217
|
+
delete item.toolName;
|
|
218
|
+
delete item.input;
|
|
219
|
+
} else if (item.toolCallId) {
|
|
220
|
+
validToolCallIds.add(item.toolCallId);
|
|
221
|
+
}
|
|
222
|
+
} else if (item && item.type === 'tool-result') {
|
|
223
|
+
// 规范化 tool-result output
|
|
224
|
+
const out = item.output;
|
|
225
|
+
if (out === undefined || out === null || typeof out !== 'object') {
|
|
226
|
+
item.output = { type: 'text', value: String(out ?? 'undefined') };
|
|
227
|
+
} else if (!VALID_OUTPUT_TYPES.has(out.type)) {
|
|
228
|
+
if (out.type === 'error-text' || out.type === 'error_text') {
|
|
229
|
+
item.output = { type: 'text', value: typeof out.value === 'string' ? out.value : String(out.value ?? '') };
|
|
230
|
+
} else if (out.type === 'error-json' || out.type === 'error_json') {
|
|
231
|
+
item.output = { type: 'json', value: out.value ?? null };
|
|
232
|
+
} else {
|
|
233
|
+
try { item.output = { type: 'text', value: JSON.stringify(out) }; }
|
|
234
|
+
catch { item.output = { type: 'text', value: String(out) }; }
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// tool_calls[] 格式(OpenAI 兼容)
|
|
241
|
+
if (Array.isArray(msg.tool_calls)) {
|
|
242
|
+
for (const tc of msg.tool_calls) {
|
|
243
|
+
if (tc.id && !invalidatedIds.has(tc.id)) validToolCallIds.add(tc.id);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
} else if (msg.role === 'tool') {
|
|
247
|
+
// 规范化 tool 消息 output
|
|
248
|
+
if (Array.isArray(msg.content)) {
|
|
249
|
+
for (const item of msg.content) {
|
|
250
|
+
if (item && item.type === 'tool-result') {
|
|
251
|
+
const out = item.output;
|
|
252
|
+
if (out === undefined || out === null || typeof out !== 'object') {
|
|
253
|
+
item.output = { type: 'text', value: String(out ?? 'undefined') };
|
|
254
|
+
} else if (!VALID_OUTPUT_TYPES.has(out.type)) {
|
|
255
|
+
if (out.type === 'error-text' || out.type === 'error_text') {
|
|
256
|
+
item.output = { type: 'text', value: typeof out.value === 'string' ? out.value : String(out.value ?? '') };
|
|
257
|
+
} else if (out.type === 'error-json' || out.type === 'error_json') {
|
|
258
|
+
item.output = { type: 'json', value: out.value ?? null };
|
|
259
|
+
} else {
|
|
260
|
+
try { item.output = { type: 'text', value: JSON.stringify(out) }; }
|
|
261
|
+
catch { item.output = { type: 'text', value: String(out) }; }
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// 第二遍:过滤 orphaned tool-result + orphaned tool messages
|
|
271
|
+
const toolMessageIndicesToRemove = new Set();
|
|
272
|
+
for (let i = 0; i < messages.length; i++) {
|
|
273
|
+
const msg = messages[i];
|
|
274
|
+
if (msg.role !== 'tool') continue;
|
|
275
|
+
|
|
276
|
+
let hasValidResult = false;
|
|
277
|
+
if (Array.isArray(msg.content)) {
|
|
278
|
+
const originalLen = msg.content.length;
|
|
279
|
+
msg.content = msg.content.filter((item) => {
|
|
280
|
+
if (!item || (item.type !== 'tool-result' && item.type !== 'tool_result')) return true;
|
|
281
|
+
const id = item.toolCallId;
|
|
282
|
+
if (!id || (!validToolCallIds.has(id) && !invalidatedIds.has(id))) return false;
|
|
283
|
+
hasValidResult = true;
|
|
284
|
+
return true;
|
|
285
|
+
});
|
|
286
|
+
if (msg.content.length === 0 && originalLen > 0) {
|
|
287
|
+
toolMessageIndicesToRemove.add(i);
|
|
288
|
+
}
|
|
289
|
+
} else if (msg.tool_call_id && !validToolCallIds.has(msg.tool_call_id)) {
|
|
290
|
+
toolMessageIndicesToRemove.add(i);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (toolMessageIndicesToRemove.size > 0) {
|
|
295
|
+
messages = messages.filter((_, i) => !toolMessageIndicesToRemove.has(i));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return messages;
|
|
176
299
|
}
|
|
177
300
|
|
|
178
301
|
// AI SDK v6 ToolResultOutput 允许的类型(用于 ModelMessage[] 输入 schema)
|
|
179
302
|
const VALID_OUTPUT_TYPES = new Set(['text', 'json', 'execution-denied', 'content']);
|
|
180
303
|
|
|
181
|
-
/**
|
|
182
|
-
* 规范化 tool-result 的 output 字段。
|
|
183
|
-
* AI SDK v6 响应中允许 'error-text' / 'error-json'(表示工具执行失败),
|
|
184
|
-
* 但 ModelMessage 输入 schema 不接受这两类。需要在发送前 / 持久化前转换。
|
|
185
|
-
*
|
|
186
|
-
* 转换规则:
|
|
187
|
-
* - error-text → text
|
|
188
|
-
* - error-json → json
|
|
189
|
-
* - 任何未知 type → text(保留 value 字段)
|
|
190
|
-
* - output 缺失或非对象 → 包装为 text 'null'
|
|
191
|
-
*
|
|
192
|
-
* @param {Array} messages - 消息数组(会被修改)
|
|
193
|
-
* @returns {number} 规范化次数
|
|
194
|
-
*/
|
|
195
304
|
function normalizeToolOutputs(messages) {
|
|
196
305
|
let fixed = 0;
|
|
197
306
|
for (const msg of messages) {
|
|
@@ -232,6 +341,60 @@ function normalizeToolOutputs(messages) {
|
|
|
232
341
|
return fixed;
|
|
233
342
|
}
|
|
234
343
|
|
|
344
|
+
/**
|
|
345
|
+
* 把 assistant 消息里"已不可用工具"的 tool-call 转成 text,
|
|
346
|
+
* 防止 LLM 在新会话里看到历史 tool-call 后重试不存在的工具。
|
|
347
|
+
*
|
|
348
|
+
* 典型场景:
|
|
349
|
+
* - 工作流文件被删除/改名后,chat log 里仍保留对该工作流的 tool-call
|
|
350
|
+
* - 插件被禁用后,chat log 里保留该插件工具的 tool-call
|
|
351
|
+
*
|
|
352
|
+
* 转换后的 text 形如 "(工具 xxx 已不可用,原 tool-call 已跳过)",
|
|
353
|
+
* 紧接的 tool-result 如果是正常的成功结果会被保留;如果是失败结果
|
|
354
|
+
* (error-text 已 normalize 为 text),orphan 的 tool-result 会被
|
|
355
|
+
* `_stripOrphanedToolMessages` / `validateMessagesPairing` 自然清理。
|
|
356
|
+
*
|
|
357
|
+
* @param {Array} messages - 消息数组(会被修改)
|
|
358
|
+
* @param {Set<string>|Array<string>} availableToolNames - 当前可用的工具名集合
|
|
359
|
+
* @returns {number} 转换的 tool-call 数量
|
|
360
|
+
*/
|
|
361
|
+
function filterOrphanToolCalls(messages, availableToolNames) {
|
|
362
|
+
if (!Array.isArray(messages) || !availableToolNames) return 0;
|
|
363
|
+
const available = availableToolNames instanceof Set
|
|
364
|
+
? availableToolNames
|
|
365
|
+
: new Set(availableToolNames);
|
|
366
|
+
let converted = 0;
|
|
367
|
+
|
|
368
|
+
for (const msg of messages) {
|
|
369
|
+
if (msg.role !== 'assistant' || !Array.isArray(msg.content)) continue;
|
|
370
|
+
// 收集本条消息中所有 tool-call ID,便于在转换后保留
|
|
371
|
+
const newContent = [];
|
|
372
|
+
for (const part of msg.content) {
|
|
373
|
+
if (!part || (part.type !== 'tool-call' && part.type !== 'tool-use')) {
|
|
374
|
+
newContent.push(part);
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
if (part.toolName && available.has(part.toolName)) {
|
|
378
|
+
newContent.push(part);
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
// tool 不可用 → 转成 text 提示
|
|
382
|
+
converted++;
|
|
383
|
+
const reason = part.toolName
|
|
384
|
+
? `(工具 ${part.toolName} 当前不可用,原 tool-call 已跳过)`
|
|
385
|
+
: `(工具调用参数不完整,已跳过)`;
|
|
386
|
+
newContent.push({
|
|
387
|
+
type: 'text',
|
|
388
|
+
text: reason,
|
|
389
|
+
});
|
|
390
|
+
// 注意:原 part 的 toolCallId 不再出现在新 content 里,
|
|
391
|
+
// _stripOrphanedToolMessages 会把对应的 tool-result 视为 orphan 并清理
|
|
392
|
+
}
|
|
393
|
+
msg.content = newContent;
|
|
394
|
+
}
|
|
395
|
+
return converted;
|
|
396
|
+
}
|
|
397
|
+
|
|
235
398
|
/**
|
|
236
399
|
* 过滤消息,保留配对的 tool-call → tool-result 链条
|
|
237
400
|
* 用于上下文压缩后保留完整的工具调用链路
|
|
@@ -341,5 +504,6 @@ module.exports = {
|
|
|
341
504
|
validateToolCalls,
|
|
342
505
|
validateAll,
|
|
343
506
|
normalizeToolOutputs,
|
|
507
|
+
filterOrphanToolCalls,
|
|
344
508
|
filterPairedMessages,
|
|
345
509
|
};
|