Feat: ai proxy monitor (#4985)

* Aiproxy ModelBoard (#4983)

* Aiproxy ModelBoard

* Add components LineChartComponent and Make some revisions

* perf: ai proxy dashboard

* doc

* remove invalid i18n

* remove invalid i18n

---------

Co-authored-by: Zhuangzai fa <143257420+ctrlz526@users.noreply.github.com>
This commit is contained in:
Archer
2025-06-10 01:46:10 +08:00
committed by GitHub
parent 01ff56b42b
commit 101a6e9516
10 changed files with 657 additions and 10 deletions

View File

@@ -9,6 +9,7 @@ weight: 789
## 🚀 新增内容 ## 🚀 新增内容
1. AI proxy 服务,支持以图表形式展示模型调用情况。
1. 商业版支持知识库分块时LLM 进行自动分段识别。 1. 商业版支持知识库分块时LLM 进行自动分段识别。
## ⚙️ 优化 ## ⚙️ 优化

View File

@@ -1,5 +1,7 @@
{ {
"Hunyuan": "Tencent Hunyuan", "Hunyuan": "Tencent Hunyuan",
"aipoint_usage": "AI points",
"all": "All",
"api_key": "API key", "api_key": "API key",
"azure": "Azure", "azure": "Azure",
"base_url": "Base url", "base_url": "Base url",
@@ -16,6 +18,10 @@
"confirm_delete_channel": "Confirm the deletion of the [{{name}}] channel?", "confirm_delete_channel": "Confirm the deletion of the [{{name}}] channel?",
"copy_model_id_success": "Copyed model id", "copy_model_id_success": "Copyed model id",
"create_channel": "Added channels", "create_channel": "Added channels",
"dashboard_error_calls": "Error Calls",
"dashboard_model": "Model",
"dashboard_points": "points",
"dashboard_token_usage": "Tokens",
"default_url": "Default address", "default_url": "Default address",
"detail": "Detail", "detail": "Detail",
"duration": "Duration", "duration": "Duration",
@@ -23,6 +29,7 @@
"edit_channel": "Channel configuration", "edit_channel": "Channel configuration",
"enable_channel": "Enable", "enable_channel": "Enable",
"forbid_channel": "Disabled", "forbid_channel": "Disabled",
"input": "Input",
"key_type": "API key format:", "key_type": "API key format:",
"log": "Call log", "log": "Call log",
"log_detail": "Log details", "log_detail": "Log details",
@@ -33,9 +40,14 @@
"maxToken_tip": "Model max_tokens parameter", "maxToken_tip": "Model max_tokens parameter",
"max_temperature_tip": "If the model temperature parameter is not filled in, it means that the model does not support the temperature parameter.", "max_temperature_tip": "If the model temperature parameter is not filled in, it means that the model does not support the temperature parameter.",
"model": "Model", "model": "Model",
"model_error_rate": "Error rate",
"model_error_request_times": "Number of failures",
"model_name": "Model name", "model_name": "Model name",
"model_request_times": "Request times",
"model_test": "Model testing", "model_test": "Model testing",
"model_tokens": "Input/Output tokens", "model_tokens": "Input/Output tokens",
"monitoring": "Monitoring",
"output": "Output",
"request_at": "Request time", "request_at": "Request time",
"request_duration": "Request duration: {{duration}}s", "request_duration": "Request duration: {{duration}}s",
"retry_times": "Number of retry times", "retry_times": "Number of retry times",

View File

@@ -1,5 +1,6 @@
{ {
"Hunyuan": "腾讯混元", "Hunyuan": "腾讯混元",
"all": "全部",
"api_key": "API 密钥", "api_key": "API 密钥",
"azure": "微软 Azure", "azure": "微软 Azure",
"base_url": "代理地址", "base_url": "代理地址",
@@ -16,6 +17,11 @@
"confirm_delete_channel": "确认删除 【{{name}}】渠道?", "confirm_delete_channel": "确认删除 【{{name}}】渠道?",
"copy_model_id_success": "已复制模型id", "copy_model_id_success": "已复制模型id",
"create_channel": "新增渠道", "create_channel": "新增渠道",
"aipoint_usage": "积分消耗",
"dashboard_error_calls": "错误次数",
"dashboard_model": "模型",
"dashboard_points": "积分",
"dashboard_token_usage": "Tokens 消耗",
"default_url": "默认地址", "default_url": "默认地址",
"detail": "详情", "detail": "详情",
"duration": "耗时", "duration": "耗时",
@@ -23,6 +29,7 @@
"edit_channel": "渠道配置", "edit_channel": "渠道配置",
"enable_channel": "启用", "enable_channel": "启用",
"forbid_channel": "禁用", "forbid_channel": "禁用",
"input": "输入",
"key_type": "API key 格式: ", "key_type": "API key 格式: ",
"log": "调用日志", "log": "调用日志",
"log_detail": "日志详情", "log_detail": "日志详情",
@@ -33,9 +40,14 @@
"maxToken_tip": "模型 max_tokens 参数", "maxToken_tip": "模型 max_tokens 参数",
"max_temperature_tip": "模型 temperature 参数,不填则代表模型不支持 temperature 参数。", "max_temperature_tip": "模型 temperature 参数,不填则代表模型不支持 temperature 参数。",
"model": "模型", "model": "模型",
"model_error_rate": "失败率",
"model_error_request_times": "失败次数",
"model_name": "模型名", "model_name": "模型名",
"model_request_times": "请求次数",
"model_test": "模型测试", "model_test": "模型测试",
"model_tokens": "输入/输出 Tokens", "model_tokens": "输入/输出 Tokens",
"monitoring": "监控",
"output": "输出",
"request_at": "请求时间", "request_at": "请求时间",
"request_duration": "请求时长: {{duration}}s", "request_duration": "请求时长: {{duration}}s",
"retry_times": "重试次数", "retry_times": "重试次数",

View File

@@ -1,5 +1,7 @@
{ {
"Hunyuan": "騰訊混元", "Hunyuan": "騰訊混元",
"aipoint_usage": "積分消耗",
"all": "全部",
"api_key": "API 金鑰", "api_key": "API 金鑰",
"azure": "Azure", "azure": "Azure",
"base_url": "代理地址", "base_url": "代理地址",
@@ -16,6 +18,7 @@
"confirm_delete_channel": "確認刪除【{{name}}】管道?", "confirm_delete_channel": "確認刪除【{{name}}】管道?",
"copy_model_id_success": "已復制模型 id", "copy_model_id_success": "已復制模型 id",
"create_channel": "新增管道", "create_channel": "新增管道",
"dashboard_token_usage": "Tokens 消耗",
"default_url": "預設地址", "default_url": "預設地址",
"detail": "詳細資訊", "detail": "詳細資訊",
"duration": "耗時", "duration": "耗時",
@@ -23,6 +26,7 @@
"edit_channel": "管道設定", "edit_channel": "管道設定",
"enable_channel": "啟用", "enable_channel": "啟用",
"forbid_channel": "停用", "forbid_channel": "停用",
"input": "輸入",
"key_type": "API key 格式:", "key_type": "API key 格式:",
"log": "呼叫日誌", "log": "呼叫日誌",
"log_detail": "日誌詳細資訊", "log_detail": "日誌詳細資訊",
@@ -33,9 +37,14 @@
"maxToken_tip": "模型 max_tokens 參數", "maxToken_tip": "模型 max_tokens 參數",
"max_temperature_tip": "模型 temperature 參數,不填則代表模型不支援 temperature 參數。", "max_temperature_tip": "模型 temperature 參數,不填則代表模型不支援 temperature 參數。",
"model": "模型", "model": "模型",
"model_error_rate": "失敗率",
"model_error_request_times": "失敗次數",
"model_name": "模型名", "model_name": "模型名",
"model_request_times": "請求次數",
"model_test": "模型測試", "model_test": "模型測試",
"model_tokens": "輸入/輸出 Tokens", "model_tokens": "輸入/輸出 Tokens",
"monitoring": "監控",
"output": "輸出",
"request_at": "請求時間", "request_at": "請求時間",
"request_duration": "請求時長:{{duration}}s", "request_duration": "請求時長:{{duration}}s",
"retry_times": "重試次數", "retry_times": "重試次數",

View File

@@ -52,3 +52,13 @@ export type ChannelLogListItemType = {
content?: string; content?: string;
retry_times?: number; retry_times?: number;
}; };
export type DashboardDataItemType = {
model: string;
request_count: number;
used_amount: number;
exception_count: number;
input_tokens?: number;
output_tokens?: number;
total_tokens?: number;
};

View File

@@ -0,0 +1,169 @@
import React, { useCallback, useMemo, useState } from 'react';
import { Box, HStack, useTheme } from '@chakra-ui/react';
import {
ResponsiveContainer,
AreaChart,
Area,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
type TooltipProps
} from 'recharts';
import { type NameType, type ValueType } from 'recharts/types/component/DefaultTooltipContent';
import { formatNumber } from '@fastgpt/global/common/math/tools';
type XAxisConfig = {
dataKey: string;
};
type LineConfig = {
dataKey: string;
name: string;
color: string;
gradient?: boolean;
};
type TooltipItem = {
label: string;
dataKey: string;
color: string;
formatter?: (value: number) => string;
customValue?: (data: any) => number;
};
type LineChartComponentProps = {
data: Record<string, any>[];
title: string;
HeaderRightChildren?: React.ReactNode;
lines: LineConfig[];
tooltipItems?: TooltipItem[];
};
const CustomTooltip = ({
active,
payload,
tooltipItems
}: TooltipProps<ValueType, NameType> & { tooltipItems?: TooltipItem[] }) => {
const data = payload?.[0]?.payload;
if (active && data && tooltipItems) {
return (
<Box bg={'white'} p={3} borderRadius={'md'} border={'base'} boxShadow={'sm'}>
<Box fontSize={'sm'} color={'myGray.900'} mb={2}>
{data.x}
</Box>
{tooltipItems.map((item, index) => {
const value = (() => {
if (item.customValue) {
return item.customValue(data);
} else {
return data[item.dataKey];
}
})();
const displayValue = item.formatter ? item.formatter(value) : formatNumber(value);
return (
<HStack key={index} fontSize={'sm'} _notLast={{ mb: 1 }}>
<Box w={2} h={2} borderRadius={'full'} bg={item.color} />
<Box>{item.label}</Box>
<Box>{displayValue}</Box>
</HStack>
);
})}
</Box>
);
}
return null;
};
const LineChartComponent = ({
data,
title,
HeaderRightChildren,
lines,
tooltipItems
}: LineChartComponentProps) => {
const theme = useTheme();
// Y-axis number formatter function
const formatYAxisNumber = useCallback((value: number): string => {
if (value >= 1000000) {
return value / 1000000 + 'M';
} else if (value >= 1000) {
return value / 1000 + 'K';
}
return value.toString();
}, []);
// Generate gradient definitions
const gradientDefs = useMemo(() => {
return (
<defs>
{lines.map((line, index) => (
<linearGradient
key={`gradient-${line.color}`}
id={`gradient-${line.color}`}
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop offset="0%" stopColor={line.color} stopOpacity={0.25} />
<stop offset="100%" stopColor={line.color} stopOpacity={0.01} />
</linearGradient>
))}
</defs>
);
}, [lines]);
return (
<>
<HStack mb={4} justifyContent={'space-between'} alignItems={'flex-start'}>
<Box fontSize={'sm'} color={'myGray.900'} fontWeight={'medium'}>
{title}
</Box>
{HeaderRightChildren && HeaderRightChildren}
</HStack>
<ResponsiveContainer width="100%" height={'100%'}>
<AreaChart
data={data}
margin={{ top: 5, right: 30, left: 0, bottom: HeaderRightChildren ? 30 : 15 }}
>
{gradientDefs}
<XAxis
dataKey={'x'}
tickMargin={10}
tick={{ fontSize: '12px', color: theme.colors.myGray['500'], fontWeight: '500' }}
interval={'preserveStartEnd'}
/>
<YAxis
axisLine={false}
tickSize={0}
tickMargin={10}
tick={{ fontSize: '12px', color: theme.colors.myGray['500'], fontWeight: '500' }}
interval={'preserveStartEnd'}
tickFormatter={formatYAxisNumber}
/>
<CartesianGrid strokeDasharray="3 3" horizontal={true} vertical={false} />
{tooltipItems && <Tooltip content={<CustomTooltip tooltipItems={tooltipItems} />} />}
{lines.map((line, index) => (
<Area
key={index}
type="monotone"
name={line.name}
dataKey={line.dataKey}
stroke={line.color}
strokeWidth={2}
fill={`url(#gradient-${line.color})`}
dot={false}
/>
))}
</AreaChart>
</ResponsiveContainer>
</>
);
};
export default LineChartComponent;

View File

@@ -0,0 +1,405 @@
import React, { useMemo, useState } from 'react';
import type { BoxProps } from '@chakra-ui/react';
import { Box, Flex, Grid, HStack, useTheme } from '@chakra-ui/react';
import { formatNumber } from '@fastgpt/global/common/math/tools';
import MyBox from '@fastgpt/web/components/common/MyBox';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next';
import { addDays } from 'date-fns';
import dayjs from 'dayjs';
import DateRangePicker, {
type DateRangeType
} from '@fastgpt/web/components/common/DateRangePicker';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import MySelect from '@fastgpt/web/components/common/MySelect';
import { getChannelList, getDashboardV2 } from '@/web/core/ai/channel';
import { getSystemModelList } from '@/web/core/ai/config';
import { getModelProvider } from '@fastgpt/global/core/ai/provider';
import LineChartComponent from './LineChartComponent';
import FillRowTabs from '@fastgpt/web/components/common/Tabs/FillRowTabs';
import { useSystemStore } from '@/web/common/system/useSystemStore';
export type ModelDashboardData = {
x: string;
totalCalls: number;
errorCalls: number;
errorRate: number;
inputTokens: number;
outputTokens: number;
totalTokens: number;
totalCost: number;
};
const ChartsBoxStyles: BoxProps = {
px: 5,
pt: 4,
pb: 8,
h: '300px',
border: 'base',
borderRadius: 'md',
overflow: 'hidden'
};
// Displays model usage statistics, token consumption and cost visualization
const ModelDashboard = ({ Tab }: { Tab: React.ReactNode }) => {
const { t } = useTranslation();
const theme = useTheme();
const { feConfigs } = useSystemStore();
const [filterProps, setFilterProps] = useState<{
channelId?: string;
model?: string;
dateRange: DateRangeType;
}>({
channelId: undefined,
model: undefined,
dateRange: {
from: (() => {
const today = addDays(new Date(), -7);
today.setHours(0, 0, 0, 0);
return today;
})(),
to: (() => {
const today = new Date();
today.setHours(23, 59, 59, 999);
return today;
})()
}
});
// Fetch channel list with "All" option
const { data: channelList = [] } = useRequest2(
async () => {
const res = await getChannelList().then((res) =>
res.map((item) => ({
label: item.name,
value: `${item.id}`
}))
);
return [
{
label: t('common:All'),
value: ''
},
...res
];
},
{
manual: false
}
);
// Get model list filtered by selected channel
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:All'),
value: ''
},
...res
];
}, [systemModelList, t]);
// Fetch dashboard data with date range and channel filters
const { data: dashboardData = [], loading: isLoading } = useRequest2(
async () => {
const params = {
channel: filterProps.channelId ? parseInt(filterProps.channelId) : 0,
start_timestamp: filterProps.dateRange.from
? Math.floor(filterProps.dateRange.from.getTime())
: undefined,
end_timestamp: filterProps.dateRange.to
? Math.floor(filterProps.dateRange.to.getTime())
: undefined,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
timespan: 'day' as const
};
let data = await getDashboardV2(params);
if (filterProps.model) {
data = data.map((item) => {
const filterModels = item.models.filter((model) => model.model === filterProps.model);
return {
...item,
models: filterModels
};
});
}
return data;
},
{
manual: false,
refreshDeps: [filterProps.channelId, filterProps.dateRange, filterProps.model]
}
);
// Process chart data - aggregate daily model calls, token usage and cost data
const chartData: ModelDashboardData[] = useMemo(() => {
if (dashboardData.length === 0) {
return [];
}
// Model price map
const modelPriceMap = new Map<
string,
{
inputPrice?: number;
outputPrice?: number;
charsPointsPrice?: number;
}
>();
systemModelList.forEach((model) => {
modelPriceMap.set(model.model, {
inputPrice: model.inputPrice,
outputPrice: model.outputPrice,
charsPointsPrice: model.charsPointsPrice
});
});
return dashboardData.map((item) => {
const date = dayjs(item.timestamp * 1000).format('MM-DD');
const totalCalls = item.models.reduce((acc, model) => acc + model.request_count, 0);
const errorCalls = item.models.reduce((acc, model) => acc + model.exception_count, 0);
const errorRate = Number((errorCalls / totalCalls).toFixed(2));
const inputTokens = item.models.reduce((acc, model) => acc + (model?.input_tokens || 0), 0);
const outputTokens = item.models.reduce((acc, model) => acc + (model?.output_tokens || 0), 0);
const totalTokens = item.models.reduce((acc, model) => acc + (model?.total_tokens || 0), 0);
const totalCost = item.models.reduce((acc, model) => {
const modelPricing = modelPriceMap.get(model.model);
if (modelPricing) {
const inputTokens = model.input_tokens || 0;
const outputTokens = model.output_tokens || 0;
const isIOPriceType =
typeof modelPricing.inputPrice === 'number' && modelPricing.inputPrice > 0;
const totalPoints = isIOPriceType
? (modelPricing.inputPrice || 0) * (inputTokens / 1000) +
(modelPricing.outputPrice || 0) * (outputTokens / 1000)
: ((modelPricing.charsPointsPrice || 0) * (inputTokens + outputTokens)) / 1000;
return acc + totalPoints;
}
return acc;
}, 0);
return {
x: date,
totalCalls,
errorCalls,
errorRate,
inputTokens,
outputTokens,
totalTokens,
totalCost
};
});
}, [dashboardData, systemModelList]);
const [tokensUsageType, setTokensUsageType] = useState<
'inputTokens' | 'outputTokens' | 'totalTokens'
>('totalTokens');
console.log(chartData);
return (
<>
<Box>{Tab}</Box>
<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>
<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>
<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>
<MyBox flex={'1 0 0'} h={0} overflowY={'auto'} isLoading={isLoading}>
{dashboardData && dashboardData.length > 0 && (
<>
<Box {...ChartsBoxStyles}>
<LineChartComponent
data={chartData}
title={t('account_model:model_request_times')}
lines={[
{
dataKey: 'totalCalls',
name: t('account_model:model_request_times'),
color: theme.colors.primary['600']
}
]}
tooltipItems={[
{
label: t('account_model:model_request_times'),
dataKey: 'totalCalls',
color: theme.colors.primary['600']
}
]}
/>
</Box>
<Grid mt={5} gridTemplateColumns={['1fr', '1fr 1fr']} gap={5}>
<Box {...ChartsBoxStyles}>
<LineChartComponent
data={chartData}
title={t('account_model:model_error_request_times')}
lines={[
{
dataKey: 'errorCalls',
name: t('account_model:model_error_request_times'),
color: '#f98e1a'
}
]}
tooltipItems={[
{
label: t('account_model:model_error_request_times'),
dataKey: 'errorCalls',
color: '#f98e1a'
}
]}
/>
</Box>
<Box {...ChartsBoxStyles}>
<LineChartComponent
data={chartData}
title={t('account_model:model_error_rate')}
lines={[
{
dataKey: 'errorRate',
name: t('account_model:model_error_rate'),
color: '#e84738'
}
]}
tooltipItems={[
{
label: t('account_model:model_error_rate'),
dataKey: 'errorRate',
color: '#e84738'
}
]}
/>
</Box>
</Grid>
<Box mt={5} {...ChartsBoxStyles}>
<LineChartComponent
data={chartData}
title={t('account_model:dashboard_token_usage')}
lines={[
{
dataKey: tokensUsageType,
name: t('account_model:dashboard_token_usage'),
color: theme.colors.primary['600']
}
]}
tooltipItems={[
{
label: t('account_model:dashboard_token_usage'),
dataKey: tokensUsageType,
color: theme.colors.primary['600']
}
]}
HeaderRightChildren={
<FillRowTabs<'inputTokens' | 'outputTokens' | 'totalTokens'>
list={[
{
label: t('account_model:all'),
value: 'totalTokens'
},
{
label: t('account_model:input'),
value: 'inputTokens'
},
{
label: t('account_model:output'),
value: 'outputTokens'
}
]}
py={1}
px={5}
value={tokensUsageType}
onChange={(val) => setTokensUsageType(val)}
/>
}
/>
</Box>
{feConfigs?.isPlus && (
<Box mt={5} {...ChartsBoxStyles}>
<LineChartComponent
data={chartData}
title={t('account_model:aipoint_usage')}
lines={[
{
dataKey: 'totalCost',
name: t('account_model:aipoint_usage'),
color: '#8774EE'
}
]}
tooltipItems={[
{
label: t('account_model:aipoint_usage'),
dataKey: 'totalCost',
color: '#8774EE'
}
]}
/>
</Box>
)}
</>
)}
</MyBox>
</>
);
};
export default React.memo(ModelDashboard);

View File

@@ -13,8 +13,9 @@ import { useSystemStore } from '@/web/common/system/useSystemStore';
const ModelConfigTable = dynamic(() => import('@/pageComponents/account/model/ModelConfigTable')); const ModelConfigTable = dynamic(() => import('@/pageComponents/account/model/ModelConfigTable'));
const ChannelTable = dynamic(() => import('@/pageComponents/account/model/Channel')); const ChannelTable = dynamic(() => import('@/pageComponents/account/model/Channel'));
const ChannelLog = dynamic(() => import('@/pageComponents/account/model/Log')); const ChannelLog = dynamic(() => import('@/pageComponents/account/model/Log'));
const ModelDashboard = dynamic(() => import('@/pageComponents/account/model/ModelDashboard'));
type TabType = 'model' | 'config' | 'channel' | 'channel_log'; type TabType = 'model' | 'config' | 'channel' | 'channel_log' | 'account_model';
const ModelProvider = () => { const ModelProvider = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -32,7 +33,8 @@ const ModelProvider = () => {
...(feConfigs?.show_aiproxy ...(feConfigs?.show_aiproxy
? [ ? [
{ label: t('account:channel'), value: 'channel' }, { label: t('account:channel'), value: 'channel' },
{ label: t('account_model:log'), value: 'channel_log' } { label: t('account_model:log'), value: 'channel_log' },
{ label: t('account_model:monitoring'), value: 'account_model' }
] ]
: []) : [])
]} ]}
@@ -50,6 +52,7 @@ const ModelProvider = () => {
{tab === 'config' && <ModelConfigTable Tab={Tab} />} {tab === 'config' && <ModelConfigTable Tab={Tab} />}
{tab === 'channel' && <ChannelTable Tab={Tab} />} {tab === 'channel' && <ChannelTable Tab={Tab} />}
{tab === 'channel_log' && <ChannelLog Tab={Tab} />} {tab === 'channel_log' && <ChannelLog Tab={Tab} />}
{tab === 'account_model' && <ModelDashboard Tab={Tab} />}
</Flex> </Flex>
</AccountContainer> </AccountContainer>
); );

View File

@@ -7,6 +7,11 @@ import { authSystemAdmin } from '@fastgpt/service/support/permission/user/auth';
const baseUrl = process.env.AIPROXY_API_ENDPOINT; const baseUrl = process.env.AIPROXY_API_ENDPOINT;
const token = process.env.AIPROXY_API_TOKEN; const token = process.env.AIPROXY_API_TOKEN;
// 特殊路径映射,标记需要在末尾保留斜杠的路径
const endPathMap: Record<string, boolean> = {
'api/dashboardv2': true
};
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try { try {
await authSystemAdmin({ req }); await authSystemAdmin({ req });
@@ -22,9 +27,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
} }
const queryStr = new URLSearchParams(query).toString(); const queryStr = new URLSearchParams(query).toString();
const requestPath = queryStr // Determine whether the base path requires a trailing slash.
? `/${path?.join('/')}?${new URLSearchParams(query).toString()}` const basePath = `/${path?.join('/')}${endPathMap[path?.join('/')] ? '/' : ''}`;
: `/${path?.join('/')}`; const requestPath = queryStr ? `${basePath}?${queryStr}` : basePath;
const parsedUrl = new URL(baseUrl); const parsedUrl = new URL(baseUrl);
delete req.headers?.cookie; delete req.headers?.cookie;

View File

@@ -1,10 +1,11 @@
import axios, { type Method, type AxiosResponse } from 'axios'; import axios, { type Method, type AxiosResponse } from 'axios';
import { getWebReqUrl } from '@fastgpt/web/common/system/utils'; import { getWebReqUrl } from '@fastgpt/web/common/system/utils';
import { import type {
type ChannelInfoType, DashboardDataItemType,
type ChannelListResponseType, ChannelInfoType,
type ChannelLogListItemType, ChannelListResponseType,
type CreateChannelProps ChannelLogListItemType,
CreateChannelProps
} from '@/global/aiproxy/type'; } from '@/global/aiproxy/type';
import type { ChannelStatusEnum } from '@/global/aiproxy/constants'; import type { ChannelStatusEnum } from '@/global/aiproxy/constants';
@@ -187,3 +188,23 @@ export const getLogDetail = (id: number) =>
request_body: string; request_body: string;
response_body: string; response_body: string;
}>(`/logs/detail/${id}`); }>(`/logs/detail/${id}`);
export const getDashboardV2 = (params: {
channel?: number;
start_timestamp?: number;
end_timestamp?: number;
timezone?: string;
timespan?: 'day' | 'hour';
}) =>
GET<
{
timestamp: number;
models: DashboardDataItemType[];
}[]
>('/dashboardv2/', {
channel: params.channel,
start_timestamp: params.start_timestamp,
end_timestamp: params.end_timestamp,
timezone: params.timezone || 'Local',
timespan: params.timespan || 'day'
});