diff --git a/README.md b/README.md index 95d4e73..9a6d980 100644 --- a/README.md +++ b/README.md @@ -14,31 +14,18 @@ pnpm install -w chatgpt undici git clone https://github.com/ikechan8370/yunzai-chatgpt.git ./plugins/chatgpt ``` 3. 修改配置 -编辑`plugins/chatgpt/index.js`文件主要修改其中的`SESSION_TOKEN`常量,修改为你的openai账号的token。token获取参见下文。 +编辑`plugins/chatgpt/config/index.js`文件主要修改其中的`SESSION_TOKEN`常量,修改为你的openai账号的token。token获取参见后文。 ## 使用 -### 默认方式 -#chatgpt开头即可,例如:#chatgpt 介绍一下米哈游 -![image](https://user-images.githubusercontent.com/21212372/205808552-a775cdea-0668-4273-865c-35c5d91ad37e.png) -(图片仅供参考,chatgpt在某些领域依然是人工智障,但语言起码流畅自信多了) - -比如让他写代码 -![image](https://user-images.githubusercontent.com/21212372/205810566-af10e141-1ab4-4629-998d-664eea3ad827.png) - -比如让他写剧本 -image - -### 群聊使用艾特(@)的方式 -如果你的机器人插件少不担心冲突问题的话,将 `index.js` 重命名为 `index.js.bak`,将 `index_no#.js` 重命名为 `index.js`,此时将基于艾特模式进行聊天。 - -此时只需在群聊中@机器人+聊天内容即可。 -![image](https://user-images.githubusercontent.com/21212372/206436999-c8d3bd48-aa39-496a-a71a-89164e9d7c18.png) - -同时,此模式下私聊直接打字聊天即可,也无需加#chatgpt前缀。 -![image](https://user-images.githubusercontent.com/21212372/206437284-afed0fc6-caaa-4c6e-92e4-53fccbeff286.png) +### 基本使用 +@机器人 发送聊内容即可 +![img.png](resources/img/example1.png) 发挥你的想象力吧! +### 获取帮助 +发送#chatgpt帮助 + ## 关于openai token获取 1. 注册openai账号 进入https://chat.openai.com/ ,选择signup注册。目前openai不对包括俄罗斯、乌克兰、伊朗、中国等国家和地区提供服务,所以自行寻找办法使用其他国家和地区的ip登录。此外,注册可能需要验证所在国家和地区的手机号码,如果没有国外手机号可以试试解码网站,收费的推荐https://sms-activate.org/。 diff --git a/apps/chat.js b/apps/chat.js new file mode 100644 index 0000000..cf26db5 --- /dev/null +++ b/apps/chat.js @@ -0,0 +1,281 @@ +import plugin from '../../../lib/plugins/plugin.js' +import { ChatGPTAPI } from 'chatgpt' +import _ from 'lodash' +import { Config } from '../config/index.js' +import showdown from 'showdown' +import mjAPI from 'mathjax-node' +// import showdownKatex from 'showdown-katex' +const SESSION_TOKEN = Config.token +const blockWords = '屏蔽词1,屏蔽词2,屏蔽词3' +const converter = new showdown.Converter({ + extensions: [ + // showdownKatex({ + // delimiters: [ + // { left: '$$', right: '$$', display: false }, + // { left: '$', right: '$', display: false, inline: true }, + // { left: '\\(', right: '\\)', display: false }, + // { left: '\\[', right: '\\]', display: true } + // ] + // }) + ] +}) +/** + * 每个对话保留的时长。单个对话内ai是保留上下文的。超时后销毁对话,再次对话创建新的对话。 + * 单位:秒 + * @type {number} + */ +const CONVERSATION_PRESERVE_TIME = 600 + +mjAPI.config({ + MathJax: { + // traditional MathJax configuration + } +}) +mjAPI.start() + +export class chatgpt extends plugin { + constructor () { + super({ + /** 功能名称 */ + name: 'chatgpt', + /** 功能描述 */ + dsc: 'chatgpt from openai', + /** https://oicqjs.github.io/oicq/#events */ + event: 'message', + /** 优先级,数字越小等级越高 */ + priority: 5000, + rule: [ + { + /** 命令正则匹配 */ + reg: '^[^#][sS]*', + /** 执行方法 */ + fnc: 'chatgpt' + }, + { + reg: '#chatgpt对话列表', + fnc: 'getConversations', + permission: 'master' + }, + { + reg: '^#结束对话([sS]*)', + fnc: 'destroyConversations' + }, + { + reg: '#chatgpt帮助', + fnc: 'help' + }, + { + reg: '#chatgpt图片模式', + fnc: 'switch2Picture' + }, + { + reg: '#chatgpt文字模式', + fnc: 'switch2Text' + } + ] + }) + this.chatGPTApi = new ChatGPTAPI({ + sessionToken: SESSION_TOKEN, + markdown: true, + userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36' + }) + } + + /** + * 获取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') + if (ats.length === 0) { + 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, '@') + 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}的对话,他仍可以@我进行聊天以开启新的对话`, true) + } + } + } + + 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) { + if (!e.msg || e.msg.startsWith('#')) { + return + } + if (e.isGroup && !e.atme) { + return + } + // let question = _.trimStart(e.msg, '#chatgpt') + let question = e.msg.trimStart() + // await e.runtime.render('chatgpt-plugin', 'content/index', { content: "", question }) + // return + try { + await this.chatGPTApi.ensureAuth() + } catch (e) { + logger.error(e) + await this.reply(`OpenAI认证失败,请检查Token:${e}`, true) + return + } + await this.reply('我正在思考如何回复你,请稍等', true, 5) + let c + logger.info(`chatgpt question: ${question}`) + let previousConversation = await redis.get(`CHATGPT:CONVERSATIONS:${e.sender.user_id}`) + if (!previousConversation) { + c = this.chatGPTApi.getConversation() + let ctime = new Date() + previousConversation = { + sender: e.sender, + conversation: c, + ctime, + utime: ctime, + num: 0 + } + await redis.set(`CHATGPT:CONVERSATIONS:${e.sender.user_id}`, JSON.stringify(previousConversation), { EX: CONVERSATION_PRESERVE_TIME }) + } else { + previousConversation = JSON.parse(previousConversation) + c = this.chatGPTApi.getConversation({ + conversationId: previousConversation.conversation.conversationId, + parentMessageId: previousConversation.conversation.parentMessageId + }) + } + try { + // console.log({ c }) + let response = await c.sendMessage(question) + // console.log({c}) + // console.log(response) + // 更新redis中的conversation对象,因为send后c已经被自动更新了 + await redis.set(`CHATGPT:CONVERSATIONS:${e.sender.user_id}`, JSON.stringify({ + sender: e.sender, + conversation: c, + ctime: previousConversation.ctime, + utime: new Date(), + num: previousConversation.num + 1 + }), { EX: CONVERSATION_PRESERVE_TIME }) + + // 检索是否有屏蔽词 + const blockWord = blockWords.split(',').find(word => response.toLowerCase().includes(word.toLowerCase())) + if (blockWord) { + await this.reply('返回内容存在敏感词,我不想回答你', true) + return + } + let userSetting = await redis.get(`CHATGPT:USER:${e.sender.user_id}`) + if (userSetting) { + userSetting = JSON.parse(userSetting) + } else { + userSetting = { + usePicture: false + } + } + if (userSetting.usePicture) { + while (!response.trimEnd().endsWith('.') && !response.trimEnd().endsWith('。') && !response.trimEnd().endsWith('……')) { + await this.reply('内容有点多,我正在奋笔疾书,请再等一会', true, 5) + const responseAppend = await c.sendMessage('Continue') + // 检索是否有屏蔽词 + const blockWord = blockWords.split(',').find(word => responseAppend.toLowerCase().includes(word.toLowerCase())) + if (blockWord) { + await this.reply('返回内容存在敏感词,我不想回答你', true) + return + } + if (responseAppend.indexOf('conversation') > -1 || responseAppend.startsWith("I'm sorry")) { + logger.warn('chatgpt might forgot what it had said') + break + } + // 更新redis中的conversation对象,因为send后c已经被自动更新了 + await redis.set(`CHATGPT:CONVERSATIONS:${e.sender.user_id}`, JSON.stringify({ + sender: e.sender, + conversation: c, + ctime: previousConversation.ctime, + utime: new Date(), + num: previousConversation.num + 1 + }), { EX: CONVERSATION_PRESERVE_TIME }) + + response = response + responseAppend + } + // logger.info(response) + // markdown转为html + // todo部分数学公式可能还有问题 + let converted = converter.makeHtml(response) + + /** 最后回复消息 */ + await e.runtime.render('chatgpt-plugin', 'content/index', { content: converted, question, senderName: e.sender.nickname }) + } else { + await this.reply(`${response}`, e.isGroup) + } + } catch (e) { + logger.error(e) + await this.reply(`与OpenAI通信异常,请稍后重试:${e}`, true, { recallMsg: e.isGroup ? 10 : 0 }) + } + } +} diff --git a/apps/help.js b/apps/help.js new file mode 100644 index 0000000..b1986dd --- /dev/null +++ b/apps/help.js @@ -0,0 +1,76 @@ +import plugin from '../../../lib/plugins/plugin.js' + +let helpData = [ + { + group: '聊天', + list: [ + { + icon: 'chat', + title: '@我+聊天内容', + desc: '与机器人聊天' + }, + { + icon: 'chat-private', + title: '私聊与我对话', + desc: '与机器人聊天' + }, + { + icon: 'picture', + title: '#chatgpt图片模式', + desc: '机器人以图片形式回答' + }, + { + icon: 'text', + title: '#chatgpt文本模式', + desc: '机器人以文本形式回答,默认选项' + }, + + ] + }, + { + group: '管理', + list: [ + { + icon: 'list', + title: '#chatgpt对话列表', + desc: '查询当前哪些人正在与机器人聊天' + }, + { + icon: 'destroy', + title: '#结束对话', + desc: '结束自己当前对话,下次开启对话机器人将遗忘掉本次对话内容。' + }, + { + icon: 'destroy-other', + title: '#结束对话 @某人', + desc: '结束该用户当前对话,下次开启对话机器人将遗忘掉本次对话内容。' + }, + { + icon: 'help', + title: '#chatgpt帮助', + desc: '获取本帮助' + } + ] + } +] + +export class help extends plugin { + constructor (e) { + super({ + name: 'ChatGPT-Plugin帮助', + dsc: 'ChatGPT-Plugin帮助', + event: 'message', + priority: 500, + rule: [ + { + reg: '^(#|[chatgpt|ChatGPT])*(命令|帮助|菜单|help|说明|功能|指令|使用说明)$', + fnc: 'help' + } + ] + }) + } + + async help (e) { + await e.runtime.render('chatgpt-plugin', 'help/index', { helpData }) + } +} diff --git a/config/index.js b/config/index.js new file mode 100644 index 0000000..f4f2957 --- /dev/null +++ b/config/index.js @@ -0,0 +1,5 @@ +const SESSION_TOKEN = '' + +export const Config = { + token: SESSION_TOKEN +} diff --git a/index.js b/index.js index 9074c8f..a1ff11e 100644 --- a/index.js +++ b/index.js @@ -1,176 +1,24 @@ -import plugin from '../../lib/plugins/plugin.js' -import { ChatGPTAPI } from 'chatgpt' -import _ from 'lodash' -const SESSION_TOKEN='' -const blockWords = '屏蔽词1,屏蔽词2,屏蔽词3' +import fs from 'node:fs' -/** - * 每个对话保留的时长。单个对话内ai是保留上下文的。超时后销毁对话,再次对话创建新的对话。 - * 单位:秒 - * @type {number} - */ -const CONVERSATION_PRESERVE_TIME = 600 -export class chatgpt extends plugin { - constructor () { - super({ - /** 功能名称 */ - name: 'chatgpt', - /** 功能描述 */ - dsc: 'chatgpt from openai', - /** https://oicqjs.github.io/oicq/#events */ - event: 'message', - /** 优先级,数字越小等级越高 */ - priority: 5000, - rule: [ - { - /** 命令正则匹配 */ - reg: '^#chatgpt([\s\S]*)', - /** 执行方法 */ - fnc: 'chatgpt' - }, - { - reg: 'chatgpt对话列表', - fnc: 'getConversations', - permission: 'master' - }, - { - reg: '^#结束对话([\s\S]*)', - fnc: 'destroyConversations' - }, - { - reg: 'chatgpt帮助', - fnc: 'help' - } - ] - }) - this.chatGPTApi = new ChatGPTAPI({ - sessionToken: SESSION_TOKEN, - markdown: true, - userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36' - }) - } +const files = fs.readdirSync('./plugins/chatgpt-plugin/apps').filter(file => file.endsWith('.js')) - /** - * 获取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) - } - } +let ret = [] - /** - * 销毁指定人的对话 - * @param e - * @returns {Promise} - */ - async destroyConversations (e) { - let ats = e.message.filter(m => m.type === 'at') - if (ats.length === 0) { - 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('已结束当前对话,请使用#chatgpt进行聊天以开启新的对话', true) - } - } else { - let at = ats[0] - let qq = at.qq - let atUser = _.trimStart(at.text, '@') - 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}的对话,他仍可以使用#chatgpt进行聊天以开启新的对话`, true) - } - } - } +files.forEach((file) => { + ret.push(import(`./apps/${file}`)) +}) - async help (e) { - let response = 'chatgpt-plugin使用帮助文字版\n' + - '#chatgpt+聊天内容: 发起对话与AI进行聊天\n' + - 'chatgpt对话列表: 查看当前发起的对话\n' + - '#结束对话: 结束自己或@用户的对话\n' + - 'chatgpt帮助: 查看本帮助\n' + - '源代码:https://github.com/ikechan8370/chatgpt-plugin' - await this.reply(response) - } +ret = await Promise.allSettled(ret) - /** - * #chatgpt - * @param e oicq传递的事件参数e - */ - async chatgpt (e) { - let question = _.trimStart(e.msg, '#chatgpt') - question = question.trimStart() - try { - await this.chatGPTApi.ensureAuth() - } catch (e) { - logger.error(e) - await this.reply(`OpenAI认证失败,请检查Token:${e}`, true) - return - } - let c - logger.info(`chatgpt question: ${question}`) - let previousConversation = await redis.get(`CHATGPT:CONVERSATIONS:${e.sender.user_id}`) - if (!previousConversation) { - c = this.chatGPTApi.getConversation() - let ctime = new Date() - previousConversation = { - sender: e.sender, - conversation: c, - ctime, - utime: ctime, - num: 0 - } - await redis.set(`CHATGPT:CONVERSATIONS:${e.sender.user_id}`, JSON.stringify(previousConversation), { EX: CONVERSATION_PRESERVE_TIME }) - } else { - previousConversation = JSON.parse(previousConversation) - c = this.chatGPTApi.getConversation({ - conversationId: previousConversation.conversation.conversationId, - parentMessageId: previousConversation.conversation.parentMessageId - }) - } - try { - // console.log({ c }) - const response = await c.sendMessage(question) - logger.info(response) - // 更新redis中的conversation对象,因为send后c已经被自动更新了 - await redis.set(`CHATGPT:CONVERSATIONS:${e.sender.user_id}`, JSON.stringify({ - sender: e.sender, - conversation: c, - ctime: previousConversation.ctime, - utime: new Date(), - num: previousConversation.num + 1 - }), { EX: CONVERSATION_PRESERVE_TIME }) - - // 检索是否有屏蔽词 - const blockWord = blockWords.split(',').find(word => response.toLowerCase().includes(word.toLowerCase())) - if (blockWord) { - await this.reply(`返回内容存在敏感词,我不想回答你`, true) - return - } - - /** 最后回复消息 */ - await this.reply(`${response}`, e.isGroup) - } catch (e) { - logger.error(e) - await this.reply(`与OpenAI通信异常,请稍后重试:${e}`, e.isGroup, { recallMsg: e.isGroup ? 10 : 0 }) - } +let apps = {} +for (let i in files) { + let name = files[i].replace('.js', '') + + if (ret[i].status !== 'fulfilled') { + logger.error(`载入插件错误:${logger.red(name)}`) + logger.error(ret[i].reason) + continue } + apps[name] = ret[i].value[Object.keys(ret[i].value)[0]] } +export { apps } diff --git a/index_no#.js b/index_no#.js deleted file mode 100644 index 14d6bd5..0000000 --- a/index_no#.js +++ /dev/null @@ -1,182 +0,0 @@ -import plugin from '../../lib/plugins/plugin.js' -import { ChatGPTAPI } from 'chatgpt' -import _ from 'lodash' -const SESSION_TOKEN = '' -const blockWords = '屏蔽词1,屏蔽词2,屏蔽词3' - -/** - * 每个对话保留的时长。单个对话内ai是保留上下文的。超时后销毁对话,再次对话创建新的对话。 - * 单位:秒 - * @type {number} - */ -const CONVERSATION_PRESERVE_TIME = 600 -export class chatgpt extends plugin { - constructor () { - super({ - /** 功能名称 */ - name: 'chatgpt', - /** 功能描述 */ - dsc: 'chatgpt from openai', - /** https://oicqjs.github.io/oicq/#events */ - event: 'message', - /** 优先级,数字越小等级越高 */ - priority: 5000, - rule: [ - { - /** 命令正则匹配 */ - reg: '^[^#][sS]*', - /** 执行方法 */ - fnc: 'chatgpt' - }, - { - reg: '#chatgpt对话列表', - fnc: 'getConversations', - permission: 'master' - }, - { - reg: '^#结束对话([sS]*)', - fnc: 'destroyConversations' - }, - { - reg: '#chatgpt帮助', - fnc: 'help' - } - ] - }) - this.chatGPTApi = new ChatGPTAPI({ - sessionToken: SESSION_TOKEN, - markdown: true, - userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36' - }) - } - - /** - * 获取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') - if (ats.length === 0) { - 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, '@') - 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}的对话,他仍可以@我进行聊天以开启新的对话`, true) - } - } - } - - 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) - } - - /** - * #chatgpt - * @param e oicq传递的事件参数e - */ - async chatgpt (e) { - if (!e.msg || e.msg.startsWith("#")) { - return - } - if (e.isGroup && !e.atme) { - return - } - // let question = _.trimStart(e.msg, '#chatgpt') - let question = e.msg.trimStart() - try { - await this.chatGPTApi.ensureAuth() - } catch (e) { - logger.error(e) - await this.reply(`OpenAI认证失败,请检查Token:${e}`, true) - return - } - let c - logger.info(`chatgpt question: ${question}`) - let previousConversation = await redis.get(`CHATGPT:CONVERSATIONS:${e.sender.user_id}`) - if (!previousConversation) { - c = this.chatGPTApi.getConversation() - let ctime = new Date() - previousConversation = { - sender: e.sender, - conversation: c, - ctime, - utime: ctime, - num: 0 - } - await redis.set(`CHATGPT:CONVERSATIONS:${e.sender.user_id}`, JSON.stringify(previousConversation), { EX: CONVERSATION_PRESERVE_TIME }) - } else { - previousConversation = JSON.parse(previousConversation) - c = this.chatGPTApi.getConversation({ - conversationId: previousConversation.conversation.conversationId, - parentMessageId: previousConversation.conversation.parentMessageId - }) - } - try { - // console.log({ c }) - const response = await c.sendMessage(question) - logger.info(response) - // 更新redis中的conversation对象,因为send后c已经被自动更新了 - await redis.set(`CHATGPT:CONVERSATIONS:${e.sender.user_id}`, JSON.stringify({ - sender: e.sender, - conversation: c, - ctime: previousConversation.ctime, - utime: new Date(), - num: previousConversation.num + 1 - }), { EX: CONVERSATION_PRESERVE_TIME }) - - // 检索是否有屏蔽词 - const blockWord = blockWords.split(',').find(word => response.toLowerCase().includes(word.toLowerCase())) - if (blockWord) { - await this.reply(`返回内容存在敏感词,我不想回答你`, true) - return - } - - /** 最后回复消息 */ - await this.reply(`${response}`, e.isGroup) - } catch (e) { - logger.error(e) - await this.reply(`与OpenAI通信异常,请稍后重试:${e}`, true, { recallMsg: e.isGroup ? 10 : 0 }) - } - } -} diff --git a/resources/content/content.css b/resources/content/content.css new file mode 100644 index 0000000..58d9263 --- /dev/null +++ b/resources/content/content.css @@ -0,0 +1,64 @@ +@font-face { + font-family: "tttgbnumber"; + src: url("../../../../../resources/font/tttgbnumber.ttf"); + font-weight: normal; + font-style: normal; +} +* { + margin: 0; + padding: 0; + box-sizing: border-box; + user-select: none; +} +body { + font-family: sans-serif; + font-size: 16px; + width: 800px; + color: #1e1f20; + transform: scale(1.5); + transform-origin: 0 0; +} +.container { + width: 800px; + padding: 20px 15px 10px 15px; + background-color: #f5f6fb; +} +.head_box { + border-radius: 15px; + font-family: tttgbnumber; + padding: 10px 20px; + position: relative; + box-shadow: 0 5px 10px 0 rgb(0 0 0 / 15%); +} +.sender { + font-size: 12px; + color: #4d4d4d; + margin-bottom: 5px; +} +.question { + background: #009FFF; /* fallback for old browsers */ + background: -webkit-linear-gradient(to right, #ec2F4B, #009FFF); /* Chrome 10-25, Safari 5.1-6 */ + background: linear-gradient(to right, #ec2F4B, #009FFF); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */ + color: #000000; + text-shadow: 0 0 10px white; + font-weight: bold; + border-radius: 5px; + padding: 8px 10px; + margin-bottom: 10px; +} + +.answer { + position: relative; + border-radius: 5px; + padding: 8px 10px; + background: #dbe9ff; + width: 100%; +} + +.logo { + margin-top: 10px; + font-size: 14px; + font-family: "tttgbnumber"; + text-align: center; + color: #7994a7; +} diff --git a/resources/content/index.html b/resources/content/index.html new file mode 100644 index 0000000..16b29e7 --- /dev/null +++ b/resources/content/index.html @@ -0,0 +1,31 @@ + + + + + + + + + +{{@headStyle}} + + +
+ + + +
+ {{question}} +
+
+
+ {{@ content}} +
+
+ +
+ + + + + diff --git a/resources/help/help.css b/resources/help/help.css new file mode 100644 index 0000000..d89db75 --- /dev/null +++ b/resources/help/help.css @@ -0,0 +1,135 @@ +@font-face { + font-family: "tttgbnumber"; + src: url("../../../../../resources/font/tttgbnumber.ttf"); + font-weight: normal; + font-style: normal; +} +* { + margin: 0; + padding: 0; + box-sizing: border-box; + user-select: none; +} +body { + font-family: sans-serif; + font-size: 16px; + width: 530px; + color: #1e1f20; + transform: scale(1.5); + transform-origin: 0 0; +} +.container { + width: 530px; + padding: 20px 15px 10px 15px; + background-color: #f5f6fb; +} +.head_box { + border-radius: 15px; + font-family: tttgbnumber; + padding: 10px 20px; + position: relative; + box-shadow: 0 5px 10px 0 rgb(0 0 0 / 15%); +} +.head_box .id_text { + font-size: 24px; +} +.head_box .day_text { + font-size: 20px; +} +.head_box .chatgpt_logo { + position: absolute; + top: 12px; + right: 15px; + width: 50px; +} +.base_info { + position: relative; + padding-left: 10px; +} +.uid { + font-family: tttgbnumber; +} + +.data_box { + border-radius: 15px; + margin-top: 20px; + margin-bottom: 15px; + padding: 20px 0px 5px 0px; + background: #fff; + box-shadow: 0 5px 10px 0 rgb(0 0 0 / 15%); + position: relative; +} +.tab_lable { + position: absolute; + top: -10px; + left: -8px; + background: #d4b98c; + color: #fff; + font-size: 14px; + padding: 3px 10px; + border-radius: 15px 0px 15px 15px; + z-index: 20; +} +.data_line { + display: flex; + justify-content: space-around; + margin-bottom: 14px; +} +.data_line_item { + width: 100px; + text-align: center; + /*margin: 0 20px;*/ +} +.num { + font-family: tttgbnumber; + font-size: 24px; +} +.data_box .lable { + font-size: 14px; + color: #7f858a; + line-height: 1; + margin-top: 3px; +} + +.list{ + display: flex; + justify-content: flex-start; + flex-wrap: wrap; +} + +.list .item { + width: 235px; + display: flex; + align-items: center; + background: #f1f1f1; + padding: 8px 6px 8px 6px; + border-radius: 8px; + margin: 0 0px 10px 10px; +} +.list .item .icon{ + width: 24px; + height: 24px; + background-repeat: no-repeat; + background-size: 100% 100%; + position: relative; + flex-shrink: 0; +} +.list .item .title{ + font-size: 16px; + margin-left: 6px; + line-height: 20px; +} +/* .list .item .title .text{ + white-space: nowrap; +} */ +.list .item .title .dec{ + font-size: 12px; + color: #999; + margin-top: 2px; +} +.logo { + font-size: 14px; + font-family: "tttgbnumber"; + text-align: center; + color: #7994a7; +} \ No newline at end of file diff --git a/resources/help/index.html b/resources/help/index.html new file mode 100644 index 0000000..c30fa5a --- /dev/null +++ b/resources/help/index.html @@ -0,0 +1,38 @@ + + + + + + + + +{{@headStyle}} + + +
+
+
ChatGPT-Plugin
+

使用说明

+ +
+ {{each helpData val}} +
+
{{val.group}}
+
+ {{each val.list item}} +
+ +
+
{{item.title}}
+
{{item.desc}}
+
+
+ {{/each}} +
+
+ {{/each}} + +
+ + + \ No newline at end of file diff --git a/resources/img/example1.png b/resources/img/example1.png new file mode 100644 index 0000000..42e07aa Binary files /dev/null and b/resources/img/example1.png differ diff --git a/resources/img/icon/chat-private.png b/resources/img/icon/chat-private.png new file mode 100644 index 0000000..d80d632 Binary files /dev/null and b/resources/img/icon/chat-private.png differ diff --git a/resources/img/icon/chat.png b/resources/img/icon/chat.png new file mode 100644 index 0000000..ca2c6e4 Binary files /dev/null and b/resources/img/icon/chat.png differ diff --git a/resources/img/icon/chatgpt.png b/resources/img/icon/chatgpt.png new file mode 100644 index 0000000..60dd9eb Binary files /dev/null and b/resources/img/icon/chatgpt.png differ diff --git a/resources/img/icon/destroy-other.png b/resources/img/icon/destroy-other.png new file mode 100644 index 0000000..b5167ce Binary files /dev/null and b/resources/img/icon/destroy-other.png differ diff --git a/resources/img/icon/destroy.png b/resources/img/icon/destroy.png new file mode 100644 index 0000000..6658263 Binary files /dev/null and b/resources/img/icon/destroy.png differ diff --git a/resources/img/icon/help.png b/resources/img/icon/help.png new file mode 100644 index 0000000..8d2105f Binary files /dev/null and b/resources/img/icon/help.png differ diff --git a/resources/img/icon/list.png b/resources/img/icon/list.png new file mode 100644 index 0000000..d7720f2 Binary files /dev/null and b/resources/img/icon/list.png differ diff --git a/resources/img/icon/picture.png b/resources/img/icon/picture.png new file mode 100644 index 0000000..18c9ef4 Binary files /dev/null and b/resources/img/icon/picture.png differ diff --git a/resources/img/icon/text.png b/resources/img/icon/text.png new file mode 100644 index 0000000..8deabf8 Binary files /dev/null and b/resources/img/icon/text.png differ diff --git a/utils/text.js b/utils/text.js new file mode 100644 index 0000000..6cd9dd3 --- /dev/null +++ b/utils/text.js @@ -0,0 +1,9 @@ + +/** + * 判断一段markdown文档中是否包含代码 + * @param text + */ +export function codeExists (text = '') { + let regex = /^[\s\S]*\$.*\$[\s\S]*/ + return regex.test(text) +}