feat: add picture mode
27
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 介绍一下米哈游
|
||||

|
||||
(图片仅供参考,chatgpt在某些领域依然是人工智障,但语言起码流畅自信多了)
|
||||
|
||||
比如让他写代码
|
||||

|
||||
|
||||
比如让他写剧本
|
||||
<img width="835" alt="image" src="https://user-images.githubusercontent.com/21212372/206387421-db00728d-1869-40f3-bde4-0dd6a4b67913.png">
|
||||
|
||||
### 群聊使用艾特(@)的方式
|
||||
如果你的机器人插件少不担心冲突问题的话,将 `index.js` 重命名为 `index.js.bak`,将 `index_no#.js` 重命名为 `index.js`,此时将基于艾特模式进行聊天。
|
||||
|
||||
此时只需在群聊中@机器人+聊天内容即可。
|
||||

|
||||
|
||||
同时,此模式下私聊直接打字聊天即可,也无需加#chatgpt前缀。
|
||||

|
||||
|
||||
### 基本使用
|
||||
@机器人 发送聊内容即可
|
||||

|
||||
发挥你的想象力吧!
|
||||
|
||||
### 获取帮助
|
||||
发送#chatgpt帮助
|
||||
|
||||
## 关于openai token获取
|
||||
1. 注册openai账号
|
||||
进入https://chat.openai.com/ ,选择signup注册。目前openai不对包括俄罗斯、乌克兰、伊朗、中国等国家和地区提供服务,所以自行寻找办法使用其他国家和地区的ip登录。此外,注册可能需要验证所在国家和地区的手机号码,如果没有国外手机号可以试试解码网站,收费的推荐https://sms-activate.org/。
|
||||
|
|
|
|||
281
apps/chat.js
Normal file
|
|
@ -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<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 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 })
|
||||
}
|
||||
}
|
||||
}
|
||||
76
apps/help.js
Normal file
|
|
@ -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 })
|
||||
}
|
||||
}
|
||||
5
config/index.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
const SESSION_TOKEN = ''
|
||||
|
||||
export const Config = {
|
||||
token: SESSION_TOKEN
|
||||
}
|
||||
194
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'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取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('已结束当前对话,请使用#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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
/**
|
||||
* #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 })
|
||||
}
|
||||
const files = fs.readdirSync('./plugins/chatgpt-plugin/apps').filter(file => file.endsWith('.js'))
|
||||
|
||||
let ret = []
|
||||
|
||||
files.forEach((file) => {
|
||||
ret.push(import(`./apps/${file}`))
|
||||
})
|
||||
|
||||
ret = await Promise.allSettled(ret)
|
||||
|
||||
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 }
|
||||
|
|
|
|||
182
index_no#.js
|
|
@ -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<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)
|
||||
}
|
||||
|
||||
/**
|
||||
* #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 })
|
||||
}
|
||||
}
|
||||
}
|
||||
64
resources/content/content.css
Normal file
|
|
@ -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;
|
||||
}
|
||||
31
resources/content/index.html
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text/html;charset=utf-8" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="{{pluResPath}}content/content.css" />
|
||||
<link rel="shortcut icon" href="#" />
|
||||
</head>
|
||||
{{@headStyle}}
|
||||
|
||||
<body style="display:none;" onload="document.body.style.display='block'">
|
||||
<div class="container" id="container">
|
||||
<!-- <div class="sender">-->
|
||||
<!-- {{senderName}}-->
|
||||
<!-- </div>-->
|
||||
<div class="question">
|
||||
{{question}}
|
||||
</div>
|
||||
<div class="answer">
|
||||
<div id="content">
|
||||
{{@ content}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="logo">Response to {{senderName}} Created By Yunzai-Bot and ChatGPT-Plugin</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
|
||||
135
resources/help/help.css
Normal file
|
|
@ -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;
|
||||
}
|
||||
38
resources/help/index.html
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text/html;charset=utf-8" />
|
||||
<link rel="stylesheet" type="text/css" href="{{pluResPath}}help/help.css" />
|
||||
<link rel="shortcut icon" href="#" />
|
||||
</head>
|
||||
{{@headStyle}}
|
||||
|
||||
<body>
|
||||
<div class="container" id="container">
|
||||
<div class="head_box">
|
||||
<div class="id_text">ChatGPT-Plugin</div>
|
||||
<h2 class="day_text">使用说明</h2>
|
||||
<img class="chatgpt_logo" src="{{pluResPath}}img/icon/chatgpt.png"/>
|
||||
</div>
|
||||
{{each helpData val}}
|
||||
<div class="data_box">
|
||||
<div class="tab_lable">{{val.group}}</div>
|
||||
<div class="list">
|
||||
{{each val.list item}}
|
||||
<div class="item">
|
||||
<img class="icon" src="{{pluResPath}}img/icon/{{item.icon}}.png" />
|
||||
<div class="title">
|
||||
<div class="text">{{item.title}}</div>
|
||||
<div class="dec">{{item.desc}}</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
<div class="logo">Created By Yunzai-Bot and ChatGPT-Plugin</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
BIN
resources/img/example1.png
Normal file
|
After Width: | Height: | Size: 228 KiB |
BIN
resources/img/icon/chat-private.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
resources/img/icon/chat.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
resources/img/icon/chatgpt.png
Normal file
|
After Width: | Height: | Size: 189 KiB |
BIN
resources/img/icon/destroy-other.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
resources/img/icon/destroy.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
resources/img/icon/help.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
resources/img/icon/list.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
resources/img/icon/picture.png
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
resources/img/icon/text.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
9
utils/text.js
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
|
||||
/**
|
||||
* 判断一段markdown文档中是否包含代码
|
||||
* @param text
|
||||
*/
|
||||
export function codeExists (text = '') {
|
||||
let regex = /^[\s\S]*\$.*\$[\s\S]*/
|
||||
return regex.test(text)
|
||||
}
|
||||