diff --git a/README.en.md b/README.en.md
index 031b8c2d95..a2167d065d 100644
--- a/README.en.md
+++ b/README.en.md
@@ -21,6 +21,8 @@
[✓] Users manager
+[✓] Random Key
+
## Screenshots
@@ -32,6 +34,7 @@
![cover3](./docs/basesettings.jpg)
![cover3](./docs/prompt_en.jpg)
![cover3](./docs/user-manager.jpg)
+![cover3](./docs/key-manager-en.jpg)
- [ChatGPT Web](#chatgpt-web)
- [Introduction](#introduction)
diff --git a/README.md b/README.md
index b36eb73cf4..3caf2d042a 100644
--- a/README.md
+++ b/README.md
@@ -20,6 +20,8 @@
[✓] 每个会话设置独有 Prompt
[✓] 用户管理
+
+[✓] 多 Key 随机
## 截图
@@ -31,6 +33,7 @@
![cover3](./docs/basesettings.jpg)
![cover3](./docs/prompt.jpg)
![cover3](./docs/user-manager.jpg)
+![cover3](./docs/key-manager.jpg)
- [ChatGPT Web](#chatgpt-web)
- [介绍](#介绍)
@@ -410,7 +413,18 @@ A: 一种可能原因是经过 Nginx 反向代理,开启了 buffer,则 Nginx
## 赞助
-如果你觉得这个项目对你有帮助,请给我点个Star。
+如果你觉得这个项目对你有帮助,请给我点个Star。并且情况允许的话,可以给我一点点支持,总之非常感谢支持~
+
+
+
+
+
WeChat Pay
+
+
+
+
Alipay
+
+
## License
MIT © [Kerwin1202](./license)
diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml
index da14bc351f..904f6717d8 100644
--- a/docker-compose/docker-compose.yml
+++ b/docker-compose/docker-compose.yml
@@ -11,16 +11,6 @@ services:
- database
environment:
TZ: Asia/Shanghai
- # 二选一
- OPENAI_API_KEY:
- # 二选一
- OPENAI_ACCESS_TOKEN:
- # API接口地址,可选,设置 OPENAI_API_KEY 时可用
- OPENAI_API_BASE_URL:
- # ChatGPTAPI 或者 ChatGPTUnofficialProxyAPI
- OPENAI_API_MODEL:
- # 反向代理,可选
- API_REVERSE_PROXY:
# 访问jwt加密参数,可选 不为空则允许登录 同时需要设置 MONGODB_URL
AUTH_SECRET_KEY:
# 每小时最大请求次数,可选,默认无限
@@ -35,8 +25,6 @@ services:
SOCKS_PROXY_USERNAME:
# Socks代理密码,可选,和 SOCKS_PROXY_HOST & SOCKS_PROXY_PORT 一起时生效
SOCKS_PROXY_PASSWORD:
- # HTTPS_PROXY 代理,可选
- HTTPS_PROXY: http://xxxx:7890
# 网站名称
SITE_TITLE: ChatGpt Web
# mongodb 的连接字符串
diff --git a/docs/alipay.png b/docs/alipay.png
new file mode 100644
index 0000000000..bb5bbca7a8
Binary files /dev/null and b/docs/alipay.png differ
diff --git a/docs/key-manager-en.jpg b/docs/key-manager-en.jpg
new file mode 100644
index 0000000000..cc23811422
Binary files /dev/null and b/docs/key-manager-en.jpg differ
diff --git a/docs/key-manager.jpg b/docs/key-manager.jpg
new file mode 100644
index 0000000000..8c2edc6532
Binary files /dev/null and b/docs/key-manager.jpg differ
diff --git a/docs/wechat.png b/docs/wechat.png
new file mode 100644
index 0000000000..92bc6aa079
Binary files /dev/null and b/docs/wechat.png differ
diff --git a/package.json b/package.json
index cfb33b1a53..d3e10d1867 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "chatgpt-web",
- "version": "2.12.4",
+ "version": "2.13.0",
"private": false,
"description": "ChatGPT Web",
"author": "ChenZhaoYu ",
diff --git a/service/src/chatgpt/index.ts b/service/src/chatgpt/index.ts
index ed6a4f207c..ae7cb05dc9 100644
--- a/service/src/chatgpt/index.ts
+++ b/service/src/chatgpt/index.ts
@@ -5,14 +5,14 @@ import { ChatGPTAPI, ChatGPTUnofficialProxyAPI } from 'chatgpt'
import { SocksProxyAgent } from 'socks-proxy-agent'
import httpsProxyAgent from 'https-proxy-agent'
import fetch from 'node-fetch'
-import type { AuditConfig, CHATMODEL } from 'src/storage/model'
+import type { AuditConfig, CHATMODEL, KeyConfig, UserInfo } from 'src/storage/model'
import jwt_decode from 'jwt-decode'
import dayjs from 'dayjs'
import type { TextAuditService } from '../utils/textAudit'
import { textAuditServices } from '../utils/textAudit'
-import { getCacheConfig, getOriginConfig } from '../storage/config'
+import { getCacheApiKeys, getCacheConfig, getOriginConfig } from '../storage/config'
import { sendResponse } from '../utils'
-import { isNotEmptyString } from '../utils/is'
+import { hasAnyRole, isNotEmptyString } from '../utils/is'
import type { ChatContext, ChatGPTUnofficialProxyAPIOptions, JWT, ModelConfig } from '../types'
import { getChatByMessageId } from '../storage/mongo'
import type { RequestOptions } from './types'
@@ -32,20 +32,17 @@ const ErrorCodeMessage: Record = {
let auditService: TextAuditService
-export async function initApi(chatModel: CHATMODEL) {
+export async function initApi(key: KeyConfig, chatModel: CHATMODEL) {
// More Info: https://github.com/transitive-bullshit/chatgpt-api
const config = await getCacheConfig()
- if (!config.apiKey && !config.accessToken)
- throw new Error('Missing OPENAI_API_KEY or OPENAI_ACCESS_TOKEN environment variable')
-
const model = chatModel as string
- if (config.apiModel === 'ChatGPTAPI') {
+ if (key.keyModel === 'ChatGPTAPI') {
const OPENAI_API_BASE_URL = config.apiBaseUrl
const options: ChatGPTAPIOptions = {
- apiKey: config.apiKey,
+ apiKey: key.key,
completionParams: { model },
debug: !config.apiDisableDebug,
messageStore: undefined,
@@ -73,7 +70,7 @@ export async function initApi(chatModel: CHATMODEL) {
}
else {
const options: ChatGPTUnofficialProxyAPIOptions = {
- accessToken: config.accessToken,
+ accessToken: key.key,
apiReverseProxyUrl: isNotEmptyString(config.reverseProxy) ? config.reverseProxy : 'https://ai.fakeopen.com/api/conversation',
model,
debug: !config.apiDisableDebug,
@@ -86,27 +83,30 @@ export async function initApi(chatModel: CHATMODEL) {
}
async function chatReplyProcess(options: RequestOptions) {
- const config = await getCacheConfig()
const model = options.chatModel
+ const key = options.key
+ if (key == null || key === undefined)
+ throw new Error('没有可用的配置。请再试一次 | No available configuration. Please try again.')
+
const { message, lastContext, process, systemMessage, temperature, top_p } = options
try {
const timeoutMs = (await getCacheConfig()).timeoutMs
let options: SendMessageOptions = { timeoutMs }
- if (config.apiModel === 'ChatGPTAPI') {
+ if (key.keyModel === 'ChatGPTAPI') {
if (isNotEmptyString(systemMessage))
options.systemMessage = systemMessage
options.completionParams = { model, temperature, top_p }
}
if (lastContext != null) {
- if (config.apiModel === 'ChatGPTAPI')
+ if (key.keyModel === 'ChatGPTAPI')
options.parentMessageId = lastContext.parentMessageId
else
options = { ...lastContext }
}
- const api = await initApi(model)
+ const api = await initApi(key, model)
const response = await api.sendMessage(message, {
...options,
onProgress: (partialResponse) => {
@@ -123,6 +123,9 @@ async function chatReplyProcess(options: RequestOptions) {
return sendResponse({ type: 'Fail', message: ErrorCodeMessage[code] })
return sendResponse({ type: 'Fail', message: error.message ?? 'Please check the back-end console' })
}
+ finally {
+ releaseApiKey(key)
+ }
}
export function initAuditService(audit: AuditConfig) {
@@ -304,6 +307,39 @@ async function getMessageById(id: string): Promise {
else { return undefined }
}
+const _lockedKeys: string[] = []
+async function randomKeyConfig(keys: KeyConfig[]): Promise < KeyConfig | null > {
+ if (keys.length <= 0)
+ return null
+ let unsedKeys = keys.filter(d => !_lockedKeys.includes(d.key))
+ const start = Date.now()
+ while (unsedKeys.length <= 0) {
+ if (Date.now() - start > 3000)
+ break
+ await new Promise(resolve => setTimeout(resolve, 1000))
+ unsedKeys = keys.filter(d => !_lockedKeys.includes(d.key))
+ }
+ if (unsedKeys.length <= 0)
+ return null
+ const thisKey = unsedKeys[Math.floor(Math.random() * unsedKeys.length)]
+ _lockedKeys.push(thisKey.key)
+ return thisKey
+}
+
+async function getRandomApiKey(user: UserInfo): Promise {
+ const keys = (await getCacheApiKeys()).filter(d => hasAnyRole(d.userRoles, user.roles))
+ return randomKeyConfig(keys)
+}
+
+async function releaseApiKey(key: KeyConfig) {
+ if (key == null || key === undefined)
+ return
+
+ const index = _lockedKeys.indexOf(key.key)
+ if (index >= 0)
+ _lockedKeys.splice(index, 1)
+}
+
export type { ChatContext, ChatMessage }
-export { chatReplyProcess, chatConfig, containsSensitiveWords }
+export { chatReplyProcess, chatConfig, containsSensitiveWords, getRandomApiKey }
diff --git a/service/src/chatgpt/types.ts b/service/src/chatgpt/types.ts
index b30b87147e..e9d193af36 100644
--- a/service/src/chatgpt/types.ts
+++ b/service/src/chatgpt/types.ts
@@ -1,5 +1,5 @@
import type { ChatMessage } from 'chatgpt'
-import type { CHATMODEL } from 'src/storage/model'
+import type { CHATMODEL, KeyConfig } from 'src/storage/model'
export interface RequestOptions {
message: string
@@ -9,6 +9,7 @@ export interface RequestOptions {
temperature?: number
top_p?: number
chatModel: CHATMODEL
+ key: KeyConfig
}
export interface BalanceResponse {
diff --git a/service/src/index.ts b/service/src/index.ts
index b5a99ff358..b3cc788c26 100644
--- a/service/src/index.ts
+++ b/service/src/index.ts
@@ -4,11 +4,11 @@ import * as dotenv from 'dotenv'
import { ObjectId } from 'mongodb'
import type { RequestProps } from './types'
import type { ChatContext, ChatMessage } from './chatgpt'
-import { chatConfig, chatReplyProcess, containsSensitiveWords, initAuditService } from './chatgpt'
+import { chatConfig, chatReplyProcess, containsSensitiveWords, getRandomApiKey, initAuditService } from './chatgpt'
import { auth } from './middleware/auth'
-import { clearConfigCache, getCacheConfig, getOriginConfig } from './storage/config'
-import type { AuditConfig, CHATMODEL, ChatInfo, ChatOptions, Config, MailConfig, SiteConfig, UsageResponse, UserInfo } from './storage/model'
-import { Status } from './storage/model'
+import { clearConfigCache, getApiKeys, getCacheConfig, getOriginConfig } from './storage/config'
+import type { AuditConfig, CHATMODEL, ChatInfo, ChatOptions, Config, KeyConfig, MailConfig, SiteConfig, UsageResponse, UserInfo } from './storage/model'
+import { Status, UserRole } from './storage/model'
import {
clearChat,
createChatRoom,
@@ -28,6 +28,7 @@ import {
insertChat,
insertChatUsage,
renameChatRoom,
+ updateApiKeyStatus,
updateChat,
updateConfig,
updateRoomPrompt,
@@ -36,6 +37,7 @@ import {
updateUserInfo,
updateUserPassword,
updateUserStatus,
+ upsertKey,
verifyUser,
} from './storage/mongo'
import { limiter } from './middleware/limiter'
@@ -390,7 +392,7 @@ router.post('/chat-process', [auth, limiter], async (req, res) => {
const userId = req.headers.userId.toString()
const user = await getUserById(userId)
if (config.auditConfig.enabled || config.auditConfig.customizeEnabled) {
- if (user.email.toLowerCase() !== process.env.ROOT_USER && await containsSensitiveWords(config.auditConfig, prompt)) {
+ if (!user.roles.includes(UserRole.Admin) && await containsSensitiveWords(config.auditConfig, prompt)) {
res.send({ status: 'Fail', message: '含有敏感词 | Contains sensitive words', data: null })
return
}
@@ -427,12 +429,13 @@ router.post('/chat-process', [auth, limiter], async (req, res) => {
temperature,
top_p,
chatModel: user.config.chatModel,
+ key: await getRandomApiKey(user),
})
// return the whole response including usage
res.write(`\n${JSON.stringify(result.data)}`)
}
catch (error) {
- res.write(JSON.stringify(error))
+ res.write(JSON.stringify({ message: error?.message }))
}
finally {
res.end()
@@ -516,9 +519,10 @@ router.post('/user-register', async (req, res) => {
return
}
const newPassword = md5(password)
- await createUser(username, newPassword)
+ const isRoot = username.toLowerCase() === process.env.ROOT_USER
+ await createUser(username, newPassword, isRoot)
- if (username.toLowerCase() === process.env.ROOT_USER) {
+ if (isRoot) {
res.send({ status: 'Success', message: '注册成功 | Register success', data: null })
}
else {
@@ -536,7 +540,7 @@ router.post('/config', rootAuth, async (req, res) => {
const userId = req.headers.userId.toString()
const user = await getUserById(userId)
- if (user == null || user.status !== Status.Normal || user.email.toLowerCase() !== process.env.ROOT_USER)
+ if (user == null || user.status !== Status.Normal || !user.roles.includes(UserRole.Admin))
throw new Error('无权限 | No permission.')
const response = await chatConfig()
@@ -584,7 +588,7 @@ router.post('/user-login', async (req, res) => {
avatar: user.avatar,
description: user.description,
userId: user._id,
- root: username.toLowerCase() === process.env.ROOT_USER,
+ root: !user.roles.includes(UserRole.Admin),
config: user.config,
}, config.siteConfig.loginSalt.trim())
res.send({ status: 'Success', message: '登录成功 | Login successfully', data: { token } })
@@ -678,7 +682,10 @@ router.get('/users', rootAuth, async (req, res) => {
router.post('/user-status', rootAuth, async (req, res) => {
try {
const { userId, status } = req.body as { userId: string; status: Status }
+ const user = await getUserById(userId)
await updateUserStatus(userId, status)
+ if ((user.status === Status.PreVerify || user.status === Status.AdminVerify) && status === Status.Normal)
+ await sendNoticeMail(user.email)
res.send({ status: 'Success', message: '更新成功 | Update successfully' })
}
catch (error) {
@@ -839,6 +846,40 @@ router.post('/audit-test', rootAuth, async (req, res) => {
}
})
+router.get('/setting-keys', rootAuth, async (req, res) => {
+ try {
+ const result = await getApiKeys()
+ res.send({ status: 'Success', message: null, data: result })
+ }
+ catch (error) {
+ res.send({ status: 'Fail', message: error.message, data: null })
+ }
+})
+
+router.post('/setting-key-status', rootAuth, async (req, res) => {
+ try {
+ const { id, status } = req.body as { id: string; status: Status }
+ await updateApiKeyStatus(id, status)
+ res.send({ status: 'Success', message: '更新成功 | Update successfully' })
+ }
+ catch (error) {
+ res.send({ status: 'Fail', message: error.message, data: null })
+ }
+})
+
+router.post('/setting-key-upsert', rootAuth, async (req, res) => {
+ try {
+ const keyConfig = req.body as KeyConfig
+ if (keyConfig._id !== undefined)
+ keyConfig._id = new ObjectId(keyConfig._id)
+ await upsertKey(keyConfig)
+ res.send({ status: 'Success', message: '成功 | Successfully' })
+ }
+ catch (error) {
+ res.send({ status: 'Fail', message: error.message, data: null })
+ }
+})
+
router.post('/statistics/by-day', auth, async (req, res) => {
try {
const userId = req.headers.userId
diff --git a/service/src/middleware/rootAuth.ts b/service/src/middleware/rootAuth.ts
index d478e4bde0..e308a861d0 100644
--- a/service/src/middleware/rootAuth.ts
+++ b/service/src/middleware/rootAuth.ts
@@ -1,6 +1,6 @@
import jwt from 'jsonwebtoken'
import * as dotenv from 'dotenv'
-import { Status } from '../storage/model'
+import { Status, UserRole } from '../storage/model'
import { getUserById } from '../storage/mongo'
import { getCacheConfig } from '../storage/config'
@@ -14,7 +14,7 @@ const rootAuth = async (req, res, next) => {
const info = jwt.verify(token, config.siteConfig.loginSalt.trim())
req.headers.userId = info.userId
const user = await getUserById(info.userId)
- if (user == null || user.status !== Status.Normal || user.email.toLowerCase() !== process.env.ROOT_USER)
+ if (user == null || user.status !== Status.Normal || !user.roles.includes(UserRole.Admin))
res.send({ status: 'Fail', message: '无权限 | No permission.', data: null })
else
next()
diff --git a/service/src/storage/config.ts b/service/src/storage/config.ts
index 636de25905..1ee488a7f9 100644
--- a/service/src/storage/config.ts
+++ b/service/src/storage/config.ts
@@ -2,8 +2,8 @@ import { ObjectId } from 'mongodb'
import * as dotenv from 'dotenv'
import type { TextAuditServiceProvider } from 'src/utils/textAudit'
import { isNotEmptyString, isTextAuditServiceProvider } from '../utils/is'
-import { AuditConfig, Config, MailConfig, SiteConfig, TextAudioType } from './model'
-import { getConfig } from './mongo'
+import { AuditConfig, CHATMODELS, Config, KeyConfig, MailConfig, SiteConfig, TextAudioType, UserRole } from './model'
+import { getConfig, getKeys, upsertKey } from './mongo'
dotenv.config()
@@ -116,3 +116,46 @@ export function clearConfigCache() {
cacheExpiration = 0
cachedConfig = null
}
+
+let apiKeysCachedConfig: KeyConfig[] | undefined
+let apiKeysCacheExpiration = 0
+
+export async function getCacheApiKeys(): Promise {
+ const now = Date.now()
+ if (apiKeysCachedConfig && apiKeysCacheExpiration > now)
+ return Promise.resolve(apiKeysCachedConfig)
+
+ const loadedConfig = (await getApiKeys()).keys
+
+ apiKeysCachedConfig = loadedConfig
+ apiKeysCacheExpiration = now + 10 * 60 * 1000
+
+ return Promise.resolve(apiKeysCachedConfig)
+}
+
+export async function getApiKeys() {
+ const result = await getKeys()
+ if (result.keys.length <= 0) {
+ const config = await getCacheConfig()
+ if (config.apiModel === 'ChatGPTAPI')
+ result.keys.push(await upsertKey(new KeyConfig(config.apiKey, 'ChatGPTAPI', [], [], '')))
+
+ if (config.apiModel === 'ChatGPTUnofficialProxyAPI')
+ result.keys.push(await upsertKey(new KeyConfig(config.accessToken, 'ChatGPTUnofficialProxyAPI', [], [], '')))
+
+ result.total++
+ }
+ result.keys.forEach((key) => {
+ if (key.userRoles == null || key.userRoles.length <= 0) {
+ key.userRoles.push(UserRole.Admin)
+ key.userRoles.push(UserRole.User)
+ key.userRoles.push(UserRole.Guest)
+ }
+ if (key.chatModels == null || key.chatModels.length <= 0) {
+ CHATMODELS.forEach((chatModel) => {
+ key.chatModels.push(chatModel)
+ })
+ }
+ })
+ return result
+}
diff --git a/service/src/storage/model.ts b/service/src/storage/model.ts
index 0e507592ed..5660994320 100644
--- a/service/src/storage/model.ts
+++ b/service/src/storage/model.ts
@@ -8,6 +8,13 @@ export enum Status {
ResponseDeleted = 3,
PreVerify = 4,
AdminVerify = 5,
+ Disabled = 6,
+}
+
+export enum UserRole {
+ Admin = 0,
+ User = 1,
+ Guest = 2,
}
export class UserInfo {
@@ -22,7 +29,7 @@ export class UserInfo {
description?: string
updateTime?: string
config?: UserConfig
- root?: boolean
+ roles?: UserRole[]
constructor(email: string, password: string) {
this.name = email
this.email = email
@@ -31,7 +38,7 @@ export class UserInfo {
this.createTime = new Date().toLocaleString()
this.verifyTime = null
this.updateTime = new Date().toLocaleString()
- this.root = false
+ this.roles = [UserRole.User]
}
}
@@ -42,6 +49,18 @@ export class UserConfig {
// https://platform.openai.com/docs/models/overview
export type CHATMODEL = 'gpt-3.5-turbo' | 'gpt-3.5-turbo-0301' | 'gpt-4' | 'gpt-4-0314' | 'gpt-4-32k' | 'gpt-4-32k-0314' | 'ext-davinci-002-render-sha-mobile' | 'gpt-4-mobile' | 'gpt-4-browsing'
+export const CHATMODELS: CHATMODEL[] = [
+ 'gpt-3.5-turbo',
+ 'gpt-3.5-turbo-0301',
+ 'gpt-4',
+ 'gpt-4-0314',
+ 'gpt-4-32k',
+ 'gpt-4-32k-0314',
+ 'ext-davinci-002-render-sha-mobile',
+ 'gpt-4-mobile',
+ 'gpt-4-browsing',
+]
+
export class ChatRoom {
_id: ObjectId
roomId: number
@@ -139,7 +158,7 @@ export class Config {
public apiDisableDebug?: boolean,
public accessToken?: string,
public apiBaseUrl?: string,
- public apiModel?: string,
+ public apiModel?: APIMODEL,
public reverseProxy?: string,
public socksProxy?: string,
public socksAuth?: string,
@@ -189,3 +208,23 @@ export enum TextAudioType {
Response = 1 << 1, // 二进制 10
All = Request | Response, // 二进制 11
}
+
+export class KeyConfig {
+ _id: ObjectId
+ key: string
+ keyModel: APIMODEL
+ chatModels: CHATMODEL[]
+ userRoles: UserRole[]
+ status: Status
+ remark: string
+ constructor(key: string, keyModel: APIMODEL, chatModels: CHATMODEL[], userRoles: UserRole[], remark: string) {
+ this.key = key
+ this.keyModel = keyModel
+ this.chatModels = chatModels
+ this.userRoles = userRoles
+ this.status = Status.Normal
+ this.remark = remark
+ }
+}
+
+export type APIMODEL = 'ChatGPTAPI' | 'ChatGPTUnofficialProxyAPI'
diff --git a/service/src/storage/mongo.ts b/service/src/storage/mongo.ts
index ea9d576315..44c6c24bed 100644
--- a/service/src/storage/mongo.ts
+++ b/service/src/storage/mongo.ts
@@ -1,8 +1,8 @@
import { MongoClient, ObjectId } from 'mongodb'
import * as dotenv from 'dotenv'
import dayjs from 'dayjs'
-import { ChatInfo, ChatRoom, ChatUsage, Status, UserConfig, UserInfo } from './model'
-import type { CHATMODEL, ChatOptions, Config, UsageResponse } from './model'
+import { ChatInfo, ChatRoom, ChatUsage, Status, UserConfig, UserInfo, UserRole } from './model'
+import type { CHATMODEL, ChatOptions, Config, KeyConfig, UsageResponse } from './model'
dotenv.config()
@@ -15,6 +15,7 @@ const roomCol = client.db(dbName).collection('chat_room')
const userCol = client.db(dbName).collection('user')
const configCol = client.db(dbName).collection('config')
const usageCol = client.db(dbName).collection('chat_usage')
+const keyCol = client.db(dbName).collection('key_config')
/**
* 插入聊天信息
@@ -176,11 +177,13 @@ export async function deleteChat(roomId: number, uuid: number, inversion: boolea
await chatCol.updateOne(query, update)
}
-export async function createUser(email: string, password: string): Promise {
+export async function createUser(email: string, password: string, isRoot: boolean): Promise {
email = email.toLowerCase()
const userInfo = new UserInfo(email, password)
- if (email === process.env.ROOT_USER)
+ if (isRoot) {
userInfo.status = Status.Normal
+ userInfo.roles = [UserRole.Admin]
+ }
await userCol.insertOne(userInfo)
return userInfo
@@ -203,11 +206,13 @@ export async function updateUserPassword(userId: string, password: string) {
export async function getUser(email: string): Promise {
email = email.toLowerCase()
- return await userCol.findOne({ email }) as UserInfo
+ const userInfo = await userCol.findOne({ email }) as UserInfo
+ initUserInfo(userInfo)
+ return userInfo
}
export async function getUsers(page: number, size: number): Promise<{ users: UserInfo[]; total: number }> {
- const cursor = userCol.find({}).sort({ createTime: -1 })
+ const cursor = userCol.find({ status: { $ne: Status.Deleted } }).sort({ createTime: -1 })
const total = await cursor.count()
const skip = (page - 1) * size
const limit = size
@@ -215,20 +220,27 @@ export async function getUsers(page: number, size: number): Promise<{ users: Use
const users: UserInfo[] = []
await pagedCursor.forEach(doc => users.push(doc))
users.forEach((user) => {
- if (user.root == null)
- user.root = process.env.ROOT_USER === user.email.toLowerCase()
+ initUserInfo(user)
})
return { users, total }
}
export async function getUserById(userId: string): Promise {
const userInfo = await userCol.findOne({ _id: new ObjectId(userId) }) as UserInfo
+ initUserInfo(userInfo)
+ return userInfo
+}
+
+function initUserInfo(userInfo: UserInfo) {
if (userInfo.config == null)
userInfo.config = new UserConfig()
if (userInfo.config.chatModel == null)
userInfo.config.chatModel = 'gpt-3.5-turbo'
-
- return userInfo
+ if (userInfo.roles == null || userInfo.roles.length <= 0) {
+ userInfo.roles = [UserRole.User]
+ if (process.env.ROOT_USER === userInfo.email.toLowerCase())
+ userInfo.roles.push(UserRole.Admin)
+ }
}
export async function verifyUser(email: string, status: Status) {
@@ -321,3 +333,23 @@ export async function getUserStatisticsByDay(userId: ObjectId, start: number, en
return result
}
+
+export async function getKeys(): Promise<{ keys: KeyConfig[]; total: number }> {
+ const cursor = await keyCol.find({ status: { $ne: Status.Disabled } })
+ const total = await cursor.count()
+ const keys = []
+ await cursor.forEach(doc => keys.push(doc))
+ return { keys, total }
+}
+
+export async function upsertKey(key: KeyConfig): Promise {
+ if (key._id === undefined)
+ await keyCol.insertOne(key)
+ else
+ await keyCol.replaceOne({ _id: key._id }, key, { upsert: true })
+ return key
+}
+
+export async function updateApiKeyStatus(id: string, status: Status) {
+ return await keyCol.updateOne({ _id: new ObjectId(id) }, { $set: { status } })
+}
diff --git a/service/src/types.ts b/service/src/types.ts
index 60ce406b24..67c34df623 100644
--- a/service/src/types.ts
+++ b/service/src/types.ts
@@ -26,7 +26,7 @@ export interface ChatGPTUnofficialProxyAPIOptions {
}
export interface ModelConfig {
- apiModel?: ApiModel
+ apiModel?: APIMODEL
reverseProxy?: string
timeoutMs?: number
socksProxy?: string
@@ -37,7 +37,7 @@ export interface ModelConfig {
accessTokenExpiredTime?: string
}
-export type ApiModel = 'ChatGPTAPI' | 'ChatGPTUnofficialProxyAPI' | undefined
+export type APIMODEL = 'ChatGPTAPI' | 'ChatGPTUnofficialProxyAPI' | undefined
export interface JWT {
'https://api.openai.com/profile': {
diff --git a/service/src/utils/is.ts b/service/src/utils/is.ts
index 05e8af0c1a..92e4438b42 100644
--- a/service/src/utils/is.ts
+++ b/service/src/utils/is.ts
@@ -1,3 +1,4 @@
+import type { UserRole } from '../storage/model'
import { TextAudioType } from '../storage/model'
import type { TextAuditServiceProvider } from './textAudit'
@@ -37,3 +38,10 @@ export function isTextAudioType(value: any): value is TextAudioType {
|| value === TextAudioType.All
)
}
+export function hasAnyRole(userRoles: UserRole[] | undefined, roles: UserRole[]): boolean {
+ if (!userRoles || userRoles.length === 0 || !roles || roles.length === 0)
+ return false
+
+ const roleNames = roles.map(role => role.toString())
+ return roleNames.some(roleName => userRoles.some(userRole => userRole.toString() === roleName))
+}
diff --git a/src/api/index.ts b/src/api/index.ts
index eeaed0d4c5..739f1ed38e 100644
--- a/src/api/index.ts
+++ b/src/api/index.ts
@@ -1,6 +1,6 @@
import type { AxiosProgressEvent, GenericAbortSignal } from 'axios'
import { get, post } from '@/utils/request'
-import type { AuditConfig, CHATMODEL, ConfigState, MailConfig, SiteConfig, Status } from '@/components/common/Setting/model'
+import type { AuditConfig, CHATMODEL, ConfigState, KeyConfig, MailConfig, SiteConfig, Status } from '@/components/common/Setting/model'
import { useAuthStore, useSettingStore } from '@/store'
export function fetchChatAPI(
@@ -258,3 +258,24 @@ export function fetchUserStatistics(start: number, end: number) {
data: { start, end },
})
}
+
+export function fetchGetKeys(page: number, size: number) {
+ return get({
+ url: '/setting-keys',
+ data: { page, size },
+ })
+}
+
+export function fetchUpdateApiKeyStatus(id: string, status: Status) {
+ return post({
+ url: '/setting-key-status',
+ data: { id, status },
+ })
+}
+
+export function fetchUpsertApiKey(keyConfig: KeyConfig) {
+ return post({
+ url: '/setting-key-upsert',
+ data: keyConfig,
+ })
+}
diff --git a/src/components/common/Setting/About.vue b/src/components/common/Setting/About.vue
index 803537e2f0..5d15956a77 100644
--- a/src/components/common/Setting/About.vue
+++ b/src/components/common/Setting/About.vue
@@ -1,6 +1,6 @@
+
+
+
+
+
+
+
+ New Key
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ $t('setting.api') }}
+
+
+
+
+
+
{{ $t('setting.chatModels') }}
+
+ keyConfig.chatModels = value"
+ />
+
+
+
+
{{ $t('setting.userRoles') }}
+
+ keyConfig.userRoles = value"
+ />
+
+
+
+
{{ $t('setting.status') }}
+
+ { keyConfig.status = val ? Status.Normal : Status.Disabled }"
+ />
+
+
+
+
{{ $t('setting.remark') }}
+
+
+
+
+
+
+
+ {{ $t('common.save') }}
+
+
+
+
+
+
diff --git a/src/components/common/Setting/User.vue b/src/components/common/Setting/User.vue
index 048d54750d..49b9a64334 100644
--- a/src/components/common/Setting/User.vue
+++ b/src/components/common/Setting/User.vue
@@ -1,7 +1,7 @@
-
+
@@ -107,6 +110,13 @@ const show = computed({
+
+
+
+ {{ $t('setting.keysConfig') }}
+
+
+
diff --git a/src/components/common/Setting/model.ts b/src/components/common/Setting/model.ts
index 3eaa7b8308..e74230ab53 100644
--- a/src/components/common/Setting/model.ts
+++ b/src/components/common/Setting/model.ts
@@ -4,7 +4,7 @@ export class ConfigState {
accessToken?: string
accessTokenExpiredTime?: string
apiBaseUrl?: string
- apiModel?: ApiModel
+ apiModel?: APIMODEL
reverseProxy?: string
socksProxy?: string
socksAuth?: string
@@ -22,8 +22,6 @@ export class UserConfig {
chatModel?: CHATMODEL
}
-export type ApiModel = 'ChatGPTAPI' | 'ChatGPTUnofficialProxyAPI' | undefined
-
export class SiteConfig {
siteTitle?: string
loginEnabled?: boolean
@@ -71,4 +69,67 @@ export enum Status {
ResponseDeleted = 3,
PreVerify = 4,
AdminVerify = 5,
+ Disabled = 6,
+}
+
+export enum UserRole {
+ Admin = 0,
+ User = 1,
+ Guest = 2,
+}
+
+export class KeyConfig {
+ _id?: string
+ key: string
+ keyModel: APIMODEL
+ chatModels: CHATMODEL[]
+ userRoles: UserRole[]
+ status: Status
+ remark: string
+ constructor(key: string, keyModel: APIMODEL, chatModels: CHATMODEL[], userRoles: UserRole[], remark: string) {
+ this.key = key
+ this.keyModel = keyModel
+ this.chatModels = chatModels
+ this.userRoles = userRoles
+ this.status = Status.Normal
+ this.remark = remark
+ }
}
+
+export type APIMODEL = 'ChatGPTAPI' | 'ChatGPTUnofficialProxyAPI' | undefined
+
+export const apiModelOptions = ['ChatGPTAPI', 'ChatGPTUnofficialProxyAPI'].map((model: string) => {
+ return {
+ label: model,
+ key: model,
+ value: model,
+ }
+})
+
+export const chatModelOptions = [
+ 'gpt-3.5-turbo',
+ 'gpt-3.5-turbo-0301',
+ 'gpt-4',
+ 'gpt-4-0314',
+ 'gpt-4-32k',
+ 'gpt-4-32k-0314',
+ 'text-davinci-002-render-sha-mobile',
+ 'gpt-4-mobile',
+].map((model: string) => {
+ let label = model
+ if (model === 'text-davinci-002-render-sha-mobile')
+ label = 'gpt-3.5-mobile'
+ return {
+ label,
+ key: model,
+ value: model,
+ }
+})
+
+export const userRoleOptions = Object.values(UserRole).filter(d => isNaN(Number(d))).map((role) => {
+ return {
+ label: role as string,
+ key: role as string,
+ value: UserRole[role as keyof typeof UserRole],
+ }
+})
diff --git a/src/locales/en-US.ts b/src/locales/en-US.ts
index e5f30eb574..09b4e5b521 100644
--- a/src/locales/en-US.ts
+++ b/src/locales/en-US.ts
@@ -65,6 +65,9 @@ export default {
deleteUser: 'Delete User',
deleteUserConfirm: 'Are you sure to delete this user?',
verifiedUser: 'Verified User',
+ deleteKey: 'Delete Key',
+ editKeyButton: 'Edit Key',
+ deleteKeyConfirm: 'Are you sure to delete this key?',
},
setting: {
setting: 'Setting',
@@ -128,6 +131,10 @@ export default {
auditCustomizeWords: 'Sensitive Words',
accessTokenExpiredTime: 'Expired Time',
userConfig: 'Users',
+ keysConfig: 'Keys Manager',
+ userRoles: 'User Role',
+ status: 'Status',
+ chatModels: 'Chat Models',
},
store: {
siderButton: 'Prompt Store',
diff --git a/src/locales/ko-KR.ts b/src/locales/ko-KR.ts
index 359e8fe7ab..abbad854e4 100644
--- a/src/locales/ko-KR.ts
+++ b/src/locales/ko-KR.ts
@@ -65,6 +65,9 @@ export default {
deleteUser: 'Delete User',
deleteUserConfirm: 'Are you sure to delete this user?',
verifiedUser: 'Verified User',
+ deleteKey: 'Delete Key',
+ editKeyButton: 'Edit Key',
+ deleteKeyConfirm: 'Are you sure to delete this key?',
},
setting: {
setting: '설정',
@@ -126,6 +129,10 @@ export default {
auditCustomizeWords: '단어 맞춤설정',
accessTokenExpiredTime: '만료된 시간',
userConfig: 'Users',
+ keysConfig: 'Keys Manager',
+ userRoles: 'User Role',
+ status: 'Status',
+ chatModels: 'Chat Models',
},
store: {
siderButton: '프롬프트 스토어',
diff --git a/src/locales/zh-CN.ts b/src/locales/zh-CN.ts
index 0cf2b570ed..cca8d1d2aa 100644
--- a/src/locales/zh-CN.ts
+++ b/src/locales/zh-CN.ts
@@ -65,6 +65,9 @@ export default {
deleteUser: '删除用户',
deleteUserConfirm: '你确定要删除这个用户吗?',
verifiedUser: '通过验证',
+ deleteKey: '删除 Key',
+ editKeyButton: '编辑 Key',
+ deleteKeyConfirm: '你确定要删除这个 key 吗?',
},
setting: {
setting: '设置',
@@ -128,6 +131,10 @@ export default {
auditCustomizeWords: '敏感词',
accessTokenExpiredTime: '过期时间',
userConfig: '用户管理',
+ keysConfig: 'Keys 管理',
+ userRoles: '用户权限',
+ status: '状态',
+ chatModels: '对话模型',
},
store: {
siderButton: '提示词商店',
diff --git a/src/locales/zh-TW.ts b/src/locales/zh-TW.ts
index 8420d06d25..0e46ed0275 100644
--- a/src/locales/zh-TW.ts
+++ b/src/locales/zh-TW.ts
@@ -65,6 +65,9 @@ export default {
deleteUser: '删除用户',
deleteUserConfirm: '你确定要删除这个用户吗?',
verifiedUser: '通过验证',
+ deleteKey: '删除 Key',
+ editKeyButton: '编辑 Key',
+ deleteKeyConfirm: '你确定要删除这个 key 吗?',
},
setting: {
setting: '設定',
@@ -128,6 +131,10 @@ export default {
auditCustomizeWords: '敏感词',
accessTokenExpiredTime: '过期时间',
userConfig: '用户管理',
+ keysConfig: 'Keys 管理',
+ userRoles: '用户权限',
+ status: '状态',
+ chatModels: '对话模型',
},
store: {
siderButton: '提示詞商店',