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 { 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 { 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 { 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 { return await configCol.findOne() as Config } export async function updateConfig(config: Config): Promise { 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 { 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 { 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 } }) }