diff --git a/package.json b/package.json index 52521567..127e273a 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,6 @@ "ajv": "^8.13.0", "async-mutex": "^0.5.0", "commander": "^13.0.0", - "cors": "^2.8.5", "esbuild": "0.25.0", "eslint": "^9.14.0", "eslint-import-resolver-typescript": "^3.6.1", @@ -63,8 +62,10 @@ }, "dependencies": { "@ffmpeg.wasm/core-mt": "^0.13.2", + "cors": "^2.8.5", "compressing": "^1.10.1", "express": "^5.0.0", + "openai": "^4.85.1", "piscina": "^4.7.0", "silk-wasm": "^3.6.1", "ws": "^8.18.0" diff --git a/src/core/apis/msg.ts b/src/core/apis/msg.ts index dda4af1a..5dd4d969 100644 --- a/src/core/apis/msg.ts +++ b/src/core/apis/msg.ts @@ -12,7 +12,7 @@ export class NTQQMsgApi { this.context = context; this.core = core; } - + async clickInlineKeyboardButton(...params: Parameters) { return this.context.session.getMsgService().clickInlineKeyboardButton(...params); } @@ -136,6 +136,20 @@ export class NTQQMsgApi { }); } + async queryFirstMsgBySender(peer: Peer, SendersUid: string[]) { + console.log(peer, SendersUid); + return await this.context.session.getMsgService().queryMsgsWithFilterEx('0', '0', '0', { + chatInfo: peer, + filterMsgType: [], + filterSendersUid: SendersUid, + filterMsgToTime: '0', + filterMsgFromTime: '0', + isReverseOrder: true, + isIncludeCurrent: true, + pageLimit: 20000, + }); + } + async setMsgRead(peer: Peer) { return this.context.session.getMsgService().setMsgRead(peer); } diff --git a/src/onebot/index.ts b/src/onebot/index.ts index 78b25397..772b2fc6 100644 --- a/src/onebot/index.ts +++ b/src/onebot/index.ts @@ -50,6 +50,7 @@ import { import { OB11Message } from './types'; import { IOB11NetworkAdapter } from '@/onebot/network/adapter'; import { OB11HttpSSEServerAdapter } from './network/http-server-sse'; +import { OB11PluginAdapter } from './network/plugin'; //OneBot实现类 export class NapCatOneBot11Adapter { @@ -113,9 +114,9 @@ export class NapCatOneBot11Adapter { //创建NetWork服务 // 注册Plugin 如果需要基于NapCat进行快速开发 - // this.networkManager.registerAdapter( - // new OB11PluginAdapter('myPlugin', this.core, this,this.actions) - // ); + this.networkManager.registerAdapter( + new OB11PluginAdapter('myPlugin', this.core, this,this.actions) + ); for (const key of ob11Config.network.httpServers) { if (key.enable) { this.networkManager.registerAdapter( diff --git a/src/onebot/network/plugin.ts b/src/onebot/network/plugin.ts index 717276d9..8376fb9e 100644 --- a/src/onebot/network/plugin.ts +++ b/src/onebot/network/plugin.ts @@ -1,5 +1,5 @@ import { OB11EmitEventContent, OB11NetworkReloadType } from './index'; -import { NapCatOneBot11Adapter, OB11Message } from '@/onebot'; +import { NapCatOneBot11Adapter, OB11ArrayMessage, OB11Message } from '@/onebot'; import { NapCatCore } from '@/core'; import { PluginConfig } from '../config/config'; import { plugin_onmessage } from '@/plugin'; @@ -22,7 +22,7 @@ export class OB11PluginAdapter extends IOB11NetworkAdapter { onEvent(event: T) { if (event.post_type === 'message') { - plugin_onmessage(this.config.name, this.core, this.obContext, event as OB11Message, this.actions, this).then().catch(); + plugin_onmessage(this.config.name, this.core, this.obContext, event as OB11ArrayMessage, this.actions, this).then().catch(); } } diff --git a/src/onebot/types/message.ts b/src/onebot/types/message.ts index ff14e8fb..bf572d98 100644 --- a/src/onebot/types/message.ts +++ b/src/onebot/types/message.ts @@ -30,6 +30,10 @@ export interface OB11Message { post_type?: EventType; raw?: RawMessage; } +export interface OB11ArrayMessage extends OB11Message { + message_format: 'array'; + message: OB11MessageData[]; +} // 合并转发消息接口定义 export interface OB11ForwardMessage extends OB11Message { diff --git a/src/plugin/index.ts b/src/plugin/index.ts index 484a58b5..5f1c3a69 100644 --- a/src/plugin/index.ts +++ b/src/plugin/index.ts @@ -1,11 +1,217 @@ -import { NapCatOneBot11Adapter, OB11Message } from '@/onebot'; -import { NapCatCore } from '@/core'; +import { NapCatOneBot11Adapter, OB11ArrayMessage, OB11MessageDataType } from '@/onebot'; +import { ChatType, NapCatCore, Peer, RawMessage } from '@/core'; import { ActionMap } from '@/onebot/action'; import { OB11PluginAdapter } from '@/onebot/network/plugin'; +import { OpenAI } from 'openai'; +import { RequestUtil } from '@/common/request'; +import { randomBytes } from 'node:crypto'; +const client = new OpenAI({ + apiKey: '',//必填多模态 + baseURL: 'https://api.bili2233.work/v1' +}); -export const plugin_onmessage = async (adapter: string, _core: NapCatCore, _obCtx: NapCatOneBot11Adapter, message: OB11Message, action: ActionMap, instance: OB11PluginAdapter) => { - if (message.raw_message === 'ping') { - const ret = await action.get('send_group_msg')?.handle({ group_id: String(message.group_id), message: 'pong' }, adapter, instance.config); - console.log(ret); +async function handleMessageArray2String(messages: RawMessage[]): Promise { + const result = []; + let data = ''; + for (let i = 0; i < messages.length; i++) { + try { + if (messages[i]) { + data += await handleMessage2String(messages[i]!) + '\n'; + } + } catch { + continue; + } + if ((i + 1) % 1000 === 0 || i === messages.length - 1) { + result.push(data); + data = ''; + } } -}; + return result; +} + +async function handleMessage2String(message: RawMessage): Promise { + let data = ''; + for (let element of message.elements) { + if (element.textElement) { + data += element.textElement.content.replaceAll('->', '').replaceAll('<-', ''); + } + if (element.replyElement) { + const records = message.records.find(msgRecord => msgRecord.msgId === element.replyElement?.sourceMsgIdInRecords); + if (records) { + data += '[Reply] 回应别人的消息 ->' + await handleMessage2String(records) + '<-'; + } + } + } + if (data.length === 0) throw new Error('消息为空'); + return (message.sendMemberName || message.sendNickName) + ' 说: ->' + data + '<- '; +} + +async function generateChatCompletion(content_data: string): Promise { + const chatCompletion = await client.chat.completions.create({ + messages: [{ role: 'user', content: content_data }], + model: 'gemini-2.0-flash-thinking-exp' + }); + console.log(chatCompletion); + return chatCompletion.choices[0]?.message.content || ''; +} + +async function generateChatCompletionWithImg(content_data: string, url: string): Promise { + const chatCompletion = await client.chat.completions.create({ + messages: [ + { + role: 'user', content: [ + { + type: 'text', + text: content_data + }, { + type: 'image_url', + image_url: { + url: url + } + } + ] + }, + + ], + model: 'gemini-2.0-flash-thinking-exp' + }); + return chatCompletion.choices[0]?.message.content || ''; +} + +export const plugin_onmessage = async ( + adapter: string, + core: NapCatCore, + _obCtx: NapCatOneBot11Adapter, + message: OB11ArrayMessage, + action: ActionMap, + instance: OB11PluginAdapter +) => { + + + if (!message.message.find(m => m.type === 'text' && m.data.text.includes('#画像'))) { + return; + } + + const user_id = message.message.find(m => m.type === 'at')?.data.qq ?? message.sender.user_id; + const user_uid = await core.apis.UserApi.getUidByUinV2(user_id.toString()); + if (!user_uid) { + return; + } + + const peer: Peer = { chatType: ChatType.KCHATTYPEGROUP, peerUid: message.group_id?.toString() ?? '' }; + const msg = await core.apis.MsgApi.queryFirstMsgBySender(peer, [user_uid]); + if (msg.msgList.length < 1) { + return; + } + let msg_tag = '根据下面图片提取该图片的描述的描述,回应只用给出头像描述即可不要给出 好的 等等无关句子也不得提及该提示词,下面为图片内容。'; + let avater_info = '头像仅供参考,Ta的头像描述: ' + await generateChatCompletionWithImg(msg_tag, `https://thirdqq.qlogo.cn/g?b=sdk&nk=${user_id}&s=100`); + console.log(`Final avater_info ret: ${avater_info}`) + const msg_string_all = await handleMessageArray2String(msg.msgList); + const user_info = await action.get('get_group_member_info')?.handle({ group_id: message.group_id?.toString()!, user_id: user_id }, adapter, instance.config); + + if (msg_string_all.length > 1) { + const summaryPromises = msg_string_all.map(async (msg_string, i) => { + const content_data = `请根据下面聊天内容,分析 ${user_info?.data?.card || user_info?.data?.nickname} 的聊天风格分析其性格特点和一些有趣的信息和好笑的信息,为其建立用户画像,并加以幽默风趣的吐槽,下面是聊天内容,通过-><-字符区分结构。注意回复内容只用输出内容,不要提及此段话,注意一定不要使用markdown,请采用纯文本回复。附加提示信息:${avater_info} \n精选聊天记录: ${msg_string}`; + try { + const data = await generateChatCompletion(content_data); + if (data) { + msg_string_all[i] = '[总结(此消息过长Ai已压缩改为总结)] ->' + data + '<- '; + console.log(`Summary for part ${i + 1}: ${data}`); + } + } catch (error) { + msg_string_all[i] = ''; + } + }); + await action.get('send_group_msg')?.handle({ + group_id: String(message.group_id), + message: [ + { + type: OB11MessageDataType.reply, + data: { + id: message.message_id.toString() + } + }, + { + type: OB11MessageDataType.text, + data: { + text: `消息过长,共` + msg.msgList.length + `条消息,预计时间` + Math.round(10 * msg.msgList.length / 1000) + `秒,请稍等...` + } + }] + }, adapter, instance.config); + await Promise.all(summaryPromises); + } + + const msg_string = msg_string_all.join('\n'); + + const content_data = + `请根据下面聊天内容,分析 ${user_info?.data?.card || user_info?.data?.nickname} 的聊天风格分析其性格特点和一些有趣的信息和好笑的信息,为其建立用户画像,并加以幽默风趣的吐槽,下面是聊天内容,通过-><-字符区分结构。注意回复内容只用输出内容,不要提及此段话,注意一定不要使用markdown,请采用纯文本回复。附加信息:${avater_info} \n精选聊天记录:${msg_string}`; + console.log(`Final content data: ${content_data}`); + const msg_ret = await generateChatCompletion(content_data); + console.log(`Final content ret: ${msg_ret}`) + let pic_tag = `请根据下面对该人物性格的分析,并虚构想象一个场景,生成如 (1 cute girl with (cat ear and cat tail:1.2) stands in the garden:1.1), (cute:1.35), (detailed beautiful eyes:1.3), (beautiful face:1.3), casual, silver hair, silver ear, (blue hair:0.8), (blue ear:0.8), long hair, coat, short skirt, hair blowing with the wind, (blue eye:1.2), flowers, (little girl:0.65), butterflys flying around 格式的文本用于描述人物,注意格式为英文加空格加逗号进行区分,请务必多的描述人物和想象和场景,至少50个描述Tag,风格是可爱动漫二次元风,注意一定要是人为主体描述,不要好的什么的回应,只用给出要求格式的文本,不需要 好的 的回应,也不要提及此段话,下面为该人物性格分析.附加信息:${avater_info}.下面是人物分析.\n${msg_ret}`; + let pic_tag_ret = await generateChatCompletion(pic_tag); + let pic = `https://thirdqq.qlogo.cn/g?b=sdk&nk=${user_id}&s=100`; + try { + let pic_generate = await RequestUtil.HttpGetJson<{ images?: Array<{ url: string }> }> + ('https://api.siliconflow.cn/v1/images/generations', 'POST', { + "model": "stabilityai/stable-diffusion-xl-base-1.0", + "prompt": 'original, (masterpiece), (illustration), (extremely fine and beautiful), perfect detailed, photorealistic, (beautiful and clear background:1.25), (depth of field:0.7),' + pic_tag_ret, + "seed": randomBytes(4).readUInt32LE(0), + "negative_prompt": "(copyright name:1.5),logo,(watermark:1.5),character_watermark,lowres, bad anatomy, bad hands, text, error, missing fingers, extra digit, fewer digits, cropped, worst quality, low quality, normal quality, jpeg artifacts, signature, watermark, username, blurry, bad feet, ((cowboy)),(((pubic))), ((((pubic_hair))))sketch, duplicate, ugly, huge eyes, text, logo, monochrome, worst face, (bad and mutated hands:1.3), (worst quality:2.0), (low quality:2.0), (blurry:2.0), horror, geometry, bad_prompt, (bad hands), (missing fingers), multiple limbs, bad anatomy, (interlocked fingers:1.2), Ugly Fingers, (extra digit and hands and fingers and legs and arms:1.4), crown braid, ((2girl)), (deformed fingers:1.2), (long fingers:1.2),succubus wings,horn,succubus horn,succubus hairstyle, (bad-artist-anime), bad-artist, bad hand" + }, { + Authorization: 'Bearer ',//必填 + 'Content-Type': 'application/json' + }); + if (pic_generate?.images?.[0]) { + pic = pic_generate?.images?.[0].url; + } + } catch (error) { + + } + await action.get('send_group_msg')?.handle({ + group_id: String(message.group_id), + message: [{ + type: OB11MessageDataType.node, + data: { + user_id: user_id, + nickname: user_info?.data?.card || user_info?.data?.nickname || '', + content: [ + { + type: OB11MessageDataType.text, + data: { text: msg_ret } + } + ] + } + }, + { + type: OB11MessageDataType.node, + data: { + user_id: user_id, + nickname: user_info?.data?.card || user_info?.data?.nickname || '', + content: [ + { + type: OB11MessageDataType.text, + data: { text: 'Tag: ' + pic_tag_ret } + } + ] + } + }, + { + type: OB11MessageDataType.node, + data: { + user_id: user_id, + nickname: user_info?.data?.card || user_info?.data?.nickname || '', + content: [ + { + type: OB11MessageDataType.image, + data: { + file: pic + } + } + ] + } + } + ] + }, adapter, instance.config); + +}; \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts index 9ff471cf..2b930a60 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -9,7 +9,8 @@ const external = [ 'ws', 'express', '@ffmpeg.wasm/core-mt', - 'piscina' + 'piscina', + 'openai' ]; const nodeModules = [...builtinModules, builtinModules.map((m) => `node:${m}`)].flat();