mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-23 13:03:50 +00:00
chat logs filter & export (#3737)
* chat logs filter & export * export chat detail
This commit is contained in:
10
projects/app/src/global/core/api/appReq.d.ts
vendored
10
projects/app/src/global/core/api/appReq.d.ts
vendored
@@ -1,7 +1,13 @@
|
||||
import { ChatSourceEnum } from '@fastgpt/global/core/chat/constants';
|
||||
import { UsageSourceEnum } from '@fastgpt/global/support/wallet/usage/constants';
|
||||
import { PaginationProps } from '@fastgpt/web/common/fetch/type';
|
||||
|
||||
export type GetAppChatLogsParams = PaginationProps<{
|
||||
export type GetAppChatLogsProps = {
|
||||
appId: string;
|
||||
dateStart: Date;
|
||||
dateEnd: Date;
|
||||
}>;
|
||||
sources?: ChatSourceEnum[];
|
||||
logTitle?: string;
|
||||
};
|
||||
|
||||
export type GetAppChatLogsParams = PaginationProps<GetAppChatLogsProps>;
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import {
|
||||
Flex,
|
||||
Box,
|
||||
@@ -9,17 +9,15 @@ import {
|
||||
Th,
|
||||
Td,
|
||||
Tbody,
|
||||
useDisclosure,
|
||||
ModalBody,
|
||||
HStack
|
||||
HStack,
|
||||
Button
|
||||
} from '@chakra-ui/react';
|
||||
import UserBox from '@fastgpt/web/components/common/UserBox';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { getAppChatLogs } from '@/web/core/app/api';
|
||||
import dayjs from 'dayjs';
|
||||
import { ChatSourceMap } from '@fastgpt/global/core/chat/constants';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { ChatSourceEnum, ChatSourceMap } from '@fastgpt/global/core/chat/constants';
|
||||
import { addDays } from 'date-fns';
|
||||
import { usePagination } from '@fastgpt/web/hooks/usePagination';
|
||||
import DateRangePicker, { DateRangeType } from '@fastgpt/web/components/common/DateRangePicker';
|
||||
@@ -29,13 +27,19 @@ import { AppContext } from '../context';
|
||||
import { cardStyles } from '../constants';
|
||||
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
||||
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||
import MultipleSelect, {
|
||||
useMultipleSelect
|
||||
} from '@fastgpt/web/components/common/MySelect/MultipleSelect';
|
||||
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
|
||||
import PopoverConfirm from '@fastgpt/web/components/common/MyPopover/PopoverConfirm';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { downloadFetch } from '@/web/common/system/utils';
|
||||
|
||||
const DetailLogsModal = dynamic(() => import('./DetailLogsModal'));
|
||||
|
||||
const Logs = () => {
|
||||
const { t } = useTranslation();
|
||||
const { isPc } = useSystem();
|
||||
|
||||
const appId = useContextSelector(AppContext, (v) => v.appId);
|
||||
|
||||
@@ -43,165 +47,236 @@ const Logs = () => {
|
||||
from: addDays(new Date(), -7),
|
||||
to: new Date()
|
||||
});
|
||||
|
||||
const [detailLogsId, setDetailLogsId] = useState<string>();
|
||||
const [logTitle, setLogTitle] = useState<string>();
|
||||
|
||||
const {
|
||||
isOpen: isOpenMarkDesc,
|
||||
onOpen: onOpenMarkDesc,
|
||||
onClose: onCloseMarkDesc
|
||||
} = useDisclosure();
|
||||
value: chatSources,
|
||||
setValue: setChatSources,
|
||||
isSelectAll: isSelectAllSource,
|
||||
setIsSelectAll: setIsSelectAllSource
|
||||
} = useMultipleSelect<ChatSourceEnum>(Object.values(ChatSourceEnum), true);
|
||||
|
||||
const sourceList = useMemo(
|
||||
() =>
|
||||
Object.entries(ChatSourceMap).map(([key, value]) => ({
|
||||
label: t(value.name as any),
|
||||
value: key as ChatSourceEnum
|
||||
})),
|
||||
[t]
|
||||
);
|
||||
|
||||
const {
|
||||
data: logs,
|
||||
isLoading,
|
||||
Pagination,
|
||||
getData,
|
||||
pageNum
|
||||
pageNum,
|
||||
total
|
||||
} = usePagination(getAppChatLogs, {
|
||||
pageSize: 20,
|
||||
params: {
|
||||
appId,
|
||||
dateStart: dateRange.from || new Date(),
|
||||
dateEnd: addDays(dateRange.to || new Date(), 1)
|
||||
}
|
||||
dateEnd: addDays(dateRange.to || new Date(), 1),
|
||||
sources: isSelectAllSource ? undefined : chatSources,
|
||||
logTitle
|
||||
},
|
||||
refreshDeps: [chatSources, logTitle]
|
||||
});
|
||||
|
||||
const [detailLogsId, setDetailLogsId] = useState<string>();
|
||||
const { runAsync: exportLogs } = useRequest2(
|
||||
async () => {
|
||||
await downloadFetch({
|
||||
url: '/api/core/app/exportChatLogs',
|
||||
filename: 'chat_logs.csv',
|
||||
body: {
|
||||
appId,
|
||||
dateStart: dateRange.from || new Date(),
|
||||
dateEnd: addDays(dateRange.to || new Date(), 1),
|
||||
sources: isSelectAllSource ? undefined : chatSources,
|
||||
logTitle,
|
||||
|
||||
title: t('app:logs_export_title'),
|
||||
sourcesMap: Object.fromEntries(
|
||||
Object.entries(ChatSourceMap).map(([key, config]) => [
|
||||
key,
|
||||
{
|
||||
label: t(config.name as any)
|
||||
}
|
||||
])
|
||||
)
|
||||
}
|
||||
});
|
||||
},
|
||||
{
|
||||
refreshDeps: [chatSources, logTitle]
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<Flex flexDirection={'column'} h={'100%'}>
|
||||
{isPc && (
|
||||
<Box {...cardStyles} boxShadow={2} px={[4, 8]} py={[4, 6]}>
|
||||
<Box fontWeight={'bold'} fontSize={['md', 'lg']} mb={2}>
|
||||
{t('app:chat_logs')}
|
||||
<Flex
|
||||
flexDirection={'column'}
|
||||
h={'100%'}
|
||||
{...cardStyles}
|
||||
boxShadow={3.5}
|
||||
px={[4, 8]}
|
||||
py={[4, 6]}
|
||||
flex={'1 0 0'}
|
||||
>
|
||||
<Flex flexDir={['column', 'row']} alignItems={['flex-start', 'center']} gap={3}>
|
||||
<Flex alignItems={'center'} gap={2}>
|
||||
<Box fontSize={'mini'} fontWeight={'medium'} color={'myGray.900'}>
|
||||
{t('app:logs_source')}
|
||||
</Box>
|
||||
<Box color={'myGray.500'} fontSize={'sm'}>
|
||||
{t('app:chat_logs_tips')},{' '}
|
||||
<Box
|
||||
as={'span'}
|
||||
mr={2}
|
||||
textDecoration={'underline'}
|
||||
cursor={'pointer'}
|
||||
onClick={onOpenMarkDesc}
|
||||
>
|
||||
{t('common:core.chat.Read Mark Description')}
|
||||
</Box>
|
||||
<Box>
|
||||
<MultipleSelect<ChatSourceEnum>
|
||||
list={sourceList}
|
||||
value={chatSources}
|
||||
onSelect={setChatSources}
|
||||
isSelectAll={isSelectAllSource}
|
||||
setIsSelectAll={setIsSelectAllSource}
|
||||
itemWrap={false}
|
||||
height={'32px'}
|
||||
bg={'myGray.50'}
|
||||
w={'160px'}
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
<Flex alignItems={'center'} gap={2}>
|
||||
<Box fontSize={'mini'} fontWeight={'medium'} color={'myGray.900'}>
|
||||
{t('common:user.Time')}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* table */}
|
||||
<Flex
|
||||
flexDirection={'column'}
|
||||
{...cardStyles}
|
||||
boxShadow={3.5}
|
||||
mt={[0, 4]}
|
||||
px={[4, 8]}
|
||||
py={[4, 6]}
|
||||
flex={'1 0 0'}
|
||||
>
|
||||
<TableContainer mt={[0, 3]} flex={'1 0 0'} h={0} overflowY={'auto'}>
|
||||
<Table variant={'simple'} fontSize={'sm'}>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>{t('common:core.app.logs.Source And Time')}</Th>
|
||||
<Th>{t('app:logs_chat_user')}</Th>
|
||||
<Th>{t('app:logs_title')}</Th>
|
||||
<Th>{t('app:logs_message_total')}</Th>
|
||||
<Th>{t('app:feedback_count')}</Th>
|
||||
<Th>{t('common:core.app.feedback.Custom feedback')}</Th>
|
||||
<Th>{t('app:mark_count')}</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody fontSize={'xs'}>
|
||||
{logs.map((item) => (
|
||||
<Tr
|
||||
key={item._id}
|
||||
_hover={{ bg: 'myWhite.600' }}
|
||||
cursor={'pointer'}
|
||||
title={t('common:core.view_chat_detail')}
|
||||
onClick={() => setDetailLogsId(item.id)}
|
||||
>
|
||||
<Td>
|
||||
{/* @ts-ignore */}
|
||||
<Box>{t(ChatSourceMap[item.source]?.name) || item.source}</Box>
|
||||
<Box color={'myGray.500'}>{dayjs(item.time).format('YYYY/MM/DD HH:mm')}</Box>
|
||||
</Td>
|
||||
<Td>
|
||||
<Box>
|
||||
{!!item.outLinkUid ? (
|
||||
item.outLinkUid
|
||||
) : (
|
||||
<UserBox sourceMember={item.sourceMember} />
|
||||
)}
|
||||
</Box>
|
||||
</Td>
|
||||
<Td className="textEllipsis" maxW={'250px'}>
|
||||
{item.title}
|
||||
</Td>
|
||||
<Td>{item.messageCount}</Td>
|
||||
<Td w={'100px'}>
|
||||
{!!item?.userGoodFeedbackCount && (
|
||||
<Flex
|
||||
mb={item?.userGoodFeedbackCount ? 1 : 0}
|
||||
bg={'green.100'}
|
||||
color={'green.600'}
|
||||
px={3}
|
||||
py={1}
|
||||
alignItems={'center'}
|
||||
justifyContent={'center'}
|
||||
borderRadius={'md'}
|
||||
fontWeight={'bold'}
|
||||
>
|
||||
<MyIcon
|
||||
mr={1}
|
||||
name={'core/chat/feedback/goodLight'}
|
||||
color={'green.600'}
|
||||
w={'14px'}
|
||||
/>
|
||||
{item.userGoodFeedbackCount}
|
||||
</Flex>
|
||||
)}
|
||||
{!!item?.userBadFeedbackCount && (
|
||||
<Flex
|
||||
bg={'#FFF2EC'}
|
||||
color={'#C96330'}
|
||||
px={3}
|
||||
py={1}
|
||||
alignItems={'center'}
|
||||
justifyContent={'center'}
|
||||
borderRadius={'md'}
|
||||
fontWeight={'bold'}
|
||||
>
|
||||
<MyIcon
|
||||
mr={1}
|
||||
name={'core/chat/feedback/badLight'}
|
||||
color={'#C96330'}
|
||||
w={'14px'}
|
||||
/>
|
||||
{item.userBadFeedbackCount}
|
||||
</Flex>
|
||||
)}
|
||||
{!item?.userGoodFeedbackCount && !item?.userBadFeedbackCount && <>-</>}
|
||||
</Td>
|
||||
<Td>{item.customFeedbacksCount || '-'}</Td>
|
||||
<Td>{item.markCount}</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
{logs.length === 0 && !isLoading && <EmptyTip text={t('app:logs_empty')}></EmptyTip>}
|
||||
</TableContainer>
|
||||
|
||||
<HStack w={'100%'} mt={3} justifyContent={'flex-end'}>
|
||||
<DateRangePicker
|
||||
defaultDate={dateRange}
|
||||
position="top"
|
||||
position="bottom"
|
||||
onChange={setDateRange}
|
||||
onSuccess={() => getData(1)}
|
||||
/>
|
||||
<Pagination />
|
||||
</HStack>
|
||||
</Flex>
|
||||
<Flex alignItems={'center'} gap={2}>
|
||||
<Box fontSize={'mini'} fontWeight={'medium'} color={'myGray.900'} whiteSpace={'nowrap'}>
|
||||
{t('app:logs_title')}
|
||||
</Box>
|
||||
<SearchInput
|
||||
placeholder={t('app:logs_title')}
|
||||
w={'240px'}
|
||||
value={logTitle}
|
||||
onChange={(e) => setLogTitle(e.target.value)}
|
||||
/>
|
||||
</Flex>
|
||||
<Box flex={'1'} />
|
||||
<PopoverConfirm
|
||||
Trigger={<Button size={'md'}>{t('common:Export')}</Button>}
|
||||
showCancel
|
||||
content={t('app:logs_export_confirm_tip', { total })}
|
||||
onConfirm={exportLogs}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
<TableContainer mt={[2, 4]} flex={'1 0 0'} h={0} overflowY={'auto'}>
|
||||
<Table variant={'simple'} fontSize={'sm'}>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>{t('common:core.app.logs.Source And Time')}</Th>
|
||||
<Th>{t('app:logs_chat_user')}</Th>
|
||||
<Th>{t('app:logs_title')}</Th>
|
||||
<Th>{t('app:logs_message_total')}</Th>
|
||||
<Th>{t('app:feedback_count')}</Th>
|
||||
<Th>{t('common:core.app.feedback.Custom feedback')}</Th>
|
||||
<Th>
|
||||
<Flex gap={1} alignItems={'center'}>
|
||||
{t('app:mark_count')}
|
||||
<QuestionTip label={t('common:core.chat.Mark Description')} />
|
||||
</Flex>
|
||||
</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody fontSize={'xs'}>
|
||||
{logs.map((item) => (
|
||||
<Tr
|
||||
key={item._id}
|
||||
_hover={{ bg: 'myWhite.600' }}
|
||||
cursor={'pointer'}
|
||||
title={t('common:core.view_chat_detail')}
|
||||
onClick={() => setDetailLogsId(item.id)}
|
||||
>
|
||||
<Td>
|
||||
{/* @ts-ignore */}
|
||||
<Box>{t(ChatSourceMap[item.source]?.name) || item.source}</Box>
|
||||
<Box color={'myGray.500'}>{dayjs(item.time).format('YYYY/MM/DD HH:mm')}</Box>
|
||||
</Td>
|
||||
<Td>
|
||||
<Box>
|
||||
{!!item.outLinkUid ? (
|
||||
item.outLinkUid
|
||||
) : (
|
||||
<UserBox sourceMember={item.sourceMember} />
|
||||
)}
|
||||
</Box>
|
||||
</Td>
|
||||
<Td className="textEllipsis" maxW={'250px'}>
|
||||
{item.customTitle || item.title}
|
||||
</Td>
|
||||
<Td>{item.messageCount}</Td>
|
||||
<Td w={'100px'}>
|
||||
{!!item?.userGoodFeedbackCount && (
|
||||
<Flex
|
||||
mb={item?.userGoodFeedbackCount ? 1 : 0}
|
||||
bg={'green.100'}
|
||||
color={'green.600'}
|
||||
px={3}
|
||||
py={1}
|
||||
alignItems={'center'}
|
||||
justifyContent={'center'}
|
||||
borderRadius={'md'}
|
||||
fontWeight={'bold'}
|
||||
>
|
||||
<MyIcon
|
||||
mr={1}
|
||||
name={'core/chat/feedback/goodLight'}
|
||||
color={'green.600'}
|
||||
w={'14px'}
|
||||
/>
|
||||
{item.userGoodFeedbackCount}
|
||||
</Flex>
|
||||
)}
|
||||
{!!item?.userBadFeedbackCount && (
|
||||
<Flex
|
||||
bg={'#FFF2EC'}
|
||||
color={'#C96330'}
|
||||
px={3}
|
||||
py={1}
|
||||
alignItems={'center'}
|
||||
justifyContent={'center'}
|
||||
borderRadius={'md'}
|
||||
fontWeight={'bold'}
|
||||
>
|
||||
<MyIcon
|
||||
mr={1}
|
||||
name={'core/chat/feedback/badLight'}
|
||||
color={'#C96330'}
|
||||
w={'14px'}
|
||||
/>
|
||||
{item.userBadFeedbackCount}
|
||||
</Flex>
|
||||
)}
|
||||
{!item?.userGoodFeedbackCount && !item?.userBadFeedbackCount && <>-</>}
|
||||
</Td>
|
||||
<Td>{item.customFeedbacksCount || '-'}</Td>
|
||||
<Td>{item.markCount}</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
{logs.length === 0 && !isLoading && <EmptyTip text={t('app:logs_empty')}></EmptyTip>}
|
||||
</TableContainer>
|
||||
|
||||
<HStack w={'100%'} mt={3} justifyContent={'center'}>
|
||||
<Pagination />
|
||||
</HStack>
|
||||
|
||||
{!!detailLogsId && (
|
||||
<DetailLogsModal
|
||||
appId={appId}
|
||||
@@ -212,13 +287,6 @@ const Logs = () => {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<MyModal
|
||||
isOpen={isOpenMarkDesc}
|
||||
onClose={onCloseMarkDesc}
|
||||
title={t('common:core.chat.Mark Description Title')}
|
||||
>
|
||||
<ModalBody whiteSpace={'pre-wrap'}>{t('common:core.chat.Mark Description')}</ModalBody>
|
||||
</MyModal>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
257
projects/app/src/pages/api/core/app/exportChatLogs.ts
Normal file
257
projects/app/src/pages/api/core/app/exportChatLogs.ts
Normal file
@@ -0,0 +1,257 @@
|
||||
import type { NextApiResponse } from 'next';
|
||||
import { responseWriteController } from '@fastgpt/service/common/response';
|
||||
import { addDays } from 'date-fns';
|
||||
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
|
||||
import { readFromSecondary } from '@fastgpt/service/common/mongo/utils';
|
||||
import { addLog } from '@fastgpt/service/common/system/log';
|
||||
import dayjs from 'dayjs';
|
||||
import { ApiRequestProps } from '@fastgpt/service/type/next';
|
||||
import { replaceRegChars } from '@fastgpt/global/common/string/tools';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { useIPFrequencyLimit } from '@fastgpt/service/common/middle/reqFrequencyLimit';
|
||||
import { GetAppChatLogsProps } from '@/global/core/api/appReq';
|
||||
import { authApp } from '@fastgpt/service/support/permission/app/auth';
|
||||
import { Types } from 'mongoose';
|
||||
import { MongoChat } from '@fastgpt/service/core/chat/chatSchema';
|
||||
import { ChatItemCollectionName } from '@fastgpt/service/core/chat/chatItemSchema';
|
||||
import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema';
|
||||
import { ChatItemValueTypeEnum, ChatSourceEnum } from '@fastgpt/global/core/chat/constants';
|
||||
import { AIChatItemValueItemType } from '@fastgpt/global/core/chat/type';
|
||||
|
||||
export type ExportChatLogsBody = GetAppChatLogsProps & {
|
||||
title: string;
|
||||
sourcesMap: Record<string, { label: string }>;
|
||||
};
|
||||
|
||||
async function handler(req: ApiRequestProps<ExportChatLogsBody, {}>, res: NextApiResponse) {
|
||||
let {
|
||||
appId,
|
||||
dateStart = addDays(new Date(), -7),
|
||||
dateEnd = new Date(),
|
||||
sources,
|
||||
logTitle,
|
||||
|
||||
title,
|
||||
sourcesMap
|
||||
} = req.body;
|
||||
|
||||
if (!appId) {
|
||||
throw new Error('缺少参数');
|
||||
}
|
||||
|
||||
const { teamId } = await authApp({ req, authToken: true, appId, per: WritePermissionVal });
|
||||
const teamMembers = await MongoTeamMember.find({ teamId });
|
||||
|
||||
const where = {
|
||||
teamId: new Types.ObjectId(teamId),
|
||||
appId: new Types.ObjectId(appId),
|
||||
updateTime: {
|
||||
$gte: new Date(dateStart),
|
||||
$lte: new Date(dateEnd)
|
||||
},
|
||||
...(sources && { source: { $in: sources } }),
|
||||
...(logTitle && {
|
||||
$or: [
|
||||
{ title: { $regex: new RegExp(`${replaceRegChars(logTitle)}`, 'i') } },
|
||||
{ customTitle: { $regex: new RegExp(`${replaceRegChars(logTitle)}`, 'i') } }
|
||||
]
|
||||
})
|
||||
};
|
||||
|
||||
res.setHeader('Content-Type', 'text/csv; charset=utf-8;');
|
||||
res.setHeader('Content-Disposition', 'attachment; filename=usage.csv; ');
|
||||
|
||||
const cursor = MongoChat.aggregate(
|
||||
[
|
||||
{ $match: where },
|
||||
{
|
||||
$sort: {
|
||||
userBadFeedbackCount: -1,
|
||||
userGoodFeedbackCount: -1,
|
||||
customFeedbacksCount: -1,
|
||||
updateTime: -1
|
||||
}
|
||||
},
|
||||
{ $limit: 50000 },
|
||||
{
|
||||
$lookup: {
|
||||
from: ChatItemCollectionName,
|
||||
let: { chatId: '$chatId' },
|
||||
pipeline: [
|
||||
{
|
||||
$match: {
|
||||
$expr: {
|
||||
$and: [
|
||||
{ $eq: ['$appId', new Types.ObjectId(appId)] },
|
||||
{ $eq: ['$chatId', '$$chatId'] }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
value: 1,
|
||||
userGoodFeedback: 1,
|
||||
userBadFeedback: 1,
|
||||
customFeedbacks: 1,
|
||||
adminFeedback: 1
|
||||
}
|
||||
}
|
||||
],
|
||||
as: 'chatitems'
|
||||
}
|
||||
},
|
||||
{
|
||||
$addFields: {
|
||||
userGoodFeedbackCount: {
|
||||
$size: {
|
||||
$filter: {
|
||||
input: '$chatitems',
|
||||
as: 'item',
|
||||
cond: { $ifNull: ['$$item.userGoodFeedback', false] }
|
||||
}
|
||||
}
|
||||
},
|
||||
userBadFeedbackCount: {
|
||||
$size: {
|
||||
$filter: {
|
||||
input: '$chatitems',
|
||||
as: 'item',
|
||||
cond: { $ifNull: ['$$item.userBadFeedback', false] }
|
||||
}
|
||||
}
|
||||
},
|
||||
customFeedbacksCount: {
|
||||
$size: {
|
||||
$filter: {
|
||||
input: '$chatitems',
|
||||
as: 'item',
|
||||
cond: { $gt: [{ $size: { $ifNull: ['$$item.customFeedbacks', []] } }, 0] }
|
||||
}
|
||||
}
|
||||
},
|
||||
markCount: {
|
||||
$size: {
|
||||
$filter: {
|
||||
input: '$chatitems',
|
||||
as: 'item',
|
||||
cond: { $ifNull: ['$$item.adminFeedback', false] }
|
||||
}
|
||||
}
|
||||
},
|
||||
chatDetails: {
|
||||
$map: {
|
||||
input: '$chatitems',
|
||||
as: 'item',
|
||||
in: {
|
||||
id: '$$item._id',
|
||||
value: '$$item.value'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
_id: 1,
|
||||
id: '$chatId',
|
||||
title: 1,
|
||||
customTitle: 1,
|
||||
source: 1,
|
||||
time: '$updateTime',
|
||||
messageCount: { $size: '$chatitems' },
|
||||
userGoodFeedbackCount: 1,
|
||||
userBadFeedbackCount: 1,
|
||||
customFeedbacksCount: 1,
|
||||
markCount: 1,
|
||||
outLinkUid: 1,
|
||||
tmbId: 1,
|
||||
chatDetails: 1
|
||||
}
|
||||
}
|
||||
],
|
||||
{
|
||||
...readFromSecondary
|
||||
}
|
||||
).cursor({ batchSize: 1000 });
|
||||
|
||||
const write = responseWriteController({
|
||||
res,
|
||||
readStream: cursor
|
||||
});
|
||||
|
||||
write(`\uFEFF${title}`);
|
||||
|
||||
cursor.on('data', (doc) => {
|
||||
const time = dayjs(doc.time.toISOString()).format('YYYY-MM-DD HH:mm:ss');
|
||||
const source = sourcesMap[doc.source as ChatSourceEnum]?.label || doc.source;
|
||||
const title = doc.customTitle || doc.title;
|
||||
const tmb = doc.outLinkUid
|
||||
? doc.outLinkUid
|
||||
: teamMembers.find((member) => String(member._id) === String(doc.tmbId))?.name;
|
||||
|
||||
const messageCount = doc.messageCount;
|
||||
const userFeedbackCount = doc.userGoodFeedbackCount || doc.userBadFeedbackCount || '-';
|
||||
const customFeedbacksCount = doc.customFeedbacksCount || '-';
|
||||
const markCount = doc.markCount;
|
||||
const chatDetails = doc.chatDetails.map(
|
||||
(chat: { id: string; value: AIChatItemValueItemType[] }) => {
|
||||
return chat.value.map((item) => {
|
||||
if ([ChatItemValueTypeEnum.text, ChatItemValueTypeEnum.reasoning].includes(item.type)) {
|
||||
return item;
|
||||
}
|
||||
if (item.type === ChatItemValueTypeEnum.tool) {
|
||||
const newTools = item.tools?.map((tool) => {
|
||||
const { functionName, toolAvatar, ...rest } = tool;
|
||||
return {
|
||||
...rest
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
...item,
|
||||
tools: newTools
|
||||
};
|
||||
}
|
||||
if (item.type === ChatItemValueTypeEnum.interactive) {
|
||||
const newInteractive = {
|
||||
type: item.interactive?.type,
|
||||
params: item.interactive?.params
|
||||
};
|
||||
|
||||
return {
|
||||
...item,
|
||||
interactive: newInteractive
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
let chatDetailsStr = '';
|
||||
try {
|
||||
chatDetailsStr = JSON.stringify(chatDetails);
|
||||
} catch (e) {
|
||||
addLog.error(`export chat logs error`, e);
|
||||
}
|
||||
|
||||
const res = `\n"${time}","${source}","${tmb}","${title}","${messageCount}","${userFeedbackCount}","${customFeedbacksCount}","${markCount}","${chatDetailsStr}"`;
|
||||
|
||||
write(res);
|
||||
});
|
||||
|
||||
cursor.on('end', () => {
|
||||
cursor.close();
|
||||
res.end();
|
||||
});
|
||||
|
||||
cursor.on('error', (err) => {
|
||||
addLog.error(`export chat logs error`, err);
|
||||
res.status(500);
|
||||
res.end();
|
||||
});
|
||||
}
|
||||
|
||||
export default NextAPI(
|
||||
useIPFrequencyLimit({ id: 'export-chat-logs', seconds: 2, limit: 1, force: true }),
|
||||
handler
|
||||
);
|
@@ -12,6 +12,7 @@ import { readFromSecondary } from '@fastgpt/service/common/mongo/utils';
|
||||
import { parsePaginationRequest } from '@fastgpt/service/common/api/pagination';
|
||||
import { PaginationResponse } from '@fastgpt/web/common/fetch/type';
|
||||
import { addSourceMember } from '@fastgpt/service/support/user/utils';
|
||||
import { replaceRegChars } from '@fastgpt/global/common/string/tools';
|
||||
|
||||
async function handler(
|
||||
req: NextApiRequest,
|
||||
@@ -20,7 +21,9 @@ async function handler(
|
||||
const {
|
||||
appId,
|
||||
dateStart = addDays(new Date(), -7),
|
||||
dateEnd = new Date()
|
||||
dateEnd = new Date(),
|
||||
sources,
|
||||
logTitle
|
||||
} = req.body as GetAppChatLogsParams;
|
||||
|
||||
const { pageSize = 20, offset } = parsePaginationRequest(req);
|
||||
@@ -38,7 +41,14 @@ async function handler(
|
||||
updateTime: {
|
||||
$gte: new Date(dateStart),
|
||||
$lte: new Date(dateEnd)
|
||||
}
|
||||
},
|
||||
...(sources && { source: { $in: sources } }),
|
||||
...(logTitle && {
|
||||
$or: [
|
||||
{ title: { $regex: new RegExp(`${replaceRegChars(logTitle)}`, 'i') } },
|
||||
{ customTitle: { $regex: new RegExp(`${replaceRegChars(logTitle)}`, 'i') } }
|
||||
]
|
||||
})
|
||||
};
|
||||
|
||||
const [list, total] = await Promise.all([
|
||||
@@ -127,6 +137,7 @@ async function handler(
|
||||
_id: 1,
|
||||
id: '$chatId',
|
||||
title: 1,
|
||||
customTitle: 1,
|
||||
source: 1,
|
||||
time: '$updateTime',
|
||||
messageCount: { $size: '$chatitems' },
|
||||
|
1
projects/app/src/types/app.d.ts
vendored
1
projects/app/src/types/app.d.ts
vendored
@@ -40,6 +40,7 @@ export type AppLogsListItemType = {
|
||||
source: string;
|
||||
time: Date;
|
||||
title: string;
|
||||
customTitle: string;
|
||||
messageCount: number;
|
||||
userGoodFeedbackCount: number;
|
||||
userBadFeedbackCount: number;
|
||||
|
Reference in New Issue
Block a user