/* 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 React, { useCallback, useEffect, useRef, useState } from 'react'; import { Modal, Button, Progress, Typography, Spin, Tag, Descriptions, Collapse, } from '@douyinfe/semi-ui'; import { API, showError } from '../../../../helpers'; const { Text } = Typography; const clampPercent = (value) => { const v = Number(value); if (!Number.isFinite(v)) return 0; return Math.max(0, Math.min(100, v)); }; const pickStrokeColor = (percent) => { const p = clampPercent(percent); if (p >= 95) return '#ef4444'; if (p >= 80) return '#f59e0b'; return '#3b82f6'; }; const normalizePlanType = (value) => { if (value == null) return ''; return String(value).trim().toLowerCase(); }; const getWindowDurationSeconds = (windowData) => { const value = Number(windowData?.limit_window_seconds); if (!Number.isFinite(value) || value <= 0) return null; return value; }; const classifyWindowByDuration = (windowData) => { const seconds = getWindowDurationSeconds(windowData); if (seconds == null) return null; return seconds >= 24 * 60 * 60 ? 'weekly' : 'fiveHour'; }; const resolveRateLimitWindows = (data) => { const rateLimit = data?.rate_limit ?? {}; const primary = rateLimit?.primary_window ?? null; const secondary = rateLimit?.secondary_window ?? null; const windows = [primary, secondary].filter(Boolean); const planType = normalizePlanType(data?.plan_type ?? rateLimit?.plan_type); let fiveHourWindow = null; let weeklyWindow = null; for (const windowData of windows) { const bucket = classifyWindowByDuration(windowData); if (bucket === 'fiveHour' && !fiveHourWindow) { fiveHourWindow = windowData; continue; } if (bucket === 'weekly' && !weeklyWindow) { weeklyWindow = windowData; } } if (planType === 'free') { if (!weeklyWindow) { weeklyWindow = primary ?? secondary ?? null; } return { fiveHourWindow: null, weeklyWindow }; } if (!fiveHourWindow && !weeklyWindow) { return { fiveHourWindow: primary ?? null, weeklyWindow: secondary ?? null, }; } if (!fiveHourWindow) { fiveHourWindow = windows.find((windowData) => windowData !== weeklyWindow) ?? null; } if (!weeklyWindow) { weeklyWindow = windows.find((windowData) => windowData !== fiveHourWindow) ?? null; } return { fiveHourWindow, weeklyWindow }; }; const formatDurationSeconds = (seconds, t) => { const tt = typeof t === 'function' ? t : (v) => v; const s = Number(seconds); if (!Number.isFinite(s) || s <= 0) return '-'; const total = Math.floor(s); const hours = Math.floor(total / 3600); const minutes = Math.floor((total % 3600) / 60); const secs = total % 60; if (hours > 0) return `${hours}${tt('小时')} ${minutes}${tt('分钟')}`; if (minutes > 0) return `${minutes}${tt('分钟')} ${secs}${tt('秒')}`; return `${secs}${tt('秒')}`; }; const formatUnixSeconds = (unixSeconds) => { const v = Number(unixSeconds); if (!Number.isFinite(v) || v <= 0) return '-'; try { return new Date(v * 1000).toLocaleString(); } catch (error) { return String(unixSeconds); } }; const getDisplayText = (value) => { if (value == null) return ''; return String(value).trim(); }; const formatAccountTypeLabel = (value, t) => { const tt = typeof t === 'function' ? t : (v) => v; const normalized = normalizePlanType(value); switch (normalized) { case 'free': return 'Free'; case 'plus': return 'Plus'; case 'pro': return 'Pro'; case 'team': return 'Team'; case 'enterprise': return 'Enterprise'; default: return getDisplayText(value) || tt('未识别'); } }; const getAccountTypeTagColor = (value) => { const normalized = normalizePlanType(value); switch (normalized) { case 'enterprise': return 'green'; case 'team': return 'cyan'; case 'pro': return 'blue'; case 'plus': return 'violet'; case 'free': return 'amber'; default: return 'grey'; } }; const resolveUsageStatusTag = (t, rateLimit) => { const tt = typeof t === 'function' ? t : (v) => v; if (!rateLimit || Object.keys(rateLimit).length === 0) { return {tt('待确认')}; } if (rateLimit?.allowed && !rateLimit?.limit_reached) { return {tt('可用')}; } return {tt('受限')}; }; const AccountInfoValue = ({ t, value, onCopy, monospace = false }) => { const tt = typeof t === 'function' ? t : (v) => v; const text = getDisplayText(value); const hasValue = text !== ''; return (
{hasValue ? text : '-'}
); }; const RateLimitWindowCard = ({ t, title, windowData }) => { const tt = typeof t === 'function' ? t : (v) => v; const hasWindowData = !!windowData && typeof windowData === 'object' && Object.keys(windowData).length > 0; const percent = clampPercent(windowData?.used_percent ?? 0); const resetAt = windowData?.reset_at; const resetAfterSeconds = windowData?.reset_after_seconds; const limitWindowSeconds = windowData?.limit_window_seconds; return (
{title}
{tt('重置时间:')} {formatUnixSeconds(resetAt)}
{hasWindowData ? (
) : (
-
)}
{tt('已使用:')} {hasWindowData ? `${percent}%` : '-'}
{tt('距离重置:')} {hasWindowData ? formatDurationSeconds(resetAfterSeconds, tt) : '-'}
{tt('窗口:')} {hasWindowData ? formatDurationSeconds(limitWindowSeconds, tt) : '-'}
); }; const CodexUsageView = ({ t, record, payload, onCopy, onRefresh }) => { const tt = typeof t === 'function' ? t : (v) => v; const [showRawJson, setShowRawJson] = useState(false); const data = payload?.data ?? null; const rateLimit = data?.rate_limit ?? {}; const { fiveHourWindow, weeklyWindow } = resolveRateLimitWindows(data); const upstreamStatus = payload?.upstream_status; const accountType = data?.plan_type ?? rateLimit?.plan_type; const accountTypeLabel = formatAccountTypeLabel(accountType, tt); const accountTypeTagColor = getAccountTypeTagColor(accountType); const statusTag = resolveUsageStatusTag(tt, rateLimit); const userId = data?.user_id; const email = data?.email; const accountId = data?.account_id; const errorMessage = payload?.success === false ? getDisplayText(payload?.message) || tt('获取用量失败') : ''; const rawText = typeof data === 'string' ? data : JSON.stringify(data ?? payload, null, 2); return (
{errorMessage && (
{errorMessage}
)}
{tt('Codex 帐号')}
{accountTypeLabel} {statusTag} {tt('上游状态码:')} {upstreamStatus ?? '-'}
{tt('渠道:')} {record?.name || '-'} ({tt('编号:')} {record?.id || '-'})
{tt('额度窗口')}
{tt('用于观察当前帐号在 Codex 上游的限额使用情况')}
{ const keys = Array.isArray(activeKey) ? activeKey : [activeKey]; setShowRawJson(keys.includes('raw-json')); }} >
            {rawText}
          
); }; const CodexUsageLoader = ({ t, record, initialPayload, onCopy }) => { const tt = typeof t === 'function' ? t : (v) => v; const [loading, setLoading] = useState(!initialPayload); const [payload, setPayload] = useState(initialPayload ?? null); const hasShownErrorRef = useRef(false); const mountedRef = useRef(true); const recordId = record?.id; const fetchUsage = useCallback(async () => { if (!recordId) { if (mountedRef.current) setPayload(null); return; } if (mountedRef.current) setLoading(true); try { const res = await API.get(`/api/channel/${recordId}/codex/usage`, { skipErrorHandler: true, }); if (!mountedRef.current) return; setPayload(res?.data ?? null); if (!res?.data?.success && !hasShownErrorRef.current) { hasShownErrorRef.current = true; showError(tt('获取用量失败')); } } catch (error) { if (!mountedRef.current) return; if (!hasShownErrorRef.current) { hasShownErrorRef.current = true; showError(tt('获取用量失败')); } setPayload({ success: false, message: String(error) }); } finally { if (mountedRef.current) setLoading(false); } }, [recordId, tt]); useEffect(() => { mountedRef.current = true; return () => { mountedRef.current = false; }; }, []); useEffect(() => { if (initialPayload) return; fetchUsage().catch(() => {}); }, [fetchUsage, initialPayload]); if (loading) { return (
); } if (!payload) { return (
{tt('获取用量失败')}
); } return ( ); }; export const openCodexUsageModal = ({ t, record, payload, onCopy }) => { const tt = typeof t === 'function' ? t : (v) => v; Modal.info({ title: tt('Codex 帐号与用量'), centered: true, width: 900, style: { maxWidth: '95vw' }, content: ( ), footer: (
), }); };