feat: 修改配置文件格式避免频繁panic;降低api模式重试次数;优化帮助显示;支持必应新增的三种模式切换

This commit is contained in:
ikechan8370 2023-03-03 20:31:20 +08:00
parent a45ae14a5c
commit 873deda49d
10 changed files with 156 additions and 172 deletions

View file

@ -5,6 +5,7 @@
* 支持单人连续对话Conversation群聊中支持加入其他人的对话
* API模式下使用 gpt-3.5-turbo APIChatGPT官网同款模型仅需OpenAI Api Key开箱即用。**注意收费**
* 支持问答图片截图
* 支持AI性格调教
* API3模式下绕过Cloudflare防护直接访问ChatGPT的SSE API与官方体验一致且保留对话记录在官网可查。免费。
* 提供基于浏览器的解决方案作为备选API3不可用的情况下或担心账户安全的用户可以选择使用浏览器模式。
* 支持新[必应](https://www.bing.com/new)Beta
@ -24,7 +25,7 @@ Node.js >= 18 / Node.js >= 14(with node-fetch)
> #### API模式和浏览器模式如何选择
>
> * API模式会调用OpenAI官方提供的gpt-3.5-turbo APIChatGPT官网同款模型只需要提供API Key。一般情况下该种方式响应速度更快可配置项多且不会像chatGPT官网一样总出现不可用的现象但注意API调用是收费的新用户有18美元试用金可用于支付价格为`$0.0020/1K tokens`。(问题和回答**加起来**算token
> * API3模式会调用第三方提供的官网反代API他会帮你绕过CF防护需要提供ChatGPT的Token。效果与官网和浏览器一致但稳定性不一定。设置token和API2方法一样
> * API3模式会调用第三方提供的官网反代API他会帮你绕过CF防护需要提供ChatGPT的Token。效果与官网和浏览器一致但稳定性不一定。发送#chatgpt设置token来设置token
> * 浏览器模式通过在本地启动Chrome等浏览器模拟用户访问ChatGPT网站使得获得和官方以及API2模式一模一样的回复质量同时保证安全性。缺点是本方法对环境要求较高需要提供桌面环境和一个可用的代理能够访问ChatGPT的IP地址且响应速度不如API而且高峰期容易无法使用。一般作为API3的下位替代。
> * 必应Bing将调用微软新必应接口进行对话。需要在必应网页能够正常使用新必应且设置有效的Bing登录Cookie方可使用。
1. 进入 Yunzai根目录
@ -45,9 +46,10 @@ pnpm i
> 2.20更新必应被大削变得蠢了建议还是API/API3优先
3. 修改配置
**本插件配置项比较多,建议使用[锅巴面板](https://github.com/guoba-yunzai/Guoba-Plugin)修改**
复制`plugins/chatgpt-plugin/config/config.example.js`并将其改名为`config.js`
编辑`plugins/chatgpt-plugin/config/config.js`文件,根据其中的注释修改必要配置项
复制`plugins/chatgpt-plugin/config/config.example.json`并将其改名为`config.json`\
编辑`plugins/chatgpt-plugin/config/config.json`文件,修改必要配置项
4. 重启Yunzai-Bot

View file

@ -72,6 +72,10 @@ export class chatgpt extends plugin {
reg: '^#结束对话([sS]*)',
fnc: 'destroyConversations'
},
{
reg: '^#结束全部对话$',
fnc: 'endAllConversations'
},
// {
// reg: '#chatgpt帮助',
// fnc: 'help'
@ -179,9 +183,40 @@ export class chatgpt extends plugin {
}
}
async endAllConversations (e) {
let use = await redis.get('CHATGPT:USE') || 'api'
let deleted = 0
switch (use) {
case 'bing':
case 'api': {
let cs = await redis.keys('CHATGPT:CONVERSATIONS:*')
for (let i = 0; i < cs.length; i++) {
await redis.del(cs[i])
if (Config.debug) {
logger.info('delete conversation of qq: ' + cs[i])
}
deleted++
}
break
}
case 'api3': {
let qcs = await redis.keys('CHATGPT:CONVERSATIONS:*')
for (let i = 0; i < qcs.length; i++) {
await redis.del(qcs[i])
if (Config.debug) {
logger.info('delete conversation bind: ' + qcs[i])
}
deleted++
}
break
}
}
await this.reply(`结束了${deleted}个中用户的对话。`, true)
}
async deleteConversation (e) {
let ats = e.message.filter(m => m.type === 'at')
let use = await redis.get('CHATGPT:USE')
let use = await redis.get('CHATGPT:USE') || 'api'
if (use !== 'api3') {
await this.reply('本功能当前仅支持API3模式', true)
return false
@ -193,7 +228,7 @@ export class chatgpt extends plugin {
return false
} else {
let deleteResponse = await deleteConversation(conversationId, newFetch)
console.log(deleteResponse)
logger.mark(deleteResponse)
let deleted = 0
let qcs = await redis.keys('CHATGPT:QQ_CONVERSATION:*')
for (let i = 0; i < qcs.length; i++) {
@ -215,7 +250,9 @@ export class chatgpt extends plugin {
let conversationId = await redis.get('CHATGPT:QQ_CONVERSATION:' + qq)
if (conversationId) {
let deleteResponse = await deleteConversation(conversationId)
console.log(deleteResponse)
if (Config.debug) {
logger.mark(deleteResponse)
}
let deleted = 0
let qcs = await redis.keys('CHATGPT:QQ_CONVERSATION:*')
for (let i = 0; i < qcs.length; i++) {
@ -235,16 +272,6 @@ export class chatgpt extends plugin {
}
}
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) {
@ -331,15 +358,8 @@ export class chatgpt extends plugin {
await this.reply('主人不让我回答你这种问题,真是抱歉了呢', true)
return false
}
// if (prompt.indexOf('<script>') != -1)
// {
// await this.reply('坏人,我要报告给主人', e.isGroup)
// Bot.pickUser(cfg.masterQQ[0]).sendMsg(`主人,我在${this.e.group_id ? '群' + this.e.group_id : '私聊' }被${e.sender.nickname}使用代码攻击了,请警惕`)
// return false
// }
const use = await redis.get('CHATGPT:USE')
if (use !== 'bing') {
const use = await redis.get('CHATGPT:USE') || 'api'
if (use === 'api3') {
let randomId = uuid()
// 队列队尾插入,开始排队
await redis.rPush('CHATGPT:CHAT_QUEUE', [randomId])
@ -352,11 +372,11 @@ export class chatgpt extends plugin {
await this.reply('我正在思考如何回复你,请稍等', true, { recallMsg: 8 })
}
} else {
if (confirmOn) {
let length = await redis.lLen('CHATGPT:CHAT_QUEUE') - 1
if (confirmOn) {
await this.reply(`我正在思考如何回复你,请稍等,当前队列前方还有${length}个问题`, true, { recallMsg: 8 })
logger.info(`chatgpt队列前方还有${length}个问题。管理员可通过#清空队列来强制清除所有等待的问题。`)
}
logger.info(`chatgpt队列前方还有${length}个问题。管理员可通过#清空队列来强制清除所有等待的问题。`)
// 开始排队
while (true) {
if (await redis.lIndex('CHATGPT:CHAT_QUEUE', 0) === randomId) {
@ -573,48 +593,6 @@ export class chatgpt extends plugin {
case 'browser': {
return await this.chatgptBrowserBased(prompt, conversation)
}
case 'apiReverse': {
const currentDate = new Date().toISOString().split('T')[0]
let promptPrefix = `You are ${Config.assistantLabel}, a large language model trained by OpenAI. ${Config.promptPrefixOverride || defaultPropmtPrefix}
Current date: ${currentDate}`
const clientOptions = {
// (Optional) Support for a reverse proxy for the completions endpoint (private API server).
// Warning: This will expose your `openaiApiKey` to a third-party. Consider the risks before using this.
reverseProxyUrl: Config.reverseProxy || 'https://chatgpt.pawan.krd/api/completions',
// (Optional) Parameters as described in https://platform.openai.com/docs/api-reference/completions
modelOptions: {
// You can override the model name and any other parameters here.
model: Config.plus ? 'text-davinci-002-render-paid' : 'text-davinci-002-render'
},
// (Optional) Set custom instructions instead of "You are ChatGPT...".
promptPrefix,
// (Optional) Set a custom name for the user
// userLabel: 'User',
// (Optional) Set a custom name for ChatGPT
chatGptLabel: Config.assistantLabel,
// (Optional) Set to true to enable `console.debug()` logging
debug: Config.debug
}
const cacheOptions = {
// Options for the Keyv cache, see https://www.npmjs.com/package/keyv
// This is used for storing conversations, and supports additional drivers (conversations are stored in memory by default)
// For example, to use a JSON file (`npm i keyv-file`) as a database:
store: new KeyvFile({ filename: 'cache.json' })
}
let accessToken = await redis.get('CHATGPT:TOKEN')
if (!accessToken) {
throw new Error('未绑定ChatGPT AccessToken请使用#chatgpt设置token命令绑定token')
}
// console.log(accessToken)
this.chatGPTApi = new ChatGPTClient(accessToken, clientOptions, cacheOptions)
let response = await tryTimes(async () => await this.chatGPTApi.sendMessage(prompt, conversation || {}), 1)
return {
text: response.response,
conversationId: response.conversationId,
id: response.messageId,
parentMessageId: conversation?.parentMessageId
}
}
case 'bing': {
let bingToken = await redis.get('CHATGPT:BING_TOKEN')
if (!bingToken) {
@ -627,12 +605,15 @@ export class chatgpt extends plugin {
const bingAIClient = new BingAIClient({
userToken: bingToken, // "_U" cookie from bing.com
cookie,
debug: Config.debug
debug: Config.debug,
proxy: Config.proxy
})
let response
let reply = ''
try {
response = await bingAIClient.sendMessage(prompt, conversation || {}, (token) => {
let opt = _.cloneDeep(conversation) || {}
opt.toneStyle = Config.toneStyle
response = await bingAIClient.sendMessage(prompt, opt, (token) => {
reply += token
})
if (response.details.adaptiveCards?.[0]?.body?.[0]?.text?.trim()) {
@ -714,7 +695,7 @@ export class chatgpt extends plugin {
if (conversation) {
option = Object.assign(option, conversation)
}
return await tryTimes(async () => await this.chatGPTApi.sendMessage(prompt, option), 5)
return await tryTimes(async () => await this.chatGPTApi.sendMessage(prompt, option), 1)
}
}
}
@ -756,15 +737,15 @@ export class chatgpt extends plugin {
async joinConversation (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
}
let use = await redis.get('CHATGPT:USE') || 'api'
// if (use !== 'api3') {
// await this.reply('本功能当前仅支持API3模式', true)
// return false
// }
if (ats.length === 0) {
await this.reply('指令错误,使用本指令时请同时@某人', true)
return false
} else {
} else if (use === 'api3') {
let at = ats[0]
let qq = at.qq
let atUser = _.trimStart(at.text, '@')
@ -775,6 +756,13 @@ export class chatgpt extends plugin {
}
await redis.set(`CHATGPT:QQ_CONVERSATION:${e.sender.user_id}`, conversationId)
await this.reply(`加入${atUser}的对话成功当前对话id为` + conversationId)
} else {
let at = ats[0]
let qq = at.qq
let atUser = _.trimStart(at.text, '@')
let target = await redis.get('CHATGPT:CONVERSATIONS:' + qq)
await redis.set('CHATGPT:CONVERSATIONS:' + e.sender.user_id, target)
await this.reply(`加入${atUser}的对话成功`)
}
}

View file

@ -66,6 +66,11 @@ let helpData = [
title: '#结束对话',
desc: '结束自己当前对话,下次开启对话机器人将遗忘掉本次对话内容。'
},
{
icon: 'destroy',
title: '#结束全部对话',
desc: '结束正在与本机器人进行对话的全部用户的对话。'
},
{
icon: 'destroy-other',
title: '#结束对话 @某人',

View file

@ -54,7 +54,7 @@ export class ChatgptManagement extends plugin {
},
{
reg: '^#chatgpt切换(必应|Bing)$',
fnc: 'useReversedBingSolution',
fnc: 'useBingSolution',
permission: 'master'
},
{
@ -157,8 +157,9 @@ export class ChatgptManagement extends plugin {
}
async useReversedAPIBasedSolution (e) {
await redis.set('CHATGPT:USE', 'apiReverse')
await this.reply('【暂时不可用请关注仓库更新和群公告】已切换到基于第三方Reversed CompletionAPI的解决方案如果已经对话过建议执行`#结束对话`避免引起404错误')
await this.reply('API2已废弃处于不可用状态不会为你切换')
// await redis.set('CHATGPT:USE', 'apiReverse')
// await this.reply('【暂时不可用请关注仓库更新和群公告】已切换到基于第三方Reversed CompletionAPI的解决方案如果已经对话过建议执行`#结束对话`避免引起404错误')
}
async useReversedAPIBasedSolution2 (e) {
@ -171,7 +172,7 @@ export class ChatgptManagement extends plugin {
}
}
async useReversedBingSolution (e) {
async useBingSolution (e) {
let use = await redis.get('CHATGPT:USE')
if (use !== 'bing') {
await redis.set('CHATGPT:USE', 'bing')

View file

@ -1,84 +0,0 @@
// 将当前文件复制为config.js使得配配置生效
// 例如http://127.0.0.1:7890
const PROXY = ''
const API_KEY = ''
export default {
// ***********************************************************************************************************************************
// 通用配置 *
// ***********************************************************************************************************************************
// 如果回答包括屏蔽词,就不返回。
// blockWords: ['屏蔽词1', '屏蔽词b'],
// 问题中如果触发屏蔽词,也会拒绝回答
// promptBlockWords: ['屏蔽词1', '屏蔽词b'],
// imgOcr: true,
// 改为true后全局默认以图片形式回复并自动发出Continue命令补全回答。长回复可能会有bug。
// defaultUsePicture: false,
// 如果true字数大于阈值autoUsePictureThreshold会自动用图片发送即使是文本模式。
// autoUsePicture: true,
// 仅当autoUsePicture为true时生效字数大于该阈值会自动用图片发送即使是文本模式。
// autoUsePictureThreshold: 1200,
// 每个人发起的对话保留时长。超过这个时长没有进行对话,再进行对话将开启新的对话。单位:秒
// conversationPreserveTime: 0,
// 触发方式 可选值at 或 prefix 。at模式下只有at机器人才会回复。prefix模式下不需要at但需要添加前缀#chat
// toggleMode: 'at',
// 是否在回复图片时引用原始消息
// quoteReply: true,
// 是否在图片模式中启用二维码。该对话内容将被发送至第三方服务器以进行渲染展示。改为false关闭该功能
// showQRCode: true,
// 图片内容渲染服务器API地址
// cacheUrl: 'https://content.alcedogroup.com',
// 图片内容渲染服务器开启预制访问代码,当渲染服务器访问较慢时可以开启,但无法保证远程渲染是否成功
// cacheEntry: false,
// 绘图指令的CD时间主人不受限制
// drawCD: 30,
// ***********************************************************************************************************************************
// 以下为API方式(默认)的配置 *
// ***********************************************************************************************************************************
apiKey: API_KEY,
// 如果有openai api的加速反代可以写
// openAiBaseUrl: null,
// 模型名称选填。如无特殊需求保持默认即可会使用chatgpt-api库提供的当前可用的最适合的默认值。保底可用的是 text-davinci-003。当发现新的可用的chatGPT模型会更新这里的值
// 20230211 text-chat-davinci-002-sh-alpha-aoruigiofdj83 中午存活了几分钟
// model: '',
// temperature: 0.8,
// ***********************************************************************************************************************************
// 以下为API3方式的配置 *
// ***********************************************************************************************************************************
// from https://github.com/acheong08/ChatGPT
// 或者: https://gpt.pawan.krd/backend-api/conversation
// api: 'https://chatgpt.duti.tech/api/conversation',
// 或者 https://gpt.pawan.krd/backend-api
// apiBaseUrl: 'https://chatgpt.duti.tech/api',
// ***********************************************************************************************************************************
// 以下为API/API2方式公用的配置 *
// ***********************************************************************************************************************************
// 给模型的前言promt。选填。默认完整值`You are ${this._assistantLabel}, a large language model trained by OpenAI. You answer as concisely as possible for each response (e.g. dont 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. Current date: ${currentDate}\n\n
// 此项配置会覆盖掉中间部分。保持为空将使用网友从对话中推测出的指令。
// 你可以在这里写入你希望AI回答的风格比如希望优先回答中文回答长一点等
// promptPrefixOverride: 'Your answer shouldn\'t be too verbose. If you are generating a list, do not have too many items. Keep the number of items short. Prefer to answer in Chinese.',
// AI认为的自己的名字当你问他你是谁是他会回答这里的名字。
// assistantLabel: 'ChatGPT',
// 是否开启AI正在思考中的提示信息
// thinkingTips: true,
// ***********************************************************************************************************************************
// 以下为浏览器方式的配置 *
// ***********************************************************************************************************************************
// username: '',
// password: '',
// UA: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36',
// 服务器无interface的话只能用true但是可能遇到验证码就一定要配置下面的2captchaToken了
// true时使用无头模式无界面的服务器可以为true但遇到验证码时可能无法使用。(实测很容易卡住,几乎不可用)
// headless: false,
// 为空使用默认puppeteer的chromium也可以传递自己本机安装的Chrome可执行文件地址提高通过率。windows可以是C:\\Program Files\\Google\\Chrome\\Application\\chrome.exelinux通过which查找路径
// chromePath: '',
// 可注册2captcha实现跳过验证码收费服务但很便宜。否则可能会遇到验证码而卡住。
// '2captchaToken': '',
// http或socks5代理
proxy: PROXY,
// debug: true,
// 各个地方的默认超时时间
// defaultTimeoutMs: 120000,
// 浏览器默认超时,浏览器可能需要更高的超时时间
// chromeTimeoutMS: 120000
}

View file

@ -0,0 +1,21 @@
{
"apiKey": "",
"proxy": "",
"openAiBaseUrl": "https://api.openai.com",
"api": "https://chatgpt.duti.tech/api/conversation",
"apiBaseUrl": "https://chatgpt.duti.tech/api",
"promptPrefixOverride": "Your answer shouldn't be too verbose. If you are generating a list, do not have too many items. Keep the number of items short. Prefer to answer in Chinese.",
"debug": false,
"blockWords": ["屏蔽词1", "屏蔽词b"],
"promptBlockWords": ["屏蔽词1", "屏蔽词b"],
"imgOcr": true,
"defaultUsePicture": false,
"autoUsePicture": true,
"autoUsePictureThreshold": 1200,
"toggleMode": "at",
"quoteReply": true,
"showQRCode": true,
"cacheUrl": "https://content.alcedogroup.com",
"cacheEntry": false,
"drawCD": 30
}

View file

@ -262,6 +262,19 @@ export function supportGuoba () {
label: '验证码平台Token',
bottomHelpMessage: '可注册2captcha实现跳过验证码收费服务但很便宜。否则可能会遇到验证码而卡住。',
component: 'InputPassword'
},
{
field: 'toneStyle',
label: 'Bing模式',
bottomHelpMessage: '微软必应官方的三种应答风格。默认为均衡',
component: 'Select',
componentProps: {
options: [
{ label: '均衡', value: 'balanced' },
{ label: '创意', value: 'creative' },
{ label: '精确', value: 'precise' }
]
}
}
],
// 获取配置数据方法(用于前端填充显示数据)

View file

@ -3,7 +3,7 @@
"type": "module",
"author": "ikechan8370",
"dependencies": {
"@waylaidwanderer/chatgpt-api": "^1.23.0",
"@waylaidwanderer/chatgpt-api": "^1.24.0",
"chatgpt": "^5.0.5",
"delay": "^5.0.0",
"keyv-file": "^0.2.0",

View file

@ -19,7 +19,7 @@ body {
transform-origin: 0 0;
}
.container {
width: 630px;
width: 930px;
padding: 20px 15px 10px 15px;
background-color: #f5f6fb;
}

View file

@ -1,5 +1,7 @@
import fs from 'fs'
import lodash from 'lodash'
import { execSync } from 'child_process'
const defaultConfig = {
blockWords: ['屏蔽词1', '屏蔽词b'],
promptBlockWords: ['屏蔽词1', '屏蔽词b'],
@ -34,19 +36,55 @@ const defaultConfig = {
debug: true,
defaultTimeoutMs: 120000,
chromeTimeoutMS: 120000,
version: '2.0.10'
// version: 'v2.0.11',
toneStyle: 'balanced' // or creative, precise
}
const _path = process.cwd()
let config = {}
if (fs.existsSync(`${_path}/plugins/chatgpt-plugin/config/config.js`)) {
if (fs.existsSync(`${_path}/plugins/chatgpt-plugin/config/config.json`)) {
const fullPath = fs.realpathSync(`${_path}/plugins/chatgpt-plugin/config/config.json`)
const data = fs.readFileSync(fullPath)
if (data) {
try {
config = JSON.parse(data)
} catch (e) {
logger.error('chatgpt插件读取配置文件出错请检查config/config.json格式将忽略用户配置转为使用默认配置', e)
logger.warn('chatgpt插件即将使用默认配置')
}
}
} else if (fs.existsSync(`${_path}/plugins/chatgpt-plugin/config/config.js`)) {
// 旧版本的config.js读取其内容生成config.json然后删掉config.js
const fullPath = fs.realpathSync(`${_path}/plugins/chatgpt-plugin/config/config.js`)
config = (await import(`file://${fullPath}`)).default
try {
logger.warn('[ChatGPT-Plugin]发现旧版本config.js文件正在读取其内容并转换为新版本config.json文件')
// 读取其内容生成config.json
fs.writeFileSync(`${_path}/plugins/chatgpt-plugin/config/config.json`, JSON.stringify(config, null, 2))
// 删掉config.js
fs.unlinkSync(`${_path}/plugins/chatgpt-plugin/config/config.js`)
logger.info('[ChatGPT-Plugin]配置文件转换处理完成')
} catch (err) {
logger.error('[ChatGPT-Plugin]转换旧版配置文件失败建议手动清理旧版config.js文件并转为使用新版config.json格式', err)
}
} else if (fs.existsSync(`${_path}/plugins/chatgpt-plugin/config/index.js`)) {
// 兼容旧版本
const fullPath = fs.realpathSync(`${_path}/plugins/chatgpt-plugin/config/index.js`)
config = (await import(`file://${fullPath}`)).Config
try {
logger.warn('[ChatGPT-Plugin]发现旧版本config.js文件正在读取其内容并转换为新版本config.json文件')
// 读取其内容生成config.json
fs.writeFileSync(`${_path}/plugins/chatgpt-plugin/config/config.json`, JSON.stringify(config, null, 2))
// index.js
fs.unlinkSync(`${_path}/plugins/chatgpt-plugin/config/index.js`)
logger.info('[ChatGPT-Plugin]配置文件转换处理完成')
} catch (err) {
logger.error('[ChatGPT-Plugin]转换旧版配置文件失败建议手动清理旧版index.js文件并转为使用新版config.json格式', err)
}
}
config = Object.assign({}, defaultConfig, config)
const latestTag = execSync('git describe --tags --abbrev=0').toString().trim()
config.version = latestTag
export const Config = new Proxy(config, {
set (target, property, value) {
target[property] = value
@ -56,9 +94,9 @@ export const Config = new Proxy(config, {
}
})
try {
fs.writeFileSync(`${_path}/plugins/chatgpt-plugin/config/config.js`, `export default ${JSON.stringify(change, '', '\t')}`, { flag: 'w' })
fs.writeFileSync(`${_path}/plugins/chatgpt-plugin/config/config.json`, JSON.stringify(change, null, 2), { flag: 'w' })
} catch (err) {
console.error(err)
logger.error(err)
return false
}
return true