/*
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see .
For commercial licensing, please contact support@quantumnous.com
*/
import { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { Modal, Tag } from '@douyinfe/semi-ui';
import {
API,
getTodayStartTimestamp,
isAdmin,
showError,
showSuccess,
timestamp2string,
renderQuota,
renderNumber,
getQuotaPerUnit,
getLogOther,
copy,
renderClaudeLogContent,
renderLogContent,
renderConsumeBillingProcess,
} from '../../helpers';
import { ITEMS_PER_PAGE } from '../../constants';
import { useTableCompactMode } from '../common/useTableCompactMode';
import ParamOverrideEntry from '../../components/table/usage-logs/components/ParamOverrideEntry';
export const useLogsData = () => {
const { t } = useTranslation();
// Define column keys for selection
const COLUMN_KEYS = {
TIME: 'time',
CHANNEL: 'channel',
USERNAME: 'username',
TOKEN: 'token',
GROUP: 'group',
TYPE: 'type',
MODEL: 'model',
USE_TIME: 'use_time',
PROMPT: 'prompt',
COMPLETION: 'completion',
COST: 'cost',
RETRY: 'retry',
IP: 'ip',
DETAILS: 'details',
};
// Basic state
const [logs, setLogs] = useState([]);
const [expandData, setExpandData] = useState({});
const [showStat, setShowStat] = useState(false);
const [loading, setLoading] = useState(false);
const [loadingStat, setLoadingStat] = useState(false);
const [activePage, setActivePage] = useState(1);
const [logCount, setLogCount] = useState(0);
const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
const [logType, setLogType] = useState(0);
// User and admin
const isAdminUser = isAdmin();
// Role-specific storage key to prevent different roles from overwriting each other
const STORAGE_KEY = isAdminUser
? 'logs-table-columns-admin'
: 'logs-table-columns-user';
const BILLING_DISPLAY_MODE_STORAGE_KEY = isAdminUser
? 'logs-billing-display-mode-admin'
: 'logs-billing-display-mode-user';
const hasVideoPerSecondDetail = (other) =>
other?.billing_mode === 'video_per_second' &&
Number(other?.video_seconds || 0) > 0 &&
Number(other?.video_price_per_second || 0) > 0 &&
Number(other?.video_quota_per_unit || 0) > 0;
const hasVideoPerVideoDetail = (other) =>
other?.billing_mode === 'video_per_video';
/**
* 异步视频任务当前所处扣费阶段(同一记录只展示其一):
* - 已结算:仅「实际扣费」
* - 仅预扣:仅「预扣费」
*/
const getVideoBillingPhase = (other, quota = 0) => {
const finalRaw = other?.video_final_quota ?? other?.actual_quota;
if (finalRaw !== undefined && finalRaw !== null) {
const final = Number(finalRaw);
if (Number.isFinite(final) && final > 0) {
return { phase: 'actual', label: t('实际扣费'), amount: final };
}
}
const isVideoTask =
other?.billing_mode === 'video_per_second' ||
other?.billing_mode === 'video_per_video' ||
Boolean(other?.task_id) ||
String(other?.request_path || '').includes('/videos');
if (!isVideoTask) {
return null;
}
const pre = Number(
other?.video_billed_quota ?? other?.video_pre_consumed_quota ?? quota ?? 0,
);
if (Number.isFinite(pre) && pre > 0) {
return { phase: 'pre', label: t('预扣费'), amount: pre };
}
return null;
};
const buildVideoCostDisplayItems = (other, billedQuota, tagValue, inlineTags) => {
const phase = getVideoBillingPhase(other, billedQuota);
if (!phase) {
return [
[
t('花费'),
inlineTags(tagValue(renderQuota(billedQuota), 'red', 'cost')),
],
];
}
return [
[
phase.label,
inlineTags(
tagValue(
renderQuota(phase.amount),
phase.phase === 'actual' ? 'red' : 'grey',
'cost',
),
),
],
];
};
const renderVideoPerSecondBillingDetail = (log, other, quota) => {
const seconds = Number(other?.video_seconds || 0);
const pricePerSecond = Number(other?.video_price_per_second || 0);
const groupRatio = Number(other?.group_ratio || 1);
const channelDiscount = Number(other?.channel_price_discount || 100);
const billedQuota = Number(other?.video_billed_quota || quota || 0);
const width = Number(other?.video_width || 0);
const height = Number(other?.video_height || 0);
const ruleWidth = Number(other?.video_rule_width || 0);
const ruleHeight = Number(other?.video_rule_height || 0);
const hasAudio = other?.video_has_audio === true;
const unifiedAudio = other?.video_unified_audio_price === true;
const audioText = hasAudio ? t('有音频') : t('无音频');
const priceLabel = unifiedAudio
? t('每秒价')
: hasAudio
? t('有音轨价')
: t('无音轨价');
const modelName = log?.model_name || '-';
const upstreamModelName = other?.upstream_model_name || '';
const specWidth = ruleWidth || width;
const specHeight = ruleHeight || height;
const calculatedPricePerSecond =
pricePerSecond * groupRatio * (channelDiscount / 100);
const calculatedTotalPrice = seconds * calculatedPricePerSecond;
const formatMoney = (value) => {
const numberValue = Number(value || 0);
if (!Number.isFinite(numberValue)) {
return '$0';
}
return `$${numberValue.toFixed(6).replace(/\.?0+$/, '')}`;
};
const tagValue = (value, color = 'blue', key = String(value)) => (
{value}
);
const inlineTags = (...nodes) => (
{nodes}
);
const modelValue = inlineTags(
tagValue(modelName, 'blue', 'model'),
isAdminUser && upstreamModelName
? tagValue(
t('实际运行:{{upstreamModel}}', {
upstreamModel: upstreamModelName,
}),
'purple',
'upstream-model',
)
: null,
);
const specValue = inlineTags(
tagValue(`${specWidth}×${specHeight}`, 'cyan', 'spec-resolution'),
tagValue(audioText, hasAudio ? 'green' : 'grey', 'spec-audio'),
tagValue(t('{{seconds}} 秒', { seconds }), 'orange', 'spec-seconds'),
);
const calculatedPriceValue = inlineTags(
tagValue(
`${formatMoney(calculatedPricePerSecond)} / ${t('秒')}`,
'green',
'calculated-price',
),
tagValue(priceLabel, 'grey', 'price-label'),
);
const calculationValue = inlineTags(
tagValue(t('{{seconds}} 秒', { seconds }), 'orange', 'calc-seconds'),
×
,
tagValue(
`${formatMoney(calculatedPricePerSecond)} / ${t('秒')}`,
'green',
'calc-price',
),
=
,
tagValue(formatMoney(calculatedTotalPrice), 'red', 'calc-total'),
);
const modeValue = inlineTags(
tagValue(t('分辨率阶梯计费'), 'blue', 'billing-mode'),
);
const actualVideoValue = inlineTags(
tagValue(`${width}×${height}`, 'cyan', 'actual-resolution'),
tagValue(audioText, hasAudio ? 'green' : 'grey', 'actual-audio'),
);
const costItems = buildVideoCostDisplayItems(
other,
billedQuota,
tagValue,
inlineTags,
);
const items = [
[t('模型'), modelValue],
[t('规格'), specValue],
[t('计费单价'), calculatedPriceValue],
[t('结算计算'), calculationValue],
...costItems,
[t('计费模式'), modeValue],
];
if (
width > 0 &&
height > 0 &&
(width !== specWidth || height !== specHeight)
) {
items.splice(2, 0, [t('实际视频'), actualVideoValue]);
}
return (
{items.map(([label, value]) => (
{label}
{value}
))}
);
};
const renderVideoPerSecondBillingBrief = (other, quota) => {
const billedQuota = Number(other?.video_billed_quota || quota || 0);
const seconds = Number(other?.video_seconds || 0);
const resolution = other?.video_resolution || '-';
const phase = getVideoBillingPhase(other, quota);
return (
{t('分辨率阶梯计费')}
{phase
? t('{{seconds}}秒 · {{resolution}} · {{label}} {{cost}}', {
seconds,
resolution,
label: phase.label,
cost: renderQuota(phase.amount),
})
: t('{{seconds}}秒 · {{resolution}} · {{cost}}', {
seconds,
resolution,
cost: renderQuota(billedQuota),
})}
);
};
const renderVideoPerVideoBillingDetail = (log, other, quota) => {
const count = Number(other?.video_count || 1);
const billedQuota = Number(other?.video_billed_quota || quota || 0);
const quotaPerUnit = Number(
other?.video_quota_per_unit || getQuotaPerUnit() || 500000,
);
const pricePerVideo =
Number(other?.video_price_per_video || 0) ||
(quotaPerUnit > 0 && count > 0 ? billedQuota / quotaPerUnit / count : 0);
const width = Number(other?.video_width || 0);
const height = Number(other?.video_height || 0);
const ruleWidth = Number(other?.video_rule_width || 0);
const ruleHeight = Number(other?.video_rule_height || 0);
const resolution = other?.video_resolution || '';
const hasAudio = other?.video_has_audio === true;
const seconds = Number(other?.video_seconds || 0);
const modelName = log?.model_name || '-';
const upstreamModelName = other?.upstream_model_name || '';
const audioText = hasAudio ? t('有音频') : t('无音频');
const priceLabel = hasAudio ? t('有音轨价') : t('无音轨价');
const totalPrice = count * pricePerVideo;
const formatMoney = (value) => {
const numberValue = Number(value || 0);
if (!Number.isFinite(numberValue)) {
return '$0';
}
return `$${numberValue.toFixed(6).replace(/\.?0+$/, '')}`;
};
const tagValue = (value, color = 'blue', key = String(value)) => (
{value}
);
const inlineTags = (...nodes) => (
{nodes}
);
const modelValue = inlineTags(
tagValue(modelName, 'blue', 'model'),
isAdminUser && upstreamModelName
? tagValue(
t('实际运行:{{upstreamModel}}', {
upstreamModel: upstreamModelName,
}),
'purple',
'upstream-model',
)
: null,
);
const specTags = [];
if (resolution || (ruleWidth > 0 && ruleHeight > 0)) {
specTags.push(
tagValue(
ruleWidth > 0 && ruleHeight > 0
? `${ruleWidth}×${ruleHeight}`
: resolution,
'cyan',
'matched-resolution',
),
);
}
if (seconds > 0) {
specTags.push(
tagValue(t('{{seconds}} 秒', { seconds }), 'orange', 'seconds'),
);
}
specTags.push(tagValue(audioText, hasAudio ? 'green' : 'grey', 'audio'));
const specificationValue = inlineTags(...specTags);
const countValue = inlineTags(
tagValue(t('{{count}} 条', { count }), 'orange', 'count'),
);
const priceValue = inlineTags(
tagValue(`${formatMoney(pricePerVideo)} / ${t('条')}`, 'green', 'price'),
tagValue(priceLabel, 'grey', 'price-label'),
);
const calculationValue = inlineTags(
tagValue(t('{{count}} 条', { count }), 'orange', 'calc-count'),
×
,
tagValue(
`${formatMoney(pricePerVideo)} / ${t('条')}`,
'green',
'calc-price',
),
=
,
tagValue(formatMoney(totalPrice), 'red', 'calc-total'),
);
const costItems = buildVideoCostDisplayItems(
other,
billedQuota,
tagValue,
inlineTags,
);
const items = [
[t('模型'), modelValue],
[t('规格'), specificationValue],
[t('视频数量'), countValue],
[t('计费单价'), priceValue],
[t('结算计算'), calculationValue],
...costItems,
[
t('计费模式'),
inlineTags(tagValue(t('按视频数量计费'), 'blue', 'billing-mode')),
],
];
return (
{items.map(([label, value]) => (
{label}
{value}
))}
);
};
const renderVideoPerVideoBillingBrief = (other, quota) => {
const billedQuota = Number(other?.video_billed_quota || quota || 0);
const count = Number(other?.video_count || 1);
const phase = getVideoBillingPhase(other, quota);
return (
{t('按视频数量计费')}
{phase
? t('{{count}}条 · {{label}} {{cost}}', {
count,
label: phase.label,
cost: renderQuota(phase.amount),
})
: t('{{count}}条 · {{cost}}', {
count,
cost: renderQuota(billedQuota),
})}
);
};
// Statistics state
const [stat, setStat] = useState({
quota: 0,
token: 0,
});
// Form state
const [formApi, setFormApi] = useState(null);
let now = new Date();
const formInitValues = {
username: '',
token_name: '',
model_name: '',
channel: '',
group: '',
request_id: '',
dateRange: [
timestamp2string(getTodayStartTimestamp()),
timestamp2string(now.getTime() / 1000 + 3600),
],
logType: '0',
};
// Get default column visibility based on user role
const getDefaultColumnVisibility = () => {
return {
[COLUMN_KEYS.TIME]: true,
[COLUMN_KEYS.CHANNEL]: isAdminUser,
[COLUMN_KEYS.USERNAME]: isAdminUser,
[COLUMN_KEYS.TOKEN]: true,
[COLUMN_KEYS.GROUP]: true,
[COLUMN_KEYS.TYPE]: true,
[COLUMN_KEYS.MODEL]: true,
[COLUMN_KEYS.USE_TIME]: true,
[COLUMN_KEYS.PROMPT]: true,
[COLUMN_KEYS.COMPLETION]: true,
[COLUMN_KEYS.COST]: true,
[COLUMN_KEYS.RETRY]: isAdminUser,
[COLUMN_KEYS.IP]: true,
[COLUMN_KEYS.DETAILS]: true,
};
};
const getInitialVisibleColumns = () => {
const defaults = getDefaultColumnVisibility();
const savedColumns = localStorage.getItem(STORAGE_KEY);
if (!savedColumns) {
return defaults;
}
try {
const parsed = JSON.parse(savedColumns);
const merged = { ...defaults, ...parsed };
if (!isAdminUser) {
merged[COLUMN_KEYS.CHANNEL] = false;
merged[COLUMN_KEYS.USERNAME] = false;
merged[COLUMN_KEYS.RETRY] = false;
}
return merged;
} catch (e) {
console.error('Failed to parse saved column preferences', e);
return defaults;
}
};
const getInitialBillingDisplayMode = () => {
const savedMode = localStorage.getItem(BILLING_DISPLAY_MODE_STORAGE_KEY);
if (savedMode === 'price' || savedMode === 'ratio') {
return savedMode;
}
return localStorage.getItem('quota_display_type') === 'TOKENS'
? 'ratio'
: 'price';
};
// Column visibility state
const [visibleColumns, setVisibleColumns] = useState(
getInitialVisibleColumns,
);
const [showColumnSelector, setShowColumnSelector] = useState(false);
const [billingDisplayMode, setBillingDisplayMode] = useState(
getInitialBillingDisplayMode,
);
// Compact mode
const [compactMode, setCompactMode] = useTableCompactMode('logs');
// User info modal state
const [showUserInfo, setShowUserInfoModal] = useState(false);
const [userInfoData, setUserInfoData] = useState(null);
// Channel affinity usage cache stats modal state (admin only)
const [
showChannelAffinityUsageCacheModal,
setShowChannelAffinityUsageCacheModal,
] = useState(false);
const [channelAffinityUsageCacheTarget, setChannelAffinityUsageCacheTarget] =
useState(null);
const [showParamOverrideModal, setShowParamOverrideModal] = useState(false);
const [paramOverrideTarget, setParamOverrideTarget] = useState(null);
/** 使用日志(错误类型)行内「错误详情」弹窗 */
const [errorLogDetail, setErrorLogDetail] = useState({
visible: false,
record: null,
});
// Initialize default column visibility
const initDefaultColumns = () => {
const defaults = getDefaultColumnVisibility();
setVisibleColumns(defaults);
localStorage.setItem(STORAGE_KEY, JSON.stringify(defaults));
};
// Handle column visibility change
const handleColumnVisibilityChange = (columnKey, checked) => {
const updatedColumns = { ...visibleColumns, [columnKey]: checked };
setVisibleColumns(updatedColumns);
};
// Handle "Select All" checkbox
const handleSelectAll = (checked) => {
const allKeys = Object.keys(COLUMN_KEYS).map((key) => COLUMN_KEYS[key]);
const updatedColumns = {};
allKeys.forEach((key) => {
if (
(key === COLUMN_KEYS.CHANNEL ||
key === COLUMN_KEYS.USERNAME ||
key === COLUMN_KEYS.RETRY) &&
!isAdminUser
) {
updatedColumns[key] = false;
} else {
updatedColumns[key] = checked;
}
});
setVisibleColumns(updatedColumns);
};
// Persist column settings to the role-specific STORAGE_KEY
useEffect(() => {
if (Object.keys(visibleColumns).length > 0) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(visibleColumns));
}
}, [visibleColumns]);
useEffect(() => {
localStorage.setItem(BILLING_DISPLAY_MODE_STORAGE_KEY, billingDisplayMode);
}, [BILLING_DISPLAY_MODE_STORAGE_KEY, billingDisplayMode]);
// 获取表单值的辅助函数,确保所有值都是字符串
const getFormValues = () => {
const formValues = formApi ? formApi.getValues() : {};
let start_timestamp = timestamp2string(getTodayStartTimestamp());
let end_timestamp = timestamp2string(now.getTime() / 1000 + 3600);
if (
formValues.dateRange &&
Array.isArray(formValues.dateRange) &&
formValues.dateRange.length === 2
) {
start_timestamp = formValues.dateRange[0];
end_timestamp = formValues.dateRange[1];
}
return {
username: formValues.username || '',
token_name: formValues.token_name || '',
model_name: formValues.model_name || '',
start_timestamp,
end_timestamp,
channel: formValues.channel || '',
group: formValues.group || '',
request_id: formValues.request_id || '',
logType: formValues.logType ? parseInt(formValues.logType) : 0,
};
};
// Statistics functions
const getLogSelfStat = async () => {
const {
token_name,
model_name,
start_timestamp,
end_timestamp,
group,
logType: formLogType,
} = getFormValues();
const currentLogType = formLogType !== undefined ? formLogType : logType;
let localStartTimestamp = Date.parse(start_timestamp) / 1000;
let localEndTimestamp = Date.parse(end_timestamp) / 1000;
let url = `/api/log/self/stat?type=${currentLogType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&group=${group}`;
url = encodeURI(url);
let res = await API.get(url);
const { success, message, data } = res.data;
if (success) {
setStat(data);
} else {
showError(message);
}
};
const getLogStat = async () => {
const {
username,
token_name,
model_name,
start_timestamp,
end_timestamp,
channel,
group,
logType: formLogType,
} = getFormValues();
const currentLogType = formLogType !== undefined ? formLogType : logType;
let localStartTimestamp = Date.parse(start_timestamp) / 1000;
let localEndTimestamp = Date.parse(end_timestamp) / 1000;
let url = `/api/log/stat?type=${currentLogType}&username=${username}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&channel=${channel}&group=${group}`;
url = encodeURI(url);
let res = await API.get(url);
const { success, message, data } = res.data;
if (success) {
setStat(data);
} else {
showError(message);
}
};
const handleEyeClick = async () => {
if (loadingStat) {
return;
}
setLoadingStat(true);
if (isAdminUser) {
await getLogStat();
} else {
await getLogSelfStat();
}
setShowStat(true);
setLoadingStat(false);
};
// User info function
const showUserInfoFunc = async (userId) => {
if (!isAdminUser) {
return;
}
const res = await API.get(`/api/user/${userId}`);
const { success, message, data } = res.data;
if (success) {
setUserInfoData(data);
setShowUserInfoModal(true);
} else {
showError(message);
}
};
const openChannelAffinityUsageCacheModal = (affinity) => {
const a = affinity || {};
setChannelAffinityUsageCacheTarget({
rule_name: a.rule_name || a.reason || '',
using_group: a.using_group || '',
key_hint: a.key_hint || '',
key_fp: a.key_fp || '',
});
setShowChannelAffinityUsageCacheModal(true);
};
const openParamOverrideModal = (log, other) => {
const lines = Array.isArray(other?.po) ? other.po.filter(Boolean) : [];
if (lines.length === 0) {
return;
}
setParamOverrideTarget({
lines,
modelName: log?.model_name || '',
requestId: log?.request_id || '',
requestPath: other?.request_path || '',
});
setShowParamOverrideModal(true);
};
/**
* 打开「错误类型」日志的完整错误内容弹窗。
* @param {object} record 表格行数据
*/
const openErrorLogDetail = (record) => {
if (!record) {
return;
}
setErrorLogDetail({ visible: true, record });
};
/**
* 关闭错误详情弹窗。
*/
const closeErrorLogDetail = () => {
setErrorLogDetail({ visible: false, record: null });
};
/**
* 同一异步视频任务的预扣日志与差额结算日志合并为一条展示。
* 预扣在提交时写入;结算在成片完成后写入(other 含 actual_quota / pre_consumed_quota)。
*/
const mergeVideoTaskBillingLogs = (rawLogs) => {
const byTaskId = new Map();
for (const log of rawLogs) {
const other = getLogOther(log.other);
const taskId = other?.task_id;
if (!taskId) {
continue;
}
const actualQuota = Number(other?.actual_quota);
const isSettlement =
Number.isFinite(actualQuota) &&
actualQuota > 0 &&
other?.pre_consumed_quota !== undefined &&
other?.pre_consumed_quota !== null;
const isPreCharge =
!isSettlement &&
(other?.billing_mode === 'video_per_second' ||
other?.billing_mode === 'video_per_video' ||
String(other?.request_path || '').includes('/videos'));
if (!isSettlement && !isPreCharge) {
continue;
}
if (!byTaskId.has(taskId)) {
byTaskId.set(taskId, { pre: null, settle: null });
}
const entry = byTaskId.get(taskId);
if (isSettlement) {
entry.settle = log;
} else if (isPreCharge) {
entry.pre = log;
}
}
const hideIds = new Set();
const patches = new Map();
for (const { pre, settle } of byTaskId.values()) {
if (!pre || !settle) {
continue;
}
hideIds.add(pre.id);
const preOther = getLogOther(pre.other);
const settleOther = getLogOther(settle.other);
const actualQuota = Number(settleOther.actual_quota);
const preConsumed = Number(
settleOther.pre_consumed_quota ?? pre.quota ?? 0,
);
const mergedOther = {
...settleOther,
request_path: settleOther.request_path || preOther.request_path,
video_billed_quota: actualQuota,
video_pre_consumed_quota: preConsumed,
video_final_quota: actualQuota,
};
patches.set(settle.id, {
quota: actualQuota,
request_id: settle.request_id || pre.request_id,
other:
typeof settle.other === 'string'
? JSON.stringify(mergedOther)
: mergedOther,
});
}
return rawLogs
.filter((log) => !hideIds.has(log.id))
.map((log) => {
const patch = patches.get(log.id);
return patch ? { ...log, ...patch } : log;
});
};
// Format logs data
const setLogsFormat = (rawLogs) => {
const logs = mergeVideoTaskBillingLogs(rawLogs);
const requestConversionDisplayValue = (conversionChain) => {
const chain = Array.isArray(conversionChain)
? conversionChain.filter(Boolean)
: [];
if (chain.length <= 1) {
return t('原生格式');
}
return `${chain.join(' -> ')}`;
};
const taskFinalQuotaMap = {};
for (let i = 0; i < logs.length; i++) {
const other = getLogOther(logs[i].other);
const taskId = other?.task_id;
const actualQuota = Number(other?.actual_quota);
if (
taskId &&
Number.isFinite(actualQuota) &&
actualQuota > 0 &&
(!taskFinalQuotaMap[taskId] || actualQuota > taskFinalQuotaMap[taskId])
) {
taskFinalQuotaMap[taskId] = actualQuota;
}
}
let expandDatesLocal = {};
for (let i = 0; i < logs.length; i++) {
logs[i].timestamp2string = timestamp2string(logs[i].created_at);
logs[i].key = logs[i].id;
let other = getLogOther(logs[i].other);
const billingPhase = getVideoBillingPhase(other, logs[i]?.quota || 0);
const actualQuota = Number(other?.actual_quota);
const displayQuota = billingPhase
? billingPhase.amount
: Number.isFinite(actualQuota) && actualQuota > 0
? actualQuota
: Number(other?.video_billed_quota) > 0
? Number(other.video_billed_quota)
: other?.task_id && taskFinalQuotaMap[other.task_id]
? taskFinalQuotaMap[other.task_id]
: logs[i]?.quota || 0;
logs[i].quota = displayQuota;
if (billingPhase) {
logs[i].video_billing_phase_label = billingPhase.label;
}
const aggregatedQuota = displayQuota;
const channelPriceDiscountLogPct =
other?.channel_price_discount_percent ?? 100;
const videoPerSecondBillingDetail = hasVideoPerSecondDetail(other)
? renderVideoPerSecondBillingDetail(logs[i], other, aggregatedQuota)
: null;
const videoPerVideoBillingDetail = hasVideoPerVideoDetail(other)
? renderVideoPerVideoBillingDetail(logs[i], other, aggregatedQuota)
: null;
const videoBillingDetail =
videoPerSecondBillingDetail || videoPerVideoBillingDetail;
let expandDataLocal = [];
if (
isAdminUser &&
(logs[i].type === 0 || logs[i].type === 2 || logs[i].type === 6)
) {
expandDataLocal.push({
key: t('渠道信息'),
value: logs[i].channel_display || String(logs[i].channel ?? ''),
});
}
if (logs[i].request_id) {
expandDataLocal.push({
key: t('Request ID'),
value: logs[i].request_id,
});
}
if (logs[i].type === 5) {
const errContent = logs[i].content;
if (errContent != null && String(errContent).trim() !== '') {
expandDataLocal.push({
key: t('错误详情'),
value: (
{String(errContent)}
),
});
}
}
if (other?.ws || other?.audio) {
expandDataLocal.push({
key: t('语音输入'),
value: other.audio_input,
});
expandDataLocal.push({
key: t('语音输出'),
value: other.audio_output,
});
expandDataLocal.push({
key: t('文字输入'),
value: other.text_input,
});
expandDataLocal.push({
key: t('文字输出'),
value: other.text_output,
});
}
if (other?.cache_tokens > 0) {
expandDataLocal.push({
key: t('缓存 Tokens'),
value: other.cache_tokens,
});
}
if (other?.cache_creation_tokens > 0) {
expandDataLocal.push({
key: t('缓存创建 Tokens'),
value: other.cache_creation_tokens,
});
}
if (logs[i].type === 2) {
expandDataLocal.push({
key: t('日志详情'),
value: videoPerSecondBillingDetail
? renderVideoPerSecondBillingBrief(other, aggregatedQuota)
: videoPerVideoBillingDetail
? renderVideoPerVideoBillingBrief(other, aggregatedQuota)
: other?.claude
? renderClaudeLogContent(
other?.model_ratio,
other?.completion_ratio,
other?.model_price,
other?.group_ratio,
other?.user_group_ratio,
other?.cache_ratio || 1.0,
other?.cache_creation_ratio || 1.0,
other?.cache_creation_tokens_5m || 0,
other?.cache_creation_ratio_5m ||
other?.cache_creation_ratio ||
1.0,
other?.cache_creation_tokens_1h || 0,
other?.cache_creation_ratio_1h ||
other?.cache_creation_ratio ||
1.0,
billingDisplayMode,
true,
channelPriceDiscountLogPct,
other,
)
: renderLogContent(
other?.model_ratio,
other?.completion_ratio,
other?.model_price,
other?.group_ratio,
other?.user_group_ratio,
other?.cache_ratio || 1.0,
false,
1.0,
other?.web_search || false,
other?.web_search_call_count || 0,
other?.file_search || false,
other?.file_search_call_count || 0,
billingDisplayMode,
true,
other?.video_ratio || 0,
other?.video_completion_ratio || 1.0,
other?.video_output_tokens || 0,
other?.video_input_text_tokens || 0,
other?.billing_mode || '',
aggregatedQuota,
channelPriceDiscountLogPct,
other,
),
});
if (
logs[i]?.content &&
!videoBillingDetail &&
other?.billing_mode !== 'video_per_video'
) {
expandDataLocal.push({
key: t('其他详情'),
value: logs[i].content,
});
}
if (isAdminUser && other?.reject_reason) {
expandDataLocal.push({
key: t('拦截原因'),
value: other.reject_reason,
});
}
}
if (logs[i].type === 2) {
let modelMapped =
other?.is_model_mapped &&
other?.upstream_model_name &&
other?.upstream_model_name !== '';
if (modelMapped && !videoBillingDetail) {
expandDataLocal.push({
key: t('请求并计费模型'),
value: logs[i].model_name,
});
if (isAdminUser) {
expandDataLocal.push({
key: t('实际模型'),
value: other.upstream_model_name,
});
}
}
const isViolationFeeLog =
other?.violation_fee === true ||
Boolean(other?.violation_fee_code) ||
Boolean(other?.violation_fee_marker);
let content = '';
if (!isViolationFeeLog) {
content = videoBillingDetail
? videoBillingDetail
: renderConsumeBillingProcess({
record: logs[i],
other,
billingDisplayMode,
channelPriceDiscountPercent: channelPriceDiscountLogPct,
t,
});
expandDataLocal.push({
key: t('计费过程'),
value: content,
});
}
if (other?.reasoning_effort) {
expandDataLocal.push({
key: t('Reasoning Effort'),
value: other.reasoning_effort,
});
}
}
if (logs[i].type === 6) {
if (other?.task_id) {
expandDataLocal.push({
key: t('任务ID'),
value: other.task_id,
});
}
if (other?.reason) {
expandDataLocal.push({
key: t('失败原因'),
value: (
{other.reason}
),
});
}
}
if (other?.request_path) {
expandDataLocal.push({
key: t('请求路径'),
value: other.request_path,
});
}
if (isAdminUser && other?.stream_status) {
const ss = other.stream_status;
const isOk = ss.status === 'ok';
const statusLabel = isOk ? '✓ ' + t('正常') : '✗ ' + t('异常');
let streamValue =
statusLabel + ' (' + (ss.end_reason || 'unknown') + ')';
if (ss.error_count > 0) {
streamValue += ` [${t('软错误')}: ${ss.error_count}]`;
}
if (ss.end_error) {
streamValue += ` - ${ss.end_error}`;
}
expandDataLocal.push({
key: t('流状态'),
value: streamValue,
});
if (Array.isArray(ss.errors) && ss.errors.length > 0) {
expandDataLocal.push({
key: t('流错误详情'),
value: (
{ss.errors.join('\n')}
),
});
}
}
if (Array.isArray(other?.po) && other.po.length > 0) {
expandDataLocal.push({
key: t('参数覆盖'),
value: (
{
event.stopPropagation();
openParamOverrideModal(logs[i], other);
}}
/>
),
});
}
if (other?.billing_source === 'subscription') {
const planId = other?.subscription_plan_id;
const planTitle = other?.subscription_plan_title || '';
const subscriptionId = other?.subscription_id;
const unit = t('额度');
const pre = other?.subscription_pre_consumed ?? 0;
const postDelta = other?.subscription_post_delta ?? 0;
const finalConsumed = other?.subscription_consumed ?? pre + postDelta;
const remain = other?.subscription_remain;
const total = other?.subscription_total;
// Use multiple Description items to avoid an overlong single line.
if (planId) {
expandDataLocal.push({
key: t('订阅套餐'),
value: `#${planId} ${planTitle}`.trim(),
});
}
if (subscriptionId) {
expandDataLocal.push({
key: t('订阅实例'),
value: `#${subscriptionId}`,
});
}
const settlementLines = [
`${t('预扣')}:${pre} ${unit}`,
`${t('结算差额')}:${postDelta > 0 ? '+' : ''}${postDelta} ${unit}`,
`${t('最终抵扣')}:${finalConsumed} ${unit}`,
]
.filter(Boolean)
.join('\n');
expandDataLocal.push({
key: t('订阅结算'),
value: (
{settlementLines}
),
});
if (remain !== undefined && total !== undefined) {
expandDataLocal.push({
key: t('订阅剩余'),
value: `${remain}/${total} ${unit}`,
});
}
expandDataLocal.push({
key: t('订阅说明'),
value: t(
'token 会按倍率换算成“额度/次数”,请求结束后再做差额结算(补扣/返还)。',
),
});
}
if (isAdminUser && logs[i].type !== 6) {
expandDataLocal.push({
key: t('请求转换'),
value: requestConversionDisplayValue(other?.request_conversion),
});
}
if (isAdminUser && logs[i].type !== 6) {
let localCountMode = '';
if (other?.billing_mode === 'video_token') {
// Video task channels billed via duration*W*H*fps/1024 token estimate;
// fully computed locally from the request body, never reads upstream usage.
localCountMode = t('视频本地按 token 计费');
} else if (other?.billing_mode === 'video_per_second') {
localCountMode = t('分辨率阶梯计费');
} else if (other?.billing_mode === 'video_per_video') {
localCountMode = t('按视频数量计费');
} else if (other?.billing_mode === 'image_per_image') {
localCountMode = t('按张计费');
} else if (
other?.image_usd_per_image > 0 ||
(other?.use_price &&
String(other?.request_path || '').includes('/images/'))
) {
localCountMode = t('按张计费');
} else if (other?.admin_info?.local_count_tokens) {
localCountMode = t('本地计费');
} else {
localCountMode = t('上游返回');
}
expandDataLocal.push({
key: t('计费模式'),
value: localCountMode,
});
}
expandDatesLocal[logs[i].key] = expandDataLocal;
}
setExpandData(expandDatesLocal);
setLogs(logs);
};
// Load logs function
const loadLogs = async (startIdx, pageSize, customLogType = null) => {
setLoading(true);
let url = '';
const {
username,
token_name,
model_name,
start_timestamp,
end_timestamp,
channel,
group,
request_id,
logType: formLogType,
} = getFormValues();
const currentLogType =
customLogType !== null
? customLogType
: formLogType !== undefined
? formLogType
: logType;
let localStartTimestamp = Date.parse(start_timestamp) / 1000;
let localEndTimestamp = Date.parse(end_timestamp) / 1000;
if (isAdminUser) {
url = `/api/log/?p=${startIdx}&page_size=${pageSize}&type=${currentLogType}&username=${username}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&channel=${channel}&group=${group}&request_id=${request_id}`;
} else {
url = `/api/log/self/?p=${startIdx}&page_size=${pageSize}&type=${currentLogType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&group=${group}&request_id=${request_id}`;
}
url = encodeURI(url);
const res = await API.get(url);
const { success, message, data } = res.data;
if (success) {
const newPageData = data.items;
setActivePage(data.page);
setPageSize(data.page_size);
setLogCount(data.total);
setLogsFormat(newPageData);
} else {
showError(message);
}
setLoading(false);
};
// Page handlers
const handlePageChange = (page) => {
setActivePage(page);
loadLogs(page, pageSize).then((r) => {});
};
const handlePageSizeChange = async (size) => {
localStorage.setItem('page-size', size + '');
setPageSize(size);
setActivePage(1);
loadLogs(activePage, size)
.then()
.catch((reason) => {
showError(reason);
});
};
// Refresh function
const refresh = async () => {
setActivePage(1);
handleEyeClick();
await loadLogs(1, pageSize);
};
// Copy text function
const copyText = async (e, text) => {
e.stopPropagation();
if (await copy(text)) {
showSuccess('已复制:' + text);
} else {
Modal.error({ title: t('无法复制到剪贴板,请手动复制'), content: text });
}
};
// Initialize data
useEffect(() => {
const localPageSize =
parseInt(localStorage.getItem('page-size')) || ITEMS_PER_PAGE;
setPageSize(localPageSize);
loadLogs(activePage, localPageSize)
.then()
.catch((reason) => {
showError(reason);
});
}, []);
// Initialize statistics when formApi is available
useEffect(() => {
if (formApi) {
handleEyeClick();
}
}, [formApi]);
// Check if any record has expandable content
const hasExpandableRows = () => {
return logs.some(
(log) => expandData[log.key] && expandData[log.key].length > 0,
);
};
return {
// Basic state
logs,
expandData,
showStat,
loading,
loadingStat,
activePage,
logCount,
pageSize,
logType,
stat,
isAdminUser,
// Form state
formApi,
setFormApi,
formInitValues,
getFormValues,
// Column visibility
visibleColumns,
showColumnSelector,
setShowColumnSelector,
billingDisplayMode,
setBillingDisplayMode,
handleColumnVisibilityChange,
handleSelectAll,
initDefaultColumns,
COLUMN_KEYS,
// Compact mode
compactMode,
setCompactMode,
// User info modal
showUserInfo,
setShowUserInfoModal,
userInfoData,
showUserInfoFunc,
// Channel affinity usage cache stats modal
showChannelAffinityUsageCacheModal,
setShowChannelAffinityUsageCacheModal,
channelAffinityUsageCacheTarget,
openChannelAffinityUsageCacheModal,
showParamOverrideModal,
setShowParamOverrideModal,
paramOverrideTarget,
errorLogDetail,
openErrorLogDetail,
closeErrorLogDetail,
// Functions
loadLogs,
handlePageChange,
handlePageSizeChange,
refresh,
copyText,
handleEyeClick,
setLogsFormat,
hasExpandableRows,
setLogType,
openParamOverrideModal,
// Translation
t,
};
};