diff --git a/src/AI/context.ts b/src/AI/context.ts index 7c3b81a..8515202 100644 --- a/src/AI/context.ts +++ b/src/AI/context.ts @@ -7,6 +7,7 @@ import { AI, AIManager, GroupInfo, UserInfo } from "./AI"; import { logger } from "../logger"; import { netExists, getFriendList, getGroupList, getGroupMemberInfo, getGroupMemberList, getStrangerInfo } from "../utils/utils_ob11"; import { revive } from "../utils/utils"; +import { sendToolRespondCompressRequest } from "../service"; export interface MessageInfo { msgId: string; @@ -228,6 +229,31 @@ export class Context { } } + async compressToolResponseIfNeeded(toolName: string, content: string, searchTarget: string = ''): Promise { + const { toolResponseCompressMinLength, toolResponseCompressPromptTemplate } = ConfigManager.tool; + if (!content.trim()) return content; + if (content.length < toolResponseCompressMinLength) return content; + + let prompt = toolResponseCompressPromptTemplate({ + "函数名": toolName, + "搜索目标": searchTarget + }); + if (toolName === 'web_search' && searchTarget.trim()) { + prompt += `\n\n搜索目标:\n${searchTarget.trim()}`; + } + + const summary = (await sendToolRespondCompressRequest([ + { role: 'system', content: prompt }, + { role: 'user', content } + ])).trim(); + + if (!summary) { + logger.warning(`工具响应压缩返回为空,保留原始响应: ${toolName}`); + return content; + } + return summary; + } + async findUserInfo(ctx: seal.MsgContext, name: string | number, findInFriendList: boolean = false): Promise { name = String(name); if (!name) return null; diff --git a/src/config/config_tool.ts b/src/config/config_tool.ts index aa7004b..e818ebd 100644 --- a/src/config/config_tool.ts +++ b/src/config/config_tool.ts @@ -20,6 +20,18 @@ export class ToolConfig { 'whole_ban', 'get_ban_list' ], "修改后保存并重载js"); + seal.ext.registerIntConfig(ToolConfig.ext, "工具响应压缩触发字数", 10000, "仅对 web_search、web_read、get_context 生效,当工具响应达到该字数时进行压缩"); + seal.ext.registerTemplateConfig(ToolConfig.ext, "工具响应压缩prompt模板", [ + `你是文本压缩助手。请将用户提供的工具返回结果压缩为简洁摘要,保留关键信息、链接、结论和可执行事项。只输出压缩结果正文。` + ], ""); + seal.ext.registerStringConfig(ToolConfig.ext, "长响应工具压缩 url地址", "", "为空时默认使用对话接口"); + seal.ext.registerStringConfig(ToolConfig.ext, "长响应工具压缩 API Key", "", "为空时默认使用对话接口"); + seal.ext.registerTemplateConfig(ToolConfig.ext, "长响应工具压缩 body", [ + `"model":"deepseek-chat"`, + `"max_tokens":1024`, + `"stop":null`, + `"stream":false` + ], "为空时默认使用对话接口body,messages不存在时将会自动替换"); seal.ext.registerTemplateConfig(ToolConfig.ext, "默认关闭的函数", [ 'rename', 'record', @@ -61,6 +73,11 @@ export class ToolConfig { toolsPromptTemplate: ConfigManager.getHandlebarsTemplateConfig(ToolConfig.ext, "工具函数prompt模板"), maxCallCount: seal.ext.getIntConfig(ToolConfig.ext, "允许连续调用函数次数"), toolsNotAllow: seal.ext.getTemplateConfig(ToolConfig.ext, "不允许调用的函数"), + toolResponseCompressMinLength: seal.ext.getIntConfig(ToolConfig.ext, "工具响应压缩触发字数"), + toolResponseCompressPromptTemplate: ConfigManager.getHandlebarsTemplateConfig(ToolConfig.ext, "工具响应压缩prompt模板"), + contextCompressUrl: seal.ext.getStringConfig(ToolConfig.ext, "长响应工具压缩 url地址"), + contextCompressApiKey: seal.ext.getStringConfig(ToolConfig.ext, "长响应工具压缩 API Key"), + contextCompressBodyTemplate: seal.ext.getTemplateConfig(ToolConfig.ext, "长响应工具压缩 body"), toolsDefaultClosed: seal.ext.getTemplateConfig(ToolConfig.ext, "默认关闭的函数"), decks: seal.ext.getTemplateConfig(ToolConfig.ext, "提供给AI的牌堆名称"), character: seal.ext.getOptionConfig(ToolConfig.ext, "ai语音使用的音色"), diff --git a/src/service.ts b/src/service.ts index 1ed8e0e..2171560 100644 --- a/src/service.ts +++ b/src/service.ts @@ -78,6 +78,42 @@ export async function sendITTRequest(messages: { } } +export async function sendToolRespondCompressRequest(messages: { + role: string, + content: string +}[]): Promise { + const { timeout, url: chatUrl, apiKey: chatApiKey, bodyTemplate: chatBodyTemplate } = ConfigManager.request; + const { contextCompressUrl, contextCompressApiKey, contextCompressBodyTemplate } = ConfigManager.tool; + + const hasCustomCompressUrl = contextCompressUrl.trim() !== ''; + const url = hasCustomCompressUrl ? contextCompressUrl.trim() : chatUrl; + const apiKey = hasCustomCompressUrl ? (contextCompressApiKey.trim() || chatApiKey) : chatApiKey; + const bodyTemplate = contextCompressBodyTemplate.some(item => item.trim() !== '') ? contextCompressBodyTemplate : chatBodyTemplate; + + try { + const bodyObject = parseBody(bodyTemplate, messages, [], "none"); + const time = Date.now(); + + const data = await withTimeout(() => fetchData(url, apiKey, bodyObject), timeout); + + if (data.choices && data.choices.length > 0) { + AIManager.updateUsage(data.model, data.usage); + + const message = data.choices[0].message; + const content = message.content || ''; + + logger.info(`长响应工具压缩响应内容:`, content, '\nlatency:', Date.now() - time, 'ms'); + + return content; + } else { + throw new Error(`服务器响应中没有choices或choices为空\n响应体:${JSON.stringify(data, null, 2)}`); + } + } catch (e) { + logger.error("在sendToolRespondCompressRequest中请求出错:", e.message); + return ''; + } +} + const vectorCache: { text: string, vector: number[] } = { text: '', vector: [] }; export async function getEmbedding(text: string): Promise { diff --git a/src/tool/tool_context.ts b/src/tool/tool_context.ts index 759305b..33d22c5 100644 --- a/src/tool/tool_context.ts +++ b/src/tool/tool_context.ts @@ -61,6 +61,7 @@ export function registerContext() { return `[${message.role}]: ${buildContent(message)}`; }).join('\n'); - return { content: s, images: images }; + const finalContent = await ai.context.compressToolResponseIfNeeded("get_context", s); + return { content: finalContent, images: images }; } } \ No newline at end of file diff --git a/src/tool/tool_web.ts b/src/tool/tool_web.ts index 23405e4..9c2365c 100644 --- a/src/tool/tool_web.ts +++ b/src/tool/tool_web.ts @@ -34,7 +34,7 @@ export function registerWeb() { } } }); - toolSearch.solve = async (_, __, ___, args) => { + toolSearch.solve = async (_, __, ai, args) => { const { q, page, categories, time_range = '' } = args; const { webSearchUrl } = ConfigManager.backend; @@ -76,7 +76,10 @@ export function registerWeb() { - 相关性:${result.score}`; }).join('\n'); - return { content: s, images: [] }; + if (s.includes('没有搜索到结果')) return { content: s, images: [] }; + + const finalContent = await ai.context.compressToolResponseIfNeeded("web_search", s, q); + return { content: finalContent, images: [] }; } catch (error) { logger.error("在web_search中请求出错:", error); return { content: `使用搜索引擎搜索失败:${error}`, images: [] }; @@ -100,7 +103,7 @@ export function registerWeb() { } } }); - tool.solve = async (_, __, ___, args) => { + tool.solve = async (_, __, ai, args) => { const { url } = args; const { webReadUrl } = ConfigManager.backend; @@ -132,7 +135,10 @@ export function registerWeb() { ? links.map((link: string, index: number) => `${index + 1}. ${link}`).join('\n') : "无链接"); - return { content: result, images: [] }; + if (result.includes('{"error":')) return { content: result, images: [] }; + + const finalContent = await ai.context.compressToolResponseIfNeeded("web_read", result); + return { content: finalContent, images: [] }; } catch (error) { logger.error("在web_read中请求出错:", error); return { content: `读取网页内容失败: ${error}`, images: [] };