/* 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, Popover, Typography, Input, Modal, } from '@douyinfe/semi-ui'; import { timestamp2string, renderGroup, renderQuota, getModelCategories, showError, } from '../../../helpers'; import { IconTreeTriangleDown, IconCopy, IconEyeOpened, IconEyeClosed, } from '@douyinfe/semi-icons'; // progress color helper 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; }; // Render functions function renderTimestamp(timestamp) { return <>{timestamp2string(timestamp)}; } // Render status column only (no usage) const renderStatus = (text, record, t) => { const enabled = text === 1; 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('已耗尽'); } return ( {tagText} ); }; // 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 separate quota usage column const renderQuotaUsage = (text, record, t) => { const { Paragraph } = Typography; const used = parseInt(record.used_quota) || 0; const remain = parseInt(record.remain_quota) || 0; const total = used + remain; if (record.unlimited_quota) { const popoverContent = (
{t('已用额度')}: {renderQuota(used)}
); return ( {t('无限额度')} ); } const percent = total > 0 ? (remain / total) * 100 : 0; const popoverContent = (
{t('已用额度')}: {renderQuota(used)} {t('剩余额度')}: {renderQuota(remain)} ({percent.toFixed(0)}%) {t('总额度')}: {renderQuota(total)}
); return (
{`${renderQuota(remain)} / ${renderQuota(total)}`} `${percent.toFixed(0)}%`} style={{ width: '100%', marginTop: '1px', marginBottom: 0 }} />
); }; // Render operations column const renderOperations = ( text, record, onOpenLink, setEditingToken, setShowEdit, manageToken, refresh, t, ) => { let chatsArray = []; try { const raw = localStorage.getItem('chats'); const parsed = JSON.parse(raw); if (Array.isArray(parsed)) { for (let i = 0; i < parsed.length; i++) { const item = parsed[i]; const name = Object.keys(item)[0]; if (!name) continue; chatsArray.push({ node: 'item', key: i, name, value: item[name], onClick: () => onOpenLink(name, item[name], record), }); } } } catch (_) { showError(t('聊天链接配置错误,请联系管理员')); } return ( {record.status === 1 ? ( ) : ( )} ); }; 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, t), }, { title: t('剩余额度/总额度'), key: 'quota_usage', render: (text, record) => renderQuotaUsage(text, record, 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, ), }, ]; };