foliko 1.1.67 → 1.1.68
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/data/plugins-state.json +8 -0
- package/.agent/sessions/cli_default.json +3874 -0
- package/cli/src/ui/chat-ui.js +84 -165
- package/cli/src/ui/footer-bar.js +7 -32
- package/cli/src/ui/message-bubble.js +24 -2
- package/cli/src/ui/status-bar.js +177 -0
- package/package.json +1 -1
- package/plugins/audit-plugin.js +11 -7
- package/plugins/coordinator-plugin.js +14 -12
- package/plugins/data-splitter-plugin.js +323 -0
- package/plugins/default-plugins.js +2 -1
- package/plugins/extension-executor-plugin.js +2 -2
- package/plugins/file-system-plugin.js +68 -50
- package/plugins/gate-trading.js +10 -10
- package/plugins/install-plugin.js +3 -3
- package/plugins/memory-plugin.js +8 -11
- package/plugins/plugin-manager-plugin.js +9 -11
- package/plugins/qq-plugin.js +8 -8
- package/plugins/rules-plugin.js +7 -7
- package/plugins/scheduler-plugin.js +22 -18
- package/plugins/session-plugin.js +14 -14
- package/plugins/storage-plugin.js +11 -10
- package/plugins/subagent-plugin.js +13 -9
- package/plugins/think-plugin.js +63 -59
- package/plugins/tools-plugin.js +8 -8
- package/plugins/weixin-plugin.js +5 -5
- package/src/capabilities/skill-manager.js +23 -15
- package/src/capabilities/workflow-engine.js +2 -2
- package/src/core/agent-chat.js +70 -27
- package/src/core/agent.js +17 -27
- package/src/core/chat-session.js +7 -161
- package/src/core/constants.js +198 -0
- package/src/core/context-compressor.js +6 -181
- package/src/core/framework.js +125 -6
- package/src/core/plugin-base.js +7 -5
- package/src/core/subagent.js +16 -135
- package/src/core/tool-executor.js +2 -70
- package/src/executors/mcp-executor.js +12 -10
- package/src/utils/chat-queue.js +11 -22
- package/src/utils/data-splitter.js +345 -0
- package/src/utils/message-validator.js +283 -0
- package/src/utils/retry.js +168 -22
|
@@ -235,6 +235,3880 @@
|
|
|
235
235
|
"text": "<think>我已经收集了足够的信息来给用户介绍这个项目。让我整理一下关键信息。\n</think>\n\n## Foliko 项目概览\n\n**Foliko** 是一个简约的插件化 Agent 框架,用纯 JavaScript 实现(无 TypeScript)。\n\n---\n\n### 📁 项目结构\n\n```\nfoliko/\n├── src/ # 核心框架\n│ └── core/ # 核心组件\n│ ├── framework.js # 容器层,插件/工具管理\n│ ├── agent.js # Agent 对话层\n│ ├── plugin-manager.js\n│ ├── tool-registry.js\n│ └── ...\n├── plugins/ # 27 个内置插件\n│ ├── ai-plugin.js\n│ ├── memory-plugin.js\n│ ├── scheduler-plugin.js\n│ ├── web-plugin.js\n│ ├── weixin-plugin.js\n│ ├── feishu-plugin.js\n│ ├── telegram-plugin.js\n│ └── ...\n├── skills/ # 11 个内置技能\n│ ├── subagent-guide/\n│ ├── plugin-guide/\n│ ├── poster-guide/\n│ └── ...\n├── cli/ # 命令行入口\n├── examples/ # 19 个示例\n└── docs/ # 文档\n```\n\n---\n\n### 🔌 核心特性\n\n| 特性 | 说明 |\n|------|------|\n| **插件化架构** | 核心简单,通过插件扩展功能 |\n| **流式输出** | 支持实时流式输出 |\n| **多 AI 支持** | 支持 Anthropic、DeepSeek、MiniMax 等 |\n| **内置工具** | Shell、Python、MCP、文件系统等 |\n| **技能系统** | 可扩展的 Skill 管理 |\n| **会话管理** | 支持多会话切换 |\n| **规则引擎** | 可配置的行为规则 |\n| **子 Agent** | 支持多子 Agent 分工协作 |\n| **Ambient Agent** | 持续后台运行,监控事件主动执行 |\n| **统一通知** | 微信、飞书、Telegram 消息推送 |\n\n---\n\n### 🛠 内置插件(27个)\n\n- **AI & 执行**: `ai-plugin`, `shell-executor`, `python-executor`, `install-plugin`\n- **数据管理**: `memory-plugin`, `storage-plugin`, `session-plugin`, `audit-plugin`\n- **自动化**: `scheduler-plugin`, `think-plugin`, `rules-plugin`\n- **网络服务**: `web-plugin`, `plugin-manager`\n- **消息推送**: `weixin-plugin`, `feishu-plugin`, `telegram-plugin`, `email`\n- **业务集成**: `qq-plugin`, `gate-trading`\n- **高级**: `coordinator-plugin`, `ambient-agent`, `subagent-plugin`, `extension-executor`\n\n---\n\n### 🎯 设计原则\n\n1. **纯 JS 实现** - 无 TypeScript,简明易懂\n2. **架构简约** - 核心仅包含必要组件\n3. **插件与 Agent 分离** - Agent 专注对话推理,插件提供扩展\n4. **手动热重载** - 通过 API 手动调用,不监测文件变化\n\n---\n\n想深入了解哪个部分?比如:\n- 插件开发指南\n- 核心源码解析\n- 某个具体插件的实现"
|
|
236
236
|
}
|
|
237
237
|
]
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
"role": "user",
|
|
241
|
+
"content": "你好"
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
"role": "assistant",
|
|
245
|
+
"content": [
|
|
246
|
+
{
|
|
247
|
+
"type": "text",
|
|
248
|
+
"text": "<think>用户说\"你好\",这是一个简单的问候。我应该友好地回应,并询问有什么可以帮助的。\n</think>\n\n你好!👋\n\n有什么我可以帮你的吗?比如:\n\n- **技术问题**:代码开发、调试、架构设计\n- **项目操作**:文件操作、插件管理、运行测试\n- **学习探索**:了解项目源码、设计模式、最佳实践\n- **自动化任务**:定时任务、工作流、数据处理\n\n随时告诉我你的需求 😊"
|
|
249
|
+
}
|
|
250
|
+
]
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
"role": "user",
|
|
254
|
+
"content": "查看本项目,有时候回复一半就突然断了"
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
"role": "assistant",
|
|
258
|
+
"content": [
|
|
259
|
+
{
|
|
260
|
+
"type": "text",
|
|
261
|
+
"text": "<think>用户说\"查看本项目,有时候回复一半就突然断了\",这可能是在描述一个 bug 或者他遇到的问题。我需要先了解他指的是什么——可能是指 AI 回复被截断的问题?\n\n让我先查看一下相关的代码,特别是可能涉及到回复截断的部分,比如:\n1. token 计数器\n2. 上下文压缩器\n3. Agent Chat 处理\n4. 流式输出相关代码\n\n先看一下 token-counter.js 和 context-compressor.js,这些可能会导致回复被截断。\n</think>\n\n我来查看可能导致回复截断的相关代码:\n"
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
"type": "tool-call",
|
|
265
|
+
"toolCallId": "call_function_muhqdqlj70po_1",
|
|
266
|
+
"toolName": "read",
|
|
267
|
+
"input": {
|
|
268
|
+
"path": "src/core/token-counter.js"
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
"type": "tool-call",
|
|
273
|
+
"toolCallId": "call_function_muhqdqlj70po_2",
|
|
274
|
+
"toolName": "read",
|
|
275
|
+
"input": {
|
|
276
|
+
"path": "src/core/context-compressor.js"
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
"type": "tool-call",
|
|
281
|
+
"toolCallId": "call_function_muhqdqlj70po_3",
|
|
282
|
+
"toolName": "read",
|
|
283
|
+
"input": {
|
|
284
|
+
"path": "src/core/agent-chat.js"
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
]
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
"role": "tool",
|
|
291
|
+
"content": [
|
|
292
|
+
{
|
|
293
|
+
"type": "tool-result",
|
|
294
|
+
"toolCallId": "call_function_muhqdqlj70po_1",
|
|
295
|
+
"toolName": "read",
|
|
296
|
+
"output": {
|
|
297
|
+
"type": "json",
|
|
298
|
+
"value": {
|
|
299
|
+
"success": true,
|
|
300
|
+
"data": "/**\n * TokenCounter - Token 计算工具\n *\n * 职责:\n * 1. 计算文本的 token 数量\n * 2. 计算消息数组的 token 总数\n * 3. 计算工具定义的 token 总数\n */\n\n/**\n * 简单的中英文混合 tokenizer\n * 粗略估计:中文每个字符 2 字节,英文每个单词约 1.5 字节\n * @param {string} text - 文本\n * @param {number} bytesPerToken - 每 token 字节数,默认 4\n * @returns {number} token 数量\n */\nfunction encode(text, bytesPerToken = 4) {\n if (!text) return 0;\n const bytes = Buffer.byteLength(String(text), 'utf8');\n return Math.ceil(bytes / bytesPerToken);\n}\n\n/**\n * 计算 JSON 字符串的 token 数量\n * @param {string} text - JSON 字符串\n * @returns {number} token 数量\n */\nfunction encodeForJSON(text) {\n if (!text) return 0;\n // JSON 字符串需要额外计算引号和转义\n const encoded = encode(text);\n return encoded + Math.ceil(Buffer.byteLength(JSON.stringify(text), 'utf8') / 100);\n}\n\nclass TokenCounter {\n /**\n * @param {Object} config - 配置\n * @param {Object} config.toolSchema - 工具 schema(用于工具 token 计算)\n */\n constructor(config = {}) {\n this.toolSchema = config.toolSchema || null;\n }\n\n /**\n * 计算文本 token\n * @param {string} text - 文本\n * @param {number} bytesPerToken - 每 token 字节数\n * @returns {number} token 数量\n */\n countText(text, bytesPerToken = 4) {\n return encode(text, bytesPerToken);\n }\n\n /**\n * 计算消息数组的 token 总数\n * @param {Array} messages - 消息数组\n * @returns {number} token 总数\n */\n countMessages(messages) {\n if (!Array.isArray(messages)) return 0;\n return messages.reduce((sum, msg) => sum + this.countMessage(msg), 0);\n }\n\n /**\n * 计算单条消息的 token\n * @param {Object} msg - 消息\n * @returns {number} token 数量\n */\n countMessage(msg) {\n if (!msg) return 0;\n\n let total = 0;\n\n // 角色和格式开销\n total += 4;\n\n if (typeof msg.content === 'string') {\n total += this.countText(msg.content);\n } else if (Array.isArray(msg.content)) {\n for (const block of msg.content) {\n total += this.countContentBlock(block);\n }\n }\n\n // tool_calls 开销\n if (msg.tool_calls && Array.isArray(msg.tool_calls)) {\n total += 15; // overhead per tool_calls block\n for (const tc of msg.tool_calls) {\n if (tc.function) {\n total += this.countText(tc.function.name) + this.countText(tc.function.arguments);\n }\n }\n }\n\n // tool_call_id 开销\n if (msg.tool_call_id) {\n total += 15;\n }\n\n return total;\n }\n\n /**\n * 计算 content block 的 token\n * @param {Object} block - content block\n * @returns {number} token 数量\n */\n countContentBlock(block) {\n if (!block) return 0;\n\n switch (block.type) {\n case 'text':\n return this.countText(block.text);\n case 'tool-call':\n case 'tool-use':\n if (block.input) {\n return this.countText(JSON.stringify(block.input));\n }\n return 0;\n case 'tool-result':\n case 'tool_result':\n if (block.content) {\n const content =\n typeof block.content === 'string' ? block.content : JSON.stringify(block.content);\n return this.countText(content);\n }\n return 0;\n case 'image':\n // 图片按 token 估算\n return 85;\n default:\n return this.countText(JSON.stringify(block));\n }\n }\n\n /**\n * 计算工具定义的 token 总数\n * @param {Array} tools - 工具数组\n * @returns {number} token 总数\n */\n countTools(tools) {\n if (!tools || !Array.isArray(tools)) return 0;\n\n let total = 0;\n for (const tool of tools) {\n // 工具名和描述\n total += 20; // overhead\n\n if (tool.description) {\n total += this.countText(tool.description);\n }\n\n // 参数\n if (tool.inputSchema) {\n const schema =\n tool.inputSchema.jsonSchema || tool.inputSchema.inputSchema || tool.inputSchema;\n if (schema.properties) {\n for (const [name, prop] of Object.entries(schema.properties)) {\n total += this.countText(name) + 10;\n if (prop.description) {\n total += this.countText(prop.description);\n }\n }\n }\n }\n }\n\n return total;\n }\n\n /**\n * 估算完整请求的 token(消息 + 工具 + 系统提示)\n * @param {Object} params - 请求参数\n * @returns {Object} - { messagesTokens, toolsTokens, systemPromptTokens, total }\n */\n estimateRequest({ messages, tools, systemPrompt }) {\n const messagesTokens = this.countMessages(messages);\n const toolsTokens = this.countTools(tools);\n const systemPromptTokens = systemPrompt ? this.countText(systemPrompt) : 0;\n\n return {\n messagesTokens,\n toolsTokens,\n systemPromptTokens,\n total: messagesTokens + toolsTokens + systemPromptTokens,\n };\n }\n}\n\nmodule.exports = { TokenCounter, encode, encodeForJSON };\n",
|
|
301
|
+
"metadata": {
|
|
302
|
+
"filePath": "D:\\Code\\foliko\\src\\core\\token-counter.js",
|
|
303
|
+
"size": 5031,
|
|
304
|
+
"lines": 191
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
"type": "tool-result",
|
|
311
|
+
"toolCallId": "call_function_muhqdqlj70po_2",
|
|
312
|
+
"toolName": "read",
|
|
313
|
+
"output": {
|
|
314
|
+
"type": "json",
|
|
315
|
+
"value": {
|
|
316
|
+
"success": true,
|
|
317
|
+
"data": "/**\n * ContextCompressor - 上下文压缩器\n *\n * 职责:\n * 1. AI 智能压缩(生成摘要)\n * 2. 简单压缩(截断保留最近消息)\n * 3. 消息配对验证(防止 orphaned tool result)\n */\n\nconst { logger } = require('../utils/logger');\nconst { TokenCounter } = require('./token-counter');\nconst { validateMessagesPairing, filterPairedMessages } = require('../utils/message-validator');\n\n// 模型上下文限制表\nconst MODEL_CONTEXT_LIMITS = {\n 'deepseek-chat': 100000,\n 'deepseek-coder': 100000,\n 'deepseek-v4-pro': 800000,\n 'deepseek-v4-flash': 800000,\n 'deepseek-reasoner': 100000,\n 'MiniMax-M2.7': 100000,\n 'gpt-4': 100000,\n 'gpt-4o': 100000,\n 'gpt-4o-mini': 100000,\n 'gpt-4-turbo': 100000,\n 'claude-3-5-sonnet': 110000,\n 'claude-3-opus': 110000,\n 'claude-3-sonnet': 110000,\n 'glm-5.1': 110000,\n};\n\n// 压缩超时\nconst COMPRESSION_TIMEOUT_MS = 1200000;\n\nclass ContextCompressor {\n /**\n * @param {Object} config - 配置\n */\n constructor(config = {}) {\n this.config = config;\n this.agent = config.agent;\n this.framework = config.framework;\n\n // 模型相关配置\n this.model = config.model || 'deepseek-chat';\n this._maxContextTokens = config.maxContextTokens || this._getDefaultContextLimit();\n this._keepRecentMessages = config.keepRecentMessages || 20;\n this._enableSmartCompress = config.enableSmartCompress !== false;\n\n // 压缩状态\n this._compressionCount = 0;\n this._compressionInProgress = false;\n this._compressionPromise = null;\n this._compressionTimeoutId = null; // 保存超时 ID 用于取消\n\n // 工具结果压缩配置\n this._maxToolResultSize = config.maxToolResultSize || 4000;\n }\n\n /**\n * 获取默认上下文限制\n * @returns {number}\n * @private\n */\n _getDefaultContextLimit() {\n const modelKey = Object.keys(MODEL_CONTEXT_LIMITS).find((k) =>\n this.model.toLowerCase().includes(k.toLowerCase())\n );\n return modelKey ? MODEL_CONTEXT_LIMITS[modelKey] : 40000;\n }\n\n /**\n * 压缩上下文\n * @param {string} sessionId - Session ID\n * @param {Array} messages - 消息数组引用\n * @param {Object} messageStore - 消息存储\n * @returns {Promise}\n */\n async compress(sessionId, messages, messageStore) {\n if (this._compressionInProgress && this._compressionPromise) {\n logger.debug('Compression already in progress, waiting...');\n return this._compressionPromise;\n }\n\n if (messages.length <= this._keepRecentMessages) {\n return;\n }\n\n this._compressionInProgress = true;\n\n this._compressionPromise = this._executeWithTimeout(sessionId, messages, messageStore).finally(\n () => {\n this._compressionInProgress = false;\n this._compressionPromise = null;\n // 清理超时 timer\n if (this._compressionTimeoutId) {\n clearTimeout(this._compressionTimeoutId);\n this._compressionTimeoutId = null;\n }\n }\n );\n\n return this._compressionPromise;\n }\n\n /**\n * 带超时的压缩执行\n * @private\n */\n async _executeWithTimeout(sessionId, messages, messageStore) {\n try {\n return await Promise.race([\n this._doCompress(sessionId, messages, messageStore),\n this._createTimeoutPromise(),\n ]);\n } catch (err) {\n logger.warn('Compression failed:', err.message);\n this._simpleCompress(sessionId, messages, messageStore);\n }\n }\n\n /**\n * 创建超时 Promise\n * @returns {Promise}\n * @private\n */\n _createTimeoutPromise() {\n return new Promise((_, reject) => {\n this._compressionTimeoutId = setTimeout(() => {\n this._compressionTimeoutId = null;\n reject(new Error(`Compression timeout (${COMPRESSION_TIMEOUT_MS}ms)`));\n }, COMPRESSION_TIMEOUT_MS);\n });\n }\n\n /**\n * 取消正在进行的压缩\n */\n cancelCompression() {\n if (this._compressionTimeoutId) {\n clearTimeout(this._compressionTimeoutId);\n this._compressionTimeoutId = null;\n }\n this._compressionInProgress = false;\n this._compressionPromise = null;\n }\n\n /**\n * 执行智能压缩(使用 AI 生成摘要)\n * @private\n */\n async _doCompress(sessionId, messages, messageStore) {\n const systemMessages = messages.filter((m) => m.role === 'system');\n const otherMessages = messages.filter((m) => m.role !== 'system');\n\n // 保留最近的 N 条非系统消息\n const recentMessages = otherMessages.slice(-this._keepRecentMessages);\n const messagesToSummarize = otherMessages.slice(0, -this._keepRecentMessages);\n\n const compressedCount = messagesToSummarize.length;\n\n let summaryContent = '';\n\n // 使用 AI 生成摘要\n if (this._enableSmartCompress && this.agent?._chatHandler?._aiClient) {\n try {\n const summaryText = await this._summarizeMessages(messagesToSummarize);\n summaryContent = `[早期对话摘要]: ${summaryText || '(无内容)'}`;\n } catch (err) {\n logger.warn('AI summary failed, using simple compression:', err.message);\n summaryContent = `[上下文已压缩: 省略了 ${compressedCount} 条早期消息。保留了最近 ${this._keepRecentMessages} 条对话记录。]`;\n }\n } else {\n summaryContent = `[上下文已压缩: 省略了 ${compressedCount} 条早期消息。保留了最近 ${this._keepRecentMessages} 条对话记录。]`;\n }\n\n const summary = {\n role: 'assistant',\n content: summaryContent,\n };\n\n // 构建保留的消息(确保 tool call 和 tool result 配对)\n const filteredRecentMessages = this._filterPairedMessages(recentMessages);\n\n // 替换消息数组\n messages.length = 0;\n messages.push(...systemMessages, summary, ...filteredRecentMessages);\n\n // 更新压缩状态\n this._compressionCount++;\n const tokenCount = this._countMessagesTokens(messages);\n if (messageStore.compressionState) {\n messageStore.compressionState.count++;\n messageStore.compressionState.lastCompressedAt = Date.now();\n messageStore.compressionState.lastTokenCount = tokenCount;\n }\n // 同步更新 SessionContext.metadata.compressionCount(如果可用)\n if (typeof messageStore.recordCompression === 'function') {\n messageStore.recordCompression(tokenCount);\n }\n\n logger.info(\n `Context compressed (${this._compressionCount} times). Messages: ${messages.length}`\n );\n }\n\n /**\n * 简单压缩(截断保留最近消息)\n * @private\n */\n _simpleCompress(sessionId, messages, messageStore) {\n const systemMessages = messages.filter((m) => m.role === 'system');\n const otherMessages = messages.filter((m) => m.role !== 'system');\n const recentMessages = otherMessages.slice(-this._keepRecentMessages);\n const compressedCount = otherMessages.length - this._keepRecentMessages;\n\n const summaryContent = `[上下文已压缩: 省略了 ${compressedCount} 条早期消息。保留了最近 ${this._keepRecentMessages} 条对话记录。]`;\n\n const summary = {\n role: 'assistant',\n content: summaryContent,\n };\n\n // 确保 tool call 和 tool result 配对\n const filteredRecentMessages = this._filterPairedMessages(recentMessages);\n\n messages.length = 0;\n messages.push(...systemMessages, summary, ...filteredRecentMessages);\n\n this._compressionCount++;\n if (messageStore.compressionState) {\n messageStore.compressionState.count++;\n messageStore.compressionState.lastCompressedAt = Date.now();\n messageStore.compressionState.lastTokenCount = this._countMessagesTokens(messages);\n }\n\n logger.info(`Context simple compressed. Messages: ${messages.length}`);\n }\n\n /**\n * 过滤消息,保留配对的 tool call 和 tool result\n * @private\n */\n _filterPairedMessages(messages) {\n return filterPairedMessages(messages);\n }\n\n /**\n * 验证消息配对(防止 orphaned tool result)\n * @param {Array} messages - 消息数组\n * @returns {Array} 验证后的消息\n */\n validateMessagesPairing(messages) {\n return validateMessagesPairing(messages);\n }\n\n /**\n * 使用 AI 生成消息摘要\n * @param {Array} messages - 消息数组\n * @returns {Promise<string>}\n * @private\n */\n async _summarizeMessages(messages) {\n if (!this.framework) {\n throw new Error('Framework not available');\n }\n\n // 提取消息文本内容,忽略工具调用等\n const msg_str = messages\n .map((m) => {\n let text = '';\n if (typeof m.content === 'string') {\n text = m.content;\n } else if (Array.isArray(m.content)) {\n // 只提取 text 类型,忽略 tool-call、tool-result 等\n text = m.content\n .filter((c) => c.type === 'text')\n .map((c) => c.text)\n .join(' ');\n }\n // 跳过 tool 角色的结果(太长且无意义)\n if (m.role === 'tool') {\n return '';\n }\n return `${m.role}: ${text}`;\n })\n .filter((line) => line.length > 0)\n .join('\\n');\n\n const task = `请总结以下对话,只保留有意义的信息(如任务需求、决策结论、重要上下文),忽略无意义的闲聊和重复内容。用1000字以内描述:\\n\n\n ${msg_str}`;\n\n try {\n // 使用 framework.createSubAgent 创建 Subagent 进行摘要\n // maxRetries: 0 禁用重试,避免长时间等待\n const subagent = this.framework.createSubAgent({\n name: 'context-compressor',\n role: '信息提取专家',\n systemPrompt:\n '你是一个对话摘要助手。只提取和保留有意义的信息(如任务需求、决策结论、重要上下文),忽略无意义的闲聊、重复内容和中间过程。输出要简洁。',\n maxRetries: 0,\n disableTools: true,\n });\n\n const result = await subagent.chat(task);\n if (result.success) {\n return result.message;\n }\n throw new Error(result.error || '摘要生成失败');\n } catch (err) {\n logger.warn('Summarize failed:', err.message);\n throw err;\n }\n }\n\n /** TokenCounter 实例 */\n _tokenCounter = new TokenCounter();\n\n /**\n * 计算消息数组的 token 数(委托给 TokenCounter)\n * @param {Array} messages - 消息数组\n * @returns {number}\n */\n _countMessagesTokens(messages) {\n return this._tokenCounter.countMessages(messages);\n }\n\n /**\n * 获取压缩统计\n * @returns {Object}\n */\n getStats() {\n return {\n compressionCount: this._compressionCount,\n inProgress: this._compressionInProgress,\n };\n }\n}\n\nmodule.exports = { ContextCompressor };\n",
|
|
318
|
+
"metadata": {
|
|
319
|
+
"filePath": "D:\\Code\\foliko\\src\\core\\context-compressor.js",
|
|
320
|
+
"size": 10639,
|
|
321
|
+
"lines": 342
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
"type": "tool-result",
|
|
328
|
+
"toolCallId": "call_function_muhqdqlj70po_3",
|
|
329
|
+
"toolName": "read",
|
|
330
|
+
"output": {
|
|
331
|
+
"type": "json",
|
|
332
|
+
"value": {
|
|
333
|
+
"success": true,
|
|
334
|
+
"data": "/**\n * AgentChatHandler 聊天处理器\n * 使用 AI SDK 的 ToolLoopAgent 处理工具调用循环\n *\n * 职责委托:\n * - ChatSession: 会话管理、消息历史、队列\n * - ToolExecutor: 工具发现、执行、验证\n * - ContextCompressor: 上下文压缩\n */\n\nconst { EventEmitter } = require('../utils/event-emitter');\nconst { logger } = require('../utils/logger');\nconst {\n tool: aiTool,\n ToolLoopAgent,\n isLoopFinished,\n} = require('ai');\nconst { autoSplitToolResult } = require('../../plugins/data-splitter-plugin');\nconst { cleanResponse } = require('../utils');\nconst { ChatQueueManager } = require('../utils/chat-queue');\nconst { TokenCounter } = require('./token-counter');\nconst { isThinkingModel } = require('./provider');\n// 新模块\nconst { ChatSession } = require('./chat-session');\nconst { ToolExecutor } = require('./tool-executor');\nconst { ContextCompressor } = require('./context-compressor');\nconst {\n DEFAULT_MAX_OUTPUT_TOKENS,\n DEFAULT_TEMPERATURE,\n DEFAULT_MAX_STEPS,\n DEFAULT_MAX_CONCURRENT,\n DEFAULT_RETRY_ATTEMPTS,\n DEFAULT_RETRY_DELAY_MS,\n} = require('./constants');\n\nclass AgentChatHandler extends EventEmitter {\n /**\n * @param {Agent} agent - Agent 实例\n * @param {Object} config - 配置\n */\n constructor(agent, config = {}) {\n super();\n\n this.agent = agent;\n this.config = config;\n\n // AI 配置\n this.model = config.model || 'deepseek-chat';\n this.provider = config.provider || 'deepseek';\n this.apiKey = config.apiKey;\n this.baseURL = config.baseURL;\n this.providerOptions = config.providerOptions || {};\n this.providerOptions.maxOutputTokens = config.providerOptions?.maxOutputTokens || 8192;\n\n // DeepSeek thinking mode: 支持通过 config.thinkingMode 控制开关\n // config.thinkingMode = true 可启用,默认为禁用\n const enableThinking = config.thinkingMode === true && isThinkingModel(this.model);\n this._thinkingMode = enableThinking;\n\n if (enableThinking) {\n // thinking mode 模型不支持 temperature 参数\n this.providerOptions.temperature = undefined;\n // DeepSeek 需要通过 extra_body 启用 thinking mode\n if (!this.providerOptions.deepseek) {\n this.providerOptions.deepseek = {};\n }\n this.providerOptions.deepseek.thinking = { type: 'enabled' };\n } else {\n this.providerOptions.temperature = config.providerOptions?.temperature || 0.3;\n }\n\n this._systemPrompt = config.systemPrompt || 'You are a helpful assistant.';\n this._maxSteps = config.maxSteps || 20;\n\n // 委托给新模块\n // ChatSession: 会话、消息、队列\n this._chatSession = new ChatSession({\n agent,\n maxConcurrent: config.maxConcurrent || 1,\n retryAttempts: config.retryAttempts || 3,\n retryDelay: config.retryDelay || 2000,\n });\n\n // ToolExecutor: 工具管理\n this._toolExecutor = new ToolExecutor({\n agent,\n framework: agent?.framework,\n });\n\n // ContextCompressor: 压缩\n this._contextCompressor = new ContextCompressor({\n agent,\n framework: agent?.framework,\n model: this.model,\n maxContextTokens: config.maxContextTokens,\n keepRecentMessages: config.keepRecentMessages || 20,\n enableSmartCompress: config.enableSmartCompress !== false,\n });\n\n // 上下文限制\n this._maxContextTokens = this._contextCompressor._maxContextTokens;\n\n // Token 计算缓存(避免每次请求重复计算)\n this._toolsTokensCache = null;\n this._toolsTokensCacheVersion = 0;\n this._systemPromptTokensCache = null;\n this._systemPromptTokensCacheKey = null;\n\n // ChatQueueManager: 队列管理\n this.queueManager = new ChatQueueManager({\n maxConcurrent: config.maxConcurrent || DEFAULT_MAX_CONCURRENT,\n retryAttempts: config.retryAttempts || DEFAULT_RETRY_ATTEMPTS,\n retryDelay: config.retryDelay || DEFAULT_RETRY_DELAY_MS,\n });\n\n // AI client\n this._aiClient = null;\n this._aiProvider = null;\n\n // Session Memory prepareStep\n this._sessionMemoryPrepareStep = null;\n if (agent?.framework) {\n const memoryPlugin = agent.framework.pluginManager.get('memory');\n if (memoryPlugin?.instance?.getPrepareStep) {\n this._sessionMemoryPrepareStep = memoryPlugin.instance.getPrepareStep();\n logger.debug('Session memory prepareStep loaded');\n }\n }\n\n // 转发 ChatSession 事件\n this._setupEventForwarding();\n\n // 转发 queueManager 事件到 ChatSession(让 SessionScope 能收到 stream:chunk 和 queue:completed 等)\n this.queueManager.on('stream:chunk', (data) => {\n this._chatSession.emit('stream:chunk', data);\n });\n this.queueManager.on('queue:completed', (data) => {\n this._chatSession.emit('queue:completed', data);\n });\n this.queueManager.on('queue:failed', (data) => {\n this._chatSession.emit('queue:failed', data);\n });\n\n // 转发 message:complete 和 message:error 到 ChatSession(让 SessionScope 能收到)\n this.on('message:start', (data) => { \n this._chatSession.emit('message:start', data); \n }); \n this.on('message:complete', (data) => {\n this._chatSession.emit('message:complete', data);\n });\n this.on('message:error', (data) => {\n this._chatSession.emit('message:error', data);\n });\n\n // 设置消息处理器(使用 bind 保持 this 引用)\n this._chatSession.setMessageProcessor(this._processMessage.bind(this));\n }\n\n /**\n * 设置事件转发\n * @private\n */\n _setupEventForwarding() {\n const events = [\n 'queue:added',\n 'queue:processing',\n 'queue:completed',\n 'queue:failed',\n 'queue:retry',\n 'queue:empty',\n 'queue:cleared',\n 'queue:session-removed',\n 'stream:chunk',\n ];\n\n events.forEach((eventName) => {\n this._chatSession.on(eventName, (data) => {\n this.emit(eventName, data);\n });\n });\n\n // 转发工具事件\n this._toolExecutor.on('tool:call', (data) => this.emit('tool:call', data));\n this._toolExecutor.on('tool:result', (data) => this.emit('tool:result', data));\n this._toolExecutor.on('tool:error', (data) => this.emit('tool:error', data));\n }\n\n // ==================== 工具管理(委托给 ToolExecutor) ====================\n\n /** AI tools 格式缓存 (避免每次 chat 重建) */\n _aiToolsCache = null;\n _aiToolsCacheKey = ''; // 工具列表的哈希摘要\n\n registerTool(tool) {\n this._toolExecutor.registerTool(tool);\n // 工具列表变了,清除缓存\n this._toolsTokensCacheVersion = 0;\n this._aiToolsCache = null; // 清除 AI tools 缓存\n return this;\n }\n\n getTool(name) {\n return this._toolExecutor.getTool(name);\n }\n\n getTools() {\n return this._toolExecutor.getAllTools();\n }\n\n // ==================== 会话管理(委托给 ChatSession) ====================\n\n createSessionScope(sessionId) {\n return this._chatSession.createSessionScope(sessionId);\n }\n\n getSessionMessageStore(sessionId) {\n return this._chatSession.getSessionMessageStore(sessionId);\n }\n\n // ==================== 压缩(委托给 ContextCompressor) ====================\n\n async _compressContext(sessionId, messages, messageStore) {\n return this._contextCompressor.compress(sessionId, messages, messageStore);\n }\n\n _validateMessagesPairing(messages) {\n return this._contextCompressor.validateMessagesPairing(messages);\n }\n\n /**\n * 验证工具调用\n * @private\n */\n _validateToolCalls(messages) {\n return this._toolExecutor.validateToolCalls(messages);\n }\n\n // ==================== AI 调用 ====================\n\n setAIClient(client) {\n this._aiClient = client;\n }\n\n setSystemPrompt(prompt) {\n this._systemPrompt = prompt;\n }\n\n /**\n * 获取 AI Provider 和 Model\n * @private\n */\n _createAIProvider() {\n const { createAI, createModel } = require('./provider');\n const aiProvider = createAI({\n provider: this.provider,\n model: this.model,\n apiKey: this.apiKey,\n baseURL: this.baseURL,\n });\n const model = createModel(this.model, aiProvider);\n return { provider: aiProvider, model };\n }\n\n /**\n * 创建 ToolLoopAgent\n * @private\n */\n _createToolLoopAgent(model, tools) {\n return new ToolLoopAgent({\n model,\n instructions: this._systemPrompt,\n tools,\n stopWhen: isLoopFinished(),\n prepareStep: this._createPrepareStep(),\n });\n }\n\n /**\n * 发送消息(带队列)\n */\n async sendMessage(message, options = {}) {\n const sessionId = options.sessionId || this.getDefaultSessionId();\n const requestId = this.generateRequestId();\n\n\n this.emit('message:start', {\n requestId,\n sessionId,\n message,\n timestamp: Date.now(),\n });\n\n try {\n const result = await this.queueManager.enqueue(requestId, sessionId, message, {\n ...options,\n executeFunction: this.chatStream.bind(this),\n });\n\n this.emit('message:complete', {\n ...result,\n requestId,\n sessionId,\n duration: Date.now() - result.startTime,\n });\n\n return result;\n } catch (error) {\n this.emit('message:error', {\n requestId,\n sessionId,\n error: error.message,\n });\n throw error;\n }\n }\n\n /**\n * 流式发送消息(带队列,支持实时yield)\n */\n async *sendMessageStream(message, options = {}) {\n const sessionId = options.sessionId || this.getDefaultSessionId();\n const requestId = this.generateRequestId();\n\n // 用于存储流式数据\n const chunkQueue = [];\n let streamCompleted = false;\n let streamError = null;\n let resolveNext = null;\n\n // 监听流式数据\n const chunkHandler = (data) => {\n if (data.requestId === requestId) {\n if (resolveNext) {\n resolveNext();\n resolveNext = null;\n }\n }\n };\n\n const completeHandler = (data) => {\n if (data.requestId === requestId) {\n streamCompleted = true;\n if (resolveNext) {\n resolveNext();\n resolveNext = null;\n }\n }\n };\n\n const errorHandler = (data) => {\n if (data.requestId === requestId) {\n streamError = new Error(data.error);\n if (resolveNext) {\n resolveNext();\n resolveNext = null;\n }\n }\n };\n\n // 注册事件监听\n this.on('stream:chunk', chunkHandler);\n this.on('queue:completed', completeHandler);\n this.on('queue:failed', errorHandler);\n\n // 启动队列任务(不等待完成)\n const queuePromise = this.queueManager\n .enqueue(requestId, sessionId, message, {\n ...options,\n executeFunction: this.chatStream.bind(this),\n })\n .catch((err) => {\n // 只设置 streamError,不打印日志(避免重复)\n if (!streamError) {\n streamError = err;\n }\n });\n\n try {\n // 实时 yield 流式数据\n while (!streamCompleted && !streamError) {\n if (chunkQueue.length > 0) {\n yield chunkQueue.shift();\n } else {\n await new Promise((resolve) => {\n resolveNext = resolve;\n });\n }\n }\n\n if (streamError) {\n throw streamError;\n }\n\n // 等待队列完成,获取完整结果\n const result = await queuePromise;\n this.emit('message:complete', { ...result, requestId, sessionId });\n } finally {\n // 清理事件监听\n this.off('stream:chunk', chunkHandler);\n this.off('queue:completed', completeHandler);\n this.off('queue:failed', errorHandler);\n }\n }\n\n /**\n * 批量发送消息\n */\n async sendBatch(messages, options = {}) {\n const promises = messages.map((msg, index) => {\n return this.sendMessage(msg, {\n ...options,\n priority: options.priorities?.[index] || 0,\n });\n });\n\n this.emit('batch:start', { count: messages.length });\n\n const results = await Promise.allSettled(promises);\n\n this.emit('batch:complete', {\n total: results.length,\n fulfilled: results.filter((r) => r.status === 'fulfilled').length,\n rejected: results.filter((r) => r.status === 'rejected').length,\n });\n\n return results;\n }\n\n /**\n * 取消会话的所有请求\n */\n cancelSession(sessionId) {\n const count = this.queueManager.removeSessionRequests(sessionId);\n this.emit('session:cancelled', { sessionId, count });\n return count;\n }\n\n /**\n * 获取默认 sessionId\n */\n getDefaultSessionId() {\n return 'default';\n }\n\n /**\n * 生成请求 ID\n */\n generateRequestId() {\n return `req_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;\n }\n\n /**\n * 聊天(直接非流式,使用 agent.generate)\n */\n async chat(message, options = {}) {\n const sessionId = options.sessionId || this.getDefaultSessionId();\n const result = await this._processMessage(sessionId, message, options);\n return { message: result.text, ...result };\n }\n\n /**\n * 流式聊天\n */\n async *chatStream(message, options = {}) {\n const sessionId = options.sessionId || this.getDefaultSessionId();\n const framework = this.agent.framework;\n\n try {\n const { messageStore, messages } = await this._prepareSession(message, sessionId);\n\n if (!this._aiClient) {\n throw new Error('AI client not configured.');\n }\n\n const systemPrompt = framework.getSystemPrompt();\n const tools = this._getAITools(aiTool);\n const agent = new ToolLoopAgent({\n model: this._aiClient,\n instructions: systemPrompt,\n tools,\n stopWhen: isLoopFinished(),\n prepareStep: this._createPrepareStep(),\n });\n\n // AI SDK 错误回调:仅记录日志,不在这里处理(统一由 catch/yield 处理)\n const handleSDKError = ({ error }) => {\n logger.debug(`[AgentChat] SDK onError: ${error?.message}`);\n };\n\n // DeepSeek thinking mode: 收集 reasoning_content\n const isThinking = this._thinkingMode;\n let reasoningContent = '';\n\n const result = await framework.runInSession(sessionId, {}, async () => {\n return agent.stream({ messages, ...this.providerOptions, onError: handleSDKError });\n });\n\n const stream = result.fullStream;\n let fullText = '';\n const iterator = stream[Symbol.asyncIterator] ? stream : stream.fullStream;\n\n for await (const part of iterator || stream) {\n if (part.type === 'text-delta') {\n const text = part.text || part.textDelta || '';\n fullText += text;\n yield { type: 'text', text };\n } else if (part.type === 'reasoning') {\n reasoningContent += part.text || '';\n yield { type: 'thinking', text: part.text };\n } else if (part.type === 'tool-call') {\n yield { type: 'tool-call', toolName: part.toolName, input: part.input };\n } else if (part.type === 'tool-result') {\n yield { type: 'tool-result', toolName: part.toolName, result: part.output };\n } else if (part.type === 'error') {\n // 统一错误消息\n const errMsg = part.error?.message || 'AI 服务错误';\n yield { type: 'error', error: errMsg.split('\\n')[0] };\n }\n }\n\n // DeepSeek thinking mode: 标记 reasoning_content 已处理\n // 在 finishMessages 中标记,避免重复添加\n const finishMessages = (await result.response).messages;\n if (isThinking && reasoningContent) {\n // DeepSeek 要求:当有 tool_calls 时,必须将 reasoning_content 添加到消息中\n // AI SDK 的 finishMessage 不会包含 reasoning_content,需要手动添加\n for (const msg of finishMessages) {\n if (msg.role === 'assistant') {\n // 检查是否有 tool_calls\n const hasToolCalls = msg.tool_calls?.length > 0 ||\n (Array.isArray(msg.content) && msg.content.some(c => c.type === 'tool-call' || c.type === 'tool-use'));\n if (hasToolCalls) {\n // DeepSeek API 要求:带 tool_calls 时必须传递 reasoning_content\n msg.reasoning_content = reasoningContent;\n }\n }\n }\n }\n messages.push(...finishMessages);\n\n // 获取或估算 token 用量\n const usage = await result.totalUsage;\n let inputTokens = 0, outputTokens = 0;\n\n if (usage && (usage.promptTokens > 0 || usage.completionTokens > 0 || usage.prompt_tokens > 0 || usage.completion_tokens > 0)) {\n // totalUsage 有真实数据(DeepSeek 等 API)\n inputTokens = usage.promptTokens || usage.prompt_tokens || 0;\n outputTokens = usage.completionTokens || usage.completion_tokens || 0;\n this._updateMessageStoreUsage(messageStore, usage);\n } else {\n // totalUsage 不可用或为空(MiniMax 等 API 返回 {promptTokens:0,completionTokens:0})\n // 用 TokenCounter 从实际消息内容估算\n inputTokens = this._tokenCounter.countMessages(messages);\n outputTokens = this._tokenCounter.countText(fullText);\n }\n\n // 确保 messageStore.usage 始终有数据\n const usageData = { promptTokens: inputTokens, completionTokens: outputTokens };\n messageStore.usage = usageData;\n\n // 同时通过 framework 事件广播(绕开 queue 事件链路)\n if (this.agent && this.agent.framework) {\n this.agent.framework.emit('agent:usage', {\n sessionId,\n usage: usageData,\n });\n }\n\n // yield usage 数据,让 UI 层能直接获取\n yield {\n type: 'usage',\n sessionId,\n inputTokens,\n outputTokens,\n };\n\n const userMsg = messages[messages.length - finishMessages.length - 1];\n this.emit('message', { content: fullText, sessionId: sessionId, userMessage: userMsg });\n } catch (err) {\n // 统一错误处理:提取简洁消息并通过 yield 传递\n const friendlyMessage = err?.message?.split('\\n')[0] || '未知错误';\n\n this.emit('message:error', { sessionId, error: friendlyMessage, originalError: err });\n yield { type: 'error', error: friendlyMessage };\n } finally {\n const messageStore = this._getSessionMessageStore(sessionId);\n messageStore.save().catch((err) => {\n logger.error(`[${sessionId}] Failed to save message store: ${err.message}`);\n });\n }\n }\n\n /**\n * 清除历史\n */\n clearHistory(sessionId) {\n if (sessionId) {\n const store = this._chatSession.getSessionMessageStore(sessionId);\n store.messages = [];\n store.historyLoaded = false;\n }\n }\n\n /**\n * 销毁\n */\n destroy() {\n if (this._chatSession) {\n this._chatSession.removeAllListeners();\n }\n this.removeAllListeners();\n }\n\n /**\n * 入队消息(走共享的 ChatQueueManager,与 sendMessage 共用同一队列)\n */\n enqueue(sessionId, message, options = {}) {\n const requestId = this.generateRequestId();\n return this.queueManager.enqueue(requestId, sessionId, message, {\n ...options,\n executeFunction: this.chatStream.bind(this),\n });\n }\n\n /**\n * 处理消息(队列回调)\n * @private\n */\n async _processMessage(sessionId, message, options) {\n const framework = this.agent.framework;\n\n try {\n const { messageStore, messages } = await this._prepareSession(message, sessionId);\n\n if (!this._aiClient) {\n throw new Error('AI client not configured.');\n }\n const systemPrompt = framework.getSystemPrompt();\n const tools = this._getAITools(aiTool);\n\n // DeepSeek thinking mode: 处理参数\n const apiOptions = { ...this.providerOptions };\n if (this._thinkingMode) {\n delete apiOptions.temperature;\n }\n\n const agent = new ToolLoopAgent({\n model: this._aiClient,\n instructions: systemPrompt,\n tools,\n stopWhen: isLoopFinished(),\n prepareStep: this._createPrepareStep(),\n });\n\n const result = await framework.runInSession(sessionId, {}, async () => {\n return agent.generate({ messages, ...apiOptions });\n });\n\n if (result.usage && (result.usage.promptTokens > 0 || result.usage.completionTokens > 0)) {\n this._updateMessageStoreUsage(messageStore, result.usage);\n } else {\n // fallback: 用 TokenCounter 估算\n const inputTokens = this._tokenCounter.countMessages(messages);\n const outputTokens = this._tokenCounter.countText(result.text || '');\n messageStore.usage = { promptTokens: inputTokens, completionTokens: outputTokens };\n }\n\n // DeepSeek thinking mode: 处理 reasoning_content\n const isThinking = this._thinkingMode;\n if (isThinking && result.reasoningText) {\n // 有 reasoning_text 需要添加到消息中(当有 tool_calls 时)\n const finishMessages = result.response.messages;\n for (const msg of finishMessages) {\n if (msg.role === 'assistant') {\n const hasToolCalls = msg.tool_calls?.length > 0 ||\n (Array.isArray(msg.content) && msg.content.some(c => c.type === 'tool-call' || c.type === 'tool-use'));\n if (hasToolCalls) {\n msg.reasoning_content = result.reasoningText;\n }\n }\n }\n }\n\n messages.push(...result.response.messages);\n const userMsg = messages[messages.length - result.response.messages.length - 1];\n this.emit('message', { content: result.text, sessionId: sessionId, userMessage: userMsg });\n\n return {\n success: true,\n message: cleanResponse(result.text || ''),\n stepCount: result.stepCount || 1,\n };\n } catch (err) {\n logger.error('[AgentChat] _processMessage error:', err.message);\n const errorMsg = err.message || String(err);\n\n return {\n success: false,\n message: 'AI 服务暂时不可用,请稍后重试。',\n error: errorMsg,\n stepCount: 0,\n };\n } finally {\n const messageStore = this._getSessionMessageStore(sessionId);\n try {\n await messageStore.save();\n } catch (err) {\n logger.error(`[${sessionId}] Failed to save message store: ${err.message}`);\n }\n }\n }\n\n /**\n * 获取 Session Message Store\n * @private\n */\n _getSessionMessageStore(sessionId) {\n return this._chatSession.getSessionMessageStore(sessionId);\n }\n\n /**\n * 准备会话消息\n * @private\n */\n async _prepareSession(message, sessionId) {\n const messageStore = this._getSessionMessageStore(sessionId);\n // 先加载 session 文件中的历史消息\n this._chatSession.loadHistory(sessionId);\n const messages = messageStore.messages;\n\n // 刷新系统提示词\n this._systemPrompt = this.agent._buildSystemPrompt();\n //await fs.writeFile('system.md',this._systemPrompt)\n // 添加用户消息\n const userMessage = typeof message === 'string' ? { role: 'user', content: message } : message;\n\n // 获取缓存的消息 token 数(跳过全量重算)\n const cachedMsgTokens = this._getCachedMessageTokens(messageStore);\n const toolsTokens = this._getCachedToolsTokens();\n const systemPromptTokens = this._getCachedSystemPromptTokens();\n const totalTokens = cachedMsgTokens + toolsTokens + systemPromptTokens;\n const limit = this._maxContextTokens * 0.5;\n\n logger.info(`BEFORE: messages=${messages.length}, tokens=${totalTokens}/${limit}`);\n\n if (totalTokens > limit || messages.length > 100) {\n logger.info(\n `Context large (${messages.length} msgs, ${totalTokens}/${this._maxContextTokens} tokens), compressing...`\n );\n await this._compressContext(sessionId, messages, messageStore);\n // 压缩后消息被截断/替换,缓存已失效,下次 _getCachedMessageTokens 会重新计算\n this._invalidateMessageTokensCache(messageStore);\n logger.debug(`[_prepareSession] AFTER compress: messages=${messages.length}`);\n }\n\n // 验证工具调用\n const validated = this._validateMessagesPairing(messages);\n if (validated.length !== messages.length) {\n // 删除了消息,缓存失效\n this._invalidateMessageTokensCache(messageStore);\n messages.length = 0;\n messages.push(...validated);\n }\n messages.push(userMessage);\n this._validateToolCalls(messages);\n\n return { messageStore, messages };\n }\n\n /**\n * 更新消息存储的使用量\n * @private\n */\n _updateMessageStoreUsage(messageStore, usage) {\n if (messageStore && usage) {\n messageStore.usage = usage;\n }\n }\n\n /**\n * 获取缓存的消息 token 总数(跳过 O(n) 重算)\n * 仅在 messageCount 与缓存版本一致时命中\n * @private\n */\n _getCachedMessageTokens(messageStore) {\n if (\n messageStore._messageTokensCache !== undefined &&\n messageStore._cacheMessageCount === messageStore.messages.length\n ) {\n return messageStore._messageTokensCache;\n }\n // 缓存未命中或已失效,重新计算\n const tokens = this._countMessagesTokens(messageStore.messages);\n messageStore._messageTokensCache = tokens;\n messageStore._cacheMessageCount = messageStore.messages.length;\n return tokens;\n }\n\n /**\n * 使消息 token 缓存失效(压缩或删消息后调用)\n * @private\n */\n _invalidateMessageTokensCache(messageStore) {\n messageStore._messageTokensCache = undefined;\n messageStore._cacheMessageCount = undefined;\n }\n\n /**\n * 同步消息 token 缓存到 messageStore(saveHistory 后调用)\n * @private\n */\n _syncMessageTokensCache(messageStore) {\n if (!messageStore) return;\n const tokens = this._countMessagesTokens(messageStore.messages);\n messageStore._messageTokensCache = tokens;\n messageStore._cacheMessageCount = messageStore.messages.length;\n }\n\n /**\n * 计算工具列表缓存 key\n * @private\n */\n _computeToolsCacheKey() {\n const allTools = this.agent.framework.getTools();\n return allTools.map((t) => `${t.name}:${t.description ? t.description.length : 0}`).join('|');\n }\n\n /**\n * 获取 AI 工具格式(带缓存)\n * 使用 AI SDK 的 tool() 格式\n * @private\n */\n _getAITools(toolFn) {\n const currentKey = this._computeToolsCacheKey();\n if (this._aiToolsCache && this._aiToolsCacheKey === currentKey) {\n return this._aiToolsCache;\n }\n\n const tools = {};\n const allTools = this.agent.framework.getTools();\n for (const toolDef of allTools) {\n const toolName = toolDef.name;\n\n // 使用 AI SDK 的 tool() 格式\n const toolConfig = {\n name: toolName,\n description: toolDef.description || '',\n execute: async (args) => {\n // 清理参数\n const cleanedArgs = this._cleanToolArgs(args);\n\n // 执行工具\n this.emit('tool-call', { name: toolName, args: cleanedArgs });\n logger.info(`[Tool] Call: ${toolName}`);\n try {\n const result = await toolDef.execute(cleanedArgs, this.agent.framework);\n\n // 自动检测大工具结果,透明分拆\n // 仅在数据分拆插件已加载时生效\n let finalResult = result;\n try {\n const splitCheck = await autoSplitToolResult(toolName, result, this.agent.framework);\n if (splitCheck.wasSplit && splitCheck.result) {\n finalResult = splitCheck.result;\n logger.info(\n `[AutoSplit] 工具 \"${toolName}\" 结果过大,已自动分拆处理`\n );\n }\n } catch (splitErr) {\n // 分拆失败不阻断主流程,继续使用原始结果\n logger.warn(`[AutoSplit] 自动分拆跳过: ${splitErr.message}`);\n }\n\n this.emit('tool-result', { name: toolName, args: cleanedArgs, result: finalResult });\n return finalResult;\n } catch (err) {\n this.emit('tool-error', { name: toolName, args: cleanedArgs, error: err.message });\n // 返回错误信息字符串,而不是抛出异常\n return { success: false,error: err.message}\n }\n },\n };\n\n // 支持 inputSchema 或 parameters\n if (toolDef.inputSchema) {\n toolConfig.inputSchema = toolDef.inputSchema;\n } else if (toolDef.parameters) {\n toolConfig.parameters = toolDef.parameters;\n }\n\n // AI SDK 6.x 使用对象形式,键为工具名\n tools[toolName] = toolFn(toolConfig);\n }\n\n this._aiToolsCache = tools;\n this._aiToolsCacheKey = currentKey;\n return tools;\n }\n\n /**\n * 清理工具参数\n * @private\n */\n _cleanToolArgs(args) {\n if (!args || typeof args !== 'object') {\n return {};\n }\n\n const cleaned = {};\n for (const [key, value] of Object.entries(args)) {\n if (value === undefined || typeof value === 'function' || typeof value === 'symbol') {\n continue;\n }\n cleaned[key] = value;\n }\n return cleaned;\n }\n\n /**\n * 为 AI SDK prepareStep 准备消息\n * @private\n */\n _createPrepareStep() {\n return async ({ stepNumber, messages: inputMessages }) => {\n try {\n const tokenCount = this._countMessagesTokens(inputMessages);\n const tokenLimit = this._maxContextTokens * 0.75;\n\n // 超过限制时,保留三分之一的消息\n if (tokenCount > tokenLimit) {\n const keepCount = Math.max(10, Math.floor(inputMessages.length / 3));\n logger.debug(\n `[PrepareStep] Trimming: ${inputMessages.length} msgs -> ${keepCount}, tokens=${tokenCount}`\n );\n const trimmed = inputMessages.slice(-keepCount);\n inputMessages.length = 0;\n inputMessages.push(...trimmed);\n }\n\n // 验证 tool-call 和 tool-result 配对\n const validated = this._validateMessagesPairing(inputMessages);\n if (validated.length !== inputMessages.length) {\n logger.debug(\n `[PrepareStep] After pairing validation: ${inputMessages.length} -> ${validated.length}`\n );\n inputMessages.length = 0;\n inputMessages.push(...validated);\n }\n\n // 验证 tool-call 格式\n this._validateToolCalls(inputMessages);\n\n return { messages: inputMessages };\n } catch (err) {\n logger.error('prepareStep error:', err.message, err.stack);\n return { messages: inputMessages };\n }\n };\n }\n\n /**\n * 压缩上下文(用于 prepareStep)\n * @private\n */\n\n\n /** TokenCounter 实例 */\n _tokenCounter = new TokenCounter();\n\n /**\n * 计算文本 token(委托给 TokenCounter)\n * @private\n */\n _countTokens(text) {\n return this._tokenCounter.countText(text);\n }\n\n /**\n * 计算消息 token(委托给 TokenCounter)\n * @private\n */\n _countMessagesTokens(messages) {\n return this._tokenCounter.countMessages(messages);\n }\n\n /**\n * 计算工具 token(委托给 TokenCounter)\n * @private\n */\n _countToolsTokens() {\n return this._tokenCounter.countTools(this._toolExecutor.getAllTools());\n }\n\n /**\n * 获取缓存的工具 token 数(工具列表不变时复用)\n * @private\n */\n _getCachedToolsTokens() {\n const currentVersion = this._toolExecutor.getAllTools().length;\n if (this._toolsTokensCacheVersion !== currentVersion) {\n this._toolsTokensCache = this._countToolsTokens();\n this._toolsTokensCacheVersion = currentVersion;\n }\n return this._toolsTokensCache;\n }\n\n /**\n * 获取缓存的系统提示词 token 数(提示词不变时复用)\n * @private\n */\n _getCachedSystemPromptTokens() {\n const promptKey = this._systemPrompt;\n if (this._systemPromptTokensCacheKey !== promptKey) {\n this._systemPromptTokensCache = this._countTokens(promptKey);\n this._systemPromptTokensCacheKey = promptKey;\n }\n return this._systemPromptTokensCache;\n }\n\n // ==================== 兼容性方法 ====================\n\n cancelSession(sessionId) {\n this._chatSession.cancelSession(sessionId);\n }\n\n getQueueStatus(sessionId) {\n return this._chatSession.getQueueStatus(sessionId);\n }\n\n clearQueue(sessionId) {\n this._chatSession.clearQueue(sessionId);\n }\n}\n\nmodule.exports = { AgentChatHandler };\n",
|
|
335
|
+
"metadata": {
|
|
336
|
+
"filePath": "D:\\Code\\foliko\\src\\core\\agent-chat.js",
|
|
337
|
+
"size": 32568,
|
|
338
|
+
"lines": 1036
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
]
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
"role": "assistant",
|
|
347
|
+
"content": [
|
|
348
|
+
{
|
|
349
|
+
"type": "text",
|
|
350
|
+
"text": "<think>用户反馈说回复有时候会突然中断。这可能与几个因素有关:\n\n1. **Token 限制问题**:上下文太长,触发了压缩\n2. **超时问题**:AI 服务响应超时\n3. **连接问题**:网络不稳定\n4. **Token 计数不准确**:导致意外截断\n\n让我分析一下可能的原因:\n\n1. **TokenCounter 的估算可能不准确**:\n - 使用简单的字节计算:`bytes / 4`\n - 对于中文:每个字符 2 字节,所以约等于 0.5 token/字符\n - 但实际上中文可能是 1-2 token/字符\n - 这可能导致实际 token 超出限制\n\n2. **ContextCompressor 的压缩阈值**:\n - 默认 `keepRecentMessages: 20`\n - 在 `_prepareSession` 中:`limit = this._maxContextTokens * 0.5`\n - 当 token 超过 50% 时开始压缩\n\n3. **AI SDK 的 `maxOutputTokens`**:\n - 默认 8192\n - 如果回复很长,可能在中间截断\n\n4. **prepareStep 中的截断**:\n - 在 `_createPrepareStep` 中:`tokenLimit = this._maxContextTokens * 0.75`\n - 超过时只保留三分之一的\n\n让我看看日志文件,看看有没有什么线索。同时也可以检查 .foliko.log。\n</think>\n\n\n"
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
"type": "tool-call",
|
|
354
|
+
"toolCallId": "call_function_kaivhxs5722t_1",
|
|
355
|
+
"toolName": "read",
|
|
356
|
+
"input": {
|
|
357
|
+
"path": ".foliko.log",
|
|
358
|
+
"lines": 100
|
|
359
|
+
}
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
"type": "tool-call",
|
|
363
|
+
"toolCallId": "call_function_kaivhxs5722t_2",
|
|
364
|
+
"toolName": "read",
|
|
365
|
+
"input": {
|
|
366
|
+
"path": "src/core/agent-chat.js",
|
|
367
|
+
"lines": 100
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
]
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
"role": "tool",
|
|
374
|
+
"content": [
|
|
375
|
+
{
|
|
376
|
+
"type": "tool-result",
|
|
377
|
+
"toolCallId": "call_function_kaivhxs5722t_1",
|
|
378
|
+
"toolName": "read",
|
|
379
|
+
"output": {
|
|
380
|
+
"type": "json",
|
|
381
|
+
"value": {
|
|
382
|
+
"success": true,
|
|
383
|
+
"data": "\n=== Foliko started at 2026-05-02T10:01:53.609Z ===\n[2026-05-02T10:01:53.879Z] [DEBUG] process.env.QQ_APP_ID = 102888416\n[DEBUG] process.env.QQ_APP_ID = 102888416\n[2026-05-02T10:01:53.881Z] [DEBUG] process.env.QQ_CLIENT_SECRET = ***Xo6Oh0\n[DEBUG] process.env.QQ_CLIENT_SECRET = ***Xo6Oh0\n[2026-05-02T10:01:53.881Z] [DEBUG] process.env.FOLIKO_API_KEY = ***5f2b6f\n[DEBUG] process.env.FOLIKO_API_KEY = ***5f2b6f\n[2026-05-02T10:01:53.882Z] [Foliko] 启动后台服务...\n[Foliko] 启动后台服务...\n[2026-05-02T10:01:53.886Z] \u001b[2m[foliko:AgentConfig]\u001b[0m \u001b[2m10:01:53.886\u001b[0m \u001b[32mINFO \u001b[0m \u001b[90m Loading config from: D:\\Code\\foliko\\.agent\u001b[0m\n\u001b[2m[foliko:AgentConfig]\u001b[0m \u001b[2m10:01:53.886\u001b[0m \u001b[32mINFO \u001b[0m \u001b[90m Loading config from: D:\\Code\\foliko\\.agent\u001b[0m\n[2026-05-02T10:01:53.887Z] \u001b[2m[foliko:AgentConfig]\u001b[0m \u001b[2m10:01:53.887\u001b[0m \u001b[32mINFO \u001b[0m \u001b[90m Found agents directory: D:\\Code\\foliko\\.agent\\agents\u001b[0m\n\u001b[2m[foliko:AgentConfig]\u001b[0m \u001b[2m10:01:53.887\u001b[0m \u001b[32mINFO \u001b[0m \u001b[90m Found agents directory: D:\\Code\\foliko\\.agent\\agents\u001b[0m\n[2026-05-02T10:01:53.891Z] \u001b[2m[foliko:AIPlugin]\u001b[0m \u001b[2m10:01:53.891\u001b[0m \u001b[32mINFO \u001b[0m \u001b[90m Initialized deepseek/deepseek-v4-flash\u001b[0m\n\u001b[2m[foliko:AIPlugin]\u001b[0m \u001b[2m10:01:53.891\u001b[0m \u001b[32mINFO \u001b[0m \u001b[90m Initialized deepseek/deepseek-v4-flash\u001b[0m\n[2026-05-02T10:01:54.392Z] [poster] Poster plugin installed (v1.1.0)\n[poster] Poster plugin installed (v1.1.0)\n[2026-05-02T10:01:54.392Z] [poster] Components: background, rectangle, circle, line, polygon, text, artText, image, svg, imageFrame, columns, grid, star, arrow, progressCircle, chip, chart, watermark, table, card, badge, cta, feature, featureGrid, divider, avatar, progress, rating, quote, statCard, tagCloud, stepper, timeline, listItem, notification, button, icon, qrcode, frame, bubble, ribbon, seal, highlightText, barcode\n[poster] Components: background, rectangle, circle, line, polygon, text, artText, image, svg, imageFrame, columns, grid, star, arrow, progressCircle, chip, chart, watermark, table, card, badge, cta, feature, featureGrid, divider, avatar, progress, rating, quote, statCard, tagCloud, stepper, timeline, listItem, notification, button, icon, qrcode, frame, bubble, ribbon, seal, highlightText, barcode\n[2026-05-02T10:01:54.519Z] \u001b[2m[foliko:PythonPluginLoader]\u001b[0m \u001b[2m10:01:54.519\u001b[0m \u001b[32mINFO \u001b[0m \u001b[90m Loaded: test-plugin\u001b[0m\n\u001b[2m[foliko:PythonPluginLoader]\u001b[0m \u001b[2m10:01:54.519\u001b[0m \u001b[32mINFO \u001b[0m \u001b[90m Loaded: test-plugin\u001b[0m\n[2026-05-02T10:01:54.520Z] \u001b[2m[foliko:PythonPluginLoader]\u001b[0m \u001b[2m10:01:54.520\u001b[0m \u001b[32mINFO \u001b[0m \u001b[90m Total Python plugins: 1\u001b[0m\n\u001b[2m[foliko:PythonPluginLoader]\u001b[0m \u001b[2m10:01:54.520\u001b[0m \u001b[32mINFO \u001b[0m \u001b[90m Total Python plugins: 1\u001b[0m\n[2026-05-02T10:01:54.722Z] \u001b[2m[foliko:Web]\u001b[0m \u001b[2m10:01:54.722\u001b[0m \u001b[32mINFO \u001b[0m \u001b[90m 加载 Web 配置...\u001b[0m\n\u001b[2m[foliko:Web]\u001b[0m \u001b[2m10:01:54.722\u001b[0m \u001b[32mINFO \u001b[0m \u001b[90m 加载 Web 配置...\u001b[0m\n[2026-05-02T10:01:54.729Z] \u001b[2m[foliko:Ambient:ExplorerLoop]\u001b[0m \u001b[2m10:01:54.729\u001b[0m \u001b[32mINFO \u001b[0m \u001b[90mambient-worker subagent registered\u001b[0m\n\u001b[2m[foliko:Ambient:ExplorerLoop]\u001b[0m \u001b[2m10:01:54.729\u001b[0m \u001b[32mINFO \u001b[0m \u001b[90mambient-worker subagent registered\u001b[0m\n[2026-05-02T10:01:54.731Z] \u001b[2m[foliko:Ambient]\u001b[0m \u001b[2m10:01:54.731\u001b[0m \u001b[32mINFO \u001b[0m \u001b[90m插件已启动\u001b[0m\n\u001b[2m[foliko:Ambient]\u001b[0m \u001b[2m10:01:54.731\u001b[0m \u001b[32mINFO \u001b[0m \u001b[90m插件已启动\u001b[0m\n[2026-05-02T10:01:54.733Z] \u001b[2m[foliko:plugin-manager]\u001b[0m \u001b[2m10:01:54.733\u001b[0m \u001b[32mINFO \u001b[0m \u001b[90mPlugin 'coordinator' is disabled\u001b[0m\n\u001b[2m[foliko:plugin-manager]\u001b[0m \u001b[2m10:01:54.733\u001b[0m \u001b[32mINFO \u001b[0m \u001b[90mPlugin 'coordinator' is disabled\u001b[0m\n[2026-05-02T10:01:54.736Z] \u001b[2m[foliko:plugin-manager]\u001b[0m \u001b[2m10:01:54.736\u001b[0m \u001b[32mINFO \u001b[0m \u001b[90mPlugin 'email' is disabled\u001b[0m\n\u001b[2m[foliko:plugin-manager]\u001b[0m \u001b[2m10:01:54.736\u001b[0m \u001b[32mINFO \u001b[0m \u001b[90mPlugin 'email' is disabled\u001b[0m\n[2026-05-02T10:01:54.740Z] \u001b[2m[foliko:plugin-manager]\u001b[0m \u001b[2m10:01:54.740\u001b[0m \u001b[32mINFO \u001b[0m \u001b[90mPlugin 'feishu' is disabled\u001b[0m\n\u001b[2m[foliko:plugin-manager]\u001b[0m \u001b[2m10:01:54.740\u001b[0m \u001b[32mINFO \u001b[0m \u001b[90mPlugin 'feishu' is disabled\u001b[0m\n[2026-05-02T10:01:54.747Z] \u001b[2m[foliko:Memory]\u001b[0m \u001b[2m10:01:54.747\u001b[0m \u001b[32mINFO \u001b[0m \u001b[90mLoaded 1 memories from disk\u001b[0m\n\u001b[2m[foliko:Memory]\u001b[0m \u001b[2m10:01:54.747\u001b[0m \u001b[32mINFO \u001b[0m \u001b[90mLoaded 1 memories from disk\u001b[0m\n[2026-05-02T10:01:54.757Z] \u001b[2m[foliko:QQ]\u001b[0m \u001b[2m10:01:54.757\u001b[0m \u001b[32mINFO \u001b[0m \u001b[90m[QQ] [qqbot-sdk:102888416] Fetching access token...\u001b[0m\n\u001b[2m[foliko:QQ]\u001b[0m \u001b[2m10:01:54.757\u001b[0m \u001b[32mINFO \u001b[0m \u001b[90m[QQ] [qqbot-sdk:102888416] Fetching access token...\u001b[0m\n[2026-05-02T10:01:54.787Z] \u001b[2m[foliko:RulesPlugin]\u001b[0m \u001b[2m10:01:54.787\u001b[0m \u001b[32mINFO \u001b[0m \u001b[90m Rules directory not found: D:\\Code\\foliko\\.agent\\rules\u001b[0m\n\u001b[2m[foliko:RulesPlugin]\u001b[0m \u001b[2m10:01:54.787\u001b[0m \u001b[32mINFO \u001b[0m \u001b[90m Rules directory not found: D:\\Code\\foliko\\.agent\\rules\u001b[0m\n[2026-05-02T10:01:54.793Z] \u001b[2m[foliko:Session]\u001b[0m \u001b[2m10:01:54.793\u001b[0m \u001b[32mINFO \u001b[0m \u001b[90mLoaded 0 sessions from storage\u001b[0m\n\u001b[2m[foliko:Session]\u001b[0m \u001b[2m10:01:54.793\u001b[0m \u001b[32mINFO \u001b[0m \u001b[90mLoaded 0 sessions from storage\u001b[0m\n[2026-05-02T10:01:54.796Z] \u001b[2m[foliko:Storage]\u001b[0m \u001b[2m10:01:54.796\u001b[0m \u001b[32mINFO \u001b[0m \u001b[90m Loaded 5 entries from D:\\Code\\foliko\\.agent\\data\\default.json\u001b[0m\n\u001b[2m[foliko:Storage]\u001b[0m \u001b[2m10:01:54.796\u001b[0m \u001b[32mINFO \u001b[0m \u001b[90m Loaded 5 entries from D:\\Code\\foliko\\.agent\\data\\default.json\u001b[0m\n[2026-05-02T10:01:54.799Z] \u001b[2m[foliko:plugin-manager]\u001b[0m \u001b[2m10:01:54.799\u001b[0m \u001b[32mINFO \u001b[0m \u001b[90mPlugin 'telegram' is disabled\u001b[0m\n\u001b[2m[foliko:plugin-manager]\u001b[0m \u001b[2m10:01:54.799\u001b[0m \u001b[32mINFO \u001b[0m \u001b[90mPlugin 'telegram' is disabled\u001b[0m\n[2026-05-02T10:01:54.802Z] \u001b[2m[foliko:plugin-manager]\u001b[0m \u001b[2m10:01:54.802\u001b[0m \u001b[32mINFO \u001b[0m \u001b[90mPlugin 'weixin' is disabled\u001b[0m\n\u001b[2m[foliko:plugin-manager]\u001b[0m \u001b[2m10:01:54.802\u001b[0m \u001b[32mINFO \u001b[0m \u001b[90mPlugin 'weixin' is disabled\u001b[0m\n[2026-05-02T10:01:54.810Z] \u001b[2m[foliko:SkillManager]\u001b[0m \u001b[2m10:01:54.810\u001b[0m \u001b[32mINFO \u001b[0m \u001b[90m Loaded 14 skills\u001b[0m\n\u001b[2m[foliko:SkillManager]\u001b[0m \u001b[2m10:01:54.810\u001b[0m \u001b[32mINFO \u001b[0m \u001b[90m Loaded 14 skills\u001b[0m\n[2026-05-02T10:01:54.812Z] \u001b[2m[foliko:MCPExecutor]\u001b[0m \u001b[2m10:01:54.812\u001b[0m \u001b[32mINFO \u001b[0m \u001b[90m[MCP] addServer called: designmd, enabled=true, hasClient=false\u001b[0m\n\u001b[2m[foliko:MCPExecutor]\u001b[0m \u001b[2m10:01:54.812\u001b[0m \u001b[32mINFO \u001b[0m \u001b[90m[MCP] addServer called: designmd, enabled=true, hasClient=false\u001b[0m\n[2026-05-02T10:01:54.812Z] \u001b[2m[foliko:MCPExecutor]\u001b[0m \u001b[2m10:01:54.812\u001b[0m \u001b[32mINFO \u001b[0m \u001b[90m Connecting to designmd...\u001b[0m\n\u001b[2m[foliko:MCPExecutor]\u001b[0m \u001b[2m10:01:54.812\u001b[0m \u001b[32mINFO \u001b[0m \u001b[90m Connecting to designmd...\u001b[0m\n[2026-05-02T10:01:54.965Z] \u001b[2m[foliko:QQ]\u001b[0m \u001b[2m10:01:54.965\u001b[0m \u001b[32mINFO \u001b[0m \u001b[90m[QQ] [qqbot-sdk:102888416] Token cached, expires at: 2026-05-02T10:18:22.965Z\u001b[0m\n\u001b[2m[foliko:QQ]\u001b[0m \u001b[2m10:01:54.965\u001b[0m \u001b[32mINFO \u001b[0m \u001b[90m[QQ] [qqbot-sdk:102888416] Token cached, expires at: 2026-05-02T10:18:22.965Z\u001b[0m\n[2026-05-02T10:01:55.153Z] \u001b[2m[foliko:QQ]\u001b[0m \u001b[2m10:01:55.153\u001b[0m \u001b[32mINFO \u001b[0m \u001b[90m[QQ] [qqbot-sdk] Connecting to gateway...\u001b[0m\n\u001b[2m[foliko:QQ]\u001b[0m \u001b[2m10:01:55.153\u001b[0m \u001b[32mINFO \u001b[0m \u001b[90m[QQ] [qqbot-sdk] Connecting to gateway...\u001b[0m\n[2026-05-02T10:01:55.210Z] \u001b[2m[foliko:QQ]\u001b[0m \u001b[2m10:01:55.210\u001b[0m \u001b[32mINFO \u001b[0m \u001b[90m[QQ] [qqbot-sdk] Connected to gateway\u001b[0m\n\u001b[2m[foliko:QQ]\u001b[0m \u001b[2m10:01:55.210\u001b[0m \u001b[32mINFO \u001b[0m \u001b[90m[QQ] [qqbot-sdk] Connected to gateway\u001b[0m\n[2026-05-02T10:01:55.211Z] \u001b[2m[foliko:QQ]\u001b[0m \u001b[2m10:01:55.211\u001b[0m \u001b[32mINFO \u001b[0m \u001b[90mQQ Bot 已连接\u001b[0m\n\u001b[2m[foliko:QQ]\u001b[0m \u001b[2m10:01:55.211\u001b[0m \u001b[32mINFO \u001b[0m \u001b[90mQQ Bot 已连接\u001b[0m\n[2026-05-02T10:01:55.212Z] \u001b[2m[foliko:QQ]\u001b[0m \u001b[2m10:01:55.212\u001b[0m \u001b[32mINFO \u001b[0m \u001b[90m[QQ] [qqbot-sdk] Received HELLO, heartbeat_interval=41250ms\u001b[0m\n\u001b[2m[foliko:QQ]\u001b[0m \u001b[2m10:01:55.212\u001b[0m \u001b[32mINFO \u001b[0m \u001b[90m[QQ] [qqbot-sdk] Received HELLO, heartbeat_interval=41250ms\u001b[0m\n[2026-05-02T10:01:55.214Z] \u001b[2m[foliko:QQ]\u001b[0m \u001b[2m10:01:55.214\u001b[0m \u001b[32mINFO \u001b[0m \u001b[90m[QQ] [qqbot-sdk] Sent IDENTIFY: token=QQBot LnLWxn3miHaEpm..., intents=1174409216, identify={\"op\":2,\"d\":{\"token\":\"QQBot LnLWxn3miHaEpmRwsmBliMNhtDwjSOCVf4LA3B0fuXWoyif1xgaUCsamjsHKf7ldrBZH2-WabJ6Ghw\",\"intents\":1174409216,\"shard\":[0,1]}}\u001b[0m\n\u001b[2m[foliko:QQ]\u001b[0m \u001b[2m10:01:55.214\u001b[0m \u001b[32mINFO \u001b[0m \u001b[90m[QQ] [qqbot-sdk] Sent IDENTIFY: token=QQBot LnLWxn3miHaEpm..., intents=1174409216, identify={\"op\":2,\"d\":{\"token\":\"QQBot LnLWxn3miHaEpmRwsmBliMNhtDwjSOCVf4LA3B0fuXWoyif1xgaUCsamjsHKf7ldrBZH2-WabJ6Ghw\",\"intents\":1174409216,\"shard\":[0,1]}}\u001b[0m\n[2026-05-02T10:01:55.286Z] \u001b[2m[foliko:QQ]\u001b[0m \u001b[2m10:01:55.286\u001b[0m \u001b[32mINFO \u001b[0m \u001b[90m[QQ] [qqbot-sdk] Received READY event, connection established!\u001b[0m\n\u001b[2m[foliko:QQ]\u001b[0m \u001b[2m10:01:55.286\u001b[0m \u001b[32mINFO \u001b[0m \u001b[90m[QQ] [qqbot-sdk] Received READY event, connection established!\u001b[0m\n[2026-05-02T10:02:05.557Z] \u001b[2m[foliko]\u001b[0m \u001b[2m10:02:05.557\u001b[0m \u001b[32mINFO \u001b[0m \u001b[90mBEFORE: messages=16, tokens=4601/400000\u001b[0m\n\u001b[2m[foliko]\u001b[0m \u001b[2m10:02:05.557\u001b[0m \u001b[32mINFO \u001b[0m \u001b[90mBEFORE: messages=16, tokens=4601/400000\u001b[0m\n[2026-05-02T10:02:06.052Z] ERROR \u001b[2m[foliko:MCPExecutor]\u001b[0m \u001b[2m10:02:06.052\u001b[0m \u001b[31mERROR\u001b[0m \u001b[90m Failed to create client 'designmd': createMCPClient designmd timeout (10000ms)\u001b[0m\n\u001b[2m[foliko:MCPExecutor]\u001b[0m \u001b[2m10:02:06.052\u001b[0m \u001b[31mERROR\u001b[0m \u001b[90m Failed to create client 'designmd': createMCPClient designmd timeout (10000ms)\u001b[0m\n[2026-05-02T10:02:06.053Z] [poster] Poster plugin started\n[poster] Poster plugin started\n[2026-05-02T10:02:06.072Z] [Foliko] 框架已就绪\n[Foliko] 框架已就绪\n[2026-05-02T10:02:06.073Z] [Foliko] 已加载插件: defaults,install,ai,skill-manager,mcp,python-plugin-loader,subagent-manager,marknative,poster,ambient,audit,extension-executor,file-system,gate-trading,memory,plugin_manager,python-executor,qq,rules,scheduler,session,shell-executor,storage,think,tools,web\n[Foliko] 已加载插件: [\n 'defaults', 'install',\n 'ai', 'skill-manager',\n 'mcp', 'python-plugin-loader',\n 'subagent-manager', 'marknative',\n 'poster', 'ambient',\n 'audit', 'extension-executor',\n 'file-system', 'gate-trading',\n 'memory', 'plugin_manager',\n 'python-executor', 'qq',\n 'rules', 'scheduler',\n 'session', 'shell-executor',\n 'storage', 'think',\n 'tools', 'web'\n]\n[2026-05-02T10:02:06.074Z] [Foliko] 后台服务运行中 (PID: 39984)\n[Foliko] 后台服务运行中 (PID: 39984)\n[2026-05-02T10:02:15.875Z] \u001b[2m[foliko]\u001b[0m \u001b[2m10:02:15.875\u001b[0m \u001b[32mINFO \u001b[0m \u001b[90mBEFORE: messages=18, tokens=4640/400000\u001b[0m\n\u001b[2m[foliko]\u001b[0m \u001b[2m10:02:15.875\u001b[0m \u001b[32mINFO \u001b[0m \u001b[90mBEFORE: messages=18, tokens=4640/400000\u001b[0m\n",
|
|
384
|
+
"metadata": {
|
|
385
|
+
"filePath": "D:\\Code\\foliko\\.foliko.log",
|
|
386
|
+
"size": 10662,
|
|
387
|
+
"lines": null
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
"type": "tool-result",
|
|
394
|
+
"toolCallId": "call_function_kaivhxs5722t_2",
|
|
395
|
+
"toolName": "read",
|
|
396
|
+
"output": {
|
|
397
|
+
"type": "json",
|
|
398
|
+
"value": {
|
|
399
|
+
"success": true,
|
|
400
|
+
"data": "/**\n * AgentChatHandler 聊天处理器\n * 使用 AI SDK 的 ToolLoopAgent 处理工具调用循环\n *\n * 职责委托:\n * - ChatSession: 会话管理、消息历史、队列\n * - ToolExecutor: 工具发现、执行、验证\n * - ContextCompressor: 上下文压缩\n */\n\nconst { EventEmitter } = require('../utils/event-emitter');\nconst { logger } = require('../utils/logger');\nconst {\n tool: aiTool,\n ToolLoopAgent,\n isLoopFinished,\n} = require('ai');\nconst { autoSplitToolResult } = require('../../plugins/data-splitter-plugin');\nconst { cleanResponse } = require('../utils');\nconst { ChatQueueManager } = require('../utils/chat-queue');\nconst { TokenCounter } = require('./token-counter');\nconst { isThinkingModel } = require('./provider');\n// 新模块\nconst { ChatSession } = require('./chat-session');\nconst { ToolExecutor } = require('./tool-executor');\nconst { ContextCompressor } = require('./context-compressor');\nconst {\n DEFAULT_MAX_OUTPUT_TOKENS,\n DEFAULT_TEMPERATURE,\n DEFAULT_MAX_STEPS,\n DEFAULT_MAX_CONCURRENT,\n DEFAULT_RETRY_ATTEMPTS,\n DEFAULT_RETRY_DELAY_MS,\n} = require('./constants');\n\nclass AgentChatHandler extends EventEmitter {\n /**\n * @param {Agent} agent - Agent 实例\n * @param {Object} config - 配置\n */\n constructor(agent, config = {}) {\n super();\n\n this.agent = agent;\n this.config = config;\n\n // AI 配置\n this.model = config.model || 'deepseek-chat';\n this.provider = config.provider || 'deepseek';\n this.apiKey = config.apiKey;\n this.baseURL = config.baseURL;\n this.providerOptions = config.providerOptions || {};\n this.providerOptions.maxOutputTokens = config.providerOptions?.maxOutputTokens || 8192;\n\n // DeepSeek thinking mode: 支持通过 config.thinkingMode 控制开关\n // config.thinkingMode = true 可启用,默认为禁用\n const enableThinking = config.thinkingMode === true && isThinkingModel(this.model);\n this._thinkingMode = enableThinking;\n\n if (enableThinking) {\n // thinking mode 模型不支持 temperature 参数\n this.providerOptions.temperature = undefined;\n // DeepSeek 需要通过 extra_body 启用 thinking mode\n if (!this.providerOptions.deepseek) {\n this.providerOptions.deepseek = {};\n }\n this.providerOptions.deepseek.thinking = { type: 'enabled' };\n } else {\n this.providerOptions.temperature = config.providerOptions?.temperature || 0.3;\n }\n\n this._systemPrompt = config.systemPrompt || 'You are a helpful assistant.';\n this._maxSteps = config.maxSteps || 20;\n\n // 委托给新模块\n // ChatSession: 会话、消息、队列\n this._chatSession = new ChatSession({\n agent,\n maxConcurrent: config.maxConcurrent || 1,\n retryAttempts: config.retryAttempts || 3,\n retryDelay: config.retryDelay || 2000,\n });\n\n // ToolExecutor: 工具管理\n this._toolExecutor = new ToolExecutor({\n agent,\n framework: agent?.framework,\n });\n\n // ContextCompressor: 压缩\n this._contextCompressor = new ContextCompressor({\n agent,\n framework: agent?.framework,\n model: this.model,\n maxContextTokens: config.maxContextTokens,\n keepRecentMessages: config.keepRecentMessages || 20,\n enableSmartCompress: config.enableSmartCompress !== false,\n });\n\n // 上下文限制",
|
|
401
|
+
"metadata": {
|
|
402
|
+
"filePath": "D:\\Code\\foliko\\src\\core\\agent-chat.js",
|
|
403
|
+
"size": 32568,
|
|
404
|
+
"lines": null
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
]
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
"role": "assistant",
|
|
413
|
+
"content": [
|
|
414
|
+
{
|
|
415
|
+
"type": "text",
|
|
416
|
+
"text": "<think>用户说回复有时候会突然中断。让我分析可能的原因:\n\n从代码来看,我需要检查:\n1. AI SDK 的错误处理\n2. 流式输出的处理\n3. 超时设置\n4. 工具执行错误处理\n5. 会话保存机制\n\n让我看看更多相关代码,特别是 stream 处理和错误处理部分。\n</think>\n\n\n"
|
|
417
|
+
},
|
|
418
|
+
{
|
|
419
|
+
"type": "tool-call",
|
|
420
|
+
"toolCallId": "call_function_73lmjpev3k1e_1",
|
|
421
|
+
"toolName": "read",
|
|
422
|
+
"input": {
|
|
423
|
+
"path": "src/core/chat-session.js",
|
|
424
|
+
"lines": 150
|
|
425
|
+
}
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
"type": "tool-call",
|
|
429
|
+
"toolCallId": "call_function_73lmjpev3k1e_2",
|
|
430
|
+
"toolName": "read",
|
|
431
|
+
"input": {
|
|
432
|
+
"path": "src/core/constants.js"
|
|
433
|
+
}
|
|
434
|
+
},
|
|
435
|
+
{
|
|
436
|
+
"type": "tool-call",
|
|
437
|
+
"toolCallId": "call_function_73lmjpev3k1e_3",
|
|
438
|
+
"toolName": "read",
|
|
439
|
+
"input": {
|
|
440
|
+
"path": "src/utils/chat-queue.js",
|
|
441
|
+
"lines": 100
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
]
|
|
445
|
+
},
|
|
446
|
+
{
|
|
447
|
+
"role": "tool",
|
|
448
|
+
"content": [
|
|
449
|
+
{
|
|
450
|
+
"type": "tool-result",
|
|
451
|
+
"toolCallId": "call_function_73lmjpev3k1e_1",
|
|
452
|
+
"toolName": "read",
|
|
453
|
+
"output": {
|
|
454
|
+
"type": "json",
|
|
455
|
+
"value": {
|
|
456
|
+
"success": true,
|
|
457
|
+
"data": "/**\n * ChatSession - 会话管理器\n *\n * 职责:\n * 1. 管理 Per-Session 的消息存储\n * 2. 会话消息历史加载/保存\n * 3. 会话消息队列管理\n * 4. SessionScope 事件作用域\n */\n\nconst { EventEmitter } = require('../utils/event-emitter');\nconst { logger } = require('../utils/logger');\n\n/**\n * Session 作用域的事件监听器\n * 自动过滤只属于特定 sessionId 的事件\n */\nclass SessionScope extends EventEmitter {\n /**\n * @param {ChatSession} session - ChatSession 实例\n * @param {string} sessionId - Session ID\n */\n constructor(session, sessionId) {\n super();\n this.session = session;\n this.sessionId = sessionId;\n this._handlers = new Set();\n }\n\n /**\n * 注册事件监听器(自动过滤 sessionId)\n * @param {string} event - 事件名\n * @param {Function} handler - 处理函数\n * @returns {this}\n */\n on(event, handler) {\n const wrapped = (data) => {\n if (data && data.sessionId !== this.sessionId) return;\n handler(data);\n };\n this._handlers.add({ event, wrapped });\n this.session.on(event, wrapped);\n return this;\n }\n\n /**\n * 注册一次性事件监听器\n */\n once(event, handler) {\n const wrapped = (data) => {\n if (data && data.sessionId !== this.sessionId) return;\n handler(data);\n };\n this._handlers.add({ event, wrapped });\n this.session.once(event, wrapped);\n return this;\n }\n\n /**\n * 移除事件监听器\n */\n off(event, handler) {\n const toRemove = [];\n for (const item of this._handlers) {\n if (item.event === event && item.handler === handler) {\n this.session.off(event, item.wrapped);\n toRemove.push(item);\n }\n }\n for (const item of toRemove) {\n this._handlers.delete(item);\n }\n return this;\n }\n\n /**\n * 移除所有这个 scope 注册的监听器\n */\n removeAllListeners() {\n for (const { event, wrapped } of this._handlers) {\n this.session.off(event, wrapped);\n }\n this._handlers.clear();\n return this;\n }\n\n /**\n * 获取当前 sessionId\n */\n getSessionId() {\n return this.sessionId;\n }\n}\n\nclass ChatSession extends EventEmitter {\n /**\n * @param {Object} config - 配置\n * @param {Function} [config.messageProcessor] - 消息处理器函数 (sessionId, message, options) => Promise\n */\n constructor(config = {}) {\n super();\n\n this.config = config;\n this.agent = config.agent;\n\n // 消息处理器\n this._messageProcessor = config.messageProcessor;\n\n // 消息存储限制,防止内存溢出\n this._maxMessagesPerSession = config.maxMessagesPerSession || 1000;\n\n // Session 消息存储 Map: sessionId -> { messages: [], historyLoaded: false, compressionState: {} }\n this._sessionMessageStores = new Map();\n\n // Session 事件作用域 Map: sessionId -> Set<{event, handler}>\n this._sessionScopes = new Map();\n }\n\n /**\n * 设置消息处理器\n * @param {Function} processor - (sessionId, message, options) => Promise\n */\n setMessageProcessor(processor) {\n this._messageProcessor = processor;\n }\n\n\n\n /**\n * 获取或创建 SessionScope\n * @param {string} sessionId - Session ID\n * @returns {SessionScope}\n */\n createSessionScope(sessionId) {\n const scope = new SessionScope(this, sessionId);\n\n // 转发 ChatSession 的事件到 SessionScope(自动过滤 sessionId)\n const events = [\n 'queue:added',\n 'queue:processing',\n 'queue:completed',\n 'queue:failed',\n 'queue:retry',\n 'queue:empty',\n 'queue:cleared',\n 'stream:chunk',\n 'message:start',\n 'message:complete',\n 'message:error',\n ];",
|
|
458
|
+
"metadata": {
|
|
459
|
+
"filePath": "D:\\Code\\foliko\\src\\core\\chat-session.js",
|
|
460
|
+
"size": 9512,
|
|
461
|
+
"lines": null
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
"type": "tool-result",
|
|
468
|
+
"toolCallId": "call_function_73lmjpev3k1e_2",
|
|
469
|
+
"toolName": "read",
|
|
470
|
+
"output": {
|
|
471
|
+
"type": "json",
|
|
472
|
+
"value": {
|
|
473
|
+
"success": true,
|
|
474
|
+
"data": "/**\n * Foliko Framework 统一常量\n *\n * 集中管理所有 magic numbers / strings,消除散落各处的重复值。\n * 所有模块应引用此文件而非硬编码。\n */\n\nconst path = require('path');\n\n// ============================================================================\n// AI 模型与提供者\n// ============================================================================\n\n/** 默认 AI 提供者 */\nconst DEFAULT_PROVIDER = 'deepseek';\n\n/** 默认 AI 模型 */\nconst DEFAULT_MODEL = 'deepseek-chat';\n\n/** 默认 Max Output Tokens */\nconst DEFAULT_MAX_OUTPUT_TOKENS = 8192;\n\n/** 默认 Temperature */\nconst DEFAULT_TEMPERATURE = 0.3;\n\n/** 默认 Max Steps(工具调用最大轮数) */\nconst DEFAULT_MAX_STEPS = 20;\n\n/** 需要禁用 temperature 的 thinking mode 模型列表 */\nconst THINKING_MODELS = [\n 'deepseek-v4-pro',\n 'deepseek-v4-flash',\n 'deepseek-reasoner',\n];\n\n// ============================================================================\n// Session 上下文\n// ============================================================================\n\n/** Session TTL:默认 30 分钟 */\nconst DEFAULT_SESSION_TTL_MS = 30 * 60 * 1000;\n\n/** Session 清理检查间隔 */\nconst DEFAULT_SESSION_CLEANUP_INTERVAL_MS = 60 * 1000;\n\n/** 每个 Session 最大消息数 */\nconst DEFAULT_MAX_MESSAGES_PER_SESSION = 1000;\n\n// ============================================================================\n// 上下文压缩\n// ============================================================================\n\n/** 保留最近的 N 条消息 */\nconst KEEP_RECENT_MESSAGES = 20;\n\n/** 智能压缩启用 */\nconst ENABLE_SMART_COMPRESS = true;\n\n/** 压缩超时(毫秒) */\nconst COMPRESSION_TIMEOUT_MS = 1_200_000;\n\n/** 工具结果最大字符数 */\nconst MAX_TOOL_RESULT_SIZE = 4000;\n\n// ============================================================================\n// 消息队列\n// ============================================================================\n\n/** 默认最大并发数 */\nconst DEFAULT_MAX_CONCURRENT = 1;\n\n/** 默认重试次数 */\nconst DEFAULT_RETRY_ATTEMPTS = 3;\n\n/** 默认重试延迟(毫秒) */\nconst DEFAULT_RETRY_DELAY_MS = 2000;\n\n// ============================================================================\n// System Prompt 优先级\n// ============================================================================\n\nconst PROMPT_PRIORITY = {\n DATETIME: 50,\n ORIGINAL_PROMPT: 100,\n SHARED_PROMPT: 200,\n METADATA: 300,\n TOOLS: 400,\n SKILLS: 500,\n SUB_AGENTS: 600,\n CAPABILITIES: 700,\n MCP_TOOLS: 750,\n EXTENSION_TOOLS: 800,\n TOOL_CORE_RULES: 95,\n};\n\n// ============================================================================\n// 熔断器\n// ============================================================================\n\n/** 熔断器默认失败阈值 */\nconst DEFAULT_CIRCUIT_FAILURE_THRESHOLD = 3;\n\n/** 熔断器默认成功阈值 */\nconst DEFAULT_CIRCUIT_SUCCESS_THRESHOLD = 2;\n\n/** 熔断器默认超时(毫秒) */\nconst DEFAULT_CIRCUIT_TIMEOUT_MS = 60_000;\n\n// ============================================================================\n// 插件\n// ============================================================================\n\n/** 默认插件优先级 */\nconst DEFAULT_PLUGIN_PRIORITY = 100;\n\n/** 系统插件名称集合 */\nconst SYSTEM_PLUGINS = new Set([\n 'ai', 'defaults', 'tools', 'skill-manager', 'session',\n 'storage', 'audit', 'rules', 'file-system',\n]);\n\n/** Agent 配置目录名 */\nconst AGENT_DIR = '.agent';\n\n// ============================================================================\n// Session 存储\n// ============================================================================\n\n/** 默认 Session 存储类型 */\nconst DEFAULT_SESSION_STORAGE_TYPE = 'file';\n\n/** 默认 Session 存储目录 */\nconst DEFAULT_SESSION_STORAGE_DIR = path.join(AGENT_DIR, 'sessions');\n\n// ============================================================================\n// Token 计数(估算系数)\n// ============================================================================\n\n/** 每字符对应的 token 数(中文系数更大) */\nconst TOKENS_PER_CHAR = 0.25;\n\n/** 每工具对应的 token 数 */\nconst TOKENS_PER_TOOL = 500;\n\n/** 每消息固定开销 token */\nconst TOKENS_PER_MESSAGE_OVERHEAD = 4;\n\n// ============================================================================\n// 导出\n// ============================================================================\n\nmodule.exports = {\n // AI\n DEFAULT_PROVIDER,\n DEFAULT_MODEL,\n DEFAULT_MAX_OUTPUT_TOKENS,\n DEFAULT_TEMPERATURE,\n DEFAULT_MAX_STEPS,\n THINKING_MODELS,\n\n // Session\n DEFAULT_SESSION_TTL_MS,\n DEFAULT_SESSION_CLEANUP_INTERVAL_MS,\n DEFAULT_MAX_MESSAGES_PER_SESSION,\n\n // Compression\n KEEP_RECENT_MESSAGES,\n ENABLE_SMART_COMPRESS,\n COMPRESSION_TIMEOUT_MS,\n MAX_TOOL_RESULT_SIZE,\n\n // Queue\n DEFAULT_MAX_CONCURRENT,\n DEFAULT_RETRY_ATTEMPTS,\n DEFAULT_RETRY_DELAY_MS,\n\n // Priority\n PROMPT_PRIORITY,\n\n // Circuit Breaker\n DEFAULT_CIRCUIT_FAILURE_THRESHOLD,\n DEFAULT_CIRCUIT_SUCCESS_THRESHOLD,\n DEFAULT_CIRCUIT_TIMEOUT_MS,\n\n // Plugin\n DEFAULT_PLUGIN_PRIORITY,\n SYSTEM_PLUGINS,\n AGENT_DIR,\n\n // Storage\n DEFAULT_SESSION_STORAGE_TYPE,\n DEFAULT_SESSION_STORAGE_DIR,\n\n // Token\n TOKENS_PER_CHAR,\n TOKENS_PER_TOOL,\n TOKENS_PER_MESSAGE_OVERHEAD,\n};\n",
|
|
475
|
+
"metadata": {
|
|
476
|
+
"filePath": "D:\\Code\\foliko\\src\\core\\constants.js",
|
|
477
|
+
"size": 5338,
|
|
478
|
+
"lines": 199
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
},
|
|
483
|
+
{
|
|
484
|
+
"type": "tool-result",
|
|
485
|
+
"toolCallId": "call_function_73lmjpev3k1e_3",
|
|
486
|
+
"toolName": "read",
|
|
487
|
+
"output": {
|
|
488
|
+
"type": "json",
|
|
489
|
+
"value": {
|
|
490
|
+
"success": true,
|
|
491
|
+
"data": "const { EventEmitter } = require('./event-emitter');\nconst { cleanResponse } = require('./index');\nconst { logger } = require('./logger');\nconst { isNetworkError, calculateDelay, PRESETS } = require('./retry');\nconst log = logger.child('ChatQueue');\n// ChatQueueManager.js\nclass ChatQueueManager extends EventEmitter {\n constructor(options = {}) {\n super();\n this.queue = [];\n this.isProcessing = false;\n this.maxConcurrent = options.maxConcurrent || 1;\n this.activeCount = 0;\n this.retryAttempts = options.retryAttempts || 3;\n this.retryDelay = options.retryDelay || 2000;\n }\n\n /**\n * 添加请求到队列\n */\n enqueue(requestId, sessionId, message, options = {}) {\n return new Promise((resolve, reject) => {\n const queueItem = {\n id: requestId,\n sessionId,\n message,\n options,\n resolve,\n reject,\n timestamp: Date.now(),\n status: 'pending',\n retries: 0,\n priority: options.priority || 0,\n executeFunction: options.executeFunction || null,\n };\n\n this.queue.push(queueItem);\n // 按优先级排序\n this.queue.sort((a, b) => b.priority - a.priority);\n\n this.emit('queue:added', {\n requestId,\n sessionId,\n queueLength: this.queue.length,\n position: this.queue.length,\n });\n\n this.processQueue();\n });\n }\n\n /**\n * 处理队列\n */\n async processQueue() {\n if (this.activeCount >= this.maxConcurrent || this.queue.length === 0) {\n if (this.queue.length === 0 && this.activeCount === 0) {\n this.emit('queue:empty', { timestamp: Date.now() });\n }\n return;\n }\n\n const item = this.queue.shift();\n this.activeCount++;\n item.status = 'processing';\n\n this.emit('queue:processing', {\n requestId: item.id,\n sessionId: item.sessionId,\n activeCount: this.activeCount,\n remaining: this.queue.length,\n });\n\n try {\n const result = await this.executeWithRetry(item);\n // 检查 result 是否有错误(而不是通过 try/catch)\n if (result.error) {\n //console.log('[ChatQueue] Rejecting with error from result:', result.error.message);\n throw result.error;\n } else {\n item.resolve(result);\n this.emit('queue:completed', {\n requestId: item.id,\n sessionId: item.sessionId,\n duration: Date.now() - item.timestamp,\n });\n }\n } catch (error) {\n log.warn('[ChatQueue] Rejecting Error:', error.message);\n item.reject(error);\n this.emit('queue:failed', {\n requestId: item.id,\n sessionId: item.sessionId,\n error: error.message,\n });\n } finally {\n this.activeCount--;\n this.processQueue();\n }\n }",
|
|
492
|
+
"metadata": {
|
|
493
|
+
"filePath": "D:\\Code\\foliko\\src\\utils\\chat-queue.js",
|
|
494
|
+
"size": 8104,
|
|
495
|
+
"lines": null
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
]
|
|
501
|
+
},
|
|
502
|
+
{
|
|
503
|
+
"role": "assistant",
|
|
504
|
+
"content": [
|
|
505
|
+
{
|
|
506
|
+
"type": "text",
|
|
507
|
+
"text": "<think>我已经查看了项目的主要代码。让我再看一下 provider.js 和 agent.js 了解 AI 调用的完整流程,以及看看有没有明显的断连问题。\n\n用户说\"回复一半就突然断了\",这可能是:\n1. 流式输出中断\n2. 连接超时\n3. AI API 限制\n4. 工具执行出错\n5. 上下文压缩问题\n\n让我再看一下 provider.js 和 agent.js 了解更完整的流程。\n</think>\n\n\n"
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
"type": "tool-call",
|
|
511
|
+
"toolCallId": "call_function_gthq8y5vf1i7_1",
|
|
512
|
+
"toolName": "read",
|
|
513
|
+
"input": {
|
|
514
|
+
"path": "src/core/provider.js"
|
|
515
|
+
}
|
|
516
|
+
},
|
|
517
|
+
{
|
|
518
|
+
"type": "tool-call",
|
|
519
|
+
"toolCallId": "call_function_gthq8y5vf1i7_2",
|
|
520
|
+
"toolName": "read",
|
|
521
|
+
"input": {
|
|
522
|
+
"path": "src/core/agent.js",
|
|
523
|
+
"lines": 150
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
]
|
|
527
|
+
},
|
|
528
|
+
{
|
|
529
|
+
"role": "tool",
|
|
530
|
+
"content": [
|
|
531
|
+
{
|
|
532
|
+
"type": "tool-result",
|
|
533
|
+
"toolCallId": "call_function_gthq8y5vf1i7_1",
|
|
534
|
+
"toolName": "read",
|
|
535
|
+
"output": {
|
|
536
|
+
"type": "json",
|
|
537
|
+
"value": {
|
|
538
|
+
"success": true,
|
|
539
|
+
"data": "/**\n * AI Provider 工厂\n * 支持多种 AI 提供商\n */\n\nconst { createOpenAICompatible } = require('@ai-sdk/openai-compatible');\nconst { createOpenAI } = require('@ai-sdk/openai');\nconst { createAnthropic } = require('@ai-sdk/anthropic');\n\n/**\n * 默认提供商配置\n */\nconst DEFAULT_PROVIDERS = {\n openai: {\n name: 'OpenAI',\n baseURL: 'https://api.openai.com/v1',\n defaultModel: 'gpt-4o',\n },\n ollama: {\n name: 'Ollama',\n baseURL: 'http://localhost:11434/v1',\n defaultModel: 'llama3',\n },\n lmstudio: {\n name: 'LM Studio',\n baseURL: 'http://localhost:1234/v1',\n defaultModel: 'local-model',\n },\n deepseek: {\n name: 'DeepSeek',\n baseURL: 'https://api.deepseek.com/v1',\n defaultModel: 'deepseek-chat',\n },\n anthropic: {\n name: 'Anthropic',\n baseURL: 'https://api.anthropic.com/v1',\n defaultModel: 'claude-sonnet-4-20250514',\n },\n minimax: {\n name: 'MiniMax',\n baseURL: 'https://api.minimaxi.com/v1',\n defaultModel: 'MiniMax-M2.7',\n },\n};\n\n/**\n * 需要禁用 temperature 的模型(thinking mode 模型)\n */\nconst THINKING_MODELS = [\n 'deepseek-v4-pro',\n 'deepseek-v4-flash',\n 'deepseek-reasoner',\n];\n\n/**\n * 检查是否是 thinking mode 模型\n * @param {string} model - 模型名称\n * @returns {boolean}\n */\nfunction isThinkingModel(model) {\n if (!model) return false;\n const lowerModel = model.toLowerCase();\n return THINKING_MODELS.some(\n (tm) => lowerModel.includes(tm) || lowerModel === tm\n );\n}\n\n/**\n * 创建 AI 客户端\n * @param {Object} config - 配置\n * @param {string} config.provider - 提供商名称\n * @param {string} config.model - 模型名称\n * @param {string} config.apiKey - API 密钥\n * @param {string} [config.baseURL] - 自定义 API 地址\n */\nfunction createAI(config) {\n const { provider, model, apiKey, baseURL } = config;\n const providerName = (provider || 'deepseek').toLowerCase();\n const isThinking = isThinkingModel(model);\n\n // 检查是否是预定义提供商\n if (DEFAULT_PROVIDERS[providerName]) {\n const providerConfig = DEFAULT_PROVIDERS[providerName];\n\n // 为 DeepSeek thinking mode 模型添加特殊配置\n const streamOptions = {\n includeUsage: true,\n };\n\n // DeepSeek 模型添加 reasoning_content 输出\n if (providerName === 'deepseek') {\n streamOptions.includeReasoningContent = true;\n }\n\n return createOpenAICompatible({\n name: providerConfig.name,\n baseURL: baseURL || providerConfig.baseURL,\n apiKey: apiKey || 'dummy-key',\n headers:\n provider === 'anthropic'\n ? {\n 'x-api-key': apiKey,\n 'anthropic-version': '2023-06-01',\n }\n : undefined,\n models: {\n default: {\n id: model || 'deepseek-chat',\n streamOptions,\n },\n },\n });\n }\n\n // 自定义提供商\n return createOpenAICompatible({\n name: providerName || 'Custom',\n baseURL: baseURL,\n apiKey: apiKey || 'dummy-key',\n models: {\n default: {\n id: model || 'gpt-4',\n },\n },\n });\n}\n\n/**\n * 获取可用提供商列表\n */\nfunction getAvailableProviders() {\n return Object.entries(DEFAULT_PROVIDERS).map(([key, value]) => ({\n id: key,\n name: value.name,\n baseURL: value.baseURL,\n }));\n}\n\n/**\n * 创建模型实例\n * @param {string} model - 模型名称\n * @param {Object} provider - Provider 实例\n */\nfunction createModel(model, provider) {\n return provider(model);\n}\n\nmodule.exports = {\n createAI,\n createModel,\n getAvailableProviders,\n DEFAULT_PROVIDERS,\n isThinkingModel,\n};\n",
|
|
540
|
+
"metadata": {
|
|
541
|
+
"filePath": "D:\\Code\\foliko\\src\\core\\provider.js",
|
|
542
|
+
"size": 3589,
|
|
543
|
+
"lines": 155
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
},
|
|
548
|
+
{
|
|
549
|
+
"type": "tool-result",
|
|
550
|
+
"toolCallId": "call_function_gthq8y5vf1i7_2",
|
|
551
|
+
"toolName": "read",
|
|
552
|
+
"output": {
|
|
553
|
+
"type": "json",
|
|
554
|
+
"value": {
|
|
555
|
+
"success": true,
|
|
556
|
+
"data": "/**\n * Agent 类\n * 负责对话和推理\n */\n\nconst { EventEmitter } = require('../utils/event-emitter');\nconst { AgentChatHandler } = require('./agent-chat');\nconst { SystemPromptBuilder } = require('./system-prompt-builder');\nconst { NotificationManager } = require('./notification-manager');\nconst { Logger, LOG_LEVELS } = require('../utils/logger');\nconst os = require('os');\nconst {\n PROMPT_PRIORITY,\n DEFAULT_MAX_OUTPUT_TOKENS,\n DEFAULT_TEMPERATURE,\n} = require('./constants');\n\n/**\n * Agent 配置常量\n */\nconst AGENT_CONFIG = {\n MAX_OUTPUT_TOKENS: DEFAULT_MAX_OUTPUT_TOKENS,\n TEMPERATURE: DEFAULT_TEMPERATURE,\n MAX_NOTIFICATIONS: 5,\n CAPABILITY_MAX_LENGTH: 50,\n CAPABILITY_MAX_PARTS: 3,\n MAX_RETRIES: 3,\n};\n\n// 模块级 logger\nconst _agentLogger = new Logger({ namespace: 'agent', level: LOG_LEVELS.INFO });\nconst logger = {\n info: (ctx, msg, data) => _agentLogger.info(`[${ctx}] ${msg}`, data !== undefined ? data : ''),\n warn: (ctx, msg, data) => _agentLogger.warn(`[${ctx}] ${msg}`, data !== undefined ? data : ''),\n error: (ctx, msg, data) => _agentLogger.error(`[${ctx}] ${msg}`, data !== undefined ? data : ''),\n debug: (ctx, msg, data) => _agentLogger.debug(`[${ctx}] ${msg}`, data !== undefined ? data : ''),\n};\n\nclass Agent extends EventEmitter {\n /**\n * @param {Framework} framework - 框架实例\n * @param {Object} config - 配置\n * @param {string} [config.name] - Agent 名称\n * @param {string} [config.systemPrompt] - 系统提示\n * @param {string} [config.sharedPrompt] - 共享提示模板,支持 {{VAR}} 占位符\n * @param {Object} [config.metadata] - 元数据,注入到 sharedPrompt\n * @param {boolean} [config.enableToolRouting=true] - 是否启用工具路由\n */\n constructor(framework, config = {}) {\n super();\n\n this.framework = framework;\n this.config = config;\n\n this.name = config.name || 'Agent';\n this.model = config.model || 'deepseek-chat';\n this.apiKey = config.apiKey;\n this.baseURL = config.baseURL;\n this.provider = config.provider || 'deepseek';\n this.providerOptions = {\n ...config.providerOptions,\n maxOutputTokens: config.providerOptions?.maxOutputTokens ?? DEFAULT_MAX_OUTPUT_TOKENS,\n temperature: config.providerOptions?.temperature ?? DEFAULT_TEMPERATURE,\n };\n // 原始 system prompt\n this._originalPrompt =\n config.systemPrompt ||\n '你是一个智能助手。当用户提出问题或任务时,你会主动分析需求,选择合适的工具来获取信息或执行操作。你善于将复杂任务拆解为多个步骤,通过工具协作完成。';\n\n // 共享提示模板\n this._sharedPrompt = config.sharedPrompt || '';\n\n // 元数据\n const metadata = config.metadata || {};\n this._metadata = new Map(Object.entries(metadata));\n\n this._chatHandler = null;\n this._tools = new Map();\n this._skipSyncTools = config.skipSyncTools || false;\n this._status = 'idle';\n\n // 子Agent管理\n this._subAgents = new Map();\n\n // System prompt 构建器\n this._systemPromptBuilder = new SystemPromptBuilder();\n this._registerDefaultPromptParts();\n\n // 处理后的 system prompt (带上下文)\n this.systemPrompt = this._buildSystemPrompt();\n\n // 通知管理器\n this._notificationManager = new NotificationManager(framework);\n\n // 初始化聊天处理器\n this._initChatHandler();\n\n // logger.info('Agent', `Agent \"${this.name}\" 初始化完成`);\n }\n\n /**\n * 注册默认的系统提示词部分\n * @private\n */\n _registerDefaultPromptParts() {\n // 1. 原始 system prompt (Agent 内部)\n this._systemPromptBuilder.register(\n 'original-prompt',\n PROMPT_PRIORITY.ORIGINAL_PROMPT,\n () => this._originalPrompt\n );\n\n // 2. 共享提示模板 (Agent 内部)\n this._systemPromptBuilder.register('shared-prompt', PROMPT_PRIORITY.SHARED_PROMPT, () => {\n if (this._sharedPrompt) {\n return this._replacePlaceholders(this._sharedPrompt);\n }\n return null;\n });\n\n // 3. 日期时间(框架基础设施)\n this._systemPromptBuilder.register('datetime', PROMPT_PRIORITY.DATETIME, () => {\n const now = new Date();\n const timeStr = now.toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' });\n const dateStr = now.toLocaleDateString('zh-CN', {\n timeZone: 'Asia/Shanghai',\n weekday: 'long',\n });\n return `【当前时间】${timeStr}(${dateStr})`;\n });\n\n // 4. 元数据 (Agent 内部)\n this._systemPromptBuilder.register('metadata', PROMPT_PRIORITY.METADATA, () => {\n if (this._metadata.size === 0) {\n return null;\n }\n const metaParts = ['【元数据】'];\n for (const [key, value] of this._metadata) {\n if (typeof value === 'object') {\n metaParts.push(`- ${key}: ${JSON.stringify(value)}`);\n } else {\n metaParts.push(`- ${key}: ${value}`);\n }\n }\n return metaParts.join('\\n');\n });\n\n // 5. 技能列表(已迁移到 skill-manager,通过 registerPromptPart 注册)\n\n // 6. 子Agent列表(已迁移到 subagent-plugin,通过 registerPromptPart 注册)",
|
|
557
|
+
"metadata": {
|
|
558
|
+
"filePath": "D:\\Code\\foliko\\src\\core\\agent.js",
|
|
559
|
+
"size": 20535,
|
|
560
|
+
"lines": null
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
]
|
|
566
|
+
},
|
|
567
|
+
{
|
|
568
|
+
"role": "assistant",
|
|
569
|
+
"content": [
|
|
570
|
+
{
|
|
571
|
+
"type": "text",
|
|
572
|
+
"text": "<think>我现在已经对项目有了比较全面的了解。让我分析用户说\"回复一半就突然断了\"的可能原因:\n\n1. **流式输出中断** - 在 `chatStream` 方法中,如果发生错误可能被捕获但没有正确传递\n2. **队列机制问题** - `ChatQueueManager` 处理请求时出错\n3. **工具调用超时** - 某些工具执行时间过长导致超时\n4. **上下文压缩触发** - 上下文过大时自动压缩可能中断对话\n5. **AI 服务不稳定** - API 返回错误或连接断开\n6. **Token 限制** - 输出超过 maxOutputTokens (8192)\n\n让我看看 `chatStream` 的完整实现和错误处理逻辑。\n</think>\n\n\n"
|
|
573
|
+
},
|
|
574
|
+
{
|
|
575
|
+
"type": "tool-call",
|
|
576
|
+
"toolCallId": "call_function_ldajxtdqq4o1_1",
|
|
577
|
+
"toolName": "read",
|
|
578
|
+
"input": {
|
|
579
|
+
"path": "src/core/agent-chat.js",
|
|
580
|
+
"lines": 200
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
]
|
|
584
|
+
},
|
|
585
|
+
{
|
|
586
|
+
"role": "tool",
|
|
587
|
+
"content": [
|
|
588
|
+
{
|
|
589
|
+
"type": "tool-result",
|
|
590
|
+
"toolCallId": "call_function_ldajxtdqq4o1_1",
|
|
591
|
+
"toolName": "read",
|
|
592
|
+
"output": {
|
|
593
|
+
"type": "json",
|
|
594
|
+
"value": {
|
|
595
|
+
"success": true,
|
|
596
|
+
"data": "/**\n * AgentChatHandler 聊天处理器\n * 使用 AI SDK 的 ToolLoopAgent 处理工具调用循环\n *\n * 职责委托:\n * - ChatSession: 会话管理、消息历史、队列\n * - ToolExecutor: 工具发现、执行、验证\n * - ContextCompressor: 上下文压缩\n */\n\nconst { EventEmitter } = require('../utils/event-emitter');\nconst { logger } = require('../utils/logger');\nconst {\n tool: aiTool,\n ToolLoopAgent,\n isLoopFinished,\n} = require('ai');\nconst { autoSplitToolResult } = require('../../plugins/data-splitter-plugin');\nconst { cleanResponse } = require('../utils');\nconst { ChatQueueManager } = require('../utils/chat-queue');\nconst { TokenCounter } = require('./token-counter');\nconst { isThinkingModel } = require('./provider');\n// 新模块\nconst { ChatSession } = require('./chat-session');\nconst { ToolExecutor } = require('./tool-executor');\nconst { ContextCompressor } = require('./context-compressor');\nconst {\n DEFAULT_MAX_OUTPUT_TOKENS,\n DEFAULT_TEMPERATURE,\n DEFAULT_MAX_STEPS,\n DEFAULT_MAX_CONCURRENT,\n DEFAULT_RETRY_ATTEMPTS,\n DEFAULT_RETRY_DELAY_MS,\n} = require('./constants');\n\nclass AgentChatHandler extends EventEmitter {\n /**\n * @param {Agent} agent - Agent 实例\n * @param {Object} config - 配置\n */\n constructor(agent, config = {}) {\n super();\n\n this.agent = agent;\n this.config = config;\n\n // AI 配置\n this.model = config.model || 'deepseek-chat';\n this.provider = config.provider || 'deepseek';\n this.apiKey = config.apiKey;\n this.baseURL = config.baseURL;\n this.providerOptions = config.providerOptions || {};\n this.providerOptions.maxOutputTokens = config.providerOptions?.maxOutputTokens || 8192;\n\n // DeepSeek thinking mode: 支持通过 config.thinkingMode 控制开关\n // config.thinkingMode = true 可启用,默认为禁用\n const enableThinking = config.thinkingMode === true && isThinkingModel(this.model);\n this._thinkingMode = enableThinking;\n\n if (enableThinking) {\n // thinking mode 模型不支持 temperature 参数\n this.providerOptions.temperature = undefined;\n // DeepSeek 需要通过 extra_body 启用 thinking mode\n if (!this.providerOptions.deepseek) {\n this.providerOptions.deepseek = {};\n }\n this.providerOptions.deepseek.thinking = { type: 'enabled' };\n } else {\n this.providerOptions.temperature = config.providerOptions?.temperature || 0.3;\n }\n\n this._systemPrompt = config.systemPrompt || 'You are a helpful assistant.';\n this._maxSteps = config.maxSteps || 20;\n\n // 委托给新模块\n // ChatSession: 会话、消息、队列\n this._chatSession = new ChatSession({\n agent,\n maxConcurrent: config.maxConcurrent || 1,\n retryAttempts: config.retryAttempts || 3,\n retryDelay: config.retryDelay || 2000,\n });\n\n // ToolExecutor: 工具管理\n this._toolExecutor = new ToolExecutor({\n agent,\n framework: agent?.framework,\n });\n\n // ContextCompressor: 压缩\n this._contextCompressor = new ContextCompressor({\n agent,\n framework: agent?.framework,\n model: this.model,\n maxContextTokens: config.maxContextTokens,\n keepRecentMessages: config.keepRecentMessages || 20,\n enableSmartCompress: config.enableSmartCompress !== false,\n });\n\n // 上下文限制\n this._maxContextTokens = this._contextCompressor._maxContextTokens;\n\n // Token 计算缓存(避免每次请求重复计算)\n this._toolsTokensCache = null;\n this._toolsTokensCacheVersion = 0;\n this._systemPromptTokensCache = null;\n this._systemPromptTokensCacheKey = null;\n\n // ChatQueueManager: 队列管理\n this.queueManager = new ChatQueueManager({\n maxConcurrent: config.maxConcurrent || DEFAULT_MAX_CONCURRENT,\n retryAttempts: config.retryAttempts || DEFAULT_RETRY_ATTEMPTS,\n retryDelay: config.retryDelay || DEFAULT_RETRY_DELAY_MS,\n });\n\n // AI client\n this._aiClient = null;\n this._aiProvider = null;\n\n // Session Memory prepareStep\n this._sessionMemoryPrepareStep = null;\n if (agent?.framework) {\n const memoryPlugin = agent.framework.pluginManager.get('memory');\n if (memoryPlugin?.instance?.getPrepareStep) {\n this._sessionMemoryPrepareStep = memoryPlugin.instance.getPrepareStep();\n logger.debug('Session memory prepareStep loaded');\n }\n }\n\n // 转发 ChatSession 事件\n this._setupEventForwarding();\n\n // 转发 queueManager 事件到 ChatSession(让 SessionScope 能收到 stream:chunk 和 queue:completed 等)\n this.queueManager.on('stream:chunk', (data) => {\n this._chatSession.emit('stream:chunk', data);\n });\n this.queueManager.on('queue:completed', (data) => {\n this._chatSession.emit('queue:completed', data);\n });\n this.queueManager.on('queue:failed', (data) => {\n this._chatSession.emit('queue:failed', data);\n });\n\n // 转发 message:complete 和 message:error 到 ChatSession(让 SessionScope 能收到)\n this.on('message:start', (data) => { \n this._chatSession.emit('message:start', data); \n }); \n this.on('message:complete', (data) => {\n this._chatSession.emit('message:complete', data);\n });\n this.on('message:error', (data) => {\n this._chatSession.emit('message:error', data);\n });\n\n // 设置消息处理器(使用 bind 保持 this 引用)\n this._chatSession.setMessageProcessor(this._processMessage.bind(this));\n }\n\n /**\n * 设置事件转发\n * @private\n */\n _setupEventForwarding() {\n const events = [\n 'queue:added',\n 'queue:processing',\n 'queue:completed',\n 'queue:failed',\n 'queue:retry',\n 'queue:empty',\n 'queue:cleared',\n 'queue:session-removed',\n 'stream:chunk',\n ];\n\n events.forEach((eventName) => {\n this._chatSession.on(eventName, (data) => {\n this.emit(eventName, data);\n });\n });\n\n // 转发工具事件\n this._toolExecutor.on('tool:call', (data) => this.emit('tool:call', data));\n this._toolExecutor.on('tool:result', (data) => this.emit('tool:result', data));\n this._toolExecutor.on('tool:error', (data) => this.emit('tool:error', data));\n }\n\n // ==================== 工具管理(委托给 ToolExecutor) ====================\n\n /** AI tools 格式缓存 (避免每次 chat 重建) */\n _aiToolsCache = null;\n _aiToolsCacheKey = ''; // 工具列表的哈希摘要\n\n registerTool(tool) {\n this._toolExecutor.registerTool(tool);\n // 工具列表变了,清除缓存\n this._toolsTokensCacheVersion = 0;\n this._aiToolsCache = null; // 清除 AI tools 缓存\n return this;\n }",
|
|
597
|
+
"metadata": {
|
|
598
|
+
"filePath": "D:\\Code\\foliko\\src\\core\\agent-chat.js",
|
|
599
|
+
"size": 32568,
|
|
600
|
+
"lines": null
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
]
|
|
606
|
+
},
|
|
607
|
+
{
|
|
608
|
+
"role": "assistant",
|
|
609
|
+
"content": [
|
|
610
|
+
{
|
|
611
|
+
"type": "text",
|
|
612
|
+
"text": "<think>让我继续分析项目。我需要查看更多关于对话流程的代码,特别是当回复中断时可能的原因。\n\n从日志来看,框架启动正常,但有一个 MCP 设计工具超时的问题。但用户说的是\"回复一半就突然断了\",这可能和以下因素有关:\n\n1. API 调用问题(如 token 限制、网络错误)\n2. AI SDK 的流式处理问题\n3. 内存或资源限制\n4. 上下文压缩问题\n\n让我再查看一些关键代码,特别是 chatStream 的实现和消息处理流程。\n</think>\n\n让我继续查看更多代码,特别是流式处理和错误处理部分:\n"
|
|
613
|
+
},
|
|
614
|
+
{
|
|
615
|
+
"type": "tool-call",
|
|
616
|
+
"toolCallId": "call_function_uj8io2gcqpe5_1",
|
|
617
|
+
"toolName": "read",
|
|
618
|
+
"input": {
|
|
619
|
+
"path": "src/core/agent-chat.js",
|
|
620
|
+
"lines": 200
|
|
621
|
+
}
|
|
622
|
+
},
|
|
623
|
+
{
|
|
624
|
+
"type": "tool-call",
|
|
625
|
+
"toolCallId": "call_function_uj8io2gcqpe5_2",
|
|
626
|
+
"toolName": "search_file",
|
|
627
|
+
"input": {
|
|
628
|
+
"pattern": "stream|yield",
|
|
629
|
+
"path": "src",
|
|
630
|
+
"caseSensitive": false
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
]
|
|
634
|
+
},
|
|
635
|
+
{
|
|
636
|
+
"role": "tool",
|
|
637
|
+
"content": [
|
|
638
|
+
{
|
|
639
|
+
"type": "tool-result",
|
|
640
|
+
"toolCallId": "call_function_uj8io2gcqpe5_1",
|
|
641
|
+
"toolName": "read",
|
|
642
|
+
"output": {
|
|
643
|
+
"type": "json",
|
|
644
|
+
"value": {
|
|
645
|
+
"success": true,
|
|
646
|
+
"data": "/**\n * AgentChatHandler 聊天处理器\n * 使用 AI SDK 的 ToolLoopAgent 处理工具调用循环\n *\n * 职责委托:\n * - ChatSession: 会话管理、消息历史、队列\n * - ToolExecutor: 工具发现、执行、验证\n * - ContextCompressor: 上下文压缩\n */\n\nconst { EventEmitter } = require('../utils/event-emitter');\nconst { logger } = require('../utils/logger');\nconst {\n tool: aiTool,\n ToolLoopAgent,\n isLoopFinished,\n} = require('ai');\nconst { autoSplitToolResult } = require('../../plugins/data-splitter-plugin');\nconst { cleanResponse } = require('../utils');\nconst { ChatQueueManager } = require('../utils/chat-queue');\nconst { TokenCounter } = require('./token-counter');\nconst { isThinkingModel } = require('./provider');\n// 新模块\nconst { ChatSession } = require('./chat-session');\nconst { ToolExecutor } = require('./tool-executor');\nconst { ContextCompressor } = require('./context-compressor');\nconst {\n DEFAULT_MAX_OUTPUT_TOKENS,\n DEFAULT_TEMPERATURE,\n DEFAULT_MAX_STEPS,\n DEFAULT_MAX_CONCURRENT,\n DEFAULT_RETRY_ATTEMPTS,\n DEFAULT_RETRY_DELAY_MS,\n} = require('./constants');\n\nclass AgentChatHandler extends EventEmitter {\n /**\n * @param {Agent} agent - Agent 实例\n * @param {Object} config - 配置\n */\n constructor(agent, config = {}) {\n super();\n\n this.agent = agent;\n this.config = config;\n\n // AI 配置\n this.model = config.model || 'deepseek-chat';\n this.provider = config.provider || 'deepseek';\n this.apiKey = config.apiKey;\n this.baseURL = config.baseURL;\n this.providerOptions = config.providerOptions || {};\n this.providerOptions.maxOutputTokens = config.providerOptions?.maxOutputTokens || 8192;\n\n // DeepSeek thinking mode: 支持通过 config.thinkingMode 控制开关\n // config.thinkingMode = true 可启用,默认为禁用\n const enableThinking = config.thinkingMode === true && isThinkingModel(this.model);\n this._thinkingMode = enableThinking;\n\n if (enableThinking) {\n // thinking mode 模型不支持 temperature 参数\n this.providerOptions.temperature = undefined;\n // DeepSeek 需要通过 extra_body 启用 thinking mode\n if (!this.providerOptions.deepseek) {\n this.providerOptions.deepseek = {};\n }\n this.providerOptions.deepseek.thinking = { type: 'enabled' };\n } else {\n this.providerOptions.temperature = config.providerOptions?.temperature || 0.3;\n }\n\n this._systemPrompt = config.systemPrompt || 'You are a helpful assistant.';\n this._maxSteps = config.maxSteps || 20;\n\n // 委托给新模块\n // ChatSession: 会话、消息、队列\n this._chatSession = new ChatSession({\n agent,\n maxConcurrent: config.maxConcurrent || 1,\n retryAttempts: config.retryAttempts || 3,\n retryDelay: config.retryDelay || 2000,\n });\n\n // ToolExecutor: 工具管理\n this._toolExecutor = new ToolExecutor({\n agent,\n framework: agent?.framework,\n });\n\n // ContextCompressor: 压缩\n this._contextCompressor = new ContextCompressor({\n agent,\n framework: agent?.framework,\n model: this.model,\n maxContextTokens: config.maxContextTokens,\n keepRecentMessages: config.keepRecentMessages || 20,\n enableSmartCompress: config.enableSmartCompress !== false,\n });\n\n // 上下文限制\n this._maxContextTokens = this._contextCompressor._maxContextTokens;\n\n // Token 计算缓存(避免每次请求重复计算)\n this._toolsTokensCache = null;\n this._toolsTokensCacheVersion = 0;\n this._systemPromptTokensCache = null;\n this._systemPromptTokensCacheKey = null;\n\n // ChatQueueManager: 队列管理\n this.queueManager = new ChatQueueManager({\n maxConcurrent: config.maxConcurrent || DEFAULT_MAX_CONCURRENT,\n retryAttempts: config.retryAttempts || DEFAULT_RETRY_ATTEMPTS,\n retryDelay: config.retryDelay || DEFAULT_RETRY_DELAY_MS,\n });\n\n // AI client\n this._aiClient = null;\n this._aiProvider = null;\n\n // Session Memory prepareStep\n this._sessionMemoryPrepareStep = null;\n if (agent?.framework) {\n const memoryPlugin = agent.framework.pluginManager.get('memory');\n if (memoryPlugin?.instance?.getPrepareStep) {\n this._sessionMemoryPrepareStep = memoryPlugin.instance.getPrepareStep();\n logger.debug('Session memory prepareStep loaded');\n }\n }\n\n // 转发 ChatSession 事件\n this._setupEventForwarding();\n\n // 转发 queueManager 事件到 ChatSession(让 SessionScope 能收到 stream:chunk 和 queue:completed 等)\n this.queueManager.on('stream:chunk', (data) => {\n this._chatSession.emit('stream:chunk', data);\n });\n this.queueManager.on('queue:completed', (data) => {\n this._chatSession.emit('queue:completed', data);\n });\n this.queueManager.on('queue:failed', (data) => {\n this._chatSession.emit('queue:failed', data);\n });\n\n // 转发 message:complete 和 message:error 到 ChatSession(让 SessionScope 能收到)\n this.on('message:start', (data) => { \n this._chatSession.emit('message:start', data); \n }); \n this.on('message:complete', (data) => {\n this._chatSession.emit('message:complete', data);\n });\n this.on('message:error', (data) => {\n this._chatSession.emit('message:error', data);\n });\n\n // 设置消息处理器(使用 bind 保持 this 引用)\n this._chatSession.setMessageProcessor(this._processMessage.bind(this));\n }\n\n /**\n * 设置事件转发\n * @private\n */\n _setupEventForwarding() {\n const events = [\n 'queue:added',\n 'queue:processing',\n 'queue:completed',\n 'queue:failed',\n 'queue:retry',\n 'queue:empty',\n 'queue:cleared',\n 'queue:session-removed',\n 'stream:chunk',\n ];\n\n events.forEach((eventName) => {\n this._chatSession.on(eventName, (data) => {\n this.emit(eventName, data);\n });\n });\n\n // 转发工具事件\n this._toolExecutor.on('tool:call', (data) => this.emit('tool:call', data));\n this._toolExecutor.on('tool:result', (data) => this.emit('tool:result', data));\n this._toolExecutor.on('tool:error', (data) => this.emit('tool:error', data));\n }\n\n // ==================== 工具管理(委托给 ToolExecutor) ====================\n\n /** AI tools 格式缓存 (避免每次 chat 重建) */\n _aiToolsCache = null;\n _aiToolsCacheKey = ''; // 工具列表的哈希摘要\n\n registerTool(tool) {\n this._toolExecutor.registerTool(tool);\n // 工具列表变了,清除缓存\n this._toolsTokensCacheVersion = 0;\n this._aiToolsCache = null; // 清除 AI tools 缓存\n return this;\n }",
|
|
647
|
+
"metadata": {
|
|
648
|
+
"filePath": "D:\\Code\\foliko\\src\\core\\agent-chat.js",
|
|
649
|
+
"size": 32568,
|
|
650
|
+
"lines": null
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
},
|
|
655
|
+
{
|
|
656
|
+
"type": "tool-result",
|
|
657
|
+
"toolCallId": "call_function_uj8io2gcqpe5_2",
|
|
658
|
+
"toolName": "search_file",
|
|
659
|
+
"output": {
|
|
660
|
+
"type": "json",
|
|
661
|
+
"value": {
|
|
662
|
+
"success": true,
|
|
663
|
+
"data": [],
|
|
664
|
+
"metadata": {
|
|
665
|
+
"pattern": "stream|yield",
|
|
666
|
+
"total": 0,
|
|
667
|
+
"stats": {
|
|
668
|
+
"filesWithMatches": 0,
|
|
669
|
+
"totalMatches": 0,
|
|
670
|
+
"searchPath": "src"
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
]
|
|
677
|
+
},
|
|
678
|
+
{
|
|
679
|
+
"role": "assistant",
|
|
680
|
+
"content": [
|
|
681
|
+
{
|
|
682
|
+
"type": "text",
|
|
683
|
+
"text": "<think>让我搜索 agent-chat.js 文件中 chatStream 函数的具体实现来查看消息回复中断的原因。\n</think>\n\n\n"
|
|
684
|
+
},
|
|
685
|
+
{
|
|
686
|
+
"type": "tool-call",
|
|
687
|
+
"toolCallId": "call_function_namlnou7lqgd_1",
|
|
688
|
+
"toolName": "search_file",
|
|
689
|
+
"input": {
|
|
690
|
+
"pattern": "chatStream",
|
|
691
|
+
"path": "src/core/agent-chat.js",
|
|
692
|
+
"contextLines": 3
|
|
693
|
+
}
|
|
694
|
+
},
|
|
695
|
+
{
|
|
696
|
+
"type": "tool-call",
|
|
697
|
+
"toolCallId": "call_function_namlnou7lqgd_2",
|
|
698
|
+
"toolName": "read",
|
|
699
|
+
"input": {
|
|
700
|
+
"path": "src/core/framework.js",
|
|
701
|
+
"lines": 200
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
]
|
|
705
|
+
},
|
|
706
|
+
{
|
|
707
|
+
"role": "tool",
|
|
708
|
+
"content": [
|
|
709
|
+
{
|
|
710
|
+
"type": "tool-result",
|
|
711
|
+
"toolCallId": "call_function_namlnou7lqgd_1",
|
|
712
|
+
"toolName": "search_file",
|
|
713
|
+
"output": {
|
|
714
|
+
"type": "json",
|
|
715
|
+
"value": {
|
|
716
|
+
"success": true,
|
|
717
|
+
"data": [],
|
|
718
|
+
"metadata": {
|
|
719
|
+
"pattern": "chatStream",
|
|
720
|
+
"total": 0,
|
|
721
|
+
"stats": {
|
|
722
|
+
"filesWithMatches": 0,
|
|
723
|
+
"totalMatches": 0,
|
|
724
|
+
"searchPath": "src/core/agent-chat.js"
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
},
|
|
730
|
+
{
|
|
731
|
+
"type": "tool-result",
|
|
732
|
+
"toolCallId": "call_function_namlnou7lqgd_2",
|
|
733
|
+
"toolName": "read",
|
|
734
|
+
"output": {
|
|
735
|
+
"type": "json",
|
|
736
|
+
"value": {
|
|
737
|
+
"success": true,
|
|
738
|
+
"data": "/**\n * Framework 核心容器\n * 管理所有子系统,提供统一入口\n */\n\nconst path = require('path');\nconst { AsyncLocalStorage } = require('async_hooks');\nconst { EventEmitter } = require('../utils/event-emitter');\nconst { PluginManager } = require('./plugin-manager');\nconst { ToolRegistry } = require('./tool-registry');\nconst { Agent } = require('./agent');\nconst { Subagent } = require('./subagent');\nconst { SubAgentConfigManager } = require('./sub-agent-config');\nconst { ContextManager } = require('./context-manager');\nconst { SessionContext } = require('./session-context');\nconst { SessionStorageAdapter, getDefaultAdapter } = require('./session-storage');\nconst { CoordinatorManager } = require('./coordinator-manager');\nconst { logEmitter, Logger, LOG_LEVELS } = require('../utils/logger');\n// 创建一个连接到 Framework 的 logger\nfunction createFrameworkLogger(framework) {\n const logger = new Logger({ namespace: 'foliko' });\n const state = { silent: false };\n\n // 获取当前有效的级别(优先用全局级别)\n const getLevel = () => Logger.globalLevel ?? logger._level ?? LOG_LEVELS.INFO;\n\n // 日志级别配置表,消除 4×2 重复代码\n const LEVEL_CONFIG = [\n ['info', LOG_LEVELS.INFO, 'INFO'],\n ['warn', LOG_LEVELS.WARN, 'WARN'],\n ['error', LOG_LEVELS.ERROR, 'ERROR'],\n ['debug', LOG_LEVELS.DEBUG, 'DEBUG'],\n ];\n\n // 通用日志拦截包装器\n function wrapLogMethod(targetLogger, originalMethod, levelThreshold, levelName) {\n return (...args) => {\n const lvl = getLevel();\n if (lvl <= levelThreshold) {\n if (!state.silent) originalMethod(...args);\n framework.emit('log', { level: levelName, message: args.join(' '), namespace: targetLogger.namespace });\n }\n };\n }\n\n // 包装所有级别\n const originals = {};\n for (const [name] of LEVEL_CONFIG) {\n originals[name] = logger[name].bind(logger);\n }\n for (const [name, threshold, label] of LEVEL_CONFIG) {\n logger[name] = wrapLogMethod(logger, originals[name], threshold, label);\n }\n\n // 拦截 child 方法\n const originalChild = logger.child.bind(logger);\n logger.child = (...args) => {\n const childLogger = originalChild(...args);\n const childOriginals = {};\n for (const [name] of LEVEL_CONFIG) {\n childOriginals[name] = childLogger[name].bind(childLogger);\n }\n for (const [name, threshold, label] of LEVEL_CONFIG) {\n childLogger[name] = wrapLogMethod(childLogger, childOriginals[name], threshold, label);\n }\n return childLogger;\n };\n\n // 暴露 setGlobalLogLevel 方法\n logger.setGlobalLogLevel = (level) => { Logger.globalLevel = level; };\n\n // 暴露 setSilent 方法\n logger.setSilent = (v) => { state.silent = v; };\n\n return logger;\n}\n\n// 添加框架目录的 node_modules 到全局搜索路径\n// 让项目插件能 require 框架内置的包(如 zod)\nconst frameworkNodeModules = path.join(__dirname, '..', '..', 'node_modules');\nif (!module.paths.includes(frameworkNodeModules)) {\n module.paths.unshift(frameworkNodeModules);\n}\n\n// 现有的 AsyncLocalStorage(保持向后兼容)\nconst asyncLocalStorage = new AsyncLocalStorage();\n\n// 新增:两层 AsyncLocalStorage\nconst requestStorage = new AsyncLocalStorage(); // Request 级别\nconst sessionStorage = new AsyncLocalStorage(); // Session 级别\n\nclass Framework extends EventEmitter {\n /**\n * @param {Object} config - 配置\n * @param {boolean} [config.debug=false] - 调试模式\n */\n constructor(config = {}) {\n super();\n this._silent=config.silent||false;\n this._debug = config.debug || false;\n this._ready = false;\n this._readyPromise = null;\n this._resolveReady = null;\n\n // 初始化子系统\n this.toolRegistry = new ToolRegistry();\n this.pluginManager = new PluginManager(this);\n\n // Agent 管理\n this._agents = []; // 所有创建的 agent\n this._mainAgent = null; // 主 agent\n\n // 保存 toolRegistry 监听器引用,用于清理\n this._toolRegistryListeners = [];\n\n // 子代理配置管理器\n this._subAgentConfigManager = null;\n\n // 执行上下文(工具调用时可用)\n this._executionContext = null;\n\n // ========== 分层上下文系统 ==========\n // Context 管理器\n this._contextManager = new ContextManager(this);\n\n // SessionContext 缓存(轻量管理,不依赖 ContextManager)\n this._sessionContexts = new Map(); // sessionId → SessionContext\n\n // Session 存储适配器(默认使用文件存储)\n this._sessionStorage = null;\n this._sessionStorageConfig = config.sessionStorage || {\n type: 'file',\n baseDir: '.agent/sessions',\n };\n\n // 注册 logger 到 framework\n this.logger = createFrameworkLogger(this);\n\n // Coordinator管理器(用于Coordinator模式)\n this._coordinatorManager = null;\n\n // 事件转发 - ToolRegistry 事件\n const toolRegisteredHandler = (tool) => {\n this.emit('tool:registered', tool);\n };\n const toolCallHandler = (data) => {\n this.emit('tool:call', data);\n // 兼容连字符格式\n this.emit('tool-call', data);\n };\n const toolResultHandler = (data) => {\n this.emit('tool:result', data);\n };\n const toolErrorHandler = (data) => {\n this.emit('tool:error', data);\n };\n this.toolRegistry.on('tool:registered', toolRegisteredHandler);\n this.toolRegistry.on('tool:call', toolCallHandler);\n this.toolRegistry.on('tool:result', toolResultHandler);\n this.toolRegistry.on('tool:error', toolErrorHandler);\n this._toolRegistryListeners = [\n { event: 'tool:registered', handler: toolRegisteredHandler },\n { event: 'tool:call', handler: toolCallHandler },\n { event: 'tool:result', handler: toolResultHandler },\n { event: 'tool:error', handler: toolErrorHandler },\n ];\n\n // 事件转发 - Agent 事件\n this._setupAgentEventForwarding();\n\n this._registerBuiltinTools();\n }\n\n /**\n * 设置 Agent 事件转发\n * @private\n */\n _setupAgentEventForwarding() {\n // 保存 agent 创建时的转发监听器引用,用于清理\n this._agentForwardingListeners = new Map();\n // 监听 agent:created 事件,为新创建的 Agent 设置事件转发\n this.on('agent:created', (agent) => {\n // 转发 Agent 的 tool-call/result/error 事件\n const toolCallHandler = (data) => {\n this.emit('tool:call', data);\n };\n const toolResultHandler = (data) => {\n this.emit('tool:result', data);\n };\n const toolErrorHandler = (data) => {\n this.emit('tool:error', data);\n };\n const messageHandler = (msg) => {\n this.emit('agent:message', msg);\n };\n agent.on('tool-call', toolCallHandler);\n agent.on('tool-result', toolResultHandler);\n agent.on('tool-error', toolErrorHandler);\n agent.on('message', messageHandler);\n // 保存引用用于后续清理",
|
|
739
|
+
"metadata": {
|
|
740
|
+
"filePath": "D:\\Code\\foliko\\src\\core\\framework.js",
|
|
741
|
+
"size": 32034,
|
|
742
|
+
"lines": null
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
]
|
|
748
|
+
},
|
|
749
|
+
{
|
|
750
|
+
"role": "assistant",
|
|
751
|
+
"content": [
|
|
752
|
+
{
|
|
753
|
+
"type": "text",
|
|
754
|
+
"text": "<think>我需要找到 `chatStream` 方法。让我用更准确的方式搜索。\n</think>\n\n\n"
|
|
755
|
+
},
|
|
756
|
+
{
|
|
757
|
+
"type": "tool-call",
|
|
758
|
+
"toolCallId": "call_function_vjyplk4f7vcv_1",
|
|
759
|
+
"toolName": "bash",
|
|
760
|
+
"input": {
|
|
761
|
+
"cmd": "grep -n \"chatStream\" src/core/agent-chat.js"
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
]
|
|
765
|
+
},
|
|
766
|
+
{
|
|
767
|
+
"role": "tool",
|
|
768
|
+
"content": [
|
|
769
|
+
{
|
|
770
|
+
"type": "tool-result",
|
|
771
|
+
"toolCallId": "call_function_vjyplk4f7vcv_1",
|
|
772
|
+
"toolName": "bash",
|
|
773
|
+
"output": {
|
|
774
|
+
"type": "json",
|
|
775
|
+
"value": {
|
|
776
|
+
"success": true,
|
|
777
|
+
"data": "296: executeFunction: this.chatStream.bind(this),\n369: executeFunction: this.chatStream.bind(this),\n464: async *chatStream(message, options = {}) {\n621: executeFunction: this.chatStream.bind(this),\n",
|
|
778
|
+
"metadata": {
|
|
779
|
+
"command": "grep -n \"chatStream\" src/core/agent-chat.js",
|
|
780
|
+
"stderr": "",
|
|
781
|
+
"duration": 105
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
]
|
|
787
|
+
},
|
|
788
|
+
{
|
|
789
|
+
"role": "assistant",
|
|
790
|
+
"content": [
|
|
791
|
+
{
|
|
792
|
+
"type": "text",
|
|
793
|
+
"text": "<think>好的,找到了 `chatStream` 在第 464 行。让我读取那部分代码。\n</think>\n\n\n"
|
|
794
|
+
},
|
|
795
|
+
{
|
|
796
|
+
"type": "tool-call",
|
|
797
|
+
"toolCallId": "call_function_ft5na9if8949_1",
|
|
798
|
+
"toolName": "read",
|
|
799
|
+
"input": {
|
|
800
|
+
"path": "src/core/agent-chat.js",
|
|
801
|
+
"lines": 200
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
]
|
|
805
|
+
},
|
|
806
|
+
{
|
|
807
|
+
"role": "tool",
|
|
808
|
+
"content": [
|
|
809
|
+
{
|
|
810
|
+
"type": "tool-result",
|
|
811
|
+
"toolCallId": "call_function_ft5na9if8949_1",
|
|
812
|
+
"toolName": "read",
|
|
813
|
+
"output": {
|
|
814
|
+
"type": "json",
|
|
815
|
+
"value": {
|
|
816
|
+
"success": true,
|
|
817
|
+
"data": "/**\n * AgentChatHandler 聊天处理器\n * 使用 AI SDK 的 ToolLoopAgent 处理工具调用循环\n *\n * 职责委托:\n * - ChatSession: 会话管理、消息历史、队列\n * - ToolExecutor: 工具发现、执行、验证\n * - ContextCompressor: 上下文压缩\n */\n\nconst { EventEmitter } = require('../utils/event-emitter');\nconst { logger } = require('../utils/logger');\nconst {\n tool: aiTool,\n ToolLoopAgent,\n isLoopFinished,\n} = require('ai');\nconst { autoSplitToolResult } = require('../../plugins/data-splitter-plugin');\nconst { cleanResponse } = require('../utils');\nconst { ChatQueueManager } = require('../utils/chat-queue');\nconst { TokenCounter } = require('./token-counter');\nconst { isThinkingModel } = require('./provider');\n// 新模块\nconst { ChatSession } = require('./chat-session');\nconst { ToolExecutor } = require('./tool-executor');\nconst { ContextCompressor } = require('./context-compressor');\nconst {\n DEFAULT_MAX_OUTPUT_TOKENS,\n DEFAULT_TEMPERATURE,\n DEFAULT_MAX_STEPS,\n DEFAULT_MAX_CONCURRENT,\n DEFAULT_RETRY_ATTEMPTS,\n DEFAULT_RETRY_DELAY_MS,\n} = require('./constants');\n\nclass AgentChatHandler extends EventEmitter {\n /**\n * @param {Agent} agent - Agent 实例\n * @param {Object} config - 配置\n */\n constructor(agent, config = {}) {\n super();\n\n this.agent = agent;\n this.config = config;\n\n // AI 配置\n this.model = config.model || 'deepseek-chat';\n this.provider = config.provider || 'deepseek';\n this.apiKey = config.apiKey;\n this.baseURL = config.baseURL;\n this.providerOptions = config.providerOptions || {};\n this.providerOptions.maxOutputTokens = config.providerOptions?.maxOutputTokens || 8192;\n\n // DeepSeek thinking mode: 支持通过 config.thinkingMode 控制开关\n // config.thinkingMode = true 可启用,默认为禁用\n const enableThinking = config.thinkingMode === true && isThinkingModel(this.model);\n this._thinkingMode = enableThinking;\n\n if (enableThinking) {\n // thinking mode 模型不支持 temperature 参数\n this.providerOptions.temperature = undefined;\n // DeepSeek 需要通过 extra_body 启用 thinking mode\n if (!this.providerOptions.deepseek) {\n this.providerOptions.deepseek = {};\n }\n this.providerOptions.deepseek.thinking = { type: 'enabled' };\n } else {\n this.providerOptions.temperature = config.providerOptions?.temperature || 0.3;\n }\n\n this._systemPrompt = config.systemPrompt || 'You are a helpful assistant.';\n this._maxSteps = config.maxSteps || 20;\n\n // 委托给新模块\n // ChatSession: 会话、消息、队列\n this._chatSession = new ChatSession({\n agent,\n maxConcurrent: config.maxConcurrent || 1,\n retryAttempts: config.retryAttempts || 3,\n retryDelay: config.retryDelay || 2000,\n });\n\n // ToolExecutor: 工具管理\n this._toolExecutor = new ToolExecutor({\n agent,\n framework: agent?.framework,\n });\n\n // ContextCompressor: 压缩\n this._contextCompressor = new ContextCompressor({\n agent,\n framework: agent?.framework,\n model: this.model,\n maxContextTokens: config.maxContextTokens,\n keepRecentMessages: config.keepRecentMessages || 20,\n enableSmartCompress: config.enableSmartCompress !== false,\n });\n\n // 上下文限制\n this._maxContextTokens = this._contextCompressor._maxContextTokens;\n\n // Token 计算缓存(避免每次请求重复计算)\n this._toolsTokensCache = null;\n this._toolsTokensCacheVersion = 0;\n this._systemPromptTokensCache = null;\n this._systemPromptTokensCacheKey = null;\n\n // ChatQueueManager: 队列管理\n this.queueManager = new ChatQueueManager({\n maxConcurrent: config.maxConcurrent || DEFAULT_MAX_CONCURRENT,\n retryAttempts: config.retryAttempts || DEFAULT_RETRY_ATTEMPTS,\n retryDelay: config.retryDelay || DEFAULT_RETRY_DELAY_MS,\n });\n\n // AI client\n this._aiClient = null;\n this._aiProvider = null;\n\n // Session Memory prepareStep\n this._sessionMemoryPrepareStep = null;\n if (agent?.framework) {\n const memoryPlugin = agent.framework.pluginManager.get('memory');\n if (memoryPlugin?.instance?.getPrepareStep) {\n this._sessionMemoryPrepareStep = memoryPlugin.instance.getPrepareStep();\n logger.debug('Session memory prepareStep loaded');\n }\n }\n\n // 转发 ChatSession 事件\n this._setupEventForwarding();\n\n // 转发 queueManager 事件到 ChatSession(让 SessionScope 能收到 stream:chunk 和 queue:completed 等)\n this.queueManager.on('stream:chunk', (data) => {\n this._chatSession.emit('stream:chunk', data);\n });\n this.queueManager.on('queue:completed', (data) => {\n this._chatSession.emit('queue:completed', data);\n });\n this.queueManager.on('queue:failed', (data) => {\n this._chatSession.emit('queue:failed', data);\n });\n\n // 转发 message:complete 和 message:error 到 ChatSession(让 SessionScope 能收到)\n this.on('message:start', (data) => { \n this._chatSession.emit('message:start', data); \n }); \n this.on('message:complete', (data) => {\n this._chatSession.emit('message:complete', data);\n });\n this.on('message:error', (data) => {\n this._chatSession.emit('message:error', data);\n });\n\n // 设置消息处理器(使用 bind 保持 this 引用)\n this._chatSession.setMessageProcessor(this._processMessage.bind(this));\n }\n\n /**\n * 设置事件转发\n * @private\n */\n _setupEventForwarding() {\n const events = [\n 'queue:added',\n 'queue:processing',\n 'queue:completed',\n 'queue:failed',\n 'queue:retry',\n 'queue:empty',\n 'queue:cleared',\n 'queue:session-removed',\n 'stream:chunk',\n ];\n\n events.forEach((eventName) => {\n this._chatSession.on(eventName, (data) => {\n this.emit(eventName, data);\n });\n });\n\n // 转发工具事件\n this._toolExecutor.on('tool:call', (data) => this.emit('tool:call', data));\n this._toolExecutor.on('tool:result', (data) => this.emit('tool:result', data));\n this._toolExecutor.on('tool:error', (data) => this.emit('tool:error', data));\n }\n\n // ==================== 工具管理(委托给 ToolExecutor) ====================\n\n /** AI tools 格式缓存 (避免每次 chat 重建) */\n _aiToolsCache = null;\n _aiToolsCacheKey = ''; // 工具列表的哈希摘要\n\n registerTool(tool) {\n this._toolExecutor.registerTool(tool);\n // 工具列表变了,清除缓存\n this._toolsTokensCacheVersion = 0;\n this._aiToolsCache = null; // 清除 AI tools 缓存\n return this;\n }",
|
|
818
|
+
"metadata": {
|
|
819
|
+
"filePath": "D:\\Code\\foliko\\src\\core\\agent-chat.js",
|
|
820
|
+
"size": 32568,
|
|
821
|
+
"lines": null
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
]
|
|
827
|
+
},
|
|
828
|
+
{
|
|
829
|
+
"role": "assistant",
|
|
830
|
+
"content": [
|
|
831
|
+
{
|
|
832
|
+
"type": "text",
|
|
833
|
+
"text": "<think>让我看看 chatStream 的具体实现,特别是流式输出的处理逻辑。\n</think>\n\n\n"
|
|
834
|
+
},
|
|
835
|
+
{
|
|
836
|
+
"type": "tool-call",
|
|
837
|
+
"toolCallId": "call_function_zy54i68vje97_1",
|
|
838
|
+
"toolName": "read",
|
|
839
|
+
"input": {
|
|
840
|
+
"path": "src/core/agent-chat.js",
|
|
841
|
+
"lines": 200
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
]
|
|
845
|
+
},
|
|
846
|
+
{
|
|
847
|
+
"role": "tool",
|
|
848
|
+
"content": [
|
|
849
|
+
{
|
|
850
|
+
"type": "tool-result",
|
|
851
|
+
"toolCallId": "call_function_zy54i68vje97_1",
|
|
852
|
+
"toolName": "read",
|
|
853
|
+
"output": {
|
|
854
|
+
"type": "json",
|
|
855
|
+
"value": {
|
|
856
|
+
"success": true,
|
|
857
|
+
"data": "/**\n * AgentChatHandler 聊天处理器\n * 使用 AI SDK 的 ToolLoopAgent 处理工具调用循环\n *\n * 职责委托:\n * - ChatSession: 会话管理、消息历史、队列\n * - ToolExecutor: 工具发现、执行、验证\n * - ContextCompressor: 上下文压缩\n */\n\nconst { EventEmitter } = require('../utils/event-emitter');\nconst { logger } = require('../utils/logger');\nconst {\n tool: aiTool,\n ToolLoopAgent,\n isLoopFinished,\n} = require('ai');\nconst { autoSplitToolResult } = require('../../plugins/data-splitter-plugin');\nconst { cleanResponse } = require('../utils');\nconst { ChatQueueManager } = require('../utils/chat-queue');\nconst { TokenCounter } = require('./token-counter');\nconst { isThinkingModel } = require('./provider');\n// 新模块\nconst { ChatSession } = require('./chat-session');\nconst { ToolExecutor } = require('./tool-executor');\nconst { ContextCompressor } = require('./context-compressor');\nconst {\n DEFAULT_MAX_OUTPUT_TOKENS,\n DEFAULT_TEMPERATURE,\n DEFAULT_MAX_STEPS,\n DEFAULT_MAX_CONCURRENT,\n DEFAULT_RETRY_ATTEMPTS,\n DEFAULT_RETRY_DELAY_MS,\n} = require('./constants');\n\nclass AgentChatHandler extends EventEmitter {\n /**\n * @param {Agent} agent - Agent 实例\n * @param {Object} config - 配置\n */\n constructor(agent, config = {}) {\n super();\n\n this.agent = agent;\n this.config = config;\n\n // AI 配置\n this.model = config.model || 'deepseek-chat';\n this.provider = config.provider || 'deepseek';\n this.apiKey = config.apiKey;\n this.baseURL = config.baseURL;\n this.providerOptions = config.providerOptions || {};\n this.providerOptions.maxOutputTokens = config.providerOptions?.maxOutputTokens || 8192;\n\n // DeepSeek thinking mode: 支持通过 config.thinkingMode 控制开关\n // config.thinkingMode = true 可启用,默认为禁用\n const enableThinking = config.thinkingMode === true && isThinkingModel(this.model);\n this._thinkingMode = enableThinking;\n\n if (enableThinking) {\n // thinking mode 模型不支持 temperature 参数\n this.providerOptions.temperature = undefined;\n // DeepSeek 需要通过 extra_body 启用 thinking mode\n if (!this.providerOptions.deepseek) {\n this.providerOptions.deepseek = {};\n }\n this.providerOptions.deepseek.thinking = { type: 'enabled' };\n } else {\n this.providerOptions.temperature = config.providerOptions?.temperature || 0.3;\n }\n\n this._systemPrompt = config.systemPrompt || 'You are a helpful assistant.';\n this._maxSteps = config.maxSteps || 20;\n\n // 委托给新模块\n // ChatSession: 会话、消息、队列\n this._chatSession = new ChatSession({\n agent,\n maxConcurrent: config.maxConcurrent || 1,\n retryAttempts: config.retryAttempts || 3,\n retryDelay: config.retryDelay || 2000,\n });\n\n // ToolExecutor: 工具管理\n this._toolExecutor = new ToolExecutor({\n agent,\n framework: agent?.framework,\n });\n\n // ContextCompressor: 压缩\n this._contextCompressor = new ContextCompressor({\n agent,\n framework: agent?.framework,\n model: this.model,\n maxContextTokens: config.maxContextTokens,\n keepRecentMessages: config.keepRecentMessages || 20,\n enableSmartCompress: config.enableSmartCompress !== false,\n });\n\n // 上下文限制\n this._maxContextTokens = this._contextCompressor._maxContextTokens;\n\n // Token 计算缓存(避免每次请求重复计算)\n this._toolsTokensCache = null;\n this._toolsTokensCacheVersion = 0;\n this._systemPromptTokensCache = null;\n this._systemPromptTokensCacheKey = null;\n\n // ChatQueueManager: 队列管理\n this.queueManager = new ChatQueueManager({\n maxConcurrent: config.maxConcurrent || DEFAULT_MAX_CONCURRENT,\n retryAttempts: config.retryAttempts || DEFAULT_RETRY_ATTEMPTS,\n retryDelay: config.retryDelay || DEFAULT_RETRY_DELAY_MS,\n });\n\n // AI client\n this._aiClient = null;\n this._aiProvider = null;\n\n // Session Memory prepareStep\n this._sessionMemoryPrepareStep = null;\n if (agent?.framework) {\n const memoryPlugin = agent.framework.pluginManager.get('memory');\n if (memoryPlugin?.instance?.getPrepareStep) {\n this._sessionMemoryPrepareStep = memoryPlugin.instance.getPrepareStep();\n logger.debug('Session memory prepareStep loaded');\n }\n }\n\n // 转发 ChatSession 事件\n this._setupEventForwarding();\n\n // 转发 queueManager 事件到 ChatSession(让 SessionScope 能收到 stream:chunk 和 queue:completed 等)\n this.queueManager.on('stream:chunk', (data) => {\n this._chatSession.emit('stream:chunk', data);\n });\n this.queueManager.on('queue:completed', (data) => {\n this._chatSession.emit('queue:completed', data);\n });\n this.queueManager.on('queue:failed', (data) => {\n this._chatSession.emit('queue:failed', data);\n });\n\n // 转发 message:complete 和 message:error 到 ChatSession(让 SessionScope 能收到)\n this.on('message:start', (data) => { \n this._chatSession.emit('message:start', data); \n }); \n this.on('message:complete', (data) => {\n this._chatSession.emit('message:complete', data);\n });\n this.on('message:error', (data) => {\n this._chatSession.emit('message:error', data);\n });\n\n // 设置消息处理器(使用 bind 保持 this 引用)\n this._chatSession.setMessageProcessor(this._processMessage.bind(this));\n }\n\n /**\n * 设置事件转发\n * @private\n */\n _setupEventForwarding() {\n const events = [\n 'queue:added',\n 'queue:processing',\n 'queue:completed',\n 'queue:failed',\n 'queue:retry',\n 'queue:empty',\n 'queue:cleared',\n 'queue:session-removed',\n 'stream:chunk',\n ];\n\n events.forEach((eventName) => {\n this._chatSession.on(eventName, (data) => {\n this.emit(eventName, data);\n });\n });\n\n // 转发工具事件\n this._toolExecutor.on('tool:call', (data) => this.emit('tool:call', data));\n this._toolExecutor.on('tool:result', (data) => this.emit('tool:result', data));\n this._toolExecutor.on('tool:error', (data) => this.emit('tool:error', data));\n }\n\n // ==================== 工具管理(委托给 ToolExecutor) ====================\n\n /** AI tools 格式缓存 (避免每次 chat 重建) */\n _aiToolsCache = null;\n _aiToolsCacheKey = ''; // 工具列表的哈希摘要\n\n registerTool(tool) {\n this._toolExecutor.registerTool(tool);\n // 工具列表变了,清除缓存\n this._toolsTokensCacheVersion = 0;\n this._aiToolsCache = null; // 清除 AI tools 缓存\n return this;\n }",
|
|
858
|
+
"metadata": {
|
|
859
|
+
"filePath": "D:\\Code\\foliko\\src\\core\\agent-chat.js",
|
|
860
|
+
"size": 32568,
|
|
861
|
+
"lines": null
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
]
|
|
867
|
+
},
|
|
868
|
+
{
|
|
869
|
+
"role": "assistant",
|
|
870
|
+
"content": [
|
|
871
|
+
{
|
|
872
|
+
"type": "text",
|
|
873
|
+
"text": "<think>让我直接读取 chatStream 部分的内容。\n</think>\n\n\n"
|
|
874
|
+
},
|
|
875
|
+
{
|
|
876
|
+
"type": "tool-call",
|
|
877
|
+
"toolCallId": "call_function_ae7r5wbiw2uj_1",
|
|
878
|
+
"toolName": "bash",
|
|
879
|
+
"input": {
|
|
880
|
+
"cmd": "sed -n '464,650p' src/core/agent-chat.js"
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
]
|
|
884
|
+
},
|
|
885
|
+
{
|
|
886
|
+
"role": "tool",
|
|
887
|
+
"content": [
|
|
888
|
+
{
|
|
889
|
+
"type": "tool-result",
|
|
890
|
+
"toolCallId": "call_function_ae7r5wbiw2uj_1",
|
|
891
|
+
"toolName": "bash",
|
|
892
|
+
"output": {
|
|
893
|
+
"type": "json",
|
|
894
|
+
"value": {
|
|
895
|
+
"success": true,
|
|
896
|
+
"data": " async *chatStream(message, options = {}) {\n const sessionId = options.sessionId || this.getDefaultSessionId();\n const framework = this.agent.framework;\n\n try {\n const { messageStore, messages } = await this._prepareSession(message, sessionId);\n\n if (!this._aiClient) {\n throw new Error('AI client not configured.');\n }\n\n const systemPrompt = framework.getSystemPrompt();\n const tools = this._getAITools(aiTool);\n const agent = new ToolLoopAgent({\n model: this._aiClient,\n instructions: systemPrompt,\n tools,\n stopWhen: isLoopFinished(),\n prepareStep: this._createPrepareStep(),\n });\n\n // AI SDK 错误回调:仅记录日志,不在这里处理(统一由 catch/yield 处理)\n const handleSDKError = ({ error }) => {\n logger.debug(`[AgentChat] SDK onError: ${error?.message}`);\n };\n\n // DeepSeek thinking mode: 收集 reasoning_content\n const isThinking = this._thinkingMode;\n let reasoningContent = '';\n\n const result = await framework.runInSession(sessionId, {}, async () => {\n return agent.stream({ messages, ...this.providerOptions, onError: handleSDKError });\n });\n\n const stream = result.fullStream;\n let fullText = '';\n const iterator = stream[Symbol.asyncIterator] ? stream : stream.fullStream;\n\n for await (const part of iterator || stream) {\n if (part.type === 'text-delta') {\n const text = part.text || part.textDelta || '';\n fullText += text;\n yield { type: 'text', text };\n } else if (part.type === 'reasoning') {\n reasoningContent += part.text || '';\n yield { type: 'thinking', text: part.text };\n } else if (part.type === 'tool-call') {\n yield { type: 'tool-call', toolName: part.toolName, input: part.input };\n } else if (part.type === 'tool-result') {\n yield { type: 'tool-result', toolName: part.toolName, result: part.output };\n } else if (part.type === 'error') {\n // 统一错误消息\n const errMsg = part.error?.message || 'AI 服务错误';\n yield { type: 'error', error: errMsg.split('\\n')[0] };\n }\n }\n\n // DeepSeek thinking mode: 标记 reasoning_content 已处理\n // 在 finishMessages 中标记,避免重复添加\n const finishMessages = (await result.response).messages;\n if (isThinking && reasoningContent) {\n // DeepSeek 要求:当有 tool_calls 时,必须将 reasoning_content 添加到消息中\n // AI SDK 的 finishMessage 不会包含 reasoning_content,需要手动添加\n for (const msg of finishMessages) {\n if (msg.role === 'assistant') {\n // 检查是否有 tool_calls\n const hasToolCalls = msg.tool_calls?.length > 0 ||\n (Array.isArray(msg.content) && msg.content.some(c => c.type === 'tool-call' || c.type === 'tool-use'));\n if (hasToolCalls) {\n // DeepSeek API 要求:带 tool_calls 时必须传递 reasoning_content\n msg.reasoning_content = reasoningContent;\n }\n }\n }\n }\n messages.push(...finishMessages);\n\n // 获取或估算 token 用量\n const usage = await result.totalUsage;\n let inputTokens = 0, outputTokens = 0;\n\n if (usage && (usage.promptTokens > 0 || usage.completionTokens > 0 || usage.prompt_tokens > 0 || usage.completion_tokens > 0)) {\n // totalUsage 有真实数据(DeepSeek 等 API)\n inputTokens = usage.promptTokens || usage.prompt_tokens || 0;\n outputTokens = usage.completionTokens || usage.completion_tokens || 0;\n this._updateMessageStoreUsage(messageStore, usage);\n } else {\n // totalUsage 不可用或为空(MiniMax 等 API 返回 {promptTokens:0,completionTokens:0})\n // 用 TokenCounter 从实际消息内容估算\n inputTokens = this._tokenCounter.countMessages(messages);\n outputTokens = this._tokenCounter.countText(fullText);\n }\n\n // 确保 messageStore.usage 始终有数据\n const usageData = { promptTokens: inputTokens, completionTokens: outputTokens };\n messageStore.usage = usageData;\n\n // 同时通过 framework 事件广播(绕开 queue 事件链路)\n if (this.agent && this.agent.framework) {\n this.agent.framework.emit('agent:usage', {\n sessionId,\n usage: usageData,\n });\n }\n\n // yield usage 数据,让 UI 层能直接获取\n yield {\n type: 'usage',\n sessionId,\n inputTokens,\n outputTokens,\n };\n\n const userMsg = messages[messages.length - finishMessages.length - 1];\n this.emit('message', { content: fullText, sessionId: sessionId, userMessage: userMsg });\n } catch (err) {\n // 统一错误处理:提取简洁消息并通过 yield 传递\n const friendlyMessage = err?.message?.split('\\n')[0] || '未知错误';\n\n this.emit('message:error', { sessionId, error: friendlyMessage, originalError: err });\n yield { type: 'error', error: friendlyMessage };\n } finally {\n const messageStore = this._getSessionMessageStore(sessionId);\n messageStore.save().catch((err) => {\n logger.error(`[${sessionId}] Failed to save message store: ${err.message}`);\n });\n }\n }\n\n /**\n * 清除历史\n */\n clearHistory(sessionId) {\n if (sessionId) {\n const store = this._chatSession.getSessionMessageStore(sessionId);\n store.messages = [];\n store.historyLoaded = false;\n }\n }\n\n /**\n * 销毁\n */\n destroy() {\n if (this._chatSession) {\n this._chatSession.removeAllListeners();\n }\n this.removeAllListeners();\n }\n\n /**\n * 入队消息(走共享的 ChatQueueManager,与 sendMessage 共用同一队列)\n */\n enqueue(sessionId, message, options = {}) {\n const requestId = this.generateRequestId();\n return this.queueManager.enqueue(requestId, sessionId, message, {\n ...options,\n executeFunction: this.chatStream.bind(this),\n });\n }\n\n /**\n * 处理消息(队列回调)\n * @private\n */\n async _processMessage(sessionId, message, options) {\n const framework = this.agent.framework;\n\n try {\n const { messageStore, messages } = await this._prepareSession(message, sessionId);\n\n if (!this._aiClient) {\n throw new Error('AI client not configured.');\n }\n const systemPrompt = framework.getSystemPrompt();\n const tools = this._getAITools(aiTool);\n\n // DeepSeek thinking mode: 处理参数\n const apiOptions = { ...this.providerOptions };\n if (this._thinkingMode) {\n delete apiOptions.temperature;\n }\n\n const agent = new ToolLoopAgent({\n model: this._aiClient,\n instructions: systemPrompt,\n tools,\n",
|
|
897
|
+
"metadata": {
|
|
898
|
+
"command": "sed -n '464,650p' src/core/agent-chat.js",
|
|
899
|
+
"stderr": "",
|
|
900
|
+
"duration": 86
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
]
|
|
906
|
+
},
|
|
907
|
+
{
|
|
908
|
+
"role": "assistant",
|
|
909
|
+
"content": [
|
|
910
|
+
{
|
|
911
|
+
"type": "text",
|
|
912
|
+
"text": "<think>我发现了问题!看第 518 行:\n\n```javascript\nconst finishMessages = (await result.response).messages;\n```\n\n这里有一个问题:`result.response` 可能已经 resolve 了,但 `result.fullStream` 可能还在进行中。当我们 await `result.response` 时,我们等待的是响应完成,但 stream 可能还没完全消费完。\n\n不过这不是回复中断的原因。让我再仔细看看 chatStream 的实现,特别是它是如何被调用的。\n\n让我看看 sendMessageStream 方法,因为它可能是使用 chatStream 的地方。\n</think>\n\n\n"
|
|
913
|
+
},
|
|
914
|
+
{
|
|
915
|
+
"type": "tool-call",
|
|
916
|
+
"toolCallId": "call_function_o79o3gsh69hr_1",
|
|
917
|
+
"toolName": "bash",
|
|
918
|
+
"input": {
|
|
919
|
+
"cmd": "sed -n '280,400p' src/core/agent-chat.js"
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
]
|
|
923
|
+
},
|
|
924
|
+
{
|
|
925
|
+
"role": "tool",
|
|
926
|
+
"content": [
|
|
927
|
+
{
|
|
928
|
+
"type": "tool-result",
|
|
929
|
+
"toolCallId": "call_function_o79o3gsh69hr_1",
|
|
930
|
+
"toolName": "bash",
|
|
931
|
+
"output": {
|
|
932
|
+
"type": "json",
|
|
933
|
+
"value": {
|
|
934
|
+
"success": true,
|
|
935
|
+
"data": " */\n async sendMessage(message, options = {}) {\n const sessionId = options.sessionId || this.getDefaultSessionId();\n const requestId = this.generateRequestId();\n\n\n this.emit('message:start', {\n requestId,\n sessionId,\n message,\n timestamp: Date.now(),\n });\n\n try {\n const result = await this.queueManager.enqueue(requestId, sessionId, message, {\n ...options,\n executeFunction: this.chatStream.bind(this),\n });\n\n this.emit('message:complete', {\n ...result,\n requestId,\n sessionId,\n duration: Date.now() - result.startTime,\n });\n\n return result;\n } catch (error) {\n this.emit('message:error', {\n requestId,\n sessionId,\n error: error.message,\n });\n throw error;\n }\n }\n\n /**\n * 流式发送消息(带队列,支持实时yield)\n */\n async *sendMessageStream(message, options = {}) {\n const sessionId = options.sessionId || this.getDefaultSessionId();\n const requestId = this.generateRequestId();\n\n // 用于存储流式数据\n const chunkQueue = [];\n let streamCompleted = false;\n let streamError = null;\n let resolveNext = null;\n\n // 监听流式数据\n const chunkHandler = (data) => {\n if (data.requestId === requestId) {\n if (resolveNext) {\n resolveNext();\n resolveNext = null;\n }\n }\n };\n\n const completeHandler = (data) => {\n if (data.requestId === requestId) {\n streamCompleted = true;\n if (resolveNext) {\n resolveNext();\n resolveNext = null;\n }\n }\n };\n\n const errorHandler = (data) => {\n if (data.requestId === requestId) {\n streamError = new Error(data.error);\n if (resolveNext) {\n resolveNext();\n resolveNext = null;\n }\n }\n };\n\n // 注册事件监听\n this.on('stream:chunk', chunkHandler);\n this.on('queue:completed', completeHandler);\n this.on('queue:failed', errorHandler);\n\n // 启动队列任务(不等待完成)\n const queuePromise = this.queueManager\n .enqueue(requestId, sessionId, message, {\n ...options,\n executeFunction: this.chatStream.bind(this),\n })\n .catch((err) => {\n // 只设置 streamError,不打印日志(避免重复)\n if (!streamError) {\n streamError = err;\n }\n });\n\n try {\n // 实时 yield 流式数据\n while (!streamCompleted && !streamError) {\n if (chunkQueue.length > 0) {\n yield chunkQueue.shift();\n } else {\n await new Promise((resolve) => {\n resolveNext = resolve;\n });\n }\n }\n\n if (streamError) {\n throw streamError;\n }\n\n // 等待队列完成,获取完整结果\n const result = await queuePromise;\n this.emit('message:complete', { ...result, requestId, sessionId });\n } finally {\n // 清理事件监听\n this.off('stream:chunk', chunkHandler);\n this.off('queue:completed', completeHandler);\n",
|
|
936
|
+
"metadata": {
|
|
937
|
+
"command": "sed -n '280,400p' src/core/agent-chat.js",
|
|
938
|
+
"stderr": "",
|
|
939
|
+
"duration": 87
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
]
|
|
945
|
+
},
|
|
946
|
+
{
|
|
947
|
+
"role": "assistant",
|
|
948
|
+
"content": [
|
|
949
|
+
{
|
|
950
|
+
"type": "text",
|
|
951
|
+
"text": "<think>Looking at the code, I notice that `sendMessageStream` method has a complex flow:\n\n1. It registers event listeners (`chunkHandler`, `completeHandler`, `errorHandler`)\n2. It enqueues a task with `chatStream.bind(this)` as the executeFunction\n3. The `chatStream` is an async generator that yields text chunks\n4. The queue manager calls `executeFunction` but doesn't handle generators properly\n\nThe key issue I see is in the `ChatQueueManager.executeWithRetry`:\n\n```javascript\nconst result = await item.executeFunction(message, {\n ...item.options,\n sessionId: item.sessionId,\n onChunk: (chunk) => { ... }\n});\n```\n\nBut `chatStream` is an async generator, not a regular async function. When you call an async generator, it returns immediately with a generator object, not a promise that resolves to the result.\n\nSo `executeWithRetry` would get a generator object instead of the actual stream results. This could cause the queue to think it's complete when it's actually not.\n\nLet me check the queue manager implementation more carefully.\n</think>\n\n\n"
|
|
952
|
+
},
|
|
953
|
+
{
|
|
954
|
+
"type": "tool-call",
|
|
955
|
+
"toolCallId": "call_function_q112u6kbcml1_1",
|
|
956
|
+
"toolName": "read",
|
|
957
|
+
"input": {
|
|
958
|
+
"path": "src/utils/chat-queue.js"
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
]
|
|
962
|
+
},
|
|
963
|
+
{
|
|
964
|
+
"role": "tool",
|
|
965
|
+
"content": [
|
|
966
|
+
{
|
|
967
|
+
"type": "tool-result",
|
|
968
|
+
"toolCallId": "call_function_q112u6kbcml1_1",
|
|
969
|
+
"toolName": "read",
|
|
970
|
+
"output": {
|
|
971
|
+
"type": "json",
|
|
972
|
+
"value": {
|
|
973
|
+
"success": true,
|
|
974
|
+
"data": "const { EventEmitter } = require('./event-emitter');\nconst { cleanResponse } = require('./index');\nconst { logger } = require('./logger');\nconst { isNetworkError, calculateDelay, PRESETS } = require('./retry');\nconst log = logger.child('ChatQueue');\n// ChatQueueManager.js\nclass ChatQueueManager extends EventEmitter {\n constructor(options = {}) {\n super();\n this.queue = [];\n this.isProcessing = false;\n this.maxConcurrent = options.maxConcurrent || 1;\n this.activeCount = 0;\n this.retryAttempts = options.retryAttempts || 3;\n this.retryDelay = options.retryDelay || 2000;\n }\n\n /**\n * 添加请求到队列\n */\n enqueue(requestId, sessionId, message, options = {}) {\n return new Promise((resolve, reject) => {\n const queueItem = {\n id: requestId,\n sessionId,\n message,\n options,\n resolve,\n reject,\n timestamp: Date.now(),\n status: 'pending',\n retries: 0,\n priority: options.priority || 0,\n executeFunction: options.executeFunction || null,\n };\n\n this.queue.push(queueItem);\n // 按优先级排序\n this.queue.sort((a, b) => b.priority - a.priority);\n\n this.emit('queue:added', {\n requestId,\n sessionId,\n queueLength: this.queue.length,\n position: this.queue.length,\n });\n\n this.processQueue();\n });\n }\n\n /**\n * 处理队列\n */\n async processQueue() {\n if (this.activeCount >= this.maxConcurrent || this.queue.length === 0) {\n if (this.queue.length === 0 && this.activeCount === 0) {\n this.emit('queue:empty', { timestamp: Date.now() });\n }\n return;\n }\n\n const item = this.queue.shift();\n this.activeCount++;\n item.status = 'processing';\n\n this.emit('queue:processing', {\n requestId: item.id,\n sessionId: item.sessionId,\n activeCount: this.activeCount,\n remaining: this.queue.length,\n });\n\n try {\n const result = await this.executeWithRetry(item);\n // 检查 result 是否有错误(而不是通过 try/catch)\n if (result.error) {\n //console.log('[ChatQueue] Rejecting with error from result:', result.error.message);\n throw result.error;\n } else {\n item.resolve(result);\n this.emit('queue:completed', {\n requestId: item.id,\n sessionId: item.sessionId,\n duration: Date.now() - item.timestamp,\n });\n }\n } catch (error) {\n log.warn('[ChatQueue] Rejecting Error:', error.message);\n item.reject(error);\n this.emit('queue:failed', {\n requestId: item.id,\n sessionId: item.sessionId,\n error: error.message,\n });\n } finally {\n this.activeCount--;\n this.processQueue();\n }\n }\n\n /**\n * 执行带重试的请求\n */\n async executeWithRetry(item) {\n let lastError;\n\n for (let attempt = 1; attempt <= this.retryAttempts; attempt++) {\n try {\n this.emit('queue:retry', {\n requestId: item.id,\n attempt,\n maxRetries: this.retryAttempts,\n });\n\n const result = await this.executeStream(item);\n\n // 检查是否有错误(通过返回的 result.error)\n if (result.error) {\n lastError = result.error;\n if (attempt < this.retryAttempts && this.isRetryableError(lastError)) {\n await this.sleep(calculateDelay(attempt, { baseDelay: this.retryDelay }));\n continue;\n }\n throw lastError;\n }\n\n return result;\n } catch (error) {\n log.warn('[ChatQueue] executeWithRetry: ', attempt, 'error:', error.message);\n lastError = error;\n if (attempt < this.retryAttempts && this.isRetryableError(error)) {\n await this.sleep(calculateDelay(attempt, { baseDelay: this.retryDelay }));\n continue;\n }\n break;\n }\n }\n\n // 将最后的错误转换为友好消息\n const friendlyMessage = isNetworkError(lastError)\n ? 'AI 服务暂时不可用,请稍后重试'\n : (lastError?.message || String(lastError)).split('\\n')[0];\n\n const friendlyError = new Error(friendlyMessage);\n friendlyError.originalError = lastError;\n //log.info('[ChatQueue] executeWithRetry: throwing friendly error:', friendlyMessage);\n throw friendlyError;\n }\n\n /**\n * 执行流式请求\n */\n async executeStream(item) {\n const chunks = [];\n\n if (!item.executeFunction) {\n throw new Error('No execute function provided');\n }\n\n const stream = item.executeFunction(item.message, item.options);\n\n try {\n for await (const chunk of stream) {\n chunks.push(chunk);\n this.emit('stream:chunk', {\n requestId: item.id,\n sessionId: item.sessionId,\n chunk,\n accumulated: chunks.length,\n });\n }\n } catch (err) {\n // SDK 直接抛出错误(没有通过 chunk 传递)\n // 转换为友好错误消息\n const friendlyMessage = isNetworkError(err)\n ? 'AI 服务暂时不可用,请稍后重试'\n : (err.message || err.toString()).split('\\n')[0];\n\n log.warn('[ChatQueue] executeStream Error:', friendlyMessage);\n chunks.push({ type: 'error', error: friendlyMessage });\n this.emit('stream:chunk', {\n requestId: item.id,\n sessionId: item.sessionId,\n chunk: chunks[chunks.length - 1],\n accumulated: chunks.length,\n });\n }\n\n // 检查是否有错误 chunk\n const errorChunk = chunks.find((c) => c.type === 'error');\n if (errorChunk) {\n // 不再抛出错误,而是返回包含错误的 result\n // 让调用者通过 result 判断是否有错误\n const error = new Error(errorChunk.error || 'Stream error');\n error.chunks = chunks;\n error.isStreamError = true;\n log.warn('[ChatQueue] executeStream Error:', error.message);\n return {\n chunks,\n content: cleanResponse(''),\n sessionId: item.sessionId,\n requestId: item.id,\n error,\n };\n }\n\n const fullText = chunks\n .filter((a) => a.type === 'text')\n .map((item) => item.text)\n .join('');\n\n return {\n chunks,\n content: cleanResponse(fullText),\n sessionId: item.sessionId,\n requestId: item.id,\n };\n }\n\n /**\n * 判断错误是否可重试\n */\n /**\n * 判断错误是否可重试(委托给统一入口)\n * @param {Error} error\n * @returns {boolean}\n */\n isRetryableError(error) {\n return isNetworkError(error);\n }\n\n /**\n * 获取队列状态\n */\n getStatus() {\n return {\n queueLength: this.queue.length,\n activeCount: this.activeCount,\n maxConcurrent: this.maxConcurrent,\n isProcessing: this.activeCount > 0,\n queue: this.queue.map((item) => ({\n id: item.id,\n sessionId: item.sessionId,\n status: item.status,\n priority: item.priority,\n waitingTime: Date.now() - item.timestamp,\n retries: item.retries,\n })),\n };\n }\n\n /**\n * 清空队列\n */\n clear() {\n const pendingItems = [...this.queue];\n this.queue = [];\n this.activeCount = 0;\n\n pendingItems.forEach((item) => {\n item.reject(new Error('Queue cleared by user'));\n });\n\n this.emit('queue:cleared', { count: pendingItems.length });\n return pendingItems.length;\n }\n\n /**\n * 移除特定会话的所有请求\n */\n removeSessionRequests(sessionId) {\n const removed = [];\n const remaining = [];\n\n for (const item of this.queue) {\n if (item.sessionId === sessionId) {\n removed.push(item);\n item.reject(new Error(`Session ${sessionId} requests cancelled`));\n } else {\n remaining.push(item);\n }\n }\n\n this.queue = remaining;\n this.emit('queue:session-removed', { sessionId, count: removed.length });\n return removed.length;\n }\n\n /**\n * 获取队列中的请求数量\n */\n size() {\n return this.queue.length;\n }\n\n sleep(ms) {\n return new Promise((resolve) => setTimeout(resolve, ms));\n }\n}\n\nmodule.exports = { ChatQueueManager };\n",
|
|
975
|
+
"metadata": {
|
|
976
|
+
"filePath": "D:\\Code\\foliko\\src\\utils\\chat-queue.js",
|
|
977
|
+
"size": 8104,
|
|
978
|
+
"lines": 303
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
]
|
|
984
|
+
},
|
|
985
|
+
{
|
|
986
|
+
"role": "assistant",
|
|
987
|
+
"content": [
|
|
988
|
+
{
|
|
989
|
+
"type": "text",
|
|
990
|
+
"text": "<think>Now I have a complete understanding of the code. The issue is in `chat-queue.js` in the `executeStream` method.\n\nLooking at the code:\n\n```javascript\nasync executeStream(item) {\n const chunks = [];\n\n if (!item.executeFunction) {\n throw new Error('No execute function provided');\n }\n\n const stream = item.executeFunction(item.message, item.options);\n\n try {\n for await (const chunk of stream) {\n chunks.push(chunk);\n this.emit('stream:chunk', {\n requestId: item.id,\n sessionId: item.sessionId,\n chunk,\n accumulated: chunks.length,\n });\n }\n } catch (err) {\n // SDK 直接抛出错误(没有通过 chunk 传递)\n // 转换为友好错误消息\n const friendlyMessage = isNetworkError(err)\n ? 'AI 服务暂时不可用,请稍后重试'\n : (err.message || err.toString()).split('\\n')[0];\n\n log.warn('[ChatQueue] executeStream Error:', friendlyMessage);\n chunks.push({ type: 'error', error: friendlyMessage });\n this.emit('stream:chunk', {\n requestId: item.id,\n sessionId: item.sessionId,\n chunk: chunks[chunks.length - 1],\n accumulated: chunks.length,\n });\n }\n\n // 检查是否有错误 chunk\n const errorChunk = chunks.find((c) => c.type === 'error');\n if (errorChunk) {\n // 不再抛出错误,而是返回包含错误的 result\n // 让调用者通过 result 判断是否有错误\n const error = new Error(errorChunk.error || 'Stream error');\n error.chunks = chunks;\n error.isStreamError = true;\n log.warn('[ChatQueue] executeStream Error:', error.message);\n return {\n chunks,\n content: cleanResponse(''),\n sessionId: item.sessionId,\n requestId: item.id,\n error,\n };\n }\n\n const fullText = chunks\n .filter((a) => a.type === 'text')\n .map((item) => item.text)\n .join('');\n\n return {\n chunks,\n content: cleanResponse(fullText),\n sessionId: item.sessionId,\n requestId: item.id,\n };\n }\n```\n\nThe problem is that `chunks.push({ type: 'error', error: friendlyMessage })` embeds the error in the chunks array, but then the method returns a result object with `error` set to the Error object itself. This means the error is being passed through two different mechanisms - both as a chunk and as a return value - which creates ambiguity about how errors should be handled downstream.\n\nLooking at `executeWithRetry`, when `result.error` exists, it throws that error. But in `processQueue`, there's a `finally` block that always decrements `activeCount` and calls `processQueue()` again. The issue is that if an error is thrown, the queue processing might not resume properly if the error handling doesn't account for all error paths.\n\nThe connection to `sendMessageStream` suggests the stream might be getting cut off before completion, possibly because the error object itself is malformed or the chunks array becomes corrupted when the error is pushed into it.\n\nLooking at the AI SDK's stream handling, when `agent.stream()` encounters an error during iteration, it can cause the consumer to stop prematurely. The issue appears to be that `isNetworkError` might not correctly identify certain error types (like stream interruptions), so they're not being retried properly. Additionally, if `cleanResponse('')` returns null or an unexpected value when called on an empty string, it could cause downstream issues when the result is processed.\n\nThe real problem is likely in how `result.error` is being checked in `processQueue`. If the error object doesn't have the right structure or if there's a mismatch between how errors are serialized and deserialized across the stream boundary, the catch block might not properly handle all error cases, leaving the stream in an inconsistent state.\n</think>\n\n根据代码分析,回复中断可能有以下几个原因:\n\n## 🔍 可能原因\n\n### 1. 队列处理错误未正确传播\n\n`chat-queue.js` 中 `processQueue` 和 `executeStream` 的错误处理逻辑:\n\n```javascript\n// processQueue 依赖 result.error 来判断是否成功\nconst result = await this.executeWithRetry(item);\nif (result.error) {\n throw result.error; // 这里抛出\n}\n// 但如果 throw 发生,finally 仍会执行\n```\n\n### 2. 流式处理中的异常中断\n\n`executeStream` 中的错误处理:\n\n```javascript\nfor await (const chunk of stream) { // 如果这里出错\n chunks.push(chunk);\n this.emit('stream:chunk', {...});\n} catch (err) {\n // 错误被捕获后,可能导致部分 chunks 未发出\n chunks.push({ type: 'error', error: friendlyMessage });\n}\n```\n\n如果 AI SDK 在流式输出中途抛出异常,部分 text chunks 可能已生成但未被正确返回。\n\n### 3. 网络超时或 API 限流\n\nDeepSeek 的 `maxOutputTokens` 限制为 `8192`,如果回复过长可能被截断。\n\n---\n\n## 🔧 建议的排查方向\n\n1. **检查 `.foliko.log` 最新日志**,看是否有错误信息\n2. **检查网络连接**,是否是间歇性网络问题\n3. **确认 AI API 的 quota/tokens 限制**\n\n你能提供 `.foliko.log` 的最新内容吗?或者描述一下具体是在什么场景下回复会中断(长回复?短回复?特定操作时?)"
|
|
991
|
+
}
|
|
992
|
+
]
|
|
993
|
+
},
|
|
994
|
+
{
|
|
995
|
+
"role": "user",
|
|
996
|
+
"content": "有没有可能是长消息截断多部分的问题?"
|
|
997
|
+
},
|
|
998
|
+
{
|
|
999
|
+
"role": "assistant",
|
|
1000
|
+
"content": [
|
|
1001
|
+
{
|
|
1002
|
+
"type": "text",
|
|
1003
|
+
"text": "<think>用户怀疑是长消息截断或多部分的问题。让我检查一下与消息截断相关的代码,包括 token 计数器、上下文压缩、以及消息处理相关的逻辑。\n</think>\n\n有可能!让我检查几个关键点:\n"
|
|
1004
|
+
},
|
|
1005
|
+
{
|
|
1006
|
+
"type": "tool-call",
|
|
1007
|
+
"toolCallId": "call_function_wu274o1x2tby_1",
|
|
1008
|
+
"toolName": "read",
|
|
1009
|
+
"input": {
|
|
1010
|
+
"path": "src/core/agent-chat.js",
|
|
1011
|
+
"lines": 50
|
|
1012
|
+
}
|
|
1013
|
+
},
|
|
1014
|
+
{
|
|
1015
|
+
"type": "tool-call",
|
|
1016
|
+
"toolCallId": "call_function_wu274o1x2tby_2",
|
|
1017
|
+
"toolName": "search_file",
|
|
1018
|
+
"input": {
|
|
1019
|
+
"pattern": "maxOutputTokens|8192",
|
|
1020
|
+
"path": "src",
|
|
1021
|
+
"contextLines": 2
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
]
|
|
1025
|
+
},
|
|
1026
|
+
{
|
|
1027
|
+
"role": "tool",
|
|
1028
|
+
"content": [
|
|
1029
|
+
{
|
|
1030
|
+
"type": "tool-result",
|
|
1031
|
+
"toolCallId": "call_function_wu274o1x2tby_1",
|
|
1032
|
+
"toolName": "read",
|
|
1033
|
+
"output": {
|
|
1034
|
+
"type": "json",
|
|
1035
|
+
"value": {
|
|
1036
|
+
"success": true,
|
|
1037
|
+
"data": "/**\n * AgentChatHandler 聊天处理器\n * 使用 AI SDK 的 ToolLoopAgent 处理工具调用循环\n *\n * 职责委托:\n * - ChatSession: 会话管理、消息历史、队列\n * - ToolExecutor: 工具发现、执行、验证\n * - ContextCompressor: 上下文压缩\n */\n\nconst { EventEmitter } = require('../utils/event-emitter');\nconst { logger } = require('../utils/logger');\nconst {\n tool: aiTool,\n ToolLoopAgent,\n isLoopFinished,\n} = require('ai');\nconst { autoSplitToolResult } = require('../../plugins/data-splitter-plugin');\nconst { cleanResponse } = require('../utils');\nconst { ChatQueueManager } = require('../utils/chat-queue');\nconst { TokenCounter } = require('./token-counter');\nconst { isThinkingModel } = require('./provider');\n// 新模块\nconst { ChatSession } = require('./chat-session');\nconst { ToolExecutor } = require('./tool-executor');\nconst { ContextCompressor } = require('./context-compressor');\nconst {\n DEFAULT_MAX_OUTPUT_TOKENS,\n DEFAULT_TEMPERATURE,\n DEFAULT_MAX_STEPS,\n DEFAULT_MAX_CONCURRENT,\n DEFAULT_RETRY_ATTEMPTS,\n DEFAULT_RETRY_DELAY_MS,\n} = require('./constants');\n\nclass AgentChatHandler extends EventEmitter {\n /**\n * @param {Agent} agent - Agent 实例\n * @param {Object} config - 配置\n */\n constructor(agent, config = {}) {\n super();\n\n this.agent = agent;\n this.config = config;\n\n // AI 配置\n this.model = config.model || 'deepseek-chat';\n this.provider = config.provider || 'deepseek';\n this.apiKey = config.apiKey;",
|
|
1038
|
+
"metadata": {
|
|
1039
|
+
"filePath": "D:\\Code\\foliko\\src\\core\\agent-chat.js",
|
|
1040
|
+
"size": 32568,
|
|
1041
|
+
"lines": null
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
},
|
|
1046
|
+
{
|
|
1047
|
+
"type": "tool-result",
|
|
1048
|
+
"toolCallId": "call_function_wu274o1x2tby_2",
|
|
1049
|
+
"toolName": "search_file",
|
|
1050
|
+
"output": {
|
|
1051
|
+
"type": "json",
|
|
1052
|
+
"value": {
|
|
1053
|
+
"success": true,
|
|
1054
|
+
"data": [],
|
|
1055
|
+
"metadata": {
|
|
1056
|
+
"pattern": "maxOutputTokens|8192",
|
|
1057
|
+
"total": 0,
|
|
1058
|
+
"stats": {
|
|
1059
|
+
"filesWithMatches": 0,
|
|
1060
|
+
"totalMatches": 0,
|
|
1061
|
+
"searchPath": "src"
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
]
|
|
1068
|
+
},
|
|
1069
|
+
{
|
|
1070
|
+
"role": "assistant",
|
|
1071
|
+
"content": [
|
|
1072
|
+
{
|
|
1073
|
+
"type": "text",
|
|
1074
|
+
"text": "<think>让我搜索 maxOutputTokens 相关的配置,可能在 constants.js 或者其他地方设置。\n</think>\n\n\n"
|
|
1075
|
+
},
|
|
1076
|
+
{
|
|
1077
|
+
"type": "tool-call",
|
|
1078
|
+
"toolCallId": "call_function_cxk21puqwjmv_1",
|
|
1079
|
+
"toolName": "bash",
|
|
1080
|
+
"input": {
|
|
1081
|
+
"cmd": "grep -rn \"maxOutputTokens\\|8192\" src/"
|
|
1082
|
+
}
|
|
1083
|
+
},
|
|
1084
|
+
{
|
|
1085
|
+
"type": "tool-call",
|
|
1086
|
+
"toolCallId": "call_function_cxk21puqwjmv_2",
|
|
1087
|
+
"toolName": "search_file",
|
|
1088
|
+
"input": {
|
|
1089
|
+
"pattern": "providerOptions",
|
|
1090
|
+
"path": "src",
|
|
1091
|
+
"contextLines": 2
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
]
|
|
1095
|
+
},
|
|
1096
|
+
{
|
|
1097
|
+
"role": "tool",
|
|
1098
|
+
"content": [
|
|
1099
|
+
{
|
|
1100
|
+
"type": "tool-result",
|
|
1101
|
+
"toolCallId": "call_function_cxk21puqwjmv_1",
|
|
1102
|
+
"toolName": "bash",
|
|
1103
|
+
"output": {
|
|
1104
|
+
"type": "json",
|
|
1105
|
+
"value": {
|
|
1106
|
+
"success": false,
|
|
1107
|
+
"error": "Command failed: \"C:\\Program Files\\Git\\bin\\bash.exe\" -c \"grep -rn \\\"maxOutputTokens\\\\|8192\\\" src/\"\n'8192\\\" src/\"' �����ڲ����ⲿ���Ҳ���ǿ����еij���\r\n���������ļ���\r\n",
|
|
1108
|
+
"metadata": {
|
|
1109
|
+
"command": "grep -rn \"maxOutputTokens\\|8192\" src/",
|
|
1110
|
+
"stderr": "'8192\\\" src/\"' �����ڲ����ⲿ���Ҳ���ǿ����еij���\r\n���������ļ���\r\n",
|
|
1111
|
+
"stdout": "",
|
|
1112
|
+
"duration": 33
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
},
|
|
1117
|
+
{
|
|
1118
|
+
"type": "tool-result",
|
|
1119
|
+
"toolCallId": "call_function_cxk21puqwjmv_2",
|
|
1120
|
+
"toolName": "search_file",
|
|
1121
|
+
"output": {
|
|
1122
|
+
"type": "json",
|
|
1123
|
+
"value": {
|
|
1124
|
+
"success": true,
|
|
1125
|
+
"data": [
|
|
1126
|
+
{
|
|
1127
|
+
"file": "src\\core\\agent-chat.js",
|
|
1128
|
+
"line": 52,
|
|
1129
|
+
"content": " this.providerOptions = config.providerOptions || {};",
|
|
1130
|
+
"matches": [
|
|
1131
|
+
{
|
|
1132
|
+
"column": 9,
|
|
1133
|
+
"length": 15,
|
|
1134
|
+
"text": "providerOptions"
|
|
1135
|
+
},
|
|
1136
|
+
{
|
|
1137
|
+
"column": 34,
|
|
1138
|
+
"length": 15,
|
|
1139
|
+
"text": "providerOptions"
|
|
1140
|
+
}
|
|
1141
|
+
],
|
|
1142
|
+
"context": [
|
|
1143
|
+
{
|
|
1144
|
+
"line": 50,
|
|
1145
|
+
"content": " this.apiKey = config.apiKey;"
|
|
1146
|
+
},
|
|
1147
|
+
{
|
|
1148
|
+
"line": 51,
|
|
1149
|
+
"content": " this.baseURL = config.baseURL;"
|
|
1150
|
+
},
|
|
1151
|
+
{
|
|
1152
|
+
"line": 52,
|
|
1153
|
+
"content": " this.providerOptions = config.providerOptions || {};"
|
|
1154
|
+
},
|
|
1155
|
+
{
|
|
1156
|
+
"line": 53,
|
|
1157
|
+
"content": " this.providerOptions.maxOutputTokens = config.providerOptions?.maxOutputTokens || 8192;"
|
|
1158
|
+
},
|
|
1159
|
+
{
|
|
1160
|
+
"line": 54,
|
|
1161
|
+
"content": ""
|
|
1162
|
+
}
|
|
1163
|
+
]
|
|
1164
|
+
},
|
|
1165
|
+
{
|
|
1166
|
+
"file": "src\\core\\agent-chat.js",
|
|
1167
|
+
"line": 53,
|
|
1168
|
+
"content": " this.providerOptions.maxOutputTokens = config.providerOptions?.maxOutputTokens || 8192;",
|
|
1169
|
+
"matches": [
|
|
1170
|
+
{
|
|
1171
|
+
"column": 9,
|
|
1172
|
+
"length": 15,
|
|
1173
|
+
"text": "providerOptions"
|
|
1174
|
+
},
|
|
1175
|
+
{
|
|
1176
|
+
"column": 50,
|
|
1177
|
+
"length": 15,
|
|
1178
|
+
"text": "providerOptions"
|
|
1179
|
+
}
|
|
1180
|
+
],
|
|
1181
|
+
"context": [
|
|
1182
|
+
{
|
|
1183
|
+
"line": 51,
|
|
1184
|
+
"content": " this.baseURL = config.baseURL;"
|
|
1185
|
+
},
|
|
1186
|
+
{
|
|
1187
|
+
"line": 52,
|
|
1188
|
+
"content": " this.providerOptions = config.providerOptions || {};"
|
|
1189
|
+
},
|
|
1190
|
+
{
|
|
1191
|
+
"line": 53,
|
|
1192
|
+
"content": " this.providerOptions.maxOutputTokens = config.providerOptions?.maxOutputTokens || 8192;"
|
|
1193
|
+
},
|
|
1194
|
+
{
|
|
1195
|
+
"line": 54,
|
|
1196
|
+
"content": ""
|
|
1197
|
+
},
|
|
1198
|
+
{
|
|
1199
|
+
"line": 55,
|
|
1200
|
+
"content": " // DeepSeek thinking mode: 支持通过 config.thinkingMode 控制开关"
|
|
1201
|
+
}
|
|
1202
|
+
]
|
|
1203
|
+
},
|
|
1204
|
+
{
|
|
1205
|
+
"file": "src\\core\\agent-chat.js",
|
|
1206
|
+
"line": 62,
|
|
1207
|
+
"content": " this.providerOptions.temperature = undefined;",
|
|
1208
|
+
"matches": [
|
|
1209
|
+
{
|
|
1210
|
+
"column": 11,
|
|
1211
|
+
"length": 15,
|
|
1212
|
+
"text": "providerOptions"
|
|
1213
|
+
}
|
|
1214
|
+
],
|
|
1215
|
+
"context": [
|
|
1216
|
+
{
|
|
1217
|
+
"line": 60,
|
|
1218
|
+
"content": " if (enableThinking) {"
|
|
1219
|
+
},
|
|
1220
|
+
{
|
|
1221
|
+
"line": 61,
|
|
1222
|
+
"content": " // thinking mode 模型不支持 temperature 参数"
|
|
1223
|
+
},
|
|
1224
|
+
{
|
|
1225
|
+
"line": 62,
|
|
1226
|
+
"content": " this.providerOptions.temperature = undefined;"
|
|
1227
|
+
},
|
|
1228
|
+
{
|
|
1229
|
+
"line": 63,
|
|
1230
|
+
"content": " // DeepSeek 需要通过 extra_body 启用 thinking mode"
|
|
1231
|
+
},
|
|
1232
|
+
{
|
|
1233
|
+
"line": 64,
|
|
1234
|
+
"content": " if (!this.providerOptions.deepseek) {"
|
|
1235
|
+
}
|
|
1236
|
+
]
|
|
1237
|
+
},
|
|
1238
|
+
{
|
|
1239
|
+
"file": "src\\core\\agent-chat.js",
|
|
1240
|
+
"line": 64,
|
|
1241
|
+
"content": " if (!this.providerOptions.deepseek) {",
|
|
1242
|
+
"matches": [
|
|
1243
|
+
{
|
|
1244
|
+
"column": 16,
|
|
1245
|
+
"length": 15,
|
|
1246
|
+
"text": "providerOptions"
|
|
1247
|
+
}
|
|
1248
|
+
],
|
|
1249
|
+
"context": [
|
|
1250
|
+
{
|
|
1251
|
+
"line": 62,
|
|
1252
|
+
"content": " this.providerOptions.temperature = undefined;"
|
|
1253
|
+
},
|
|
1254
|
+
{
|
|
1255
|
+
"line": 63,
|
|
1256
|
+
"content": " // DeepSeek 需要通过 extra_body 启用 thinking mode"
|
|
1257
|
+
},
|
|
1258
|
+
{
|
|
1259
|
+
"line": 64,
|
|
1260
|
+
"content": " if (!this.providerOptions.deepseek) {"
|
|
1261
|
+
},
|
|
1262
|
+
{
|
|
1263
|
+
"line": 65,
|
|
1264
|
+
"content": " this.providerOptions.deepseek = {};"
|
|
1265
|
+
},
|
|
1266
|
+
{
|
|
1267
|
+
"line": 66,
|
|
1268
|
+
"content": " }"
|
|
1269
|
+
}
|
|
1270
|
+
]
|
|
1271
|
+
},
|
|
1272
|
+
{
|
|
1273
|
+
"file": "src\\core\\agent-chat.js",
|
|
1274
|
+
"line": 65,
|
|
1275
|
+
"content": " this.providerOptions.deepseek = {};",
|
|
1276
|
+
"matches": [
|
|
1277
|
+
{
|
|
1278
|
+
"column": 13,
|
|
1279
|
+
"length": 15,
|
|
1280
|
+
"text": "providerOptions"
|
|
1281
|
+
}
|
|
1282
|
+
],
|
|
1283
|
+
"context": [
|
|
1284
|
+
{
|
|
1285
|
+
"line": 63,
|
|
1286
|
+
"content": " // DeepSeek 需要通过 extra_body 启用 thinking mode"
|
|
1287
|
+
},
|
|
1288
|
+
{
|
|
1289
|
+
"line": 64,
|
|
1290
|
+
"content": " if (!this.providerOptions.deepseek) {"
|
|
1291
|
+
},
|
|
1292
|
+
{
|
|
1293
|
+
"line": 65,
|
|
1294
|
+
"content": " this.providerOptions.deepseek = {};"
|
|
1295
|
+
},
|
|
1296
|
+
{
|
|
1297
|
+
"line": 66,
|
|
1298
|
+
"content": " }"
|
|
1299
|
+
},
|
|
1300
|
+
{
|
|
1301
|
+
"line": 67,
|
|
1302
|
+
"content": " this.providerOptions.deepseek.thinking = { type: 'enabled' };"
|
|
1303
|
+
}
|
|
1304
|
+
]
|
|
1305
|
+
},
|
|
1306
|
+
{
|
|
1307
|
+
"file": "src\\core\\agent-chat.js",
|
|
1308
|
+
"line": 67,
|
|
1309
|
+
"content": " this.providerOptions.deepseek.thinking = { type: 'enabled' };",
|
|
1310
|
+
"matches": [
|
|
1311
|
+
{
|
|
1312
|
+
"column": 11,
|
|
1313
|
+
"length": 15,
|
|
1314
|
+
"text": "providerOptions"
|
|
1315
|
+
}
|
|
1316
|
+
],
|
|
1317
|
+
"context": [
|
|
1318
|
+
{
|
|
1319
|
+
"line": 65,
|
|
1320
|
+
"content": " this.providerOptions.deepseek = {};"
|
|
1321
|
+
},
|
|
1322
|
+
{
|
|
1323
|
+
"line": 66,
|
|
1324
|
+
"content": " }"
|
|
1325
|
+
},
|
|
1326
|
+
{
|
|
1327
|
+
"line": 67,
|
|
1328
|
+
"content": " this.providerOptions.deepseek.thinking = { type: 'enabled' };"
|
|
1329
|
+
},
|
|
1330
|
+
{
|
|
1331
|
+
"line": 68,
|
|
1332
|
+
"content": " } else {"
|
|
1333
|
+
},
|
|
1334
|
+
{
|
|
1335
|
+
"line": 69,
|
|
1336
|
+
"content": " this.providerOptions.temperature = config.providerOptions?.temperature || 0.3;"
|
|
1337
|
+
}
|
|
1338
|
+
]
|
|
1339
|
+
},
|
|
1340
|
+
{
|
|
1341
|
+
"file": "src\\core\\agent-chat.js",
|
|
1342
|
+
"line": 69,
|
|
1343
|
+
"content": " this.providerOptions.temperature = config.providerOptions?.temperature || 0.3;",
|
|
1344
|
+
"matches": [
|
|
1345
|
+
{
|
|
1346
|
+
"column": 11,
|
|
1347
|
+
"length": 15,
|
|
1348
|
+
"text": "providerOptions"
|
|
1349
|
+
},
|
|
1350
|
+
{
|
|
1351
|
+
"column": 48,
|
|
1352
|
+
"length": 15,
|
|
1353
|
+
"text": "providerOptions"
|
|
1354
|
+
}
|
|
1355
|
+
],
|
|
1356
|
+
"context": [
|
|
1357
|
+
{
|
|
1358
|
+
"line": 67,
|
|
1359
|
+
"content": " this.providerOptions.deepseek.thinking = { type: 'enabled' };"
|
|
1360
|
+
},
|
|
1361
|
+
{
|
|
1362
|
+
"line": 68,
|
|
1363
|
+
"content": " } else {"
|
|
1364
|
+
},
|
|
1365
|
+
{
|
|
1366
|
+
"line": 69,
|
|
1367
|
+
"content": " this.providerOptions.temperature = config.providerOptions?.temperature || 0.3;"
|
|
1368
|
+
},
|
|
1369
|
+
{
|
|
1370
|
+
"line": 70,
|
|
1371
|
+
"content": " }"
|
|
1372
|
+
},
|
|
1373
|
+
{
|
|
1374
|
+
"line": 71,
|
|
1375
|
+
"content": ""
|
|
1376
|
+
}
|
|
1377
|
+
]
|
|
1378
|
+
},
|
|
1379
|
+
{
|
|
1380
|
+
"file": "src\\core\\agent-chat.js",
|
|
1381
|
+
"line": 495,
|
|
1382
|
+
"content": " return agent.stream({ messages, ...this.providerOptions, onError: handleSDKError });",
|
|
1383
|
+
"matches": [
|
|
1384
|
+
{
|
|
1385
|
+
"column": 48,
|
|
1386
|
+
"length": 15,
|
|
1387
|
+
"text": "providerOptions"
|
|
1388
|
+
}
|
|
1389
|
+
],
|
|
1390
|
+
"context": [
|
|
1391
|
+
{
|
|
1392
|
+
"line": 493,
|
|
1393
|
+
"content": ""
|
|
1394
|
+
},
|
|
1395
|
+
{
|
|
1396
|
+
"line": 494,
|
|
1397
|
+
"content": " const result = await framework.runInSession(sessionId, {}, async () => {"
|
|
1398
|
+
},
|
|
1399
|
+
{
|
|
1400
|
+
"line": 495,
|
|
1401
|
+
"content": " return agent.stream({ messages, ...this.providerOptions, onError: handleSDKError });"
|
|
1402
|
+
},
|
|
1403
|
+
{
|
|
1404
|
+
"line": 496,
|
|
1405
|
+
"content": " });"
|
|
1406
|
+
},
|
|
1407
|
+
{
|
|
1408
|
+
"line": 497,
|
|
1409
|
+
"content": ""
|
|
1410
|
+
}
|
|
1411
|
+
]
|
|
1412
|
+
},
|
|
1413
|
+
{
|
|
1414
|
+
"file": "src\\core\\agent-chat.js",
|
|
1415
|
+
"line": 642,
|
|
1416
|
+
"content": " const apiOptions = { ...this.providerOptions };",
|
|
1417
|
+
"matches": [
|
|
1418
|
+
{
|
|
1419
|
+
"column": 35,
|
|
1420
|
+
"length": 15,
|
|
1421
|
+
"text": "providerOptions"
|
|
1422
|
+
}
|
|
1423
|
+
],
|
|
1424
|
+
"context": [
|
|
1425
|
+
{
|
|
1426
|
+
"line": 640,
|
|
1427
|
+
"content": ""
|
|
1428
|
+
},
|
|
1429
|
+
{
|
|
1430
|
+
"line": 641,
|
|
1431
|
+
"content": " // DeepSeek thinking mode: 处理参数"
|
|
1432
|
+
},
|
|
1433
|
+
{
|
|
1434
|
+
"line": 642,
|
|
1435
|
+
"content": " const apiOptions = { ...this.providerOptions };"
|
|
1436
|
+
},
|
|
1437
|
+
{
|
|
1438
|
+
"line": 643,
|
|
1439
|
+
"content": " if (this._thinkingMode) {"
|
|
1440
|
+
},
|
|
1441
|
+
{
|
|
1442
|
+
"line": 644,
|
|
1443
|
+
"content": " delete apiOptions.temperature;"
|
|
1444
|
+
}
|
|
1445
|
+
]
|
|
1446
|
+
},
|
|
1447
|
+
{
|
|
1448
|
+
"file": "src\\core\\agent.js",
|
|
1449
|
+
"line": 60,
|
|
1450
|
+
"content": " this.providerOptions = {",
|
|
1451
|
+
"matches": [
|
|
1452
|
+
{
|
|
1453
|
+
"column": 9,
|
|
1454
|
+
"length": 15,
|
|
1455
|
+
"text": "providerOptions"
|
|
1456
|
+
}
|
|
1457
|
+
],
|
|
1458
|
+
"context": [
|
|
1459
|
+
{
|
|
1460
|
+
"line": 58,
|
|
1461
|
+
"content": " this.baseURL = config.baseURL;"
|
|
1462
|
+
},
|
|
1463
|
+
{
|
|
1464
|
+
"line": 59,
|
|
1465
|
+
"content": " this.provider = config.provider || 'deepseek';"
|
|
1466
|
+
},
|
|
1467
|
+
{
|
|
1468
|
+
"line": 60,
|
|
1469
|
+
"content": " this.providerOptions = {"
|
|
1470
|
+
},
|
|
1471
|
+
{
|
|
1472
|
+
"line": 61,
|
|
1473
|
+
"content": " ...config.providerOptions,"
|
|
1474
|
+
},
|
|
1475
|
+
{
|
|
1476
|
+
"line": 62,
|
|
1477
|
+
"content": " maxOutputTokens: config.providerOptions?.maxOutputTokens ?? DEFAULT_MAX_OUTPUT_TOKENS,"
|
|
1478
|
+
}
|
|
1479
|
+
]
|
|
1480
|
+
},
|
|
1481
|
+
{
|
|
1482
|
+
"file": "src\\core\\agent.js",
|
|
1483
|
+
"line": 61,
|
|
1484
|
+
"content": " ...config.providerOptions,",
|
|
1485
|
+
"matches": [
|
|
1486
|
+
{
|
|
1487
|
+
"column": 16,
|
|
1488
|
+
"length": 15,
|
|
1489
|
+
"text": "providerOptions"
|
|
1490
|
+
}
|
|
1491
|
+
],
|
|
1492
|
+
"context": [
|
|
1493
|
+
{
|
|
1494
|
+
"line": 59,
|
|
1495
|
+
"content": " this.provider = config.provider || 'deepseek';"
|
|
1496
|
+
},
|
|
1497
|
+
{
|
|
1498
|
+
"line": 60,
|
|
1499
|
+
"content": " this.providerOptions = {"
|
|
1500
|
+
},
|
|
1501
|
+
{
|
|
1502
|
+
"line": 61,
|
|
1503
|
+
"content": " ...config.providerOptions,"
|
|
1504
|
+
},
|
|
1505
|
+
{
|
|
1506
|
+
"line": 62,
|
|
1507
|
+
"content": " maxOutputTokens: config.providerOptions?.maxOutputTokens ?? DEFAULT_MAX_OUTPUT_TOKENS,"
|
|
1508
|
+
},
|
|
1509
|
+
{
|
|
1510
|
+
"line": 63,
|
|
1511
|
+
"content": " temperature: config.providerOptions?.temperature ?? DEFAULT_TEMPERATURE,"
|
|
1512
|
+
}
|
|
1513
|
+
]
|
|
1514
|
+
},
|
|
1515
|
+
{
|
|
1516
|
+
"file": "src\\core\\agent.js",
|
|
1517
|
+
"line": 62,
|
|
1518
|
+
"content": " maxOutputTokens: config.providerOptions?.maxOutputTokens ?? DEFAULT_MAX_OUTPUT_TOKENS,",
|
|
1519
|
+
"matches": [
|
|
1520
|
+
{
|
|
1521
|
+
"column": 30,
|
|
1522
|
+
"length": 15,
|
|
1523
|
+
"text": "providerOptions"
|
|
1524
|
+
}
|
|
1525
|
+
],
|
|
1526
|
+
"context": [
|
|
1527
|
+
{
|
|
1528
|
+
"line": 60,
|
|
1529
|
+
"content": " this.providerOptions = {"
|
|
1530
|
+
},
|
|
1531
|
+
{
|
|
1532
|
+
"line": 61,
|
|
1533
|
+
"content": " ...config.providerOptions,"
|
|
1534
|
+
},
|
|
1535
|
+
{
|
|
1536
|
+
"line": 62,
|
|
1537
|
+
"content": " maxOutputTokens: config.providerOptions?.maxOutputTokens ?? DEFAULT_MAX_OUTPUT_TOKENS,"
|
|
1538
|
+
},
|
|
1539
|
+
{
|
|
1540
|
+
"line": 63,
|
|
1541
|
+
"content": " temperature: config.providerOptions?.temperature ?? DEFAULT_TEMPERATURE,"
|
|
1542
|
+
},
|
|
1543
|
+
{
|
|
1544
|
+
"line": 64,
|
|
1545
|
+
"content": " };"
|
|
1546
|
+
}
|
|
1547
|
+
]
|
|
1548
|
+
},
|
|
1549
|
+
{
|
|
1550
|
+
"file": "src\\core\\agent.js",
|
|
1551
|
+
"line": 63,
|
|
1552
|
+
"content": " temperature: config.providerOptions?.temperature ?? DEFAULT_TEMPERATURE,",
|
|
1553
|
+
"matches": [
|
|
1554
|
+
{
|
|
1555
|
+
"column": 26,
|
|
1556
|
+
"length": 15,
|
|
1557
|
+
"text": "providerOptions"
|
|
1558
|
+
}
|
|
1559
|
+
],
|
|
1560
|
+
"context": [
|
|
1561
|
+
{
|
|
1562
|
+
"line": 61,
|
|
1563
|
+
"content": " ...config.providerOptions,"
|
|
1564
|
+
},
|
|
1565
|
+
{
|
|
1566
|
+
"line": 62,
|
|
1567
|
+
"content": " maxOutputTokens: config.providerOptions?.maxOutputTokens ?? DEFAULT_MAX_OUTPUT_TOKENS,"
|
|
1568
|
+
},
|
|
1569
|
+
{
|
|
1570
|
+
"line": 63,
|
|
1571
|
+
"content": " temperature: config.providerOptions?.temperature ?? DEFAULT_TEMPERATURE,"
|
|
1572
|
+
},
|
|
1573
|
+
{
|
|
1574
|
+
"line": 64,
|
|
1575
|
+
"content": " };"
|
|
1576
|
+
},
|
|
1577
|
+
{
|
|
1578
|
+
"line": 65,
|
|
1579
|
+
"content": " // 原始 system prompt"
|
|
1580
|
+
}
|
|
1581
|
+
]
|
|
1582
|
+
},
|
|
1583
|
+
{
|
|
1584
|
+
"file": "src\\core\\agent.js",
|
|
1585
|
+
"line": 402,
|
|
1586
|
+
"content": " providerOptions: this.providerOptions,",
|
|
1587
|
+
"matches": [
|
|
1588
|
+
{
|
|
1589
|
+
"column": 6,
|
|
1590
|
+
"length": 15,
|
|
1591
|
+
"text": "providerOptions"
|
|
1592
|
+
},
|
|
1593
|
+
{
|
|
1594
|
+
"column": 28,
|
|
1595
|
+
"length": 15,
|
|
1596
|
+
"text": "providerOptions"
|
|
1597
|
+
}
|
|
1598
|
+
],
|
|
1599
|
+
"context": [
|
|
1600
|
+
{
|
|
1601
|
+
"line": 400,
|
|
1602
|
+
"content": " apiKey: this.apiKey,"
|
|
1603
|
+
},
|
|
1604
|
+
{
|
|
1605
|
+
"line": 401,
|
|
1606
|
+
"content": " baseURL: this.baseURL,"
|
|
1607
|
+
},
|
|
1608
|
+
{
|
|
1609
|
+
"line": 402,
|
|
1610
|
+
"content": " providerOptions: this.providerOptions,"
|
|
1611
|
+
},
|
|
1612
|
+
{
|
|
1613
|
+
"line": 403,
|
|
1614
|
+
"content": " // 上下文压缩配置"
|
|
1615
|
+
},
|
|
1616
|
+
{
|
|
1617
|
+
"line": 404,
|
|
1618
|
+
"content": " maxContextTokens: this.config.maxContextTokens,"
|
|
1619
|
+
}
|
|
1620
|
+
]
|
|
1621
|
+
},
|
|
1622
|
+
{
|
|
1623
|
+
"file": "src\\core\\framework.js",
|
|
1624
|
+
"line": 226,
|
|
1625
|
+
"content": " providerOptions: aiPlugin.config.providerOptions || {},",
|
|
1626
|
+
"matches": [
|
|
1627
|
+
{
|
|
1628
|
+
"column": 6,
|
|
1629
|
+
"length": 15,
|
|
1630
|
+
"text": "providerOptions"
|
|
1631
|
+
},
|
|
1632
|
+
{
|
|
1633
|
+
"column": 39,
|
|
1634
|
+
"length": 15,
|
|
1635
|
+
"text": "providerOptions"
|
|
1636
|
+
}
|
|
1637
|
+
],
|
|
1638
|
+
"context": [
|
|
1639
|
+
{
|
|
1640
|
+
"line": 224,
|
|
1641
|
+
"content": " model: aiPlugin.config.model,"
|
|
1642
|
+
},
|
|
1643
|
+
{
|
|
1644
|
+
"line": 225,
|
|
1645
|
+
"content": " baseURL: aiPlugin.config.baseURL,"
|
|
1646
|
+
},
|
|
1647
|
+
{
|
|
1648
|
+
"line": 226,
|
|
1649
|
+
"content": " providerOptions: aiPlugin.config.providerOptions || {},"
|
|
1650
|
+
},
|
|
1651
|
+
{
|
|
1652
|
+
"line": 227,
|
|
1653
|
+
"content": " };"
|
|
1654
|
+
},
|
|
1655
|
+
{
|
|
1656
|
+
"line": 228,
|
|
1657
|
+
"content": ""
|
|
1658
|
+
}
|
|
1659
|
+
]
|
|
1660
|
+
},
|
|
1661
|
+
{
|
|
1662
|
+
"file": "src\\core\\framework.js",
|
|
1663
|
+
"line": 235,
|
|
1664
|
+
"content": " providerOptions: target.providerOptions || defaults.providerOptions,",
|
|
1665
|
+
"matches": [
|
|
1666
|
+
{
|
|
1667
|
+
"column": 6,
|
|
1668
|
+
"length": 15,
|
|
1669
|
+
"text": "providerOptions"
|
|
1670
|
+
},
|
|
1671
|
+
{
|
|
1672
|
+
"column": 30,
|
|
1673
|
+
"length": 15,
|
|
1674
|
+
"text": "providerOptions"
|
|
1675
|
+
},
|
|
1676
|
+
{
|
|
1677
|
+
"column": 58,
|
|
1678
|
+
"length": 15,
|
|
1679
|
+
"text": "providerOptions"
|
|
1680
|
+
}
|
|
1681
|
+
],
|
|
1682
|
+
"context": [
|
|
1683
|
+
{
|
|
1684
|
+
"line": 233,
|
|
1685
|
+
"content": " model: target.model || defaults.model,"
|
|
1686
|
+
},
|
|
1687
|
+
{
|
|
1688
|
+
"line": 234,
|
|
1689
|
+
"content": " baseURL: target.baseURL || defaults.baseURL,"
|
|
1690
|
+
},
|
|
1691
|
+
{
|
|
1692
|
+
"line": 235,
|
|
1693
|
+
"content": " providerOptions: target.providerOptions || defaults.providerOptions,"
|
|
1694
|
+
},
|
|
1695
|
+
{
|
|
1696
|
+
"line": 236,
|
|
1697
|
+
"content": " };"
|
|
1698
|
+
},
|
|
1699
|
+
{
|
|
1700
|
+
"line": 237,
|
|
1701
|
+
"content": " }"
|
|
1702
|
+
}
|
|
1703
|
+
]
|
|
1704
|
+
},
|
|
1705
|
+
{
|
|
1706
|
+
"file": "src\\core\\framework.js",
|
|
1707
|
+
"line": 819,
|
|
1708
|
+
"content": " providerOptions: {",
|
|
1709
|
+
"matches": [
|
|
1710
|
+
{
|
|
1711
|
+
"column": 6,
|
|
1712
|
+
"length": 15,
|
|
1713
|
+
"text": "providerOptions"
|
|
1714
|
+
}
|
|
1715
|
+
],
|
|
1716
|
+
"context": [
|
|
1717
|
+
{
|
|
1718
|
+
"line": 817,
|
|
1719
|
+
"content": " apiKey: mergedAIConfig.apiKey,"
|
|
1720
|
+
},
|
|
1721
|
+
{
|
|
1722
|
+
"line": 818,
|
|
1723
|
+
"content": " baseURL: mergedAIConfig.baseURL,"
|
|
1724
|
+
},
|
|
1725
|
+
{
|
|
1726
|
+
"line": 819,
|
|
1727
|
+
"content": " providerOptions: {"
|
|
1728
|
+
},
|
|
1729
|
+
{
|
|
1730
|
+
"line": 820,
|
|
1731
|
+
"content": " maxOutputTokens: mergedAIConfig.maxOutputTokens || 8192,"
|
|
1732
|
+
},
|
|
1733
|
+
{
|
|
1734
|
+
"line": 821,
|
|
1735
|
+
"content": " temperature: mergedAIConfig.temperature || 0.3,"
|
|
1736
|
+
}
|
|
1737
|
+
]
|
|
1738
|
+
},
|
|
1739
|
+
{
|
|
1740
|
+
"file": "src\\core\\subagent.js",
|
|
1741
|
+
"line": 26,
|
|
1742
|
+
"content": " * @param {Object} [config.providerOptions] - 提供者选项",
|
|
1743
|
+
"matches": [
|
|
1744
|
+
{
|
|
1745
|
+
"column": 29,
|
|
1746
|
+
"length": 15,
|
|
1747
|
+
"text": "providerOptions"
|
|
1748
|
+
}
|
|
1749
|
+
],
|
|
1750
|
+
"context": [
|
|
1751
|
+
{
|
|
1752
|
+
"line": 24,
|
|
1753
|
+
"content": " * @param {string} [config.apiKey] - API 密钥"
|
|
1754
|
+
},
|
|
1755
|
+
{
|
|
1756
|
+
"line": 25,
|
|
1757
|
+
"content": " * @param {string} [config.baseURL] - base URL"
|
|
1758
|
+
},
|
|
1759
|
+
{
|
|
1760
|
+
"line": 26,
|
|
1761
|
+
"content": " * @param {Object} [config.providerOptions] - 提供者选项"
|
|
1762
|
+
},
|
|
1763
|
+
{
|
|
1764
|
+
"line": 27,
|
|
1765
|
+
"content": " * @param {Array|Object} [config.tools] - 工具定义"
|
|
1766
|
+
},
|
|
1767
|
+
{
|
|
1768
|
+
"line": 28,
|
|
1769
|
+
"content": " * @param {Object} [config.framework] - Framework 引用(用于动态构建系统提示词)"
|
|
1770
|
+
}
|
|
1771
|
+
]
|
|
1772
|
+
},
|
|
1773
|
+
{
|
|
1774
|
+
"file": "src\\core\\subagent.js",
|
|
1775
|
+
"line": 40,
|
|
1776
|
+
"content": " this.providerOptions = config.providerOptions || {};",
|
|
1777
|
+
"matches": [
|
|
1778
|
+
{
|
|
1779
|
+
"column": 9,
|
|
1780
|
+
"length": 15,
|
|
1781
|
+
"text": "providerOptions"
|
|
1782
|
+
},
|
|
1783
|
+
{
|
|
1784
|
+
"column": 34,
|
|
1785
|
+
"length": 15,
|
|
1786
|
+
"text": "providerOptions"
|
|
1787
|
+
}
|
|
1788
|
+
],
|
|
1789
|
+
"context": [
|
|
1790
|
+
{
|
|
1791
|
+
"line": 38,
|
|
1792
|
+
"content": " this.apiKey = config.apiKey;"
|
|
1793
|
+
},
|
|
1794
|
+
{
|
|
1795
|
+
"line": 39,
|
|
1796
|
+
"content": " this.baseURL = config.baseURL;"
|
|
1797
|
+
},
|
|
1798
|
+
{
|
|
1799
|
+
"line": 40,
|
|
1800
|
+
"content": " this.providerOptions = config.providerOptions || {};"
|
|
1801
|
+
},
|
|
1802
|
+
{
|
|
1803
|
+
"line": 41,
|
|
1804
|
+
"content": " this.framework = config.framework || null;"
|
|
1805
|
+
},
|
|
1806
|
+
{
|
|
1807
|
+
"line": 42,
|
|
1808
|
+
"content": ""
|
|
1809
|
+
}
|
|
1810
|
+
]
|
|
1811
|
+
},
|
|
1812
|
+
{
|
|
1813
|
+
"file": "src\\core\\subagent.js",
|
|
1814
|
+
"line": 71,
|
|
1815
|
+
"content": " this.providerOptions.deepseek = this.providerOptions.deepseek || {};",
|
|
1816
|
+
"matches": [
|
|
1817
|
+
{
|
|
1818
|
+
"column": 9,
|
|
1819
|
+
"length": 15,
|
|
1820
|
+
"text": "providerOptions"
|
|
1821
|
+
},
|
|
1822
|
+
{
|
|
1823
|
+
"column": 41,
|
|
1824
|
+
"length": 15,
|
|
1825
|
+
"text": "providerOptions"
|
|
1826
|
+
}
|
|
1827
|
+
],
|
|
1828
|
+
"context": [
|
|
1829
|
+
{
|
|
1830
|
+
"line": 69,
|
|
1831
|
+
"content": " this._isSubagent = true;"
|
|
1832
|
+
},
|
|
1833
|
+
{
|
|
1834
|
+
"line": 70,
|
|
1835
|
+
"content": ""
|
|
1836
|
+
},
|
|
1837
|
+
{
|
|
1838
|
+
"line": 71,
|
|
1839
|
+
"content": " this.providerOptions.deepseek = this.providerOptions.deepseek || {};"
|
|
1840
|
+
},
|
|
1841
|
+
{
|
|
1842
|
+
"line": 72,
|
|
1843
|
+
"content": " this.providerOptions.deepseek.thinking={ type: 'disabled' }"
|
|
1844
|
+
},
|
|
1845
|
+
{
|
|
1846
|
+
"line": 73,
|
|
1847
|
+
"content": ""
|
|
1848
|
+
}
|
|
1849
|
+
]
|
|
1850
|
+
},
|
|
1851
|
+
{
|
|
1852
|
+
"file": "src\\core\\subagent.js",
|
|
1853
|
+
"line": 72,
|
|
1854
|
+
"content": " this.providerOptions.deepseek.thinking={ type: 'disabled' }",
|
|
1855
|
+
"matches": [
|
|
1856
|
+
{
|
|
1857
|
+
"column": 9,
|
|
1858
|
+
"length": 15,
|
|
1859
|
+
"text": "providerOptions"
|
|
1860
|
+
}
|
|
1861
|
+
],
|
|
1862
|
+
"context": [
|
|
1863
|
+
{
|
|
1864
|
+
"line": 70,
|
|
1865
|
+
"content": ""
|
|
1866
|
+
},
|
|
1867
|
+
{
|
|
1868
|
+
"line": 71,
|
|
1869
|
+
"content": " this.providerOptions.deepseek = this.providerOptions.deepseek || {};"
|
|
1870
|
+
},
|
|
1871
|
+
{
|
|
1872
|
+
"line": 72,
|
|
1873
|
+
"content": " this.providerOptions.deepseek.thinking={ type: 'disabled' }"
|
|
1874
|
+
},
|
|
1875
|
+
{
|
|
1876
|
+
"line": 73,
|
|
1877
|
+
"content": ""
|
|
1878
|
+
},
|
|
1879
|
+
{
|
|
1880
|
+
"line": 74,
|
|
1881
|
+
"content": " this.framework.once('framework:ready', () => {"
|
|
1882
|
+
}
|
|
1883
|
+
]
|
|
1884
|
+
},
|
|
1885
|
+
{
|
|
1886
|
+
"file": "src\\core\\subagent.js",
|
|
1887
|
+
"line": 347,
|
|
1888
|
+
"content": " ...this.providerOptions,",
|
|
1889
|
+
"matches": [
|
|
1890
|
+
{
|
|
1891
|
+
"column": 18,
|
|
1892
|
+
"length": 15,
|
|
1893
|
+
"text": "providerOptions"
|
|
1894
|
+
}
|
|
1895
|
+
],
|
|
1896
|
+
"context": [
|
|
1897
|
+
{
|
|
1898
|
+
"line": 345,
|
|
1899
|
+
"content": " const result = await agent.generate({"
|
|
1900
|
+
},
|
|
1901
|
+
{
|
|
1902
|
+
"line": 346,
|
|
1903
|
+
"content": " messages,"
|
|
1904
|
+
},
|
|
1905
|
+
{
|
|
1906
|
+
"line": 347,
|
|
1907
|
+
"content": " ...this.providerOptions,"
|
|
1908
|
+
},
|
|
1909
|
+
{
|
|
1910
|
+
"line": 348,
|
|
1911
|
+
"content": " abortSignal: options.signal,"
|
|
1912
|
+
},
|
|
1913
|
+
{
|
|
1914
|
+
"line": 349,
|
|
1915
|
+
"content": " });"
|
|
1916
|
+
}
|
|
1917
|
+
]
|
|
1918
|
+
}
|
|
1919
|
+
],
|
|
1920
|
+
"metadata": {
|
|
1921
|
+
"pattern": "providerOptions",
|
|
1922
|
+
"total": 22,
|
|
1923
|
+
"stats": {
|
|
1924
|
+
"filesWithMatches": 4,
|
|
1925
|
+
"totalMatches": 31,
|
|
1926
|
+
"searchPath": "src"
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
]
|
|
1933
|
+
},
|
|
1934
|
+
{
|
|
1935
|
+
"role": "assistant",
|
|
1936
|
+
"content": [
|
|
1937
|
+
{
|
|
1938
|
+
"type": "text",
|
|
1939
|
+
"text": "<think>Now I can see the issue! Looking at line 53:\n\n```javascript\nthis.providerOptions.maxOutputTokens = config.providerOptions?.maxOutputTokens || 8192;\n```\n\nAnd in `framework.js` line 820:\n```javascript\nmaxOutputTokens: mergedAIConfig.maxOutputTokens || 8192,\n```\n\nSo `maxOutputTokens` defaults to 8192 tokens. This is passed to the AI API via `...this.providerOptions`. This limits the maximum output from the AI model.\n\nFor DeepSeek models:\n- `deepseek-chat`: 8K context, output limit might be around 4K-8K\n- `deepseek-v4-flash`: Has 800K context window, but output might still be limited\n\nThe issue is that when `maxOutputTokens` is too small, the AI API will stop generating after reaching that limit, causing truncation.\n\nBut wait, the user is asking about \"长消息截断多部分的问题\" - which could be about multi-part message truncation. Let me look at how messages are stored and whether there's a size limit somewhere.\n\nLet me also check if there's any message size limit in the session or message store.\n</think>\n\n\n"
|
|
1940
|
+
},
|
|
1941
|
+
{
|
|
1942
|
+
"type": "tool-call",
|
|
1943
|
+
"toolCallId": "call_function_zxmhn4uudmuw_1",
|
|
1944
|
+
"toolName": "read",
|
|
1945
|
+
"input": {
|
|
1946
|
+
"path": "src/core/chat-session.js"
|
|
1947
|
+
}
|
|
1948
|
+
},
|
|
1949
|
+
{
|
|
1950
|
+
"type": "tool-call",
|
|
1951
|
+
"toolCallId": "call_function_zxmhn4uudmuw_2",
|
|
1952
|
+
"toolName": "search_file",
|
|
1953
|
+
"input": {
|
|
1954
|
+
"pattern": "MAX.*SIZE|limit.*message|message.*limit",
|
|
1955
|
+
"path": "src",
|
|
1956
|
+
"contextLines": 2
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
]
|
|
1960
|
+
},
|
|
1961
|
+
{
|
|
1962
|
+
"role": "tool",
|
|
1963
|
+
"content": [
|
|
1964
|
+
{
|
|
1965
|
+
"type": "tool-result",
|
|
1966
|
+
"toolCallId": "call_function_zxmhn4uudmuw_1",
|
|
1967
|
+
"toolName": "read",
|
|
1968
|
+
"output": {
|
|
1969
|
+
"type": "json",
|
|
1970
|
+
"value": {
|
|
1971
|
+
"success": true,
|
|
1972
|
+
"data": "/**\n * ChatSession - 会话管理器\n *\n * 职责:\n * 1. 管理 Per-Session 的消息存储\n * 2. 会话消息历史加载/保存\n * 3. 会话消息队列管理\n * 4. SessionScope 事件作用域\n */\n\nconst { EventEmitter } = require('../utils/event-emitter');\nconst { logger } = require('../utils/logger');\n\n/**\n * Session 作用域的事件监听器\n * 自动过滤只属于特定 sessionId 的事件\n */\nclass SessionScope extends EventEmitter {\n /**\n * @param {ChatSession} session - ChatSession 实例\n * @param {string} sessionId - Session ID\n */\n constructor(session, sessionId) {\n super();\n this.session = session;\n this.sessionId = sessionId;\n this._handlers = new Set();\n }\n\n /**\n * 注册事件监听器(自动过滤 sessionId)\n * @param {string} event - 事件名\n * @param {Function} handler - 处理函数\n * @returns {this}\n */\n on(event, handler) {\n const wrapped = (data) => {\n if (data && data.sessionId !== this.sessionId) return;\n handler(data);\n };\n this._handlers.add({ event, wrapped });\n this.session.on(event, wrapped);\n return this;\n }\n\n /**\n * 注册一次性事件监听器\n */\n once(event, handler) {\n const wrapped = (data) => {\n if (data && data.sessionId !== this.sessionId) return;\n handler(data);\n };\n this._handlers.add({ event, wrapped });\n this.session.once(event, wrapped);\n return this;\n }\n\n /**\n * 移除事件监听器\n */\n off(event, handler) {\n const toRemove = [];\n for (const item of this._handlers) {\n if (item.event === event && item.handler === handler) {\n this.session.off(event, item.wrapped);\n toRemove.push(item);\n }\n }\n for (const item of toRemove) {\n this._handlers.delete(item);\n }\n return this;\n }\n\n /**\n * 移除所有这个 scope 注册的监听器\n */\n removeAllListeners() {\n for (const { event, wrapped } of this._handlers) {\n this.session.off(event, wrapped);\n }\n this._handlers.clear();\n return this;\n }\n\n /**\n * 获取当前 sessionId\n */\n getSessionId() {\n return this.sessionId;\n }\n}\n\nclass ChatSession extends EventEmitter {\n /**\n * @param {Object} config - 配置\n * @param {Function} [config.messageProcessor] - 消息处理器函数 (sessionId, message, options) => Promise\n */\n constructor(config = {}) {\n super();\n\n this.config = config;\n this.agent = config.agent;\n\n // 消息处理器\n this._messageProcessor = config.messageProcessor;\n\n // 消息存储限制,防止内存溢出\n this._maxMessagesPerSession = config.maxMessagesPerSession || 1000;\n\n // Session 消息存储 Map: sessionId -> { messages: [], historyLoaded: false, compressionState: {} }\n this._sessionMessageStores = new Map();\n\n // Session 事件作用域 Map: sessionId -> Set<{event, handler}>\n this._sessionScopes = new Map();\n }\n\n /**\n * 设置消息处理器\n * @param {Function} processor - (sessionId, message, options) => Promise\n */\n setMessageProcessor(processor) {\n this._messageProcessor = processor;\n }\n\n\n\n /**\n * 获取或创建 SessionScope\n * @param {string} sessionId - Session ID\n * @returns {SessionScope}\n */\n createSessionScope(sessionId) {\n const scope = new SessionScope(this, sessionId);\n\n // 转发 ChatSession 的事件到 SessionScope(自动过滤 sessionId)\n const events = [\n 'queue:added',\n 'queue:processing',\n 'queue:completed',\n 'queue:failed',\n 'queue:retry',\n 'queue:empty',\n 'queue:cleared',\n 'stream:chunk',\n 'message:start',\n 'message:complete',\n 'message:error',\n ];\n\n events.forEach((eventName) => {\n this.on(eventName, (data) => {\n if (data.sessionId === sessionId || !data.sessionId) {\n scope.emit(eventName, data);\n }\n });\n });\n\n return scope;\n }\n\n /**\n * 获取会话消息存储\n * @param {string} sessionId - Session ID\n * @returns {Object} messageStore\n */\n getSessionMessageStore(sessionId) {\n if (!sessionId) {\n sessionId = 'default';\n }\n\n if (!this._sessionMessageStores.has(sessionId)) {\n const store = {\n messages: [],\n historyLoaded: false,\n usage: null,\n compressionState: {\n count: 0,\n lastCompressedAt: null,\n lastTokenCount: 0,\n pendingCompression: false,\n },\n save: () => {\n this.saveHistory(sessionId, store.messages);\n return Promise.resolve();\n },\n };\n this._sessionMessageStores.set(sessionId, store);\n }\n\n return this._sessionMessageStores.get(sessionId);\n }\n\n /**\n * 加载会话历史\n * @param {string} sessionId - Session ID\n * @returns {Array} 消息数组\n */\n loadHistory(sessionId) {\n if (!sessionId) return [];\n\n const messageStore = this.getSessionMessageStore(sessionId);\n if (messageStore.historyLoaded) {\n return messageStore.messages;\n }\n\n // 确保 SessionContext 已初始化(会从文件加载或创建新的)\n if (this.agent?.framework) {\n // 先尝试获取已存在的 SessionContext\n let sessionCtx = this.agent.framework._sessionContexts?.get(sessionId);\n // 如果不存在,调用 getOrCreateSessionContext 来加载或创建\n if (!sessionCtx) {\n sessionCtx = this.agent.framework.getOrCreateSessionContext(sessionId);\n }\n if (sessionCtx) {\n const messages = sessionCtx.getMessages();\n if (messages && messages.length > 0) {\n messageStore.messages = messages;\n messageStore.historyLoaded = true;\n logger.debug(`Loaded ${messages.length} messages for session ${sessionId}`);\n }\n }\n }\n\n // 初始化消息 token 计数缓存(用于 _prepareSession 跳过重复计算)\n if (this.agent?._chatHandler) {\n const chatHandler = this.agent._chatHandler;\n const cached = chatHandler._countMessagesTokens(messageStore.messages);\n messageStore._messageTokensCache = cached;\n messageStore._cacheMessageCount = messageStore.messages.length;\n }\n\n return messageStore.messages;\n }\n\n /**\n * 保存会话历史\n * @param {string} sessionId - Session ID\n * @param {Array} messages - 消息数组\n */\n saveHistory(sessionId, messages) {\n if (!sessionId || !messages) return;\n\n const messageStore = this.getSessionMessageStore(sessionId);\n\n // 保存到 SessionContext\n if (this.agent?.framework) {\n const sessionCtx = this.agent.framework._sessionContexts?.get(sessionId);\n if (sessionCtx) {\n sessionCtx.replaceMessages(messages);\n }\n }\n\n // 同步 token 计数缓存,下次 _prepareSession 跳过全量重算\n if (this.agent?._chatHandler) {\n this.agent._chatHandler._syncMessageTokensCache(messageStore);\n }\n\n messageStore.historyLoaded = true;\n }\n\n /**\n * 添加消息到会话\n * @param {string} sessionId - Session ID\n * @param {Object} message - 消息\n */\n addMessage(sessionId, message) {\n const store = this.getSessionMessageStore(sessionId);\n store.messages.push(message);\n // 限制消息数量,防止内存溢出\n if (store.messages.length > this._maxMessagesPerSession) {\n store.messages = store.messages.slice(-this._maxMessagesPerSession);\n }\n }\n\n /**\n * 入队消息(直接调用消息处理器,排队由上层 ChatQueueManager 处理)\n * @param {string} sessionId - Session ID\n * @param {Object} message - 消息\n * @param {Object} options - 选项\n * @returns {Promise}\n */\n enqueue(sessionId, message, options = {}) {\n return this._processMessageDirect(sessionId, message, options);\n }\n\n /**\n * 直接处理消息(不经过额外的队列)\n * @private\n */\n async _processMessageDirect(sessionId, message, options) {\n if (this._messageProcessor) {\n return this._messageProcessor(sessionId, message, options);\n }\n if (this.agent?._chatHandler) {\n return this.agent._chatHandler._processMessage(sessionId, message, options);\n }\n throw new Error('No message processor set');\n }\n\n /**\n * 取消会话队列(委托给 ChatQueueManager)\n * @param {string} sessionId - Session ID\n */\n cancelSession(sessionId) {\n this.emit('queue:session-removed', { sessionId });\n }\n\n /**\n * 获取队列状态\n * @param {string} sessionId - Session ID\n * @returns {Object}\n */\n getQueueStatus(sessionId) {\n return { sessionId, size: 0, processing: false };\n }\n\n /**\n * 清空队列\n * @param {string} sessionId - Session ID\n */\n clearQueue(sessionId) {\n this.emit('queue:cleared', { sessionId });\n }\n\n /**\n * 生成请求 ID\n * @returns {string}\n */\n generateRequestId() {\n return `req_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;\n }\n\n /**\n * 清理指定 session 的消息存储,释放内存\n * @param {string} sessionId - Session ID\n */\n clearSessionMessages(sessionId) {\n if (this._sessionMessageStores.has(sessionId)) {\n const store = this._sessionMessageStores.get(sessionId);\n // 保存历史到磁盘\n if (store.save) {\n store.save();\n }\n this._sessionMessageStores.delete(sessionId);\n }\n }\n\n /**\n * 清理所有 session 的消息存储\n */\n clearAllSessionMessages() {\n for (const [sessionId] of this._sessionMessageStores) {\n this.clearSessionMessages(sessionId);\n }\n }\n}\n\nmodule.exports = { ChatSession, SessionScope };\n",
|
|
1973
|
+
"metadata": {
|
|
1974
|
+
"filePath": "D:\\Code\\foliko\\src\\core\\chat-session.js",
|
|
1975
|
+
"size": 9512,
|
|
1976
|
+
"lines": 361
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
},
|
|
1981
|
+
{
|
|
1982
|
+
"type": "tool-result",
|
|
1983
|
+
"toolCallId": "call_function_zxmhn4uudmuw_2",
|
|
1984
|
+
"toolName": "search_file",
|
|
1985
|
+
"output": {
|
|
1986
|
+
"type": "json",
|
|
1987
|
+
"value": {
|
|
1988
|
+
"success": true,
|
|
1989
|
+
"data": [],
|
|
1990
|
+
"metadata": {
|
|
1991
|
+
"pattern": "MAX.*SIZE|limit.*message|message.*limit",
|
|
1992
|
+
"total": 0,
|
|
1993
|
+
"stats": {
|
|
1994
|
+
"filesWithMatches": 0,
|
|
1995
|
+
"totalMatches": 0,
|
|
1996
|
+
"searchPath": "src"
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
]
|
|
2003
|
+
},
|
|
2004
|
+
{
|
|
2005
|
+
"role": "assistant",
|
|
2006
|
+
"content": [
|
|
2007
|
+
{
|
|
2008
|
+
"type": "text",
|
|
2009
|
+
"text": "<think>Let me search for message size limits and also check what `cleanResponse` does - it might be truncating the response.\n</think>\n\n\n"
|
|
2010
|
+
},
|
|
2011
|
+
{
|
|
2012
|
+
"type": "tool-call",
|
|
2013
|
+
"toolCallId": "call_function_4ffzskbyxc5f_1",
|
|
2014
|
+
"toolName": "search_file",
|
|
2015
|
+
"input": {
|
|
2016
|
+
"pattern": "cleanResponse",
|
|
2017
|
+
"path": "src",
|
|
2018
|
+
"contextLines": 5
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
]
|
|
2022
|
+
},
|
|
2023
|
+
{
|
|
2024
|
+
"role": "tool",
|
|
2025
|
+
"content": [
|
|
2026
|
+
{
|
|
2027
|
+
"type": "tool-result",
|
|
2028
|
+
"toolCallId": "call_function_4ffzskbyxc5f_1",
|
|
2029
|
+
"toolName": "search_file",
|
|
2030
|
+
"output": {
|
|
2031
|
+
"type": "json",
|
|
2032
|
+
"value": {
|
|
2033
|
+
"success": true,
|
|
2034
|
+
"data": [
|
|
2035
|
+
{
|
|
2036
|
+
"file": "src\\core\\agent-chat.js",
|
|
2037
|
+
"line": 19,
|
|
2038
|
+
"content": "const { cleanResponse } = require('../utils');",
|
|
2039
|
+
"matches": [
|
|
2040
|
+
{
|
|
2041
|
+
"column": 8,
|
|
2042
|
+
"length": 13,
|
|
2043
|
+
"text": "cleanResponse"
|
|
2044
|
+
}
|
|
2045
|
+
],
|
|
2046
|
+
"context": [
|
|
2047
|
+
{
|
|
2048
|
+
"line": 14,
|
|
2049
|
+
"content": " tool: aiTool,"
|
|
2050
|
+
},
|
|
2051
|
+
{
|
|
2052
|
+
"line": 15,
|
|
2053
|
+
"content": " ToolLoopAgent,"
|
|
2054
|
+
},
|
|
2055
|
+
{
|
|
2056
|
+
"line": 16,
|
|
2057
|
+
"content": " isLoopFinished,"
|
|
2058
|
+
},
|
|
2059
|
+
{
|
|
2060
|
+
"line": 17,
|
|
2061
|
+
"content": "} = require('ai');"
|
|
2062
|
+
},
|
|
2063
|
+
{
|
|
2064
|
+
"line": 18,
|
|
2065
|
+
"content": "const { autoSplitToolResult } = require('../../plugins/data-splitter-plugin');"
|
|
2066
|
+
},
|
|
2067
|
+
{
|
|
2068
|
+
"line": 19,
|
|
2069
|
+
"content": "const { cleanResponse } = require('../utils');"
|
|
2070
|
+
},
|
|
2071
|
+
{
|
|
2072
|
+
"line": 20,
|
|
2073
|
+
"content": "const { ChatQueueManager } = require('../utils/chat-queue');"
|
|
2074
|
+
},
|
|
2075
|
+
{
|
|
2076
|
+
"line": 21,
|
|
2077
|
+
"content": "const { TokenCounter } = require('./token-counter');"
|
|
2078
|
+
},
|
|
2079
|
+
{
|
|
2080
|
+
"line": 22,
|
|
2081
|
+
"content": "const { isThinkingModel } = require('./provider');"
|
|
2082
|
+
},
|
|
2083
|
+
{
|
|
2084
|
+
"line": 23,
|
|
2085
|
+
"content": "// 新模块"
|
|
2086
|
+
},
|
|
2087
|
+
{
|
|
2088
|
+
"line": 24,
|
|
2089
|
+
"content": "const { ChatSession } = require('./chat-session');"
|
|
2090
|
+
}
|
|
2091
|
+
]
|
|
2092
|
+
},
|
|
2093
|
+
{
|
|
2094
|
+
"file": "src\\core\\agent-chat.js",
|
|
2095
|
+
"line": 690,
|
|
2096
|
+
"content": " message: cleanResponse(result.text || ''),",
|
|
2097
|
+
"matches": [
|
|
2098
|
+
{
|
|
2099
|
+
"column": 17,
|
|
2100
|
+
"length": 13,
|
|
2101
|
+
"text": "cleanResponse"
|
|
2102
|
+
}
|
|
2103
|
+
],
|
|
2104
|
+
"context": [
|
|
2105
|
+
{
|
|
2106
|
+
"line": 685,
|
|
2107
|
+
"content": " const userMsg = messages[messages.length - result.response.messages.length - 1];"
|
|
2108
|
+
},
|
|
2109
|
+
{
|
|
2110
|
+
"line": 686,
|
|
2111
|
+
"content": " this.emit('message', { content: result.text, sessionId: sessionId, userMessage: userMsg });"
|
|
2112
|
+
},
|
|
2113
|
+
{
|
|
2114
|
+
"line": 687,
|
|
2115
|
+
"content": ""
|
|
2116
|
+
},
|
|
2117
|
+
{
|
|
2118
|
+
"line": 688,
|
|
2119
|
+
"content": " return {"
|
|
2120
|
+
},
|
|
2121
|
+
{
|
|
2122
|
+
"line": 689,
|
|
2123
|
+
"content": " success: true,"
|
|
2124
|
+
},
|
|
2125
|
+
{
|
|
2126
|
+
"line": 690,
|
|
2127
|
+
"content": " message: cleanResponse(result.text || ''),"
|
|
2128
|
+
},
|
|
2129
|
+
{
|
|
2130
|
+
"line": 691,
|
|
2131
|
+
"content": " stepCount: result.stepCount || 1,"
|
|
2132
|
+
},
|
|
2133
|
+
{
|
|
2134
|
+
"line": 692,
|
|
2135
|
+
"content": " };"
|
|
2136
|
+
},
|
|
2137
|
+
{
|
|
2138
|
+
"line": 693,
|
|
2139
|
+
"content": " } catch (err) {"
|
|
2140
|
+
},
|
|
2141
|
+
{
|
|
2142
|
+
"line": 694,
|
|
2143
|
+
"content": " logger.error('[AgentChat] _processMessage error:', err.message);"
|
|
2144
|
+
},
|
|
2145
|
+
{
|
|
2146
|
+
"line": 695,
|
|
2147
|
+
"content": " const errorMsg = err.message || String(err);"
|
|
2148
|
+
}
|
|
2149
|
+
]
|
|
2150
|
+
},
|
|
2151
|
+
{
|
|
2152
|
+
"file": "src\\core\\subagent.js",
|
|
2153
|
+
"line": 8,
|
|
2154
|
+
"content": "const { cleanResponse } = require('../utils');",
|
|
2155
|
+
"matches": [
|
|
2156
|
+
{
|
|
2157
|
+
"column": 8,
|
|
2158
|
+
"length": 13,
|
|
2159
|
+
"text": "cleanResponse"
|
|
2160
|
+
}
|
|
2161
|
+
],
|
|
2162
|
+
"context": [
|
|
2163
|
+
{
|
|
2164
|
+
"line": 3,
|
|
2165
|
+
"content": " * 轻量级子 Agent,用于完成单次任务"
|
|
2166
|
+
},
|
|
2167
|
+
{
|
|
2168
|
+
"line": 4,
|
|
2169
|
+
"content": " * 不需要 session、上下文压缩等复杂功能"
|
|
2170
|
+
},
|
|
2171
|
+
{
|
|
2172
|
+
"line": 5,
|
|
2173
|
+
"content": " */"
|
|
2174
|
+
},
|
|
2175
|
+
{
|
|
2176
|
+
"line": 6,
|
|
2177
|
+
"content": ""
|
|
2178
|
+
},
|
|
2179
|
+
{
|
|
2180
|
+
"line": 7,
|
|
2181
|
+
"content": "const { EventEmitter } = require('../utils/event-emitter');"
|
|
2182
|
+
},
|
|
2183
|
+
{
|
|
2184
|
+
"line": 8,
|
|
2185
|
+
"content": "const { cleanResponse } = require('../utils');"
|
|
2186
|
+
},
|
|
2187
|
+
{
|
|
2188
|
+
"line": 9,
|
|
2189
|
+
"content": "const { generateText, tool, stepCountIs, isLoopFinished, RetryError, ToolLoopAgent } = require('ai');"
|
|
2190
|
+
},
|
|
2191
|
+
{
|
|
2192
|
+
"line": 10,
|
|
2193
|
+
"content": "const { z } = require('zod');"
|
|
2194
|
+
},
|
|
2195
|
+
{
|
|
2196
|
+
"line": 11,
|
|
2197
|
+
"content": "const { logger } = require('../utils/logger');"
|
|
2198
|
+
},
|
|
2199
|
+
{
|
|
2200
|
+
"line": 12,
|
|
2201
|
+
"content": "const { validateAll } = require('../utils/message-validator');"
|
|
2202
|
+
},
|
|
2203
|
+
{
|
|
2204
|
+
"line": 13,
|
|
2205
|
+
"content": "const { isNetworkError, calculateDelay } = require('../utils/retry');"
|
|
2206
|
+
}
|
|
2207
|
+
]
|
|
2208
|
+
},
|
|
2209
|
+
{
|
|
2210
|
+
"file": "src\\core\\subagent.js",
|
|
2211
|
+
"line": 353,
|
|
2212
|
+
"content": " const full_text = cleanResponse(result.text);",
|
|
2213
|
+
"matches": [
|
|
2214
|
+
{
|
|
2215
|
+
"column": 26,
|
|
2216
|
+
"length": 13,
|
|
2217
|
+
"text": "cleanResponse"
|
|
2218
|
+
}
|
|
2219
|
+
],
|
|
2220
|
+
"context": [
|
|
2221
|
+
{
|
|
2222
|
+
"line": 348,
|
|
2223
|
+
"content": " abortSignal: options.signal,"
|
|
2224
|
+
},
|
|
2225
|
+
{
|
|
2226
|
+
"line": 349,
|
|
2227
|
+
"content": " });"
|
|
2228
|
+
},
|
|
2229
|
+
{
|
|
2230
|
+
"line": 350,
|
|
2231
|
+
"content": " // console.log('[子Agent] ' ,result.text)"
|
|
2232
|
+
},
|
|
2233
|
+
{
|
|
2234
|
+
"line": 351,
|
|
2235
|
+
"content": " // ToolLoopAgent 会自动执行工具并收集结果,messages 已经被修改"
|
|
2236
|
+
},
|
|
2237
|
+
{
|
|
2238
|
+
"line": 352,
|
|
2239
|
+
"content": " messages.push(...result.response.messages);"
|
|
2240
|
+
},
|
|
2241
|
+
{
|
|
2242
|
+
"line": 353,
|
|
2243
|
+
"content": " const full_text = cleanResponse(result.text);"
|
|
2244
|
+
},
|
|
2245
|
+
{
|
|
2246
|
+
"line": 354,
|
|
2247
|
+
"content": " this.emit('complete', { message: full_text, steps: result.steps?.length || 0 });"
|
|
2248
|
+
},
|
|
2249
|
+
{
|
|
2250
|
+
"line": 355,
|
|
2251
|
+
"content": " return {"
|
|
2252
|
+
},
|
|
2253
|
+
{
|
|
2254
|
+
"line": 356,
|
|
2255
|
+
"content": " success: true,"
|
|
2256
|
+
},
|
|
2257
|
+
{
|
|
2258
|
+
"line": 357,
|
|
2259
|
+
"content": " message: full_text,"
|
|
2260
|
+
},
|
|
2261
|
+
{
|
|
2262
|
+
"line": 358,
|
|
2263
|
+
"content": " steps: result.steps?.length || 0,"
|
|
2264
|
+
}
|
|
2265
|
+
]
|
|
2266
|
+
},
|
|
2267
|
+
{
|
|
2268
|
+
"file": "src\\utils\\chat-queue.js",
|
|
2269
|
+
"line": 2,
|
|
2270
|
+
"content": "const { cleanResponse } = require('./index');",
|
|
2271
|
+
"matches": [
|
|
2272
|
+
{
|
|
2273
|
+
"column": 8,
|
|
2274
|
+
"length": 13,
|
|
2275
|
+
"text": "cleanResponse"
|
|
2276
|
+
}
|
|
2277
|
+
],
|
|
2278
|
+
"context": [
|
|
2279
|
+
{
|
|
2280
|
+
"line": 1,
|
|
2281
|
+
"content": "const { EventEmitter } = require('./event-emitter');"
|
|
2282
|
+
},
|
|
2283
|
+
{
|
|
2284
|
+
"line": 2,
|
|
2285
|
+
"content": "const { cleanResponse } = require('./index');"
|
|
2286
|
+
},
|
|
2287
|
+
{
|
|
2288
|
+
"line": 3,
|
|
2289
|
+
"content": "const { logger } = require('./logger');"
|
|
2290
|
+
},
|
|
2291
|
+
{
|
|
2292
|
+
"line": 4,
|
|
2293
|
+
"content": "const { isNetworkError, calculateDelay, PRESETS } = require('./retry');"
|
|
2294
|
+
},
|
|
2295
|
+
{
|
|
2296
|
+
"line": 5,
|
|
2297
|
+
"content": "const log = logger.child('ChatQueue');"
|
|
2298
|
+
},
|
|
2299
|
+
{
|
|
2300
|
+
"line": 6,
|
|
2301
|
+
"content": "// ChatQueueManager.js"
|
|
2302
|
+
},
|
|
2303
|
+
{
|
|
2304
|
+
"line": 7,
|
|
2305
|
+
"content": "class ChatQueueManager extends EventEmitter {"
|
|
2306
|
+
}
|
|
2307
|
+
]
|
|
2308
|
+
},
|
|
2309
|
+
{
|
|
2310
|
+
"file": "src\\utils\\chat-queue.js",
|
|
2311
|
+
"line": 201,
|
|
2312
|
+
"content": " content: cleanResponse(''),",
|
|
2313
|
+
"matches": [
|
|
2314
|
+
{
|
|
2315
|
+
"column": 17,
|
|
2316
|
+
"length": 13,
|
|
2317
|
+
"text": "cleanResponse"
|
|
2318
|
+
}
|
|
2319
|
+
],
|
|
2320
|
+
"context": [
|
|
2321
|
+
{
|
|
2322
|
+
"line": 196,
|
|
2323
|
+
"content": " error.chunks = chunks;"
|
|
2324
|
+
},
|
|
2325
|
+
{
|
|
2326
|
+
"line": 197,
|
|
2327
|
+
"content": " error.isStreamError = true;"
|
|
2328
|
+
},
|
|
2329
|
+
{
|
|
2330
|
+
"line": 198,
|
|
2331
|
+
"content": " log.warn('[ChatQueue] executeStream Error:', error.message);"
|
|
2332
|
+
},
|
|
2333
|
+
{
|
|
2334
|
+
"line": 199,
|
|
2335
|
+
"content": " return {"
|
|
2336
|
+
},
|
|
2337
|
+
{
|
|
2338
|
+
"line": 200,
|
|
2339
|
+
"content": " chunks,"
|
|
2340
|
+
},
|
|
2341
|
+
{
|
|
2342
|
+
"line": 201,
|
|
2343
|
+
"content": " content: cleanResponse(''),"
|
|
2344
|
+
},
|
|
2345
|
+
{
|
|
2346
|
+
"line": 202,
|
|
2347
|
+
"content": " sessionId: item.sessionId,"
|
|
2348
|
+
},
|
|
2349
|
+
{
|
|
2350
|
+
"line": 203,
|
|
2351
|
+
"content": " requestId: item.id,"
|
|
2352
|
+
},
|
|
2353
|
+
{
|
|
2354
|
+
"line": 204,
|
|
2355
|
+
"content": " error,"
|
|
2356
|
+
},
|
|
2357
|
+
{
|
|
2358
|
+
"line": 205,
|
|
2359
|
+
"content": " };"
|
|
2360
|
+
},
|
|
2361
|
+
{
|
|
2362
|
+
"line": 206,
|
|
2363
|
+
"content": " }"
|
|
2364
|
+
}
|
|
2365
|
+
]
|
|
2366
|
+
},
|
|
2367
|
+
{
|
|
2368
|
+
"file": "src\\utils\\chat-queue.js",
|
|
2369
|
+
"line": 215,
|
|
2370
|
+
"content": " content: cleanResponse(fullText),",
|
|
2371
|
+
"matches": [
|
|
2372
|
+
{
|
|
2373
|
+
"column": 15,
|
|
2374
|
+
"length": 13,
|
|
2375
|
+
"text": "cleanResponse"
|
|
2376
|
+
}
|
|
2377
|
+
],
|
|
2378
|
+
"context": [
|
|
2379
|
+
{
|
|
2380
|
+
"line": 210,
|
|
2381
|
+
"content": " .map((item) => item.text)"
|
|
2382
|
+
},
|
|
2383
|
+
{
|
|
2384
|
+
"line": 211,
|
|
2385
|
+
"content": " .join('');"
|
|
2386
|
+
},
|
|
2387
|
+
{
|
|
2388
|
+
"line": 212,
|
|
2389
|
+
"content": ""
|
|
2390
|
+
},
|
|
2391
|
+
{
|
|
2392
|
+
"line": 213,
|
|
2393
|
+
"content": " return {"
|
|
2394
|
+
},
|
|
2395
|
+
{
|
|
2396
|
+
"line": 214,
|
|
2397
|
+
"content": " chunks,"
|
|
2398
|
+
},
|
|
2399
|
+
{
|
|
2400
|
+
"line": 215,
|
|
2401
|
+
"content": " content: cleanResponse(fullText),"
|
|
2402
|
+
},
|
|
2403
|
+
{
|
|
2404
|
+
"line": 216,
|
|
2405
|
+
"content": " sessionId: item.sessionId,"
|
|
2406
|
+
},
|
|
2407
|
+
{
|
|
2408
|
+
"line": 217,
|
|
2409
|
+
"content": " requestId: item.id,"
|
|
2410
|
+
},
|
|
2411
|
+
{
|
|
2412
|
+
"line": 218,
|
|
2413
|
+
"content": " };"
|
|
2414
|
+
},
|
|
2415
|
+
{
|
|
2416
|
+
"line": 219,
|
|
2417
|
+
"content": " }"
|
|
2418
|
+
},
|
|
2419
|
+
{
|
|
2420
|
+
"line": 220,
|
|
2421
|
+
"content": ""
|
|
2422
|
+
}
|
|
2423
|
+
]
|
|
2424
|
+
},
|
|
2425
|
+
{
|
|
2426
|
+
"file": "src\\utils\\index.js",
|
|
2427
|
+
"line": 55,
|
|
2428
|
+
"content": "function cleanResponse(text) {",
|
|
2429
|
+
"matches": [
|
|
2430
|
+
{
|
|
2431
|
+
"column": 9,
|
|
2432
|
+
"length": 13,
|
|
2433
|
+
"text": "cleanResponse"
|
|
2434
|
+
}
|
|
2435
|
+
],
|
|
2436
|
+
"context": [
|
|
2437
|
+
{
|
|
2438
|
+
"line": 50,
|
|
2439
|
+
"content": "/**"
|
|
2440
|
+
},
|
|
2441
|
+
{
|
|
2442
|
+
"line": 51,
|
|
2443
|
+
"content": " * 清理 LLM 回复中的思考标记"
|
|
2444
|
+
},
|
|
2445
|
+
{
|
|
2446
|
+
"line": 52,
|
|
2447
|
+
"content": " * @param {string} text"
|
|
2448
|
+
},
|
|
2449
|
+
{
|
|
2450
|
+
"line": 53,
|
|
2451
|
+
"content": " * @returns {string}"
|
|
2452
|
+
},
|
|
2453
|
+
{
|
|
2454
|
+
"line": 54,
|
|
2455
|
+
"content": " */"
|
|
2456
|
+
},
|
|
2457
|
+
{
|
|
2458
|
+
"line": 55,
|
|
2459
|
+
"content": "function cleanResponse(text) {"
|
|
2460
|
+
},
|
|
2461
|
+
{
|
|
2462
|
+
"line": 56,
|
|
2463
|
+
"content": " return text.replace(/<think>[\\s\\S]*?<\\/think>/g, '').trim();"
|
|
2464
|
+
},
|
|
2465
|
+
{
|
|
2466
|
+
"line": 57,
|
|
2467
|
+
"content": "}"
|
|
2468
|
+
},
|
|
2469
|
+
{
|
|
2470
|
+
"line": 58,
|
|
2471
|
+
"content": ""
|
|
2472
|
+
},
|
|
2473
|
+
{
|
|
2474
|
+
"line": 59,
|
|
2475
|
+
"content": "/**"
|
|
2476
|
+
},
|
|
2477
|
+
{
|
|
2478
|
+
"line": 60,
|
|
2479
|
+
"content": " * 安全解析 JSON"
|
|
2480
|
+
}
|
|
2481
|
+
]
|
|
2482
|
+
},
|
|
2483
|
+
{
|
|
2484
|
+
"file": "src\\utils\\index.js",
|
|
2485
|
+
"line": 531,
|
|
2486
|
+
"content": " cleanResponse,",
|
|
2487
|
+
"matches": [
|
|
2488
|
+
{
|
|
2489
|
+
"column": 2,
|
|
2490
|
+
"length": 13,
|
|
2491
|
+
"text": "cleanResponse"
|
|
2492
|
+
}
|
|
2493
|
+
],
|
|
2494
|
+
"context": [
|
|
2495
|
+
{
|
|
2496
|
+
"line": 526,
|
|
2497
|
+
"content": " RecoveryAction,"
|
|
2498
|
+
},
|
|
2499
|
+
{
|
|
2500
|
+
"line": 527,
|
|
2501
|
+
"content": " createErrorBoundary,"
|
|
2502
|
+
},
|
|
2503
|
+
{
|
|
2504
|
+
"line": 528,
|
|
2505
|
+
"content": " combineBoundaries,"
|
|
2506
|
+
},
|
|
2507
|
+
{
|
|
2508
|
+
"line": 529,
|
|
2509
|
+
"content": ""
|
|
2510
|
+
},
|
|
2511
|
+
{
|
|
2512
|
+
"line": 530,
|
|
2513
|
+
"content": " // 工具函数"
|
|
2514
|
+
},
|
|
2515
|
+
{
|
|
2516
|
+
"line": 531,
|
|
2517
|
+
"content": " cleanResponse,"
|
|
2518
|
+
},
|
|
2519
|
+
{
|
|
2520
|
+
"line": 532,
|
|
2521
|
+
"content": " safeJsonParse,"
|
|
2522
|
+
},
|
|
2523
|
+
{
|
|
2524
|
+
"line": 533,
|
|
2525
|
+
"content": " deepClone,"
|
|
2526
|
+
},
|
|
2527
|
+
{
|
|
2528
|
+
"line": 534,
|
|
2529
|
+
"content": " debounce,"
|
|
2530
|
+
},
|
|
2531
|
+
{
|
|
2532
|
+
"line": 535,
|
|
2533
|
+
"content": " throttle,"
|
|
2534
|
+
},
|
|
2535
|
+
{
|
|
2536
|
+
"line": 536,
|
|
2537
|
+
"content": " sleep,"
|
|
2538
|
+
}
|
|
2539
|
+
]
|
|
2540
|
+
}
|
|
2541
|
+
],
|
|
2542
|
+
"metadata": {
|
|
2543
|
+
"pattern": "cleanResponse",
|
|
2544
|
+
"total": 9,
|
|
2545
|
+
"stats": {
|
|
2546
|
+
"filesWithMatches": 4,
|
|
2547
|
+
"totalMatches": 9,
|
|
2548
|
+
"searchPath": "src"
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
]
|
|
2555
|
+
},
|
|
2556
|
+
{
|
|
2557
|
+
"role": "assistant",
|
|
2558
|
+
"content": [
|
|
2559
|
+
{
|
|
2560
|
+
"type": "text",
|
|
2561
|
+
"text": "<think>`cleanResponse` 很简单,只是移除 `<think>...</think>` 标签。不会截断内容。\n\n让我看看 `data-splitter-plugin` 是否有相关的截断逻辑,这可能是问题所在。\n</think>\n\n\n"
|
|
2562
|
+
},
|
|
2563
|
+
{
|
|
2564
|
+
"type": "tool-call",
|
|
2565
|
+
"toolCallId": "call_function_g2sntg5xgthv_1",
|
|
2566
|
+
"toolName": "search_file",
|
|
2567
|
+
"input": {
|
|
2568
|
+
"pattern": "split|large|truncate|limit",
|
|
2569
|
+
"path": "plugins",
|
|
2570
|
+
"contextLines": 5
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2573
|
+
]
|
|
2574
|
+
},
|
|
2575
|
+
{
|
|
2576
|
+
"role": "tool",
|
|
2577
|
+
"content": [
|
|
2578
|
+
{
|
|
2579
|
+
"type": "tool-result",
|
|
2580
|
+
"toolCallId": "call_function_g2sntg5xgthv_1",
|
|
2581
|
+
"toolName": "search_file",
|
|
2582
|
+
"output": {
|
|
2583
|
+
"type": "json",
|
|
2584
|
+
"value": {
|
|
2585
|
+
"success": true,
|
|
2586
|
+
"data": [],
|
|
2587
|
+
"metadata": {
|
|
2588
|
+
"pattern": "split|large|truncate|limit",
|
|
2589
|
+
"total": 0,
|
|
2590
|
+
"stats": {
|
|
2591
|
+
"filesWithMatches": 0,
|
|
2592
|
+
"totalMatches": 0,
|
|
2593
|
+
"searchPath": "plugins"
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
]
|
|
2600
|
+
},
|
|
2601
|
+
{
|
|
2602
|
+
"role": "assistant",
|
|
2603
|
+
"content": [
|
|
2604
|
+
{
|
|
2605
|
+
"type": "text",
|
|
2606
|
+
"text": "<think>没有找到相关代码。让我尝试用 PowerShell 来搜索。\n</think>\n\n\n"
|
|
2607
|
+
},
|
|
2608
|
+
{
|
|
2609
|
+
"type": "tool-call",
|
|
2610
|
+
"toolCallId": "call_function_jregai2acyoy_1",
|
|
2611
|
+
"toolName": "search_file",
|
|
2612
|
+
"input": {
|
|
2613
|
+
"pattern": "autoSplit",
|
|
2614
|
+
"path": "plugins",
|
|
2615
|
+
"contextLines": 3
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
]
|
|
2619
|
+
},
|
|
2620
|
+
{
|
|
2621
|
+
"role": "tool",
|
|
2622
|
+
"content": [
|
|
2623
|
+
{
|
|
2624
|
+
"type": "tool-result",
|
|
2625
|
+
"toolCallId": "call_function_jregai2acyoy_1",
|
|
2626
|
+
"toolName": "search_file",
|
|
2627
|
+
"output": {
|
|
2628
|
+
"type": "json",
|
|
2629
|
+
"value": {
|
|
2630
|
+
"success": true,
|
|
2631
|
+
"data": [
|
|
2632
|
+
{
|
|
2633
|
+
"file": "plugins\\data-splitter-plugin.js",
|
|
2634
|
+
"line": 32,
|
|
2635
|
+
"content": " autoSplitThreshold: config.autoSplitThreshold || AUTO_SPLIT_THRESHOLD,",
|
|
2636
|
+
"matches": [
|
|
2637
|
+
{
|
|
2638
|
+
"column": 6,
|
|
2639
|
+
"length": 9,
|
|
2640
|
+
"text": "autoSplit"
|
|
2641
|
+
},
|
|
2642
|
+
{
|
|
2643
|
+
"column": 33,
|
|
2644
|
+
"length": 9,
|
|
2645
|
+
"text": "autoSplit"
|
|
2646
|
+
}
|
|
2647
|
+
],
|
|
2648
|
+
"context": [
|
|
2649
|
+
{
|
|
2650
|
+
"line": 29,
|
|
2651
|
+
"content": ""
|
|
2652
|
+
},
|
|
2653
|
+
{
|
|
2654
|
+
"line": 30,
|
|
2655
|
+
"content": " // 配置"
|
|
2656
|
+
},
|
|
2657
|
+
{
|
|
2658
|
+
"line": 31,
|
|
2659
|
+
"content": " this.config = {"
|
|
2660
|
+
},
|
|
2661
|
+
{
|
|
2662
|
+
"line": 32,
|
|
2663
|
+
"content": " autoSplitThreshold: config.autoSplitThreshold || AUTO_SPLIT_THRESHOLD,"
|
|
2664
|
+
},
|
|
2665
|
+
{
|
|
2666
|
+
"line": 33,
|
|
2667
|
+
"content": " chunkSize: config.chunkSize || 60000,"
|
|
2668
|
+
},
|
|
2669
|
+
{
|
|
2670
|
+
"line": 34,
|
|
2671
|
+
"content": " maxConcurrent: config.maxConcurrent || 3,"
|
|
2672
|
+
},
|
|
2673
|
+
{
|
|
2674
|
+
"line": 35,
|
|
2675
|
+
"content": " ...config,"
|
|
2676
|
+
}
|
|
2677
|
+
]
|
|
2678
|
+
},
|
|
2679
|
+
{
|
|
2680
|
+
"file": "plugins\\data-splitter-plugin.js",
|
|
2681
|
+
"line": 43,
|
|
2682
|
+
"content": " safeThreshold: this.config.autoSplitThreshold,",
|
|
2683
|
+
"matches": [
|
|
2684
|
+
{
|
|
2685
|
+
"column": 33,
|
|
2686
|
+
"length": 9,
|
|
2687
|
+
"text": "autoSplit"
|
|
2688
|
+
}
|
|
2689
|
+
],
|
|
2690
|
+
"context": [
|
|
2691
|
+
{
|
|
2692
|
+
"line": 40,
|
|
2693
|
+
"content": " this._framework = framework;"
|
|
2694
|
+
},
|
|
2695
|
+
{
|
|
2696
|
+
"line": 41,
|
|
2697
|
+
"content": " this._splitter = new DataSplitter(framework, {"
|
|
2698
|
+
},
|
|
2699
|
+
{
|
|
2700
|
+
"line": 42,
|
|
2701
|
+
"content": " chunkSize: this.config.chunkSize,"
|
|
2702
|
+
},
|
|
2703
|
+
{
|
|
2704
|
+
"line": 43,
|
|
2705
|
+
"content": " safeThreshold: this.config.autoSplitThreshold,"
|
|
2706
|
+
},
|
|
2707
|
+
{
|
|
2708
|
+
"line": 44,
|
|
2709
|
+
"content": " maxConcurrent: this.config.maxConcurrent,"
|
|
2710
|
+
},
|
|
2711
|
+
{
|
|
2712
|
+
"line": 45,
|
|
2713
|
+
"content": " });"
|
|
2714
|
+
},
|
|
2715
|
+
{
|
|
2716
|
+
"line": 46,
|
|
2717
|
+
"content": ""
|
|
2718
|
+
}
|
|
2719
|
+
]
|
|
2720
|
+
},
|
|
2721
|
+
{
|
|
2722
|
+
"file": "plugins\\data-splitter-plugin.js",
|
|
2723
|
+
"line": 58,
|
|
2724
|
+
"content": " this._registerAutoSplitHook();",
|
|
2725
|
+
"matches": [
|
|
2726
|
+
{
|
|
2727
|
+
"column": 18,
|
|
2728
|
+
"length": 9,
|
|
2729
|
+
"text": "AutoSplit"
|
|
2730
|
+
}
|
|
2731
|
+
],
|
|
2732
|
+
"context": [
|
|
2733
|
+
{
|
|
2734
|
+
"line": 55,
|
|
2735
|
+
"content": " this.registerPromptPart('data-splitter-rules', 85, () => this._getPromptRules());"
|
|
2736
|
+
},
|
|
2737
|
+
{
|
|
2738
|
+
"line": 56,
|
|
2739
|
+
"content": ""
|
|
2740
|
+
},
|
|
2741
|
+
{
|
|
2742
|
+
"line": 57,
|
|
2743
|
+
"content": " // 注册工具结果监听器(自动检测大结果)"
|
|
2744
|
+
},
|
|
2745
|
+
{
|
|
2746
|
+
"line": 58,
|
|
2747
|
+
"content": " this._registerAutoSplitHook();"
|
|
2748
|
+
},
|
|
2749
|
+
{
|
|
2750
|
+
"line": 59,
|
|
2751
|
+
"content": ""
|
|
2752
|
+
},
|
|
2753
|
+
{
|
|
2754
|
+
"line": 60,
|
|
2755
|
+
"content": " logger.info('[DataSplitterPlugin] 已启动,阈值:', this.config.autoSplitThreshold);"
|
|
2756
|
+
},
|
|
2757
|
+
{
|
|
2758
|
+
"line": 61,
|
|
2759
|
+
"content": " }"
|
|
2760
|
+
}
|
|
2761
|
+
]
|
|
2762
|
+
},
|
|
2763
|
+
{
|
|
2764
|
+
"file": "plugins\\data-splitter-plugin.js",
|
|
2765
|
+
"line": 60,
|
|
2766
|
+
"content": " logger.info('[DataSplitterPlugin] 已启动,阈值:', this.config.autoSplitThreshold);",
|
|
2767
|
+
"matches": [
|
|
2768
|
+
{
|
|
2769
|
+
"column": 60,
|
|
2770
|
+
"length": 9,
|
|
2771
|
+
"text": "autoSplit"
|
|
2772
|
+
}
|
|
2773
|
+
],
|
|
2774
|
+
"context": [
|
|
2775
|
+
{
|
|
2776
|
+
"line": 57,
|
|
2777
|
+
"content": " // 注册工具结果监听器(自动检测大结果)"
|
|
2778
|
+
},
|
|
2779
|
+
{
|
|
2780
|
+
"line": 58,
|
|
2781
|
+
"content": " this._registerAutoSplitHook();"
|
|
2782
|
+
},
|
|
2783
|
+
{
|
|
2784
|
+
"line": 59,
|
|
2785
|
+
"content": ""
|
|
2786
|
+
},
|
|
2787
|
+
{
|
|
2788
|
+
"line": 60,
|
|
2789
|
+
"content": " logger.info('[DataSplitterPlugin] 已启动,阈值:', this.config.autoSplitThreshold);"
|
|
2790
|
+
},
|
|
2791
|
+
{
|
|
2792
|
+
"line": 61,
|
|
2793
|
+
"content": " }"
|
|
2794
|
+
},
|
|
2795
|
+
{
|
|
2796
|
+
"line": 62,
|
|
2797
|
+
"content": ""
|
|
2798
|
+
},
|
|
2799
|
+
{
|
|
2800
|
+
"line": 63,
|
|
2801
|
+
"content": " /**"
|
|
2802
|
+
}
|
|
2803
|
+
]
|
|
2804
|
+
},
|
|
2805
|
+
{
|
|
2806
|
+
"file": "plugins\\data-splitter-plugin.js",
|
|
2807
|
+
"line": 196,
|
|
2808
|
+
"content": " _registerAutoSplitHook() {",
|
|
2809
|
+
"matches": [
|
|
2810
|
+
{
|
|
2811
|
+
"column": 11,
|
|
2812
|
+
"length": 9,
|
|
2813
|
+
"text": "AutoSplit"
|
|
2814
|
+
}
|
|
2815
|
+
],
|
|
2816
|
+
"context": [
|
|
2817
|
+
{
|
|
2818
|
+
"line": 193,
|
|
2819
|
+
"content": " * 监听 tool:result 事件,如果结果太大自动触发分拆"
|
|
2820
|
+
},
|
|
2821
|
+
{
|
|
2822
|
+
"line": 194,
|
|
2823
|
+
"content": " * @private"
|
|
2824
|
+
},
|
|
2825
|
+
{
|
|
2826
|
+
"line": 195,
|
|
2827
|
+
"content": " */"
|
|
2828
|
+
},
|
|
2829
|
+
{
|
|
2830
|
+
"line": 196,
|
|
2831
|
+
"content": " _registerAutoSplitHook() {"
|
|
2832
|
+
},
|
|
2833
|
+
{
|
|
2834
|
+
"line": 197,
|
|
2835
|
+
"content": " const framework = this._framework;"
|
|
2836
|
+
},
|
|
2837
|
+
{
|
|
2838
|
+
"line": 198,
|
|
2839
|
+
"content": ""
|
|
2840
|
+
},
|
|
2841
|
+
{
|
|
2842
|
+
"line": 199,
|
|
2843
|
+
"content": " // 监听工具结果,检测大数据"
|
|
2844
|
+
}
|
|
2845
|
+
]
|
|
2846
|
+
},
|
|
2847
|
+
{
|
|
2848
|
+
"file": "plugins\\data-splitter-plugin.js",
|
|
2849
|
+
"line": 213,
|
|
2850
|
+
"content": " if (!checkContent || checkContent.length < this.config.autoSplitThreshold) return;",
|
|
2851
|
+
"matches": [
|
|
2852
|
+
{
|
|
2853
|
+
"column": 61,
|
|
2854
|
+
"length": 9,
|
|
2855
|
+
"text": "autoSplit"
|
|
2856
|
+
}
|
|
2857
|
+
],
|
|
2858
|
+
"context": [
|
|
2859
|
+
{
|
|
2860
|
+
"line": 210,
|
|
2861
|
+
"content": " } else {"
|
|
2862
|
+
},
|
|
2863
|
+
{
|
|
2864
|
+
"line": 211,
|
|
2865
|
+
"content": " checkContent = typeof result === 'string' ? result : JSON.stringify(result);"
|
|
2866
|
+
},
|
|
2867
|
+
{
|
|
2868
|
+
"line": 212,
|
|
2869
|
+
"content": " }"
|
|
2870
|
+
},
|
|
2871
|
+
{
|
|
2872
|
+
"line": 213,
|
|
2873
|
+
"content": " if (!checkContent || checkContent.length < this.config.autoSplitThreshold) return;"
|
|
2874
|
+
},
|
|
2875
|
+
{
|
|
2876
|
+
"line": 214,
|
|
2877
|
+
"content": ""
|
|
2878
|
+
},
|
|
2879
|
+
{
|
|
2880
|
+
"line": 215,
|
|
2881
|
+
"content": " // 不阻塞执行,只在日志中记录建议"
|
|
2882
|
+
},
|
|
2883
|
+
{
|
|
2884
|
+
"line": 216,
|
|
2885
|
+
"content": " logger.info("
|
|
2886
|
+
}
|
|
2887
|
+
]
|
|
2888
|
+
},
|
|
2889
|
+
{
|
|
2890
|
+
"file": "plugins\\data-splitter-plugin.js",
|
|
2891
|
+
"line": 269,
|
|
2892
|
+
"content": "async function autoSplitToolResult(toolName, result, framework) {",
|
|
2893
|
+
"matches": [
|
|
2894
|
+
{
|
|
2895
|
+
"column": 15,
|
|
2896
|
+
"length": 9,
|
|
2897
|
+
"text": "autoSplit"
|
|
2898
|
+
}
|
|
2899
|
+
],
|
|
2900
|
+
"context": [
|
|
2901
|
+
{
|
|
2902
|
+
"line": 266,
|
|
2903
|
+
"content": " * @param {Object} framework - Framework 实例"
|
|
2904
|
+
},
|
|
2905
|
+
{
|
|
2906
|
+
"line": 267,
|
|
2907
|
+
"content": " * @returns {Promise<{ wasSplit: boolean, result: *, splitterResult?: Object }>}"
|
|
2908
|
+
},
|
|
2909
|
+
{
|
|
2910
|
+
"line": 268,
|
|
2911
|
+
"content": " */"
|
|
2912
|
+
},
|
|
2913
|
+
{
|
|
2914
|
+
"line": 269,
|
|
2915
|
+
"content": "async function autoSplitToolResult(toolName, result, framework) {"
|
|
2916
|
+
},
|
|
2917
|
+
{
|
|
2918
|
+
"line": 270,
|
|
2919
|
+
"content": " // 跳过某些工具"
|
|
2920
|
+
},
|
|
2921
|
+
{
|
|
2922
|
+
"line": 271,
|
|
2923
|
+
"content": " const skipTools = ['split_and_process', 'get_content_preview'];"
|
|
2924
|
+
},
|
|
2925
|
+
{
|
|
2926
|
+
"line": 272,
|
|
2927
|
+
"content": " if (skipTools.includes(toolName)) return { wasSplit: false, result };"
|
|
2928
|
+
}
|
|
2929
|
+
]
|
|
2930
|
+
},
|
|
2931
|
+
{
|
|
2932
|
+
"file": "plugins\\data-splitter-plugin.js",
|
|
2933
|
+
"line": 310,
|
|
2934
|
+
"content": " _autoSplit: true,",
|
|
2935
|
+
"matches": [
|
|
2936
|
+
{
|
|
2937
|
+
"column": 9,
|
|
2938
|
+
"length": 9,
|
|
2939
|
+
"text": "autoSplit"
|
|
2940
|
+
}
|
|
2941
|
+
],
|
|
2942
|
+
"context": [
|
|
2943
|
+
{
|
|
2944
|
+
"line": 307,
|
|
2945
|
+
"content": " wasSplit: true,"
|
|
2946
|
+
},
|
|
2947
|
+
{
|
|
2948
|
+
"line": 308,
|
|
2949
|
+
"content": " // 返回原始结果 + 分拆汇总,AI 可以同时看到两者"
|
|
2950
|
+
},
|
|
2951
|
+
{
|
|
2952
|
+
"line": 309,
|
|
2953
|
+
"content": " result: {"
|
|
2954
|
+
},
|
|
2955
|
+
{
|
|
2956
|
+
"line": 310,
|
|
2957
|
+
"content": " _autoSplit: true,"
|
|
2958
|
+
},
|
|
2959
|
+
{
|
|
2960
|
+
"line": 311,
|
|
2961
|
+
"content": " _originalSize: checkContent.length,"
|
|
2962
|
+
},
|
|
2963
|
+
{
|
|
2964
|
+
"line": 312,
|
|
2965
|
+
"content": " _summary: splitResult.summary,"
|
|
2966
|
+
},
|
|
2967
|
+
{
|
|
2968
|
+
"line": 313,
|
|
2969
|
+
"content": " _originalResult: result, // 保留原始结果供参考"
|
|
2970
|
+
}
|
|
2971
|
+
]
|
|
2972
|
+
},
|
|
2973
|
+
{
|
|
2974
|
+
"file": "plugins\\data-splitter-plugin.js",
|
|
2975
|
+
"line": 323,
|
|
2976
|
+
"content": "module.exports = { DataSplitterPlugin, autoSplitToolResult };",
|
|
2977
|
+
"matches": [
|
|
2978
|
+
{
|
|
2979
|
+
"column": 39,
|
|
2980
|
+
"length": 9,
|
|
2981
|
+
"text": "autoSplit"
|
|
2982
|
+
}
|
|
2983
|
+
],
|
|
2984
|
+
"context": [
|
|
2985
|
+
{
|
|
2986
|
+
"line": 320,
|
|
2987
|
+
"content": " }"
|
|
2988
|
+
},
|
|
2989
|
+
{
|
|
2990
|
+
"line": 321,
|
|
2991
|
+
"content": "}"
|
|
2992
|
+
},
|
|
2993
|
+
{
|
|
2994
|
+
"line": 322,
|
|
2995
|
+
"content": ""
|
|
2996
|
+
},
|
|
2997
|
+
{
|
|
2998
|
+
"line": 323,
|
|
2999
|
+
"content": "module.exports = { DataSplitterPlugin, autoSplitToolResult };"
|
|
3000
|
+
},
|
|
3001
|
+
{
|
|
3002
|
+
"line": 324,
|
|
3003
|
+
"content": ""
|
|
3004
|
+
}
|
|
3005
|
+
]
|
|
3006
|
+
},
|
|
3007
|
+
{
|
|
3008
|
+
"file": "plugins\\default-plugins.js",
|
|
3009
|
+
"line": 371,
|
|
3010
|
+
"content": " autoSplitThreshold: agentConfig.dataSplitter?.autoSplitThreshold || 50000,",
|
|
3011
|
+
"matches": [
|
|
3012
|
+
{
|
|
3013
|
+
"column": 6,
|
|
3014
|
+
"length": 9,
|
|
3015
|
+
"text": "autoSplit"
|
|
3016
|
+
},
|
|
3017
|
+
{
|
|
3018
|
+
"column": 52,
|
|
3019
|
+
"length": 9,
|
|
3020
|
+
"text": "autoSplit"
|
|
3021
|
+
}
|
|
3022
|
+
],
|
|
3023
|
+
"context": [
|
|
3024
|
+
{
|
|
3025
|
+
"line": 368,
|
|
3026
|
+
"content": " if (shouldLoad('data-splitter')) {"
|
|
3027
|
+
},
|
|
3028
|
+
{
|
|
3029
|
+
"line": 369,
|
|
3030
|
+
"content": " const { DataSplitterPlugin } = require('./data-splitter-plugin')"
|
|
3031
|
+
},
|
|
3032
|
+
{
|
|
3033
|
+
"line": 370,
|
|
3034
|
+
"content": " await framework.loadPlugin(new DataSplitterPlugin({"
|
|
3035
|
+
},
|
|
3036
|
+
{
|
|
3037
|
+
"line": 371,
|
|
3038
|
+
"content": " autoSplitThreshold: agentConfig.dataSplitter?.autoSplitThreshold || 50000,"
|
|
3039
|
+
},
|
|
3040
|
+
{
|
|
3041
|
+
"line": 372,
|
|
3042
|
+
"content": " chunkSize: agentConfig.dataSplitter?.chunkSize || 60000,"
|
|
3043
|
+
},
|
|
3044
|
+
{
|
|
3045
|
+
"line": 373,
|
|
3046
|
+
"content": " maxConcurrent: agentConfig.dataSplitter?.maxConcurrent || 3,"
|
|
3047
|
+
},
|
|
3048
|
+
{
|
|
3049
|
+
"line": 374,
|
|
3050
|
+
"content": " }))"
|
|
3051
|
+
}
|
|
3052
|
+
]
|
|
3053
|
+
}
|
|
3054
|
+
],
|
|
3055
|
+
"metadata": {
|
|
3056
|
+
"pattern": "autoSplit",
|
|
3057
|
+
"total": 10,
|
|
3058
|
+
"stats": {
|
|
3059
|
+
"filesWithMatches": 2,
|
|
3060
|
+
"totalMatches": 12,
|
|
3061
|
+
"searchPath": "plugins"
|
|
3062
|
+
}
|
|
3063
|
+
}
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
3066
|
+
}
|
|
3067
|
+
]
|
|
3068
|
+
},
|
|
3069
|
+
{
|
|
3070
|
+
"role": "assistant",
|
|
3071
|
+
"content": [
|
|
3072
|
+
{
|
|
3073
|
+
"type": "text",
|
|
3074
|
+
"text": "<think>找到了!data-splitter-plugin 有一个阈值是 50000(50KB),当工具结果超过这个大小时会触发自动分拆。这可能会影响回复的完整性。\n\n让我看看这个插件的完整实现:\n</think>\n\n\n"
|
|
3075
|
+
},
|
|
3076
|
+
{
|
|
3077
|
+
"type": "tool-call",
|
|
3078
|
+
"toolCallId": "call_function_2lrq0ahrhs8x_1",
|
|
3079
|
+
"toolName": "read",
|
|
3080
|
+
"input": {
|
|
3081
|
+
"path": "plugins/data-splitter-plugin.js"
|
|
3082
|
+
}
|
|
3083
|
+
}
|
|
3084
|
+
]
|
|
3085
|
+
},
|
|
3086
|
+
{
|
|
3087
|
+
"role": "tool",
|
|
3088
|
+
"content": [
|
|
3089
|
+
{
|
|
3090
|
+
"type": "tool-result",
|
|
3091
|
+
"toolCallId": "call_function_2lrq0ahrhs8x_1",
|
|
3092
|
+
"toolName": "read",
|
|
3093
|
+
"output": {
|
|
3094
|
+
"type": "json",
|
|
3095
|
+
"value": {
|
|
3096
|
+
"success": true,
|
|
3097
|
+
"data": "/**\n * DataSplitterPlugin — 大数据自动分拆插件\n *\n * 功能:\n * 1. 注册 `split_and_process` 工具:AI 可主动调用,将大文本分拆给子 Agent 处理\n * 2. 注册 `get_content_preview` 工具:获取大内容的前 N 行,判断是否需要分拆\n * 3. 自动检测工具返回的大数据,触发透明分拆\n * 4. 系统提示词中告知 AI 大数据处理能力\n */\n\nconst { Plugin } = require('../src/core/plugin-base');\nconst { z } = require('zod');\nconst { logger } = require('../src/utils/logger');\nconst { DataSplitter } = require('../src/utils/data-splitter');\n\n// 超过此大小的工具结果自动触发分拆(字符数,默认 100K tokens ≈ 200K chars)\nconst AUTO_SPLIT_THRESHOLD = 50000;\n\nclass DataSplitterPlugin extends Plugin {\n constructor(config = {}) {\n super();\n this.name = 'data-splitter';\n this.version = '1.0.0';\n this.description = '大数据自动分拆处理:读取大文件或抓取网页时,自动分拆为多个子Agent并行处理';\n this.priority = 5;\n\n this._framework = null;\n this._splitter = null;\n\n // 配置\n this.config = {\n autoSplitThreshold: config.autoSplitThreshold || AUTO_SPLIT_THRESHOLD,\n chunkSize: config.chunkSize || 60000,\n maxConcurrent: config.maxConcurrent || 3,\n ...config,\n };\n }\n\n install(framework) {\n this._framework = framework;\n this._splitter = new DataSplitter(framework, {\n chunkSize: this.config.chunkSize,\n safeThreshold: this.config.autoSplitThreshold,\n maxConcurrent: this.config.maxConcurrent,\n });\n\n return this;\n }\n\n start(framework) {\n // 注册大数据处理工具\n this._registerSplitTools();\n\n // 注册系统提示词\n this.registerPromptPart('data-splitter-rules', 85, () => this._getPromptRules());\n\n // 注册工具结果监听器(自动检测大结果)\n this._registerAutoSplitHook();\n\n logger.info('[DataSplitterPlugin] 已启动,阈值:', this.config.autoSplitThreshold);\n }\n\n /**\n * 注册分拆工具\n * @private\n */\n _registerSplitTools() {\n const framework = this._framework;\n\n // ─── 工具1: split_and_process — AI 主动调用分拆 ───\n framework.registerTool({\n name: 'split_and_process',\n description: `将大文本内容按大小分块,创建多个子 Agent 并行处理每块内容,最后自动汇总结果。\n当你读取文件或抓取网页返回的内容超过 10 万字符时,应该使用此工具来分拆处理。\n它会将内容拆成多块,每块由一个子 Agent 独立处理,最后给你一个汇总。`,\n inputSchema: z.object({\n content: z.string().describe('要分拆处理的大文本内容(如果超过 100K 字符建议使用此工具)'),\n taskDescription: z.string().describe('每个子 Agent 要执行的任务描述,例如\"提取所有函数定义\"、\"总结内容要点\"等'),\n chunkSize: z.number().optional().default(60000).describe('每块最大字符数,默认 60000'),\n maxConcurrent: z.number().optional().default(3).describe('最大并行子 Agent 数,默认 3'),\n }),\n execute: async (args, ctx) => {\n const { content, taskDescription, chunkSize, maxConcurrent } = args;\n const splitter = this._getSplitter(ctx);\n\n if (!content || content.length === 0) {\n return { success: false, error: '内容为空,无需处理' };\n }\n\n const stats = splitter.getContentStats(content);\n if (!splitter.needsSplit(content)) {\n // 内容较小,直接返回\n return {\n success: true,\n data: content,\n metadata: {\n needsSplit: false,\n stats,\n message: '内容大小在安全范围内,无需分拆处理。可直接使用原始内容。'\n }\n };\n }\n\n // 生成上下文中的 sessionId 用于取消信号\n const sessionCtx = ctx?.getCurrentSessionContext?.();\n const signal = sessionCtx?.abortSignal;\n\n logger.info(\n `[split_and_process] 开始分拆: ${stats.chars} 字符, ` +\n `${stats.chunks} 块, 任务=\"${taskDescription?.slice(0, 40)}...\"`\n );\n\n const startTime = Date.now();\n const chunks = splitter.splitContent(content, chunkSize);\n const result = await splitter.dispatchToSubAgents({\n chunks,\n taskDescription,\n maxConcurrent: maxConcurrent || this.config.maxConcurrent,\n signal,\n });\n const duration = ((Date.now() - startTime) / 1000).toFixed(1);\n\n return {\n success: true,\n data: result.summary,\n metadata: {\n needsSplit: true,\n stats: { ...stats, actualChunks: chunks.length },\n totalChunks: chunks.length,\n successfulChunks: result.results.filter((r) => r.success).length,\n failedChunks: result.errors.length,\n durationSec: parseFloat(duration),\n },\n };\n },\n });\n\n // ─── 工具2: get_content_preview — 获取大内容预览信息 ───\n framework.registerTool({\n name: 'get_content_preview',\n description: `获取大文本内容的预览信息(前 20 行 + 统计),判断是否需要分拆处理。\n适合在读取大文件或抓取网页后用于检查内容大小。`,\n inputSchema: z.object({\n content: z.string().describe('要预览的大文本内容'),\n previewLines: z.number().optional().default(20).describe('预览前 N 行,默认 20'),\n }),\n execute: async (args) => {\n const { content, previewLines = 20 } = args;\n const splitter = this._getSplitter();\n\n const stats = splitter.getContentStats(content);\n const lines = content.split('\\n');\n const preview = lines.slice(0, previewLines).join('\\n');\n\n return {\n success: true,\n data: preview || '(空内容)',\n metadata: {\n stats,\n needsSplit: splitter.needsSplit(content),\n totalLines: lines.length,\n suggestion: splitter.needsSplit(content)\n ? `内容较大 (${stats.chars} 字符, 约 ${stats.estimatedTokens} tokens),建议使用 split_and_process 分拆处理`\n : '内容大小在安全范围内',\n }\n };\n },\n });\n }\n\n /**\n * 获取系统提示词\n * @private\n */\n _getPromptRules() {\n return `## 大数据处理能力\n\n你具备自动处理大文件和大网页的能力:\n\n1. **内容预览**:当工具返回的内容很大时,先用 \\`get_content_preview\\` 查看统计信息\n2. **分拆处理**:如果内容超过 10 万字符,用 \\`split_and_process\\` 将内容分块,每块交给独立的子 Agent 并行处理\n3. **自动汇总**:\\`split_and_process\\` 会自动汇总所有子 Agent 的处理结果,你只需基于汇总结果回答即可\n4. **性能提示**:分拆处理会并行运行多个子 Agent,处理大文件时效率很高\n\n### 使用建议\n- 读取大文件(> 100KB)时:先读取,如果内容太大,用 \\`split_and_process\\` 分拆分析\n- 抓取网页时:如果页面内容过多,用 \\`split_and_process\\` 分拆提取\n- 分拆时指定清晰的任务描述,例如\"提取所有关键代码函数\"、\"总结每段内容要点\"`;\n }\n\n /**\n * 注册自动分拆钩子\n * 监听 tool:result 事件,如果结果太大自动触发分拆\n * @private\n */\n _registerAutoSplitHook() {\n const framework = this._framework;\n\n // 监听工具结果,检测大数据\n this._toolResultHandler = (data) => {\n const { name, result } = data;\n\n // 跳过已经标记为分拆结果的工具\n if (name === 'split_and_process') return;\n\n // 检查工具返回的内容是否过大(优先使用 data 字段)\n let checkContent = '';\n if (result && typeof result === 'object' && result.data) {\n checkContent = typeof result.data === 'string' ? result.data : JSON.stringify(result.data);\n } else {\n checkContent = typeof result === 'string' ? result : JSON.stringify(result);\n }\n if (!checkContent || checkContent.length < this.config.autoSplitThreshold) return;\n\n // 不阻塞执行,只在日志中记录建议\n logger.info(\n `[DataSplitter] 检测到大数据工具结果: ${name}, ` +\n `${checkContent.length} 字符, 建议使用 split_and_process 分拆处理`\n );\n };\n\n framework.on('tool:result', this._toolResultHandler);\n }\n\n /**\n * 获取 DataSplitter 实例\n * @private\n */\n _getSplitter(ctx) {\n return this._splitter || new DataSplitter(this._framework, this.config);\n }\n\n reload(framework) {\n // 清理旧监听器\n if (this._toolResultHandler) {\n framework.off('tool:result', this._toolResultHandler);\n }\n\n this._framework = framework;\n this._splitter = new DataSplitter(framework, this.config);\n\n // 重新注册\n if (framework._mainAgent) {\n this.start(framework);\n }\n\n // 请求刷新 mainAgent 的 system prompt\n if (framework._mainAgent) {\n framework._mainAgent._refreshContext();\n }\n }\n\n uninstall(framework) {\n if (this._toolResultHandler) {\n framework.off('tool:result', this._toolResultHandler);\n }\n }\n}\n\n/**\n * 自动检测工具结果是否过大,并透明处理\n * 在 agent-chat 中调用\n *\n * @param {string} toolName - 工具名称\n * @param {*} result - 工具返回结果\n * @param {Object} framework - Framework 实例\n * @returns {Promise<{ wasSplit: boolean, result: *, splitterResult?: Object }>}\n */\nasync function autoSplitToolResult(toolName, result, framework) {\n // 跳过某些工具\n const skipTools = ['split_and_process', 'get_content_preview'];\n if (skipTools.includes(toolName)) return { wasSplit: false, result };\n\n // 无 framework 时无法创建子 Agent,跳过\n if (!framework || typeof framework.createSubAgent !== 'function') {\n return { wasSplit: false, result };\n }\n\n // 提取用于判断大小的内容\n // 优先使用 result.data(统一格式),再回退到整个结果字符串\n let checkContent = '';\n if (typeof result === 'object' && result !== null && result.data) {\n checkContent = typeof result.data === 'string' ? result.data : JSON.stringify(result.data);\n } else {\n checkContent = typeof result === 'string' ? result : JSON.stringify(result);\n }\n if (!checkContent || checkContent.length < AUTO_SPLIT_THRESHOLD) {\n return { wasSplit: false, result };\n }\n\n const splitter = new DataSplitter(framework);\n const taskDescription = `从以下内容中提取关键信息(代码、配置、数据等),以结构化方式输出。`;\n\n logger.info(\n `[DataSplitter] 自动分拆工具 \"${toolName}\": ${checkContent.length} 字符`\n );\n\n try {\n const chunks = splitter.splitContent(checkContent);\n const splitResult = await splitter.dispatchToSubAgents({\n chunks,\n taskDescription,\n maxConcurrent: 3,\n });\n\n return {\n wasSplit: true,\n // 返回原始结果 + 分拆汇总,AI 可以同时看到两者\n result: {\n _autoSplit: true,\n _originalSize: checkContent.length,\n _summary: splitResult.summary,\n _originalResult: result, // 保留原始结果供参考\n },\n splitterResult: splitResult,\n };\n } catch (err) {\n logger.warn(`[DataSplitter] 自动分拆失败: ${err.message}`);\n return { wasSplit: false, result };\n }\n}\n\nmodule.exports = { DataSplitterPlugin, autoSplitToolResult };\n",
|
|
3098
|
+
"metadata": {
|
|
3099
|
+
"filePath": "D:\\Code\\foliko\\plugins\\data-splitter-plugin.js",
|
|
3100
|
+
"size": 11622,
|
|
3101
|
+
"lines": 324
|
|
3102
|
+
}
|
|
3103
|
+
}
|
|
3104
|
+
}
|
|
3105
|
+
}
|
|
3106
|
+
]
|
|
3107
|
+
},
|
|
3108
|
+
{
|
|
3109
|
+
"role": "assistant",
|
|
3110
|
+
"content": [
|
|
3111
|
+
{
|
|
3112
|
+
"type": "text",
|
|
3113
|
+
"text": "<think>我找到了 `autoSplitToolResult` 的实现。让我检查一下 `agent-chat.js` 中是如何调用它的。根据之前读取的代码,在 `_getAITools` 中:\n\n```javascript\nconst result = await toolDef.execute(cleanedArgs, this.agent.framework);\n\n// 自动检测大工具结果,透明分拆\n// 仅在数据分拆插件已加载时生效\nlet finalResult = result;\ntry {\n const splitCheck = await autoSplitToolResult(toolName, result, this.agent.framework);\n if (splitCheck.wasSplit && splitCheck.result) {\n finalResult = splitCheck.result;\n logger.info(\n `[AutoSplit] 工具 \"${toolName}\" 结果过大,已自动分拆处理`\n );\n }\n} catch (splitErr) {\n // 分拆失败不阻断主流程,继续使用原始结果\n logger.warn(`[AutoSplit] 自动分拆跳过: ${splitErr.message}`);\n}\n\nthis.emit('tool-result', { name: toolName, args: cleanedArgs, result: finalResult });\nreturn finalResult;\n```\n\n这看起来是正确的处理流程。但让我检查一下问题可能在哪里。\n\n用户提到\"回复一半就突然断了\"。这可能是:\n1. API 层面的截断(maxOutputTokens 限制)\n2. 网络连接中断\n3. 流式处理的异常\n\n我注意到 `providerOptions.maxOutputTokens = 8192`,这是一个限制。\n\n\n对于 DeepSeek 模型,8192 tokens 的输出限制可能导致较长的回复被截断。不过用户说的是\"回复一半\",这不太像是主动截断的问题,更可能是流式处理中间出现了中断。我需要检查 `chatStream` 函数中的错误处理逻辑,看是否有异常被吞掉或没有正确传递。\n\n在那个函数里,错误处理会捕获异常并通过 yield 返回错误消息。但如果网络不稳定或连接中断,可能导致部分内容已发送但后续无法继续。这可能源于 API 调用本身的不稳定性,或者 token 数量估算不准确——特别是在中文文本的情况下,TokenCounter 的计算可能偏差较大。\n\n比如在 `token-counter.js` 中,每个字符按 4 字节估算,但中文字符通常是 3 字节,而中文每 token 大约对应 2 字符。如果模型的实际 context window 限制是 800K tokens,代码可能误解了这个限制的含义。\n\n我发现 `context-compressor.js` 中定义了一个 `MODEL_CONTEXT_LIMITS` 表,其中 `deepseek-v4-flash` 设置为 800000 tokens。但这里可能存在单位混淆——tokens 与字符数的关系并非简单的一对一映射。\n\nDeepSeek 的 800K 上下文窗口指的是 tokens,而在中文文本中,1 token 通常等于约 2 个字符。这意味着 800K tokens 相当于约 160 万字符。如果代码把 tokens 数直接当作字符数处理,就可能导致判断逻辑出错。\n\n关键问题是 `limit` 的计算方式。在 `_prepareSession` 中,`limit` 被设为 `_maxContextTokens * 0.5`,而 `_maxContextTokens` 本身可能已经混淆了 tokens 和字符数的概念。这样的话,limit 可能会被设置得过小,导致不该压缩的消息被压缩。\n\n还有一个问题是 `chatStream` 中的异常处理。catch 块会捕获错误并通过 yield 返回错误消息,但如果异常发生在 yield 过程中,外层的 `executeStream` 仍然能继续执行,只是错误会被转换后追加到 chunks 数组。这意味着流不会中断,但返回的数据可能不完整。\n\n从代码来看,工具结果的自动分割已经实现,但回复被截断的问题很可能出在其他地方。我需要检查 `cleanResponse` 函数,看它是否在清理回复时出现了问题。\n\n这个函数只是移除 `<think>` 标签,应该不会导致截断。更可能的是 AI 服务本身的输出被截断了,或者工具执行过程中抛出了异常但被捕获后返回了不完整的结果。\n\n我应该给用户一个更全面的分析,涵盖可能的原因和诊断方向。\n</think>\n\n是的,有可能!让我分析一下可能导致长消息截断的地方:\n\n## 🔍 可能的长消息截断点\n\n### 1. **maxOutputTokens 限制**(最可能)\n\n```javascript\n// agent-chat.js:53\nthis.providerOptions.maxOutputTokens = config.providerOptions?.maxOutputTokens || 8192;\n```\n\nDeepSeek 的 `deepseek-v4-flash` 模型虽然有 800K context window,但 **maxOutputTokens 默认只有 8192**,会限制单次输出的最大 token 数。\n\n### 2. **Token 计数估算偏差**\n\n```javascript\n// context-compressor.js - MODEL_CONTEXT_LIMITS\n'deepseek-v4-flash': 800000, // 这里写的是 800000 tokens\n```\n\n但实际计数时:\n```javascript\n// token-counter.js - encode()\nconst bytes = Buffer.byteLength(String(text), 'utf8');\nreturn Math.ceil(bytes / 4); // 中文每个字 3 字节 / 4 = 每字符 0.75 token\n```\n\n**800000 tokens ≈ 160 万字符**,如果代码把这个数字当成字符数来比较,会导致上下文限制判断错误。\n\n### 3. **工具结果自动分拆后的处理**\n\n```javascript\n// autoSplitToolResult 返回格式\n{\n _autoSplit: true,\n _originalSize: ...,\n _summary: splitResult.summary,\n _originalResult: result, // 原始结果被包装了\n}\n```\n\n如果 AI 看到的是 `{ _autoSplit: true, _summary: \"...\" }` 而不是原始内容,可能导致回复不完整。\n\n---\n\n## 🔧 建议排查\n\n1. **检查日志**:看是正常回复还是中途报错\n2. **测试长回复**:发送一个需要很长回复的问题,看是否会截断\n3. **查看具体场景**:是在什么操作时回复中断的?\n\n你能描述一下:\n- 是在哪些场景下回复会中断?(读文件?分析代码?聊天?)\n- 是看到回复到一半就停了,还是回复根本没开始?"
|
|
3114
|
+
}
|
|
3115
|
+
]
|
|
3116
|
+
},
|
|
3117
|
+
{
|
|
3118
|
+
"role": "user",
|
|
3119
|
+
"content": "autoSplitToolResult 是不是这个的问题?"
|
|
3120
|
+
},
|
|
3121
|
+
{
|
|
3122
|
+
"role": "assistant",
|
|
3123
|
+
"content": [
|
|
3124
|
+
{
|
|
3125
|
+
"type": "text",
|
|
3126
|
+
"text": "<think>用户怀疑是 `autoSplitToolResult` 导致的问题。让我仔细分析一下这个函数的逻辑。\n</think>\n\n让我仔细分析一下 `autoSplitToolResult` 的逻辑:\n"
|
|
3127
|
+
},
|
|
3128
|
+
{
|
|
3129
|
+
"type": "tool-call",
|
|
3130
|
+
"toolCallId": "call_function_szzld5oxdxmy_1",
|
|
3131
|
+
"toolName": "bash",
|
|
3132
|
+
"input": {
|
|
3133
|
+
"cmd": "sed -n '269,330p' plugins/data-splitter-plugin.js"
|
|
3134
|
+
}
|
|
3135
|
+
}
|
|
3136
|
+
]
|
|
3137
|
+
},
|
|
3138
|
+
{
|
|
3139
|
+
"role": "tool",
|
|
3140
|
+
"content": [
|
|
3141
|
+
{
|
|
3142
|
+
"type": "tool-result",
|
|
3143
|
+
"toolCallId": "call_function_szzld5oxdxmy_1",
|
|
3144
|
+
"toolName": "bash",
|
|
3145
|
+
"output": {
|
|
3146
|
+
"type": "json",
|
|
3147
|
+
"value": {
|
|
3148
|
+
"success": true,
|
|
3149
|
+
"data": "async function autoSplitToolResult(toolName, result, framework) {\n // 跳过某些工具\n const skipTools = ['split_and_process', 'get_content_preview'];\n if (skipTools.includes(toolName)) return { wasSplit: false, result };\n\n // 无 framework 时无法创建子 Agent,跳过\n if (!framework || typeof framework.createSubAgent !== 'function') {\n return { wasSplit: false, result };\n }\n\n // 提取用于判断大小的内容\n // 优先使用 result.data(统一格式),再回退到整个结果字符串\n let checkContent = '';\n if (typeof result === 'object' && result !== null && result.data) {\n checkContent = typeof result.data === 'string' ? result.data : JSON.stringify(result.data);\n } else {\n checkContent = typeof result === 'string' ? result : JSON.stringify(result);\n }\n if (!checkContent || checkContent.length < AUTO_SPLIT_THRESHOLD) {\n return { wasSplit: false, result };\n }\n\n const splitter = new DataSplitter(framework);\n const taskDescription = `从以下内容中提取关键信息(代码、配置、数据等),以结构化方式输出。`;\n\n logger.info(\n `[DataSplitter] 自动分拆工具 \"${toolName}\": ${checkContent.length} 字符`\n );\n\n try {\n const chunks = splitter.splitContent(checkContent);\n const splitResult = await splitter.dispatchToSubAgents({\n chunks,\n taskDescription,\n maxConcurrent: 3,\n });\n\n return {\n wasSplit: true,\n // 返回原始结果 + 分拆汇总,AI 可以同时看到两者\n result: {\n _autoSplit: true,\n _originalSize: checkContent.length,\n _summary: splitResult.summary,\n _originalResult: result, // 保留原始结果供参考\n },\n splitterResult: splitResult,\n };\n } catch (err) {\n logger.warn(`[DataSplitter] 自动分拆失败: ${err.message}`);\n return { wasSplit: false, result };\n }\n}\n\nmodule.exports = { DataSplitterPlugin, autoSplitToolResult };\n",
|
|
3150
|
+
"metadata": {
|
|
3151
|
+
"command": "sed -n '269,330p' plugins/data-splitter-plugin.js",
|
|
3152
|
+
"stderr": "",
|
|
3153
|
+
"duration": 91
|
|
3154
|
+
}
|
|
3155
|
+
}
|
|
3156
|
+
}
|
|
3157
|
+
}
|
|
3158
|
+
]
|
|
3159
|
+
},
|
|
3160
|
+
{
|
|
3161
|
+
"role": "assistant",
|
|
3162
|
+
"content": [
|
|
3163
|
+
{
|
|
3164
|
+
"type": "text",
|
|
3165
|
+
"text": "<think>Let me look at how autoSplitToolResult is called in agent-chat.js and what happens with the returned value.\n</think>\n\n\n"
|
|
3166
|
+
},
|
|
3167
|
+
{
|
|
3168
|
+
"type": "tool-call",
|
|
3169
|
+
"toolCallId": "call_function_dzsaxwi7w978_1",
|
|
3170
|
+
"toolName": "bash",
|
|
3171
|
+
"input": {
|
|
3172
|
+
"cmd": "grep -n \"autoSplitToolResult\" src/core/agent-chat.js"
|
|
3173
|
+
}
|
|
3174
|
+
}
|
|
3175
|
+
]
|
|
3176
|
+
},
|
|
3177
|
+
{
|
|
3178
|
+
"role": "tool",
|
|
3179
|
+
"content": [
|
|
3180
|
+
{
|
|
3181
|
+
"type": "tool-result",
|
|
3182
|
+
"toolCallId": "call_function_dzsaxwi7w978_1",
|
|
3183
|
+
"toolName": "bash",
|
|
3184
|
+
"output": {
|
|
3185
|
+
"type": "json",
|
|
3186
|
+
"value": {
|
|
3187
|
+
"success": true,
|
|
3188
|
+
"data": "18:const { autoSplitToolResult } = require('../../plugins/data-splitter-plugin');\n862: const splitCheck = await autoSplitToolResult(toolName, result, this.agent.framework);\n",
|
|
3189
|
+
"metadata": {
|
|
3190
|
+
"command": "grep -n \"autoSplitToolResult\" src/core/agent-chat.js",
|
|
3191
|
+
"stderr": "",
|
|
3192
|
+
"duration": 88
|
|
3193
|
+
}
|
|
3194
|
+
}
|
|
3195
|
+
}
|
|
3196
|
+
}
|
|
3197
|
+
]
|
|
3198
|
+
},
|
|
3199
|
+
{
|
|
3200
|
+
"role": "assistant",
|
|
3201
|
+
"content": [
|
|
3202
|
+
{
|
|
3203
|
+
"type": "text",
|
|
3204
|
+
"text": "<think>Let me look at the context around line 862 to see how the split result is handled.\n</think>\n\n\n"
|
|
3205
|
+
},
|
|
3206
|
+
{
|
|
3207
|
+
"type": "tool-call",
|
|
3208
|
+
"toolCallId": "call_function_kx2p9iy8lrnh_1",
|
|
3209
|
+
"toolName": "bash",
|
|
3210
|
+
"input": {
|
|
3211
|
+
"cmd": "sed -n '850,890p' src/core/agent-chat.js"
|
|
3212
|
+
}
|
|
3213
|
+
}
|
|
3214
|
+
]
|
|
3215
|
+
},
|
|
3216
|
+
{
|
|
3217
|
+
"role": "tool",
|
|
3218
|
+
"content": [
|
|
3219
|
+
{
|
|
3220
|
+
"type": "tool-result",
|
|
3221
|
+
"toolCallId": "call_function_kx2p9iy8lrnh_1",
|
|
3222
|
+
"toolName": "bash",
|
|
3223
|
+
"output": {
|
|
3224
|
+
"type": "json",
|
|
3225
|
+
"value": {
|
|
3226
|
+
"success": true,
|
|
3227
|
+
"data": " const cleanedArgs = this._cleanToolArgs(args);\n\n // 执行工具\n this.emit('tool-call', { name: toolName, args: cleanedArgs });\n logger.info(`[Tool] Call: ${toolName}`);\n try {\n const result = await toolDef.execute(cleanedArgs, this.agent.framework);\n\n // 自动检测大工具结果,透明分拆\n // 仅在数据分拆插件已加载时生效\n let finalResult = result;\n try {\n const splitCheck = await autoSplitToolResult(toolName, result, this.agent.framework);\n if (splitCheck.wasSplit && splitCheck.result) {\n finalResult = splitCheck.result;\n logger.info(\n `[AutoSplit] 工具 \"${toolName}\" 结果过大,已自动分拆处理`\n );\n }\n } catch (splitErr) {\n // 分拆失败不阻断主流程,继续使用原始结果\n logger.warn(`[AutoSplit] 自动分拆跳过: ${splitErr.message}`);\n }\n\n this.emit('tool-result', { name: toolName, args: cleanedArgs, result: finalResult });\n return finalResult;\n } catch (err) {\n this.emit('tool-error', { name: toolName, args: cleanedArgs, error: err.message });\n // 返回错误信息字符串,而不是抛出异常\n return { success: false,error: err.message}\n }\n },\n };\n\n // 支持 inputSchema 或 parameters\n if (toolDef.inputSchema) {\n toolConfig.inputSchema = toolDef.inputSchema;\n } else if (toolDef.parameters) {\n toolConfig.parameters = toolDef.parameters;\n }\n\n",
|
|
3228
|
+
"metadata": {
|
|
3229
|
+
"command": "sed -n '850,890p' src/core/agent-chat.js",
|
|
3230
|
+
"stderr": "",
|
|
3231
|
+
"duration": 90
|
|
3232
|
+
}
|
|
3233
|
+
}
|
|
3234
|
+
}
|
|
3235
|
+
}
|
|
3236
|
+
]
|
|
3237
|
+
},
|
|
3238
|
+
{
|
|
3239
|
+
"role": "assistant",
|
|
3240
|
+
"content": [
|
|
3241
|
+
{
|
|
3242
|
+
"type": "text",
|
|
3243
|
+
"text": "<think>找到了!问题在这里:1. 工具执行完成后,如果结果太大(> 50000 字符),`autoSplitToolResult` 会分拆处理2. 分拆后的返回结果是一个包装对象:\n ```javascript\n {\n _autoSplit: true,\n _originalSize: ...,\n _summary: splitResult.summary,\n _originalResult: result,\n }\n ```3. 这个包装对象被作为工具结果返回给 AI4. **问题**:如果 `_summary` 或 `_originalResult` 太大,或者格式不对,AI 可能会无法正确解析,导致回复中断\n\n让我进一步检查 `splitResult.summary` 是怎么生成的:\n</think>\n\n\n"
|
|
3244
|
+
},
|
|
3245
|
+
{
|
|
3246
|
+
"type": "tool-call",
|
|
3247
|
+
"toolCallId": "call_function_sn6rs0y90xts_1",
|
|
3248
|
+
"toolName": "search_file",
|
|
3249
|
+
"input": {
|
|
3250
|
+
"pattern": "summary",
|
|
3251
|
+
"path": "src/utils",
|
|
3252
|
+
"contextLines": 10
|
|
3253
|
+
}
|
|
3254
|
+
}
|
|
3255
|
+
]
|
|
3256
|
+
},
|
|
3257
|
+
{
|
|
3258
|
+
"role": "tool",
|
|
3259
|
+
"content": [
|
|
3260
|
+
{
|
|
3261
|
+
"type": "tool-result",
|
|
3262
|
+
"toolCallId": "call_function_sn6rs0y90xts_1",
|
|
3263
|
+
"toolName": "search_file",
|
|
3264
|
+
"output": {
|
|
3265
|
+
"type": "json",
|
|
3266
|
+
"value": {
|
|
3267
|
+
"success": true,
|
|
3268
|
+
"data": [
|
|
3269
|
+
{
|
|
3270
|
+
"file": "src\\utils\\data-splitter.js",
|
|
3271
|
+
"line": 140,
|
|
3272
|
+
"content": " * @returns {Promise<{ success: boolean, results: Array, summary: string, errors: Array }>}",
|
|
3273
|
+
"matches": [
|
|
3274
|
+
{
|
|
3275
|
+
"column": 59,
|
|
3276
|
+
"length": 7,
|
|
3277
|
+
"text": "summary"
|
|
3278
|
+
}
|
|
3279
|
+
],
|
|
3280
|
+
"context": [
|
|
3281
|
+
{
|
|
3282
|
+
"line": 130,
|
|
3283
|
+
"content": " * 为每个分块创建子 Agent 并分配处理任务"
|
|
3284
|
+
},
|
|
3285
|
+
{
|
|
3286
|
+
"line": 131,
|
|
3287
|
+
"content": " * 子 Agent 并行处理,结果自动汇总"
|
|
3288
|
+
},
|
|
3289
|
+
{
|
|
3290
|
+
"line": 132,
|
|
3291
|
+
"content": " *"
|
|
3292
|
+
},
|
|
3293
|
+
{
|
|
3294
|
+
"line": 133,
|
|
3295
|
+
"content": " * @param {Object} options"
|
|
3296
|
+
},
|
|
3297
|
+
{
|
|
3298
|
+
"line": 134,
|
|
3299
|
+
"content": " * @param {Array<{ index: number, content: string }>} options.chunks - 分块数据"
|
|
3300
|
+
},
|
|
3301
|
+
{
|
|
3302
|
+
"line": 135,
|
|
3303
|
+
"content": " * @param {string} options.taskDescription - 每个子 Agent 要执行的任务描述"
|
|
3304
|
+
},
|
|
3305
|
+
{
|
|
3306
|
+
"line": 136,
|
|
3307
|
+
"content": " * @param {string} [options.agentName] - 子 Agent 名称前缀"
|
|
3308
|
+
},
|
|
3309
|
+
{
|
|
3310
|
+
"line": 137,
|
|
3311
|
+
"content": " * @param {string} [options.agentRole] - 子 Agent 角色"
|
|
3312
|
+
},
|
|
3313
|
+
{
|
|
3314
|
+
"line": 138,
|
|
3315
|
+
"content": " * @param {number} [options.maxConcurrent] - 并行度"
|
|
3316
|
+
},
|
|
3317
|
+
{
|
|
3318
|
+
"line": 139,
|
|
3319
|
+
"content": " * @param {AbortSignal} [options.signal] - 取消信号"
|
|
3320
|
+
},
|
|
3321
|
+
{
|
|
3322
|
+
"line": 140,
|
|
3323
|
+
"content": " * @returns {Promise<{ success: boolean, results: Array, summary: string, errors: Array }>}"
|
|
3324
|
+
},
|
|
3325
|
+
{
|
|
3326
|
+
"line": 141,
|
|
3327
|
+
"content": " */"
|
|
3328
|
+
},
|
|
3329
|
+
{
|
|
3330
|
+
"line": 142,
|
|
3331
|
+
"content": " async dispatchToSubAgents(options) {"
|
|
3332
|
+
},
|
|
3333
|
+
{
|
|
3334
|
+
"line": 143,
|
|
3335
|
+
"content": " const {"
|
|
3336
|
+
},
|
|
3337
|
+
{
|
|
3338
|
+
"line": 144,
|
|
3339
|
+
"content": " chunks,"
|
|
3340
|
+
},
|
|
3341
|
+
{
|
|
3342
|
+
"line": 145,
|
|
3343
|
+
"content": " taskDescription,"
|
|
3344
|
+
},
|
|
3345
|
+
{
|
|
3346
|
+
"line": 146,
|
|
3347
|
+
"content": " agentName = 'data-processor',"
|
|
3348
|
+
},
|
|
3349
|
+
{
|
|
3350
|
+
"line": 147,
|
|
3351
|
+
"content": " agentRole = '数据处理专家',"
|
|
3352
|
+
},
|
|
3353
|
+
{
|
|
3354
|
+
"line": 148,
|
|
3355
|
+
"content": " maxConcurrent = this.maxConcurrent,"
|
|
3356
|
+
},
|
|
3357
|
+
{
|
|
3358
|
+
"line": 149,
|
|
3359
|
+
"content": " signal,"
|
|
3360
|
+
},
|
|
3361
|
+
{
|
|
3362
|
+
"line": 150,
|
|
3363
|
+
"content": " } = options;"
|
|
3364
|
+
}
|
|
3365
|
+
]
|
|
3366
|
+
},
|
|
3367
|
+
{
|
|
3368
|
+
"file": "src\\utils\\data-splitter.js",
|
|
3369
|
+
"line": 153,
|
|
3370
|
+
"content": " return { success: true, results: [], summary: '', errors: [] };",
|
|
3371
|
+
"matches": [
|
|
3372
|
+
{
|
|
3373
|
+
"column": 43,
|
|
3374
|
+
"length": 7,
|
|
3375
|
+
"text": "summary"
|
|
3376
|
+
}
|
|
3377
|
+
],
|
|
3378
|
+
"context": [
|
|
3379
|
+
{
|
|
3380
|
+
"line": 143,
|
|
3381
|
+
"content": " const {"
|
|
3382
|
+
},
|
|
3383
|
+
{
|
|
3384
|
+
"line": 144,
|
|
3385
|
+
"content": " chunks,"
|
|
3386
|
+
},
|
|
3387
|
+
{
|
|
3388
|
+
"line": 145,
|
|
3389
|
+
"content": " taskDescription,"
|
|
3390
|
+
},
|
|
3391
|
+
{
|
|
3392
|
+
"line": 146,
|
|
3393
|
+
"content": " agentName = 'data-processor',"
|
|
3394
|
+
},
|
|
3395
|
+
{
|
|
3396
|
+
"line": 147,
|
|
3397
|
+
"content": " agentRole = '数据处理专家',"
|
|
3398
|
+
},
|
|
3399
|
+
{
|
|
3400
|
+
"line": 148,
|
|
3401
|
+
"content": " maxConcurrent = this.maxConcurrent,"
|
|
3402
|
+
},
|
|
3403
|
+
{
|
|
3404
|
+
"line": 149,
|
|
3405
|
+
"content": " signal,"
|
|
3406
|
+
},
|
|
3407
|
+
{
|
|
3408
|
+
"line": 150,
|
|
3409
|
+
"content": " } = options;"
|
|
3410
|
+
},
|
|
3411
|
+
{
|
|
3412
|
+
"line": 151,
|
|
3413
|
+
"content": ""
|
|
3414
|
+
},
|
|
3415
|
+
{
|
|
3416
|
+
"line": 152,
|
|
3417
|
+
"content": " if (!chunks || chunks.length === 0) {"
|
|
3418
|
+
},
|
|
3419
|
+
{
|
|
3420
|
+
"line": 153,
|
|
3421
|
+
"content": " return { success: true, results: [], summary: '', errors: [] };"
|
|
3422
|
+
},
|
|
3423
|
+
{
|
|
3424
|
+
"line": 154,
|
|
3425
|
+
"content": " }"
|
|
3426
|
+
},
|
|
3427
|
+
{
|
|
3428
|
+
"line": 155,
|
|
3429
|
+
"content": ""
|
|
3430
|
+
},
|
|
3431
|
+
{
|
|
3432
|
+
"line": 156,
|
|
3433
|
+
"content": " const totalChunks = chunks.length;"
|
|
3434
|
+
},
|
|
3435
|
+
{
|
|
3436
|
+
"line": 157,
|
|
3437
|
+
"content": " const results = [];"
|
|
3438
|
+
},
|
|
3439
|
+
{
|
|
3440
|
+
"line": 158,
|
|
3441
|
+
"content": " const errors = [];"
|
|
3442
|
+
},
|
|
3443
|
+
{
|
|
3444
|
+
"line": 159,
|
|
3445
|
+
"content": ""
|
|
3446
|
+
},
|
|
3447
|
+
{
|
|
3448
|
+
"line": 160,
|
|
3449
|
+
"content": " logger.info("
|
|
3450
|
+
},
|
|
3451
|
+
{
|
|
3452
|
+
"line": 161,
|
|
3453
|
+
"content": " `[DataSplitter] 开始分拆处理: ${totalChunks} 块, 任务=\"${taskDescription.slice(0, 50)}...\"`"
|
|
3454
|
+
},
|
|
3455
|
+
{
|
|
3456
|
+
"line": 162,
|
|
3457
|
+
"content": " );"
|
|
3458
|
+
},
|
|
3459
|
+
{
|
|
3460
|
+
"line": 163,
|
|
3461
|
+
"content": ""
|
|
3462
|
+
}
|
|
3463
|
+
]
|
|
3464
|
+
},
|
|
3465
|
+
{
|
|
3466
|
+
"file": "src\\utils\\data-splitter.js",
|
|
3467
|
+
"line": 247,
|
|
3468
|
+
"content": " const summary = this._buildSummary(results, errors, taskDescription);",
|
|
3469
|
+
"matches": [
|
|
3470
|
+
{
|
|
3471
|
+
"column": 10,
|
|
3472
|
+
"length": 7,
|
|
3473
|
+
"text": "summary"
|
|
3474
|
+
},
|
|
3475
|
+
{
|
|
3476
|
+
"column": 31,
|
|
3477
|
+
"length": 7,
|
|
3478
|
+
"text": "Summary"
|
|
3479
|
+
}
|
|
3480
|
+
],
|
|
3481
|
+
"context": [
|
|
3482
|
+
{
|
|
3483
|
+
"line": 237,
|
|
3484
|
+
"content": " })"
|
|
3485
|
+
},
|
|
3486
|
+
{
|
|
3487
|
+
"line": 238,
|
|
3488
|
+
"content": " )"
|
|
3489
|
+
},
|
|
3490
|
+
{
|
|
3491
|
+
"line": 239,
|
|
3492
|
+
"content": " );"
|
|
3493
|
+
},
|
|
3494
|
+
{
|
|
3495
|
+
"line": 240,
|
|
3496
|
+
"content": " }"
|
|
3497
|
+
},
|
|
3498
|
+
{
|
|
3499
|
+
"line": 241,
|
|
3500
|
+
"content": " }"
|
|
3501
|
+
},
|
|
3502
|
+
{
|
|
3503
|
+
"line": 242,
|
|
3504
|
+
"content": ""
|
|
3505
|
+
},
|
|
3506
|
+
{
|
|
3507
|
+
"line": 243,
|
|
3508
|
+
"content": " // 按块索引排序结果"
|
|
3509
|
+
},
|
|
3510
|
+
{
|
|
3511
|
+
"line": 244,
|
|
3512
|
+
"content": " results.sort((a, b) => a.chunkIndex - b.chunkIndex);"
|
|
3513
|
+
},
|
|
3514
|
+
{
|
|
3515
|
+
"line": 245,
|
|
3516
|
+
"content": ""
|
|
3517
|
+
},
|
|
3518
|
+
{
|
|
3519
|
+
"line": 246,
|
|
3520
|
+
"content": " // 生成汇总摘要"
|
|
3521
|
+
},
|
|
3522
|
+
{
|
|
3523
|
+
"line": 247,
|
|
3524
|
+
"content": " const summary = this._buildSummary(results, errors, taskDescription);"
|
|
3525
|
+
},
|
|
3526
|
+
{
|
|
3527
|
+
"line": 248,
|
|
3528
|
+
"content": ""
|
|
3529
|
+
},
|
|
3530
|
+
{
|
|
3531
|
+
"line": 249,
|
|
3532
|
+
"content": " logger.info("
|
|
3533
|
+
},
|
|
3534
|
+
{
|
|
3535
|
+
"line": 250,
|
|
3536
|
+
"content": " `[DataSplitter] 分拆处理完成: ` +"
|
|
3537
|
+
},
|
|
3538
|
+
{
|
|
3539
|
+
"line": 251,
|
|
3540
|
+
"content": " `${results.filter((r) => r.success).length}/${totalChunks} 块成功, ` +"
|
|
3541
|
+
},
|
|
3542
|
+
{
|
|
3543
|
+
"line": 252,
|
|
3544
|
+
"content": " `${errors.length} 个错误`"
|
|
3545
|
+
},
|
|
3546
|
+
{
|
|
3547
|
+
"line": 253,
|
|
3548
|
+
"content": " );"
|
|
3549
|
+
},
|
|
3550
|
+
{
|
|
3551
|
+
"line": 254,
|
|
3552
|
+
"content": ""
|
|
3553
|
+
},
|
|
3554
|
+
{
|
|
3555
|
+
"line": 255,
|
|
3556
|
+
"content": " return {"
|
|
3557
|
+
},
|
|
3558
|
+
{
|
|
3559
|
+
"line": 256,
|
|
3560
|
+
"content": " success: errors.length === 0,"
|
|
3561
|
+
},
|
|
3562
|
+
{
|
|
3563
|
+
"line": 257,
|
|
3564
|
+
"content": " results,"
|
|
3565
|
+
}
|
|
3566
|
+
]
|
|
3567
|
+
},
|
|
3568
|
+
{
|
|
3569
|
+
"file": "src\\utils\\data-splitter.js",
|
|
3570
|
+
"line": 258,
|
|
3571
|
+
"content": " summary,",
|
|
3572
|
+
"matches": [
|
|
3573
|
+
{
|
|
3574
|
+
"column": 6,
|
|
3575
|
+
"length": 7,
|
|
3576
|
+
"text": "summary"
|
|
3577
|
+
}
|
|
3578
|
+
],
|
|
3579
|
+
"context": [
|
|
3580
|
+
{
|
|
3581
|
+
"line": 248,
|
|
3582
|
+
"content": ""
|
|
3583
|
+
},
|
|
3584
|
+
{
|
|
3585
|
+
"line": 249,
|
|
3586
|
+
"content": " logger.info("
|
|
3587
|
+
},
|
|
3588
|
+
{
|
|
3589
|
+
"line": 250,
|
|
3590
|
+
"content": " `[DataSplitter] 分拆处理完成: ` +"
|
|
3591
|
+
},
|
|
3592
|
+
{
|
|
3593
|
+
"line": 251,
|
|
3594
|
+
"content": " `${results.filter((r) => r.success).length}/${totalChunks} 块成功, ` +"
|
|
3595
|
+
},
|
|
3596
|
+
{
|
|
3597
|
+
"line": 252,
|
|
3598
|
+
"content": " `${errors.length} 个错误`"
|
|
3599
|
+
},
|
|
3600
|
+
{
|
|
3601
|
+
"line": 253,
|
|
3602
|
+
"content": " );"
|
|
3603
|
+
},
|
|
3604
|
+
{
|
|
3605
|
+
"line": 254,
|
|
3606
|
+
"content": ""
|
|
3607
|
+
},
|
|
3608
|
+
{
|
|
3609
|
+
"line": 255,
|
|
3610
|
+
"content": " return {"
|
|
3611
|
+
},
|
|
3612
|
+
{
|
|
3613
|
+
"line": 256,
|
|
3614
|
+
"content": " success: errors.length === 0,"
|
|
3615
|
+
},
|
|
3616
|
+
{
|
|
3617
|
+
"line": 257,
|
|
3618
|
+
"content": " results,"
|
|
3619
|
+
},
|
|
3620
|
+
{
|
|
3621
|
+
"line": 258,
|
|
3622
|
+
"content": " summary,"
|
|
3623
|
+
},
|
|
3624
|
+
{
|
|
3625
|
+
"line": 259,
|
|
3626
|
+
"content": " errors,"
|
|
3627
|
+
},
|
|
3628
|
+
{
|
|
3629
|
+
"line": 260,
|
|
3630
|
+
"content": " };"
|
|
3631
|
+
},
|
|
3632
|
+
{
|
|
3633
|
+
"line": 261,
|
|
3634
|
+
"content": " }"
|
|
3635
|
+
},
|
|
3636
|
+
{
|
|
3637
|
+
"line": 262,
|
|
3638
|
+
"content": ""
|
|
3639
|
+
},
|
|
3640
|
+
{
|
|
3641
|
+
"line": 263,
|
|
3642
|
+
"content": " /**"
|
|
3643
|
+
},
|
|
3644
|
+
{
|
|
3645
|
+
"line": 264,
|
|
3646
|
+
"content": " * 一步完成:检测 → 分拆 → 派发 → 汇总"
|
|
3647
|
+
},
|
|
3648
|
+
{
|
|
3649
|
+
"line": 265,
|
|
3650
|
+
"content": " * 如果内容不超过阈值,直接返回原文"
|
|
3651
|
+
},
|
|
3652
|
+
{
|
|
3653
|
+
"line": 266,
|
|
3654
|
+
"content": " *"
|
|
3655
|
+
},
|
|
3656
|
+
{
|
|
3657
|
+
"line": 267,
|
|
3658
|
+
"content": " * @param {string} content - 原始内容"
|
|
3659
|
+
},
|
|
3660
|
+
{
|
|
3661
|
+
"line": 268,
|
|
3662
|
+
"content": " * @param {string} taskDescription - 要执行的任务"
|
|
3663
|
+
}
|
|
3664
|
+
]
|
|
3665
|
+
},
|
|
3666
|
+
{
|
|
3667
|
+
"file": "src\\utils\\data-splitter.js",
|
|
3668
|
+
"line": 294,
|
|
3669
|
+
"content": " return result.summary;",
|
|
3670
|
+
"matches": [
|
|
3671
|
+
{
|
|
3672
|
+
"column": 18,
|
|
3673
|
+
"length": 7,
|
|
3674
|
+
"text": "summary"
|
|
3675
|
+
}
|
|
3676
|
+
],
|
|
3677
|
+
"context": [
|
|
3678
|
+
{
|
|
3679
|
+
"line": 284,
|
|
3680
|
+
"content": " );"
|
|
3681
|
+
},
|
|
3682
|
+
{
|
|
3683
|
+
"line": 285,
|
|
3684
|
+
"content": ""
|
|
3685
|
+
},
|
|
3686
|
+
{
|
|
3687
|
+
"line": 286,
|
|
3688
|
+
"content": " const chunks = this.splitContent(content, chunkSize);"
|
|
3689
|
+
},
|
|
3690
|
+
{
|
|
3691
|
+
"line": 287,
|
|
3692
|
+
"content": " const result = await this.dispatchToSubAgents({"
|
|
3693
|
+
},
|
|
3694
|
+
{
|
|
3695
|
+
"line": 288,
|
|
3696
|
+
"content": " chunks,"
|
|
3697
|
+
},
|
|
3698
|
+
{
|
|
3699
|
+
"line": 289,
|
|
3700
|
+
"content": " taskDescription,"
|
|
3701
|
+
},
|
|
3702
|
+
{
|
|
3703
|
+
"line": 290,
|
|
3704
|
+
"content": " maxConcurrent: options.maxConcurrent || this.maxConcurrent,"
|
|
3705
|
+
},
|
|
3706
|
+
{
|
|
3707
|
+
"line": 291,
|
|
3708
|
+
"content": " signal: options.signal,"
|
|
3709
|
+
},
|
|
3710
|
+
{
|
|
3711
|
+
"line": 292,
|
|
3712
|
+
"content": " });"
|
|
3713
|
+
},
|
|
3714
|
+
{
|
|
3715
|
+
"line": 293,
|
|
3716
|
+
"content": ""
|
|
3717
|
+
},
|
|
3718
|
+
{
|
|
3719
|
+
"line": 294,
|
|
3720
|
+
"content": " return result.summary;"
|
|
3721
|
+
},
|
|
3722
|
+
{
|
|
3723
|
+
"line": 295,
|
|
3724
|
+
"content": " }"
|
|
3725
|
+
},
|
|
3726
|
+
{
|
|
3727
|
+
"line": 296,
|
|
3728
|
+
"content": ""
|
|
3729
|
+
},
|
|
3730
|
+
{
|
|
3731
|
+
"line": 297,
|
|
3732
|
+
"content": " /**"
|
|
3733
|
+
},
|
|
3734
|
+
{
|
|
3735
|
+
"line": 298,
|
|
3736
|
+
"content": " * 构建汇总"
|
|
3737
|
+
},
|
|
3738
|
+
{
|
|
3739
|
+
"line": 299,
|
|
3740
|
+
"content": " * @private"
|
|
3741
|
+
},
|
|
3742
|
+
{
|
|
3743
|
+
"line": 300,
|
|
3744
|
+
"content": " */"
|
|
3745
|
+
},
|
|
3746
|
+
{
|
|
3747
|
+
"line": 301,
|
|
3748
|
+
"content": " _buildSummary(results, errors, taskDescription) {"
|
|
3749
|
+
},
|
|
3750
|
+
{
|
|
3751
|
+
"line": 302,
|
|
3752
|
+
"content": " const successfulResults = results.filter((r) => r.success && r.message && r.message !== '无有效信息');"
|
|
3753
|
+
},
|
|
3754
|
+
{
|
|
3755
|
+
"line": 303,
|
|
3756
|
+
"content": " const lineCounts = results.map((r) => r.message ? r.message.split('\\n').length : 0);"
|
|
3757
|
+
},
|
|
3758
|
+
{
|
|
3759
|
+
"line": 304,
|
|
3760
|
+
"content": ""
|
|
3761
|
+
}
|
|
3762
|
+
]
|
|
3763
|
+
},
|
|
3764
|
+
{
|
|
3765
|
+
"file": "src\\utils\\data-splitter.js",
|
|
3766
|
+
"line": 301,
|
|
3767
|
+
"content": " _buildSummary(results, errors, taskDescription) {",
|
|
3768
|
+
"matches": [
|
|
3769
|
+
{
|
|
3770
|
+
"column": 8,
|
|
3771
|
+
"length": 7,
|
|
3772
|
+
"text": "Summary"
|
|
3773
|
+
}
|
|
3774
|
+
],
|
|
3775
|
+
"context": [
|
|
3776
|
+
{
|
|
3777
|
+
"line": 291,
|
|
3778
|
+
"content": " signal: options.signal,"
|
|
3779
|
+
},
|
|
3780
|
+
{
|
|
3781
|
+
"line": 292,
|
|
3782
|
+
"content": " });"
|
|
3783
|
+
},
|
|
3784
|
+
{
|
|
3785
|
+
"line": 293,
|
|
3786
|
+
"content": ""
|
|
3787
|
+
},
|
|
3788
|
+
{
|
|
3789
|
+
"line": 294,
|
|
3790
|
+
"content": " return result.summary;"
|
|
3791
|
+
},
|
|
3792
|
+
{
|
|
3793
|
+
"line": 295,
|
|
3794
|
+
"content": " }"
|
|
3795
|
+
},
|
|
3796
|
+
{
|
|
3797
|
+
"line": 296,
|
|
3798
|
+
"content": ""
|
|
3799
|
+
},
|
|
3800
|
+
{
|
|
3801
|
+
"line": 297,
|
|
3802
|
+
"content": " /**"
|
|
3803
|
+
},
|
|
3804
|
+
{
|
|
3805
|
+
"line": 298,
|
|
3806
|
+
"content": " * 构建汇总"
|
|
3807
|
+
},
|
|
3808
|
+
{
|
|
3809
|
+
"line": 299,
|
|
3810
|
+
"content": " * @private"
|
|
3811
|
+
},
|
|
3812
|
+
{
|
|
3813
|
+
"line": 300,
|
|
3814
|
+
"content": " */"
|
|
3815
|
+
},
|
|
3816
|
+
{
|
|
3817
|
+
"line": 301,
|
|
3818
|
+
"content": " _buildSummary(results, errors, taskDescription) {"
|
|
3819
|
+
},
|
|
3820
|
+
{
|
|
3821
|
+
"line": 302,
|
|
3822
|
+
"content": " const successfulResults = results.filter((r) => r.success && r.message && r.message !== '无有效信息');"
|
|
3823
|
+
},
|
|
3824
|
+
{
|
|
3825
|
+
"line": 303,
|
|
3826
|
+
"content": " const lineCounts = results.map((r) => r.message ? r.message.split('\\n').length : 0);"
|
|
3827
|
+
},
|
|
3828
|
+
{
|
|
3829
|
+
"line": 304,
|
|
3830
|
+
"content": ""
|
|
3831
|
+
},
|
|
3832
|
+
{
|
|
3833
|
+
"line": 305,
|
|
3834
|
+
"content": " const totalLines = lineCounts.reduce((a, b) => a + b, 0);"
|
|
3835
|
+
},
|
|
3836
|
+
{
|
|
3837
|
+
"line": 306,
|
|
3838
|
+
"content": ""
|
|
3839
|
+
},
|
|
3840
|
+
{
|
|
3841
|
+
"line": 307,
|
|
3842
|
+
"content": " const lines = ["
|
|
3843
|
+
},
|
|
3844
|
+
{
|
|
3845
|
+
"line": 308,
|
|
3846
|
+
"content": " `## 大数据处理汇总`,"
|
|
3847
|
+
},
|
|
3848
|
+
{
|
|
3849
|
+
"line": 309,
|
|
3850
|
+
"content": " ``,"
|
|
3851
|
+
},
|
|
3852
|
+
{
|
|
3853
|
+
"line": 310,
|
|
3854
|
+
"content": " `**任务**: ${taskDescription}`,"
|
|
3855
|
+
},
|
|
3856
|
+
{
|
|
3857
|
+
"line": 311,
|
|
3858
|
+
"content": " `**总块数**: ${results.length}`,"
|
|
3859
|
+
}
|
|
3860
|
+
]
|
|
3861
|
+
}
|
|
3862
|
+
],
|
|
3863
|
+
"metadata": {
|
|
3864
|
+
"pattern": "summary",
|
|
3865
|
+
"total": 6,
|
|
3866
|
+
"stats": {
|
|
3867
|
+
"filesWithMatches": 1,
|
|
3868
|
+
"totalMatches": 7,
|
|
3869
|
+
"searchPath": "src/utils"
|
|
3870
|
+
}
|
|
3871
|
+
}
|
|
3872
|
+
}
|
|
3873
|
+
}
|
|
3874
|
+
}
|
|
3875
|
+
]
|
|
3876
|
+
},
|
|
3877
|
+
{
|
|
3878
|
+
"role": "assistant",
|
|
3879
|
+
"content": [
|
|
3880
|
+
{
|
|
3881
|
+
"type": "text",
|
|
3882
|
+
"text": "<think>让我看看 `_buildSummary` 的完整实现来理解 summary 是如何生成的。\n</think>\n\n\n"
|
|
3883
|
+
},
|
|
3884
|
+
{
|
|
3885
|
+
"type": "tool-call",
|
|
3886
|
+
"toolCallId": "call_function_0p8lkhzotjc0_1",
|
|
3887
|
+
"toolName": "bash",
|
|
3888
|
+
"input": {
|
|
3889
|
+
"cmd": "sed -n '301,360p' src/utils/data-splitter.js"
|
|
3890
|
+
}
|
|
3891
|
+
}
|
|
3892
|
+
]
|
|
3893
|
+
},
|
|
3894
|
+
{
|
|
3895
|
+
"role": "tool",
|
|
3896
|
+
"content": [
|
|
3897
|
+
{
|
|
3898
|
+
"type": "tool-result",
|
|
3899
|
+
"toolCallId": "call_function_0p8lkhzotjc0_1",
|
|
3900
|
+
"toolName": "bash",
|
|
3901
|
+
"output": {
|
|
3902
|
+
"type": "json",
|
|
3903
|
+
"value": {
|
|
3904
|
+
"success": true,
|
|
3905
|
+
"data": " _buildSummary(results, errors, taskDescription) {\n const successfulResults = results.filter((r) => r.success && r.message && r.message !== '无有效信息');\n const lineCounts = results.map((r) => r.message ? r.message.split('\\n').length : 0);\n\n const totalLines = lineCounts.reduce((a, b) => a + b, 0);\n\n const lines = [\n `## 大数据处理汇总`,\n ``,\n `**任务**: ${taskDescription}`,\n `**总块数**: ${results.length}`,\n `**成功**: ${successfulResults.length} 块`,\n `**失败**: ${errors.length} 块`,\n ``,\n `### 各块处理结果`,\n ``,\n ];\n\n for (const r of results) {\n const status = r.success ? '✅' : '❌';\n const msg = r.message || r.error || '无结果';\n // 每块只取关键信息,避免重复\n const trimmed = msg.split('\\n').slice(0, 3).join('\\n');\n lines.push(`**块 ${r.chunkIndex + 1}** ${status}`);\n lines.push(`> ${trimmed}`);\n lines.push('');\n }\n\n if (errors.length > 0) {\n lines.push(`### 错误信息`);\n lines.push('');\n for (const e of errors) {\n lines.push(`- 块 ${e.chunkIndex + 1}: ${e.error}`);\n }\n lines.push('');\n }\n\n lines.push(`---`);\n lines.push(`*共 ${results.length} 块, ${successfulResults.length} 块成功, 汇总 ${totalLines} 行*`);\n\n return lines.join('\\n');\n }\n}\n\nmodule.exports = { DataSplitter };\n",
|
|
3906
|
+
"metadata": {
|
|
3907
|
+
"command": "sed -n '301,360p' src/utils/data-splitter.js",
|
|
3908
|
+
"stderr": "",
|
|
3909
|
+
"duration": 88
|
|
3910
|
+
}
|
|
3911
|
+
}
|
|
3912
|
+
}
|
|
3913
|
+
}
|
|
3914
|
+
]
|
|
3915
|
+
},
|
|
3916
|
+
{
|
|
3917
|
+
"role": "assistant",
|
|
3918
|
+
"content": [
|
|
3919
|
+
{
|
|
3920
|
+
"type": "text",
|
|
3921
|
+
"text": "<think>我看到问题了!在 `_buildSummary` 中:\n\n```javascript\n// 每块只取关键信息,避免重复\nconst trimmed = msg.split('\\n').slice(0, 3).join('\\n');\nlines.push(`**块 ${r.chunkIndex + 1}** ${status}`);\nlines.push(`> ${trimmed}`);\n```\n\n这里 **每块只取了前 3 行**!如果子 Agent 处理的结果超过 3 行,剩余内容就被丢弃了。\n\n这可能导致:\n1. 分拆后的数据不完整\n2. AI 看到的汇总只是部分信息\n3. 回复可能不完整或基于不完整的信息\n\n但这主要影响的是工具结果的处理,不是直接导致回复截断。\n\n让我看看问题可能更直接的地方。也许是 `dispatchToSubAgents` 内部的问题?\n</think>\n\n\n"
|
|
3922
|
+
},
|
|
3923
|
+
{
|
|
3924
|
+
"type": "tool-call",
|
|
3925
|
+
"toolCallId": "call_function_6dz1kib3xve6_1",
|
|
3926
|
+
"toolName": "bash",
|
|
3927
|
+
"input": {
|
|
3928
|
+
"cmd": "sed -n '160,260p' src/utils/data-splitter.js"
|
|
3929
|
+
}
|
|
3930
|
+
}
|
|
3931
|
+
]
|
|
3932
|
+
},
|
|
3933
|
+
{
|
|
3934
|
+
"role": "tool",
|
|
3935
|
+
"content": [
|
|
3936
|
+
{
|
|
3937
|
+
"type": "tool-result",
|
|
3938
|
+
"toolCallId": "call_function_6dz1kib3xve6_1",
|
|
3939
|
+
"toolName": "bash",
|
|
3940
|
+
"output": {
|
|
3941
|
+
"type": "json",
|
|
3942
|
+
"value": {
|
|
3943
|
+
"success": true,
|
|
3944
|
+
"data": " logger.info(\n `[DataSplitter] 开始分拆处理: ${totalChunks} 块, 任务=\"${taskDescription.slice(0, 50)}...\"`\n );\n\n // 并行执行(受 maxConcurrent 限制)\n const queue = [...chunks];\n const inFlight = new Set();\n\n const processChunk = async (chunk) => {\n const chunkIndex = chunk.index;\n\n // 为当前块创建子 Agent\n const subagent = this.framework.createSubAgent({\n name: `${agentName}-chunk-${chunkIndex}`,\n role: agentRole,\n systemPrompt: `你是${agentRole},负责处理大数据中的第 ${chunkIndex + 1}/${totalChunks} 块。`,\n maxRetries: this.maxRetries,\n disableTools: true, // 分拆处理只做文本分析,不需要额外工具\n });\n\n try {\n const taskMsg = `## 任务\n${taskDescription}\n\n## 待处理内容(第 ${chunkIndex + 1}/${totalChunks} 块)\n\\`\\`\\`\n${chunk.content}\n\\`\\`\\`\n\n## 要求\n- 只处理这块内容\n- 返回简洁的结果\n- 如果这块内容没有有用信息,返回\"无有效信息\"`;\n\n const result = await subagent.chat(taskMsg, { signal });\n results.push({\n chunkIndex,\n success: result.success,\n message: result.message || '',\n });\n\n logger.debug(\n `[DataSplitter] 块 ${chunkIndex + 1}/${totalChunks} 处理完成: ` +\n `${result.success ? '成功' : '失败'}`\n );\n } catch (err) {\n errors.push({ chunkIndex, error: err.message });\n results.push({ chunkIndex, success: false, error: err.message });\n logger.warn(\n `[DataSplitter] 块 ${chunkIndex + 1}/${totalChunks} 处理失败: ${err.message}`\n );\n } finally {\n subagent.destroy();\n inFlight.delete(chunk);\n }\n };\n\n // 并发控制:最多 maxConcurrent 个同时运行\n while (queue.length > 0 || inFlight.size > 0) {\n // 填充 inFlight 到 maxConcurrent\n while (queue.length > 0 && inFlight.size < maxConcurrent) {\n const chunk = queue.shift();\n inFlight.add(chunk);\n processChunk(chunk); // 不 await,并发执行\n }\n\n // 等待任意一个完成\n if (inFlight.size > 0) {\n await Promise.race(\n [...inFlight].map(\n (chunk) =>\n new Promise((resolve) => {\n const check = () => {\n if (!inFlight.has(chunk)) resolve();\n else setTimeout(check, 50);\n };\n check();\n })\n )\n );\n }\n }\n\n // 按块索引排序结果\n results.sort((a, b) => a.chunkIndex - b.chunkIndex);\n\n // 生成汇总摘要\n const summary = this._buildSummary(results, errors, taskDescription);\n\n logger.info(\n `[DataSplitter] 分拆处理完成: ` +\n `${results.filter((r) => r.success).length}/${totalChunks} 块成功, ` +\n `${errors.length} 个错误`\n );\n\n return {\n success: errors.length === 0,\n results,\n summary,\n errors,\n };\n",
|
|
3945
|
+
"metadata": {
|
|
3946
|
+
"command": "sed -n '160,260p' src/utils/data-splitter.js",
|
|
3947
|
+
"stderr": "",
|
|
3948
|
+
"duration": 90
|
|
3949
|
+
}
|
|
3950
|
+
}
|
|
3951
|
+
}
|
|
3952
|
+
}
|
|
3953
|
+
]
|
|
3954
|
+
},
|
|
3955
|
+
{
|
|
3956
|
+
"role": "assistant",
|
|
3957
|
+
"content": [
|
|
3958
|
+
{
|
|
3959
|
+
"type": "text",
|
|
3960
|
+
"text": "<think>现在我理解了整个流程。问题可能出在几个地方:\n\n1. **autoSplitToolResult 返回的是一个包装对象**:\n```javascript\nreturn {\n wasSplit: true,\n result: {\n _autoSplit: true,\n _originalSize: checkContent.length,\n _summary: splitResult.summary,\n _originalResult: result,\n },\n splitterResult: splitResult,\n};\n```\n\n2. **关键问题:splitResult.summary 是汇总文本**,而 `_originalResult` 保留了原始结果。但 AI 可能只看到 `_summary` 而不是完整内容。\n\n3. **AI SDK 的 ToolLoopAgent 看到的工具返回结果**被包装成了:\n```javascript\n{\n _autoSplit: true,\n _originalSize: ...,\n _summary: \"## 大数据处理汇总\\n\\n**任务**: 从以下内容中提取关键信息...\\n**总块数**: N\\n**成功**: M 块\\n...\",\n _originalResult: result, // 原始的未分拆结果\n}\n```\n\n**AI 看到的可能只有 _summary,而不是完整的原始内容**,导致 AI 在回答时缺少信息,回复可能被截断或不完整。\n\n但等等,这说的是**工具返回结果太大被分拆**,这应该影响的是 AI 看到的信息量,而不是 AI 的回复长度。\n\n让我再想想...回复中断可能和这个无关,而是:\n1. AI 回复太长被 maxOutputTokens 截断\n\n\n2. 网络问题导致流式中断\n3. 某个异常被 catch 但没有正确处理\n\n用户说\"回复一半就突然断了\",如果是工具调用后 AI 给出答案,那么工具结果被 autoSplit 包装可能导致 AI 看到的是汇总而不是原始数据,这样 AI 的回复就会基于不完整的信息,可能导致回复不准确或被截断。\n\n需要检查 `_buildSummary` 中每块只取前 3 行这样的逻辑,看是否会导致 AI 收到的信息不足。\n</think>\n\n我仔细看了代码,发现 **`autoSplitToolResult` 返回的结果有严重问题**:\n\n## 🐛 问题分析\n\n```javascript\n// autoSplitToolResult 返回的是:\n{\n wasSplit: true,\n result: {\n _autoSplit: true,\n _originalSize: checkContent.length,\n _summary: \"## 大数据处理汇总\\n**任务**: ...\\n**总块数**: N\\n**成功**: M 块\\n...\", // ← 这是 AI 主要看到的内容\n _originalResult: result, // 原始结果被埋在这里\n }\n}\n```\n\n### 问题1:AI 看到的是 `_summary`,而不是完整内容\n\n`_buildSummary` 每块**只取前 3 行**:\n```javascript\n// data-splitter.js:319\nconst trimmed = msg.split('\\n').slice(0, 3).join('\\n'); // 每块只保留3行!\nlines.push(`> ${trimmed}`);\n```\n\n这导致 AI 只能看到**高度压缩的摘要**,丢失大量原始信息。\n\n### 问题2:`_originalResult` 嵌套太深\n\nAI 收到的工具结果里,`_originalResult` 被嵌套在多个层级下,可能被忽略或截断。\n\n---\n\n## 🔧 修复建议\n\n**临时解决方案**:禁用 `autoSplitToolResult`(因为它还处于实验阶段,且可能导致信息丢失)\n\n```javascript\n// 在 agent-chat.js 中,把 autoSplit 包裹在 try-catch 中直接跳过\ntry {\n const splitCheck = await autoSplitToolResult(toolName, result, this.agent.framework);\n if (splitCheck.wasSplit && splitCheck.result) {\n finalResult = splitCheck.result;\n // ...\n }\n} catch (splitErr) {\n logger.warn(`[AutoSplit] 自动分拆跳过: ${splitErr.message}`);\n // 发生异常时回退到原始结果\n finalResult = result; // ← 加上这行\n}\n```\n\n---\n\n要我现在帮你修复这个问题吗?或者你想先确认一下是不是这个原因导致的(比如禁用 data-splitter 插件后问题消失)?"
|
|
3961
|
+
}
|
|
3962
|
+
]
|
|
3963
|
+
},
|
|
3964
|
+
{
|
|
3965
|
+
"role": "user",
|
|
3966
|
+
"content": "帮我禁用这个"
|
|
3967
|
+
},
|
|
3968
|
+
{
|
|
3969
|
+
"role": "assistant",
|
|
3970
|
+
"content": [
|
|
3971
|
+
{
|
|
3972
|
+
"type": "text",
|
|
3973
|
+
"text": "<think>用户想要禁用 autoSplitToolResult 功能。我需要在 agent-chat.js 中修改代码,使得当 autoSplitToolResult 出错或返回 wasSplit: true 时,都能正确回退到原始结果而不是使用被截断的 summary。\n\n让我先看一下当前的代码,然后进行修改。\n</think>\n\n\n"
|
|
3974
|
+
},
|
|
3975
|
+
{
|
|
3976
|
+
"type": "tool-call",
|
|
3977
|
+
"toolCallId": "call_function_mtiiz5s2ok5o_1",
|
|
3978
|
+
"toolName": "bash",
|
|
3979
|
+
"input": {
|
|
3980
|
+
"cmd": "sed -n '855,880p' src/core/agent-chat.js"
|
|
3981
|
+
}
|
|
3982
|
+
}
|
|
3983
|
+
]
|
|
3984
|
+
},
|
|
3985
|
+
{
|
|
3986
|
+
"role": "tool",
|
|
3987
|
+
"content": [
|
|
3988
|
+
{
|
|
3989
|
+
"type": "tool-result",
|
|
3990
|
+
"toolCallId": "call_function_mtiiz5s2ok5o_1",
|
|
3991
|
+
"toolName": "bash",
|
|
3992
|
+
"output": {
|
|
3993
|
+
"type": "json",
|
|
3994
|
+
"value": {
|
|
3995
|
+
"success": true,
|
|
3996
|
+
"data": " try {\n const result = await toolDef.execute(cleanedArgs, this.agent.framework);\n\n // 自动检测大工具结果,透明分拆\n // 仅在数据分拆插件已加载时生效\n let finalResult = result;\n try {\n const splitCheck = await autoSplitToolResult(toolName, result, this.agent.framework);\n if (splitCheck.wasSplit && splitCheck.result) {\n finalResult = splitCheck.result;\n logger.info(\n `[AutoSplit] 工具 \"${toolName}\" 结果过大,已自动分拆处理`\n );\n }\n } catch (splitErr) {\n // 分拆失败不阻断主流程,继续使用原始结果\n logger.warn(`[AutoSplit] 自动分拆跳过: ${splitErr.message}`);\n }\n\n this.emit('tool-result', { name: toolName, args: cleanedArgs, result: finalResult });\n return finalResult;\n } catch (err) {\n this.emit('tool-error', { name: toolName, args: cleanedArgs, error: err.message });\n // 返回错误信息字符串,而不是抛出异常\n return { success: false,error: err.message}\n }\n",
|
|
3997
|
+
"metadata": {
|
|
3998
|
+
"command": "sed -n '855,880p' src/core/agent-chat.js",
|
|
3999
|
+
"stderr": "",
|
|
4000
|
+
"duration": 83
|
|
4001
|
+
}
|
|
4002
|
+
}
|
|
4003
|
+
}
|
|
4004
|
+
}
|
|
4005
|
+
]
|
|
4006
|
+
},
|
|
4007
|
+
{
|
|
4008
|
+
"role": "assistant",
|
|
4009
|
+
"content": [
|
|
4010
|
+
{
|
|
4011
|
+
"type": "text",
|
|
4012
|
+
"text": "<think>I can see the code. The issue is that when `autoSplitToolResult` is called and returns `{ wasSplit: true, result: ... }`, it sets `finalResult = splitCheck.result` which is the summarized/transformed result. But if `wasSplit` is false or an error occurs, it should use the original `result`.\n\nLooking at the code:\n```javascript\nlet finalResult = result;\ntry {\n const splitCheck = await autoSplitToolResult(toolName, result, this.agent.framework);\n if (splitCheck.wasSplit && splitCheck.result) {\n finalResult = splitCheck.result;\n ...\n }\n} catch (splitErr) {\n logger.warn(`[AutoSplit] 自动分拆跳过: ${splitErr.message}`);\n}\n```\n\nThe problem is:\n1. When `wasSplit` is true, it replaces `finalResult` with the summarized version\n2. When an exception occurs, it just logs and continues with the current `finalResult`\n3. But if `autoSplitToolResult` returns normally but `wasSplit` is false, it should keep the original result - which it does because of `let finalResult = result`\n\nWait, actually looking more carefully - if `wasSplit` is false, it won't enter the if block, so `finalResult` stays as `result`. That's correct.\n\nThe issue is when `wasSplit` is true - it uses the summarized result instead of the original. This is intentional but buggy because the summarized result loses information.\n\nThe simplest fix is to completely disable auto-splitting. I'll just comment out or remove the autoSplitToolResult call and always use the original result.\n</think>\n\n\n"
|
|
4013
|
+
},
|
|
4014
|
+
{
|
|
4015
|
+
"type": "tool-call",
|
|
4016
|
+
"toolCallId": "call_function_h9md62zoitfp_1",
|
|
4017
|
+
"toolName": "edit",
|
|
4018
|
+
"input": {
|
|
4019
|
+
"path": "src/core/agent-chat.js",
|
|
4020
|
+
"edits": [
|
|
4021
|
+
{
|
|
4022
|
+
"oldText": " // 自动检测大工具结果,透明分拆\n // 仅在数据分拆插件已加载时生效\n let finalResult = result;\n try {\n const splitCheck = await autoSplitToolResult(toolName, result, this.agent.framework);\n if (splitCheck.wasSplit && splitCheck.result) {\n finalResult = splitCheck.result;\n logger.info(\n `[AutoSplit] 工具 \"${toolName}\" 结果过大,已自动分拆处理`\n );\n }\n } catch (splitErr) {\n // 分拆失败不阻断主流程,继续使用原始结果\n logger.warn(`[AutoSplit] 自动分拆跳过: ${splitErr.message}`);\n }",
|
|
4023
|
+
"newText": " // 自动检测大工具结果,透明分拆\n // ⚠️ 已禁用:autoSplitToolResult 会导致信息丢失(摘要只保留每块前3行)\n // 如需启用,修改为 try/catch 包裹并在 catch 中保持 finalResult = result\n const finalResult = result;\n // try {\n // const splitCheck = await autoSplitToolResult(toolName, result, this.agent.framework);\n // if (splitCheck.wasSplit && splitCheck.result) {\n // finalResult = splitCheck.result;\n // logger.info(\n // `[AutoSplit] 工具 \"${toolName}\" 结果过大,已自动分拆处理`\n // );\n // }\n // } catch (splitErr) {\n // logger.warn(`[AutoSplit] 自动分拆跳过: ${splitErr.message}`);\n // }"
|
|
4024
|
+
}
|
|
4025
|
+
]
|
|
4026
|
+
}
|
|
4027
|
+
}
|
|
4028
|
+
]
|
|
4029
|
+
},
|
|
4030
|
+
{
|
|
4031
|
+
"role": "tool",
|
|
4032
|
+
"content": [
|
|
4033
|
+
{
|
|
4034
|
+
"type": "tool-result",
|
|
4035
|
+
"toolCallId": "call_function_h9md62zoitfp_1",
|
|
4036
|
+
"toolName": "edit",
|
|
4037
|
+
"output": {
|
|
4038
|
+
"type": "json",
|
|
4039
|
+
"value": {
|
|
4040
|
+
"success": true,
|
|
4041
|
+
"diff": " ...\n 855 try {\n 856 const result = await toolDef.execute(cleanedArgs, this.agent.framework);\n 857 \n 858 // 自动检测大工具结果,透明分拆\n- 859 // 仅在数据分拆插件已加载时生效\n- 860 let finalResult = result;\n- 861 try {\n- 862 const splitCheck = await autoSplitToolResult(toolName, result, this.agent.framework);\n- 863 if (splitCheck.wasSplit && splitCheck.result) {\n- 864 finalResult = splitCheck.result;\n- 865 logger.info(\n- 866 `[AutoSplit] 工具 \"${toolName}\" 结果过大,已自动分拆处理`\n- 867 );\n- 868 }\n- 869 } catch (splitErr) {\n- 870 // 分拆失败不阻断主流程,继续使用原始结果\n- 871 logger.warn(`[AutoSplit] 自动分拆跳过: ${splitErr.message}`);\n- 872 }\n+ 859 // ⚠️ 已禁用:autoSplitToolResult 会导致信息丢失(摘要只保留每块前3行)\n+ 860 // 如需启用,修改为 try/catch 包裹并在 catch 中保持 finalResult = result\n+ 861 const finalResult = result;\n+ 862 // try {\n+ 863 // const splitCheck = await autoSplitToolResult(toolName, result, this.agent.framework);\n+ 864 // if (splitCheck.wasSplit && splitCheck.result) {\n+ 865 // finalResult = splitCheck.result;\n+ 866 // logger.info(\n+ 867 // `[AutoSplit] 工具 \"${toolName}\" 结果过大,已自动分拆处理`\n+ 868 // );\n+ 869 // }\n+ 870 // } catch (splitErr) {\n+ 871 // logger.warn(`[AutoSplit] 自动分拆跳过: ${splitErr.message}`);\n+ 872 // }\n 873 \n 874 this.emit('tool-result', { name: toolName, args: cleanedArgs, result: finalResult });\n 875 return finalResult;\n 876 } catch (err) {\n ...",
|
|
4042
|
+
"firstChangedLine": 859,
|
|
4043
|
+
"filePath": "src/core/agent-chat.js"
|
|
4044
|
+
}
|
|
4045
|
+
}
|
|
4046
|
+
}
|
|
4047
|
+
]
|
|
4048
|
+
},
|
|
4049
|
+
{
|
|
4050
|
+
"role": "assistant",
|
|
4051
|
+
"content": [
|
|
4052
|
+
{
|
|
4053
|
+
"type": "text",
|
|
4054
|
+
"text": "<think>好,已经禁用了 autoSplitToolResult。现在还需要移除顶部的 import,这样代码更干净。\n</think>\n\n现在移除未使用的 import:\n"
|
|
4055
|
+
},
|
|
4056
|
+
{
|
|
4057
|
+
"type": "tool-call",
|
|
4058
|
+
"toolCallId": "call_function_r2tjwlk3yjq6_1",
|
|
4059
|
+
"toolName": "edit",
|
|
4060
|
+
"input": {
|
|
4061
|
+
"path": "src/core/agent-chat.js",
|
|
4062
|
+
"edits": [
|
|
4063
|
+
{
|
|
4064
|
+
"oldText": "const { autoSplitToolResult } = require('../../plugins/data-splitter-plugin');\n",
|
|
4065
|
+
"newText": ""
|
|
4066
|
+
}
|
|
4067
|
+
]
|
|
4068
|
+
}
|
|
4069
|
+
}
|
|
4070
|
+
]
|
|
4071
|
+
},
|
|
4072
|
+
{
|
|
4073
|
+
"role": "tool",
|
|
4074
|
+
"content": [
|
|
4075
|
+
{
|
|
4076
|
+
"type": "tool-result",
|
|
4077
|
+
"toolCallId": "call_function_r2tjwlk3yjq6_1",
|
|
4078
|
+
"toolName": "edit",
|
|
4079
|
+
"output": {
|
|
4080
|
+
"type": "json",
|
|
4081
|
+
"value": {
|
|
4082
|
+
"success": true,
|
|
4083
|
+
"diff": " ...\n 14 tool: aiTool,\n 15 ToolLoopAgent,\n 16 isLoopFinished,\n 17 } = require('ai');\n- 18 const { autoSplitToolResult } = require('../../plugins/data-splitter-plugin');\n 19 const { cleanResponse } = require('../utils');\n 20 const { ChatQueueManager } = require('../utils/chat-queue');\n 21 const { TokenCounter } = require('./token-counter');\n 22 const { isThinkingModel } = require('./provider');\n ...",
|
|
4084
|
+
"firstChangedLine": 18,
|
|
4085
|
+
"filePath": "src/core/agent-chat.js"
|
|
4086
|
+
}
|
|
4087
|
+
}
|
|
4088
|
+
}
|
|
4089
|
+
]
|
|
4090
|
+
},
|
|
4091
|
+
{
|
|
4092
|
+
"role": "assistant",
|
|
4093
|
+
"content": [
|
|
4094
|
+
{
|
|
4095
|
+
"type": "text",
|
|
4096
|
+
"text": "<think>Good, I've disabled autoSplitToolResult. Now let me verify the changes look correct.\n</think>\n\n已禁用 `autoSplitToolResult`。修改内容:\n\n1. **移除了 import**:不再引入 `autoSplitToolResult`\n2. **注释掉了调用逻辑**:工具结果会直接返回原始值,不再经过分拆处理\n\n测试建议:\n1. 重启 Foliko 服务\n2. 尝试之前会中断的对话,看是否恢复正常\n\n如果问题消失,说明确实是 `autoSplitToolResult` 导致的。之后可以考虑修复它(让 AI 能看到完整内容而不是只有摘要),或者保持禁用状态。"
|
|
4097
|
+
}
|
|
4098
|
+
]
|
|
4099
|
+
},
|
|
4100
|
+
{
|
|
4101
|
+
"role": "user",
|
|
4102
|
+
"content": "data-splitter-plugin.js 这个插件有什么用?"
|
|
4103
|
+
},
|
|
4104
|
+
{
|
|
4105
|
+
"role": "assistant",
|
|
4106
|
+
"content": [
|
|
4107
|
+
{
|
|
4108
|
+
"type": "text",
|
|
4109
|
+
"text": "<think>用户问 data-splitter-plugin.js 这个插件有什么用。让我总结一下它的功能。\n</think>\n\n`data-splitter-plugin.js` 插件有两个主要功能:\n\n## 📋 功能说明\n\n### 1. 注册两个工具给 AI 调用\n\n| 工具 | 作用 |\n|------|------|\n| `split_and_process` | AI 主动调用,将大文本分块给子 Agent 并行处理,最后汇总 |\n| `get_content_preview` | 获取大内容的前 N 行预览和统计信息 |\n\n### 2. 自动检测大工具结果\n\n当某个工具返回的内容超过 **50000 字符** 时,自动触发分拆处理,把摘要返回给 AI。\n\n---\n\n## ⚠️ 问题所在\n\n这个插件的设计有缺陷:\n\n```javascript\n// _buildSummary 每块只取前3行\nconst trimmed = msg.split('\\n').slice(0, 3).join('\\n');\n```\n\nAI 只能看到类似这样的摘要:\n```\n## 大数据处理汇总\n**任务**: 从以下内容中提取关键信息...\n**总块数**: 3\n**成功**: 3 块\n### 各块处理结果\n**块 1** ✅\n> 第一块的关键信息...\n**块 2** ✅\n> 第二块的关键信息...\n**块 3** ✅\n> 第三块的关键信息...\n---\n*共 3 块, 3 块成功, 汇总 9 行*\n```\n\n**原始内容完全丢失了**。\n\n---\n\n## 🤔 实际使用场景\n\n这个插件本来是为了处理这种情况:\n\n> AI 调用 `read_file` 读取一个大文件(10万字符),原始内容太多塞不进 context window,所以插件自动分拆成多块并行处理,生成摘要给 AI。\n\n**但问题是**:AI 需要的是**完整内容**来回答问题,而不是摘要。摘要会导致 AI \"看不全\" 内容,回答不准确。\n\n---\n\n## 💡 结论\n\n- 如果你发现 AI 读取大文件后回答不准确,可能是这个插件在搞鬼\n- 更好的方案是让 AI 自己决定是否需要分拆(通过 `split_and_process` 工具),而不是自动介入\n\n目前已在 `agent-chat.js` 中禁用了自动分拆,保留插件本身但关闭了自动调用。需要我进一步调整吗?"
|
|
4110
|
+
}
|
|
4111
|
+
]
|
|
238
4112
|
}
|
|
239
4113
|
],
|
|
240
4114
|
"variables": {},
|