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:
CaIon
2026-04-09 17:12:21 +08:00
100 changed files with 6875 additions and 2699 deletions

View File

@@ -150,7 +150,18 @@ export const buildApiPayload = (
const value = inputs[param];
const hasValue = value !== undefined && value !== null;
if (enabled && hasValue) {
if (!enabled) {
return;
}
if (param === 'max_tokens') {
if (typeof value === 'number') {
payload[param] = value;
}
return;
}
if (hasValue) {
payload[param] = value;
}
});

View File

@@ -387,3 +387,58 @@ export const generateChartTimePoints = (
return chartTimePoints;
};
// ========== 用户维度数据处理 ==========
export const processUserData = (data, dataExportDefaultTime, limit = 10) => {
const userQuotaTotal = new Map();
data.forEach((item) => {
const prev = userQuotaTotal.get(item.username) || 0;
userQuotaTotal.set(item.username, prev + item.quota);
});
const sorted = Array.from(userQuotaTotal.entries()).sort(
(a, b) => b[1] - a[1],
);
const topUsers = sorted.slice(0, limit).map(([u]) => u);
const topUserSet = new Set(topUsers);
const rankingData = sorted.slice(0, limit).map(([username, quota]) => ({
User: username,
Quota: quota,
}));
const showYear = isDataCrossYear(data.map((item) => item.created_at));
const timeUserMap = new Map();
const allTimePoints = new Set();
data.forEach((item) => {
const timeKey = timestamp2string1(
item.created_at,
dataExportDefaultTime,
showYear,
);
allTimePoints.add(timeKey);
const user = topUserSet.has(item.username) ? item.username : null;
if (!user) return;
const key = `${timeKey}-${user}`;
const prev = timeUserMap.get(key) || { quota: 0 };
timeUserMap.set(key, { quota: prev.quota + item.quota });
});
const sortedTimePoints = Array.from(allTimePoints).sort();
const trendData = [];
sortedTimePoints.forEach((time) => {
topUsers.forEach((user) => {
const key = `${time}-${user}`;
const val = timeUserMap.get(key);
trendData.push({
Time: time,
User: user,
Quota: val?.quota || 0,
});
});
});
return { rankingData, trendData, topUsers };
};

View File

@@ -1625,6 +1625,18 @@ function renderPriceSimpleCore({
return result;
}
export function renderTaskBillingProcess(other, content) {
if (other?.task_id != null) {
return renderBillingArticle(
[content].filter(Boolean),
{ showReferenceNote: false },
);
}
return renderBillingArticle([
buildBillingText('任务预扣费将在任务完成后按实际token重算'),
]);
}
export function renderModelPrice(opts) {
const {
prompt_tokens: inputTokens = 0,

View File

@@ -33,6 +33,20 @@ export async function fetchTokenKey(tokenId) {
return data.key;
}
/**
* 批量获取多个令牌的真实 key
* @param {number[]} tokenIds
* @returns {Promise<Record<number, string>>} 返回 {id: key} mapkey 不带 sk- 前缀
*/
export async function fetchTokenKeysBatch(tokenIds) {
const response = await API.post('/api/token/batch/keys', { ids: tokenIds });
const { success, data, message } = response.data || {};
if (!success || !data?.keys) {
throw new Error(message || 'Failed to fetch token keys');
}
return data.keys;
}
/**
* 获取可用的 token keys
* @returns {Promise<string[]>} 返回 active 状态的不带 sk- 前缀的真实 token key 数组