dvc890's picture
[feat]add project files
41af422
import { MongoClient, ObjectId } from 'mongodb'
import * as dotenv from 'dotenv'
import dayjs from 'dayjs'
import { ChatInfo, ChatRoom, ChatUsage, Status, UserConfig, UserInfo, UserRole } from './model'
import type { CHATMODEL, ChatOptions, Config, KeyConfig, UsageResponse } from './model'
dotenv.config()
const url = process.env.MONGODB_URL
const parsedUrl = new URL(url)
const dbName = (parsedUrl.pathname && parsedUrl.pathname !== '/') ? parsedUrl.pathname.substring(1) : 'chatgpt'
const client = new MongoClient(url)
const chatCol = client.db(dbName).collection('chat')
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')
/**
* 插入聊天信息
* @param uuid
* @param text 内容 prompt or response
* @param roomId
* @param options
* @returns model
*/
export async function insertChat(uuid: number, text: string, roomId: number, options?: ChatOptions) {
const chatInfo = new ChatInfo(roomId, uuid, text, options)
await chatCol.insertOne(chatInfo)
return chatInfo
}
export async function getChat(roomId: number, uuid: number) {
return await chatCol.findOne({ roomId, uuid }) as ChatInfo
}
export async function getChatByMessageId(messageId: string) {
return await chatCol.findOne({ 'options.messageId': messageId }) as ChatInfo
}
export async function updateChat(chatId: string, response: string, messageId: string, conversationId: string, usage: UsageResponse, previousResponse?: []) {
const query = { _id: new ObjectId(chatId) }
const update = {
$set: {
'response': response,
'options.messageId': messageId,
'options.conversationId': conversationId,
'options.prompt_tokens': usage?.prompt_tokens,
'options.completion_tokens': usage?.completion_tokens,
'options.total_tokens': usage?.total_tokens,
'options.estimated': usage?.estimated,
},
}
if (previousResponse)
update.$set.previousResponse = previousResponse
await chatCol.updateOne(query, update)
}
export async function insertChatUsage(userId: ObjectId, roomId: number, chatId: ObjectId, messageId: string, usage: UsageResponse) {
const chatUsage = new ChatUsage(userId, roomId, chatId, messageId, usage)
await usageCol.insertOne(chatUsage)
return chatUsage
}
export async function createChatRoom(userId: string, title: string, roomId: number) {
const room = new ChatRoom(userId, title, roomId)
await roomCol.insertOne(room)
return room
}
export async function renameChatRoom(userId: string, title: string, roomId: number) {
const query = { userId, roomId }
const update = {
$set: {
title,
},
}
return await roomCol.updateOne(query, update)
}
export async function deleteChatRoom(userId: string, roomId: number) {
const result = await roomCol.updateOne({ roomId, userId }, { $set: { status: Status.Deleted } })
await clearChat(roomId)
return result
}
export async function updateRoomPrompt(userId: string, roomId: number, prompt: string) {
const query = { userId, roomId }
const update = {
$set: {
prompt,
},
}
const result = await roomCol.updateOne(query, update)
return result.modifiedCount > 0
}
export async function updateRoomUsingContext(userId: string, roomId: number, using: boolean) {
const query = { userId, roomId }
const update = {
$set: {
usingContext: using,
},
}
const result = await roomCol.updateOne(query, update)
return result.modifiedCount > 0
}
export async function updateRoomAccountId(userId: string, roomId: number, accountId: string) {
const query = { userId, roomId }
const update = {
$set: {
accountId,
},
}
const result = await roomCol.updateOne(query, update)
return result.modifiedCount > 0
}
export async function getChatRooms(userId: string) {
const cursor = await roomCol.find({ userId, status: { $ne: Status.Deleted } })
const rooms = []
await cursor.forEach(doc => rooms.push(doc))
return rooms
}
export async function getChatRoom(userId: string, roomId: number) {
return await roomCol.findOne({ userId, roomId, status: { $ne: Status.Deleted } }) as ChatRoom
}
export async function existsChatRoom(userId: string, roomId: number) {
const room = await roomCol.findOne({ roomId, userId })
return !!room
}
export async function deleteAllChatRooms(userId: string) {
await roomCol.updateMany({ userId, status: Status.Normal }, { $set: { status: Status.Deleted } })
await chatCol.updateMany({ userId, status: Status.Normal }, { $set: { status: Status.Deleted } })
}
export async function getChats(roomId: number, lastId?: number) {
if (!lastId)
lastId = new Date().getTime()
const query = { roomId, uuid: { $lt: lastId }, status: { $ne: Status.Deleted } }
const limit = 20
const cursor = await chatCol.find(query).sort({ dateTime: -1 }).limit(limit)
const chats = []
await cursor.forEach(doc => chats.push(doc))
chats.reverse()
return chats
}
export async function clearChat(roomId: number) {
const query = { roomId }
const update = {
$set: {
status: Status.Deleted,
},
}
await chatCol.updateMany(query, update)
}
export async function deleteChat(roomId: number, uuid: number, inversion: boolean) {
const query = { roomId, uuid }
let update = {
$set: {
status: Status.Deleted,
},
}
const chat = await chatCol.findOne(query)
if (chat.status === Status.InversionDeleted && !inversion) { /* empty */ }
else if (chat.status === Status.ResponseDeleted && inversion) { /* empty */ }
else if (inversion) {
update = {
$set: {
status: Status.InversionDeleted,
},
}
}
else {
update = {
$set: {
status: Status.ResponseDeleted,
},
}
}
await chatCol.updateOne(query, update)
}
export async function createUser(email: string, password: string, isRoot: boolean): Promise<UserInfo> {
email = email.toLowerCase()
const userInfo = new UserInfo(email, password)
if (isRoot) {
userInfo.status = Status.Normal
userInfo.roles = [UserRole.Admin]
}
await userCol.insertOne(userInfo)
return userInfo
}
export async function updateUserInfo(userId: string, user: UserInfo) {
return userCol.updateOne({ _id: new ObjectId(userId) }
, { $set: { name: user.name, description: user.description, avatar: user.avatar } })
}
export async function updateUserChatModel(userId: string, chatModel: CHATMODEL) {
return userCol.updateOne({ _id: new ObjectId(userId) }
, { $set: { 'config.chatModel': chatModel } })
}
export async function updateUserPassword(userId: string, password: string) {
return userCol.updateOne({ _id: new ObjectId(userId) }
, { $set: { password, updateTime: new Date().toLocaleString() } })
}
export async function getUser(email: string): Promise<UserInfo> {
email = email.toLowerCase()
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 query = { status: { $ne: Status.Deleted } }
const cursor = userCol.find(query).sort({ createTime: -1 })
const total = await userCol.countDocuments(query)
const skip = (page - 1) * size
const limit = size
const pagedCursor = cursor.skip(skip).limit(limit)
const users: UserInfo[] = []
await pagedCursor.forEach(doc => users.push(doc))
users.forEach((user) => {
initUserInfo(user)
})
return { users, total }
}
export async function getUserById(userId: string): Promise<UserInfo> {
const userInfo = await userCol.findOne({ _id: new ObjectId(userId) }) as UserInfo
initUserInfo(userInfo)
return userInfo
}
function initUserInfo(userInfo: UserInfo) {
if (userInfo == null)
return
if (userInfo.config == null)
userInfo.config = new UserConfig()
if (userInfo.config.chatModel == null)
userInfo.config.chatModel = 'gpt-3.5-turbo'
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) {
email = email.toLowerCase()
return await userCol.updateOne({ email }, { $set: { status, verifyTime: new Date().toLocaleString() } })
}
export async function updateUserStatus(userId: string, status: Status) {
return await userCol.updateOne({ _id: new ObjectId(userId) }, { $set: { status, verifyTime: new Date().toLocaleString() } })
}
export async function updateUserRole(userId: string, roles: UserRole[]) {
return await userCol.updateOne({ _id: new ObjectId(userId) }, { $set: { roles, verifyTime: new Date().toLocaleString() } })
}
export async function getConfig(): Promise<Config> {
return await configCol.findOne() as Config
}
export async function updateConfig(config: Config): Promise<Config> {
const result = await configCol.replaceOne({ _id: config._id }, config, { upsert: true })
if (result.modifiedCount > 0 || result.upsertedCount > 0)
return config
if (result.matchedCount > 0 && result.modifiedCount <= 0 && result.upsertedCount <= 0)
return config
return null
}
export async function getUserStatisticsByDay(userId: ObjectId, start: number, end: number): Promise<any> {
const pipeline = [
{ // filter by dateTime
$match: {
dateTime: {
$gte: start,
$lte: end,
},
userId,
},
},
{ // convert dateTime to date
$addFields: {
date: {
$dateToString: {
format: '%Y-%m-%d',
date: {
$toDate: '$dateTime',
},
},
},
},
},
{ // group by date
$group: {
_id: '$date',
promptTokens: {
$sum: '$promptTokens',
},
completionTokens: {
$sum: '$completionTokens',
},
totalTokens: {
$sum: '$totalTokens',
},
},
},
{ // sort by date
$sort: {
_id: 1,
},
},
]
const aggStatics = await usageCol.aggregate(pipeline).toArray()
const step = 86400000 // 1 day in milliseconds
const result = {
promptTokens: null,
completionTokens: null,
totalTokens: null,
chartData: [],
}
for (let i = start; i <= end; i += step) {
// Convert the timestamp to a Date object
const date = dayjs(i, 'x').format('YYYY-MM-DD')
const dateData = aggStatics.find(x => x._id === date)
|| { _id: date, promptTokens: 0, completionTokens: 0, totalTokens: 0 }
result.promptTokens += dateData.promptTokens
result.completionTokens += dateData.completionTokens
result.totalTokens += dateData.totalTokens
result.chartData.push(dateData)
}
return result
}
export async function getKeys(): Promise<{ keys: KeyConfig[]; total: number }> {
const query = { status: { $ne: Status.Disabled } }
const cursor = await keyCol.find(query)
const total = await keyCol.countDocuments(query)
const keys = []
await cursor.forEach(doc => keys.push(doc))
return { keys, total }
}
export async function upsertKey(key: KeyConfig): Promise<KeyConfig> {
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 } })
}