/* 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, { useEffect, useState, useContext, useRef } from 'react'; import { API, showError, showSuccess, timestamp2string, renderGroupOption, renderQuotaWithPrompt, getModelCategories, selectFilter, } from '../../../../helpers'; import { useIsMobile } from '../../../../hooks/common/useIsMobile'; import { Button, SideSheet, Space, Spin, Typography, Card, Tag, Avatar, Form, Col, Row, } from '@douyinfe/semi-ui'; import { IconCreditCard, IconLink, IconSave, IconClose, IconKey, } from '@douyinfe/semi-icons'; import { useTranslation } from 'react-i18next'; import { StatusContext } from '../../../../context/Status'; const { Text, Title } = Typography; const EditTokenModal = (props) => { const { t } = useTranslation(); const [statusState, statusDispatch] = useContext(StatusContext); const [loading, setLoading] = useState(false); const isMobile = useIsMobile(); const formApiRef = useRef(null); const [models, setModels] = useState([]); const [groups, setGroups] = useState([]); const isEdit = props.editingToken.id !== undefined; const getInitValues = () => ({ name: '', remain_quota: 0, expired_time: -1, unlimited_quota: true, model_limits_enabled: false, model_limits: [], allow_ips: '', group: '', tokenCount: 1, }); const handleCancel = () => { props.handleClose(); }; const setExpiredTime = (month, day, hour, minute) => { let now = new Date(); let timestamp = now.getTime() / 1000; let seconds = month * 30 * 24 * 60 * 60; seconds += day * 24 * 60 * 60; seconds += hour * 60 * 60; seconds += minute * 60; if (!formApiRef.current) return; if (seconds !== 0) { timestamp += seconds; formApiRef.current.setValue('expired_time', timestamp2string(timestamp)); } else { formApiRef.current.setValue('expired_time', -1); } }; const loadModels = async () => { let res = await API.get(`/api/user/models`); const { success, message, data } = res.data; if (success) { const categories = getModelCategories(t); let localModelOptions = data.map((model) => { let icon = null; for (const [key, category] of Object.entries(categories)) { if (key !== 'all' && category.filter({ model_name: model })) { icon = category.icon; break; } } return { label: ( {icon} {model} ), value: model, }; }); setModels(localModelOptions); } else { showError(t(message)); } }; const loadGroups = async () => { let res = await API.get(`/api/user/self/groups`); const { success, message, data } = res.data; if (success) { let localGroupOptions = Object.entries(data).map(([group, info]) => ({ label: info.desc, value: group, ratio: info.ratio, })); if (statusState?.status?.default_use_auto_group) { if (localGroupOptions.some((group) => group.value === 'auto')) { localGroupOptions.sort((a, b) => (a.value === 'auto' ? -1 : 1)); } } setGroups(localGroupOptions); // if (statusState?.status?.default_use_auto_group && formApiRef.current) { // formApiRef.current.setValue('group', 'auto'); // } } else { showError(t(message)); } }; const loadToken = async () => { setLoading(true); let res = await API.get(`/api/token/${props.editingToken.id}`); const { success, message, data } = res.data; if (success) { if (data.expired_time !== -1) { data.expired_time = timestamp2string(data.expired_time); } if (data.model_limits !== '') { data.model_limits = data.model_limits.split(','); } else { data.model_limits = []; } if (formApiRef.current) { formApiRef.current.setValues({ ...getInitValues(), ...data }); } } else { showError(message); } setLoading(false); }; useEffect(() => { if (formApiRef.current) { if (!isEdit) { formApiRef.current.setValues(getInitValues()); } } loadModels(); loadGroups(); }, [props.editingToken.id]); useEffect(() => { if (props.visiable) { if (isEdit) { loadToken(); } else { formApiRef.current?.setValues(getInitValues()); } } else { formApiRef.current?.reset(); } }, [props.visiable, props.editingToken.id]); const generateRandomSuffix = () => { const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; let result = ''; for (let i = 0; i < 6; i++) { result += characters.charAt( Math.floor(Math.random() * characters.length), ); } return result; }; const submit = async (values) => { setLoading(true); if (isEdit) { let { tokenCount: _tc, ...localInputs } = values; localInputs.remain_quota = parseInt(localInputs.remain_quota); if (localInputs.expired_time !== -1) { let time = Date.parse(localInputs.expired_time); if (isNaN(time)) { showError(t('过期时间格式错误!')); setLoading(false); return; } localInputs.expired_time = Math.ceil(time / 1000); } localInputs.model_limits = localInputs.model_limits.join(','); localInputs.model_limits_enabled = localInputs.model_limits.length > 0; let res = await API.put(`/api/token/`, { ...localInputs, id: parseInt(props.editingToken.id), }); const { success, message } = res.data; if (success) { showSuccess(t('令牌更新成功!')); props.refresh(); props.handleClose(); } else { showError(t(message)); } } else { const count = parseInt(values.tokenCount, 10) || 1; let successCount = 0; for (let i = 0; i < count; i++) { let { tokenCount: _tc, ...localInputs } = values; const baseName = values.name.trim() === '' ? 'default' : values.name.trim(); if (i !== 0 || values.name.trim() === '') { localInputs.name = `${baseName}-${generateRandomSuffix()}`; } else { localInputs.name = baseName; } localInputs.remain_quota = parseInt(localInputs.remain_quota); if (localInputs.expired_time !== -1) { let time = Date.parse(localInputs.expired_time); if (isNaN(time)) { showError(t('过期时间格式错误!')); setLoading(false); break; } localInputs.expired_time = Math.ceil(time / 1000); } localInputs.model_limits = localInputs.model_limits.join(','); localInputs.model_limits_enabled = localInputs.model_limits.length > 0; let res = await API.post(`/api/token/`, localInputs); const { success, message } = res.data; if (success) { successCount++; } else { showError(t(message)); break; } } if (successCount > 0) { showSuccess(t('令牌创建成功,请在列表页面点击复制获取令牌!')); props.refresh(); props.handleClose(); } } setLoading(false); formApiRef.current?.setValues(getInitValues()); }; return ( {isEdit ? ( {t('更新')} ) : ( {t('新建')} )} {isEdit ? t('更新令牌信息') : t('创建新的令牌')} } bodyStyle={{ padding: '0' }} visible={props.visiable} width={isMobile ? '100%' : 600} footer={
} closeIcon={null} onCancel={() => handleCancel()} >
(formApiRef.current = api)} onSubmit={submit} > {({ values }) => (
{/* 基本信息 */}
{t('基本信息')}
{t('设置令牌的基本信息')}
{groups.length > 0 ? ( ) : ( )} { // 允许 -1 表示永不过期,也允许空值在必填校验时被拦截 if (value === -1 || !value) return Promise.resolve(); const time = Date.parse(value); if (isNaN(time)) { return Promise.reject(t('过期时间格式错误!')); } if (time <= Date.now()) { return Promise.reject( t('过期时间不能早于当前时间!'), ); } return Promise.resolve(); }, }, ]} showClear style={{ width: '100%' }} /> {!isEdit && ( )}
{/* 额度设置 */}
{t('额度设置')}
{t('设置令牌可用额度和数量')}
{/* 访问限制 */}
{t('访问限制')}
{t('设置令牌的访问限制')}
)}
); }; export default EditTokenModal;