新增管理面板,重写必应token管理,错误修复 (#340)

* 修复引用转发,默认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配置项

* 添加消息队列超时弹出

* 优化图片模式处理,解决对话队列卡住的问题

* 添加对图片ocr的支持

* 添加图片识别配置项

* 添加黑名单配置项

* 修复一些bug

* 修改锅巴配置格式和描述

* 传入数据也使用markdown

* 图片识别换行改为marked兼容

* 添加绘图CD配置项

* 独立render模块,添加图片回复引用

* 添加必应风格

* 修复上下文,修改bing样式

* 修复上下文

* 添加Sydney上下文支持

* 调整不同模式下的bing渲染颜色

* 修复样式

* 修复无法结束会话的问题

* fix: 更新版本号

* 修复无法结束对话的问题

* 向缓存服务器传送样式

* 为网址格式的配置添加验证

* 去除重复的Keyv删除,取消锅巴配置格式检查

* 闭合中断的代码块

* 试添加Sydney图片模式的情感显示

* 修复at不兼容

* 处理意外的markdown包裹和结构解析修复

* 修复markdown处理的顺序错误

* 兼容json换行

* 重写completeJSON和使用

* 修复换行格式异常

* 均衡BingToken使用

* 修复删除token的数组处理错误

* 修改token文字描述

* 创建本地缓存服务

* 修复首次使用无法添加bingtoken的问题

* 修复意外的删除格式问题,添加查看token功能

* 修复路由错误,暂时固定ip测试

* 恢复引用功能

* 更新渲染页面

* 更换缓存目录

* 清除调试用消息

* 调整屏幕分辨率

* 使用服务器生成的访问地址

* 改为使用api获取公网ip

* 修复引用显示

* 添加依赖需求

* 更新渲染页面和渲染api

* 修复渲染页面错误

* 修复建议字符串切割,添加帮助路由

* 添加内容中图片数据获取功能

* 试修复suggestbug

* 修复图片导致服务器卡死的问题

* 暂时禁用图片

* 尝试恢复图片

* 添加链接图片识别

* 替换掉request

* 修复可能的responseUrls空值

* 优化格式

* 更新渲染页面

* 尝试新的引用索引

* 取消渲染时旧的策略

* 更新帮助页面

* 修复帮助路由

* 修复渲染页面错误

* 修复错误的正则

* 修改系统api服务

* 添加配置项

* 将新渲染方式加入配置并还原原渲染方式,进行并存

* 暂时取消端口设置功能

* 重新开启端口设置

* 修复旧渲染引用

* 更新帮助样式

* 更新帮助,增强功能

* 有cacheHost的情况下不再附带端口号

* 添加渲染图片的宽度设置

* 添加渲染页面宽度调整,修bug

* 修复二维码不显示

* 添加第三方渲染支持

* 修复一些渲染页面问题

* 更新渲染页面

* 修正错误的变量调用

* 添加新渲染模式bot命名

* 修复空消息问题

* 撤销之前的修复,使用新方法修复

* 修复返回空页面问题

* 尝试不依赖网络获取外网地址

* 修bug,初步创建管理系统

* 依赖名写错了

* 修复错误的异步

* 修正错误的配置调用

* 放弃本机设置的获取方案,对服务器获取多半失效

* 添加配置页面接口

* 更新渲染页面

* 添加依赖

* 修复bug

* 移除windows性能显示,更换依赖

* 添加依赖

* 修复图片异常时不反回文字而是直接报错的问题

* 修改必应token记录和均衡方法,更新渲染页面

* 修复错误

* 修复bug,更新渲染页面

* 更新渲染

* 修复ip错误

* 完善配置页面

* 渲染页面错误修复

* 更新版本号

* 只获取一次有效ip

* 修复渲染页面bug

---------

Co-authored-by: ikechan8370 <geyinchibuaa@gmail.com>
Co-authored-by: Err0rCM <68117733+Err0rCM@users.noreply.github.com>
This commit is contained in:
HalcyonAlcedo 2023-04-12 16:35:41 +08:00 committed by GitHub
parent c0a596a608
commit 458b04c666
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 767 additions and 236 deletions

View file

@ -891,7 +891,7 @@ 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 this.renderImage(e, use !== 'bing' ? 'content/ChatGPT/index' : 'content/Bing/index', `通信异常,错误信息如下 ${err?.message || err?.data?.message || (typeof (err) === 'object' ? JSON.stringify(err) : err) || '未能确认错误类型!'}`, prompt) await this.renderImage(e, use !== 'bing' ? 'content/ChatGPT/index' : 'content/Bing/index', `通信异常,错误信息如下 ${err?.message || err?.data?.message || (typeof(err) === 'object' ? JSON.stringify(err) : err) || '未能确认错误类型!'}`, prompt)
} }
} }
} }
@ -1007,7 +1007,7 @@ export class chatgpt extends plugin {
if (cacheres.ok) { if (cacheres.ok) {
cacheData = Object.assign({}, cacheData, await cacheres.json()) cacheData = Object.assign({}, cacheData, await cacheres.json())
} }
if (cacheData.error) { await this.reply(`出现错误:${cacheData.error}`, true) } else { await e.reply(await renderUrl(e, viewHost + `page/${cacheData.file}?qr=${Config.showQRCode ? 'true' : 'false'}`, { retType: Config.quoteReply ? 'base64' : '', Viewport: { width: Config.chatViewWidth, height: parseInt(Config.chatViewWidth * 0.56) } }), e.isGroup && Config.quoteReply) } if (cacheData.error || cacheres.status != 200) { await this.reply(`出现错误:${cacheData.error || 'server error ' + cacheres.status}`, true) } else { await e.reply(await renderUrl(e, viewHost + `page/${cacheData.file}?qr=${Config.showQRCode ? 'true' : 'false'}`, { retType: Config.quoteReply ? 'base64' : '', Viewport: { width: Config.chatViewWidth, height: parseInt(Config.chatViewWidth * 0.56) } }), e.isGroup && Config.quoteReply) }
} else { } else {
if (Config.cacheEntry) cacheData.file = randomString() if (Config.cacheEntry) cacheData.file = randomString()
const cacheresOption = { const cacheresOption = {
@ -1204,17 +1204,22 @@ export class chatgpt extends plugin {
const message = error?.message || error?.data?.message || error || '出错了' const message = error?.message || error?.data?.message || error || '出错了'
if (message && message.indexOf('限流') > -1) { if (message && message.indexOf('限流') > -1) {
throttledTokens.push(bingToken) throttledTokens.push(bingToken)
let bingTokens = JSON.parse(await redis.get('CHATGPT:BING_TOKENS'))
const badBingToken = bingTokens.findIndex(element => element.Token === bingToken)
const now = new Date()
const hours = now.getHours()
now.setHours(hours + 6)
bingTokens[badBingToken].State = '受限'
bingTokens[index].DisactivationTime = now
await redis.set('CHATGPT:BING_TOKENS', JSON.stringify(bingTokens))
// 不减次数 // 不减次数
} else if (message && message.indexOf('UnauthorizedRequest') > -1) { } else if (message && message.indexOf('UnauthorizedRequest') > -1) {
// token过期了 // token过期了
logger.warn(`token${bingToken}过期了,将自动移除`) let bingTokens = JSON.parse(await redis.get('CHATGPT:BING_TOKENS'))
let savedBingToken = await redis.get('CHATGPT:BING_TOKEN') const badBingToken = bingTokens.findIndex(element => element.Token === bingToken)
savedBingToken = savedBingToken.split('|') bingTokens[badBingToken].State = '过期'
let tokenId = savedBingToken.indexOf(bingToken) await redis.set('CHATGPT:BING_TOKENS', JSON.stringify(bingTokens))
savedBingToken.splice(tokenId, 1) logger.warn(`token${bingToken}已过期`)
savedBingToken = savedBingToken.filter(function (element) { return element !== '' })
await redis.set('CHATGPT:BING_TOKEN', savedBingToken.join('|'))
logger.mark(`token${bingToken}已移除`)
} else { } else {
retry-- retry--
errorMessage = message === 'Timed out waiting for response. Try enabling debug mode to see more information.' ? (reply ? `${reply}\n不行了,我的大脑过载了,处理不过来了!` : '必应的小脑瓜不好使了,不知道怎么回答!') : message errorMessage = message === 'Timed out waiting for response. Try enabling debug mode to see more information.' ? (reply ? `${reply}\n不行了,我的大脑过载了,处理不过来了!` : '必应的小脑瓜不好使了,不知道怎么回答!') : message
@ -1469,34 +1474,49 @@ export class chatgpt extends plugin {
async function getAvailableBingToken (conversation, throttled = []) { async function getAvailableBingToken (conversation, throttled = []) {
let allThrottled = false let allThrottled = false
let bingToken = await redis.get('CHATGPT:BING_TOKEN') if (!await redis.get('CHATGPT:BING_TOKENS')) {
if (!bingToken) {
throw new Error('未绑定Bing Cookie请使用#chatgpt设置必应token命令绑定Bing Cookie') throw new Error('未绑定Bing Cookie请使用#chatgpt设置必应token命令绑定Bing Cookie')
} }
const bingTokens = bingToken.split('|')
// 负载均衡 let bingToken = ''
if (Config.toneStyle === 'Sydney' || Config.toneStyle === 'Custom') { let bingTokens = JSON.parse(await redis.get('CHATGPT:BING_TOKENS'))
// sydney下不需要保证同一token const normal = bingTokens.filter(element => element.State === '正常')
let notThrottled = bingTokens.filter(t => throttled.indexOf(t) === -1) const restricted = bingTokens.filter(element => element.State === '受限')
if (notThrottled.length > 0) {
bingToken = notThrottled[0] // 判断受限的token是否已经可以解除
} else { for (const restrictedToken of restricted) {
// 全都被限流了,随便找一个算了 const now = new Date()
allThrottled = true const tk = new Date(restrictedToken.DisactivationTime)
const select = Math.floor(Math.random() * bingTokens.length) if (tk <= now) {
bingToken = bingTokens[select] const index = bingTokens.findIndex(element => element.Token === restrictedToken.Token)
bingTokens[index].Usage = 0
bingTokens[index].State = '正常'
} }
// const select = Math.floor(Math.random() * bingTokens.length) }
// bingToken = bingTokens[select] if (normal.length > 0) {
const minElement = normal.reduce((min, current) => {
return current.Usage < min.Usage ? current : min
})
bingToken = minElement.Token
} else if (restricted.length > 0) {
allThrottled = true
const minElement = restricted.reduce((min, current) => {
return current.Usage < min.Usage ? current : min
})
bingToken = minElement.Token
} else { } else {
throw new Error('全部Token均已失效暂时无法使用')
}
if (Config.toneStyle != 'Sydney' && Config.toneStyle != 'Custom') {
// bing 下需要保证同一对话使用同一账号的token // bing 下需要保证同一对话使用同一账号的token
if (!conversation.bingToken) { if (bingTokens.findIndex(element => element.Token === conversation.bingToken) > -1) {
const select = Math.floor(Math.random() * bingTokens.length)
bingToken = bingTokens[select]
} else if (bingTokens.indexOf(conversation.bingToken) > -1) {
bingToken = conversation.bingToken bingToken = conversation.bingToken
} }
} }
// 记录使用情况
const index = bingTokens.findIndex(element => element.Token === bingToken)
bingTokens[index].Usage += 1
await redis.set('CHATGPT:BING_TOKENS', JSON.stringify(bingTokens))
return { return {
bingToken, bingToken,
allThrottled allThrottled

View file

@ -239,17 +239,36 @@ export class dalle extends plugin {
this.reply('请提供绘图prompt') this.reply('请提供绘图prompt')
return false return false
} }
let bingToken = await redis.get('CHATGPT:BING_TOKEN')
let bingToken = ''
if (await redis.exists('CHATGPT:BING_TOKENS') != 0) {
let bingTokens = JSON.parse(await redis.get('CHATGPT:BING_TOKENS'))
const normal = bingTokens.filter(element => element.State === '正常')
const restricted = bingTokens.filter(element => element.State === '受限')
if (normal.length > 0) {
const minElement = normal.reduce((min, current) => {
return current.Usage < min.Usage ? current : min
})
bingToken = minElement.Token
} else if (restricted.length > 0) {
allThrottled = true
const minElement = restricted.reduce((min, current) => {
return current.Usage < min.Usage ? current : min
})
bingToken = minElement.Token
} else {
throw new Error('全部Token均已失效暂时无法使用')
}
}
if (!bingToken) { if (!bingToken) {
throw new Error('未绑定Bing Cookie请使用#chatgpt设置必应token命令绑定Bing Cookie') throw new Error('未绑定Bing Cookie请使用#chatgpt设置必应token命令绑定Bing Cookie')
} }
const bingTokens = bingToken.split('|') // 记录token使用
// 负载均衡 let bingTokens = JSON.parse(await redis.get('CHATGPT:BING_TOKENS'))
if (Config.toneStyle === 'Sydney' || Config.toneStyle === 'Custom') { const index = bingTokens.findIndex(element => element.Token === bingToken)
// sydney下不需要保证同一token bingTokens[index].Usage += 1
const select = Math.floor(Math.random() * bingTokens.length) await redis.set('CHATGPT:BING_TOKENS', JSON.stringify(bingTokens))
bingToken = bingTokens[select]
}
let client = new BingDrawClient({ let client = new BingDrawClient({
baseUrl: Config.sydneyReverseProxy, baseUrl: Config.sydneyReverseProxy,
userToken: bingToken userToken: bingToken

View file

@ -303,7 +303,7 @@ export class help extends plugin {
} }
async newHelp (e) { async newHelp (e) {
let use = e.msg.replace(/^#帮助-/, '').toUpperCase() let use = e.msg.replace(/^#帮助-/, '').toUpperCase().trim()
await renderUrl(e, `http://127.0.0.1:${Config.serverPort || 3321}/help/` + use, {Viewport: {width: 800, height: 600}}) await renderUrl(e, `http://127.0.0.1:${Config.serverPort || 3321}/help/` + use, {Viewport: {width: 800, height: 600}})
} }

View file

@ -2,7 +2,7 @@ import plugin from '../../../lib/plugins/plugin.js'
import { Config } from '../utils/config.js' import { Config } from '../utils/config.js'
import { BingAIClient } from '@waylaidwanderer/chatgpt-api' import { BingAIClient } from '@waylaidwanderer/chatgpt-api'
import { exec } from 'child_process' import { exec } from 'child_process'
import { checkPnpm, formatDuration, parseDuration } from '../utils/common.js' import { checkPnpm, formatDuration, parseDuration, getPublicIP } from '../utils/common.js'
import SydneyAIClient from '../utils/SydneyAIClient.js' import SydneyAIClient from '../utils/SydneyAIClient.js'
export class ChatgptManagement extends plugin { export class ChatgptManagement extends plugin {
@ -43,6 +43,11 @@ export class ChatgptManagement extends plugin {
fnc: 'getBingAccessToken', fnc: 'getBingAccessToken',
permission: 'master' permission: 'master'
}, },
{
reg: '#chatgpt(迁移|恢复)(必应|Bing |bing )(token|Token)',
fnc: 'migrateBingAccessToken',
permission: 'master'
},
{ {
reg: '^#chatgpt切换浏览器$', reg: '^#chatgpt切换浏览器$',
fnc: 'useBrowserBasedSolution', fnc: 'useBrowserBasedSolution',
@ -137,6 +142,16 @@ export class ChatgptManagement extends plugin {
/** 执行方法 */ /** 执行方法 */
fnc: 'enableGroupContext', fnc: 'enableGroupContext',
permission: 'master' permission: 'master'
},
{
reg: '^#(设置|修改)管理密码',
fnc: 'setAdminPassword',
permission: 'master'
},
{
reg: '^#chatgpt系统(设置|配置|管理)',
fnc: 'adminPage',
permission: 'master'
} }
] ]
}) })
@ -182,22 +197,49 @@ export class ChatgptManagement extends plugin {
return false return false
} }
async migrateBingAccessToken () {
let token = await redis.get('CHATGPT:BING_TOKEN')
if (token) {
token = token.split('|')
token = token.map((item, index) => (
{
Token: item,
State: '正常',
Usage: 0,
}
))
} else {
token = []
}
let tokens = await redis.get('CHATGPT:BING_TOKENS')
if (tokens) {
tokens = JSON.parse(tokens)
} else {
tokens = []
}
await redis.set('CHATGPT:BING_TOKENS', JSON.stringify([...token, ...tokens]))
await this.reply(`迁移完成`, true)
}
async getBingAccessToken (e) { async getBingAccessToken (e) {
let tokens = await redis.get('CHATGPT:BING_TOKEN') let tokens = await redis.get('CHATGPT:BING_TOKENS')
tokens = tokens.split('|') if (tokens) tokens = JSON.parse(tokens)
tokens = tokens.map((item, index) => ( else tokens = []
`${index}】 Token${item.substring(0, 5 / 2) + '...' + item.substring(item.length - 5 / 2, item.length)}` tokens = tokens.length > 0 ? tokens.map((item, index) => (
)).join('\n') `${index}】 Token${item.Token.substring(0, 5 / 2) + '...' + item.Token.substring(item.Token.length - 5 / 2, item.Token.length)}`
)).join('\n') : '无必应Token记录'
await this.reply(`${tokens}`, true) await this.reply(`${tokens}`, true)
return false return false
} }
async delBingAccessToken (e) { async delBingAccessToken (e) {
this.setContext('deleteBingToken') this.setContext('deleteBingToken')
let tokens = await redis.get('CHATGPT:BING_TOKEN') let tokens = await redis.get('CHATGPT:BING_TOKENS')
tokens = tokens.split('|') if (tokens) tokens = JSON.parse(tokens)
tokens = tokens.map((item, index) => ( else tokens = []
`${index}】 Token${item.substring(0, 5 / 2) + '...' + item.substring(item.length - 5 / 2, item.length)}` tokens = tokens.length > 0 ? tokens.map((item, index) => (
)).join('\n') `${index}】 Token${item.Token.substring(0, 5 / 2) + '...' + item.Token.substring(item.Token.length - 5 / 2, item.Token.length)}`
)).join('\n') : '无必应Token记录'
await this.reply(`请发送要删除的token编号\n${tokens}`, true) await this.reply(`请发送要删除的token编号\n${tokens}`, true)
return false return false
} }
@ -226,38 +268,57 @@ export class ChatgptManagement extends plugin {
} else { } else {
logger.error('bing token 无效', res) logger.error('bing token 无效', res)
// 移除无效token // 移除无效token
if (await redis.exists('CHATGPT:BING_TOKENS') != 0) {
let bingToken = JSON.parse(await redis.get('CHATGPT:BING_TOKENS'))
const element = bingToken.findIndex(element => element.token === token)
if (element >= 0) {
bingToken[element].State = '异常'
await redis.set('CHATGPT:BING_TOKENS', JSON.stringify(bingToken))
}
}
await this.reply(`经检测Bing Token无效。来自Bing的错误提示${res.result?.message}`) await this.reply(`经检测Bing Token无效。来自Bing的错误提示${res.result?.message}`)
} }
}) })
if (await redis.exists('CHATGPT:BING_TOKEN') != 0) { let bingToken = []
let bingToken = await redis.get('CHATGPT:BING_TOKEN') if (await redis.exists('CHATGPT:BING_TOKENS') != 0) {
bingToken = bingToken.split('|') bingToken = JSON.parse(await redis.get('CHATGPT:BING_TOKENS'))
if (!bingToken.includes(token)) bingToken.push(token) if (!bingToken.some(element => element.token === token)) bingToken.push({
bingToken = bingToken.filter(function (element) { return element !== '' }) Token: token,
token = bingToken.join('|') State: '正常',
Usage: 0,
})
} else {
bingToken = [{
Token: token,
State: '正常',
Usage: 0,
}]
} }
await redis.set('CHATGPT:BING_TOKEN', token) await redis.set('CHATGPT:BING_TOKENS', JSON.stringify(bingToken))
await this.reply('Bing Token设置成功', true) await this.reply('Bing Token设置成功', true)
this.finish('saveBingToken') this.finish('saveBingToken')
} }
async deleteBingToken () { async deleteBingToken () {
if (!this.e.msg) return if (!this.e.msg) return
let bingToken = await redis.get('CHATGPT:BING_TOKEN')
bingToken = bingToken.split('|')
let tokenId = this.e.msg let tokenId = this.e.msg
if (bingToken[tokenId] === null || bingToken[tokenId] === undefined) { if (await redis.exists('CHATGPT:BING_TOKENS') != 0) {
await this.reply('Token编号错误', true) let bingToken = JSON.parse(await redis.get('CHATGPT:BING_TOKENS'))
if (tokenId >= 0 && tokenId < bingToken.length) {
const removeToken = bingToken[tokenId].Token
bingToken.splice(tokenId,1)
await redis.set('CHATGPT:BING_TOKENS', JSON.stringify(bingToken))
await this.reply(`Token ${removeToken.substring(0, 5 / 2) + '...' + removeToken.substring(removeToken.length - 5 / 2, removeToken.length)} 移除成功`, true)
this.finish('deleteBingToken')
} else {
await this.reply('Token编号错误', true)
this.finish('deleteBingToken')
return
}
} else {
await this.reply('Token记录异常', true)
this.finish('deleteBingToken') this.finish('deleteBingToken')
return
} }
const removeToken = bingToken[tokenId]
bingToken.splice(tokenId, 1)
bingToken = bingToken.filter(function (element) { return element !== '' })
let token = bingToken.join('|')
await redis.set('CHATGPT:BING_TOKEN', token)
await this.reply(`Token ${removeToken.substring(0, 5 / 2) + '...' + removeToken.substring(removeToken.length - 5 / 2, removeToken.length)} 移除成功`, true)
this.finish('deleteBingToken')
} }
async saveToken () { async saveToken () {
@ -653,4 +714,24 @@ export class ChatgptManagement extends plugin {
async queryBingPromptPrefix (e) { async queryBingPromptPrefix (e) {
await this.reply(Config.sydney, true) await this.reply(Config.sydney, true)
} }
async setAdminPassword (e) {
this.setContext('saveAdminPassword')
await this.reply('请发送系统管理密码', true)
return false
}
async saveAdminPassword (e) {
if (!this.e.msg) return
let passwd = this.e.msg
await redis.set('CHATGPT:ADMIN_PASSWD', passwd)
await this.reply('设置成功', true)
this.finish('saveAdminPassword')
}
async adminPage (e) {
const viewHost = Config.serverHost ? `http://${Config.serverHost}/` : `http://${await getPublicIP()}:${Config.serverPort || 3321}/`
await this.reply(`请登录${viewHost + 'admin/settings'}进行系统配置`, true)
}
} }

View file

@ -30,13 +30,15 @@ for (let i in files) {
try { try {
await import('fastify') await import('fastify')
await import('fastify-cookie')
await import('@fastify/cors') await import('@fastify/cors')
await import('@fastify/static') await import('@fastify/static')
// 启动服务器 await import('os-utils')
await createServer()
} catch (err) { } catch (err) {
logger.warn('【ChatGPT-Plugin】依赖fastify、@fastify/cors、@fastify/static未安装可能影响系统Api服务运行当前Api服务模块已禁用建议执行pnpm install fastify @fastify/cors @fastify/static安装') logger.warn('【ChatGPT-Plugin】依赖fastify、fastify-cookie、@fastify/cors、@fastify/static、os-utils未安装可能影响系统Api服务运行当前Api服务模块已禁用建议执行pnpm install fastify @fastify/cors @fastify/static fastify-cookie os-utils安装')
} }
// 启动服务器
await createServer()
logger.info('**************************************') logger.info('**************************************')
logger.info('chatgpt-plugin加载成功') logger.info('chatgpt-plugin加载成功')

View file

@ -5,12 +5,14 @@
"dependencies": { "dependencies": {
"@fastify/cors": "^8.2.0", "@fastify/cors": "^8.2.0",
"@fastify/static": "^6.9.0", "@fastify/static": "^6.9.0",
"@fastify/cookie": "^8.3.0",
"@waylaidwanderer/chatgpt-api": "^1.33.2", "@waylaidwanderer/chatgpt-api": "^1.33.2",
"chatgpt": "^5.1.1", "chatgpt": "^5.1.1",
"delay": "^5.0.0", "delay": "^5.0.0",
"eventsource": "^2.0.2", "eventsource": "^2.0.2",
"eventsource-parser": "^1.0.0", "eventsource-parser": "^1.0.0",
"fastify": "^4.13.0", "fastify": "^4.13.0",
"fastify-cookie": "^5.7.0",
"https-proxy-agent": "5.0.1", "https-proxy-agent": "5.0.1",
"keyv": "^4.5.2", "keyv": "^4.5.2",
"keyv-file": "^0.2.0", "keyv-file": "^0.2.0",

View file

@ -205,6 +205,9 @@
"text": "设置必应和open的Token和ApiKey", "text": "设置必应和open的Token和ApiKey",
"list": [ "list": [
"#chatgpt设置必应token", "#chatgpt设置必应token",
"#chatgpt删除必应token",
"#chatgpt查看必应token",
"#chatgpt迁移必应token",
"#chatgpt设置APIKey" "#chatgpt设置APIKey"
], ],
"tip": "管理员功能" "tip": "管理员功能"
@ -216,7 +219,7 @@
"list": [ "list": [
"#OpenAI剩余额度" "#OpenAI剩余额度"
], ],
"tip": "管理员功能" "tip": "失效"
}, },
{ {
"icon": "fas fa-coffee", "icon": "fas fa-coffee",
@ -229,6 +232,16 @@
"#chatgpt查看Sydney设定" "#chatgpt查看Sydney设定"
], ],
"tip": "管理员功能" "tip": "管理员功能"
},
{
"icon": "fas fa-key",
"title": "管理面板",
"text": "后台管理面板",
"list": [
"#chatgpt系统管理",
"#修改管理密码"
],
"tip": "管理员功能"
} }
] ]
} }

View file

@ -1,46 +1,65 @@
import fastify from 'fastify' import fastify from 'fastify'
import fastifyCookie from '@fastify/cookie'
import cors from '@fastify/cors' import cors from '@fastify/cors'
import fstatic from '@fastify/static' import fstatic from '@fastify/static'
import fs from 'fs' import fs from 'fs'
import path from 'path' import path from 'path'
import http from 'http' import os from 'os'
import schedule from 'node-schedule'
import { Config } from '../utils/config.js' import { Config } from '../utils/config.js'
import { randomString, getPublicIP } from '../utils/common.js'
function getPublicIP() {
return new Promise((resolve, reject) => {
http.get('http://ipinfo.io/json', (res) => {
let data = ''
res.on('data', (chunk) => {
data += chunk
});
res.on('end', () => {
try {
const ip = JSON.parse(data).ip
resolve(ip)
} catch (e) {
reject(e)
}
})
}).on('error', (err) => {
reject(err)
})
})
}
const __dirname = path.resolve() const __dirname = path.resolve()
const server = fastify({ const server = fastify({
logger: Config.debug logger: Config.debug
}) })
let usertoken = ''
let Statistics = {
SystemAccess: {
count: 0,
oldCount: 0
},
CacheFile: {
count: 0,
oldCount: 0
},
WebAccess: {
count: 0,
oldCount: 0
},
SystemLoad: {
count: 0,
oldCount: 0
}
}
async function getLoad() {
// 获取当前操作系统平台
const platform = os.platform();
// 判断平台是Linux还是Windows
if (platform === 'linux') {
// 如果是Linux使用os.loadavg()方法获取负载平均值
const loadAvg = os.loadavg();
return loadAvg[0] * 100
} else if (platform === 'win32') {
// 如果是Windows不获取性能
return 0
} else {
return 0
}
}
export async function createServer() { export async function createServer() {
await server.register(cors, { await server.register(cors, {
origin: '*', origin: '*',
}) })
await server.register(fstatic, { await server.register(fstatic, {
root: path.join(__dirname, 'plugins/chatgpt-plugin/server/static/'), root: path.join(__dirname, 'plugins/chatgpt-plugin/server/static/'),
}) })
await server.register(fastifyCookie)
await server.get('/page/*', (request, reply) => { await server.get('/page/*', (request, reply) => {
const stream = fs.createReadStream('plugins/chatgpt-plugin/server/static/index.html') const stream = fs.createReadStream('plugins/chatgpt-plugin/server/static/index.html')
reply.type('text/html').send(stream) reply.type('text/html').send(stream)
@ -49,69 +68,171 @@ export async function createServer() {
const stream = fs.createReadStream('plugins/chatgpt-plugin/server/static/index.html') const stream = fs.createReadStream('plugins/chatgpt-plugin/server/static/index.html')
reply.type('text/html').send(stream) reply.type('text/html').send(stream)
}) })
await server.get('/auth/*', (request, reply) => {
const stream = fs.createReadStream('plugins/chatgpt-plugin/server/static/index.html')
reply.type('text/html').send(stream)
})
await server.get('/admin/*', (request, reply) => {
const token = request.cookies.token || 'unknown'
if (token != usertoken) {
reply.redirect(301, '/auth/login')
}
const stream = fs.createReadStream('plugins/chatgpt-plugin/server/static/index.html')
reply.type('text/html').send(stream)
})
// 登录
server.post('/login', async (request, reply) => {
const body = request.body || {}
if (body.qq && body.passwd) {
if (body.qq == Bot.uin && await redis.get('CHATGPT:ADMIN_PASSWD') == body.passwd) {
usertoken = randomString(32)
reply.setCookie('token', usertoken, {path: '/'})
reply.send({login:true})
} else {
reply.send({login:false,err:'用户名密码错误'})
}
} else {
reply.send({login:false,err:'未输入用户名或密码'})
}
})
// 页面数据获取 // 页面数据获取
server.post('/page', async (request, reply) => { server.post('/page', async (request, reply) => {
const body = request.body || {} const body = request.body || {}
if (body.code) { if (body.code) {
const dir = 'resources/ChatGPTCache/page' const dir = 'resources/ChatGPTCache/page'
const filename = body.code + '.json' const filename = body.code + '.json'
const filepath = path.join(dir, filename) const filepath = path.join(dir, filename)
let data = fs.readFileSync(filepath, 'utf8')
let data = fs.readFileSync(filepath, 'utf8') reply.send(data)
reply.send(data) }
}
}) })
// 帮助内容获取 // 帮助内容获取
server.post('/help', async (request, reply) => { server.post('/help', async (request, reply) => {
const body = request.body || {} const body = request.body || {}
if (body.use) { if (body.use) {
const dir = 'plugins/chatgpt-plugin/resources' const dir = 'plugins/chatgpt-plugin/resources'
const filename = 'help.json' const filename = 'help.json'
const filepath = path.join(dir, filename) const filepath = path.join(dir, filename)
let data = fs.readFileSync(filepath, 'utf8') let data = fs.readFileSync(filepath, 'utf8')
data = JSON.parse(data) data = JSON.parse(data)
reply.send(data[body.use]) reply.send(data[body.use])
} }
}) })
// 创建页面缓存内容 // 创建页面缓存内容
server.post('/cache', async (request, reply) => { server.post('/cache', async (request, reply) => {
const body = request.body || {} const body = request.body || {}
if (body.content) { if (body.content) {
const dir = 'resources/ChatGPTCache/page' const dir = 'resources/ChatGPTCache/page'
const filename = body.entry + '.json' const filename = body.entry + '.json'
const filepath = path.join(dir, filename) const filepath = path.join(dir, filename)
const regexUrl = /\b((?:https?|ftp|file):\/\/[-a-zA-Z0-9+&@#\/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#\/%=~_|])/g const regexUrl = /\b((?:https?|ftp|file):\/\/[-a-zA-Z0-9+&@#\/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#\/%=~_|])/g
const ip = await getPublicIP() const ip = await getPublicIP()
try { try {
fs.mkdirSync(dir, { recursive: true }); fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(filepath, JSON.stringify({ fs.writeFileSync(filepath, JSON.stringify({
user: body.content.senderName, user: body.content.senderName,
bot: Config.chatViewBotName || (body.bing ? 'Bing' : 'ChatGPT'), bot: Config.chatViewBotName || (body.bing ? 'Bing' : 'ChatGPT'),
userImg: body.userImg || '', userImg: body.userImg || '',
botImg: body.botImg || '', botImg: body.botImg || '',
question: body.content.prompt, question: body.content.prompt,
message: body.content.content, message: body.content.content,
group: body.content.group, group: body.content.group,
herf: `http://${body.cacheHost || (ip + ':' + Config.serverPort || 3321)}/page/${body.entry}`, herf: `http://${body.cacheHost || (ip + ':' + Config.serverPort || 3321)}/page/${body.entry}`,
quote: body.content.quote, quote: body.content.quote,
images: body.content.images || [], images: body.content.images || [],
suggest: body.content.suggest || [], suggest: body.content.suggest || [],
time: new Date() time: new Date()
})) }))
reply.send({ file: body.entry, cacheUrl: `http://${ip}:${Config.serverPort || 3321}/page/${body.entry}` }) Statistics.CacheFile.count += 1
} catch (err) { reply.send({ file: body.entry, cacheUrl: `http://${ip}:${Config.serverPort || 3321}/page/${body.entry}` })
console.error(err) } catch (err) {
reply.send({ file: body.entry, cacheUrl: `http://${ip}/page/${body.entry}`, error: '生成失败' }) console.error(err)
} reply.send({ file: body.entry, cacheUrl: `http://${ip}:${Config.serverPort || 3321}/page/${body.entry}`, error: '生成失败' })
} }
}
}) })
server.listen({ // 获取系统状态
port: Config.serverPort || 3321, server.post('/system-statistics', async (request, reply) => {
host: '0.0.0.0' Statistics.SystemLoad.count = await getLoad()
}, (error) => { reply.send(Statistics)
if (error) { })
console.error(error);
server.post('/sysconfig', async (request, reply) => {
const token = request.cookies.token || 'unknown'
if (token != usertoken) {
reply.send({err: '未登录'})
} else {
let redisConfig = {}
if (await redis.exists('CHATGPT:BING_TOKENS') != 0) {
let bingTokens = await redis.get('CHATGPT:BING_TOKENS')
if (bingTokens)
bingTokens = JSON.parse(bingTokens)
else bingTokens = []
redisConfig.bingTokens = bingTokens
} else {
redisConfig.bingTokens = []
} }
server.log.info(`server listening on ${server.server.address().port}`) if (await redis.exists('CHATGPT:CONFIRM') != 0) {
redisConfig.turnConfirm = await redis.get('CHATGPT:CONFIRM') === 'on'
}
reply.send({
chatConfig: Config,
redisConfig: redisConfig
})
}
})
server.post('/saveconfig', async (request, reply) => {
const token = request.cookies.token || 'unknown'
if (token != usertoken) {
reply.send({err: '未登录'})
} else {
const body = request.body || {}
const chatdata = body.chatConfig || {}
for (let [keyPath, value] of Object.entries(chatdata)) {
if (keyPath === 'blockWords' || keyPath === 'promptBlockWords' || keyPath === 'initiativeChatGroups') { value = value.toString().split(/[,;\|]/) }
if (Config[keyPath] != value) { Config[keyPath] = value }
}
const redisConfig = body.redisConfig || {}
if (redisConfig.bingTokens != null) {
await redis.set('CHATGPT:BING_TOKENS', JSON.stringify(redisConfig.bingTokens))
}
if (redisConfig.turnConfirm != null) {
await redis.set('CHATGPT:CONFIRM', redisConfig.turnConfirm ? 'on' : 'off')
}
}
})
server.addHook('onRequest', (request, reply, done) => {
if(request.method == 'POST')
Statistics.SystemAccess.count += 1
if(request.method == 'GET')
Statistics.WebAccess.count += 1
done()
})
//定时任务
var rule = new schedule.RecurrenceRule();
rule.hour = 0;
rule.minute = 0;
let job_Statistics = schedule.scheduleJob(rule, function() {
Statistics.SystemAccess.oldCount = Statistics.SystemAccess.count
Statistics.CacheFile.oldCount = Statistics.CacheFile.count
Statistics.WebAccess.oldCount = Statistics.WebAccess.count
Statistics.SystemAccess.count = 0
Statistics.CacheFile.count = 0
Statistics.WebAccess.count = 0
});
let job_Statistics_SystemLoad = schedule.scheduleJob('0 * * * *', async function(){
Statistics.SystemLoad.count = await getLoad()
Statistics.SystemLoad.oldCount = Statistics.SystemLoad.count
});
server.listen({
port: Config.serverPort || 3321,
host: '0.0.0.0'
}, (error) => {
if (error) {
console.error(error)
}
server.log.info(`server listening on ${server.server.address().port}`)
}) })
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

View file

@ -17,4 +17,4 @@
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
--> -->
<!doctype html><html lang="en"><head><meta charset="utf-8"/><meta http-equiv="X-UA-Compatible" content="IE=edge"/><meta name="viewport" content="width=device-width,initial-scale=1"/><link rel="shortcut icon" href="/favicon.ico"/><link rel="apple-touch-icon" sizes="76x76" href="/apple-icon.png"/><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/katex.min.css"/><script src="https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/katex.min.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/mermaid/8.6.3/mermaid.min.js"></script><title>ChatGPT-Plugin</title><script defer="defer" src="/js/chunk-vendors.bc8c7ae8.js"></script><script defer="defer" src="/js/app.4195158e.js"></script><link href="/css/chunk-vendors.f10f650e.css" rel="stylesheet"><link href="/css/app.6561b27a.css" rel="stylesheet"></head><body class="text-blueGray-700 antialiased"><noscript><strong>We're sorry but vue-notus doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div></body></html> <!doctype html><html lang="en"><head><meta charset="utf-8"/><meta http-equiv="X-UA-Compatible" content="IE=edge"/><meta name="viewport" content="width=device-width,initial-scale=1"/><link rel="shortcut icon" href="/favicon.ico"/><link rel="apple-touch-icon" sizes="76x76" href="/apple-icon.png"/><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/katex.min.css"/><script src="https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/katex.min.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/mermaid/8.6.3/mermaid.min.js"></script><title>ChatGPT-Plugin</title><script defer="defer" src="/js/chunk-vendors.ea19a27f.js"></script><script defer="defer" src="/js/app.d8378ba7.js"></script><link href="/css/chunk-vendors.f10f650e.css" rel="stylesheet"><link href="/css/app.6561b27a.css" rel="stylesheet"></head><body class="text-blueGray-700 antialiased"><noscript><strong>We're sorry but vue-notus doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -13,6 +13,8 @@ import { Config } from './config.js'
// .processSync(markdown ?? '') // .processSync(markdown ?? '')
// .toString() // .toString()
// } // }
let localIP = ''
export function escapeHtml (str) { export function escapeHtml (str) {
const htmlEntities = { const htmlEntities = {
'&': '&amp;', '&': '&amp;',
@ -588,6 +590,20 @@ export async function isImage(link) {
let magic = buf.toString('hex', 0, 4) let magic = buf.toString('hex', 0, 4)
return ['ffd8', '8950', '4749'].includes(magic) return ['ffd8', '8950', '4749'].includes(magic)
} catch (error) { } catch (error) {
throw error return false
} }
}
export async function getPublicIP() {
try {
if (localIP === '') {
const res = await fetch('https://api.ipify.org?format=json')
const data = await res.json()
localIP = data.ip
}
return localIP
} catch (err) {
return '127.0.0.1'
}
} }

View file

@ -85,7 +85,7 @@ const defaultConfig = {
viewHost: '', viewHost: '',
chatViewWidth: 1280, chatViewWidth: 1280,
chatViewBotName: '', chatViewBotName: '',
version: 'v2.4.13' version: 'v2.4.14'
} }
const _path = process.cwd() const _path = process.cwd()
let config = {} let config = {}