chatgpt-plugin/apps/chat.js
2022-12-19 12:36:56 +08:00

296 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import plugin from '../../../lib/plugins/plugin.js'
import { ChatGPTAPIBrowser } from 'chatgpt'
import _ from 'lodash'
import { Config } from '../config/index.js'
import showdown from 'showdown'
import mjAPI from 'mathjax-node'
import { ChatGPTPuppeteer } from '../utils/browser.js'
// import puppeteer from '../utils/browser.js'
// import showdownKatex from 'showdown-katex'
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'
}
]
})
}
/**
* 获取chatgpt当前对话列表
* @param e
* @returns {Promise<void>}
*/
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<void>}
*/
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 api = await redis.get('CHATGPT:API_OPTION')
if (!api) {
let option = { markdown: true }
if (Config['2captchaToken']) {
option.captchaToken = Config['2captchaToken']
}
// option.debug = true
option.email = Config.username
option.password = Config.password
this.chatGPTApi = new ChatGPTPuppeteer(option)
await redis.set('CHATGPT:API_OPTION', JSON.stringify(option))
} else {
let option = JSON.parse(api)
this.chatGPTApi = new ChatGPTPuppeteer(option)
}
let question = e.msg.trimStart()
await this.reply('我正在思考如何回复你,请稍等', true, { recallMsg: 5 })
logger.info(`chatgpt question: ${question}`)
// try {
// await this.chatGPTApi.init()
// } catch (e) {
// await this.reply('chatgpt初始化出错' + e.msg, true)
// }
let previousConversation = await redis.get(`CHATGPT:CONVERSATIONS:${e.sender.user_id}`)
let conversation = null
if (!previousConversation) {
let ctime = new Date()
previousConversation = {
sender: e.sender,
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)
conversation = {
conversationId: previousConversation.conversation.conversationId,
parentMessageId: previousConversation.conversation.parentMessageId
}
}
try {
let option = {
onConversationResponse: function (c) {
previousConversation.conversation = {
conversationId: c.conversation_id,
parentMessageId: c.message.id
}
redis.set(`CHATGPT:CONVERSATIONS:${e.sender.user_id}`, JSON.stringify(previousConversation), { EX: CONVERSATION_PRESERVE_TIME }).then(res => {
logger.debug('redis set conversation')
})
}
}
if (conversation) {
option = Object.assign(option, conversation)
}
let response = await this.chatGPTApi.sendMessage(question, option)
// 检索是否有屏蔽词
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) {
let endTokens = ['.', '。', '……', '!', '', ']', ')', '', '】', '?', '', '~', '"', "'"]
while (!endTokens.find(token => response.trimEnd().endsWith(token))) {
// while (!response.trimEnd().endsWith('.') && !response.trimEnd().endsWith('。') && !response.trimEnd().endsWith('……') &&
// !response.trimEnd().endsWith('') && !response.trimEnd().endsWith('!') && !response.trimEnd().endsWith(']') && !response.trimEnd().endsWith('】')
// ) {
await this.reply('内容有点多,我正在奋笔疾书,请再等一会', true, { recallMsg: 5 })
option = {
onConversationResponse: function (c) {
previousConversation.conversation = {
conversationId: c.conversation_id,
parentMessageId: c.message.id
}
redis.set(`CHATGPT:CONVERSATIONS:${e.sender.user_id}`, JSON.stringify(previousConversation), { EX: CONVERSATION_PRESERVE_TIME }).then(res => {
logger.debug('redis set conversation')
})
}
}
option = Object.assign(option, previousConversation.conversation)
const responseAppend = await this.chatGPTApi.sendMessage('Continue', option)
// console.log(responseAppend)
// 检索是否有屏蔽词
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 forget what it had said')
break
}
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 })
}
}
}