/* Copyright (c) 2025 Tethys Plex This file is part of Veloera. This program is free software: you can redistribute it and/or modify it under the terms of the GNU 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ import React, { useContext, useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { API, copy, isRoot, showError, showInfo, showSuccess, } from '../helpers'; import Turnstile from 'react-turnstile'; import { UserContext } from '../context/User'; import { onGitHubOAuthClicked, onOIDCClicked, onLinuxDOOAuthClicked, onIDCFlareOAuthClicked, } from './utils'; import { Avatar, Banner, Button, Card, Descriptions, Image, Input, InputNumber, Layout, Modal, Space, Tag, Typography, Collapsible, Select, Radio, RadioGroup, AutoComplete, Checkbox, Tabs, TabPane, Switch, } from '@douyinfe/semi-ui'; import { getQuotaPerUnit, renderQuota, renderQuotaWithPrompt, stringToColor, } from '../helpers/render'; import TelegramLoginButton from 'react-telegram-login'; import { useTranslation } from 'react-i18next'; const PersonalSetting = () => { const [userState, userDispatch] = useContext(UserContext); let navigate = useNavigate(); const { t } = useTranslation(); const [inputs, setInputs] = useState({ wechat_verification_code: '', email_verification_code: '', email: '', self_account_deletion_confirmation: '', set_new_password: '', set_new_password_confirmation: '', }); const [status, setStatus] = useState({}); const [showChangePasswordModal, setShowChangePasswordModal] = useState(false); const [showWeChatBindModal, setShowWeChatBindModal] = useState(false); const [showEmailBindModal, setShowEmailBindModal] = useState(false); const [showAccountDeleteModal, setShowAccountDeleteModal] = useState(false); const [turnstileEnabled, setTurnstileEnabled] = useState(false); const [turnstileSiteKey, setTurnstileSiteKey] = useState(''); const [turnstileToken, setTurnstileToken] = useState(''); const [loading, setLoading] = useState(false); const [disableButton, setDisableButton] = useState(false); const [countdown, setCountdown] = useState(30); const [affLink, setAffLink] = useState(''); const [systemToken, setSystemToken] = useState(''); const [affEnabled, setAffEnabled] = useState(true); const [models, setModels] = useState([]); const [openTransfer, setOpenTransfer] = useState(false); const [transferAmount, setTransferAmount] = useState(0); const [checkInEnabled, setCheckInEnabled] = useState(false); const [checkInLoading, setCheckInLoading] = useState(false); const [canCheckIn, setCanCheckIn] = useState(true); const [isModelsExpanded, setIsModelsExpanded] = useState(() => { // Initialize from localStorage if available const savedState = localStorage.getItem('modelsExpanded'); return savedState ? JSON.parse(savedState) : false; }); const MODELS_DISPLAY_COUNT = 10; // 默认显示的模型数量 const [notificationSettings, setNotificationSettings] = useState({ warningType: 'email', warningThreshold: 100000, webhookUrl: '', webhookSecret: '', notificationEmail: '', acceptUnsetModelRatioModel: false, showIPInLogs: false, }); const [showWebhookDocs, setShowWebhookDocs] = useState(false); useEffect(() => { let status = localStorage.getItem('status'); if (status) { status = JSON.parse(status); setStatus(status); if (status.turnstile_check) { setTurnstileEnabled(true); setTurnstileSiteKey(status.turnstile_site_key); } setCheckInEnabled(status.check_in_enabled === true); setAffEnabled(status.aff_enabled === true); } getUserData().then((res) => { console.log(userState); }); loadModels().then(); if (status && status.aff_enabled === true) { getAffLink().then(); } checkUserCanCheckIn().then(); setTransferAmount(getQuotaPerUnit()); }, []); // 监听checkInEnabled状态变化,确保状态同步 useEffect(() => { if (checkInEnabled) { checkUserCanCheckIn(); } }, [checkInEnabled]); useEffect(() => { let countdownInterval = null; if (disableButton && countdown > 0) { countdownInterval = setInterval(() => { setCountdown(countdown - 1); }, 1000); } else if (countdown === 0) { setDisableButton(false); setCountdown(30); } return () => clearInterval(countdownInterval); // Clean up on unmount }, [disableButton, countdown]); useEffect(() => { if (userState?.user?.setting) { const settings = JSON.parse(userState.user.setting); setNotificationSettings({ warningType: settings.notify_type || 'email', warningThreshold: settings.quota_warning_threshold || 500000, webhookUrl: settings.webhook_url || '', webhookSecret: settings.webhook_secret || '', notificationEmail: settings.notification_email || '', acceptUnsetModelRatioModel: settings.accept_unset_model_ratio_model || false, showIPInLogs: settings.show_ip_in_logs || false, }); } }, [userState?.user?.setting]); // Save models expanded state to localStorage whenever it changes useEffect(() => { localStorage.setItem('modelsExpanded', JSON.stringify(isModelsExpanded)); }, [isModelsExpanded]); const handleInputChange = (name, value) => { setInputs((inputs) => ({ ...inputs, [name]: value })); }; const generateAccessToken = async () => { const res = await API.get('/api/user/token'); const { success, message, data } = res.data; if (success) { setSystemToken(data); await copy(data); showSuccess(t('令牌已重置并已复制到剪贴板')); } else { showError(message); } }; const getAffLink = async () => { const res = await API.get('/api/user/aff'); const { success, message, data } = res.data; if (success) { let link = `${window.location.origin}/register?aff=${data}`; setAffLink(link); } else { showError(message); } }; const getUserData = async () => { let res = await API.get(`/api/user/self`); const { success, message, data } = res.data; if (success) { userDispatch({ type: 'login', payload: data }); } else { showError(message); } }; const loadModels = async () => { let res = await API.get(`/api/user/models`); const { success, message, data } = res.data; if (success) { if (data != null) { setModels(data); } } else { showError(message); } }; const handleAffLinkClick = async (e) => { e.target.select(); await copy(e.target.value); showSuccess(t('邀请链接已复制到剪切板')); }; const handleSystemTokenClick = async (e) => { e.target.select(); await copy(e.target.value); showSuccess(t('系统令牌已复制到剪切板')); }; const checkUserCanCheckIn = async () => { if (!checkInEnabled) return; try { const res = await API.get('/api/user/check_in_status'); const { success, message, data } = res.data; if (success) { setCanCheckIn(data.can_check_in); } else { showError(message); } } catch (error) { console.error('Failed to check user check-in status:', error); } }; const handleCheckIn = async () => { if (!checkInEnabled || !canCheckIn) return; if (turnstileEnabled && turnstileToken === '') { showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!'); return; } try { setCheckInLoading(true); const url = turnstileEnabled ? `/api/user/check_in?turnstile=${turnstileToken}` : '/api/user/check_in'; const res = await API.post(url); const { success, message, data } = res.data; if (success) { const quotaPerUnit = status.quota_per_unit || 500000; const dollarAmount = (data.quota / quotaPerUnit).toFixed(2); showSuccess(t('签到成功!获得 ') + `$${dollarAmount}`); setCanCheckIn(false); getUserData(); // Refresh user data to get updated quota } else { showError(message); } } catch (error) { showError(t('签到失败,请稍后再试')); console.error('Check-in failed:', error); } finally { setCheckInLoading(false); } }; const deleteAccount = async () => { if (inputs.self_account_deletion_confirmation !== userState.user.username) { showError(t('请输入你的账户名以确认删除!')); return; } const res = await API.delete('/api/user/self'); const { success, message } = res.data; if (success) { showSuccess(t('账户已删除!')); await API.get('/api/user/logout'); userDispatch({ type: 'logout' }); localStorage.removeItem('user'); navigate('/login'); } else { showError(message); } }; const bindWeChat = async () => { if (inputs.wechat_verification_code === '') return; const res = await API.get( `/api/oauth/wechat/bind?code=${inputs.wechat_verification_code}`, ); const { success, message } = res.data; if (success) { showSuccess(t('微信账户绑定成功!')); setShowWeChatBindModal(false); } else { showError(message); } }; const changePassword = async () => { if (inputs.set_new_password !== inputs.set_new_password_confirmation) { showError(t('两次输入的密码不一致!')); return; } const res = await API.put(`/api/user/self`, { password: inputs.set_new_password, }); const { success, message } = res.data; if (success) { showSuccess(t('密码修改成功!')); setShowWeChatBindModal(false); } else { showError(message); } setShowChangePasswordModal(false); }; const transfer = async () => { if (transferAmount < getQuotaPerUnit()) { showError(t('划转金额最低为') + ' ' + renderQuota(getQuotaPerUnit())); return; } const res = await API.post(`/api/user/aff_transfer`, { quota: transferAmount, }); const { success, message } = res.data; if (success) { showSuccess(message); setOpenTransfer(false); getUserData().then(); } else { showError(message); } }; const sendVerificationCode = async () => { if (inputs.email === '') { showError(t('请输入邮箱!')); return; } setDisableButton(true); if (turnstileEnabled && turnstileToken === '') { showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!'); return; } setLoading(true); const res = await API.get( `/api/verification?email=${inputs.email}&turnstile=${turnstileToken}`, ); const { success, message } = res.data; if (success) { showSuccess(t('验证码发送成功,请检查邮箱!')); } else { showError(message); } setLoading(false); }; const bindEmail = async () => { if (inputs.email_verification_code === '') { showError(t('请输入邮箱验证码!')); return; } setLoading(true); const res = await API.get( `/api/oauth/email/bind?email=${inputs.email}&code=${inputs.email_verification_code}`, ); const { success, message } = res.data; if (success) { showSuccess(t('邮箱账户绑定成功!')); setShowEmailBindModal(false); userState.user.email = inputs.email; } else { showError(message); } setLoading(false); }; const getDisplayName = () => { if (userState.user) { return userState.user.display_name || userState.user.username; } else { return 'null'; } }; const handleCancel = () => { setOpenTransfer(false); }; const copyText = async (text) => { if (await copy(text)) { showSuccess(t('已复制:') + text); } else { // setSearchKeyword(text); Modal.error({ title: t('无法复制到剪贴板,请手动复制'), content: text }); } }; const handleNotificationSettingChange = (type, value) => { setNotificationSettings((prev) => ({ ...prev, [type]: value.target ? value.target.value : value, // 处理 Radio 事件对象 })); }; const saveNotificationSettings = async () => { try { const res = await API.put('/api/user/setting', { notify_type: notificationSettings.warningType, quota_warning_threshold: parseFloat( notificationSettings.warningThreshold, ), webhook_url: notificationSettings.webhookUrl, webhook_secret: notificationSettings.webhookSecret, notification_email: notificationSettings.notificationEmail, accept_unset_model_ratio_model: notificationSettings.acceptUnsetModelRatioModel, show_ip_in_logs: notificationSettings.showIPInLogs, }); if (res.data.success) { showSuccess(t('设置已更新')); await getUserData(); } else { showError(res.data.message); } } catch (error) { showError(t('更新设置失败')); } }; return (
{affEnabled && (
{t('可用额度')} {renderQuotaWithPrompt(userState?.user?.aff_quota)}
{t('划转额度')} {renderQuotaWithPrompt(transferAmount)}{' '} {t('最低') + renderQuota(getQuotaPerUnit())}
setTransferAmount(value)} disabled={false} >
)}
{typeof getDisplayName() === 'string' && getDisplayName().slice(0, 1)} } title={{getDisplayName()}} description={ isRoot() ? ( {t('管理员')} ) : ( {t('普通用户')} ) } > } headerExtraContent={ <> {'ID: ' + userState?.user?.id} {userState?.user?.group} } footer={ <>
{t('可用模型')}
{models.length <= MODELS_DISPLAY_COUNT ? ( {models.map((model) => ( { copyText(model); }} > {model} ))} ) : ( <> {models.map((model) => ( { copyText(model); }} > {model} ))} setIsModelsExpanded(false)} > {t('收起')} {!isModelsExpanded && ( {models .slice(0, MODELS_DISPLAY_COUNT) .map((model) => ( { copyText(model); }} > {model} ))} setIsModelsExpanded(true)} > {t('更多')} {models.length - MODELS_DISPLAY_COUNT}{' '} {t('个模型')} )} )}
} > {renderQuota(userState?.user?.quota)} {renderQuota(userState?.user?.used_quota)} {userState.user?.request_count}
{checkInEnabled && ( {t('每日签到')}
{t('签到可获得额外的 Token 奖励。每天只能签到一次,请按时签到哦!')}
{!canCheckIn && ( {t('已完成今日签到,明天再来哦!')} )}
{turnstileEnabled && canCheckIn && (
{ setTurnstileToken(token); }} onExpire={() => { setTurnstileToken(''); }} />
)}
)} {affEnabled && ( {t('邀请链接')}
} > {t('邀请信息')}
{renderQuota(userState?.user?.aff_quota)} {renderQuota(userState?.user?.aff_history_quota)} {userState?.user?.aff_count}
)} {t('个人信息')}
{t('邮箱')}
{status.wechat_login && (
{t('微信')}
)} {status.github_oauth && (
{t('GitHub')}
)} {status.oidc_enabled && (
{t('OIDC')}
)} {status.telegram_oauth && (
{t('Telegram')}
{userState.user.telegram_id !== '' ? ( ) : ( )}
)} {status.linuxdo_oauth && (
{t('LinuxDO')}
)} {status.idcflare_oauth && (
{t('IDC Flare')}
)}
{systemToken && ( )} setShowWeChatBindModal(false)} visible={showWeChatBindModal} size={'small'} >

微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)

handleInputChange('wechat_verification_code', v) } />
{t('通知方式')}
handleNotificationSettingChange('warningType', value) } > {t('邮件通知')} {t('Webhook通知')}
{notificationSettings.warningType === 'webhook' && ( <>
{t('Webhook地址')}
handleNotificationSettingChange('webhookUrl', val) } placeholder={t( '请输入Webhook地址,例如: https://example.com/webhook', )} /> {t( '只支持https,系统将以 POST 方式发送通知,请确保地址可以接收 POST 请求', )}
setShowWebhookDocs(!showWebhookDocs) } > {t('Webhook请求结构')}{' '} {showWebhookDocs ? '▼' : '▶'}
                                {`{
    "type": "quota_exceed",      // 通知类型
    "title": "标题",             // 通知标题
    "content": "通知内容",       // 通知内容,支持 {{value}} 变量占位符
    "values": ["值1", "值2"],    // 按顺序替换content中的 {{value}} 占位符
    "timestamp": 1739950503      // 时间戳
}

示例:
{
    "type": "quota_exceed",
    "title": "额度预警通知",
    "content": "您的额度即将用尽,当前剩余额度为 {{value}}",
    "values": ["$0.99"],
    "timestamp": 1739950503
}`}
                              
{t('接口凭证(可选)')}
handleNotificationSettingChange( 'webhookSecret', val, ) } placeholder={t('请输入密钥')} /> {t( '密钥将以 Bearer 方式添加到请求头中,用于验证webhook请求的合法性', )} {t('Authorization: Bearer your-secret-key')}
)} {notificationSettings.warningType === 'email' && (
{t('通知邮箱')}
handleNotificationSettingChange( 'notificationEmail', val, ) } placeholder={t('留空则使用账号绑定的邮箱')} /> {t( '设置用于接收额度预警的邮箱地址,不填则使用账号绑定的邮箱', )}
)}
{t('额度预警阈值')}{' '} {renderQuotaWithPrompt( notificationSettings.warningThreshold, )}
handleNotificationSettingChange( 'warningThreshold', val, ) } style={{ width: 200 }} placeholder={t('请输入预警额度')} data={[ { value: 100000, label: '0.2$' }, { value: 500000, label: '1$' }, { value: 1000000, label: '5$' }, { value: 5000000, label: '10$' }, ]} />
{t( '当剩余额度低于此数值时,系统将通过选择的方式发送通知', )}
{t('接受未设置价格模型')}
handleNotificationSettingChange('acceptUnsetModelRatioModel', e.target.checked)} > {t('接受未设置价格模型')} {t('当模型没有设置价格时仍接受调用,仅当您信任该网站时使用,可能会产生高额费用')}
{t('在消费日志显示调用 IP')}
handleNotificationSettingChange('showIPInLogs', checked) } /> {t('启用后,您的消费日志中将显示请求的 IP 地址,用于安全监控和审计')}
setShowEmailBindModal(false)} onOk={bindEmail} visible={showEmailBindModal} size={'small'} centered={true} maskClosable={false} > {t('绑定邮箱地址')}
handleInputChange('email', value)} name='email' type='email' />
handleInputChange('email_verification_code', value) } />
{turnstileEnabled ? ( { setTurnstileToken(token); }} /> ) : ( <> )}
setShowAccountDeleteModal(false)} visible={showAccountDeleteModal} size={'small'} centered={true} onOk={deleteAccount} >
handleInputChange( 'self_account_deletion_confirmation', value, ) } /> {turnstileEnabled ? ( { setTurnstileToken(token); }} /> ) : ( <> )}
setShowChangePasswordModal(false)} visible={showChangePasswordModal} size={'small'} centered={true} onOk={changePassword} >
handleInputChange('set_new_password', value) } /> handleInputChange('set_new_password_confirmation', value) } /> {turnstileEnabled ? ( { setTurnstileToken(token); }} /> ) : ( <> )}
); }; export default PersonalSetting;