添加用户管理功能 (#345)

* 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

* 删除旧依赖

* 优化用户系统

* 修复登录数据错误

* 修复用户密码创建失败问题

* 限制密码私聊发送

* 修复数据获取异常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-13 22:47:20 +08:00 committed by GitHub
parent 4b29e261a0
commit 9b9b69634e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 249 additions and 64 deletions

View file

@ -1023,7 +1023,8 @@ export class chatgpt extends plugin {
entry: cacheData.file,
userImg: `https://q1.qlogo.cn/g?b=qq&s=0&nk=${e.sender.user_id}`,
botImg: `https://q1.qlogo.cn/g?b=qq&s=0&nk=${Bot.uin}`,
cacheHost: Config.serverHost
cacheHost: Config.serverHost,
qq: e.sender.user_id
})
}
const viewHost = Config.viewHost ? `${Config.viewHost}/` : `http://127.0.0.1:${Config.serverPort || 3321}/`

View file

@ -4,6 +4,10 @@ import { exec } from 'child_process'
import { checkPnpm, formatDuration, parseDuration, getPublicIP } from '../utils/common.js'
import SydneyAIClient from '../utils/SydneyAIClient.js'
import { convertSpeaker, speakers } from '../utils/tts.js'
import md5 from 'md5'
import path from 'path'
import fs from 'fs'
let isWhiteList = true
export class ChatgptManagement extends plugin {
constructor (e) {
@ -173,10 +177,18 @@ export class ChatgptManagement extends plugin {
fnc: 'setAdminPassword',
permission: 'master'
},
{
reg: '^#(设置|修改)用户密码',
fnc: 'setUserPassword',
},
{
reg: '^#chatgpt系统(设置|配置|管理)',
fnc: 'adminPage',
permission: 'master'
},
{
reg: '^#chatgpt用户(设置|配置|管理)',
fnc: 'userPage',
}
]
})
@ -441,6 +453,7 @@ export class ChatgptManagement extends plugin {
`${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)
if (tokens.length == 0) this.finish('saveBingToken')
return false
}
@ -916,22 +929,77 @@ export class ChatgptManagement extends plugin {
}
async setAdminPassword (e) {
if (e.isGroup) {
await this.reply('请私聊发生命令', true)
return true
}
this.setContext('saveAdminPassword')
await this.reply('请发送系统管理密码', true)
return false
}
async setUserPassword (e) {
if (e.isGroup) {
await this.reply('请私聊发生命令', true)
return true
}
this.setContext('saveUserPassword')
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)
const passwd = this.e.msg
await redis.set('CHATGPT:ADMIN_PASSWD', md5(passwd))
await this.reply('设置成功', true)
this.finish('saveAdminPassword')
}
async saveUserPassword (e) {
if (!this.e.msg) return
const passwd = this.e.msg
const dir = 'resources/ChatGPTCache/user'
const filename = `${this.e.user_id}.json`
const filepath = path.join(dir, filename)
fs.mkdirSync(dir, { recursive: true })
if (fs.existsSync(filepath)) {
fs.readFile(filepath, 'utf8', (err, data) => {
if (err) {
console.error(err)
return
}
const config = JSON.parse(data)
config.passwd = md5(passwd)
fs.writeFile(filepath, JSON.stringify(config), 'utf8', (err) => {
if (err) {
console.error(err)
return
}
})
})
} else {
fs.writeFile(filepath, JSON.stringify({
user: this.e.user_id,
passwd: md5(passwd),
chat: []
}), 'utf8', (err) => {
if (err) {
console.error(err)
return
}
})
}
await this.reply('设置完成', true)
this.finish('saveUserPassword')
}
async adminPage (e) {
const viewHost = Config.serverHost ? `http://${Config.serverHost}/` : `http://${await getPublicIP()}:${Config.serverPort || 3321}/`
await this.reply(`请登录${viewHost + 'admin/settings'}进行系统配置`, true)
}
async userPage (e) {
const viewHost = Config.serverHost ? `http://${Config.serverHost}/` : `http://${await getPublicIP()}:${Config.serverPort || 3321}/`
await this.reply(`请登录${viewHost + 'admin/dashboard'}进行系统配置`, true)
}
}

View file

@ -28,15 +28,6 @@ for (let i in files) {
apps[name] = ret[i].value[Object.keys(ret[i].value)[0]]
}
try {
await import('fastify')
await import('fastify-cookie')
await import('@fastify/cors')
await import('@fastify/static')
await import('os-utils')
} catch (err) {
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()

View file

@ -17,7 +17,7 @@ const server = fastify({
logger: Config.debug
})
let usertoken = ''
let usertoken = []
let Statistics = {
SystemAccess: {
count: 0,
@ -53,6 +53,30 @@ async function getLoad() {
}
}
async function getUserData(qq) {
const dir = 'resources/ChatGPTCache/user'
const filename = `${qq}.json`
const filepath = path.join(dir, filename)
try {
let data = fs.readFileSync(filepath, 'utf8')
return JSON.parse(data)
} catch (error) {
return {
user: qq,
passwd: '',
chat: []
}
}
}
async function setUserData(qq, data) {
const dir = 'resources/ChatGPTCache/user'
const filename = `${qq}.json`
const filepath = path.join(dir, filename)
fs.mkdirSync(dir, { recursive: true })
fs.writeFileSync(filepath, JSON.stringify(data))
}
export async function createServer() {
await server.register(cors, {
origin: '*',
@ -75,22 +99,52 @@ export async function createServer() {
})
await server.get('/admin/*', (request, reply) => {
const token = request.cookies.token || 'unknown'
if (token != usertoken) {
const user = usertoken.find(user => user.token === token)
if (!user) {
reply.redirect(301, '/auth/login')
}
const stream = fs.createReadStream('plugins/chatgpt-plugin/server/static/index.html')
reply.type('text/html').send(stream)
})
await server.get('/admin/dashboard', (request, reply) => {
const token = request.cookies.token || 'unknown'
const user = usertoken.find(user => user.token === token)
if (!user) {
reply.redirect(301, '/auth/login')
}
if (user.autho === 'admin') {
reply.redirect(301, '/admin/settings')
}
const stream = fs.createReadStream('plugins/chatgpt-plugin/server/static/index.html')
reply.type('text/html').send(stream)
})
await server.get('/admin/settings', (request, reply) => {
const token = request.cookies.token || 'unknown'
const user = usertoken.find(user => user.token === token)
if (!user || user.autho != 'admin') {
reply.redirect(301, '/admin/')
}
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) {
const token = randomString(32)
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})
usertoken.push({user: body.qq, token: token, autho: 'admin'})
reply.setCookie('token', token, {path: '/'})
reply.send({login:true, autho: 'admin'})
} else {
reply.send({login:false,err:'用户名密码错误'})
const user = await getUserData(body.qq)
if (user.passwd != '' && user.passwd === body.passwd) {
usertoken.push({user: body.qq, token: token, autho: 'user'})
reply.setCookie('token', token, {path: '/'})
reply.send({login: true, autho: 'user'})
} else {
reply.send({login:false,err:`用户名密码错误,如果忘记密码请私聊机器人输入 ${body.qq == Bot.uin ? '#修改管理密码' : '#修改用户密码'} 进行修改`})
}
}
} else {
reply.send({login:false,err:'未输入用户名或密码'})
@ -130,7 +184,7 @@ export async function createServer() {
const ip = await getPublicIP()
try {
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(filepath, JSON.stringify({
const data = {
user: body.content.senderName,
bot: Config.chatViewBotName || (body.bing ? 'Bing' : 'ChatGPT'),
userImg: body.userImg || '',
@ -142,13 +196,25 @@ export async function createServer() {
quote: body.content.quote,
images: body.content.images || [],
suggest: body.content.suggest || [],
model: body.bing ? 'Bing' : 'ChatGPT',
time: new Date()
}))
}
fs.writeFileSync(filepath, JSON.stringify(data))
const user = await getUserData(body.qq)
user.chat.push({
user: data.user,
bot: data.bot,
group: data.group,
herf: data.herf,
model: data.model,
time: data.time,
})
await setUserData(body.qq, user)
Statistics.CacheFile.count += 1
reply.send({ file: body.entry, cacheUrl: `http://${ip}:${Config.serverPort || 3321}/page/${body.entry}` })
} catch (err) {
console.error(err)
reply.send({ file: body.entry, cacheUrl: `http://${ip}:${Config.serverPort || 3321}/page/${body.entry}`, error: '生成失败' })
server.log.error(`用户生成缓存${body.entry}时发生错误: ${err}`)
reply.send({ file: body.entry, cacheUrl: `http://${ip}:${Config.serverPort || 3321}/page/${body.entry}`, error: body.entry + '生成失败' })
}
}
})
@ -158,11 +224,39 @@ export async function createServer() {
reply.send(Statistics)
})
// 获取用户数据
server.post('/userData', async (request, reply) => {
const token = request.cookies.token || 'unknown'
let user = usertoken.find(user => user.token === token)
if (!user) user = {user: ''}
const userData = await getUserData(user.user)
reply.send(userData.chat)
})
//清除缓存数据
server.post('/cleanCache', async (request, reply) => {
const token = request.cookies.token || 'unknown'
let user = usertoken.find(user => user.token === token)
if (!user) user = {user: ''}
const userData = await getUserData(user.user)
const dir = 'resources/ChatGPTCache/page'
userData.chat.forEach(function (item, index) {
const filename = item.herf.substring(item.herf.lastIndexOf("/") + 1) + '.json'
const filepath = path.join(dir, filename)
fs.unlinkSync(filepath)
})
userData.chat = []
await setUserData(user.user, userData)
reply.send({state: true})
})
// 获取系统参数
server.post('/sysconfig', async (request, reply) => {
const token = request.cookies.token || 'unknown'
if (token != usertoken) {
const user = usertoken.find(user => user.token === token)
if (!user) {
reply.send({err: '未登录'})
} else {
} else if(user.autho === 'admin') {
let redisConfig = {}
if (await redis.exists('CHATGPT:BING_TOKENS') != 0) {
let bingTokens = await redis.get('CHATGPT:BING_TOKENS')
@ -180,14 +274,31 @@ export async function createServer() {
chatConfig: Config,
redisConfig: redisConfig
})
} else {
let userSetting = await redis.get(`CHATGPT:USER:${user.user}`)
if (!userSetting) {
userSetting = {
usePicture: Config.defaultUsePicture,
useTTS: Config.defaultUseTTS,
ttsRole: Config.defaultTTSRole
}
} else {
userSetting = JSON.parse(userSetting)
}
reply.send({
userSetting: userSetting
})
}
})
// 设置系统参数
server.post('/saveconfig', async (request, reply) => {
const token = request.cookies.token || 'unknown'
if (token != usertoken) {
reply.send({err: '未登录'})
} else {
const user = usertoken.find(user => user.token === token)
const body = request.body || {}
if (!user) {
reply.send({err: '未登录'})
} else if(user.autho === 'admin') {
const chatdata = body.chatConfig || {}
for (let [keyPath, value] of Object.entries(chatdata)) {
if (keyPath === 'blockWords' || keyPath === 'promptBlockWords' || keyPath === 'initiativeChatGroups') { value = value.toString().split(/[,;\|]/) }
@ -200,6 +311,10 @@ export async function createServer() {
if (redisConfig.turnConfirm != null) {
await redis.set('CHATGPT:CONFIRM', redisConfig.turnConfirm ? 'on' : 'off')
}
} else {
if (body.userSetting){
await redis.set(`CHATGPT:USER:${user.user}`, JSON.stringify(body.userSetting))
}
}
})
@ -233,8 +348,9 @@ export async function createServer() {
host: '::'
}, (error) => {
if (error) {
console.error(error)
}
server.log.error(`服务启动失败: ${error}`)
} else {
server.log.info(`server listening on ${server.server.address().port}`)
}
})
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 260 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Before

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.
-->
<!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>
<!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.875c3adb.js"></script><script defer="defer" src="/js/app.258abc47.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