From 2852996f1898edfaae8ca0a17b99a2d972635038 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=89=8B=E7=93=9C=E4=B8=80=E5=8D=81=E9=9B=AA?= Date: Tue, 25 Feb 2025 13:52:05 +0800 Subject: [PATCH] x --- src/plugin/index.ts | 331 ++++++++++++++++++++++++++------------------ 1 file changed, 194 insertions(+), 137 deletions(-) diff --git a/src/plugin/index.ts b/src/plugin/index.ts index db4f270b..4199d52b 100644 --- a/src/plugin/index.ts +++ b/src/plugin/index.ts @@ -3,26 +3,29 @@ import { NapCatCore } from '@/core'; import { ActionMap } from '@/onebot/action'; import { OB11PluginAdapter } from '@/onebot/network/plugin'; import { OpenAI } from 'openai'; +import { ChatCompletionContentPart, ChatCompletionMessageParam } from 'openai/resources'; +import { Mutex } from 'async-mutex'; const API_KEY = 'sk-vDXUiGa1fx8ygDJDjAlr2rUyoz3uPhMxr8zId8n3ycMkV23i'; const BASE_URL = 'https://api.bili2233.work/v1'; const MODEL = 'gemini-2.0-flash-thinking-exp'; const SHORT_TERM_MEMORY_LIMIT = 100; const BOT_NAME = '千千'; -const PROMPT = - `你的名字叫千千,你现在处于一个QQ群聊之中,作为博学多识的可爱群员,热心解答各种问题和高强度水群 -记住你说的话要尽量的简洁但具有情感,不要长篇大论,一句话不宜超过五十个字。`; -const CQCODE = `增加一下能力通过不同昵称和QQ进行区分哦,注意理清回复消息的人物, At人直接发送 [CQ:at,qq=1234] 这样可以直接at某个人喵这 回复消息需要发送[CQ:reply,id=xxx]这种格式叫CQ码,发送图片等操作你可以从聊天记录中学习哦, 如果聊天记录的image CQ码 maface类型你可以直接复制使用` +const BOT_ADMIN = '1627126029'; +const PROMPT = `你的名字叫千千,你现在处于一个QQ群聊之中,作为博学多识的可爱群员,不要故意装可爱卖萌,而是更自然,注意少使用标点符号,热心解答各种问题和高强度水群 +记住你说的话要尽量的简洁但具有情感,不要长篇大论,一句话不宜超过五十个字但实际回复可以超过。`; +const CQCODE = `增加一下能力通过不同昵称和QQ进行区分哦,注意理清回复消息的人物, At人直接发送 [CQ:at,qq=1234] 这样可以直接at某个人喵这 回复消息需要发送[CQ:reply,id=xxx]这种格式叫CQ码,发送图片等操作你可以从聊天记录中学习哦, 如果聊天记录的image CQ码 maface类型你可以直接复制使用`; const client = new OpenAI({ apiKey: API_KEY, baseURL: BASE_URL }); const longTermMemory: Map = new Map(); -const shortTermMemory: Map = new Map(); -const memoryTransferCount: Map = new Map(); -//聊天热度 -const chatHot: Map = new Map(); +const shortTermMemory: Map[]> = new Map(); +const MemoryCount: Map = new Map(); +const chatHot: Map = new Map(); +const chatHotMutex = new Mutex(); +const memMutex = new Mutex(); async function createChatCompletionWithRetry(params: any, retries: number = 3): Promise { for (let attempt = 0; attempt < retries; attempt++) { @@ -35,7 +38,8 @@ async function createChatCompletionWithRetry(params: any, retries: number = 3): } } -async function msg2string(adapter: string, msg: OB11MessageData[], group_id: string, action: ActionMap, plugin: OB11PluginAdapter): Promise { +async function messageToOpenAi(adapter: string, msg: OB11MessageData[], groupId: string, action: ActionMap, plugin: OB11PluginAdapter, message: OB11ArrayMessage) { + const msgArray: Array = []; let ret = ''; for (const m of msg) { if (m.type === 'reply') { @@ -44,28 +48,28 @@ async function msg2string(adapter: string, msg: OB11MessageData[], group_id: str ret += m.data.text; } else if (m.type === 'at') { const memberInfo = await action.get('get_group_member_info') - ?.handle({ group_id: group_id, user_id: m.data.qq }, adapter, plugin.config); + ?.handle({ group_id: groupId, user_id: m.data.qq }, adapter, plugin.config); ret += `[CQ:at=${m.data.qq},name=${memberInfo?.data?.nickname}]`; } else if (m.type === 'image') { - ret += '[CQ:image,file=' + m.data.url + ']'; + ret += `[CQ:image,file=${m.data.url}]`; + msgArray.push({ + type: 'image_url', + image_url: { + url: m.data.url?.replace('https://', 'http://') || '' + } + }); } else if (m.type === 'face') { ret += '[CQ:face,id=' + m.data.id + ']'; } } - return ret; + msgArray.push({ + type: 'text', + text: `${message.sender.nickname}(${message.sender.user_id})发送了消息(消息id:${message.message_id}) :` + ret + }); + return msgArray.reverse(); } -function updateMemoryLayer(memoryLayer: Map, group_id: string, newMessages: string[]) { - const currentMemory = memoryLayer.get(group_id) || []; - currentMemory.push(...newMessages); - if (currentMemory.length > SHORT_TERM_MEMORY_LIMIT) { - memoryLayer.set(group_id, currentMemory.slice(-SHORT_TERM_MEMORY_LIMIT)); - } else { - memoryLayer.set(group_id, currentMemory); - } -} - -async function mergeAndUpdateMemory(existing_memories: string, new_memory: string): Promise { +async function mergeAndUpdateMemory(existingMemories: Array[], newMemory: Array[]): Promise { const prompt = ` 你是合并、更新和组织记忆的专家。当提供现有记忆和新信息时,你的任务是合并和更新记忆列表,以反映最准确和最新的信息。你还会得到每个现有记忆与新信息的匹配分数。确保利用这些信息做出明智的决定,决定哪些记忆需要更新或合并。 指南: @@ -76,110 +80,163 @@ async function mergeAndUpdateMemory(existing_memories: string, new_memory: strin - 注意区分对应人物的记忆和印象, 不要产生混淆人物的印象和记忆。 - 在所有记忆中保持一致且清晰的风格,确保每个条目简洁而信息丰富。 - 如果新记忆是现有记忆的变体或扩展,更新现有记忆以反映新信息。 -以下是任务的详细信息: -- 现有记忆: -${existing_memories} -- 新记忆: -${new_memory}`; - +`; const completion = await createChatCompletionWithRetry({ - messages: [{ role: 'user', content: prompt }], + messages: await toSingleRole([ + { role: 'user', content: [{ type: 'text', text: prompt }] }, + { role: 'user', content: [{ type: 'text', text: '接下来是旧记忆' }] }, + ...(existingMemories.map(msg => ({ role: 'user', content: msg.filter(e => e.type === 'text') }))), + { role: 'user', content: [{ type: 'text', text: '接下来是新记忆' }] }, + ...(newMemory.map(msg => ({ role: 'user', content: msg.filter(e => e.type === 'text') })))]), model: MODEL }); return completion.choices[0]?.message.content || ''; } -async function generateChatCompletion(content_data: string, url_image?: string[]): Promise { - const messages: any = { - role: 'user', content: [{ - type: 'text', - text: content_data - }] - }; - if (url_image && url_image.length > 0) { - url_image.forEach(url => { - messages.content.push({ - type: 'image_url', - image_url: { - url: url.replace('https://', 'http://') - } - }); - }); - } - console.log(JSON.stringify(messages, null, 2)); +async function generateChatCompletion(contentData: Array): Promise { const chatCompletion = await createChatCompletionWithRetry({ - messages: [messages], + messages: contentData, model: MODEL }); return chatCompletion.choices[0]?.message.content || ''; } -async function updateMemory(group_id: string, newMessages: string) { - updateMemoryLayer(shortTermMemory, group_id, [newMessages]); - const currentMemory = longTermMemory.get(group_id) || ''; - const transferCount = memoryTransferCount.get(group_id) || 0; - - if (shortTermMemory.get(group_id)!.length >= SHORT_TERM_MEMORY_LIMIT) { - memoryTransferCount.set(group_id, transferCount + 1); - if (memoryTransferCount.get(group_id)! >= 1) { - const mergedMemory = await mergeAndUpdateMemory(currentMemory, shortTermMemory.get(group_id)!.join('\n')); - longTermMemory.set(group_id, mergedMemory); - shortTermMemory.set(group_id, []); - memoryTransferCount.set(group_id, 0); - } +async function updateMemory(groupId: string, newMessages: Array[], core: NapCatCore) { + const currentMemory = shortTermMemory.get(groupId) || []; + const memCount = await memMutex.runExclusive(() => { + const memCount = MemoryCount.get(groupId) || 0; + MemoryCount.set(groupId, memCount + 1); + return memCount + 1; + }); + console.log('memCount', memCount); + currentMemory.push(...newMessages); + if (memCount > SHORT_TERM_MEMORY_LIMIT) { + await memMutex.runExclusive(async () => { + const containsBotName = currentMemory.some(messages => messages.some(msg => msg.type === 'text' && msg.text.includes(core.selfInfo.uin))); + if (containsBotName) { + const mergedMemory = await mergeAndUpdateMemory(currentMemory, newMessages); + longTermMemory.set(groupId, mergedMemory); + } + shortTermMemory.set(groupId, currentMemory.slice(-SHORT_TERM_MEMORY_LIMIT)); + MemoryCount.set(groupId, 0); + }); } + shortTermMemory.set(groupId, currentMemory); } -async function clearShortTermMemory(group_id: string) { - shortTermMemory.set(group_id, []); - memoryTransferCount.set(group_id, 0); -} - -async function clearLongTermMemory(group_id: string) { - longTermMemory.set(group_id, ''); -} - -async function handleClearMemoryCommand(group_id: string, type: 'short' | 'long', action: ActionMap, adapter: string, instance: OB11PluginAdapter) { +async function clearMemory(groupId: string, type: 'short' | 'long') { if (type === 'short') { - await clearShortTermMemory(group_id); - await sendGroupMessage(group_id, '短期上下文已清理', action, adapter, instance); + shortTermMemory.set(groupId, []); } else { - await clearLongTermMemory(group_id); - await sendGroupMessage(group_id, '长期上下文已清理', action, adapter, instance); + longTermMemory.set(groupId, ''); } } -async function sendGroupMessage(group_id: string, text: string, action: ActionMap, adapter: string, instance: OB11PluginAdapter) { +async function handleClearMemoryCommand(groupId: string, type: 'short' | 'long', action: ActionMap, adapter: string, instance: OB11PluginAdapter) { + await clearMemory(groupId, type); + const message = type === 'short' ? '短期上下文已清理' : '长期上下文已清理'; + await sendGroupMessage(groupId, message, action, adapter, instance); +} + +async function sendGroupMessage(groupId: string, text: string, action: ActionMap, adapter: string, instance: OB11PluginAdapter) { return await action.get('send_group_msg')?.handle({ - group_id: String(group_id), + group_id: String(groupId), message: text }, adapter, instance.config); } -async function handleMessage(message: OB11ArrayMessage, adapter: string, action: ActionMap, instance: OB11PluginAdapter): Promise { - let msg_string = ''; - try { - msg_string += `${message.sender.nickname}(${message.sender.user_id})发送了消息(消息id:${message.message_id}) :` - msg_string += await msg2string(adapter, message.message, message.group_id?.toString()!, action, instance); - } catch (error) { - if (msg_string == '') { - return ''; - } - } - return msg_string; +async function handleMessage(message: OB11ArrayMessage, adapter: string, action: ActionMap, instance: OB11PluginAdapter) { + return await messageToOpenAi(adapter, message.message, message.group_id?.toString()!, action, instance, message); } -async function handleChatResponse(message: OB11ArrayMessage, msg_string: string, adapter: string, action: ActionMap, instance: OB11PluginAdapter, _core: NapCatCore) { - const longTermMemoryString = longTermMemory.get(message.group_id?.toString()!) || ''; - const shortTermMemoryString = shortTermMemory.get(message.group_id?.toString()!)?.join('\n') || ''; - const user_info = await action.get('get_group_member_info')?.handle({ group_id: message.group_id?.toString()!, user_id: message.sender.user_id }, adapter, instance.config); - const content_data = - `请根据下面聊天内容,继续与 ${user_info?.data?.card || user_info?.data?.nickname} 进行对话。${CQCODE},注意回复内容只用输出内容,不要提及此段话,注意一定不要使用markdown,请采用纯文本回复。你的人设:${PROMPT}长时间记忆:\n${longTermMemoryString}\n短时间记忆:\n${shortTermMemoryString}\n当前对话:\n${msg_string}\n}`; - const msg_ret = await generateChatCompletion(content_data, message.message.filter(e => e.type === 'image').map(e => e.data.url!)); - let msg = await sendGroupMessage(message.group_id?.toString()!, msg_ret, action, adapter, instance); - chatHot.set(message.group_id?.toString()!, (chatHot.get(message.group_id?.toString()!) || 0) + 3); - return msg?.data?.message_id; +async function toSingleRole(msg: Array) { + let ret = { role: 'user', content: new Array() }; + for (const m of msg) { + ret.content.push(...m.content as any) + } + console.log(JSON.stringify(ret, null, 2)); + return [ret] as Array; +} + +async function handleChatResponse(message: OB11ArrayMessage, msgArray: Array, adapter: string, action: ActionMap, instance: OB11PluginAdapter, _core: NapCatCore, reply?: Array) { + const group_id = message.group_id?.toString()!; + const longTermMemoryList = longTermMemory.get(group_id) || ''; + let shortTermMemoryList = shortTermMemory.get(group_id); + if (!shortTermMemoryList) { + let MemoryShort: Array[] = []; + shortTermMemory.set(group_id, MemoryShort); + shortTermMemoryList = MemoryShort; + } + const prompt = `请根据下面聊天内容,继续与 ${message?.sender?.card || message?.sender?.nickname} 进行对话。${CQCODE},注意回复内容只用输出内容,不要提及此段话,注意一定不要使用markdown,请采用纯文本回复。你的人设:${PROMPT}` + let data = shortTermMemoryList.map(msg => ({ role: 'user' as const, content: msg.filter(e => e.type === 'text') })); + let contentData: Array = await toSingleRole([ + { role: 'user', content: [{ type: 'text', text: prompt }] }, + { role: 'user', content: [{ type: 'text', text: '接下来是长时间记忆' }] }, + { role: 'user', content: [{ type: 'text', text: longTermMemoryList }] }, + { role: 'user', content: [{ type: 'text', text: '接下来是短时间记忆' }] }, + ...data, + { role: 'user', content: [{ type: 'text' as const, text: '接下来是本次引用消息' }] }, + ...(reply ? [{ role: 'user' as const, content: reply }] : []), + { role: 'user', content: [{ type: 'text' as const, text: '接下来是当前对话' }] }, + { role: 'user', content: msgArray } + ]); + const msgRet = await generateChatCompletion(contentData); + const sentMsg = await sendGroupMessage(group_id, msgRet, action, adapter, instance); + return { id: sentMsg?.data?.message_id, text: msgRet }; +} + +async function shouldRespond(message: OB11ArrayMessage, core: NapCatCore, oriMsg: any, currentHot: number, msgArray: Array, reply?: Array): Promise { + if ( + !message.raw_message.startsWith(BOT_NAME) && + !message.message.find(e => e.type == 'at' && e.data.qq == core.selfInfo.uin) && + oriMsg?.sender.user_id.toString() !== core.selfInfo.uin + ) { + if (currentHot > 0) { + if (msgArray.length > 0) { + const longTermMemoryList = longTermMemory.get(message.group_id?.toString()!) || ''; + let shortTermMemoryList = shortTermMemory.get(message.group_id?.toString()!); + let prompt = `请根据在群内聊天与 ${message.sender.card || message.sender?.nickname} 发送的聊天消息推测本次消息是否应该回应。自身无关的话题和图片不要回复,尤其减少对图片消息的回复可能性, 注意回复内容只用输出2 - 3个字, 一定注意不想回复请回应不回复三个字即可, 想回复回应回复即可, 你的人设:${PROMPT}`; + if (!shortTermMemoryList) { + let MemoryShort: Array[] = []; + shortTermMemory.set(message.group_id?.toString()!, MemoryShort); + shortTermMemoryList = MemoryShort; + } + const contentData: Array = await toSingleRole([ + { role: 'user', content: [{ type: 'text', text: prompt }] }, + { role: 'user', content: [{ type: 'text', text: '接下来是长时间记忆' }] }, + { role: 'user', content: [{ type: 'text', text: longTermMemoryList }] }, + { role: 'user', content: [{ type: 'text', text: '接下来是短时间记忆' }] }, + + ...(shortTermMemoryList.map(msg => ({ role: 'user' as const, content: msg.filter(e => e.type === 'text') }))), + { role: 'user', content: [{ type: 'text' as const, text: '接下来是本次引用消息' }] }, + ...(reply ? [{ role: 'user' as const, content: reply }] : []), + { role: 'user', content: [{ type: 'text' as const, text: '接下来是当前对话' }] }, + { role: 'user', content: msgArray } + ]); + const msgRet = await generateChatCompletion(contentData); + if (msgRet.indexOf('不回复') !== -1) { + return false; + } + } + } else { + return false; + } + } + return true; +} + +async function handleClearMemory(message: OB11ArrayMessage, action: ActionMap, adapter: string, instance: OB11PluginAdapter) { + if (message.raw_message === '/清除短期上下文' && message.sender.user_id.toString() === BOT_ADMIN) { + await handleClearMemoryCommand(message.group_id?.toString()!, 'short', action, adapter, instance); + return true; + } + + if (message.raw_message === '/清除长期上下文' && message.sender.user_id.toString() === BOT_ADMIN) { + await handleClearMemoryCommand(message.group_id?.toString()!, 'long', action, adapter, instance); + return true; + } + return false; } export const plugin_onmessage = async ( @@ -190,47 +247,47 @@ export const plugin_onmessage = async ( action: ActionMap, instance: OB11PluginAdapter ) => { - const current_hot = chatHot.get(message.group_id?.toString()!) || 0; - const orimsgid = message.message.find(e => e.type == 'reply')?.data.id; - const orimsg = orimsgid ? await action.get('get_msg')?._handle({ message_id: orimsgid }, adapter, instance.config) : undefined; - - if (message.raw_message === '/清除短期上下文') { - await handleClearMemoryCommand(message.group_id?.toString()!, 'short', action, adapter, instance); - return; - } - - if (message.raw_message === '/清除长期上下文') { - await handleClearMemoryCommand(message.group_id?.toString()!, 'long', action, adapter, instance); - return; - } - - if ( - !message.raw_message.startsWith(BOT_NAME) && - !message.message.find(e => e.type == 'at' && e.data.qq == core.selfInfo.uin) && - orimsg?.sender.user_id.toString() !== core.selfInfo.uin - ) { - if (current_hot > 0) { - const msg_string = await handleMessage(message, adapter, action, instance); - if (msg_string) { - const longTermMemoryString = longTermMemory.get(message.group_id?.toString()!) || ''; - const shortTermMemoryString = shortTermMemory.get(message.group_id?.toString()!)?.join('\n') || ''; - const user_info = await action.get('get_group_member_info')?.handle({ group_id: message.group_id?.toString()!, user_id: message.sender.user_id }, adapter, instance.config); - const content_data = - `请根据在群内聊天与 ${user_info?.data?.card || user_info?.data?.nickname} 发送的聊天消息推测本次消息是否应该回应。${CQCODE},注意回复内容只用输出内容,一定注意不想回复请回应不回复三个字即可,想回复回应回复即可,你的人设:${PROMPT}长时间记忆:\n${longTermMemoryString}\n短时间记忆:\n${shortTermMemoryString}\n当前对话:\n${msg_string}\n}` - const msg_ret = await generateChatCompletion(content_data, message.message.filter(e => e.type === 'image').map(e => e.data.url!)); - if (msg_ret.indexOf('不回复') == -1) { - return; - } + const currentHot = await chatHotMutex.runExclusive(async () => { + const group_id = message.group_id?.toString()!; + const chatHotData = chatHot.get(group_id); + const currentTime = Date.now(); + if (chatHotData) { + if (currentTime - chatHotData.usetime > 30000) { + chatHot.set(group_id, { count: chatHotData.count, usetime: currentTime, usecount: 0 }); + } else if (chatHotData.usecount > 2) { + chatHot.set(group_id, { count: 0, usetime: currentTime, usecount: 0 }); } } else { - return; + chatHot.set(group_id, { count: 0, usetime: currentTime, usecount: 0 }); } + return chatHot.get(group_id)!; + }); + console.log('currentHot', currentHot); + const oriMsgId = message.message.find(e => e.type == 'reply')?.data.id; + const oriMsg = (oriMsgId ? await action.get('get_msg')?._handle({ message_id: oriMsgId }, adapter, instance.config) : undefined) as OB11ArrayMessage | undefined; + const msgArray = await handleMessage(message, adapter, action, instance); + if (!msgArray) return; + await updateMemory(message.group_id?.toString()!, [msgArray], core); + + if (await handleClearMemory(message, action, adapter, instance)) return; + + const oriMsgOpenai = oriMsg ? await handleMessage(oriMsg, adapter, action, instance) : undefined; + + if (await shouldRespond(message, core, oriMsg, currentHot?.count || 0, msgArray, oriMsgOpenai)) { + const sentMsg = await handleChatResponse(message, msgArray, adapter, action, instance, core, oriMsgOpenai); + await updateMemory(message.group_id?.toString()!, [[{ + type: 'text', + text: `我(群昵称: 乔千)(${core.selfInfo.uin})发送了消息(消息id: ${sentMsg.id}) : ` + sentMsg.text + }]], core); + await chatHotMutex.runExclusive(() => { + const currentTime = Date.now(); + const group_id = message.group_id?.toString()!; + if (currentTime - currentHot.usetime < 40000) { + chatHot.set(group_id, { count: currentHot.count + 1, usetime: currentHot.usetime, usecount: currentHot.usecount + 1 }); + } else { + chatHot.set(group_id, { count: 0, usetime: currentTime, usecount: 0 }); + } + }) } - const msg_string = await handleMessage(message, adapter, action, instance); - if (!msg_string) return; - - await updateMemory(message.group_id?.toString()!, msg_string); - let sended_msg = await handleChatResponse(message, msg_string, adapter, action, instance, core); - await updateMemory(message.group_id?.toString()!, `乔千(${core.selfInfo.uin})发送了消息(消息id:${sended_msg}) :` + msg_string); }; \ No newline at end of file