Spaces:
Runtime error
Runtime error
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 } }) | |
} | |