import plugin from '../../../lib/plugins/plugin.js' import _ from 'lodash' import { Config } from '../utils/config.js' import { v4 as uuid } from 'uuid' import delay from 'delay' import { ChatGPTAPI } from 'chatgpt' import { ChatGPTClient, BingAIClient } from '@waylaidwanderer/chatgpt-api' import { escapeHtml, getMessageById, makeForwardMsg, tryTimes, upsertMessage, randomString } from '../utils/common.js' import { ChatGPTPuppeteer } from '../utils/browser.js' import { KeyvFile } from 'keyv-file' import { OfficialChatGPTClient } from '../utils/message.js' import fetch from 'node-fetch' import { deleteConversation, getConversations, getLatestMessageIdByConversationId } from '../utils/conversation.js' let version = Config.version /** * 每个对话保留的时长。单个对话内ai是保留上下文的。超时后销毁对话,再次对话创建新的对话。 * 单位:秒 * @type {number} * * 这里使用动态数据获取,以便于锅巴动态更新数据 */ // const CONVERSATION_PRESERVE_TIME = Config.conversationPreserveTime const defaultPropmtPrefix = 'You answer as concisely as possible for each response (e.g. don’t be verbose). It is very important that you answer as concisely as possible, so please remember this. If you are generating a list, do not have too many items. Keep the number of items short.' export class chatgpt extends plugin { constructor () { let toggleMode = Config.toggleMode super({ /** 功能名称 */ name: 'chatgpt', /** 功能描述 */ dsc: 'chatgpt from openai', /** https://oicqjs.github.io/oicq/#events */ event: 'message', /** 优先级,数字越小等级越高 */ priority: 15000, rule: [ { /** 命令正则匹配 */ reg: toggleMode === 'at' ? '^[^#][sS]*' : '#chat[^gpt][sS]*', /** 执行方法 */ fnc: 'chatgpt' }, { reg: '#chatgpt对话列表', fnc: 'getAllConversations', permission: 'master' }, { reg: '^#结束对话([sS]*)', fnc: 'destroyConversations' }, // { // reg: '#chatgpt帮助', // fnc: 'help' // }, { reg: '#chatgpt图片模式', fnc: 'switch2Picture' }, { reg: '#chatgpt文本模式', fnc: 'switch2Text' }, { reg: '#清空(chat)?队列', fnc: 'emptyQueue', permission: 'master' }, { reg: '#移出(chat)?队列首位', fnc: 'removeQueueFirst', permission: 'master' }, { reg: '#(OpenAI|openai)(剩余)?(余额|额度)', fnc: 'totalAvailable', permission: 'master' }, { reg: '^#chatgpt切换对话', fnc: 'attachConversation' }, { reg: '^#chatgpt加入对话', fnc: 'joinConversation' }, { reg: '^#chatgpt删除对话', fnc: 'deleteConversation', permission: 'master' } ] }) this.toggleMode = toggleMode } /** * 获取chatgpt当前对话列表 * @param e * @returns {Promise} */ async getConversations (e) { let keys = await redis.keys('CHATGPT:CONVERSATIONS:*') if (!keys || keys.length === 0) { await this.reply('当前没有人正在与机器人对话', true) } else { let response = '当前对话列表:(格式为【开始时间 | qq昵称 | 对话长度 | 最后活跃时间】)\n' await Promise.all(keys.map(async (key) => { let conversation = await redis.get(key) if (conversation) { conversation = JSON.parse(conversation) response += `${conversation.ctime} | ${conversation.sender.nickname} | ${conversation.num} | ${conversation.utime} \n` } })) await this.reply(`${response}`, true) } } /** * 销毁指定人的对话 * @param e * @returns {Promise} */ async destroyConversations (e) { let ats = e.message.filter(m => m.type === 'at') let use = await redis.get('CHATGPT:USE') if (ats.length === 0) { if (use === 'api3') { await redis.del(`CHATGPT:QQ_CONVERSATION:${e.sender.user_id}`) await this.reply('已退出当前对话,该对话仍然保留。请@我进行聊天以开启新的对话', true) } else { let c = await redis.get(`CHATGPT:CONVERSATIONS:${e.sender.user_id}`) if (!c) { await this.reply('当前没有开启对话', true) } else { await redis.del(`CHATGPT:CONVERSATIONS:${e.sender.user_id}`) await this.reply('已结束当前对话,请@我进行聊天以开启新的对话', true) } } } else { let at = ats[0] let qq = at.qq let atUser = _.trimStart(at.text, '@') if (use === 'api3') { await redis.del(`CHATGPT:QQ_CONVERSATION:${qq}`) await this.reply(`${atUser}已退出TA当前的对话,TA仍可以@我进行聊天以开启新的对话`, true) } else { let c = await redis.get(`CHATGPT:CONVERSATIONS:${qq}`) if (!c) { await this.reply(`当前${atUser}没有开启对话`, true) } else { await redis.del(`CHATGPT:CONVERSATIONS:${qq}`) await this.reply(`已结束${atUser}的对话,TA仍可以@我进行聊天以开启新的对话`, true) } } } } async deleteConversation (e) { let ats = e.message.filter(m => m.type === 'at') let use = await redis.get('CHATGPT:USE') if (use !== 'api3') { await this.reply('本功能当前仅支持API3模式', true) return false } if (ats.length === 0 || (ats.length === 1 && e.atme)) { let conversationId = _.trimStart(e.msg, '#chatgpt删除对话').trim() if (!conversationId) { await this.reply('指令格式错误,请同时加上对话id或@某人以删除他当前进行的对话', true) return false } else { let deleteResponse = await deleteConversation(conversationId) console.log(deleteResponse) let deleted = 0 let qcs = await redis.keys('CHATGPT:QQ_CONVERSATION:*') for (let i = 0; i < qcs.length; i++) { if (await redis.get(qcs[i]) === conversationId) { await redis.del(qcs[i]) if (Config.debug) { logger.info('delete conversation bind: ' + qcs[i]) } deleted++ } } await this.reply(`对话删除成功,同时清理了${deleted}个同一对话中用户的对话。`, true) } } else { for (let u = 0; u < ats.length; u++) { let at = ats[u] let qq = at.qq let atUser = _.trimStart(at.text, '@') let conversationId = await redis.get('CHATGPT:QQ_CONVERSATION:' + qq) if (conversationId) { let deleteResponse = await deleteConversation(conversationId) console.log(deleteResponse) let deleted = 0 let qcs = await redis.keys('CHATGPT:QQ_CONVERSATION:*') for (let i = 0; i < qcs.length; i++) { if (await redis.get(qcs[i]) === conversationId) { await redis.del(qcs[i]) if (Config.debug) { logger.info('delete conversation bind: ' + qcs[i]) } deleted++ } } await this.reply(`${atUser}的对话${conversationId}删除成功,同时清理了${deleted}个同一对话中用户的对话。`) } else { await this.reply(`${atUser}当前已没有进行对话`) } } } } async help (e) { let response = 'chatgpt-plugin使用帮助文字版\n' + '@我+聊天内容: 发起对话与AI进行聊天\n' + '#chatgpt对话列表: 查看当前发起的对话\n' + '#结束对话: 结束自己或@用户的对话\n' + '#chatgpt帮助: 查看本帮助\n' + '源代码:https://github.com/ikechan8370/chatgpt-plugin' await this.reply(response) } async switch2Picture (e) { let userSetting = await redis.get(`CHATGPT:USER:${e.sender.user_id}`) if (!userSetting) { userSetting = { usePicture: true } } else { userSetting = JSON.parse(userSetting) } userSetting.usePicture = true await redis.set(`CHATGPT:USER:${e.sender.user_id}`, JSON.stringify(userSetting)) await this.reply('ChatGPT回复已转换为图片模式') } async switch2Text (e) { let userSetting = await redis.get(`CHATGPT:USER:${e.sender.user_id}`) if (!userSetting) { userSetting = { usePicture: false } } else { userSetting = JSON.parse(userSetting) } userSetting.usePicture = false await redis.set(`CHATGPT:USER:${e.sender.user_id}`, JSON.stringify(userSetting)) await this.reply('ChatGPT回复已转换为文字模式') } /** * #chatgpt * @param e oicq传递的事件参数e */ async chatgpt (e) { let prompt if (this.toggleMode === 'at') { if (!e.msg || e.msg.startsWith('#')) { return false } if (e.isGroup && !e.atme) { return false } prompt = e.msg.trim() } else { prompt = _.trimStart(e.msg.trimStart(), '#chat').trim() if (prompt.length === 0) { return false } } if (Config.imgOcr) { // 取消息中的图片、at的头像、回复的图片,放入e.img if (e.at && !e.source) { e.img = [`https://q1.qlogo.cn/g?b=qq&s=0&nk=${e.at}`] } if (e.source) { let reply if (e.isGroup) { reply = (await e.group.getChatHistory(e.source.seq, 1)).pop()?.message } else { reply = (await e.friend.getChatHistory(e.source.time, 1)).pop()?.message } if (reply) { for (let val of reply) { if (val.type === 'image') { e.img = [val.url] break } } } } if (e.img) { try { let imgOcrText = '' for (let i in e.img) { const imgorc = await Bot.imageOcr(e.img[i]) if (imgorc.language === 'zh' || imgorc.language === 'en') { for (let text of imgorc.wordslist) { imgOcrText += `${text.words} \n` } } } prompt = imgOcrText + prompt } catch (err) {} } } // 检索是否有屏蔽词 const promtBlockWord = Config.promptBlockWords.find(word => prompt.toLowerCase().includes(word.toLowerCase())) if (promtBlockWord) { await this.reply('主人不让我回答你这种问题,真是抱歉了呢', true) return false } // if (prompt.indexOf('