/* 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 from 'react'; import { Button, Dropdown, Space, SplitButtonGroup, Tag, AvatarGroup, Avatar, Tooltip, Progress, Switch, Input, Modal } from '@douyinfe/semi-ui'; import { timestamp2string, renderGroup, renderQuota, getModelCategories, showError } from '../../../helpers'; import { IconTreeTriangleDown, IconCopy, IconEyeOpened, IconEyeClosed, } from '@douyinfe/semi-icons'; // Render functions function renderTimestamp(timestamp) { return <>{timestamp2string(timestamp)}; } // Render status column with switch and progress bar const renderStatus = (text, record, manageToken, t) => { const enabled = text === 1; const handleToggle = (checked) => { if (checked) { manageToken(record.id, 'enable', record); } else { manageToken(record.id, 'disable', record); } }; let tagColor = 'black'; let tagText = t('未知状态'); if (enabled) { tagColor = 'green'; tagText = t('已启用'); } else if (text === 2) { tagColor = 'red'; tagText = t('已禁用'); } else if (text === 3) { tagColor = 'yellow'; tagText = t('已过期'); } else if (text === 4) { tagColor = 'grey'; tagText = t('已耗尽'); } const used = parseInt(record.used_quota) || 0; const remain = parseInt(record.remain_quota) || 0; const total = used + remain; const percent = total > 0 ? (remain / total) * 100 : 0; const getProgressColor = (pct) => { if (pct === 100) return 'var(--semi-color-success)'; if (pct <= 10) return 'var(--semi-color-danger)'; if (pct <= 30) return 'var(--semi-color-warning)'; return undefined; }; const quotaSuffix = record.unlimited_quota ? (
{t('无限额度')}
) : (
{`${renderQuota(remain)} / ${renderQuota(total)}`} `${percent.toFixed(0)}%`} style={{ width: '100%', marginTop: '1px', marginBottom: 0 }} />
); const content = ( } suffixIcon={quotaSuffix} > {tagText} ); const tooltipContent = record.unlimited_quota ? (
{t('已用额度')}: {renderQuota(used)}
) : (
{t('已用额度')}: {renderQuota(used)}
{t('剩余额度')}: {renderQuota(remain)} ({percent.toFixed(0)}%)
{t('总额度')}: {renderQuota(total)}
); return ( {content} ); }; // Render group column const renderGroupColumn = (text, t) => { if (text === 'auto') { return ( {t('智能熔断')} ); } return renderGroup(text); }; // Render token key column with show/hide and copy functionality const renderTokenKey = (text, record, showKeys, setShowKeys, copyText) => { const fullKey = 'sk-' + record.key; const maskedKey = 'sk-' + record.key.slice(0, 4) + '**********' + record.key.slice(-4); const revealed = !!showKeys[record.id]; return (
} /> ); }; // Render model limits column const renderModelLimits = (text, record, t) => { if (record.model_limits_enabled && text) { const models = text.split(',').filter(Boolean); const categories = getModelCategories(t); const vendorAvatars = []; const matchedModels = new Set(); Object.entries(categories).forEach(([key, category]) => { if (key === 'all') return; if (!category.icon || !category.filter) return; const vendorModels = models.filter((m) => category.filter({ model_name: m })); if (vendorModels.length > 0) { vendorAvatars.push( {category.icon} ); vendorModels.forEach((m) => matchedModels.add(m)); } }); const unmatchedModels = models.filter((m) => !matchedModels.has(m)); if (unmatchedModels.length > 0) { vendorAvatars.push( {t('其他')} ); } return ( {vendorAvatars} ); } else { return ( {t('无限制')} ); } }; // Render IP restrictions column const renderAllowIps = (text, t) => { if (!text || text.trim() === '') { return ( {t('无限制')} ); } const ips = text .split('\n') .map((ip) => ip.trim()) .filter(Boolean); const displayIps = ips.slice(0, 1); const extraCount = ips.length - displayIps.length; const ipTags = displayIps.map((ip, idx) => ( {ip} )); if (extraCount > 0) { ipTags.push( {'+' + extraCount} ); } return {ipTags}; }; // Render operations column const renderOperations = (text, record, onOpenLink, setEditingToken, setShowEdit, manageToken, refresh, t) => { let chats = localStorage.getItem('chats'); let chatsArray = []; let shouldUseCustom = true; if (shouldUseCustom) { try { chats = JSON.parse(chats); if (Array.isArray(chats)) { for (let i = 0; i < chats.length; i++) { let chat = {}; chat.node = 'item'; for (let key in chats[i]) { if (chats[i].hasOwnProperty(key)) { chat.key = i; chat.name = key; chat.onClick = () => { onOpenLink(key, chats[i][key], record); }; } } chatsArray.push(chat); } } } catch (e) { console.log(e); showError(t('聊天链接配置错误,请联系管理员')); } } return ( ); }; export const getTokensColumns = ({ t, showKeys, setShowKeys, copyText, manageToken, onOpenLink, setEditingToken, setShowEdit, refresh, }) => { return [ { title: t('名称'), dataIndex: 'name', }, { title: t('状态'), dataIndex: 'status', key: 'status', render: (text, record) => renderStatus(text, record, manageToken, t), }, { title: t('分组'), dataIndex: 'group', key: 'group', render: (text) => renderGroupColumn(text, t), }, { title: t('密钥'), key: 'token_key', render: (text, record) => renderTokenKey(text, record, showKeys, setShowKeys, copyText), }, { title: t('可用模型'), dataIndex: 'model_limits', render: (text, record) => renderModelLimits(text, record, t), }, { title: t('IP限制'), dataIndex: 'allow_ips', render: (text) => renderAllowIps(text, t), }, { title: t('创建时间'), dataIndex: 'created_time', render: (text, record, index) => { return
{renderTimestamp(text)}
; }, }, { title: t('过期时间'), dataIndex: 'expired_time', render: (text, record, index) => { return (
{record.expired_time === -1 ? t('永不过期') : renderTimestamp(text)}
); }, }, { title: '', dataIndex: 'operate', fixed: 'right', render: (text, record, index) => renderOperations( text, record, onOpenLink, setEditingToken, setShowEdit, manageToken, refresh, t ), }, ]; };