From 828a78792a9f6d065d5848a7c2e0e8f8d2d24980 Mon Sep 17 00:00:00 2001 From: baiyu-yu <135424680+baiyu-yu@users.noreply.github.com> Date: Mon, 16 Feb 2026 16:51:19 +0800 Subject: [PATCH 1/6] =?UTF-8?q?feat:=20=E8=BE=BE=E5=88=B0=E4=B8=80?= =?UTF-8?q?=E5=AE=9A=E9=95=BF=E5=BA=A6=E5=8E=8B=E7=BC=A9=E4=B8=8A=E4=B8=8B?= =?UTF-8?q?=E6=96=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/AI/context.ts | 94 ++++++++++++++++++++++++++++++++++++ src/config/config_message.ts | 61 ++++++++++++++++++++++- src/service.ts | 38 +++++++++++++++ src/utils/utils_message.ts | 30 +++++++----- 4 files changed, 210 insertions(+), 13 deletions(-) diff --git a/src/AI/context.ts b/src/AI/context.ts index 7c3b81a..aca7b63 100644 --- a/src/AI/context.ts +++ b/src/AI/context.ts @@ -7,6 +7,8 @@ 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 { sendContextCompressRequest } from "../service"; +import { buildRequestMessages } from "../utils/utils_message"; export interface MessageInfo { msgId: string; @@ -35,6 +37,7 @@ export class Context { lastReply: string; counter: number; timer: number; + compressingContext: boolean; constructor() { this.messages = []; @@ -43,6 +46,7 @@ export class Context { this.lastReply = ''; this.counter = 0; this.timer = null; + this.compressingContext = false; } reviveMessages() { @@ -156,6 +160,9 @@ export class Context { //更新记忆权重 ai.memory.updateRelatedMemoryWeight(ctx, ai.context, content, role); + // 压缩早期上下文 + await this.compressMessagesIfNeeded(ctx); + //删除多余的上下文 this.limitMessages(); } @@ -228,6 +235,93 @@ export class Context { } } + async compressMessagesIfNeeded(ctx: seal.MsgContext) { + const { isContextCompress, contextCompressLength, contextCompressPromptTemplate, maxRounds, isPrefix, showNumber, showMsgId, showTime } = ConfigManager.message; + const { localImagePathMap, receiveImage, condition } = ConfigManager.image; + const messages = this.messages; + if (!isContextCompress) return; + if (this.compressingContext) return; + if (maxRounds <= 0 || contextCompressLength <= 0) return; + + const round = messages.filter(message => message.role === 'user' && !message.name.startsWith('_')).length; + if (round < maxRounds) return; + + let compressCount = Math.min(contextCompressLength, messages.length - 1); + if (compressCount <= 0) return; + + // 避免切断 tool_call,向前移动 + while (compressCount > 0) { + const left = messages[compressCount - 1]; + const right = messages[compressCount]; + const leftIsTool = left?.role === 'tool'; + const rightIsTool = right?.role === 'tool'; + const leftIsToolCall = left?.role === 'assistant' && left?.tool_calls && left.tool_calls.length > 0; + if (!(leftIsTool || rightIsTool || leftIsToolCall)) { + break; + } + compressCount--; + } + if (compressCount <= 0 || compressCount >= messages.length) return; + + const compressMessages = messages.slice(0, compressCount); + const requestMessages = buildRequestMessages(compressMessages); + if (requestMessages.length === 0) return; + + const sandableImagesPrompt: string = Object.keys(localImagePathMap) + .map((id, index) => `${index + 1}. ${id}`) + .join('\n'); + + this.compressingContext = true; + try { + const prompt = contextCompressPromptTemplate({ + "平台": ctx.endPoint.platform, + "私聊": ctx.isPrivate, + "展示号码": showNumber, + "用户名称": ctx.player.name, + "用户号码": ctx.player.userId.replace(/^.+:/, ''), + "群聊名称": ctx.group.groupName, + "群聊号码": ctx.group.groupId.replace(/^.+:/, ''), + "添加前缀": isPrefix, + "展示消息ID": showMsgId, + "展示时间": showTime, + "接收图片": receiveImage, + "图片条件不为零": condition !== '0', + "可发送图片不为空": sandableImagesPrompt, + "可发送图片列表": sandableImagesPrompt + }); + logger.info(`上下文压缩prompt:\n`, prompt); + + const summary = (await sendContextCompressRequest([ + { role: 'system', content: prompt }, + ...requestMessages + ])).trim(); + if (!summary) { + logger.warning(`上下文压缩返回为空,跳过本次压缩`); + return; + } + + const now = Math.floor(Date.now() / 1000); + const summaryMessage: Message = { + role: 'user', + uid: '', + name: '_历史上下文摘要', + images: [], + msgArray: [{ + msgId: '', + time: now, + content: `以下为历史对话摘要:\n${summary}` + }] + }; + + messages.splice(0, compressCount, summaryMessage); + logger.info(`上下文压缩完成,压缩条数:${compressCount},压缩后条数:${messages.length}`); + } catch (e) { + logger.error(`上下文压缩失败: ${e.message}`); + } finally { + this.compressingContext = false; + } + } + 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_message.ts b/src/config/config_message.ts index 2f122e5..1c30ce1 100644 --- a/src/config/config_message.ts +++ b/src/config/config_message.ts @@ -98,6 +98,57 @@ export class MessageConfig { seal.ext.registerBoolConfig(MessageConfig.ext, "是否合并user content", false, "在不支持连续多个role为user的情况下开启,可用于适配deepseek-reasoner"); seal.ext.registerIntConfig(MessageConfig.ext, "存储上下文对话限制轮数", 15, "出现一次user视作一轮"); seal.ext.registerIntConfig(MessageConfig.ext, "上下文插入system message间隔轮数", 0, "需要小于限制轮数的二分之一才能生效,为0时不生效,示例对话不计入轮数"); + + seal.ext.registerBoolConfig(MessageConfig.ext, "是否启用上下文压缩", false, ''); + seal.ext.registerIntConfig(MessageConfig.ext, "每次压缩上下文条数", 10, '优先压缩最早的上下文'); + seal.ext.registerStringConfig(MessageConfig.ext, "上下文压缩 url地址", "", '为空时默认使用对话接口'); + seal.ext.registerStringConfig(MessageConfig.ext, "上下文压缩 API Key", "你的API Key", '若使用对话接口无需填写'); + seal.ext.registerTemplateConfig(MessageConfig.ext, "上下文压缩 body", [ + `"model":"deepseek-chat"`, + `"max_tokens":1024`, + `"stop":null`, + `"stream":false` + ], "messages不存在时,将会自动替换"); + seal.ext.registerTemplateConfig(MessageConfig.ext, "上下文压缩prompt模板", [ + `你是QQ群聊对话压缩助手。请将后续给出的历史消息压缩为可供后续继续对话的一段摘要。 + +## 聊天相关 + - 当前平台:{{{平台}}} +{{#if 私聊}} + - 当前私聊:<{{{用户名称}}}>{{#if 展示号码}}({{{用户号码}}}){{/if}} +{{else}} + - 当前群聊:<{{{群聊名称}}}>{{#if 展示号码}}({{{群聊号码}}}){{/if}} + - <|at:xxx|>表示@某个群成员 + - <|poke:xxx|>表示戳一戳某个群成员 +{{/if}} +{{#if 添加前缀}} + - <|from:xxx|>表示消息来源,不要在生成的回复中使用 +{{/if}} +{{#if 展示消息ID}} + - <|msg_id:xxx|>表示消息ID,仅用于调用函数时使用,不要在生成的回复中提及或使用 + - <|quote:xxx|>表示引用消息,xxx为对应的消息ID + - <|face:xxx|>表示使用某个表情,xxx为表情名称,注意与img表情包区分 +{{/if}} +{{#if 展示时间}} + - <|time:xxxx-xx-xx xx:xx:xx|>表示消息发送时间,不要在生成的回复中提及或使用 +{{/if}} + - \\f用于分割多条消息 + +## 图片相关 +{{#if 接收图片}} +{{#if 图片条件不为零}} + - <|img:xxxxxx:yyy|>为图片,其中xxxxxx为6位的图片id,yyy为图片描述(可能没有),如果要发送出现过的图片请使用<|img:xxxxxx|>的格式 +{{else}} + - <|img:xxxxxx|>为图片,其中xxxxxx为6位的图片id,如果要发送出现过的图片请使用<|img:xxxxxx|>的格式 +{{/if}} +{{/if}} + +## 输出要求 +1. 保留人物关系、主要话题、关键事实、明确结论、未完成事项、后续约定。 +2. 需要体现发言归属,避免把不同人的观点混淆。 +3. 忽略闲聊、重复、噪声内容,但不要丢失约束信息。 +4. 不要编造,不要解释,不要使用JSON,只输出摘要正文。` + ], ""); } static get() { @@ -112,7 +163,13 @@ export class MessageConfig { showTime: seal.ext.getBoolConfig(MessageConfig.ext, "是否在消息内添加发送时间"), isMerge: seal.ext.getBoolConfig(MessageConfig.ext, "是否合并user content"), maxRounds: seal.ext.getIntConfig(MessageConfig.ext, "存储上下文对话限制轮数"), - insertCount: seal.ext.getIntConfig(MessageConfig.ext, "上下文插入system message间隔轮数") + insertCount: seal.ext.getIntConfig(MessageConfig.ext, "上下文插入system message间隔轮数"), + isContextCompress: seal.ext.getBoolConfig(MessageConfig.ext, "是否启用上下文压缩"), + contextCompressLength: seal.ext.getIntConfig(MessageConfig.ext, "每次压缩上下文条数"), + contextCompressUrl: seal.ext.getStringConfig(MessageConfig.ext, "上下文压缩 url地址"), + contextCompressApiKey: seal.ext.getStringConfig(MessageConfig.ext, "上下文压缩 API Key"), + contextCompressBodyTemplate: seal.ext.getTemplateConfig(MessageConfig.ext, "上下文压缩 body"), + contextCompressPromptTemplate: ConfigManager.getHandlebarsTemplateConfig(MessageConfig.ext, "上下文压缩prompt模板") } } -} \ No newline at end of file +} diff --git a/src/service.ts b/src/service.ts index 1ed8e0e..d93b6e0 100644 --- a/src/service.ts +++ b/src/service.ts @@ -78,6 +78,44 @@ export async function sendITTRequest(messages: { } } +export async function sendContextCompressRequest(messages: { + role: string, + content: string +}[]): Promise { + const { timeout, url: chatUrl, apiKey: chatApiKey } = ConfigManager.request; + const { contextCompressUrl, contextCompressApiKey, contextCompressBodyTemplate } = ConfigManager.message; + + let url = chatUrl; + let apiKey = chatApiKey; + if (contextCompressUrl.trim()) { + url = contextCompressUrl; + apiKey = contextCompressApiKey; + } + + try { + const bodyObject = parseBody(contextCompressBodyTemplate, 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("在sendContextCompressRequest中请求出错:", e.message); + return ''; + } +} + const vectorCache: { text: string, vector: number[] } = { text: '', vector: [] }; export async function getEmbedding(text: string): Promise { diff --git a/src/utils/utils_message.ts b/src/utils/utils_message.ts index 1f812ba..1371407 100644 --- a/src/utils/utils_message.ts +++ b/src/utils/utils_message.ts @@ -156,28 +156,36 @@ function buildContextMessages(systemMessage: Message, messages: Message[]): Mess } export async function handleMessages(ctx: seal.MsgContext, ai: AI) { - const { isMerge } = ConfigManager.message; - const systemMessage = await buildSystemMessage(ctx, ai); const samplesMessages = buildSamplesMessages(ctx); const contextMessages = buildContextMessages(systemMessage, ai.context.messages); const messages = [systemMessage, ...samplesMessages, ...contextMessages]; + return buildRequestMessages(messages); +} + +export function buildRequestMessages(messages: Message[]) { + const { isMerge } = ConfigManager.message; + + const copiedMessages = messages.map(message => ({ + ...message, + tool_calls: message.tool_calls ? message.tool_calls.slice() : undefined + })); // 处理 tool_calls 并过滤无效项 - for (let i = 0; i < messages.length; i++) { - const message = messages[i]; + for (let i = 0; i < copiedMessages.length; i++) { + const message = copiedMessages[i]; if (!message?.tool_calls) { continue; } // 获取tool_calls消息后面的所有tool_call_id const tool_call_id_set = new Set(); - for (let j = i + 1; j < messages.length; j++) { - if (messages[j].role !== 'tool') { + for (let j = i + 1; j < copiedMessages.length; j++) { + if (copiedMessages[j].role !== 'tool') { break; } - tool_call_id_set.add(messages[j].tool_call_id); + tool_call_id_set.add(copiedMessages[j].tool_call_id); } // 过滤无对应 tool_call_id 的 tool_calls @@ -191,7 +199,7 @@ export async function handleMessages(ctx: seal.MsgContext, ai: AI) { // 如果 tool_calls 为空则移除消息 if (message.tool_calls.length === 0) { - messages.splice(i, 1); + copiedMessages.splice(i, 1); i--; // 调整索引 } } @@ -199,8 +207,8 @@ export async function handleMessages(ctx: seal.MsgContext, ai: AI) { // 处理前缀并合并消息(如果有) let processedMessages = []; let last_role = ''; - for (let i = 0; i < messages.length; i++) { - const message = messages[i]; + for (let i = 0; i < copiedMessages.length; i++) { + const message = copiedMessages[i]; if (isMerge && message.role === last_role && message.role !== 'tool') { processedMessages[processedMessages.length - 1].content += '\f' + buildContent(message); @@ -317,4 +325,4 @@ export function getRoleSetting(ctx: seal.MsgContext) { if (exists2 && roleIndex2 >= 0 && roleIndex2 < roleSettingTemplate.length) roleIndex = roleIndex2; } return { roleName, roleIndex, roleSetting: roleSettingTemplate[roleIndex] } -} \ No newline at end of file +} From 125d897a0fd2464c2fc7fcd3363af4a5843b825d Mon Sep 17 00:00:00 2001 From: baiyu-yu <135424680+baiyu-yu@users.noreply.github.com> Date: Tue, 3 Mar 2026 16:55:19 +0800 Subject: [PATCH 2/6] =?UTF-8?q?feat:=20=E5=9B=9E=E9=80=80=E7=84=B6?= =?UTF-8?q?=E5=90=8E=E4=BF=AE=E6=94=B9=E4=B8=BA=E5=AF=B9=E4=BA=8E=E9=95=BF?= =?UTF-8?q?=E5=93=8D=E5=BA=94=E5=B7=A5=E5=85=B7=E7=9A=84=E6=89=A7=E8=A1=8C?= =?UTF-8?q?=E5=8E=8B=E7=BC=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/AI/context.ts | 110 +++++++---------------------------- src/config/config_message.ts | 59 +------------------ src/config/config_tool.ts | 17 ++++++ src/service.ts | 24 ++++---- src/tool/tool.ts | 2 +- src/tool/tool_context.ts | 3 +- src/tool/tool_web.ts | 14 +++-- src/utils/utils_message.ts | 27 +++------ 8 files changed, 72 insertions(+), 184 deletions(-) diff --git a/src/AI/context.ts b/src/AI/context.ts index aca7b63..43e24c8 100644 --- a/src/AI/context.ts +++ b/src/AI/context.ts @@ -1,4 +1,4 @@ -import { ToolCall } from "../tool/tool"; +import { ToolCall } from "../tool/tool"; import { ConfigManager } from "../config/configManager"; import { Image, ImageManager } from "./image"; import { getCtxAndMsg } from "../utils/utils_seal"; @@ -7,8 +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 { sendContextCompressRequest } from "../service"; -import { buildRequestMessages } from "../utils/utils_message"; +import { sendToolRespondCompressRequest } from "../service"; export interface MessageInfo { msgId: string; @@ -37,7 +36,6 @@ export class Context { lastReply: string; counter: number; timer: number; - compressingContext: boolean; constructor() { this.messages = []; @@ -46,7 +44,6 @@ export class Context { this.lastReply = ''; this.counter = 0; this.timer = null; - this.compressingContext = false; } reviveMessages() { @@ -160,9 +157,6 @@ export class Context { //更新记忆权重 ai.memory.updateRelatedMemoryWeight(ctx, ai.context, content, role); - // 压缩早期上下文 - await this.compressMessagesIfNeeded(ctx); - //删除多余的上下文 this.limitMessages(); } @@ -235,91 +229,29 @@ export class Context { } } - async compressMessagesIfNeeded(ctx: seal.MsgContext) { - const { isContextCompress, contextCompressLength, contextCompressPromptTemplate, maxRounds, isPrefix, showNumber, showMsgId, showTime } = ConfigManager.message; - const { localImagePathMap, receiveImage, condition } = ConfigManager.image; - const messages = this.messages; - if (!isContextCompress) return; - if (this.compressingContext) return; - if (maxRounds <= 0 || contextCompressLength <= 0) return; - - const round = messages.filter(message => message.role === 'user' && !message.name.startsWith('_')).length; - if (round < maxRounds) return; - - let compressCount = Math.min(contextCompressLength, messages.length - 1); - if (compressCount <= 0) return; - - // 避免切断 tool_call,向前移动 - while (compressCount > 0) { - const left = messages[compressCount - 1]; - const right = messages[compressCount]; - const leftIsTool = left?.role === 'tool'; - const rightIsTool = right?.role === 'tool'; - const leftIsToolCall = left?.role === 'assistant' && left?.tool_calls && left.tool_calls.length > 0; - if (!(leftIsTool || rightIsTool || leftIsToolCall)) { - break; - } - compressCount--; + async compressToolResponseIfNeeded(toolName: string, content: string, searchTarget: string = ''): Promise { + const { isToolResponseCompress, toolResponseCompressPromptTemplate } = ConfigManager.tool; + if (!isToolResponseCompress) return content; + if (!content.trim()) return content; + + let prompt = toolResponseCompressPromptTemplate({ + "函数名": toolName, + "搜索目标": searchTarget + }); + if (toolName === 'web_search' && searchTarget.trim()) { + prompt += `\n\n搜索目标:\n${searchTarget.trim()}`; } - if (compressCount <= 0 || compressCount >= messages.length) return; - - const compressMessages = messages.slice(0, compressCount); - const requestMessages = buildRequestMessages(compressMessages); - if (requestMessages.length === 0) return; - - const sandableImagesPrompt: string = Object.keys(localImagePathMap) - .map((id, index) => `${index + 1}. ${id}`) - .join('\n'); - - this.compressingContext = true; - try { - const prompt = contextCompressPromptTemplate({ - "平台": ctx.endPoint.platform, - "私聊": ctx.isPrivate, - "展示号码": showNumber, - "用户名称": ctx.player.name, - "用户号码": ctx.player.userId.replace(/^.+:/, ''), - "群聊名称": ctx.group.groupName, - "群聊号码": ctx.group.groupId.replace(/^.+:/, ''), - "添加前缀": isPrefix, - "展示消息ID": showMsgId, - "展示时间": showTime, - "接收图片": receiveImage, - "图片条件不为零": condition !== '0', - "可发送图片不为空": sandableImagesPrompt, - "可发送图片列表": sandableImagesPrompt - }); - logger.info(`上下文压缩prompt:\n`, prompt); - - const summary = (await sendContextCompressRequest([ - { role: 'system', content: prompt }, - ...requestMessages - ])).trim(); - if (!summary) { - logger.warning(`上下文压缩返回为空,跳过本次压缩`); - return; - } - const now = Math.floor(Date.now() / 1000); - const summaryMessage: Message = { - role: 'user', - uid: '', - name: '_历史上下文摘要', - images: [], - msgArray: [{ - msgId: '', - time: now, - content: `以下为历史对话摘要:\n${summary}` - }] - }; + const summary = (await sendToolRespondCompressRequest([ + { role: 'system', content: prompt }, + { role: 'user', content } + ])).trim(); - messages.splice(0, compressCount, summaryMessage); - logger.info(`上下文压缩完成,压缩条数:${compressCount},压缩后条数:${messages.length}`); - } catch (e) { - logger.error(`上下文压缩失败: ${e.message}`); - } finally { - this.compressingContext = false; + if (!summary) { + logger.warning(`工具响应压缩返回为空,保留原始响应: ${toolName}`); + return content; } + return summary; } async findUserInfo(ctx: seal.MsgContext, name: string | number, findInFriendList: boolean = false): Promise { diff --git a/src/config/config_message.ts b/src/config/config_message.ts index 1c30ce1..2b60ce3 100644 --- a/src/config/config_message.ts +++ b/src/config/config_message.ts @@ -98,57 +98,6 @@ export class MessageConfig { seal.ext.registerBoolConfig(MessageConfig.ext, "是否合并user content", false, "在不支持连续多个role为user的情况下开启,可用于适配deepseek-reasoner"); seal.ext.registerIntConfig(MessageConfig.ext, "存储上下文对话限制轮数", 15, "出现一次user视作一轮"); seal.ext.registerIntConfig(MessageConfig.ext, "上下文插入system message间隔轮数", 0, "需要小于限制轮数的二分之一才能生效,为0时不生效,示例对话不计入轮数"); - - seal.ext.registerBoolConfig(MessageConfig.ext, "是否启用上下文压缩", false, ''); - seal.ext.registerIntConfig(MessageConfig.ext, "每次压缩上下文条数", 10, '优先压缩最早的上下文'); - seal.ext.registerStringConfig(MessageConfig.ext, "上下文压缩 url地址", "", '为空时默认使用对话接口'); - seal.ext.registerStringConfig(MessageConfig.ext, "上下文压缩 API Key", "你的API Key", '若使用对话接口无需填写'); - seal.ext.registerTemplateConfig(MessageConfig.ext, "上下文压缩 body", [ - `"model":"deepseek-chat"`, - `"max_tokens":1024`, - `"stop":null`, - `"stream":false` - ], "messages不存在时,将会自动替换"); - seal.ext.registerTemplateConfig(MessageConfig.ext, "上下文压缩prompt模板", [ - `你是QQ群聊对话压缩助手。请将后续给出的历史消息压缩为可供后续继续对话的一段摘要。 - -## 聊天相关 - - 当前平台:{{{平台}}} -{{#if 私聊}} - - 当前私聊:<{{{用户名称}}}>{{#if 展示号码}}({{{用户号码}}}){{/if}} -{{else}} - - 当前群聊:<{{{群聊名称}}}>{{#if 展示号码}}({{{群聊号码}}}){{/if}} - - <|at:xxx|>表示@某个群成员 - - <|poke:xxx|>表示戳一戳某个群成员 -{{/if}} -{{#if 添加前缀}} - - <|from:xxx|>表示消息来源,不要在生成的回复中使用 -{{/if}} -{{#if 展示消息ID}} - - <|msg_id:xxx|>表示消息ID,仅用于调用函数时使用,不要在生成的回复中提及或使用 - - <|quote:xxx|>表示引用消息,xxx为对应的消息ID - - <|face:xxx|>表示使用某个表情,xxx为表情名称,注意与img表情包区分 -{{/if}} -{{#if 展示时间}} - - <|time:xxxx-xx-xx xx:xx:xx|>表示消息发送时间,不要在生成的回复中提及或使用 -{{/if}} - - \\f用于分割多条消息 - -## 图片相关 -{{#if 接收图片}} -{{#if 图片条件不为零}} - - <|img:xxxxxx:yyy|>为图片,其中xxxxxx为6位的图片id,yyy为图片描述(可能没有),如果要发送出现过的图片请使用<|img:xxxxxx|>的格式 -{{else}} - - <|img:xxxxxx|>为图片,其中xxxxxx为6位的图片id,如果要发送出现过的图片请使用<|img:xxxxxx|>的格式 -{{/if}} -{{/if}} - -## 输出要求 -1. 保留人物关系、主要话题、关键事实、明确结论、未完成事项、后续约定。 -2. 需要体现发言归属,避免把不同人的观点混淆。 -3. 忽略闲聊、重复、噪声内容,但不要丢失约束信息。 -4. 不要编造,不要解释,不要使用JSON,只输出摘要正文。` - ], ""); } static get() { @@ -163,13 +112,7 @@ export class MessageConfig { showTime: seal.ext.getBoolConfig(MessageConfig.ext, "是否在消息内添加发送时间"), isMerge: seal.ext.getBoolConfig(MessageConfig.ext, "是否合并user content"), maxRounds: seal.ext.getIntConfig(MessageConfig.ext, "存储上下文对话限制轮数"), - insertCount: seal.ext.getIntConfig(MessageConfig.ext, "上下文插入system message间隔轮数"), - isContextCompress: seal.ext.getBoolConfig(MessageConfig.ext, "是否启用上下文压缩"), - contextCompressLength: seal.ext.getIntConfig(MessageConfig.ext, "每次压缩上下文条数"), - contextCompressUrl: seal.ext.getStringConfig(MessageConfig.ext, "上下文压缩 url地址"), - contextCompressApiKey: seal.ext.getStringConfig(MessageConfig.ext, "上下文压缩 API Key"), - contextCompressBodyTemplate: seal.ext.getTemplateConfig(MessageConfig.ext, "上下文压缩 body"), - contextCompressPromptTemplate: ConfigManager.getHandlebarsTemplateConfig(MessageConfig.ext, "上下文压缩prompt模板") + insertCount: seal.ext.getIntConfig(MessageConfig.ext, "上下文插入system message间隔轮数") } } } diff --git a/src/config/config_tool.ts b/src/config/config_tool.ts index aa7004b..d149c7d 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.registerBoolConfig(ToolConfig.ext, "是否启用工具响应压缩", false, "仅对 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, "不允许调用的函数"), + isToolResponseCompress: seal.ext.getBoolConfig(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 d93b6e0..2171560 100644 --- a/src/service.ts +++ b/src/service.ts @@ -78,22 +78,20 @@ export async function sendITTRequest(messages: { } } -export async function sendContextCompressRequest(messages: { +export async function sendToolRespondCompressRequest(messages: { role: string, content: string }[]): Promise { - const { timeout, url: chatUrl, apiKey: chatApiKey } = ConfigManager.request; - const { contextCompressUrl, contextCompressApiKey, contextCompressBodyTemplate } = ConfigManager.message; - - let url = chatUrl; - let apiKey = chatApiKey; - if (contextCompressUrl.trim()) { - url = contextCompressUrl; - apiKey = contextCompressApiKey; - } + 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(contextCompressBodyTemplate, messages, [], "none"); + const bodyObject = parseBody(bodyTemplate, messages, [], "none"); const time = Date.now(); const data = await withTimeout(() => fetchData(url, apiKey, bodyObject), timeout); @@ -104,14 +102,14 @@ export async function sendContextCompressRequest(messages: { const message = data.choices[0].message; const content = message.content || ''; - logger.info(`上下文压缩响应内容:`, content, '\nlatency:', Date.now() - time, 'ms'); + 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("在sendContextCompressRequest中请求出错:", e.message); + logger.error("在sendToolRespondCompressRequest中请求出错:", e.message); return ''; } } diff --git a/src/tool/tool.ts b/src/tool/tool.ts index 299f30b..2e2493f 100644 --- a/src/tool/tool.ts +++ b/src/tool/tool.ts @@ -1,4 +1,4 @@ -import { AI } from "../AI/AI" +import { AI } from "../AI/AI" import { ConfigManager } from "../config/configManager" import { registerAttr } from "./tool_attr" import { registerBan } from "./tool_ban" 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: [] }; diff --git a/src/utils/utils_message.ts b/src/utils/utils_message.ts index 1371407..2413fd0 100644 --- a/src/utils/utils_message.ts +++ b/src/utils/utils_message.ts @@ -156,36 +156,27 @@ function buildContextMessages(systemMessage: Message, messages: Message[]): Mess } export async function handleMessages(ctx: seal.MsgContext, ai: AI) { + const { isMerge } = ConfigManager.message; const systemMessage = await buildSystemMessage(ctx, ai); const samplesMessages = buildSamplesMessages(ctx); const contextMessages = buildContextMessages(systemMessage, ai.context.messages); const messages = [systemMessage, ...samplesMessages, ...contextMessages]; - return buildRequestMessages(messages); -} - -export function buildRequestMessages(messages: Message[]) { - const { isMerge } = ConfigManager.message; - - const copiedMessages = messages.map(message => ({ - ...message, - tool_calls: message.tool_calls ? message.tool_calls.slice() : undefined - })); // 处理 tool_calls 并过滤无效项 - for (let i = 0; i < copiedMessages.length; i++) { - const message = copiedMessages[i]; + for (let i = 0; i < messages.length; i++) { + const message = messages[i]; if (!message?.tool_calls) { continue; } // 获取tool_calls消息后面的所有tool_call_id const tool_call_id_set = new Set(); - for (let j = i + 1; j < copiedMessages.length; j++) { - if (copiedMessages[j].role !== 'tool') { + for (let j = i + 1; j < messages.length; j++) { + if (messages[j].role !== 'tool') { break; } - tool_call_id_set.add(copiedMessages[j].tool_call_id); + tool_call_id_set.add(messages[j].tool_call_id); } // 过滤无对应 tool_call_id 的 tool_calls @@ -199,7 +190,7 @@ export function buildRequestMessages(messages: Message[]) { // 如果 tool_calls 为空则移除消息 if (message.tool_calls.length === 0) { - copiedMessages.splice(i, 1); + messages.splice(i, 1); i--; // 调整索引 } } @@ -207,8 +198,8 @@ export function buildRequestMessages(messages: Message[]) { // 处理前缀并合并消息(如果有) let processedMessages = []; let last_role = ''; - for (let i = 0; i < copiedMessages.length; i++) { - const message = copiedMessages[i]; + for (let i = 0; i < messages.length; i++) { + const message = messages[i]; if (isMerge && message.role === last_role && message.role !== 'tool') { processedMessages[processedMessages.length - 1].content += '\f' + buildContent(message); From 3bc1b979ab6657bc666f3178f84392e13d181fca Mon Sep 17 00:00:00 2001 From: baiyu-yu <135424680+baiyu-yu@users.noreply.github.com> Date: Wed, 4 Mar 2026 15:02:56 +0800 Subject: [PATCH 3/6] =?UTF-8?q?=E8=BF=99=E9=87=8C=E5=8F=91=E7=94=9F?= =?UTF-8?q?=E4=BA=86=E4=BB=80=E4=B9=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config/config_message.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/config_message.ts b/src/config/config_message.ts index 2b60ce3..2f122e5 100644 --- a/src/config/config_message.ts +++ b/src/config/config_message.ts @@ -115,4 +115,4 @@ export class MessageConfig { insertCount: seal.ext.getIntConfig(MessageConfig.ext, "上下文插入system message间隔轮数") } } -} +} \ No newline at end of file From aa74748ca14ac300b6d5b5f10fdb9f57e3e52dd8 Mon Sep 17 00:00:00 2001 From: baiyu-yu <135424680+baiyu-yu@users.noreply.github.com> Date: Wed, 4 Mar 2026 15:23:08 +0800 Subject: [PATCH 4/6] =?UTF-8?q?=E6=88=91=E6=98=AF=E6=8D=A2=E8=A1=8C?= =?UTF-8?q?=E9=AB=98=E6=89=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/utils_message.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/utils/utils_message.ts b/src/utils/utils_message.ts index 2413fd0..1f812ba 100644 --- a/src/utils/utils_message.ts +++ b/src/utils/utils_message.ts @@ -157,6 +157,7 @@ function buildContextMessages(systemMessage: Message, messages: Message[]): Mess export async function handleMessages(ctx: seal.MsgContext, ai: AI) { const { isMerge } = ConfigManager.message; + const systemMessage = await buildSystemMessage(ctx, ai); const samplesMessages = buildSamplesMessages(ctx); const contextMessages = buildContextMessages(systemMessage, ai.context.messages); @@ -316,4 +317,4 @@ export function getRoleSetting(ctx: seal.MsgContext) { if (exists2 && roleIndex2 >= 0 && roleIndex2 < roleSettingTemplate.length) roleIndex = roleIndex2; } return { roleName, roleIndex, roleSetting: roleSettingTemplate[roleIndex] } -} +} \ No newline at end of file From c565c4d9943f4bf98a1aa504ade1fbaff6c9ca1a Mon Sep 17 00:00:00 2001 From: baiyu-yu <135424680+baiyu-yu@users.noreply.github.com> Date: Wed, 4 Mar 2026 15:37:17 +0800 Subject: [PATCH 5/6] =?UTF-8?q?=E5=8C=BA=E5=8C=BA=E7=BC=96=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/AI/context.ts | 2 +- src/tool/tool.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/AI/context.ts b/src/AI/context.ts index 43e24c8..a4c856a 100644 --- a/src/AI/context.ts +++ b/src/AI/context.ts @@ -1,4 +1,4 @@ -import { ToolCall } from "../tool/tool"; +import { ToolCall } from "../tool/tool"; import { ConfigManager } from "../config/configManager"; import { Image, ImageManager } from "./image"; import { getCtxAndMsg } from "../utils/utils_seal"; diff --git a/src/tool/tool.ts b/src/tool/tool.ts index 2e2493f..299f30b 100644 --- a/src/tool/tool.ts +++ b/src/tool/tool.ts @@ -1,4 +1,4 @@ -import { AI } from "../AI/AI" +import { AI } from "../AI/AI" import { ConfigManager } from "../config/configManager" import { registerAttr } from "./tool_attr" import { registerBan } from "./tool_ban" From 82fd7224c533f6835e15828f23c863e66fe8591a Mon Sep 17 00:00:00 2001 From: baiyu-yu <135424680+baiyu-yu@users.noreply.github.com> Date: Wed, 4 Mar 2026 15:57:16 +0800 Subject: [PATCH 6/6] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E5=BC=80=E5=85=B3?= =?UTF-8?q?=E4=B8=BA=E8=BE=BE=E5=88=B0=E4=B8=80=E5=AE=9A=E9=95=BF=E5=BA=A6?= =?UTF-8?q?=E8=A7=A6=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/AI/context.ts | 4 ++-- src/config/config_tool.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/AI/context.ts b/src/AI/context.ts index a4c856a..8515202 100644 --- a/src/AI/context.ts +++ b/src/AI/context.ts @@ -230,9 +230,9 @@ export class Context { } async compressToolResponseIfNeeded(toolName: string, content: string, searchTarget: string = ''): Promise { - const { isToolResponseCompress, toolResponseCompressPromptTemplate } = ConfigManager.tool; - if (!isToolResponseCompress) return content; + const { toolResponseCompressMinLength, toolResponseCompressPromptTemplate } = ConfigManager.tool; if (!content.trim()) return content; + if (content.length < toolResponseCompressMinLength) return content; let prompt = toolResponseCompressPromptTemplate({ "函数名": toolName, diff --git a/src/config/config_tool.ts b/src/config/config_tool.ts index d149c7d..e818ebd 100644 --- a/src/config/config_tool.ts +++ b/src/config/config_tool.ts @@ -20,7 +20,7 @@ export class ToolConfig { 'whole_ban', 'get_ban_list' ], "修改后保存并重载js"); - seal.ext.registerBoolConfig(ToolConfig.ext, "是否启用工具响应压缩", false, "仅对 web_search、web_read、get_context 生效"); + seal.ext.registerIntConfig(ToolConfig.ext, "工具响应压缩触发字数", 10000, "仅对 web_search、web_read、get_context 生效,当工具响应达到该字数时进行压缩"); seal.ext.registerTemplateConfig(ToolConfig.ext, "工具响应压缩prompt模板", [ `你是文本压缩助手。请将用户提供的工具返回结果压缩为简洁摘要,保留关键信息、链接、结论和可执行事项。只输出压缩结果正文。` ], ""); @@ -73,7 +73,7 @@ export class ToolConfig { toolsPromptTemplate: ConfigManager.getHandlebarsTemplateConfig(ToolConfig.ext, "工具函数prompt模板"), maxCallCount: seal.ext.getIntConfig(ToolConfig.ext, "允许连续调用函数次数"), toolsNotAllow: seal.ext.getTemplateConfig(ToolConfig.ext, "不允许调用的函数"), - isToolResponseCompress: seal.ext.getBoolConfig(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"),