diff --git a/service/src/index.ts b/service/src/index.ts index a26f486e..f26783e0 100644 --- a/service/src/index.ts +++ b/service/src/index.ts @@ -180,6 +180,7 @@ router.post('/session', async (req, res) => { chatModels, allChatModels: chatModelOptions, showWatermark: config.siteConfig?.showWatermark, + adminViewChatHistoryEnabled: process.env.ADMIN_VIEW_CHAT_HISTORY_ENABLED === 'true', }, }) return @@ -244,6 +245,7 @@ router.post('/session', async (req, res) => { allChatModels: chatModelOptions, usageCountLimit: config.siteConfig?.usageCountLimit, showWatermark: config.siteConfig?.showWatermark, + adminViewChatHistoryEnabled: process.env.ADMIN_VIEW_CHAT_HISTORY_ENABLED === 'true', userInfo, }, }) @@ -261,6 +263,7 @@ router.post('/session', async (req, res) => { chatModels: chatModelOptions, allChatModels: chatModelOptions, showWatermark: config.siteConfig?.showWatermark, + adminViewChatHistoryEnabled: process.env.ADMIN_VIEW_CHAT_HISTORY_ENABLED === 'true', userInfo, }, }) diff --git a/service/src/routes/chat.ts b/service/src/routes/chat.ts index 7b596289..f4c308f1 100644 --- a/service/src/routes/chat.ts +++ b/service/src/routes/chat.ts @@ -2,6 +2,7 @@ import type { ResponseChunk } from '../chatgpt/types' import type { ChatInfo, ChatOptions, UsageResponse, UserInfo } from '../storage/model' import type { RequestProps } from '../types' import * as console from 'node:console' +import * as process from 'node:process' import Router from 'express' import { ObjectId } from 'mongodb' import { abortChatProcess, chatReplyProcess, containsSensitiveWords } from '../chatgpt' @@ -38,12 +39,14 @@ router.get('/chat-history', auth, async (req, res) => { return } + // When 'all' parameter is not empty, it means requesting to view all users' chat history + // This requires: 1) user must be an admin, 2) ADMIN_VIEW_CHAT_HISTORY_ENABLED environment variable must be set to 'true' if (all !== null && all !== 'undefined' && all !== undefined && all.trim().length !== 0) { const config = await getCacheConfig() if (config.siteConfig.loginEnabled) { try { const user = await getUserById(userId) - if (user == null || user.status !== Status.Normal || !user.roles.includes(UserRole.Admin)) { + if (user == null || user.status !== Status.Normal || !user.roles.includes(UserRole.Admin) || process.env.ADMIN_VIEW_CHAT_HISTORY_ENABLED !== 'true') { res.send({ status: 'Fail', message: '无权限 | No permission.', data: null }) return } diff --git a/service/src/storage/mongo.ts b/service/src/storage/mongo.ts index afb12a96..72d11141 100644 --- a/service/src/storage/mongo.ts +++ b/service/src/storage/mongo.ts @@ -431,14 +431,40 @@ export async function getChatRoomsCount(userId: string, page: number, size: numb { $lookup: { from: 'chat', - localField: 'roomId', - foreignField: 'roomId', - as: 'chat', + let: { roomId: '$roomId' }, + pipeline: [ + { + $match: { + $expr: { + $and: [ + { $eq: ['$roomId', '$$roomId'] }, + { $ne: ['$status', Status.InversionDeleted] }, + ], + }, + }, + }, + { + $sort: { dateTime: -1 }, + }, + { + $group: { + _id: null, + chatCount: { $sum: 1 }, + lastChat: { $first: '$$ROOT' }, + }, + }, + ], + as: 'chatInfo', }, }, { $addFields: { - title: '$chat.prompt', + chatCount: { + $ifNull: [{ $arrayElemAt: ['$chatInfo.chatCount', 0] }, 0], + }, + lastChat: { + $arrayElemAt: ['$chatInfo.lastChat', 0], + }, user_ObjectId: { $toObjectId: '$userId', }, @@ -450,6 +476,13 @@ export async function getChatRoomsCount(userId: string, page: number, size: numb localField: 'user_ObjectId', foreignField: '_id', as: 'user', + pipeline: [ + { + $project: { + name: 1, + }, + }, + ], }, }, { @@ -458,37 +491,14 @@ export async function getChatRoomsCount(userId: string, page: number, size: numb preserveNullAndEmptyArrays: false, }, }, - { - $sort: { - 'chat.dateTime': -1, - }, - }, - { - $addFields: { - chatCount: { - $size: '$chat', - }, - chat: { - $arrayElemAt: [ - { - $slice: [ - '$chat', - -1, - ], - }, - 0, - ], - }, - }, - }, { $project: { userId: 1, - title: '$chat.prompt', + title: { $ifNull: ['$lastChat.prompt', ''] }, username: '$user.name', roomId: 1, chatCount: 1, - dateTime: '$chat.dateTime', + dateTime: { $ifNull: ['$lastChat.dateTime', null] }, }, }, { diff --git a/src/api/index.ts b/src/api/index.ts index d2a1ff73..a55bd518 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -377,8 +377,15 @@ export function fetchDeleteChatRoom(roomId: number) { } export function fetchGetChatHistory(roomId: number, lastId?: number, all?: string) { + let url = `/chat-history?roomId=${roomId}` + if (lastId !== undefined && lastId !== null) { + url += `&lastId=${lastId}` + } + if (all !== undefined && all !== null) { + url += `&all=${all}` + } return get({ - url: `/chat-history?roomId=${roomId}&lastId=${lastId}&all=${all}`, + url, }) } diff --git a/src/components/common/Setting/ChatRecord.vue b/src/components/common/Setting/ChatRecord.vue index eb999ca2..6aad582f 100644 --- a/src/components/common/Setting/ChatRecord.vue +++ b/src/components/common/Setting/ChatRecord.vue @@ -5,6 +5,8 @@ import { SvgIcon } from '@/components/common' import { useBasicLayout } from '@/hooks/useBasicLayout' import Message from '@/views/chat/components/Message/index.vue' +const { t } = useI18n() + interface HistoryChat { uuid?: number model?: string @@ -70,13 +72,13 @@ const columns = [{ show.value = true dataSources.value.length = 0 chatLoading.value = true - fetchGetChatHistory(row.uuid, undefined, 'all').then((res: any) => { + fetchGetChatHistory(row.roomId, undefined, 'all').then((res: any) => { dataSources.value = res.data as HistoryChat[] chatLoading.value = false }) }, }, - { default: () => 'view' }, + { default: () => t('setting.view') }, )) return actions }, @@ -144,6 +146,7 @@ onMounted(async () => { diff --git a/src/components/common/Setting/index.vue b/src/components/common/Setting/index.vue index 7f072139..39b5de68 100644 --- a/src/components/common/Setting/index.vue +++ b/src/components/common/Setting/index.vue @@ -2,7 +2,7 @@ import { SvgIcon } from '@/components/common' import ChatRecord from '@/components/common/Setting/ChatRecord.vue' import { useBasicLayout } from '@/hooks/useBasicLayout' -import { useUserStore } from '@/store' +import { useAuthStore, useUserStore } from '@/store' import About from './About.vue' import Advanced from './Advanced.vue' import Announcement from './Anonuncement.vue' @@ -25,6 +25,7 @@ const emit = defineEmits() const { t } = useI18n() const userStore = useUserStore() +const authStore = useAuthStore() const { isMobile } = useBasicLayout() interface Props { @@ -37,6 +38,11 @@ interface Emit { const active = ref('General') +// Check if admin view chat history is enabled +const showChatRecordTab = computed(() => { + return userStore.userInfo.root && authStore.session?.adminViewChatHistoryEnabled === true +}) + const show = computed({ get() { return props.visible @@ -99,7 +105,7 @@ const show = computed({ - +