foliko 1.1.21 → 1.1.23
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/mcp_config.json +1 -0
- package/.agent/sessions/cli_default.json +72 -2212
- package/.claude/settings.local.json +13 -1
- package/cli/src/commands/chat.js +1 -1
- package/cli/src/ui/chat-ui.js +15 -9
- package/examples/test-chat.js +7 -1
- package/package.json +2 -1
- package/plugins/telegram-plugin.js +8 -0
- package/plugins/weixin-plugin.js +1 -3
- package/poster_sanhuali.svg +1 -0
- package/src/core/agent-chat.js +17 -37
- package/src/core/context-compressor.js +52 -36
- package/src/core/framework.js +4 -0
- package/src/core/subagent.js +34 -9
- package/src/utils/chat-queue.js +12 -30
- package/src/utils/logger.js +6 -3
- package/undefined.svg +1 -8
- package/.agent/data/web/web-config.json +0 -30
- package/nul +0 -3
- package/weixin-bot-poster-v2.png +0 -0
- package/weixin-bot-poster.png +0 -0
|
@@ -204,7 +204,19 @@
|
|
|
204
204
|
"Bash(node -e \"require\\('./src/core/agent-chat.js'\\); console.log\\('OK'\\)\" 2>&1)",
|
|
205
205
|
"Bash(node -e \"const ai = require\\('ai'\\); console.log\\('tool type:', typeof ai.tool\\)\")",
|
|
206
206
|
"Bash(taskkill //PID 26152 //F)",
|
|
207
|
-
"Bash(netstat -ano | grep 3000)"
|
|
207
|
+
"Bash(netstat -ano | grep 3000)",
|
|
208
|
+
"Bash(node test-error.js 2>&1)",
|
|
209
|
+
"Bash(node -c src/core/agent-chat.js && node -c src/utils/chat-queue.js && node -c cli/src/ui/chat-ui.js && node -c plugins/weixin-plugin.js && node -c plugins/telegram-plugin.js)",
|
|
210
|
+
"Bash(node -c plugins/weixin-plugin.js && node -c plugins/telegram-plugin.js && node -c cli/src/ui/chat-ui.js)",
|
|
211
|
+
"Bash(node -c src/utils/chat-queue.js)",
|
|
212
|
+
"Bash(node -c src/utils/logger.js)",
|
|
213
|
+
"WebFetch(domain:github.com)",
|
|
214
|
+
"mcp__MiniMax__web_search",
|
|
215
|
+
"WebFetch(domain:www.npmjs.com)",
|
|
216
|
+
"Bash(cd D:/code/vb-agent && npm show @anthropic-ai/tokenizer 2>&1 | head -30)",
|
|
217
|
+
"Bash(cd D:/code/vb-agent && node -e \"const t = require\\('@anthropic-ai/tokenizer'\\); console.log\\(typeof t.countTokens\\)\" 2>&1)",
|
|
218
|
+
"Bash(cd D:/code/vb-agent && node -e \"const { countTokens } = require\\('@anthropic-ai/tokenizer'\\); console.log\\('hello:', countTokens\\('hello'\\)\\); console.log\\('你好:', countTokens\\('你好'\\)\\);\")",
|
|
219
|
+
"Bash(node -c src/core/subagent.js)"
|
|
208
220
|
]
|
|
209
221
|
}
|
|
210
222
|
}
|
package/cli/src/commands/chat.js
CHANGED
|
@@ -126,7 +126,7 @@ async function runContinuousTest(agent, options) {
|
|
|
126
126
|
} else if (chunk.type === 'tool-call') {
|
|
127
127
|
console.log(`\n[工具调用] ${chunk.toolName} ${JSON.stringify(chunk.input)}`);
|
|
128
128
|
} else if (chunk.type === 'error') {
|
|
129
|
-
console.error(`\n[错误] ${chunk.error}`);
|
|
129
|
+
//console.error(`\n[错误] ${chunk.error}`);
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
132
|
console.log('\n' + '-'.repeat(50));
|
package/cli/src/ui/chat-ui.js
CHANGED
|
@@ -136,6 +136,8 @@ class ChatUI {
|
|
|
136
136
|
|
|
137
137
|
process.on('SIGINT', interruptHandler);
|
|
138
138
|
|
|
139
|
+
let hasError = false;
|
|
140
|
+
|
|
139
141
|
try {
|
|
140
142
|
const renderState = { inThink: false, inCodeBlock: false };
|
|
141
143
|
const { sessionId } = this;
|
|
@@ -150,7 +152,6 @@ class ChatUI {
|
|
|
150
152
|
// 创建 session scope 过滤事件
|
|
151
153
|
const sessionScope = this.agent.createSessionScope(sessionId);
|
|
152
154
|
|
|
153
|
-
// 监听流式数据
|
|
154
155
|
sessionScope.on('stream:chunk', ({ chunk }) => {
|
|
155
156
|
if (interrupted) return;
|
|
156
157
|
|
|
@@ -167,11 +168,15 @@ class ChatUI {
|
|
|
167
168
|
console.log(renderLine(line, renderState));
|
|
168
169
|
}
|
|
169
170
|
}
|
|
170
|
-
} else if (chunk.type === 'error') {
|
|
171
|
-
console.error(`\n${colored('[错误]', RED)} ${chunk.error}`);
|
|
172
171
|
}
|
|
173
172
|
});
|
|
174
173
|
|
|
174
|
+
// 监听 queue:failed 事件(处理队列级别的最终错误)
|
|
175
|
+
sessionScope.on('queue:failed', ({ error }) => {
|
|
176
|
+
hasError = true;
|
|
177
|
+
console.error(`\n${colored('[错误]', RED)} ${error}`);
|
|
178
|
+
});
|
|
179
|
+
|
|
175
180
|
// 调用 sendMessage 触发事件
|
|
176
181
|
const res = await this.agent.sendMessage(message, { sessionId });
|
|
177
182
|
// 清理
|
|
@@ -199,13 +204,14 @@ class ChatUI {
|
|
|
199
204
|
}
|
|
200
205
|
} catch (err) {
|
|
201
206
|
// 只打印简洁错误消息,不打印堆栈
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
+
// 避免与 message:error 事件重复打印
|
|
208
|
+
if (!hasError && !interrupted) {
|
|
209
|
+
const errName = err?.name || '';
|
|
210
|
+
const isRetryError = errName === 'AI_RetryError' || errName === 'RetryError';
|
|
211
|
+
const friendlyMessage = isRetryError
|
|
212
|
+
? 'AI 服务暂时不可用,请稍后重试'
|
|
213
|
+
: (err.message || '未知错误').split('\n')[0];
|
|
207
214
|
|
|
208
|
-
if (!interrupted) {
|
|
209
215
|
console.error(`\n${colored('[错误]', RED)} ${friendlyMessage}\n`);
|
|
210
216
|
}
|
|
211
217
|
} finally {
|
package/examples/test-chat.js
CHANGED
|
@@ -89,7 +89,9 @@ async function main() {
|
|
|
89
89
|
} else if (chunk.type === 'tool-result') {
|
|
90
90
|
console.log('\n[工具结果]', JSON.stringify(chunk.result).substring(0, 100));
|
|
91
91
|
} else if (chunk.type === 'error') {
|
|
92
|
-
|
|
92
|
+
throw new Error('sdfsdf');
|
|
93
|
+
|
|
94
|
+
//console.error('\n[错误]', chunk.error);
|
|
93
95
|
}
|
|
94
96
|
}
|
|
95
97
|
|
|
@@ -110,6 +112,10 @@ async function main() {
|
|
|
110
112
|
'请帮我注册一个 GET 路由:\n- 路径:/test\n- 处理函数:return "1232432"\n\n注册完成后,用 web_request 工具测试访问这个路由,确认返回 "1232432"'
|
|
111
113
|
);
|
|
112
114
|
|
|
115
|
+
await ask(
|
|
116
|
+
'关闭服务'
|
|
117
|
+
);
|
|
118
|
+
|
|
113
119
|
console.log('\n--- 测试完成,现在你可以继续对话 ---\n');
|
|
114
120
|
|
|
115
121
|
// 主循环
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "foliko",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.23",
|
|
4
4
|
"description": "简约的插件化 Agent 框架",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
"@ai-sdk/openai": "^3.0.41",
|
|
54
54
|
"@ai-sdk/openai-compatible": "^2.0.35",
|
|
55
55
|
"@anthropic-ai/sdk": "^0.39.0",
|
|
56
|
+
"@anthropic-ai/tokenizer": "^0.0.4",
|
|
56
57
|
"@chnak/weixin-bot": "^1.2.9",
|
|
57
58
|
"@chnak/zod-to-markdown": "1.0.7",
|
|
58
59
|
"@hono/node-server": "^1.19.11",
|
|
@@ -329,6 +329,14 @@ class TelegramPlugin extends Plugin {
|
|
|
329
329
|
}
|
|
330
330
|
})
|
|
331
331
|
|
|
332
|
+
// 监听错误事件
|
|
333
|
+
sessionScope.on('queue:failed', async ({ error }) => {
|
|
334
|
+
await this._bot.editMessageText(escapeMarkdown(`❌ ${error}`), {
|
|
335
|
+
chat_id: chatId,
|
|
336
|
+
message_id: thinkingMsg.message_id
|
|
337
|
+
})
|
|
338
|
+
})
|
|
339
|
+
|
|
332
340
|
await agent.sendMessage(text, { sessionId })
|
|
333
341
|
|
|
334
342
|
sessionScope.removeAllListeners()
|
package/plugins/weixin-plugin.js
CHANGED
|
@@ -537,8 +537,6 @@ class WeixinPlugin extends Plugin {
|
|
|
537
537
|
message+=colored(`[工具开始](${chunk.toolName})`,YELLOW)
|
|
538
538
|
}else if(chunk.type==='tool-result'){
|
|
539
539
|
message+=colored(`[工具结束](${chunk.toolName})`,YELLOW)
|
|
540
|
-
}else if(chunk.type==='error'){
|
|
541
|
-
message+=colored(`[错误](${chunk.error})`,RED)
|
|
542
540
|
}
|
|
543
541
|
while (message.includes('\n')) {
|
|
544
542
|
const nlIndex = message.indexOf('\n');
|
|
@@ -564,7 +562,7 @@ class WeixinPlugin extends Plugin {
|
|
|
564
562
|
lineBuffer = ''
|
|
565
563
|
message=''
|
|
566
564
|
})
|
|
567
|
-
sessionScope.on('
|
|
565
|
+
sessionScope.on('queue:failed', async ({error}) => {
|
|
568
566
|
this.stopTypingInterval()
|
|
569
567
|
await this._sendMessageBatch(originalMsg, userId, error, true)
|
|
570
568
|
lineBuffer = ''
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1080" height="1920" viewBox="0,0,1080,1920"><g fill="none" fill-rule="nonzero" stroke="none" stroke-width="none" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="10" stroke-dasharray="" stroke-dashoffset="0" font-family="none" font-weight="none" font-size="none" text-anchor="none" style="mix-blend-mode: normal"><path d="M0,1920v-1920h1080v1920z" fill="#f5f5f0" stroke="none" stroke-width="1"></path><text x="540" y="120" fill="#c41e3a" stroke="none" stroke-width="1" font-family="Microsoft YaHei, SimHei, KaiTi, seguiemj, sans-serif" font-weight="bold" font-size="48" text-anchor="middle">广东信宜特产</text><text x="540" y="200" fill="#2c1810" stroke="none" stroke-width="1" font-family="Microsoft YaHei, SimHei, KaiTi, seguiemj, sans-serif" font-weight="bold" font-size="72" text-anchor="middle">钱排银妃三华李</text><text x="540" y="300" fill="#8b4513" stroke="none" stroke-width="1" font-family="Microsoft YaHei, SimHei, KaiTi, seguiemj, sans-serif" font-weight="normal" font-size="36" text-anchor="middle">果中爱马仕 | 酸甜脆爽</text><text x="540" y="480" fill="#c41e3a" stroke="none" stroke-width="1" font-family="Microsoft YaHei, SimHei, KaiTi, seguiemj, sans-serif" font-weight="bold" font-size="32" text-anchor="middle">核心卖点</text><g><rect x="140" y="520" width="300" height="100" rx="12" fill="#c41e3a" stroke="none"/><text x="290" y="570" fill="#ffffff" stroke="none" font-size="28" text-anchor="middle" font-family="Microsoft YaHei">📍 产地直发</text><text x="290" y="600" fill="#ffe4e1" stroke="none" font-size="18" text-anchor="middle" font-family="Microsoft YaHei">广东信宜钱排镇</text></g><g><rect x="640" y="520" width="300" height="100" rx="12" fill="#c41e3a" stroke="none"/><text x="790" y="570" fill="#ffffff" stroke="none" font-size="28" text-anchor="middle" font-family="Microsoft YaHei">⭐ 精选大果</text><text x="790" y="600" fill="#ffe4e1" stroke="none" font-size="18" text-anchor="middle" font-family="Microsoft YaHei">单果30g以上</text></g><g><rect x="140" y="650" width="300" height="100" rx="12" fill="#228b22" stroke="none"/><text x="290" y="700" fill="#ffffff" stroke="none" font-size="28" text-anchor="middle" font-family="Microsoft YaHei">🌿 新鲜采摘</text><text x="290" y="730" fill="#90ee90" stroke="none" font-size="18" text-anchor="middle" font-family="Microsoft YaHei">当季现摘 冷链配送</text></g><g><rect x="640" y="650" width="300" height="100" rx="12" fill="#228b22" stroke="none"/><text x="790" y="700" fill="#ffffff" stroke="none" font-size="28" text-anchor="middle" font-family="Microsoft YaHei">💚 酸甜脆爽</text><text x="790" y="730" fill="#90ee90" stroke="none" font-size="18" text-anchor="middle" font-family="Microsoft YaHei">自然成熟 口感极佳</text></g><g><rect x="240" y="800" width="600" height="180" rx="20" fill="#ffffff" stroke="#c41e3a" stroke-width="4"/><text x="540" y="870" fill="#c41e3a" stroke="none" font-size="24" text-anchor="middle" font-family="Microsoft YaHei">活动价格</text><text x="540" y="940" fill="#c41e3a" stroke="none" font-size="72" font-weight="bold" text-anchor="middle" font-family="Microsoft YaHei">¥19.8</text><text x="540" y="980" fill="#666666" stroke="none" font-size="20" text-anchor="middle" font-family="Microsoft YaHei">5斤装 顺丰包邮</text></g><g><rect x="340" y="1100" width="400" height="400" rx="20" fill="#fff5ee" stroke="#deb887" stroke-width="2"/><text x="540" y="1300" fill="#ff6b6b" stroke="none" font-size="120" text-anchor="middle">🍑</text><text x="540" y="1450" fill="#8b4513" stroke="none" font-size="28" text-anchor="middle" font-family="Microsoft YaHei">钱排三华李</text><text x="540" y="1480" fill="#d2691e" stroke="none" font-size="20" text-anchor="middle" font-family="Microsoft YaHei">新鲜水果 产地直发</text></g><g><rect x="340" y="1550" width="400" height="250" rx="16" fill="#ffffff" stroke="#e0e0e0" stroke-width="2"/><text x="540" y="1600" fill="#333333" stroke="none" font-size="24" text-anchor="middle" font-family="Microsoft YaHei">[ 扫码下单 ]</text><text x="540" y="1640" fill="#666666" stroke="none" font-size="18" text-anchor="middle" font-family="Microsoft YaHei">手机淘宝/京东 扫码购买</text><text x="540" y="1700" fill="#c41e3a" stroke="none" font-size="20" text-anchor="middle" font-family="Microsoft YaHei">长按识别二维码</text></g><text x="540" y="1850" fill="#666666" stroke="none" font-size="24" text-anchor="middle" font-family="Microsoft YaHei">长按识别 立即购买</text><text x="540" y="1880" fill="#999999" stroke="none" font-size="18" text-anchor="middle" font-family="Microsoft YaHei">正宗钱排三华李 假一赔十</text></g></svg>
|
package/src/core/agent-chat.js
CHANGED
|
@@ -21,6 +21,7 @@ const {
|
|
|
21
21
|
} = require('ai');
|
|
22
22
|
const { prepareMessagesForAPI, cleanResponse } = require('../utils');
|
|
23
23
|
const { ChatQueueManager } = require('../utils/chat-queue');
|
|
24
|
+
const { countTokens } = require('@anthropic-ai/tokenizer');
|
|
24
25
|
const fs = require('fs/promises');
|
|
25
26
|
// 新模块
|
|
26
27
|
const { ChatSession } = require('./chat-session');
|
|
@@ -56,7 +57,7 @@ class AgentChatHandler extends EventEmitter {
|
|
|
56
57
|
agent,
|
|
57
58
|
maxConcurrent: config.maxConcurrent || 1,
|
|
58
59
|
retryAttempts: config.retryAttempts || 3,
|
|
59
|
-
retryDelay: config.retryDelay ||
|
|
60
|
+
retryDelay: config.retryDelay || 10000,
|
|
60
61
|
});
|
|
61
62
|
|
|
62
63
|
// ToolExecutor: 工具管理
|
|
@@ -438,8 +439,13 @@ class AgentChatHandler extends EventEmitter {
|
|
|
438
439
|
prepareStep: this._createPrepareStep(),
|
|
439
440
|
});
|
|
440
441
|
|
|
442
|
+
// AI SDK 错误回调:仅记录日志,不在这里处理(统一由 catch/yield 处理)
|
|
443
|
+
const handleSDKError = ({ error }) => {
|
|
444
|
+
logger.debug(`[AgentChat] SDK onError: ${error?.message}`);
|
|
445
|
+
};
|
|
446
|
+
|
|
441
447
|
const result = await framework.runInSession(sessionId, {}, async () => {
|
|
442
|
-
return agent.stream({ messages, ...this.providerOptions });
|
|
448
|
+
return agent.stream({ messages, ...this.providerOptions, onError: handleSDKError });
|
|
443
449
|
});
|
|
444
450
|
|
|
445
451
|
const stream = result.fullStream;
|
|
@@ -458,7 +464,9 @@ class AgentChatHandler extends EventEmitter {
|
|
|
458
464
|
} else if (part.type === 'tool-result') {
|
|
459
465
|
yield { type: 'tool-result', toolName: part.toolName, result: part.output };
|
|
460
466
|
} else if (part.type === 'error') {
|
|
461
|
-
|
|
467
|
+
// 统一错误消息
|
|
468
|
+
const errMsg = part.error?.message || 'AI 服务错误';
|
|
469
|
+
yield { type: 'error', error: errMsg.split('\n')[0] };
|
|
462
470
|
}
|
|
463
471
|
}
|
|
464
472
|
|
|
@@ -472,35 +480,10 @@ class AgentChatHandler extends EventEmitter {
|
|
|
472
480
|
const userMsg = messages[messages.length - finishMessages.length - 1];
|
|
473
481
|
this.emit('message', { content: fullText, sessionId: sessionId, userMessage: userMsg });
|
|
474
482
|
} catch (err) {
|
|
475
|
-
//
|
|
476
|
-
const
|
|
477
|
-
const errName = err?.name || '';
|
|
478
|
-
|
|
479
|
-
// 使用 AI SDK 的错误类型判断
|
|
480
|
-
const isRetryError =
|
|
481
|
-
RetryError.isInstance?.(err) ||
|
|
482
|
-
errName === 'AI_RetryError' ||
|
|
483
|
-
errName === 'RetryError' ||
|
|
484
|
-
err?.reason === 'maxRetriesExceeded';
|
|
485
|
-
|
|
486
|
-
// AI SDK 的错误 map
|
|
487
|
-
const errorMessages = {
|
|
488
|
-
AI_RetryError: 'AI 服务暂时不可用,请稍后重试',
|
|
489
|
-
AI_APICallError: err?.isRetryable ? 'AI 服务暂时不可用,请稍后重试' : simpleMessage,
|
|
490
|
-
AI_NoContentGeneratedError: 'AI 未生成有效内容,请重试',
|
|
491
|
-
AI_NoOutputGeneratedError: 'AI 未生成有效内容,请重试',
|
|
492
|
-
AI_NoSuchModelError: '指定的 AI 模型不存在',
|
|
493
|
-
AI_NoSuchProviderError: 'AI 提供商配置错误',
|
|
494
|
-
AI_LoadAPIKeyError: 'AI API 密钥配置错误',
|
|
495
|
-
AI_InvalidPromptError: '输入内容格式错误',
|
|
496
|
-
AI_ToolCallRepairError: '工具调用处理失败,请重试',
|
|
497
|
-
};
|
|
498
|
-
|
|
499
|
-
const friendlyMessage =
|
|
500
|
-
errorMessages[errName] || (isRetryError ? 'AI 服务暂时不可用,请稍后重试' : simpleMessage);
|
|
483
|
+
// 统一错误处理:提取简洁消息并通过 yield 传递
|
|
484
|
+
const friendlyMessage = err?.message?.split('\n')[0] || '未知错误';
|
|
501
485
|
|
|
502
|
-
|
|
503
|
-
this.emit('message:error', { sessionId, error: friendlyMessage });
|
|
486
|
+
this.emit('message:error', { sessionId, error: friendlyMessage, originalError: err });
|
|
504
487
|
yield { type: 'error', error: friendlyMessage };
|
|
505
488
|
} finally {
|
|
506
489
|
const messageStore = this._getSessionMessageStore(sessionId);
|
|
@@ -623,12 +606,9 @@ class AgentChatHandler extends EventEmitter {
|
|
|
623
606
|
const totalTokens = messagesTokens + toolsTokens + systemPromptTokens;
|
|
624
607
|
const limit = this._maxContextTokens * 0.5;
|
|
625
608
|
|
|
626
|
-
|
|
627
|
-
`[_prepareSession] BEFORE: messages=${messages.length}, tokens=${totalTokens}/${limit}`
|
|
628
|
-
);
|
|
609
|
+
logger.info(`BEFORE: messages=${messages.length}, tokens=${totalTokens}/${limit}`);
|
|
629
610
|
|
|
630
611
|
if (totalTokens > limit) {
|
|
631
|
-
console.log(`[_prepareSession] Compressing context...`);
|
|
632
612
|
logger.info(
|
|
633
613
|
`Context large (${totalTokens}/${this._maxContextTokens} tokens), compressing...`
|
|
634
614
|
);
|
|
@@ -946,9 +926,9 @@ class AgentChatHandler extends EventEmitter {
|
|
|
946
926
|
* 计算 token
|
|
947
927
|
* @private
|
|
948
928
|
*/
|
|
949
|
-
_countTokens(text
|
|
929
|
+
_countTokens(text) {
|
|
950
930
|
if (!text) return 0;
|
|
951
|
-
return
|
|
931
|
+
return countTokens(String(text));
|
|
952
932
|
}
|
|
953
933
|
|
|
954
934
|
/**
|
|
@@ -9,25 +9,26 @@
|
|
|
9
9
|
|
|
10
10
|
const { logger } = require('../utils/logger');
|
|
11
11
|
const { zodSchemaToMarkdown, zodSchemaToTable } = require('@chnak/zod-to-markdown');
|
|
12
|
+
const { Subagent } = require('./subagent');
|
|
12
13
|
|
|
13
14
|
// 模型上下文限制表
|
|
14
15
|
const MODEL_CONTEXT_LIMITS = {
|
|
15
|
-
'deepseek-chat':
|
|
16
|
-
'deepseek-coder':
|
|
17
|
-
'deepseek-reasoner':
|
|
18
|
-
'MiniMax-M2.7':
|
|
16
|
+
'deepseek-chat': 110000,
|
|
17
|
+
'deepseek-coder': 110000,
|
|
18
|
+
'deepseek-reasoner': 110000,
|
|
19
|
+
'MiniMax-M2.7': 110000,
|
|
19
20
|
'gpt-4': 100000,
|
|
20
21
|
'gpt-4o': 100000,
|
|
21
22
|
'gpt-4o-mini': 100000,
|
|
22
23
|
'gpt-4-turbo': 100000,
|
|
23
|
-
'claude-3-5-sonnet':
|
|
24
|
-
'claude-3-opus':
|
|
25
|
-
'claude-3-sonnet':
|
|
26
|
-
'glm-5.1':
|
|
24
|
+
'claude-3-5-sonnet': 110000,
|
|
25
|
+
'claude-3-opus': 110000,
|
|
26
|
+
'claude-3-sonnet': 110000,
|
|
27
|
+
'glm-5.1': 110000,
|
|
27
28
|
};
|
|
28
29
|
|
|
29
30
|
// 压缩超时
|
|
30
|
-
const COMPRESSION_TIMEOUT =
|
|
31
|
+
const COMPRESSION_TIMEOUT = 1200000;
|
|
31
32
|
|
|
32
33
|
class ContextCompressor {
|
|
33
34
|
/**
|
|
@@ -409,38 +410,53 @@ class ContextCompressor {
|
|
|
409
410
|
* @private
|
|
410
411
|
*/
|
|
411
412
|
async _summarizeMessages(messages) {
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
throw new Error('AI client not available');
|
|
413
|
+
if (!this.framework) {
|
|
414
|
+
throw new Error('Framework not available');
|
|
415
415
|
}
|
|
416
416
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
content
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
.
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
417
|
+
// 提取消息文本内容,忽略工具调用等
|
|
418
|
+
const msg_str = messages
|
|
419
|
+
.map((m) => {
|
|
420
|
+
let text = '';
|
|
421
|
+
if (typeof m.content === 'string') {
|
|
422
|
+
text = m.content;
|
|
423
|
+
} else if (Array.isArray(m.content)) {
|
|
424
|
+
// 只提取 text 类型,忽略 tool-call、tool-result 等
|
|
425
|
+
text = m.content
|
|
426
|
+
.filter((c) => c.type === 'text')
|
|
427
|
+
.map((c) => c.text)
|
|
428
|
+
.join(' ');
|
|
429
|
+
}
|
|
430
|
+
// 跳过 tool 角色的结果(太长且无意义)
|
|
431
|
+
if (m.role === 'tool') {
|
|
432
|
+
return '';
|
|
433
|
+
}
|
|
434
|
+
return `${m.role}: ${text}`;
|
|
435
|
+
})
|
|
436
|
+
.filter((line) => line.length > 0)
|
|
437
|
+
.join('\n');
|
|
438
|
+
|
|
439
|
+
const task = `请总结以下对话,只保留有意义的信息(如任务需求、决策结论、重要上下文),忽略无意义的闲聊和重复内容。用1000字以内描述:\n
|
|
440
|
+
|
|
441
|
+
${msg_str}`;
|
|
434
442
|
|
|
435
443
|
try {
|
|
436
|
-
//
|
|
437
|
-
|
|
438
|
-
const
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
444
|
+
// 使用 framework.createSubAgent 创建 Subagent 进行摘要
|
|
445
|
+
// maxRetries: 0 禁用重试,避免长时间等待
|
|
446
|
+
const subagent = this.framework.createSubAgent({
|
|
447
|
+
name: 'context-compressor',
|
|
448
|
+
role: '信息提取专家',
|
|
449
|
+
systemPrompt:
|
|
450
|
+
'你是一个对话摘要助手。只提取和保留有意义的信息(如任务需求、决策结论、重要上下文),忽略无意义的闲聊、重复内容和中间过程。输出要简洁。',
|
|
451
|
+
maxRetries: 0,
|
|
452
|
+
disableTools: true,
|
|
442
453
|
});
|
|
443
|
-
|
|
454
|
+
|
|
455
|
+
const result = await subagent.chat(task);
|
|
456
|
+
if (result.success) {
|
|
457
|
+
return result.message;
|
|
458
|
+
}
|
|
459
|
+
throw new Error(result.error || '摘要生成失败');
|
|
444
460
|
} catch (err) {
|
|
445
461
|
logger.warn('Summarize failed:', err.message);
|
|
446
462
|
throw err;
|
package/src/core/framework.js
CHANGED
|
@@ -586,6 +586,8 @@ class Framework extends EventEmitter {
|
|
|
586
586
|
tools = {},
|
|
587
587
|
parentTools = [], // 如果不传,默认继承主agent所有工具
|
|
588
588
|
llmConfig = null,
|
|
589
|
+
maxRetries,
|
|
590
|
+
disableTools,
|
|
589
591
|
} = config;
|
|
590
592
|
|
|
591
593
|
// 获取 AI 配置
|
|
@@ -626,6 +628,8 @@ class Framework extends EventEmitter {
|
|
|
626
628
|
tools: toolList,
|
|
627
629
|
parentTools: parentTools,
|
|
628
630
|
framework: this, // 传递 framework 引用用于动态构建系统提示词
|
|
631
|
+
maxRetries,
|
|
632
|
+
disableTools,
|
|
629
633
|
});
|
|
630
634
|
|
|
631
635
|
this._agents.push(subagent);
|
package/src/core/subagent.js
CHANGED
|
@@ -55,6 +55,12 @@ class Subagent extends EventEmitter {
|
|
|
55
55
|
this._tools = new Map();
|
|
56
56
|
this._registerTools(config.tools || []);
|
|
57
57
|
|
|
58
|
+
// 重试配置
|
|
59
|
+
this.maxRetries = config.maxRetries ?? 2;
|
|
60
|
+
this.retryDelay = config.retryDelay ?? 5000;
|
|
61
|
+
// 禁用工具
|
|
62
|
+
this.disableTools = config.disableTools ?? false;
|
|
63
|
+
|
|
58
64
|
this.framework.once('framework:ready', () => {
|
|
59
65
|
const extExecutor = this.framework?.pluginManager?.get('extension-executor');
|
|
60
66
|
extExecutor._refreshAllAgentsExtPrompt(this.framework);
|
|
@@ -118,6 +124,8 @@ class Subagent extends EventEmitter {
|
|
|
118
124
|
* 构建 AI 工具格式
|
|
119
125
|
*/
|
|
120
126
|
_buildAITools() {
|
|
127
|
+
// 禁用工具时返回空
|
|
128
|
+
|
|
121
129
|
const tools = {};
|
|
122
130
|
// 从父Agent继承工具
|
|
123
131
|
const all_tools = this.framework.getTools();
|
|
@@ -165,9 +173,9 @@ class Subagent extends EventEmitter {
|
|
|
165
173
|
}
|
|
166
174
|
lines.push('');
|
|
167
175
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
176
|
+
if (this._customSystemPrompt) {
|
|
177
|
+
lines.push(this._customSystemPrompt);
|
|
178
|
+
}
|
|
171
179
|
|
|
172
180
|
// 2. 主Agent的系统提示词
|
|
173
181
|
if (this.framework && this.framework._mainAgent) {
|
|
@@ -234,28 +242,42 @@ class Subagent extends EventEmitter {
|
|
|
234
242
|
*/
|
|
235
243
|
async chat(task, options = {}) {
|
|
236
244
|
const maxSteps = options?.maxSteps || 30;
|
|
237
|
-
const maxRetries = options?.maxRetries ??
|
|
245
|
+
const maxRetries = options?.maxRetries ?? this.maxRetries;
|
|
246
|
+
const retryDelay = options?.retryDelay ?? this.retryDelay;
|
|
238
247
|
const aiProvider = this._getAIProvider();
|
|
239
248
|
const messages = [];
|
|
240
249
|
messages.push({ role: 'user', content: task });
|
|
241
250
|
|
|
251
|
+
logger.info(
|
|
252
|
+
`[Subagent:${this.name}] chat called, maxRetries=${maxRetries}, disableTools=${this.disableTools}`
|
|
253
|
+
);
|
|
254
|
+
|
|
242
255
|
let lastError;
|
|
243
256
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
257
|
+
logger.info(
|
|
258
|
+
`[Subagent:${this.name}] attempt ${attempt + 1}, model=${this.model}, provider=${this.provider}`
|
|
259
|
+
);
|
|
244
260
|
try {
|
|
245
|
-
const tools = this._buildAITools();
|
|
246
|
-
const systemPrompt = this.
|
|
261
|
+
const tools = this.disableTools ? {} : this._buildAITools();
|
|
262
|
+
const systemPrompt = this.disableTools
|
|
263
|
+
? this._customSystemPrompt
|
|
264
|
+
: this._buildSystemPrompt();
|
|
265
|
+
// logger.info(`[Subagent:${this.name}] calling generateText...`);
|
|
247
266
|
const result = await generateText({
|
|
267
|
+
...this.providerOptions,
|
|
248
268
|
model: aiProvider(this.model),
|
|
249
269
|
system: systemPrompt,
|
|
250
270
|
messages: messages,
|
|
251
271
|
tools: tools,
|
|
252
272
|
stopWhen: stepCountIs(maxSteps),
|
|
253
|
-
...this.providerOptions,
|
|
254
273
|
abortSignal: options.signal,
|
|
255
274
|
onChunk: (chunk) => {
|
|
256
275
|
this.emit('chunk', chunk);
|
|
257
276
|
},
|
|
258
277
|
});
|
|
278
|
+
logger.info(
|
|
279
|
+
`[Subagent:${this.name}] generateText completed, text length=${result.text.length}`
|
|
280
|
+
);
|
|
259
281
|
messages.push(...result.response.messages);
|
|
260
282
|
const full_text = cleanResponse(result.text);
|
|
261
283
|
this.emit('complete', { message: full_text, steps: result.steps?.length || 0 });
|
|
@@ -265,6 +287,7 @@ class Subagent extends EventEmitter {
|
|
|
265
287
|
steps: result.steps?.length || 0,
|
|
266
288
|
};
|
|
267
289
|
} catch (err) {
|
|
290
|
+
logger.warn(`[Subagent:${this.name}] generateText error: ${err.message}`);
|
|
268
291
|
lastError = err;
|
|
269
292
|
const errName = err?.name || '';
|
|
270
293
|
|
|
@@ -276,8 +299,10 @@ class Subagent extends EventEmitter {
|
|
|
276
299
|
err?.reason === 'maxRetriesExceeded';
|
|
277
300
|
|
|
278
301
|
if (isRetryError && attempt < maxRetries) {
|
|
279
|
-
logger.warn(
|
|
280
|
-
|
|
302
|
+
logger.warn(
|
|
303
|
+
`[Subagent:${this.name}] AI 服务不可用,正在重试 (${attempt + 1}/${maxRetries})`
|
|
304
|
+
);
|
|
305
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelay * Math.pow(2, attempt)));
|
|
281
306
|
continue;
|
|
282
307
|
}
|
|
283
308
|
|
package/src/utils/chat-queue.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
const { EventEmitter } = require('./event-emitter');
|
|
2
2
|
const { cleanResponse } = require('./index');
|
|
3
|
+
const { logger } = require('./logger');
|
|
4
|
+
const log = logger.child('ChatQueue');
|
|
3
5
|
// ChatQueueManager.js
|
|
4
6
|
class ChatQueueManager extends EventEmitter {
|
|
5
7
|
constructor(options = {}) {
|
|
@@ -8,7 +10,7 @@ class ChatQueueManager extends EventEmitter {
|
|
|
8
10
|
this.isProcessing = false;
|
|
9
11
|
this.maxConcurrent = options.maxConcurrent || 1;
|
|
10
12
|
this.activeCount = 0;
|
|
11
|
-
this.retryAttempts = options.retryAttempts ||
|
|
13
|
+
this.retryAttempts = options.retryAttempts || 3;
|
|
12
14
|
this.retryDelay = options.retryDelay || 10000;
|
|
13
15
|
}
|
|
14
16
|
|
|
@@ -72,13 +74,8 @@ class ChatQueueManager extends EventEmitter {
|
|
|
72
74
|
const result = await this.executeWithRetry(item);
|
|
73
75
|
// 检查 result 是否有错误(而不是通过 try/catch)
|
|
74
76
|
if (result.error) {
|
|
75
|
-
console.log('[ChatQueue] Rejecting with error from result:', result.error.message);
|
|
76
|
-
|
|
77
|
-
this.emit('queue:failed', {
|
|
78
|
-
requestId: item.id,
|
|
79
|
-
sessionId: item.sessionId,
|
|
80
|
-
error: result.error.message,
|
|
81
|
-
});
|
|
77
|
+
//console.log('[ChatQueue] Rejecting with error from result:', result.error.message);
|
|
78
|
+
throw result.error;
|
|
82
79
|
} else {
|
|
83
80
|
item.resolve(result);
|
|
84
81
|
this.emit('queue:completed', {
|
|
@@ -88,7 +85,7 @@ class ChatQueueManager extends EventEmitter {
|
|
|
88
85
|
});
|
|
89
86
|
}
|
|
90
87
|
} catch (error) {
|
|
91
|
-
|
|
88
|
+
log.info('[ChatQueue] Rejecting Error:', error.message);
|
|
92
89
|
item.reject(error);
|
|
93
90
|
this.emit('queue:failed', {
|
|
94
91
|
requestId: item.id,
|
|
@@ -119,30 +116,18 @@ class ChatQueueManager extends EventEmitter {
|
|
|
119
116
|
|
|
120
117
|
// 检查是否有错误(通过返回的 result.error)
|
|
121
118
|
if (result.error) {
|
|
122
|
-
console.log(
|
|
123
|
-
'[ChatQueue] executeWithRetry: attempt',
|
|
124
|
-
attempt,
|
|
125
|
-
'got error:',
|
|
126
|
-
result.error.message
|
|
127
|
-
);
|
|
128
119
|
lastError = result.error;
|
|
129
120
|
if (attempt < this.retryAttempts && this.isRetryableError(lastError)) {
|
|
130
121
|
await this.sleep(this.retryDelay * Math.pow(2, attempt - 1));
|
|
131
122
|
continue;
|
|
132
123
|
}
|
|
133
|
-
//
|
|
134
|
-
|
|
135
|
-
return result;
|
|
124
|
+
// 重试耗尽,直接抛出 result.error
|
|
125
|
+
throw lastError;
|
|
136
126
|
}
|
|
137
127
|
|
|
138
128
|
return result;
|
|
139
129
|
} catch (error) {
|
|
140
|
-
|
|
141
|
-
'[ChatQueue] executeWithRetry: attempt',
|
|
142
|
-
attempt,
|
|
143
|
-
'threw error:',
|
|
144
|
-
error.message
|
|
145
|
-
);
|
|
130
|
+
log.info('[ChatQueue] executeWithRetry: ', attempt, 'error:', error.message);
|
|
146
131
|
lastError = error;
|
|
147
132
|
if (attempt < this.retryAttempts && this.isRetryableError(error)) {
|
|
148
133
|
await this.sleep(this.retryDelay * Math.pow(2, attempt - 1));
|
|
@@ -161,7 +146,7 @@ class ChatQueueManager extends EventEmitter {
|
|
|
161
146
|
|
|
162
147
|
const friendlyError = new Error(friendlyMessage);
|
|
163
148
|
friendlyError.originalError = lastError;
|
|
164
|
-
|
|
149
|
+
//log.info('[ChatQueue] executeWithRetry: throwing friendly error:', friendlyMessage);
|
|
165
150
|
throw friendlyError;
|
|
166
151
|
}
|
|
167
152
|
|
|
@@ -196,10 +181,7 @@ class ChatQueueManager extends EventEmitter {
|
|
|
196
181
|
? 'AI 服务暂时不可用,请稍后重试'
|
|
197
182
|
: (err.message || err.toString()).split('\n')[0];
|
|
198
183
|
|
|
199
|
-
|
|
200
|
-
'[ChatQueue] executeStream caught error, converting to friendly:',
|
|
201
|
-
friendlyMessage
|
|
202
|
-
);
|
|
184
|
+
log.info('[ChatQueue] executeStream Error:', friendlyMessage);
|
|
203
185
|
chunks.push({ type: 'error', error: friendlyMessage });
|
|
204
186
|
this.emit('stream:chunk', {
|
|
205
187
|
requestId: item.id,
|
|
@@ -217,7 +199,7 @@ class ChatQueueManager extends EventEmitter {
|
|
|
217
199
|
const error = new Error(errorChunk.error || 'Stream error');
|
|
218
200
|
error.chunks = chunks;
|
|
219
201
|
error.isStreamError = true;
|
|
220
|
-
|
|
202
|
+
log.info('[ChatQueue] executeStream Error:', error.message);
|
|
221
203
|
return {
|
|
222
204
|
chunks,
|
|
223
205
|
content: cleanResponse(''),
|