Merge remote-tracking branch 'origin/main' into nightly
# Conflicts: # web/src/helpers/render.jsx # web/src/hooks/usage-logs/useUsageLogsData.jsx # web/src/i18n/locales/en.json
This commit is contained in:
+131
-6
@@ -34,8 +34,14 @@ import {
|
||||
updateChartSpec,
|
||||
updateMapValue,
|
||||
initializeMaps,
|
||||
processUserData,
|
||||
} from '../../helpers/dashboard';
|
||||
|
||||
const USER_COLORS = [
|
||||
'#3b82f6', '#ef4444', '#10b981', '#f59e0b', '#8b5cf6',
|
||||
'#ec4899', '#06b6d4', '#f97316', '#6366f1', '#14b8a6',
|
||||
];
|
||||
|
||||
export const useDashboardCharts = (
|
||||
dataExportDefaultTime,
|
||||
setTrendData,
|
||||
@@ -179,7 +185,6 @@ export const useDashboardCharts = (
|
||||
},
|
||||
});
|
||||
|
||||
// 模型消耗趋势折线图
|
||||
const [spec_model_line, setSpecModelLine] = useState({
|
||||
type: 'line',
|
||||
data: [
|
||||
@@ -197,7 +202,7 @@ export const useDashboardCharts = (
|
||||
},
|
||||
title: {
|
||||
visible: true,
|
||||
text: t('模型消耗趋势'),
|
||||
text: t('调用趋势'),
|
||||
subtext: '',
|
||||
},
|
||||
tooltip: {
|
||||
@@ -215,7 +220,6 @@ export const useDashboardCharts = (
|
||||
},
|
||||
});
|
||||
|
||||
// 模型调用次数排行柱状图
|
||||
const [spec_rank_bar, setSpecRankBar] = useState({
|
||||
type: 'bar',
|
||||
data: [
|
||||
@@ -259,6 +263,82 @@ export const useDashboardCharts = (
|
||||
},
|
||||
});
|
||||
|
||||
// ========== Admin: 用户消耗排行 ==========
|
||||
const [spec_user_rank, setSpecUserRank] = useState({
|
||||
type: 'bar',
|
||||
data: [{ id: 'userRankData', values: [] }],
|
||||
xField: 'rawQuota',
|
||||
yField: 'User',
|
||||
seriesField: 'User',
|
||||
direction: 'horizontal',
|
||||
legends: { visible: false },
|
||||
title: {
|
||||
visible: true,
|
||||
text: t('用户消耗排行'),
|
||||
subtext: '',
|
||||
},
|
||||
bar: {
|
||||
state: { hover: { stroke: '#000', lineWidth: 1 } },
|
||||
},
|
||||
label: {
|
||||
visible: true,
|
||||
position: 'outside',
|
||||
formatMethod: (value, datum) => renderQuota(datum['rawQuota'] || 0, 2),
|
||||
},
|
||||
axes: [{
|
||||
orient: 'left',
|
||||
type: 'band',
|
||||
label: { visible: true },
|
||||
}, {
|
||||
orient: 'bottom',
|
||||
type: 'linear',
|
||||
visible: false,
|
||||
}],
|
||||
tooltip: {
|
||||
mark: {
|
||||
content: [{
|
||||
key: (datum) => datum['User'],
|
||||
value: (datum) => renderQuota(datum['rawQuota'] || 0, 4),
|
||||
}],
|
||||
},
|
||||
},
|
||||
color: { type: 'ordinal', range: USER_COLORS },
|
||||
});
|
||||
|
||||
// ========== Admin: 用户消耗趋势 ==========
|
||||
const [spec_user_trend, setSpecUserTrend] = useState({
|
||||
type: 'area',
|
||||
data: [{ id: 'userTrendData', values: [] }],
|
||||
xField: 'Time',
|
||||
yField: 'rawQuota',
|
||||
seriesField: 'User',
|
||||
stack: false,
|
||||
legends: { visible: true, selectMode: 'single' },
|
||||
title: {
|
||||
visible: true,
|
||||
text: t('用户消耗趋势'),
|
||||
subtext: '',
|
||||
},
|
||||
axes: [{
|
||||
orient: 'left',
|
||||
label: {
|
||||
formatMethod: (value) => renderQuota(value, 2),
|
||||
},
|
||||
}],
|
||||
area: { style: { fillOpacity: 0.15 } },
|
||||
line: { style: { lineWidth: 2 } },
|
||||
point: { visible: false },
|
||||
tooltip: {
|
||||
mark: {
|
||||
content: [{
|
||||
key: (datum) => datum['User'],
|
||||
value: (datum) => renderQuota(datum['rawQuota'] || 0, 4),
|
||||
}],
|
||||
},
|
||||
},
|
||||
color: { type: 'ordinal', range: USER_COLORS },
|
||||
});
|
||||
|
||||
// ========== 数据处理函数 ==========
|
||||
const generateModelColors = useCallback((uniqueModels, modelColors) => {
|
||||
const newModelColors = {};
|
||||
@@ -426,6 +506,51 @@ export const useDashboardCharts = (
|
||||
],
|
||||
);
|
||||
|
||||
// ========== 用户维度图表数据处理 ==========
|
||||
const updateUserChartData = useCallback(
|
||||
(data) => {
|
||||
const { rankingData, trendData: userTrend } = processUserData(
|
||||
data,
|
||||
dataExportDefaultTime,
|
||||
10,
|
||||
);
|
||||
|
||||
const userRankValues = rankingData.map((item) => ({
|
||||
User: item.User,
|
||||
rawQuota: item.Quota,
|
||||
Quota: getQuotaWithUnit(item.Quota, 4),
|
||||
})).sort((a, b) => b.rawQuota - a.rawQuota);
|
||||
|
||||
const totalUserQuota = rankingData.reduce((s, i) => s + i.Quota, 0);
|
||||
|
||||
setSpecUserRank((prev) => ({
|
||||
...prev,
|
||||
data: [{ id: 'userRankData', values: userRankValues }],
|
||||
title: {
|
||||
...prev.title,
|
||||
subtext: `${t('总计')}:${renderQuota(totalUserQuota, 2)}`,
|
||||
},
|
||||
}));
|
||||
|
||||
const userTrendValues = userTrend.map((item) => ({
|
||||
Time: item.Time,
|
||||
User: item.User,
|
||||
rawQuota: item.Quota,
|
||||
Usage: item.Quota ? getQuotaWithUnit(item.Quota, 4) : 0,
|
||||
}));
|
||||
|
||||
setSpecUserTrend((prev) => ({
|
||||
...prev,
|
||||
data: [{ id: 'userTrendData', values: userTrendValues }],
|
||||
title: {
|
||||
...prev.title,
|
||||
subtext: `${t('总计')}:${renderQuota(totalUserQuota, 2)}`,
|
||||
},
|
||||
}));
|
||||
},
|
||||
[dataExportDefaultTime, t],
|
||||
);
|
||||
|
||||
// ========== 初始化图表主题 ==========
|
||||
useEffect(() => {
|
||||
initVChartSemiTheme({
|
||||
@@ -434,14 +559,14 @@ export const useDashboardCharts = (
|
||||
}, []);
|
||||
|
||||
return {
|
||||
// 图表规格
|
||||
spec_pie,
|
||||
spec_line,
|
||||
spec_model_line,
|
||||
spec_rank_bar,
|
||||
|
||||
// 函数
|
||||
spec_user_rank,
|
||||
spec_user_trend,
|
||||
updateChartData,
|
||||
updateUserChartData,
|
||||
generateModelColors,
|
||||
};
|
||||
};
|
||||
|
||||
+22
@@ -213,6 +213,27 @@ export const useDashboardData = (userState, userDispatch, statusState) => {
|
||||
}
|
||||
}, [activeUptimeTab]);
|
||||
|
||||
const loadUserQuotaData = useCallback(async () => {
|
||||
if (!isAdminUser) return [];
|
||||
try {
|
||||
const { start_timestamp, end_timestamp } = inputs;
|
||||
const localStartTimestamp = Date.parse(start_timestamp) / 1000;
|
||||
const localEndTimestamp = Date.parse(end_timestamp) / 1000;
|
||||
const url = `/api/data/users?start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`;
|
||||
const res = await API.get(url);
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
return data || [];
|
||||
} else {
|
||||
showError(message);
|
||||
return [];
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return [];
|
||||
}
|
||||
}, [inputs, isAdminUser]);
|
||||
|
||||
const getUserData = useCallback(async () => {
|
||||
let res = await API.get(`/api/user/self`);
|
||||
const { success, message, data } = res.data;
|
||||
@@ -311,6 +332,7 @@ export const useDashboardData = (userState, userDispatch, statusState) => {
|
||||
showSearchModal,
|
||||
handleCloseModal,
|
||||
loadQuotaData,
|
||||
loadUserQuotaData,
|
||||
loadUptimeData,
|
||||
getUserData,
|
||||
refresh,
|
||||
|
||||
+8
-1
@@ -167,7 +167,14 @@ export const usePlaygroundState = () => {
|
||||
// 配置导入/重置
|
||||
const handleConfigImport = useCallback((importedConfig) => {
|
||||
if (importedConfig.inputs) {
|
||||
setInputs((prev) => ({ ...prev, ...importedConfig.inputs }));
|
||||
const parsedMaxTokens = parseInt(importedConfig.inputs.max_tokens, 10);
|
||||
setInputs((prev) => ({
|
||||
...prev,
|
||||
...importedConfig.inputs,
|
||||
max_tokens: Number.isNaN(parsedMaxTokens)
|
||||
? importedConfig.inputs.max_tokens
|
||||
: parsedMaxTokens,
|
||||
}));
|
||||
}
|
||||
if (importedConfig.parameterEnabled) {
|
||||
setParameterEnabled((prev) => ({
|
||||
|
||||
+23
-6
@@ -31,6 +31,7 @@ import { ITEMS_PER_PAGE } from '../../constants';
|
||||
import { useTableCompactMode } from '../common/useTableCompactMode';
|
||||
import {
|
||||
fetchTokenKey as fetchTokenKeyById,
|
||||
fetchTokenKeysBatch,
|
||||
getServerAddress,
|
||||
encodeChannelConnectionString,
|
||||
} from '../../helpers/token';
|
||||
@@ -41,6 +42,7 @@ export const useTokensData = (openFluentNotification, openCCSwitchModal) => {
|
||||
// Basic state
|
||||
const [tokens, setTokens] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [groupRatios, setGroupRatios] = useState({});
|
||||
const [activePage, setActivePage] = useState(1);
|
||||
const [tokenCount, setTokenCount] = useState(0);
|
||||
const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
|
||||
@@ -408,14 +410,17 @@ export const useTokensData = (openFluentNotification, openCCSwitchModal) => {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const keys = await Promise.all(
|
||||
selectedKeys.map((token) => fetchTokenKey(token, { suppressError: true })),
|
||||
);
|
||||
const ids = selectedKeys.map((token) => token.id);
|
||||
const keysMap = await fetchTokenKeysBatch(ids);
|
||||
|
||||
setResolvedTokenKeys((prev) => ({ ...prev, ...keysMap }));
|
||||
|
||||
let content = '';
|
||||
for (let i = 0; i < selectedKeys.length; i++) {
|
||||
const fullKey = keys[i];
|
||||
for (const token of selectedKeys) {
|
||||
const fullKey = keysMap[token.id];
|
||||
if (!fullKey) continue;
|
||||
if (copyType === 'name+key') {
|
||||
content += `${selectedKeys[i].name} sk-${fullKey}\n`;
|
||||
content += `${token.name} sk-${fullKey}\n`;
|
||||
} else {
|
||||
content += `sk-${fullKey}\n`;
|
||||
}
|
||||
@@ -433,6 +438,17 @@ export const useTokensData = (openFluentNotification, openCCSwitchModal) => {
|
||||
.catch((reason) => {
|
||||
showError(reason);
|
||||
});
|
||||
API.get('/api/user/self/groups')
|
||||
.then((res) => {
|
||||
if (res.data.success && res.data.data) {
|
||||
const ratios = {};
|
||||
for (const [name, info] of Object.entries(res.data.data)) {
|
||||
ratios[name] = info.ratio;
|
||||
}
|
||||
setGroupRatios(ratios);
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
}, [pageSize]);
|
||||
|
||||
return {
|
||||
@@ -443,6 +459,7 @@ export const useTokensData = (openFluentNotification, openCCSwitchModal) => {
|
||||
tokenCount,
|
||||
pageSize,
|
||||
searching,
|
||||
groupRatios,
|
||||
|
||||
// Selection state
|
||||
selectedKeys,
|
||||
|
||||
+5
-1
@@ -37,6 +37,7 @@ import {
|
||||
renderClaudeModelPrice,
|
||||
renderModelPrice,
|
||||
renderTieredModelPrice,
|
||||
renderTaskBillingProcess,
|
||||
} from '../../helpers';
|
||||
import { ITEMS_PER_PAGE } from '../../constants';
|
||||
import { useTableCompactMode } from '../common/useTableCompactMode';
|
||||
@@ -475,7 +476,10 @@ export const useLogsData = () => {
|
||||
completion_tokens: logs[i].completion_tokens,
|
||||
displayMode: billingDisplayMode,
|
||||
};
|
||||
if (other?.ws || other?.audio) {
|
||||
const isTaskLog = other?.is_task === true || other?.task_id != null;
|
||||
if (isTaskLog && other?.model_price === -1) {
|
||||
content = renderTaskBillingProcess(other, logs[i].content);
|
||||
} else if (other?.ws || other?.audio) {
|
||||
content = renderAudioModelPrice(logOpts);
|
||||
} else if (other?.claude) {
|
||||
content = renderClaudeModelPrice(logOpts);
|
||||
|
||||
Reference in New Issue
Block a user