mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-26 15:54:11 +00:00

* fix: collection list count * fix: collection list count * update doc * perf: init log * yml
407 lines
13 KiB
TypeScript
407 lines
13 KiB
TypeScript
import { getChannelList, getChannelLog, getLogDetail } from '@/web/core/ai/channel';
|
|
import { getSystemModelList } from '@/web/core/ai/config';
|
|
import { useUserStore } from '@/web/support/user/useUserStore';
|
|
import {
|
|
Table,
|
|
Thead,
|
|
Tbody,
|
|
Tr,
|
|
Th,
|
|
Td,
|
|
TableContainer,
|
|
Box,
|
|
Flex,
|
|
Button,
|
|
HStack,
|
|
ModalBody,
|
|
Grid,
|
|
GridItem,
|
|
BoxProps
|
|
} from '@chakra-ui/react';
|
|
import { getModelProvider } from '@fastgpt/global/core/ai/provider';
|
|
import DateRangePicker, { DateRangeType } from '@fastgpt/web/components/common/DateRangePicker';
|
|
import MyBox from '@fastgpt/web/components/common/MyBox';
|
|
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
|
|
import MySelect from '@fastgpt/web/components/common/MySelect';
|
|
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
|
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
|
|
import { addDays } from 'date-fns';
|
|
import { useTranslation } from 'next-i18next';
|
|
import React, { useCallback, useMemo, useState } from 'react';
|
|
import MyIcon from '@fastgpt/web/components/common/Icon';
|
|
import { formatTime2YMDHMS } from '@fastgpt/global/common/string/time';
|
|
import MyModal from '@fastgpt/web/components/common/MyModal';
|
|
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
|
|
|
type LogDetailType = {
|
|
id: number;
|
|
request_id: string;
|
|
channelName: string | number;
|
|
model: React.JSX.Element;
|
|
duration: number;
|
|
request_at: string;
|
|
code: number;
|
|
prompt_tokens: number;
|
|
completion_tokens: number;
|
|
endpoint: string;
|
|
|
|
content?: string;
|
|
request_body?: string;
|
|
response_body?: string;
|
|
};
|
|
const ChannelLog = ({ Tab }: { Tab: React.ReactNode }) => {
|
|
const { t } = useTranslation();
|
|
const { userInfo } = useUserStore();
|
|
|
|
const isRoot = userInfo?.username === 'root';
|
|
const [filterProps, setFilterProps] = useState<{
|
|
channelId?: string;
|
|
model?: string;
|
|
code_type: 'all' | 'success' | 'error';
|
|
dateRange: DateRangeType;
|
|
}>({
|
|
code_type: 'all',
|
|
dateRange: {
|
|
from: (() => {
|
|
const today = addDays(new Date(), -1);
|
|
today.setHours(0, 0, 0, 0);
|
|
return today;
|
|
})(),
|
|
to: (() => {
|
|
const today = new Date();
|
|
today.setHours(23, 59, 59, 999);
|
|
return today;
|
|
})()
|
|
}
|
|
});
|
|
|
|
const { data: channelList = [] } = useRequest2(
|
|
async () => {
|
|
const res = await getChannelList().then((res) =>
|
|
res.map((item) => ({
|
|
label: item.name,
|
|
value: `${item.id}`
|
|
}))
|
|
);
|
|
return [
|
|
{
|
|
label: t('common:common.All'),
|
|
value: ''
|
|
},
|
|
...res
|
|
];
|
|
},
|
|
{
|
|
manual: false
|
|
}
|
|
);
|
|
|
|
const { data: systemModelList = [] } = useRequest2(getSystemModelList, {
|
|
manual: false
|
|
});
|
|
const modelList = useMemo(() => {
|
|
const res = systemModelList
|
|
.map((item) => {
|
|
const provider = getModelProvider(item.provider);
|
|
|
|
return {
|
|
order: provider.order,
|
|
icon: provider.avatar,
|
|
label: item.model,
|
|
value: item.model
|
|
};
|
|
})
|
|
.sort((a, b) => a.order - b.order);
|
|
return [
|
|
{
|
|
label: t('common:common.All'),
|
|
value: ''
|
|
},
|
|
...res
|
|
];
|
|
}, [systemModelList, t]);
|
|
|
|
const { data, isLoading, ScrollData } = useScrollPagination(getChannelLog, {
|
|
pageSize: 20,
|
|
refreshDeps: [filterProps],
|
|
params: {
|
|
channel: filterProps.channelId,
|
|
model_name: filterProps.model,
|
|
code_type: filterProps.code_type,
|
|
start_timestamp: filterProps.dateRange.from?.getTime() || 0,
|
|
end_timestamp: filterProps.dateRange.to?.getTime() || 0
|
|
}
|
|
});
|
|
|
|
const formatData = useMemo<LogDetailType[]>(() => {
|
|
return data.map((item) => {
|
|
const duration = item.created_at - item.request_at;
|
|
const durationSecond = duration / 1000;
|
|
|
|
const channelName = channelList.find((channel) => channel.value === `${item.channel}`)?.label;
|
|
|
|
const model = systemModelList.find((model) => model.model === item.model);
|
|
const provider = getModelProvider(model?.provider);
|
|
|
|
return {
|
|
id: item.id,
|
|
channelName: channelName || item.channel,
|
|
model: (
|
|
<HStack>
|
|
<MyIcon name={provider?.avatar as any} w={'1rem'} />
|
|
<Box>{model?.model}</Box>
|
|
</HStack>
|
|
),
|
|
duration: durationSecond,
|
|
request_at: formatTime2YMDHMS(item.request_at),
|
|
code: item.code,
|
|
prompt_tokens: item.prompt_tokens,
|
|
completion_tokens: item.completion_tokens,
|
|
request_id: item.request_id,
|
|
endpoint: item.endpoint,
|
|
content: item.content
|
|
};
|
|
});
|
|
}, [channelList, data, systemModelList]);
|
|
|
|
const [logDetail, setLogDetail] = useState<LogDetailType>();
|
|
|
|
return (
|
|
<>
|
|
{isRoot && (
|
|
<Flex alignItems={'center'}>
|
|
{Tab}
|
|
<Box flex={1} />
|
|
</Flex>
|
|
)}
|
|
<HStack spacing={4}>
|
|
<HStack>
|
|
<FormLabel>{t('common:user.Time')}</FormLabel>
|
|
<Box>
|
|
<DateRangePicker
|
|
defaultDate={filterProps.dateRange}
|
|
dateRange={filterProps.dateRange}
|
|
position="bottom"
|
|
onSuccess={(e) => setFilterProps({ ...filterProps, dateRange: e })}
|
|
/>
|
|
</Box>
|
|
</HStack>
|
|
<HStack flex={'0 0 200px'}>
|
|
<FormLabel>{t('account_model:channel_name')}</FormLabel>
|
|
<Box flex={'1 0 0'}>
|
|
<MySelect<string>
|
|
bg={'myGray.50'}
|
|
isSearch
|
|
list={channelList}
|
|
placeholder={t('account_model:select_channel')}
|
|
value={filterProps.channelId}
|
|
onchange={(val) => setFilterProps({ ...filterProps, channelId: val })}
|
|
/>
|
|
</Box>
|
|
</HStack>
|
|
<HStack flex={'0 0 200px'}>
|
|
<FormLabel>{t('account_model:model_name')}</FormLabel>
|
|
<Box flex={'1 0 0'}>
|
|
<MySelect<string>
|
|
bg={'myGray.50'}
|
|
isSearch
|
|
list={modelList}
|
|
placeholder={t('account_model:select_model')}
|
|
value={filterProps.model}
|
|
onchange={(val) => setFilterProps({ ...filterProps, model: val })}
|
|
/>
|
|
</Box>
|
|
</HStack>
|
|
<HStack flex={'0 0 200px'}>
|
|
<FormLabel>{t('account_model:log_status')}</FormLabel>
|
|
<Box flex={'1 0 0'}>
|
|
<MySelect<'all' | 'success' | 'error'>
|
|
bg={'myGray.50'}
|
|
list={[
|
|
{ label: t('common:common.All'), value: 'all' },
|
|
{ label: t('common:common.Success'), value: 'success' },
|
|
{ label: t('common:common.failed'), value: 'error' }
|
|
]}
|
|
value={filterProps.code_type}
|
|
onchange={(val) => setFilterProps({ ...filterProps, code_type: val })}
|
|
/>
|
|
</Box>
|
|
</HStack>
|
|
</HStack>
|
|
<MyBox flex={'1 0 0'} h={0} isLoading={isLoading}>
|
|
<ScrollData h={'100%'}>
|
|
<TableContainer fontSize={'sm'}>
|
|
<Table>
|
|
<Thead>
|
|
<Tr>
|
|
<Th>{t('account_model:channel_name')}</Th>
|
|
<Th>{t('account_model:model')}</Th>
|
|
<Th>{t('account_model:model_tokens')}</Th>
|
|
<Th>{t('account_model:duration')}</Th>
|
|
<Th>{t('account_model:channel_status')}</Th>
|
|
<Th>{t('account_model:request_at')}</Th>
|
|
<Th></Th>
|
|
</Tr>
|
|
</Thead>
|
|
<Tbody>
|
|
{formatData.map((item) => (
|
|
<Tr key={item.id}>
|
|
<Td>{item.channelName}</Td>
|
|
<Td>{item.model}</Td>
|
|
<Td>
|
|
{item.prompt_tokens} / {item.completion_tokens}
|
|
</Td>
|
|
<Td color={item.duration > 10 ? 'red.600' : ''}>{item.duration.toFixed(2)}s</Td>
|
|
<Td color={item.code === 200 ? 'green.600' : 'red.600'}>
|
|
{item.code}
|
|
{item.content && <QuestionTip label={item.content} />}
|
|
</Td>
|
|
<Td>{item.request_at}</Td>
|
|
<Td>
|
|
<Button
|
|
leftIcon={<MyIcon name={'menu'} w={'1rem'} />}
|
|
size={'sm'}
|
|
variant={'outline'}
|
|
onClick={() => setLogDetail(item)}
|
|
>
|
|
{t('account_model:detail')}
|
|
</Button>
|
|
</Td>
|
|
</Tr>
|
|
))}
|
|
</Tbody>
|
|
</Table>
|
|
</TableContainer>
|
|
</ScrollData>
|
|
</MyBox>
|
|
|
|
{!!logDetail && <LogDetail data={logDetail} onClose={() => setLogDetail(undefined)} />}
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default ChannelLog;
|
|
|
|
const LogDetail = ({ data, onClose }: { data: LogDetailType; onClose: () => void }) => {
|
|
const { t } = useTranslation();
|
|
const { data: detailData } = useRequest2(
|
|
async () => {
|
|
if (data.code === 200) return data;
|
|
const res = await getLogDetail(data.id);
|
|
return {
|
|
...res,
|
|
...data
|
|
};
|
|
},
|
|
{
|
|
manual: false
|
|
}
|
|
);
|
|
|
|
const Title = useCallback(({ children, ...props }: { children: React.ReactNode } & BoxProps) => {
|
|
return (
|
|
<Box
|
|
bg={'myGray.50'}
|
|
color="myGray.900 "
|
|
borderRight={'base'}
|
|
p={3}
|
|
flex={'0 0 100px'}
|
|
{...props}
|
|
>
|
|
{children}
|
|
</Box>
|
|
);
|
|
}, []);
|
|
const Container = useCallback(
|
|
({ children, ...props }: { children: React.ReactNode } & BoxProps) => {
|
|
return (
|
|
<Box p={3} flex={1} {...props}>
|
|
{children}
|
|
</Box>
|
|
);
|
|
},
|
|
[]
|
|
);
|
|
|
|
return (
|
|
<MyModal
|
|
isOpen
|
|
iconSrc="support/bill/payRecordLight"
|
|
title={t('account_model:log_detail')}
|
|
onClose={onClose}
|
|
maxW={['90vw', '800px']}
|
|
w={'100%'}
|
|
>
|
|
{detailData && (
|
|
<ModalBody>
|
|
{/* 基本信息表格 */}
|
|
<Grid
|
|
templateColumns="repeat(2, 1fr)"
|
|
gap={0}
|
|
borderWidth="1px"
|
|
borderRadius="md"
|
|
fontSize={'sm'}
|
|
overflow={'hidden'}
|
|
>
|
|
{/* 第一行 */}
|
|
<GridItem display={'flex'} borderBottomWidth="1px" borderRightWidth="1px">
|
|
<Title>RequestID</Title>
|
|
<Container>{detailData?.request_id}</Container>
|
|
</GridItem>
|
|
<GridItem display={'flex'} borderBottomWidth="1px" borderRightWidth="1px">
|
|
<Title>{t('account_model:channel_status')}</Title>
|
|
<Container color={detailData.code === 200 ? 'green.600' : 'red.600'}>
|
|
{detailData?.code}
|
|
</Container>
|
|
</GridItem>
|
|
<GridItem display={'flex'} borderBottomWidth="1px" borderRightWidth="1px">
|
|
<Title>Endpoint</Title>
|
|
<Container>{detailData?.endpoint}</Container>
|
|
</GridItem>
|
|
<GridItem display={'flex'} borderBottomWidth="1px" borderRightWidth="1px">
|
|
<Title>{t('account_model:channel_name')}</Title>
|
|
<Container>{detailData?.channelName}</Container>
|
|
</GridItem>
|
|
<GridItem display={'flex'} borderBottomWidth="1px" borderRightWidth="1px">
|
|
<Title>{t('account_model:request_at')}</Title>
|
|
<Container>{detailData?.request_at}</Container>
|
|
</GridItem>
|
|
<GridItem display={'flex'} borderBottomWidth="1px" borderRightWidth="1px">
|
|
<Title>{t('account_model:duration')}</Title>
|
|
<Container>{detailData?.duration.toFixed(2)}s</Container>
|
|
</GridItem>
|
|
<GridItem display={'flex'} borderBottomWidth="1px" borderRightWidth="1px">
|
|
<Title>{t('account_model:model')}</Title>
|
|
<Container>{detailData?.model}</Container>
|
|
</GridItem>
|
|
<GridItem display={'flex'} borderBottomWidth="1px" borderRightWidth="1px">
|
|
<Title flex={'0 0 150px'}>{t('account_model:model_tokens')}</Title>
|
|
<Container>
|
|
{detailData?.prompt_tokens} / {detailData?.completion_tokens}
|
|
</Container>
|
|
</GridItem>
|
|
{detailData?.content && (
|
|
<GridItem display={'flex'} borderBottomWidth="1px" borderRightWidth="1px" colSpan={2}>
|
|
<Title>Content</Title>
|
|
<Container>{detailData?.content}</Container>
|
|
</GridItem>
|
|
)}
|
|
{detailData?.request_body && (
|
|
<GridItem display={'flex'} borderBottomWidth="1px" borderRightWidth="1px" colSpan={2}>
|
|
<Title>Request Body</Title>
|
|
<Container userSelect={'all'}>{detailData?.request_body}</Container>
|
|
</GridItem>
|
|
)}
|
|
{detailData?.response_body && (
|
|
<GridItem display={'flex'} borderBottomWidth="1px" borderRightWidth="1px" colSpan={2}>
|
|
<Title>Response Body</Title>
|
|
<Container>{detailData?.response_body}</Container>
|
|
</GridItem>
|
|
)}
|
|
</Grid>
|
|
</ModalBody>
|
|
)}
|
|
</MyModal>
|
|
);
|
|
};
|