foliko 2.0.10 → 2.0.11
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/package.json +1 -1
- package/plugins/core/workflow/index.js +10 -2
- package/src/session/chat.js +18 -2
- package/src/utils/message-validator.js +55 -14
- package/weixin_o9cq80zgZqKPA2-s59PN43GdDy1w@im.wechat.jsonl +116 -153
- package/weixin_o9cq80zgZqKPA2-s59PN43GdDy1w@im.wechat.jsonl.bak.1783160606497 +0 -303
package/package.json
CHANGED
|
@@ -999,7 +999,12 @@ class WorkflowPlugin extends Plugin {
|
|
|
999
999
|
for (const file of files) {
|
|
1000
1000
|
if (!file.endsWith('.json') && !file.endsWith('.js')) continue;
|
|
1001
1001
|
const filePath = path.join(dir, file);
|
|
1002
|
-
|
|
1002
|
+
let workflowName = path.basename(file, path.extname(file));
|
|
1003
|
+
// 兼容 .flow.js / .flow.json 命名约定:去掉 .flow 后缀
|
|
1004
|
+
// (如 weather_card.flow.js → workflow_weather_card 而不是 workflow_weather_card.flow)
|
|
1005
|
+
if (workflowName.endsWith('.flow')) {
|
|
1006
|
+
workflowName = workflowName.slice(0, -'.flow'.length);
|
|
1007
|
+
}
|
|
1003
1008
|
// 跳过已存在的工作流(先加载的优先)
|
|
1004
1009
|
if (this._workflows.has(workflowName)) continue;
|
|
1005
1010
|
try {
|
|
@@ -1064,7 +1069,10 @@ class WorkflowPlugin extends Plugin {
|
|
|
1064
1069
|
*/
|
|
1065
1070
|
_registerWorkflowTools() {
|
|
1066
1071
|
for (const [name, workflow] of this._workflows) {
|
|
1067
|
-
|
|
1072
|
+
// 工具名 sanitize:把文件名里的 . - 空格等替换为 _,避免 LLM 把名字当成
|
|
1073
|
+
// "module.method" 调用语法(如 weather_card.flow.js → workflow_weather_card_flow)
|
|
1074
|
+
const safeName = name.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
1075
|
+
const toolName = `workflow_${safeName}`;
|
|
1068
1076
|
const description = workflow.description || `执行工作流: ${name}`;
|
|
1069
1077
|
// 注意:framework.registerTool 不做去重,导致 reload 后会有两个同名
|
|
1070
1078
|
// `workflow_<name>` 工具。getTool 返回第一个(旧的)。每次注册前先清掉。
|
package/src/session/chat.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
const { EventEmitter } = require('../common/events');
|
|
12
12
|
const { logger } = require('../common/logger');
|
|
13
|
-
const { normalizeToolOutputs } = require('../utils/message-validator');
|
|
13
|
+
const { normalizeToolOutputs, filterOrphanToolCalls, validateMessagesPairing } = require('../utils/message-validator');
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Session 作用域的事件监听器
|
|
@@ -230,7 +230,23 @@ class ChatSession extends EventEmitter {
|
|
|
230
230
|
if (fixed > 0) {
|
|
231
231
|
logger.info(`[loadHistory] 规范化 ${fixed} 条 tool-result(error-text/error-json → text/json)`);
|
|
232
232
|
}
|
|
233
|
-
|
|
233
|
+
// 聊天前过滤:把对"已不可用工具"的 tool-call 转成 text。
|
|
234
|
+
// 防止 LLM 在新会话里看到历史 tool-call 后继续重试不存在的工具
|
|
235
|
+
// (典型场景:工作流文件被删除/改名,chat log 保留了对它的引用)。
|
|
236
|
+
const availableToolNames = this.agent?.framework?.toolRegistry?.getNames?.() || [];
|
|
237
|
+
if (availableToolNames.length > 0) {
|
|
238
|
+
const orphans = filterOrphanToolCalls(messages, availableToolNames);
|
|
239
|
+
if (orphans > 0) {
|
|
240
|
+
logger.info(`[loadHistory] 转换 ${orphans} 条 orphan tool-call 为 text(工具已不可用)`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// 转换 tool-call 后,相应的 tool-result 变成 orphan(没有对应 tool-call),
|
|
244
|
+
// 这里立即清理掉(和 _prepareSession 中的 _stripOrphanedToolMessages 等价)
|
|
245
|
+
const cleaned = validateMessagesPairing(messages, { log: false });
|
|
246
|
+
if (cleaned.length !== messages.length) {
|
|
247
|
+
logger.info(`[loadHistory] 清理 ${messages.length - cleaned.length} 条 orphan tool-result`);
|
|
248
|
+
}
|
|
249
|
+
messageStore.messages = cleaned;
|
|
234
250
|
messageStore.historyLoaded = true;
|
|
235
251
|
messageStore._hasMoreHistory = allMessages.length > MAX_INITIAL_LOAD;
|
|
236
252
|
//logger.debug(`Loaded ${messages.length} msgs for session ${sessionId} (total: ${allMessages.length})`);
|
|
@@ -178,20 +178,6 @@ function validateAll(messages) {
|
|
|
178
178
|
// AI SDK v6 ToolResultOutput 允许的类型(用于 ModelMessage[] 输入 schema)
|
|
179
179
|
const VALID_OUTPUT_TYPES = new Set(['text', 'json', 'execution-denied', 'content']);
|
|
180
180
|
|
|
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
181
|
function normalizeToolOutputs(messages) {
|
|
196
182
|
let fixed = 0;
|
|
197
183
|
for (const msg of messages) {
|
|
@@ -232,6 +218,60 @@ function normalizeToolOutputs(messages) {
|
|
|
232
218
|
return fixed;
|
|
233
219
|
}
|
|
234
220
|
|
|
221
|
+
/**
|
|
222
|
+
* 把 assistant 消息里"已不可用工具"的 tool-call 转成 text,
|
|
223
|
+
* 防止 LLM 在新会话里看到历史 tool-call 后重试不存在的工具。
|
|
224
|
+
*
|
|
225
|
+
* 典型场景:
|
|
226
|
+
* - 工作流文件被删除/改名后,chat log 里仍保留对该工作流的 tool-call
|
|
227
|
+
* - 插件被禁用后,chat log 里保留该插件工具的 tool-call
|
|
228
|
+
*
|
|
229
|
+
* 转换后的 text 形如 "(工具 xxx 已不可用,原 tool-call 已跳过)",
|
|
230
|
+
* 紧接的 tool-result 如果是正常的成功结果会被保留;如果是失败结果
|
|
231
|
+
* (error-text 已 normalize 为 text),orphan 的 tool-result 会被
|
|
232
|
+
* `_stripOrphanedToolMessages` / `validateMessagesPairing` 自然清理。
|
|
233
|
+
*
|
|
234
|
+
* @param {Array} messages - 消息数组(会被修改)
|
|
235
|
+
* @param {Set<string>|Array<string>} availableToolNames - 当前可用的工具名集合
|
|
236
|
+
* @returns {number} 转换的 tool-call 数量
|
|
237
|
+
*/
|
|
238
|
+
function filterOrphanToolCalls(messages, availableToolNames) {
|
|
239
|
+
if (!Array.isArray(messages) || !availableToolNames) return 0;
|
|
240
|
+
const available = availableToolNames instanceof Set
|
|
241
|
+
? availableToolNames
|
|
242
|
+
: new Set(availableToolNames);
|
|
243
|
+
let converted = 0;
|
|
244
|
+
|
|
245
|
+
for (const msg of messages) {
|
|
246
|
+
if (msg.role !== 'assistant' || !Array.isArray(msg.content)) continue;
|
|
247
|
+
// 收集本条消息中所有 tool-call ID,便于在转换后保留
|
|
248
|
+
const newContent = [];
|
|
249
|
+
for (const part of msg.content) {
|
|
250
|
+
if (!part || (part.type !== 'tool-call' && part.type !== 'tool-use')) {
|
|
251
|
+
newContent.push(part);
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
if (part.toolName && available.has(part.toolName)) {
|
|
255
|
+
newContent.push(part);
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
// tool 不可用 → 转成 text 提示
|
|
259
|
+
converted++;
|
|
260
|
+
const reason = part.toolName
|
|
261
|
+
? `(工具 ${part.toolName} 当前不可用,原 tool-call 已跳过)`
|
|
262
|
+
: `(工具调用参数不完整,已跳过)`;
|
|
263
|
+
newContent.push({
|
|
264
|
+
type: 'text',
|
|
265
|
+
text: reason,
|
|
266
|
+
});
|
|
267
|
+
// 注意:原 part 的 toolCallId 不再出现在新 content 里,
|
|
268
|
+
// _stripOrphanedToolMessages 会把对应的 tool-result 视为 orphan 并清理
|
|
269
|
+
}
|
|
270
|
+
msg.content = newContent;
|
|
271
|
+
}
|
|
272
|
+
return converted;
|
|
273
|
+
}
|
|
274
|
+
|
|
235
275
|
/**
|
|
236
276
|
* 过滤消息,保留配对的 tool-call → tool-result 链条
|
|
237
277
|
* 用于上下文压缩后保留完整的工具调用链路
|
|
@@ -341,5 +381,6 @@ module.exports = {
|
|
|
341
381
|
validateToolCalls,
|
|
342
382
|
validateAll,
|
|
343
383
|
normalizeToolOutputs,
|
|
384
|
+
filterOrphanToolCalls,
|
|
344
385
|
filterPairedMessages,
|
|
345
386
|
};
|