From 7192a8c6fac0d403649c21f00341300eb3485779 Mon Sep 17 00:00:00 2001 From: HalcyonAlcedo <41666148+HalcyonAlcedo@users.noreply.github.com> Date: Thu, 23 Feb 2023 18:47:35 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=9B=BE=E7=89=87=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F=E5=8F=91=E9=80=81=E5=8A=9F=E8=83=BD=EF=BC=8C=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E5=AF=B9=E8=AF=9D=E9=98=9F=E5=88=97=E8=B6=85=E6=97=B6?= =?UTF-8?q?=E6=A3=80=E6=B5=8B=20(#188)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 修复引用转发,默认bing模式并发 * 开启stream增加稳定性 * fix: remove queue element only in non-bing mode * 使用chatgpt-api自带的超时逻辑,文字过多时启动切换到图片输出防止被吞 * Update chat.js * 添加Bing专用的图片输出样式 * 添加chatgpt的新图片模式,临时处理切换api导致的对话异常 * 修改bing样式表 * 为图片添加外部页面缓存 * 为图片模式添加MathJax * feat: add switch for qrcode * 防止script攻击 * 修复网页模板错误 * 修复bing页面引用错误 * 缓存服务器异常时处理 * 添加默认配置加载 * 修复配置文件路径错误 * 删除重复的模板文件,修复二维码地址错误 * 修正图片渲染错误 * 修复引用渲染错误 * 二维码网址统一改为使用本地配置 * 添加关闭思考提示的配置项 * 修复在Windows上无法载入配置文件的问题 * 修复关闭qr的情况下渲染错误 * 改为使用base64传递返回数据 * 当异常过多时使用图片输出 * 添加锅巴面板配置支持 * 补充遗漏的默认配置 * 修复qr模式下引用未被传递的问题 * 修复未将引用数据传输给缓存服务器的问题 * 删除无用的bingTimeoutMs配置项 * 添加消息队列超时弹出 * 优化图片模式处理,解决对话队列卡住的问题 --------- Co-authored-by: ikechan8370 --- apps/chat.js | 131 ++++++++++++++++++++------------------- config/config.example.js | 4 +- guoba.support.js | 38 +++++++++--- utils/common.js | 9 +++ utils/config.js | 1 + 5 files changed, 107 insertions(+), 76 deletions(-) diff --git a/apps/chat.js b/apps/chat.js index 11f550d..4b55344 100644 --- a/apps/chat.js +++ b/apps/chat.js @@ -5,7 +5,7 @@ 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 } from '../utils/common.js' +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' @@ -288,6 +288,8 @@ export class chatgpt extends plugin { let confirm = await redis.get('CHATGPT:CONFIRM') let confirmOn = (!confirm || confirm === 'on') && Config.thinkingTips if (await redis.lIndex('CHATGPT:CHAT_QUEUE', 0) === randomId) { + // 添加超时设置 + await redis.pSetEx("CHATGPT:CHAT_QUEUE_TIMEOUT", Config.defaultTimeoutMs, randomId); if (confirmOn) { await this.reply('我正在思考如何回复你,请稍等', true, { recallMsg: 8 }) } @@ -300,8 +302,19 @@ export class chatgpt extends plugin { // 开始排队 while (true) { if (await redis.lIndex('CHATGPT:CHAT_QUEUE', 0) === randomId) { + await redis.pSetEx("CHATGPT:CHAT_QUEUE_TIMEOUT", Config.defaultTimeoutMs, randomId); break } else { + // 超时检查 + if (await redis.exists("CHATGPT:CHAT_QUEUE_TIMEOUT") === 0) { + await redis.lPop('CHATGPT:CHAT_QUEUE', 0) + await redis.pSetEx("CHATGPT:CHAT_QUEUE_TIMEOUT", Config.defaultTimeoutMs, await redis.lIndex('CHATGPT:CHAT_QUEUE', 0)); + if (confirmOn) { + let length = await redis.lLen('CHATGPT:CHAT_QUEUE') - 1 + await this.reply(`问题想不明白放弃了,开始思考下一个问题,当前队列前方还有${length}个问题`, true, { recallMsg: 8 }) + logger.info(`问题超时已弹出,chatgpt队列前方还有${length}个问题。管理员可通过#清空队列来强制清除所有等待的问题。`) + } + } await delay(1500) } } @@ -403,37 +416,12 @@ export class chatgpt extends plugin { } if (userSetting.usePicture) { // todo use next api of chatgpt to complete incomplete respoonse - response = new Buffer.from(response).toString('base64') - if (Config.showQRCode) { - try { - let cacheres = await fetch(`${Config.cacheUrl}/cache`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - content: { - content: response, - prompt, - senderName: e.sender.nickname - // quote: quotemessage - }, - bing: use === 'bing' - }) - } - ) - let cache = { file: '', cacheUrl: Config.cacheUrl } - if (cacheres.ok) { - cache = Object.assign({}, cache, await cacheres.json()) - } - await e.runtime.render('chatgpt-plugin', use !== 'bing' ? 'content/ChatGPT/index' : 'content/Bing/index', { content: response, prompt: escapeHtml(prompt), senderName: e.sender.nickname, cache }) - } catch (err) { - logger.warn('error happened while uploading content to the cache server. QR Code will not be showed in this picture.') - logger.error(err) - await e.runtime.render('chatgpt-plugin', use !== 'bing' ? 'content/ChatGPT/index' : 'content/Bing/index', { content: response, prompt: escapeHtml(prompt), senderName: e.sender.nickname, cache: { file: '', cacheUrl: Config.cacheUrl } }) - } - } else { - await e.runtime.render('chatgpt-plugin', use !== 'bing' ? 'content/ChatGPT/index' : 'content/Bing/index', { content: response, prompt: escapeHtml(prompt), senderName: e.sender.nickname, cache: { file: '', cacheUrl: Config.cacheUrl } }) + try { + await this.renderImage(e, use !== 'bing' ? 'content/ChatGPT/index' : 'content/Bing/index', response, prompt, [], Config.showQRCode) + } catch (err) { + logger.warn('error happened while uploading content to the cache server. QR Code will not be showed in this picture.') + logger.error(err) + await this.renderImage(e, use !== 'bing' ? 'content/ChatGPT/index' : 'content/Bing/index', response, prompt) } } else { let quotemessage = [] @@ -445,38 +433,13 @@ export class chatgpt extends plugin { }) } if (Config.autoUsePicture && response.length > Config.autoUsePictureThreshold) { - response = new Buffer.from(response).toString('base64') // 文字过多时自动切换到图片模式输出 - if (Config.showQRCode) { - try { - let cacheres = await fetch(`${Config.cacheUrl}/cache`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - content: { - content: response, - prompt, - senderName: e.sender.nickname - // quote: quotemessage - }, - bing: use === 'bing' - }) - } - ) - let cache = { file: '', cacheUrl: Config.cacheUrl } - if (cacheres.ok) { - cache = Object.assign({}, cache, await cacheres.json()) - } - await e.runtime.render('chatgpt-plugin', use !== 'bing' ? 'content/ChatGPT/index' : 'content/Bing/index', { content: response, prompt: escapeHtml(prompt), senderName: e.sender.nickname, quote: quotemessage.length > 0, quotes: quotemessage, cache }) - } catch (err) { - logger.warn('error happened while uploading content to the cache server. QR Code will not be showed in this picture.') - logger.error(err) - await e.runtime.render('chatgpt-plugin', use !== 'bing' ? 'content/ChatGPT/index' : 'content/Bing/index', { content: response, prompt: escapeHtml(prompt), senderName: e.sender.nickname, cache: { file: '', cacheUrl: Config.cacheUrl } }) - } - } else { - await e.runtime.render('chatgpt-plugin', use !== 'bing' ? 'content/ChatGPT/index' : 'content/Bing/index', { content: response, prompt: escapeHtml(prompt), senderName: e.sender.nickname, quote: quotemessage.length > 0, quotes: quotemessage, cache: { file: '', cacheUrl: Config.cacheUrl } }) + try { + await this.renderImage(e, use !== 'bing' ? 'content/ChatGPT/index' : 'content/Bing/index', response, prompt, quotemessage, Config.showQRCode) + } catch (err) { + logger.warn('error happened while uploading content to the cache server. QR Code will not be showed in this picture.') + logger.error(err) + await this.renderImage(e, use !== 'bing' ? 'content/ChatGPT/index' : 'content/Bing/index', response, prompt) } } else { await this.reply(`${response}`, e.isGroup) @@ -503,12 +466,52 @@ export class chatgpt extends plugin { await this.reply(`通信异常,请稍后重试:${err}`, true, { recallMsg: e.isGroup ? 10 : 0 }) } else { //这里是否还需要上传到缓存服务器呐?多半是代理服务器的问题,本地也修不了,应该不用吧。 - await e.runtime.render('chatgpt-plugin', use !== 'bing' ? 'content/ChatGPT/index' : 'content/Bing/index', { content: new Buffer.from(`通信异常,错误信息如下 ${err}`).toString("base64"), prompt: escapeHtml(prompt), senderName: e.sender.nickname, quote: false , quotes: [], cache: {file:'',cacheUrl:Config.cacheUrl} }) + await this.renderImage(e, use !== 'bing' ? 'content/ChatGPT/index' : 'content/Bing/index', `通信异常,错误信息如下 ${err}`, prompt) } } } } + async renderImage (e, template, content, prompt, quote = [], cache = false) { + let cacheData = { file: '', cacheUrl: Config.cacheUrl } + if (cache) { + if (Config.cacheEntry) cacheData.file = randomString() + const use = await redis.get('CHATGPT:USE') + const cacheresOption = { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + content: { + content: new Buffer.from(content).toString('base64'), + prompt, + senderName: e.sender.nickname, + quote: quote + }, + bing: use === 'bing', + entry: Config.cacheEntry ? cacheData.file : '' + }) + } + if (Config.cacheEntry) { + fetch(`${Config.cacheUrl}/cache`, cacheresOption) + } else { + const cacheres = await fetch(`${Config.cacheUrl}/cache`, cacheresOption) + if (cacheres.ok) { + cacheData = Object.assign({}, cacheData, await cacheres.json()) + } + } + } + await e.runtime.render('chatgpt-plugin', template, { + content: new Buffer.from(content).toString("base64"), + prompt: escapeHtml(prompt), + senderName: e.sender.nickname, + quote: quote.length > 0, + quotes: quote, + cache: cacheData + }) + } + async sendMessage (prompt, conversation = {}, use, e) { if (!conversation) { conversation = { diff --git a/config/config.example.js b/config/config.example.js index 5aeec18..ef485de 100644 --- a/config/config.example.js +++ b/config/config.example.js @@ -25,6 +25,8 @@ export default { // showQRCode: true, // 图片内容渲染服务器API地址 // cacheUrl: 'https://content.alcedogroup.com', + // 图片内容渲染服务器开启预制访问代码,当渲染服务器访问较慢时可以开启,但无法保证远程渲染是否成功 + // cacheEntry: false, // *********************************************************************************************************************************** // 以下为API方式(默认)的配置 * // *********************************************************************************************************************************** @@ -79,8 +81,6 @@ export default { // debug: true, // 各个地方的默认超时时间 // defaultTimeoutMs: 120000, - // bing默认超时时间,bing太慢了有的时候 - // bingTimeoutMs: 360000, // 浏览器默认超时,浏览器可能需要更高的超时时间 // chromeTimeoutMS: 120000 } \ No newline at end of file diff --git a/guoba.support.js b/guoba.support.js index ff6acb2..b7932a1 100644 --- a/guoba.support.js +++ b/guoba.support.js @@ -81,6 +81,12 @@ export function supportGuoba() { bottomHelpMessage: '用于缓存图片模式会话内容并渲染的服务器地址。', component: 'Input', }, + { + field: 'cacheEntry', + label: '预制渲染服务器访问代码', + bottomHelpMessage: '图片内容渲染服务器开启预制访问代码,当渲染服务器访问较慢时可以开启,但无法保证访问代码可以正常访问页面。', + component: 'Switch', + }, { field: 'proxy', label: '代理服务器地址', @@ -107,16 +113,6 @@ export function supportGuoba() { min: 0, }, }, - { - field: 'bingTimeoutMs', - label: 'Bing超时时间', - helpMessage: '单位:毫秒', - bottomHelpMessage: 'bing默认超时时间,bing太慢了有的时候。', - component: 'InputNumber', - componentProps: { - min: 0, - }, - }, { field: 'chromeTimeoutMS', label: '浏览器超时时间', @@ -143,6 +139,12 @@ export function supportGuoba() { bottomHelpMessage: '模型名称,如无特殊需求保持默认即可,会使用chatgpt-api库提供的当前可用的最适合的默认值。保底可用的是 text-davinci-003。当发现新的可用的chatGPT模型会更新这里的值。', component: 'Input', }, + { + field: 'thinkingTips', + label: '思考提示', + bottomHelpMessage: '是否开启AI正在思考中的提示信息。', + component: 'Switch', + }, { label: '以下为API2方式的配置', component: 'Divider', @@ -175,6 +177,22 @@ export function supportGuoba() { bottomHelpMessage: 'apiBaseUrl地址', component: 'Input', }, + { + label: '以下为API/API2方式公用的配置', + component: 'Divider', + }, + { + field: 'promptPrefixOverride', + label: 'AI风格', + bottomHelpMessage: '你可以在这里写入你希望AI回答的风格,比如希望优先回答中文,回答长一点等。', + component: 'Input', + }, + { + field: 'assistantLabel', + label: 'AI名字', + bottomHelpMessage: 'AI认为的自己的名字,当你问他你是谁是他会回答这里的名字。', + component: 'Input', + }, { label: '以下为浏览器方式的配置', component: 'Divider', diff --git a/utils/common.js b/utils/common.js index 07794f3..110cc10 100644 --- a/utils/common.js +++ b/utils/common.js @@ -1,6 +1,7 @@ // import { remark } from 'remark' // import stripMarkdown from 'strip-markdown' import { exec } from 'child_process' +import lodash from 'lodash' // export function markdownToText (markdown) { // return remark() // .use(stripMarkdown) @@ -19,6 +20,14 @@ export function escapeHtml (str) { return str.replace(/[&<>"'/]/g, (match) => htmlEntities[match]) } +export function randomString(length = 5) { + let str = '' + for (let i = 0; i < length; i++) { + str += lodash.random(36).toString(36) + } + return str.substr(0, length) +} + export async function upsertMessage (message) { await redis.set(`CHATGPT:MESSAGE:${message.id}`, JSON.stringify(message)) } diff --git a/utils/config.js b/utils/config.js index d0c240b..6dcf189 100644 --- a/utils/config.js +++ b/utils/config.js @@ -10,6 +10,7 @@ const defaultConfig = { toggleMode: 'at', showQRCode: true, cacheUrl: 'https://content.alcedogroup.com', + cacheEntry: false, apiKey: '', model: '', api: 'https://chatgpt.duti.tech/api/conversation',