mirror of
https://github.com/ikechan8370/chatgpt-plugin.git
synced 2025-12-16 13:27:08 +00:00
优化图片模式发送功能,增加对话队列超时检测 (#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:
parent
38f726d92e
commit
7192a8c6fa
5 changed files with 107 additions and 76 deletions
131
apps/chat.js
131
apps/chat.js
|
|
@ -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')
|
try {
|
||||||
if (Config.showQRCode) {
|
await this.renderImage(e, use !== 'bing' ? 'content/ChatGPT/index' : 'content/Bing/index', response, prompt, [], Config.showQRCode)
|
||||||
try {
|
} catch (err) {
|
||||||
let cacheres = await fetch(`${Config.cacheUrl}/cache`, {
|
logger.warn('error happened while uploading content to the cache server. QR Code will not be showed in this picture.')
|
||||||
method: 'POST',
|
logger.error(err)
|
||||||
headers: {
|
await this.renderImage(e, use !== 'bing' ? 'content/ChatGPT/index' : 'content/Bing/index', response, prompt)
|
||||||
'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 } })
|
|
||||||
}
|
}
|
||||||
} 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 {
|
await this.renderImage(e, use !== 'bing' ? 'content/ChatGPT/index' : 'content/Bing/index', response, prompt, quotemessage, Config.showQRCode)
|
||||||
let cacheres = await fetch(`${Config.cacheUrl}/cache`, {
|
} catch (err) {
|
||||||
method: 'POST',
|
logger.warn('error happened while uploading content to the cache server. QR Code will not be showed in this picture.')
|
||||||
headers: {
|
logger.error(err)
|
||||||
'Content-Type': 'application/json'
|
await this.renderImage(e, use !== 'bing' ? 'content/ChatGPT/index' : 'content/Bing/index', response, prompt)
|
||||||
},
|
|
||||||
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 } })
|
|
||||||
}
|
}
|
||||||
} 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 = {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue