/* 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, { useRef, useEffect, useState, useContext } from 'react'; import { Button, Typography, Card, Avatar, Form, Radio, Toast, Tabs, TabPane, Switch, Row, Col, } from '@douyinfe/semi-ui'; import { IconMail, IconKey, IconBell, IconLink } from '@douyinfe/semi-icons'; import { ShieldCheck, Bell, DollarSign, Settings } from 'lucide-react'; import { renderQuotaWithPrompt, API, showSuccess, showError, } from '../../../../helpers'; import CodeViewer from '../../../playground/CodeViewer'; import { StatusContext } from '../../../../context/Status'; import { UserContext } from '../../../../context/User'; import { useUserPermissions } from '../../../../hooks/common/useUserPermissions'; import { useSidebar } from '../../../../hooks/common/useSidebar'; const NotificationSettings = ({ t, notificationSettings, handleNotificationSettingChange, saveNotificationSettings, }) => { const formApiRef = useRef(null); const [statusState] = useContext(StatusContext); const [userState] = useContext(UserContext); // 左侧边栏设置相关状态 const [sidebarLoading, setSidebarLoading] = useState(false); const [activeTabKey, setActiveTabKey] = useState('notification'); const [sidebarModulesUser, setSidebarModulesUser] = useState({ chat: { enabled: true, playground: true, chat: true, }, console: { enabled: true, detail: true, token: true, log: true, midjourney: true, task: true, }, personal: { enabled: true, topup: true, personal: true, }, admin: { enabled: true, channel: true, models: true, redemption: true, user: true, setting: true, }, }); const [adminConfig, setAdminConfig] = useState(null); // 使用后端权限验证替代前端角色判断 const { permissions, loading: permissionsLoading, hasSidebarSettingsPermission, isSidebarSectionAllowed, isSidebarModuleAllowed, } = useUserPermissions(); // 使用useSidebar钩子获取刷新方法 const { refreshUserConfig } = useSidebar(); // 左侧边栏设置处理函数 const handleSectionChange = (sectionKey) => { return (checked) => { const newModules = { ...sidebarModulesUser, [sectionKey]: { ...sidebarModulesUser[sectionKey], enabled: checked, }, }; setSidebarModulesUser(newModules); }; }; const handleModuleChange = (sectionKey, moduleKey) => { return (checked) => { const newModules = { ...sidebarModulesUser, [sectionKey]: { ...sidebarModulesUser[sectionKey], [moduleKey]: checked, }, }; setSidebarModulesUser(newModules); }; }; const saveSidebarSettings = async () => { setSidebarLoading(true); try { const res = await API.put('/api/user/self', { sidebar_modules: JSON.stringify(sidebarModulesUser), }); if (res.data.success) { showSuccess(t('侧边栏设置保存成功')); // 刷新useSidebar钩子中的用户配置,实现实时更新 await refreshUserConfig(); } else { showError(res.data.message); } } catch (error) { showError(t('保存失败')); } setSidebarLoading(false); }; const resetSidebarModules = () => { const defaultConfig = { chat: { enabled: true, playground: true, chat: true }, console: { enabled: true, detail: true, token: true, log: true, midjourney: true, task: true, }, personal: { enabled: true, topup: true, personal: true }, admin: { enabled: true, channel: true, models: true, redemption: true, user: true, setting: true, }, }; setSidebarModulesUser(defaultConfig); }; // 加载左侧边栏配置 useEffect(() => { const loadSidebarConfigs = async () => { try { // 获取管理员全局配置 if (statusState?.status?.SidebarModulesAdmin) { const adminConf = JSON.parse(statusState.status.SidebarModulesAdmin); setAdminConfig(adminConf); } // 获取用户个人配置 const userRes = await API.get('/api/user/self'); if (userRes.data.success && userRes.data.data.sidebar_modules) { const userConf = JSON.parse(userRes.data.data.sidebar_modules); setSidebarModulesUser(userConf); } } catch (error) { console.error('加载边栏配置失败:', error); } }; loadSidebarConfigs(); }, [statusState]); // 初始化表单值 useEffect(() => { if (formApiRef.current && notificationSettings) { formApiRef.current.setValues(notificationSettings); } }, [notificationSettings]); // 处理表单字段变化 const handleFormChange = (field, value) => { handleNotificationSettingChange(field, value); }; // 检查功能是否被管理员允许 const isAllowedByAdmin = (sectionKey, moduleKey = null) => { if (!adminConfig) return true; if (moduleKey) { return ( adminConfig[sectionKey]?.enabled && adminConfig[sectionKey]?.[moduleKey] ); } else { return adminConfig[sectionKey]?.enabled; } }; // 区域配置数据(根据权限过滤) const sectionConfigs = [ { key: 'chat', title: t('聊天区域'), description: t('操练场和聊天功能'), modules: [ { key: 'playground', title: t('操练场'), description: t('AI模型测试环境'), }, { key: 'chat', title: t('聊天'), description: t('聊天会话管理') }, ], }, { key: 'console', title: t('控制台区域'), description: t('数据管理和日志查看'), modules: [ { key: 'detail', title: t('数据看板'), description: t('系统数据统计') }, { key: 'token', title: t('令牌管理'), description: t('API令牌管理') }, { key: 'log', title: t('使用日志'), description: t('API使用记录') }, { key: 'midjourney', title: t('绘图日志'), description: t('绘图任务记录'), }, { key: 'task', title: t('任务日志'), description: t('系统任务记录') }, ], }, { key: 'personal', title: t('个人中心区域'), description: t('用户个人功能'), modules: [ { key: 'topup', title: t('钱包管理'), description: t('余额充值管理') }, { key: 'personal', title: t('个人设置'), description: t('个人信息设置'), }, ], }, // 管理员区域:根据后端权限控制显示 { key: 'admin', title: t('管理员区域'), description: t('系统管理功能'), modules: [ { key: 'channel', title: t('渠道管理'), description: t('API渠道配置') }, { key: 'models', title: t('模型管理'), description: t('AI模型配置') }, { key: 'redemption', title: t('兑换码管理'), description: t('兑换码生成管理'), }, { key: 'user', title: t('用户管理'), description: t('用户账户管理') }, { key: 'setting', title: t('系统设置'), description: t('系统参数配置'), }, ], }, ] .filter((section) => { // 使用后端权限验证替代前端角色判断 return isSidebarSectionAllowed(section.key); }) .map((section) => ({ ...section, modules: section.modules.filter((module) => isSidebarModuleAllowed(section.key, module.key), ), })) .filter( (section) => // 过滤掉没有可用模块的区域 section.modules.length > 0 && isAllowedByAdmin(section.key), ); // 表单提交 const handleSubmit = () => { if (formApiRef.current) { formApiRef.current .validate() .then(() => { saveNotificationSettings(); }) .catch((errors) => { console.log('表单验证失败:', errors); Toast.error(t('请检查表单填写是否正确')); }); } else { saveNotificationSettings(); } }; return ( {activeTabKey === 'sidebar' ? ( // 边栏设置标签页的按钮 <> ) : ( // 其他标签页的通用保存按钮 )} } > {/* 卡片头部 */}
{t('其他设置')}
{t('通知、价格和隐私相关设置')}
(formApiRef.current = api)} initValues={notificationSettings} onSubmit={handleSubmit} > {() => ( setActiveTabKey(key)} > {/* 通知配置 Tab */} {t('通知配置')} } itemKey='notification' >
handleFormChange('warningType', value)} rules={[{ required: true, message: t('请选择通知方式') }]} > {t('邮件通知')} {t('Webhook通知')} {t('Bark通知')} {t('Gotify通知')} {t('额度预警阈值')}{' '} {renderQuotaWithPrompt( notificationSettings.warningThreshold, )} } placeholder={t('请输入预警额度')} data={[ { value: 100000, label: '0.2$' }, { value: 500000, label: '1$' }, { value: 1000000, label: '5$' }, { value: 5000000, label: '10$' }, ]} onChange={(val) => handleFormChange('warningThreshold', val)} prefix={} extraText={t( '当剩余额度低于此数值时,系统将通过选择的方式发送通知', )} style={{ width: '100%', maxWidth: '300px' }} rules={[ { required: true, message: t('请输入预警阈值') }, { validator: (rule, value) => { const numValue = Number(value); if (isNaN(numValue) || numValue <= 0) { return Promise.reject(t('预警阈值必须为正数')); } return Promise.resolve(); }, }, ]} /> {/* 邮件通知设置 */} {notificationSettings.warningType === 'email' && ( handleFormChange('notificationEmail', val) } prefix={} extraText={t( '设置用于接收额度预警的邮箱地址,不填则使用账号绑定的邮箱', )} showClear /> )} {/* Webhook通知设置 */} {notificationSettings.warningType === 'webhook' && ( <> handleFormChange('webhookUrl', val)} prefix={} extraText={t( '只支持HTTPS,系统将以POST方式发送通知,请确保地址可以接收POST请求', )} showClear rules={[ { required: notificationSettings.warningType === 'webhook', message: t('请输入Webhook地址'), }, { pattern: /^https:\/\/.+/, message: t('Webhook地址必须以https://开头'), }, ]} /> handleFormChange('webhookSecret', val)} prefix={} extraText={t( '密钥将以Bearer方式添加到请求头中,用于验证webhook请求的合法性', )} showClear />
type:{' '} {t('通知类型 (quota_exceed: 额度预警)')}{' '}
title: {t('通知标题')}
content:{' '} {t('通知内容,支持 {{value}} 变量占位符')}
values:{' '} {t('按顺序替换content中的变量占位符')}
timestamp: {t('Unix时间戳')}
)} {/* Bark推送设置 */} {notificationSettings.warningType === 'bark' && ( <> handleFormChange('barkUrl', val)} prefix={} extraText={t( '支持HTTP和HTTPS,模板变量: {{title}} (通知标题), {{content}} (通知内容)', )} showClear rules={[ { required: notificationSettings.warningType === 'bark', message: t('请输入Bark推送URL'), }, { pattern: /^https?:\/\/.+/, message: t('Bark推送URL必须以http://或https://开头'), }, ]} />
{t('模板示例')}
https://api.day.app/yourkey/{'{{title}}'}/ {'{{content}}'}?sound=alarm&group=quota
{'title'}: {t('通知标题')}
{'content'}: {t('通知内容')}
{t('更多参数请参考')} {' '} Bark {t('官方文档')}
)} {/* Gotify推送设置 */} {notificationSettings.warningType === 'gotify' && ( <> handleFormChange('gotifyUrl', val)} prefix={} extraText={t( '支持HTTP和HTTPS,填写Gotify服务器的完整URL地址', )} showClear rules={[ { required: notificationSettings.warningType === 'gotify', message: t('请输入Gotify服务器地址'), }, { pattern: /^https?:\/\/.+/, message: t( 'Gotify服务器地址必须以http://或https://开头', ), }, ]} /> handleFormChange('gotifyToken', val)} prefix={} extraText={t( '在Gotify服务器创建应用后获得的令牌,用于发送通知', )} showClear rules={[ { required: notificationSettings.warningType === 'gotify', message: t('请输入Gotify应用令牌'), }, ]} /> handleFormChange('gotifyPriority', val) } prefix={} extraText={t('消息优先级,范围0-10,默认为5')} style={{ width: '100%', maxWidth: '300px' }} />
{t('配置说明')}
1. {t('在Gotify服务器的应用管理中创建新应用')}
2.{' '} {t( '复制应用的令牌(Token)并填写到上方的应用令牌字段', )}
3. {t('填写Gotify服务器的完整URL地址')}
{t('更多信息请参考')} {' '} Gotify {t('官方文档')}
)}
{/* 价格设置 Tab */} {t('价格设置')} } itemKey='pricing' >
handleFormChange('acceptUnsetModelRatioModel', value) } extraText={t( '当模型没有设置价格时仍接受调用,仅当您信任该网站时使用,可能会产生高额费用', )} />
{/* 隐私设置 Tab */} {t('隐私设置')} } itemKey='privacy' >
handleFormChange('recordIpLog', value)} extraText={t( '开启后,仅"消费"和"错误"日志将记录您的客户端IP地址', )} />
{/* 左侧边栏设置 Tab - 根据后端权限控制显示 */} {hasSidebarSettingsPermission() && ( {t('边栏设置')} } itemKey='sidebar' >
{t('您可以个性化设置侧边栏的要显示功能')}
{/* 边栏设置功能区域容器 */}
{sectionConfigs.map((section) => (
{/* 区域标题和总开关 */}
{section.title}
{section.description}
{/* 功能模块网格 */} {section.modules .filter((module) => isAllowedByAdmin(section.key, module.key), ) .map((module) => (
{module.title}
{module.description}
))}
))}
{' '} {/* 关闭边栏设置功能区域容器 */}
)}
)}
); }; export default NotificationSettings;