/* 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, Tag, Typography, Spin, } 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 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 data = payload?.data ?? null; const rateLimit = data?.rate_limit ?? {}; const { fiveHourWindow, weeklyWindow } = resolveRateLimitWindows(data); const allowed = !!rateLimit?.allowed; const limitReached = !!rateLimit?.limit_reached; const upstreamStatus = payload?.upstream_status; const statusTag = allowed && !limitReached ? ( {tt('可用')} ) : ( {tt('受限')} ); const rawText = typeof data === 'string' ? data : JSON.stringify(data ?? payload, null, 2); return (
{tt('渠道:')} {record?.name || '-'} ({tt('编号:')} {record?.id || '-'})
{statusTag}
{tt('上游状态码:')} {upstreamStatus ?? '-'}
{tt('原始 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: (
), }); };