优化图片模式发送功能,增加对话队列超时检测 (#188)

* 修复引用转发,默认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 <geyinchibuaa@gmail.com>
This commit is contained in:
HalcyonAlcedo 2023-02-23 18:47:35 +08:00 committed by GitHub
parent 38f726d92e
commit 7192a8c6fa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 107 additions and 76 deletions

View file

@ -5,7 +5,7 @@ import { v4 as uuid } from 'uuid'
import delay from 'delay' import delay from 'delay'
import { ChatGPTAPI } from 'chatgpt' import { ChatGPTAPI } from 'chatgpt'
import { ChatGPTClient, BingAIClient } from '@waylaidwanderer/chatgpt-api' 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 { ChatGPTPuppeteer } from '../utils/browser.js'
import { KeyvFile } from 'keyv-file' import { KeyvFile } from 'keyv-file'
import { OfficialChatGPTClient } from '../utils/message.js' import { OfficialChatGPTClient } from '../utils/message.js'
@ -288,6 +288,8 @@ export class chatgpt extends plugin {
let confirm = await redis.get('CHATGPT:CONFIRM') let confirm = await redis.get('CHATGPT:CONFIRM')
let confirmOn = (!confirm || confirm === 'on') && Config.thinkingTips let confirmOn = (!confirm || confirm === 'on') && Config.thinkingTips
if (await redis.lIndex('CHATGPT:CHAT_QUEUE', 0) === randomId) { if (await redis.lIndex('CHATGPT:CHAT_QUEUE', 0) === randomId) {
// 添加超时设置
await redis.pSetEx("CHATGPT:CHAT_QUEUE_TIMEOUT", Config.defaultTimeoutMs, randomId);
if (confirmOn) { if (confirmOn) {
await this.reply('我正在思考如何回复你,请稍等', true, { recallMsg: 8 }) await this.reply('我正在思考如何回复你,请稍等', true, { recallMsg: 8 })
} }
@ -300,8 +302,19 @@ export class chatgpt extends plugin {
// 开始排队 // 开始排队
while (true) { while (true) {
if (await redis.lIndex('CHATGPT:CHAT_QUEUE', 0) === randomId) { if (await redis.lIndex('CHATGPT:CHAT_QUEUE', 0) === randomId) {
await redis.pSetEx("CHATGPT:CHAT_QUEUE_TIMEOUT", Config.defaultTimeoutMs, randomId);
break break
} else { } 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) await delay(1500)
} }
} }
@ -403,37 +416,12 @@ export class chatgpt extends plugin {
} }
if (userSetting.usePicture) { if (userSetting.usePicture) {
// todo use next api of chatgpt to complete incomplete respoonse // todo use next api of chatgpt to complete incomplete respoonse
response = new Buffer.from(response).toString('base64')
if (Config.showQRCode) {
try { try {
let cacheres = await fetch(`${Config.cacheUrl}/cache`, { await this.renderImage(e, use !== 'bing' ? 'content/ChatGPT/index' : 'content/Bing/index', response, prompt, [], Config.showQRCode)
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) { } catch (err) {
logger.warn('error happened while uploading content to the cache server. QR Code will not be showed in this picture.') logger.warn('error happened while uploading content to the cache server. QR Code will not be showed in this picture.')
logger.error(err) 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 } }) await this.renderImage(e, use !== 'bing' ? 'content/ChatGPT/index' : 'content/Bing/index', response, prompt)
}
} 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 } })
} }
} else { } else {
let quotemessage = [] let quotemessage = []
@ -445,38 +433,13 @@ export class chatgpt extends plugin {
}) })
} }
if (Config.autoUsePicture && response.length > Config.autoUsePictureThreshold) { if (Config.autoUsePicture && response.length > Config.autoUsePictureThreshold) {
response = new Buffer.from(response).toString('base64')
// 文字过多时自动切换到图片模式输出 // 文字过多时自动切换到图片模式输出
if (Config.showQRCode) {
try { try {
let cacheres = await fetch(`${Config.cacheUrl}/cache`, { await this.renderImage(e, use !== 'bing' ? 'content/ChatGPT/index' : 'content/Bing/index', response, prompt, quotemessage, Config.showQRCode)
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) { } catch (err) {
logger.warn('error happened while uploading content to the cache server. QR Code will not be showed in this picture.') logger.warn('error happened while uploading content to the cache server. QR Code will not be showed in this picture.')
logger.error(err) 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 } }) await this.renderImage(e, use !== 'bing' ? 'content/ChatGPT/index' : 'content/Bing/index', response, prompt)
}
} 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 } })
} }
} else { } else {
await this.reply(`${response}`, e.isGroup) 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 }) await this.reply(`通信异常,请稍后重试:${err}`, true, { recallMsg: e.isGroup ? 10 : 0 })
} else { } 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) { async sendMessage (prompt, conversation = {}, use, e) {
if (!conversation) { if (!conversation) {
conversation = { conversation = {

View file

@ -25,6 +25,8 @@ export default {
// showQRCode: true, // showQRCode: true,
// 图片内容渲染服务器API地址 // 图片内容渲染服务器API地址
// cacheUrl: 'https://content.alcedogroup.com', // cacheUrl: 'https://content.alcedogroup.com',
// 图片内容渲染服务器开启预制访问代码,当渲染服务器访问较慢时可以开启,但无法保证远程渲染是否成功
// cacheEntry: false,
// *********************************************************************************************************************************** // ***********************************************************************************************************************************
// 以下为API方式(默认)的配置 * // 以下为API方式(默认)的配置 *
// *********************************************************************************************************************************** // ***********************************************************************************************************************************
@ -79,8 +81,6 @@ export default {
// debug: true, // debug: true,
// 各个地方的默认超时时间 // 各个地方的默认超时时间
// defaultTimeoutMs: 120000, // defaultTimeoutMs: 120000,
// bing默认超时时间bing太慢了有的时候
// bingTimeoutMs: 360000,
// 浏览器默认超时,浏览器可能需要更高的超时时间 // 浏览器默认超时,浏览器可能需要更高的超时时间
// chromeTimeoutMS: 120000 // chromeTimeoutMS: 120000
} }

View file

@ -81,6 +81,12 @@ export function supportGuoba() {
bottomHelpMessage: '用于缓存图片模式会话内容并渲染的服务器地址。', bottomHelpMessage: '用于缓存图片模式会话内容并渲染的服务器地址。',
component: 'Input', component: 'Input',
}, },
{
field: 'cacheEntry',
label: '预制渲染服务器访问代码',
bottomHelpMessage: '图片内容渲染服务器开启预制访问代码,当渲染服务器访问较慢时可以开启,但无法保证访问代码可以正常访问页面。',
component: 'Switch',
},
{ {
field: 'proxy', field: 'proxy',
label: '代理服务器地址', label: '代理服务器地址',
@ -107,16 +113,6 @@ export function supportGuoba() {
min: 0, min: 0,
}, },
}, },
{
field: 'bingTimeoutMs',
label: 'Bing超时时间',
helpMessage: '单位:毫秒',
bottomHelpMessage: 'bing默认超时时间bing太慢了有的时候。',
component: 'InputNumber',
componentProps: {
min: 0,
},
},
{ {
field: 'chromeTimeoutMS', field: 'chromeTimeoutMS',
label: '浏览器超时时间', label: '浏览器超时时间',
@ -143,6 +139,12 @@ export function supportGuoba() {
bottomHelpMessage: '模型名称如无特殊需求保持默认即可会使用chatgpt-api库提供的当前可用的最适合的默认值。保底可用的是 text-davinci-003。当发现新的可用的chatGPT模型会更新这里的值。', bottomHelpMessage: '模型名称如无特殊需求保持默认即可会使用chatgpt-api库提供的当前可用的最适合的默认值。保底可用的是 text-davinci-003。当发现新的可用的chatGPT模型会更新这里的值。',
component: 'Input', component: 'Input',
}, },
{
field: 'thinkingTips',
label: '思考提示',
bottomHelpMessage: '是否开启AI正在思考中的提示信息。',
component: 'Switch',
},
{ {
label: '以下为API2方式的配置', label: '以下为API2方式的配置',
component: 'Divider', component: 'Divider',
@ -175,6 +177,22 @@ export function supportGuoba() {
bottomHelpMessage: 'apiBaseUrl地址', bottomHelpMessage: 'apiBaseUrl地址',
component: 'Input', component: 'Input',
}, },
{
label: '以下为API/API2方式公用的配置',
component: 'Divider',
},
{
field: 'promptPrefixOverride',
label: 'AI风格',
bottomHelpMessage: '你可以在这里写入你希望AI回答的风格比如希望优先回答中文回答长一点等。',
component: 'Input',
},
{
field: 'assistantLabel',
label: 'AI名字',
bottomHelpMessage: 'AI认为的自己的名字当你问他你是谁是他会回答这里的名字。',
component: 'Input',
},
{ {
label: '以下为浏览器方式的配置', label: '以下为浏览器方式的配置',
component: 'Divider', component: 'Divider',

View file

@ -1,6 +1,7 @@
// import { remark } from 'remark' // import { remark } from 'remark'
// import stripMarkdown from 'strip-markdown' // import stripMarkdown from 'strip-markdown'
import { exec } from 'child_process' import { exec } from 'child_process'
import lodash from 'lodash'
// export function markdownToText (markdown) { // export function markdownToText (markdown) {
// return remark() // return remark()
// .use(stripMarkdown) // .use(stripMarkdown)
@ -19,6 +20,14 @@ export function escapeHtml (str) {
return str.replace(/[&<>"'/]/g, (match) => htmlEntities[match]) 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) { export async function upsertMessage (message) {
await redis.set(`CHATGPT:MESSAGE:${message.id}`, JSON.stringify(message)) await redis.set(`CHATGPT:MESSAGE:${message.id}`, JSON.stringify(message))
} }

View file

@ -10,6 +10,7 @@ const defaultConfig = {
toggleMode: 'at', toggleMode: 'at',
showQRCode: true, showQRCode: true,
cacheUrl: 'https://content.alcedogroup.com', cacheUrl: 'https://content.alcedogroup.com',
cacheEntry: false,
apiKey: '', apiKey: '',
model: '', model: '',
api: 'https://chatgpt.duti.tech/api/conversation', api: 'https://chatgpt.duti.tech/api/conversation',