foliko 1.1.7 → 1.1.9
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/.agent/agents/poster-expert.md +228 -0
- package/.agent/data/default.json +10 -0
- package/.agent/data/plugins-state.json +1 -1
- package/.agent/mcp_config.json +0 -6
- package/.agent/plugins/test-plugin.py +15 -0
- package/.agent/plugins/test_nested_plugin.py +85 -0
- package/.agent/sessions/cli_default.json +83 -803
- package/.agent/sessions/weixin_o9cq80zgZqKPA2-s59PN43GdDy1w@im.wechat.json +189 -0
- package/.claude/settings.local.json +8 -1
- package/cli/src/ui/chat-ui.js +32 -21
- package/foliko_poster.png +0 -0
- package/output/business_poster_final.png +0 -0
- package/output/emoji_test_v2.png +0 -0
- package/output/foliko-ai-launch-poster.png +0 -0
- package/output/foliko-poster.png +0 -0
- package/output/muji_style_poster.png +0 -0
- package/output/new_product_launch.png +0 -0
- package/output/news_poster_625yi_subsidy.png +0 -0
- package/output/tech_future_2026.png +0 -0
- package/output/tech_future_poster.png +0 -0
- package/package.json +2 -2
- package/plugins/default-plugins.js +60 -7
- package/plugins/extension-executor-plugin.js +32 -22
- package/plugins/feishu-plugin.js +2 -2
- package/plugins/python-plugin-loader.js +461 -40
- package/plugins/python-plugin-loader.js.bak +856 -0
- package/plugins/subagent-plugin.js +0 -121
- package/plugins/telegram-plugin.js +2 -2
- package/plugins/weixin-plugin.js +40 -31
- package/poster.png +0 -0
- package/skills/foliko-dev/SKILL.md +6 -0
- package/skills/python-plugin-dev/SKILL.md +124 -2
- package/src/core/agent-chat.js +220 -281
- package/src/core/agent.js +127 -29
- package/src/executors/mcp-executor.js +282 -26
- package/src/utils/plugin-helpers.js +22 -4
- package/system.md +362 -109
- package/test-scan.js +18 -0
- package/.agent/memory/feedback/mnt7jrlt-d67qs7.md +0 -15
- package/.agent/memory/feedback/mnt88ja3-al4fuy.md +0 -9
- package/.agent/plugins/poster-plugin/README.md +0 -304
- package/.agent/plugins/poster-plugin/emojis/rocket.png +0 -1
- package/.agent/plugins/poster-plugin/fonts/PatuaOne-Regular.ttf +0 -0
- package/.agent/plugins/poster-plugin/fonts/SegoeUI Emoji.ttf +0 -0
- package/.agent/plugins/poster-plugin/fonts//345/276/256/350/275/257/351/233/205/351/273/221.ttf +0 -0
- package/.agent/plugins/poster-plugin/fonts//345/276/256/350/275/257/351/233/205/351/273/221/347/262/227/344/275/223.ttf +0 -0
- package/.agent/plugins/poster-plugin/index.js +0 -13
- package/.agent/plugins/poster-plugin/package.json +0 -29
- package/.agent/plugins/poster-plugin/src/canvas.js +0 -224
- package/.agent/plugins/poster-plugin/src/components/arrow.js +0 -84
- package/.agent/plugins/poster-plugin/src/components/avatar.js +0 -71
- package/.agent/plugins/poster-plugin/src/components/badge.js +0 -85
- package/.agent/plugins/poster-plugin/src/components/barcode.js +0 -120
- package/.agent/plugins/poster-plugin/src/components/bubble.js +0 -153
- package/.agent/plugins/poster-plugin/src/components/button.js +0 -124
- package/.agent/plugins/poster-plugin/src/components/card.js +0 -88
- package/.agent/plugins/poster-plugin/src/components/chart.js +0 -127
- package/.agent/plugins/poster-plugin/src/components/chip.js +0 -88
- package/.agent/plugins/poster-plugin/src/components/columns.js +0 -107
- package/.agent/plugins/poster-plugin/src/components/cta.js +0 -87
- package/.agent/plugins/poster-plugin/src/components/divider.js +0 -55
- package/.agent/plugins/poster-plugin/src/components/feature.js +0 -85
- package/.agent/plugins/poster-plugin/src/components/featureGrid.js +0 -117
- package/.agent/plugins/poster-plugin/src/components/frame.js +0 -230
- package/.agent/plugins/poster-plugin/src/components/grid.js +0 -118
- package/.agent/plugins/poster-plugin/src/components/highlightText.js +0 -144
- package/.agent/plugins/poster-plugin/src/components/icon.js +0 -94
- package/.agent/plugins/poster-plugin/src/components/imageFrame.js +0 -155
- package/.agent/plugins/poster-plugin/src/components/index.js +0 -81
- package/.agent/plugins/poster-plugin/src/components/listItem.js +0 -147
- package/.agent/plugins/poster-plugin/src/components/notification.js +0 -123
- package/.agent/plugins/poster-plugin/src/components/progress.js +0 -79
- package/.agent/plugins/poster-plugin/src/components/progressCircle.js +0 -117
- package/.agent/plugins/poster-plugin/src/components/qrcode.js +0 -74
- package/.agent/plugins/poster-plugin/src/components/quote.js +0 -97
- package/.agent/plugins/poster-plugin/src/components/rating.js +0 -85
- package/.agent/plugins/poster-plugin/src/components/ribbon.js +0 -193
- package/.agent/plugins/poster-plugin/src/components/seal.js +0 -146
- package/.agent/plugins/poster-plugin/src/components/star.js +0 -70
- package/.agent/plugins/poster-plugin/src/components/statCard.js +0 -105
- package/.agent/plugins/poster-plugin/src/components/stepper.js +0 -118
- package/.agent/plugins/poster-plugin/src/components/table.js +0 -167
- package/.agent/plugins/poster-plugin/src/components/tagCloud.js +0 -85
- package/.agent/plugins/poster-plugin/src/components/timeline.js +0 -117
- package/.agent/plugins/poster-plugin/src/components/watermark.js +0 -52
- package/.agent/plugins/poster-plugin/src/composer.js +0 -2146
- package/.agent/plugins/poster-plugin/src/elements/artText.js +0 -60
- package/.agent/plugins/poster-plugin/src/elements/background.js +0 -84
- package/.agent/plugins/poster-plugin/src/elements/circle.js +0 -31
- package/.agent/plugins/poster-plugin/src/elements/image.js +0 -28
- package/.agent/plugins/poster-plugin/src/elements/index.js +0 -28
- package/.agent/plugins/poster-plugin/src/elements/line.js +0 -23
- package/.agent/plugins/poster-plugin/src/elements/polygon.js +0 -63
- package/.agent/plugins/poster-plugin/src/elements/rectangle.js +0 -32
- package/.agent/plugins/poster-plugin/src/elements/richText.js +0 -230
- package/.agent/plugins/poster-plugin/src/elements/svg.js +0 -108
- package/.agent/plugins/poster-plugin/src/elements/text.js +0 -109
- package/.agent/plugins/poster-plugin/src/fonts.js +0 -243
- package/.agent/plugins/poster-plugin/src/index.js +0 -2081
- package/.agent/plugins/poster-plugin/src/presets.js +0 -36
- package/.agent/plugins/poster-plugin/src/templates/business.js +0 -60
- package/.agent/plugins/poster-plugin/src/templates/gradient.js +0 -64
- package/.agent/plugins/poster-plugin/src/templates/index.js +0 -43
- package/.agent/plugins/poster-plugin/src/templates/modern.js +0 -69
- package/.agent/plugins/poster-plugin/src/templates/simple.js +0 -58
- package/.agent/plugins/poster-plugin/src/templates/social.js +0 -62
- package/.agent/plugins/poster-plugin/src/templates/tech.js +0 -84
- package/.agent/plugins/poster-plugin/src/utils/imageLoader.js +0 -84
- package/.agent/plugins/poster-plugin/test-background.svg +0 -1
- package/.agent/plugins/poster-plugin/test-full-poster.svg +0 -2
- package/.agent/plugins/poster-plugin/test-image.png +0 -0
- package/.agent/plugins/poster-plugin/yarn.lock +0 -1007
- package/output/zen_silence.png +0 -0
package/src/core/agent-chat.js
CHANGED
|
@@ -96,6 +96,87 @@ const _globalTokenizer = new SimpleTokenizer();
|
|
|
96
96
|
// 压缩超时时间(毫秒)
|
|
97
97
|
const COMPRESSION_TIMEOUT = 30000;
|
|
98
98
|
|
|
99
|
+
/**
|
|
100
|
+
* ChatStreamQueue - 流式消息队列
|
|
101
|
+
* 按顺序处理消息,每条消息使用 chatStream 流式输出
|
|
102
|
+
*/
|
|
103
|
+
class ChatStreamQueue {
|
|
104
|
+
constructor(chatHandler) {
|
|
105
|
+
this.chatHandler = chatHandler;
|
|
106
|
+
this.queue = []; // 消息队列
|
|
107
|
+
this.isProcessing = false;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 添加消息到队列,返回 Promise
|
|
111
|
+
enqueue(message, options = {}) {
|
|
112
|
+
const { onChunk, onComplete, onError, sessionId } = options;
|
|
113
|
+
|
|
114
|
+
return new Promise((resolve, reject) => {
|
|
115
|
+
this.queue.push({
|
|
116
|
+
message,
|
|
117
|
+
sessionId,
|
|
118
|
+
onChunk,
|
|
119
|
+
onComplete,
|
|
120
|
+
onError,
|
|
121
|
+
resolve,
|
|
122
|
+
reject,
|
|
123
|
+
});
|
|
124
|
+
this.process();
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 推送消息到队列(供回调中使用)
|
|
129
|
+
push(message, options = {}) {
|
|
130
|
+
return this.enqueue(message, options);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 按顺序处理
|
|
134
|
+
async process() {
|
|
135
|
+
if (this.isProcessing || this.queue.length === 0) return;
|
|
136
|
+
|
|
137
|
+
this.isProcessing = true;
|
|
138
|
+
const task = this.queue.shift();
|
|
139
|
+
|
|
140
|
+
// 创建 push 上下文,传递给回调
|
|
141
|
+
const ctx = {
|
|
142
|
+
push: (msg, opts = {}) => {
|
|
143
|
+
this.push(msg, { ...task.options, ...opts });
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
const fullText = await this.sendMessage(task);
|
|
149
|
+
task.resolve(fullText);
|
|
150
|
+
if (task.onComplete) {
|
|
151
|
+
await task.onComplete(fullText, ctx);
|
|
152
|
+
}
|
|
153
|
+
} catch (error) {
|
|
154
|
+
task.reject(error);
|
|
155
|
+
if (task.onError) {
|
|
156
|
+
await task.onError(error, ctx);
|
|
157
|
+
}
|
|
158
|
+
} finally {
|
|
159
|
+
this.isProcessing = false;
|
|
160
|
+
this.process(); // 处理下一个
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// 发送消息,使用 chatStream 流式输出
|
|
165
|
+
async sendMessage(task) {
|
|
166
|
+
const { message, sessionId, onChunk } = task;
|
|
167
|
+
let fullText = '';
|
|
168
|
+
|
|
169
|
+
for await (const chunk of this.chatHandler.chatStream(message, { sessionId })) {
|
|
170
|
+
fullText += chunk.text || '';
|
|
171
|
+
if (onChunk) {
|
|
172
|
+
onChunk(chunk);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return fullText;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
99
180
|
class AgentChatHandler extends EventEmitter {
|
|
100
181
|
/**
|
|
101
182
|
* @param {Agent} agent - Agent 实例
|
|
@@ -147,6 +228,9 @@ class AgentChatHandler extends EventEmitter {
|
|
|
147
228
|
this._sessionQueues = new Map(); // sessionId -> Queue of {message, options, resolve, reject}
|
|
148
229
|
this._processingSessions = new Set(); // sessionId -> processing flag
|
|
149
230
|
|
|
231
|
+
// 流式消息队列(用于 continue)
|
|
232
|
+
this._chatStreamQueue = new ChatStreamQueue(this);
|
|
233
|
+
|
|
150
234
|
// Session Memory prepareStep(从 memory 插件获取)
|
|
151
235
|
this._sessionMemoryPrepareStep = null;
|
|
152
236
|
if (agent?.framework) {
|
|
@@ -1176,191 +1260,184 @@ ${truncatedContent}${truncatedNote}
|
|
|
1176
1260
|
}
|
|
1177
1261
|
|
|
1178
1262
|
/**
|
|
1179
|
-
*
|
|
1263
|
+
* 继续完成之前的响应
|
|
1264
|
+
* 当流式响应提前结束时,调用此方法设置 continue 标志
|
|
1265
|
+
* @param {string} prompt - 继续时的提示文本
|
|
1266
|
+
* @param {string} sessionId - 会话 ID
|
|
1267
|
+
* @param {Object} options - 选项 { onChunk, onComplete, onError }
|
|
1268
|
+
*/
|
|
1269
|
+
continue(prompt, sessionId = null, options = {}) {
|
|
1270
|
+
const queue = this._chatStreamQueue;
|
|
1271
|
+
const finalOptions = { sessionId, ...options };
|
|
1272
|
+
|
|
1273
|
+
// enqueue 返回 Promise
|
|
1274
|
+
const promise = queue.enqueue(
|
|
1275
|
+
prompt || '请继续完成你的回答,输出之前的完整内容。',
|
|
1276
|
+
finalOptions
|
|
1277
|
+
);
|
|
1278
|
+
|
|
1279
|
+
// 返回包含 push 方法的上下文
|
|
1280
|
+
return {
|
|
1281
|
+
push: (msg, opts = {}) => {
|
|
1282
|
+
queue.push(msg, { sessionId, ...opts });
|
|
1283
|
+
},
|
|
1284
|
+
then: promise.then.bind(promise),
|
|
1285
|
+
catch: promise.catch.bind(promise),
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
/**
|
|
1290
|
+
* 准备聊天会话:刷新系统提示词、添加用户消息、压缩上下文
|
|
1180
1291
|
* @param {string|Object} message - 消息
|
|
1181
|
-
* @param {
|
|
1292
|
+
* @param {string} sessionId - 会话 ID
|
|
1293
|
+
* @returns {Object} - { messageStore, messages, tools, maxSteps }
|
|
1182
1294
|
* @private
|
|
1183
1295
|
*/
|
|
1184
|
-
async
|
|
1185
|
-
const sessionId = options.sessionId || null;
|
|
1186
|
-
const framework = this.agent.framework;
|
|
1187
|
-
|
|
1188
|
-
// 获取 Per-Session 的消息存储(自动从 session 加载)
|
|
1296
|
+
async _prepareSession(message, sessionId) {
|
|
1189
1297
|
const messageStore = this._getSessionMessageStore(sessionId);
|
|
1190
1298
|
const messages = messageStore.messages;
|
|
1191
1299
|
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
// 解决上下文过长时 LLM 不调用工具的问题
|
|
1195
|
-
this._systemPrompt = this.agent._buildSystemPrompt();
|
|
1196
|
-
// 动态导入 AI SDK
|
|
1197
|
-
const { tool, ToolLoopAgent } = await this._importAI();
|
|
1300
|
+
// 刷新系统提示词
|
|
1301
|
+
this._systemPrompt = this.agent._buildSystemPrompt();
|
|
1198
1302
|
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
// 检查是否需要压缩上下文(包括工具定义)
|
|
1204
|
-
const messagesTokens = this._countMessagesTokens(messages);
|
|
1205
|
-
const toolsTokens = this._countToolsTokens();
|
|
1206
|
-
const systemPromptTokens = this._countTokens(this._systemPrompt);
|
|
1207
|
-
const totalTokens = messagesTokens + toolsTokens + systemPromptTokens;
|
|
1208
|
-
const limit = this._maxContextTokens * 0.5; // 降低到 50%,更早压缩,防止 context window 超限
|
|
1209
|
-
|
|
1210
|
-
if (totalTokens > limit) {
|
|
1211
|
-
logger.info(
|
|
1212
|
-
`Context large (${totalTokens}/${this._maxContextTokens} tokens = msgs:${messagesTokens} + tools:${toolsTokens} + sys:${systemPromptTokens}), compressing...`
|
|
1213
|
-
);
|
|
1214
|
-
// 使用带超时控制的压缩(传入 messages 引用)
|
|
1215
|
-
await this._compressContext(sessionId, messages, messageStore);
|
|
1216
|
-
// 压缩后验证消息配对(防止 orphaned tool result)
|
|
1217
|
-
const validatedMessages = this._validateMessagesPairing(messages);
|
|
1218
|
-
if (validatedMessages.length !== messages.length) {
|
|
1219
|
-
messages.length = 0;
|
|
1220
|
-
messages.push(...validatedMessages);
|
|
1221
|
-
}
|
|
1222
|
-
}
|
|
1303
|
+
// 添加用户消息
|
|
1304
|
+
const userMessage = typeof message === 'string' ? { role: 'user', content: message } : message;
|
|
1305
|
+
messages.push(userMessage);
|
|
1223
1306
|
|
|
1224
|
-
|
|
1225
|
-
|
|
1307
|
+
// 检查上下文大小,必要时压缩
|
|
1308
|
+
const messagesTokens = this._countMessagesTokens(messages);
|
|
1309
|
+
const toolsTokens = this._countToolsTokens();
|
|
1310
|
+
const systemPromptTokens = this._countTokens(this._systemPrompt);
|
|
1311
|
+
const totalTokens = messagesTokens + toolsTokens + systemPromptTokens;
|
|
1312
|
+
const limit = this._maxContextTokens * 0.5;
|
|
1226
1313
|
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1314
|
+
if (totalTokens > limit) {
|
|
1315
|
+
logger.info(
|
|
1316
|
+
`Context large (${totalTokens}/${this._maxContextTokens} tokens), compressing...`
|
|
1317
|
+
);
|
|
1318
|
+
await this._compressContext(sessionId, messages, messageStore);
|
|
1319
|
+
const validated = this._validateMessagesPairing(messages);
|
|
1320
|
+
if (validated.length !== messages.length) {
|
|
1230
1321
|
messages.length = 0;
|
|
1231
|
-
messages.push(...
|
|
1322
|
+
messages.push(...validated);
|
|
1232
1323
|
}
|
|
1324
|
+
}
|
|
1233
1325
|
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
const finalSystemPromptTokens = this._countTokens(this._systemPrompt);
|
|
1241
|
-
const finalTotalTokens = finalMessagesTokens + finalToolsTokens + finalSystemPromptTokens;
|
|
1242
|
-
const finalLimit = this._maxContextTokens * 0.5;
|
|
1243
|
-
|
|
1244
|
-
// logger.info(
|
|
1245
|
-
// `[API Call Check] messages=${messages.length}, tokens=(${finalMessagesTokens}+${finalToolsTokens}+${finalSystemPromptTokens}=${finalTotalTokens}) vs limit=${finalLimit}`
|
|
1246
|
-
// );
|
|
1247
|
-
|
|
1248
|
-
// 如果仍然超过限制,强制压缩
|
|
1249
|
-
if (finalTotalTokens > finalLimit) {
|
|
1250
|
-
logger.warn(`[API Call Check] Still over limit after validation, forcing compression`);
|
|
1251
|
-
await this._compressContext(sessionId, messages, messageStore);
|
|
1252
|
-
const compressedMessages = this._validateMessagesPairing(messages);
|
|
1253
|
-
messages.length = 0;
|
|
1254
|
-
messages.push(...compressedMessages);
|
|
1255
|
-
const afterTokens = this._countMessagesTokens(messages);
|
|
1256
|
-
logger.info(
|
|
1257
|
-
`[After Forced Compression] messages=${messages.length}, tokens=${afterTokens}`
|
|
1258
|
-
);
|
|
1259
|
-
}
|
|
1326
|
+
// 验证消息配对
|
|
1327
|
+
const validatedMessages = this._validateMessagesPairing(messages);
|
|
1328
|
+
if (validatedMessages.length !== messages.length) {
|
|
1329
|
+
messages.length = 0;
|
|
1330
|
+
messages.push(...validatedMessages);
|
|
1331
|
+
}
|
|
1260
1332
|
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
try {
|
|
1264
|
-
// 1. 验证消息格式
|
|
1265
|
-
if (!Array.isArray(inputMessages)) {
|
|
1266
|
-
logger.warn('prepareStep received non-array messages');
|
|
1267
|
-
return { messages: inputMessages };
|
|
1268
|
-
}
|
|
1333
|
+
// 验证工具调用
|
|
1334
|
+
this._validateToolCalls(messages);
|
|
1269
1335
|
|
|
1270
|
-
|
|
1271
|
-
|
|
1336
|
+
// 最终 token 检查
|
|
1337
|
+
const finalTokens =
|
|
1338
|
+
this._countMessagesTokens(messages) +
|
|
1339
|
+
this._countToolsTokens() +
|
|
1340
|
+
this._countTokens(this._systemPrompt);
|
|
1341
|
+
if (finalTokens > this._maxContextTokens * 0.5) {
|
|
1342
|
+
logger.warn('Still over limit after validation, forcing compression');
|
|
1343
|
+
await this._compressContext(sessionId, messages, messageStore);
|
|
1344
|
+
const compressed = this._validateMessagesPairing(messages);
|
|
1345
|
+
messages.length = 0;
|
|
1346
|
+
messages.push(...compressed);
|
|
1347
|
+
}
|
|
1272
1348
|
|
|
1273
|
-
|
|
1274
|
-
const pairedMessages = this._validateMessagesPairing(inputMessages);
|
|
1275
|
-
if (pairedMessages.length !== inputMessages.length) {
|
|
1276
|
-
logger.debug(
|
|
1277
|
-
`prepareStep pairing: ${inputMessages.length} -> ${pairedMessages.length}`
|
|
1278
|
-
);
|
|
1279
|
-
}
|
|
1280
|
-
// 重要:用配对验证后的结果替换 inputMessages
|
|
1281
|
-
inputMessages.length = 0;
|
|
1282
|
-
inputMessages.push(...pairedMessages);
|
|
1349
|
+
const maxSteps = this._maxSteps;
|
|
1283
1350
|
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
const tokenLimit = this._maxContextTokens * 0.75; // 75% 阈值,保留缓冲空间
|
|
1287
|
-
if (inputMessages.length <= 30 && tokenCount <= tokenLimit) {
|
|
1288
|
-
return { messages: inputMessages }; // 消息少且 token 未超限,不修剪
|
|
1289
|
-
}
|
|
1351
|
+
return { messageStore, messages, maxSteps };
|
|
1352
|
+
}
|
|
1290
1353
|
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1354
|
+
/**
|
|
1355
|
+
* 创建 prepareStep 函数(供 AI SDK 调用)
|
|
1356
|
+
* @returns {Function}
|
|
1357
|
+
* @private
|
|
1358
|
+
*/
|
|
1359
|
+
_createPrepareStep() {
|
|
1360
|
+
return async ({ stepNumber, messages: inputMessages }) => {
|
|
1361
|
+
try {
|
|
1362
|
+
if (!Array.isArray(inputMessages)) {
|
|
1363
|
+
return { messages: inputMessages };
|
|
1364
|
+
}
|
|
1294
1365
|
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1366
|
+
this._validateToolCalls(inputMessages);
|
|
1367
|
+
const pairedMessages = this._validateMessagesPairing(inputMessages);
|
|
1368
|
+
if (pairedMessages.length !== inputMessages.length) {
|
|
1369
|
+
inputMessages.length = 0;
|
|
1370
|
+
inputMessages.push(...pairedMessages);
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
const tokenCount = this._countMessagesTokens(inputMessages);
|
|
1374
|
+
const tokenLimit = this._maxContextTokens * 0.75;
|
|
1375
|
+
if (inputMessages.length <= 30 && tokenCount <= tokenLimit) {
|
|
1299
1376
|
return { messages: inputMessages };
|
|
1300
1377
|
}
|
|
1301
|
-
|
|
1378
|
+
|
|
1379
|
+
const pruned = this._prepareMessagesForAI(inputMessages);
|
|
1380
|
+
return { messages: pruned };
|
|
1381
|
+
} catch (err) {
|
|
1382
|
+
logger.error('prepareStep error:', err.message);
|
|
1383
|
+
return { messages: inputMessages };
|
|
1384
|
+
}
|
|
1385
|
+
};
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
/**
|
|
1389
|
+
* 执行实际聊天逻辑
|
|
1390
|
+
* @param {string|Object} message - 消息
|
|
1391
|
+
* @param {Object} options - 选项
|
|
1392
|
+
* @private
|
|
1393
|
+
*/
|
|
1394
|
+
async _doChat(message, options = {}) {
|
|
1395
|
+
const sessionId = options.sessionId || null;
|
|
1396
|
+
const framework = this.agent.framework;
|
|
1397
|
+
|
|
1398
|
+
try {
|
|
1399
|
+
const { tool, ToolLoopAgent } = await this._importAI();
|
|
1400
|
+
const { messageStore, messages, maxSteps } = await this._prepareSession(message, sessionId);
|
|
1302
1401
|
|
|
1303
1402
|
if (!this._aiClient) {
|
|
1304
1403
|
throw new Error('AI client not configured.');
|
|
1305
1404
|
}
|
|
1306
1405
|
|
|
1307
|
-
|
|
1406
|
+
const tools = this._getAITools(tool);
|
|
1407
|
+
|
|
1308
1408
|
const agent = new ToolLoopAgent({
|
|
1309
1409
|
model: this._aiClient,
|
|
1310
1410
|
instructions: this._systemPrompt,
|
|
1311
1411
|
tools: tools,
|
|
1312
|
-
prepareStep:
|
|
1412
|
+
prepareStep: this._createPrepareStep(),
|
|
1313
1413
|
});
|
|
1314
1414
|
|
|
1315
|
-
// 使用 runInSession 让工具执行时能获取 sessionId(Per-Session 隔离)
|
|
1316
1415
|
const result = await framework.runInSession(sessionId, {}, async () => {
|
|
1317
1416
|
return agent.generate({ messages, ...this.providerOptions });
|
|
1318
1417
|
});
|
|
1319
1418
|
|
|
1320
|
-
// 捕获 API 返回的 usage 用于更准确的 token 估算
|
|
1321
1419
|
if (result.usage) {
|
|
1322
1420
|
this._updateMessageStoreUsage(messageStore, result.usage);
|
|
1323
|
-
logger.debug(
|
|
1324
|
-
`API usage: input=${result.usage.inputTokens}, output=${result.usage.outputTokens}`
|
|
1325
|
-
);
|
|
1326
1421
|
}
|
|
1327
1422
|
|
|
1328
1423
|
messages.push(...result.response.messages);
|
|
1329
|
-
|
|
1330
|
-
// 触发 agent:message 事件,让 memory 插件可以自动提取记忆
|
|
1331
1424
|
const userMsg = messages[messages.length - result.response.messages.length - 1];
|
|
1332
1425
|
this.emit('message', { content: result.text, sessionId: sessionId, userMessage: userMsg });
|
|
1333
1426
|
|
|
1334
|
-
// 生成后检查:如果消息太长,下次需要压缩
|
|
1335
|
-
const afterTokens = this._countMessagesTokens(messages);
|
|
1336
|
-
if (afterTokens > this._maxContextTokens * 0.5) {
|
|
1337
|
-
logger.info(`After generation: ${afterTokens} tokens, will compress on next turn`);
|
|
1338
|
-
}
|
|
1339
|
-
|
|
1340
1427
|
return {
|
|
1341
1428
|
success: true,
|
|
1342
1429
|
message: result.text || '',
|
|
1343
1430
|
stepCount: result.stepCount || 1,
|
|
1344
1431
|
};
|
|
1345
1432
|
} catch (err) {
|
|
1346
|
-
// 捕获 AI API 瞬时错误(如 Invalid JSON response、服务负载过高等)
|
|
1347
1433
|
const errorMsg = err.message || String(err);
|
|
1348
|
-
const causeMsg = err.cause?.message || String(err.cause) || '';
|
|
1349
|
-
const fullMsg = errorMsg + causeMsg;
|
|
1350
|
-
|
|
1351
1434
|
const isTransientError =
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
fullMsg.includes('timeout') ||
|
|
1356
|
-
fullMsg.includes('retry') ||
|
|
1435
|
+
errorMsg.includes('Invalid JSON response') ||
|
|
1436
|
+
errorMsg.includes('负载较高') ||
|
|
1437
|
+
errorMsg.includes('Failed after') ||
|
|
1357
1438
|
err.name === 'AI_RetryError' ||
|
|
1358
1439
|
err.name === 'AI_APICallError';
|
|
1359
1440
|
|
|
1360
|
-
logger.warn(
|
|
1361
|
-
`[AgentChat] Error caught in _doChat: msg="${errorMsg}", name=${err.name}, isTransient=${isTransientError}`
|
|
1362
|
-
);
|
|
1363
|
-
|
|
1364
1441
|
if (isTransientError) {
|
|
1365
1442
|
return {
|
|
1366
1443
|
success: false,
|
|
@@ -1370,15 +1447,9 @@ ${truncatedContent}${truncatedNote}
|
|
|
1370
1447
|
};
|
|
1371
1448
|
}
|
|
1372
1449
|
|
|
1373
|
-
// 未知错误,打印调试信息后继续抛出
|
|
1374
|
-
logger.error(
|
|
1375
|
-
`[AgentChat] Unknown error in _doChat: ${err.message}, name: ${err.name}, cause: ${err.cause?.message || err.cause}`
|
|
1376
|
-
);
|
|
1377
1450
|
throw err;
|
|
1378
1451
|
} finally {
|
|
1379
|
-
|
|
1380
|
-
this._validateToolCalls(messages);
|
|
1381
|
-
// 确保保存聊天历史到 session(无论成功还是失败)
|
|
1452
|
+
const messageStore = this._getSessionMessageStore(sessionId);
|
|
1382
1453
|
messageStore.save();
|
|
1383
1454
|
}
|
|
1384
1455
|
}
|
|
@@ -1392,144 +1463,30 @@ ${truncatedContent}${truncatedNote}
|
|
|
1392
1463
|
const sessionId = options.sessionId || null;
|
|
1393
1464
|
const framework = this.agent.framework;
|
|
1394
1465
|
|
|
1395
|
-
// 获取 Per-Session 的消息存储
|
|
1396
|
-
const messageStore = this._getSessionMessageStore(sessionId);
|
|
1397
|
-
const messages = messageStore.messages;
|
|
1398
|
-
|
|
1399
1466
|
try {
|
|
1400
|
-
// 关键:每次 chat 调用时刷新系统提示词,确保包含最新的工具/技能描述
|
|
1401
|
-
// 解决上下文过长时 LLM 不调用工具的问题
|
|
1402
|
-
this._systemPrompt = this.agent._buildSystemPrompt();
|
|
1403
|
-
// 动态导入 AI SDK
|
|
1404
1467
|
const { tool, ToolLoopAgent } = await this._importAI();
|
|
1405
|
-
|
|
1406
|
-
const userMessage =
|
|
1407
|
-
typeof message === 'string' ? { role: 'user', content: message } : message;
|
|
1408
|
-
messages.push(userMessage);
|
|
1409
|
-
|
|
1410
|
-
// 检查是否需要压缩上下文(包括工具定义)
|
|
1411
|
-
const messagesTokens = this._countMessagesTokens(messages);
|
|
1412
|
-
const toolsTokens = this._countToolsTokens();
|
|
1413
|
-
const systemPromptTokens = this._countTokens(this._systemPrompt);
|
|
1414
|
-
const totalTokens = messagesTokens + toolsTokens + systemPromptTokens;
|
|
1415
|
-
const limit = this._maxContextTokens * 0.5; // 降低到 50%,更早压缩,防止 context window 超限
|
|
1416
|
-
// 对于流式调用,如果上下文太大,先压缩再开始
|
|
1417
|
-
if (totalTokens > limit) {
|
|
1418
|
-
logger.info(
|
|
1419
|
-
`Context large (${totalTokens}/${this._maxContextTokens} tokens), compressing...`
|
|
1420
|
-
);
|
|
1421
|
-
// 流式调用时等待压缩完成(使用带超时控制的压缩)
|
|
1422
|
-
await this._compressContext(sessionId, messages, messageStore);
|
|
1423
|
-
// 压缩后验证消息配对(防止 orphaned tool result)
|
|
1424
|
-
const validatedMessages = this._validateMessagesPairing(messages);
|
|
1425
|
-
if (validatedMessages.length !== messages.length) {
|
|
1426
|
-
messages.length = 0;
|
|
1427
|
-
messages.push(...validatedMessages);
|
|
1428
|
-
}
|
|
1429
|
-
}
|
|
1468
|
+
const { messageStore, messages } = await this._prepareSession(message, sessionId);
|
|
1430
1469
|
|
|
1431
|
-
const maxSteps = options.maxSteps || this._maxSteps;
|
|
1432
|
-
const tools = this._getAITools(tool);
|
|
1433
1470
|
if (!this._aiClient) {
|
|
1434
1471
|
throw new Error('AI client not configured.');
|
|
1435
1472
|
}
|
|
1436
1473
|
|
|
1437
|
-
|
|
1438
|
-
const validatedMessages = this._validateMessagesPairing(messages);
|
|
1439
|
-
if (validatedMessages.length !== messages.length) {
|
|
1440
|
-
messages.length = 0;
|
|
1441
|
-
messages.push(...validatedMessages);
|
|
1442
|
-
}
|
|
1443
|
-
|
|
1444
|
-
// 验证并修复消息中的不完整工具调用(在 API 调用前执行,防止开始聊天的时候就报错)
|
|
1445
|
-
this._validateToolCalls(messages);
|
|
1446
|
-
|
|
1447
|
-
// 最终检查:在 API 调用前再次验证 token 数
|
|
1448
|
-
const finalMessagesTokens = this._countMessagesTokens(messages);
|
|
1449
|
-
const finalToolsTokens = this._countToolsTokens();
|
|
1450
|
-
const finalSystemPromptTokens = this._countTokens(this._systemPrompt);
|
|
1451
|
-
const finalTotalTokens = finalMessagesTokens + finalToolsTokens + finalSystemPromptTokens;
|
|
1452
|
-
const finalLimit = this._maxContextTokens * 0.5;
|
|
1453
|
-
|
|
1454
|
-
logger.info(
|
|
1455
|
-
`[API Call Check (stream)] messages=${messages.length}, tokens=(${finalMessagesTokens}+${finalToolsTokens}+${finalSystemPromptTokens}=${finalTotalTokens}) vs limit=${finalLimit}`
|
|
1456
|
-
);
|
|
1457
|
-
|
|
1458
|
-
// 如果仍然超过限制,强制压缩
|
|
1459
|
-
if (finalTotalTokens > finalLimit) {
|
|
1460
|
-
logger.warn(`[API Call Check (stream)] Still over limit, forcing compression`);
|
|
1461
|
-
await this._compressContext(sessionId, messages, messageStore);
|
|
1462
|
-
const compressedMessages = this._validateMessagesPairing(messages);
|
|
1463
|
-
messages.length = 0;
|
|
1464
|
-
messages.push(...compressedMessages);
|
|
1465
|
-
const afterTokens = this._countMessagesTokens(messages);
|
|
1466
|
-
logger.info(
|
|
1467
|
-
`[After Forced Compression (stream)] messages=${messages.length}, tokens=${afterTokens}`
|
|
1468
|
-
);
|
|
1469
|
-
}
|
|
1470
|
-
|
|
1471
|
-
// 准备传给 agent 的消息 (流式版本)
|
|
1472
|
-
const prepareStepChainStream = async ({ stepNumber, messages: inputMessages }) => {
|
|
1473
|
-
try {
|
|
1474
|
-
// 1. 验证消息格式
|
|
1475
|
-
if (!Array.isArray(inputMessages)) {
|
|
1476
|
-
logger.warn('prepareStep received non-array messages');
|
|
1477
|
-
return { messages: inputMessages };
|
|
1478
|
-
}
|
|
1479
|
-
|
|
1480
|
-
// 2. 修复不完整的工具调用(input 只有 { 或其他无效值)
|
|
1481
|
-
this._validateToolCalls(inputMessages);
|
|
1482
|
-
|
|
1483
|
-
// 3. 验证消息配对并更新 inputMessages(防止 API 报 orphaned tool result 错误)
|
|
1484
|
-
const pairedMessages = this._validateMessagesPairing(inputMessages);
|
|
1485
|
-
if (pairedMessages.length !== inputMessages.length) {
|
|
1486
|
-
logger.debug(
|
|
1487
|
-
`prepareStep pairing: ${inputMessages.length} -> ${pairedMessages.length}`
|
|
1488
|
-
);
|
|
1489
|
-
}
|
|
1490
|
-
// 重要:用配对验证后的结果替换 inputMessages
|
|
1491
|
-
inputMessages.length = 0;
|
|
1492
|
-
inputMessages.push(...pairedMessages);
|
|
1493
|
-
|
|
1494
|
-
// 4. Token 数检查:超过限制才修剪
|
|
1495
|
-
const tokenCount = this._countMessagesTokens(inputMessages);
|
|
1496
|
-
const tokenLimit = this._maxContextTokens * 0.75; // 75% 阈值,保留缓冲空间
|
|
1497
|
-
if (inputMessages.length <= 30 && tokenCount <= tokenLimit) {
|
|
1498
|
-
return { messages: inputMessages }; // 消息少且 token 未超限,不修剪
|
|
1499
|
-
}
|
|
1500
|
-
|
|
1501
|
-
// 5. 保留配对完整的消息(使用 AI SDK 标准格式)
|
|
1502
|
-
const pruned = this._prepareMessagesForAI(inputMessages);
|
|
1503
|
-
logger.debug(`prepareStep pruned: ${inputMessages.length} -> ${pruned.length} messages`);
|
|
1504
|
-
|
|
1505
|
-
// 6. 返回 AI SDK 6.x 要求的格式
|
|
1506
|
-
return { messages: pruned };
|
|
1507
|
-
} catch (err) {
|
|
1508
|
-
logger.error('prepareStep error:', err.message);
|
|
1509
|
-
return { messages: inputMessages };
|
|
1510
|
-
}
|
|
1511
|
-
};
|
|
1474
|
+
const tools = this._getAITools(tool);
|
|
1512
1475
|
|
|
1513
1476
|
const agent = new ToolLoopAgent({
|
|
1514
1477
|
model: this._aiClient,
|
|
1515
1478
|
instructions: this._systemPrompt,
|
|
1516
1479
|
tools: tools,
|
|
1517
1480
|
stopWhen: isLoopFinished(),
|
|
1518
|
-
prepareStep:
|
|
1481
|
+
prepareStep: this._createPrepareStep(),
|
|
1519
1482
|
});
|
|
1520
1483
|
|
|
1521
|
-
// 使用 runInSession 让工具执行时能获取 sessionId(Per-Session 隔离)
|
|
1522
1484
|
const result = await framework.runInSession(sessionId, {}, async () => {
|
|
1523
|
-
return agent.stream({
|
|
1524
|
-
messages,
|
|
1525
|
-
...this.providerOptions,
|
|
1526
|
-
});
|
|
1485
|
+
return agent.stream({ messages, ...this.providerOptions });
|
|
1527
1486
|
});
|
|
1528
1487
|
|
|
1529
1488
|
const stream = result.fullStream;
|
|
1530
1489
|
let fullText = '';
|
|
1531
|
-
|
|
1532
|
-
// 流式迭代器
|
|
1533
1490
|
const iterator = stream[Symbol.asyncIterator] ? stream : stream.fullStream;
|
|
1534
1491
|
|
|
1535
1492
|
for await (const part of iterator || stream) {
|
|
@@ -1538,7 +1495,8 @@ ${truncatedContent}${truncatedNote}
|
|
|
1538
1495
|
fullText += text;
|
|
1539
1496
|
yield { type: 'text', text };
|
|
1540
1497
|
} else if (part.type === 'reasoning') {
|
|
1541
|
-
this.emit('thinking', { content: part.text });
|
|
1498
|
+
// this.emit('thinking', { content: part.text });
|
|
1499
|
+
yield { type: 'thinking', text: part.text };
|
|
1542
1500
|
} else if (part.type === 'tool-call') {
|
|
1543
1501
|
yield { type: 'tool-call', toolName: part.toolName, input: part.input };
|
|
1544
1502
|
} else if (part.type === 'tool-result') {
|
|
@@ -1550,49 +1508,30 @@ ${truncatedContent}${truncatedNote}
|
|
|
1550
1508
|
|
|
1551
1509
|
const finishMessages = (await result.response).messages;
|
|
1552
1510
|
messages.push(...finishMessages);
|
|
1553
|
-
// 捕获 API 返回的 usage 用于更准确的 token 估算
|
|
1554
1511
|
const usage = await result.totalUsage;
|
|
1555
1512
|
if (usage) {
|
|
1556
|
-
|
|
1557
|
-
this._updateMessageStoreUsage(messageStore, usage);
|
|
1558
|
-
logger.debug(
|
|
1559
|
-
`API usage (stream): input=${usage.inputTokens}, output=${usage.outputTokens}`
|
|
1560
|
-
);
|
|
1561
|
-
} catch (e) {
|
|
1562
|
-
logger.debug('Failed to capture stream usage:', e.message);
|
|
1563
|
-
}
|
|
1513
|
+
this._updateMessageStoreUsage(messageStore, usage);
|
|
1564
1514
|
}
|
|
1565
1515
|
|
|
1566
|
-
// 触发 agent:message 事件,让 memory 插件可以自动提取记忆
|
|
1567
1516
|
const userMsg = messages[messages.length - finishMessages.length - 1];
|
|
1568
1517
|
this.emit('message', { content: fullText, sessionId: sessionId, userMessage: userMsg });
|
|
1569
1518
|
} catch (err) {
|
|
1570
|
-
// 捕获 AI API 瞬时错误
|
|
1571
1519
|
const errorMsg = err.message || String(err);
|
|
1572
|
-
const causeMsg = err.cause?.message || String(err.cause) || '';
|
|
1573
|
-
const fullMsg = errorMsg + causeMsg;
|
|
1574
|
-
|
|
1575
1520
|
const isTransientError =
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
fullMsg.includes('Failed after') ||
|
|
1579
|
-
fullMsg.includes('timeout') ||
|
|
1580
|
-
fullMsg.includes('retry') ||
|
|
1521
|
+
errorMsg.includes('Invalid JSON response') ||
|
|
1522
|
+
errorMsg.includes('负载较高') ||
|
|
1581
1523
|
err.name === 'AI_RetryError' ||
|
|
1582
1524
|
err.name === 'AI_APICallError';
|
|
1583
1525
|
|
|
1584
1526
|
if (isTransientError) {
|
|
1585
|
-
logger.warn(`[AgentChat] Transient AI error in stream: ${errorMsg}`);
|
|
1586
1527
|
yield { type: 'error', error: 'AI 服务暂时不可用,请稍后重试。' };
|
|
1587
1528
|
} else {
|
|
1588
|
-
logger.error(`[AgentChat] Stream error: ${err.message}
|
|
1529
|
+
logger.error(`[AgentChat] Stream error: ${err.message}`);
|
|
1589
1530
|
this.emit('error', { error: err.message });
|
|
1590
1531
|
yield { type: 'error', error: err.message };
|
|
1591
1532
|
}
|
|
1592
1533
|
} finally {
|
|
1593
|
-
|
|
1594
|
-
this._validateToolCalls(messages);
|
|
1595
|
-
// 确保保存聊天历史到 session(无论成功还是失败)
|
|
1534
|
+
const messageStore = this._getSessionMessageStore(sessionId);
|
|
1596
1535
|
messageStore.save();
|
|
1597
1536
|
}
|
|
1598
1537
|
}
|