Merge branch 'alpha' into imageratio-and-audioratio-edit

This commit is contained in:
creamlike1024
2025-09-15 14:12:24 +08:00
139 changed files with 7093 additions and 1483 deletions

View File

@@ -34,7 +34,6 @@ import {
Tag,
} from '@douyinfe/semi-ui';
import { IconSearch } from '@douyinfe/semi-icons';
import { CheckCircle, XCircle, AlertCircle, HelpCircle } from 'lucide-react';
const ChannelSelectorModal = forwardRef(
(
@@ -65,6 +64,18 @@ const ChannelSelectorModal = forwardRef(
},
}));
// 官方渠道识别
const isOfficialChannel = (record) => {
const id = record?.key ?? record?.value ?? record?._originalData?.id;
const base = record?._originalData?.base_url || '';
const name = record?.label || '';
return (
id === -100 ||
base === 'https://basellm.github.io' ||
name === '官方倍率预设'
);
};
useEffect(() => {
if (!allChannels) return;
@@ -77,7 +88,13 @@ const ChannelSelectorModal = forwardRef(
})
: allChannels;
setFilteredData(matched);
const sorted = [...matched].sort((a, b) => {
const wa = isOfficialChannel(a) ? 0 : 1;
const wb = isOfficialChannel(b) ? 0 : 1;
return wa - wb;
});
setFilteredData(sorted);
}, [allChannels, searchText]);
const total = filteredData.length;
@@ -143,45 +160,49 @@ const ChannelSelectorModal = forwardRef(
);
};
const renderStatusCell = (status) => {
const renderStatusCell = (record) => {
const status = record?._originalData?.status || 0;
const official = isOfficialChannel(record);
let statusTag = null;
switch (status) {
case 1:
return (
<Tag
color='green'
shape='circle'
prefixIcon={<CheckCircle size={14} />}
>
statusTag = (
<Tag color='green' shape='circle'>
{t('已启用')}
</Tag>
);
break;
case 2:
return (
<Tag color='red' shape='circle' prefixIcon={<XCircle size={14} />}>
statusTag = (
<Tag color='red' shape='circle'>
{t('已禁用')}
</Tag>
);
break;
case 3:
return (
<Tag
color='yellow'
shape='circle'
prefixIcon={<AlertCircle size={14} />}
>
statusTag = (
<Tag color='yellow' shape='circle'>
{t('自动禁用')}
</Tag>
);
break;
default:
return (
<Tag
color='grey'
shape='circle'
prefixIcon={<HelpCircle size={14} />}
>
statusTag = (
<Tag color='grey' shape='circle'>
{t('未知状态')}
</Tag>
);
}
return (
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
{statusTag}
{official && (
<Tag color='green' shape='circle' type='light'>
{t('官方')}
</Tag>
)}
</div>
);
};
const renderNameCell = (text) => (
@@ -207,8 +228,7 @@ const ChannelSelectorModal = forwardRef(
{
title: t('状态'),
dataIndex: '_originalData.status',
render: (_, record) =>
renderStatusCell(record._originalData?.status || 0),
render: (_, record) => renderStatusCell(record),
},
{
title: t('同步接口'),

View File

@@ -20,6 +20,8 @@ For commercial licensing, please contact support@quantumnous.com
import React, { useEffect, useState } from 'react';
import { Card, Spin } from '@douyinfe/semi-ui';
import SettingsGeneral from '../../pages/Setting/Operation/SettingsGeneral';
import SettingsHeaderNavModules from '../../pages/Setting/Operation/SettingsHeaderNavModules';
import SettingsSidebarModulesAdmin from '../../pages/Setting/Operation/SettingsSidebarModulesAdmin';
import SettingsSensitiveWords from '../../pages/Setting/Operation/SettingsSensitiveWords';
import SettingsLog from '../../pages/Setting/Operation/SettingsLog';
import SettingsMonitoring from '../../pages/Setting/Operation/SettingsMonitoring';
@@ -46,6 +48,12 @@ const OperationSetting = () => {
DemoSiteEnabled: false,
SelfUseModeEnabled: false,
/* 顶栏模块管理 */
HeaderNavModules: '',
/* 左侧边栏模块管理(管理员) */
SidebarModulesAdmin: '',
/* 敏感词设置 */
CheckSensitiveEnabled: false,
CheckSensitiveOnPromptEnabled: false,
@@ -60,6 +68,8 @@ const OperationSetting = () => {
AutomaticDisableChannelEnabled: false,
AutomaticEnableChannelEnabled: false,
AutomaticDisableKeywords: '',
'monitor_setting.auto_test_channel_enabled': false,
'monitor_setting.auto_test_channel_minutes': 10,
});
let [loading, setLoading] = useState(false);
@@ -70,10 +80,7 @@ const OperationSetting = () => {
if (success) {
let newInputs = {};
data.forEach((item) => {
if (
item.key.endsWith('Enabled') ||
['DefaultCollapseSidebar'].includes(item.key)
) {
if (typeof inputs[item.key] === 'boolean') {
newInputs[item.key] = toBoolean(item.value);
} else {
newInputs[item.key] = item.value;
@@ -108,6 +115,14 @@ const OperationSetting = () => {
<Card style={{ marginTop: '10px' }}>
<SettingsGeneral options={inputs} refresh={onRefresh} />
</Card>
{/* 顶栏模块管理 */}
<div style={{ marginTop: '10px' }}>
<SettingsHeaderNavModules options={inputs} refresh={onRefresh} />
</div>
{/* 左侧边栏模块管理(管理员) */}
<div style={{ marginTop: '10px' }}>
<SettingsSidebarModulesAdmin options={inputs} refresh={onRefresh} />
</div>
{/* 屏蔽词过滤设置 */}
<Card style={{ marginTop: '10px' }}>
<SettingsSensitiveWords options={inputs} refresh={onRefresh} />

View File

@@ -37,6 +37,8 @@ const PaymentSetting = () => {
TopupGroupRatio: '',
CustomCallbackAddress: '',
PayMethods: '',
AmountOptions: '',
AmountDiscount: '',
StripeApiSecret: '',
StripeWebhookSecret: '',
@@ -66,6 +68,30 @@ const PaymentSetting = () => {
newInputs[item.key] = item.value;
}
break;
case 'payment_setting.amount_options':
try {
newInputs['AmountOptions'] = JSON.stringify(
JSON.parse(item.value),
null,
2,
);
} catch (error) {
console.error('解析AmountOptions出错:', error);
newInputs['AmountOptions'] = item.value;
}
break;
case 'payment_setting.amount_discount':
try {
newInputs['AmountDiscount'] = JSON.stringify(
JSON.parse(item.value),
null,
2,
);
} catch (error) {
console.error('解析AmountDiscount出错:', error);
newInputs['AmountDiscount'] = item.value;
}
break;
case 'Price':
case 'MinTopUp':
case 'StripeUnitPrice':

View File

@@ -65,6 +65,7 @@ const PersonalSetting = () => {
webhookUrl: '',
webhookSecret: '',
notificationEmail: '',
barkUrl: '',
acceptUnsetModelRatioModel: false,
recordIpLog: false,
});
@@ -106,6 +107,7 @@ const PersonalSetting = () => {
webhookUrl: settings.webhook_url || '',
webhookSecret: settings.webhook_secret || '',
notificationEmail: settings.notification_email || '',
barkUrl: settings.bark_url || '',
acceptUnsetModelRatioModel:
settings.accept_unset_model_ratio_model || false,
recordIpLog: settings.record_ip_log || false,
@@ -283,6 +285,7 @@ const PersonalSetting = () => {
webhook_url: notificationSettings.webhookUrl,
webhook_secret: notificationSettings.webhookSecret,
notification_email: notificationSettings.notificationEmail,
bark_url: notificationSettings.barkUrl,
accept_unset_model_ratio_model:
notificationSettings.acceptUnsetModelRatioModel,
record_ip_log: notificationSettings.recordIpLog,

View File

@@ -17,7 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import React, { useRef, useEffect } from 'react';
import React, { useRef, useEffect, useState, useContext } from 'react';
import {
Button,
Typography,
@@ -28,11 +28,22 @@ import {
Toast,
Tabs,
TabPane,
Switch,
Row,
Col,
} from '@douyinfe/semi-ui';
import { IconMail, IconKey, IconBell, IconLink } from '@douyinfe/semi-icons';
import { ShieldCheck, Bell, DollarSign } from 'lucide-react';
import { renderQuotaWithPrompt } from '../../../../helpers';
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';
const NotificationSettings = ({
t,
@@ -41,6 +52,142 @@ const NotificationSettings = ({
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();
// 左侧边栏设置处理函数
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('侧边栏设置保存成功'));
} 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(() => {
@@ -54,6 +201,101 @@ const NotificationSettings = ({
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) {
@@ -75,10 +317,32 @@ const NotificationSettings = ({
<Card
className='!rounded-2xl shadow-sm border-0'
footer={
<div className='flex justify-end'>
<Button type='primary' onClick={handleSubmit}>
{t('保存设置')}
</Button>
<div className='flex justify-end gap-3'>
{activeTabKey === 'sidebar' ? (
// 边栏设置标签页的按钮
<>
<Button
type='tertiary'
onClick={resetSidebarModules}
className='!rounded-lg'
>
{t('重置为默认')}
</Button>
<Button
type='primary'
onClick={saveSidebarSettings}
loading={sidebarLoading}
className='!rounded-lg'
>
{t('保存边栏设置')}
</Button>
</>
) : (
// 其他标签页的通用保存按钮
<Button type='primary' onClick={handleSubmit}>
{t('保存设置')}
</Button>
)}
</div>
}
>
@@ -103,7 +367,11 @@ const NotificationSettings = ({
onSubmit={handleSubmit}
>
{() => (
<Tabs type='card' defaultActiveKey='notification'>
<Tabs
type='card'
defaultActiveKey='notification'
onChange={(key) => setActiveTabKey(key)}
>
{/* 通知配置 Tab */}
<TabPane
tab={
@@ -124,6 +392,7 @@ const NotificationSettings = ({
>
<Radio value='email'>{t('邮件通知')}</Radio>
<Radio value='webhook'>{t('Webhook通知')}</Radio>
<Radio value='bark'>{t('Bark通知')}</Radio>
</Form.RadioGroup>
<Form.AutoComplete
@@ -260,6 +529,66 @@ const NotificationSettings = ({
</Form.Slot>
</>
)}
{/* Bark推送设置 */}
{notificationSettings.warningType === 'bark' && (
<>
<Form.Input
field='barkUrl'
label={t('Bark推送URL')}
placeholder={t(
'请输入Bark推送URL例如: https://api.day.app/yourkey/{{title}}/{{content}}',
)}
onChange={(val) => handleFormChange('barkUrl', val)}
prefix={<IconLink />}
extraText={t(
'支持HTTP和HTTPS模板变量: {{title}} (通知标题), {{content}} (通知内容)',
)}
showClear
rules={[
{
required: notificationSettings.warningType === 'bark',
message: t('请输入Bark推送URL'),
},
{
pattern: /^https?:\/\/.+/,
message: t('Bark推送URL必须以http://或https://开头'),
},
]}
/>
<div className='mt-3 p-4 bg-gray-50/50 rounded-xl'>
<div className='text-sm text-gray-700 mb-3'>
<strong>{t('模板示例')}</strong>
</div>
<div className='text-xs text-gray-600 font-mono bg-white p-3 rounded-lg shadow-sm mb-4'>
https://api.day.app/yourkey/{'{{title}}'}/
{'{{content}}'}?sound=alarm&group=quota
</div>
<div className='text-xs text-gray-500 space-y-2'>
<div>
<strong>{'title'}:</strong> {t('通知标题')}
</div>
<div>
<strong>{'content'}:</strong> {t('通知内容')}
</div>
<div className='mt-3 pt-3 border-t border-gray-200'>
<span className='text-gray-400'>
{t('更多参数请参考')}
</span>{' '}
<a
href='https://github.com/Finb/Bark'
target='_blank'
rel='noopener noreferrer'
className='text-blue-500 hover:text-blue-600 font-medium'
>
Bark 官方文档
</a>
</div>
</div>
</div>
</>
)}
</div>
</TabPane>
@@ -312,6 +641,147 @@ const NotificationSettings = ({
/>
</div>
</TabPane>
{/* 左侧边栏设置 Tab - 根据后端权限控制显示 */}
{hasSidebarSettingsPermission() && (
<TabPane
tab={
<div className='flex items-center'>
<Settings size={16} className='mr-2' />
{t('边栏设置')}
</div>
}
itemKey='sidebar'
>
<div className='py-4'>
<div className='mb-4'>
<Typography.Text
type='secondary'
size='small'
style={{
fontSize: '12px',
lineHeight: '1.5',
color: 'var(--semi-color-text-2)',
}}
>
{t('您可以个性化设置侧边栏的要显示功能')}
</Typography.Text>
</div>
{/* 边栏设置功能区域容器 */}
<div
className='border rounded-xl p-4'
style={{
borderColor: 'var(--semi-color-border)',
backgroundColor: 'var(--semi-color-bg-1)',
}}
>
{sectionConfigs.map((section) => (
<div key={section.key} className='mb-6'>
{/* 区域标题和总开关 */}
<div
className='flex justify-between items-center mb-4 p-4 rounded-lg'
style={{
backgroundColor: 'var(--semi-color-fill-0)',
border: '1px solid var(--semi-color-border-light)',
borderColor: 'var(--semi-color-fill-1)',
}}
>
<div>
<div className='font-semibold text-base text-gray-900 mb-1'>
{section.title}
</div>
<Typography.Text
type='secondary'
size='small'
style={{
fontSize: '12px',
lineHeight: '1.5',
color: 'var(--semi-color-text-2)',
}}
>
{section.description}
</Typography.Text>
</div>
<Switch
checked={sidebarModulesUser[section.key]?.enabled}
onChange={handleSectionChange(section.key)}
size='default'
/>
</div>
{/* 功能模块网格 */}
<Row gutter={[12, 12]}>
{section.modules
.filter((module) =>
isAllowedByAdmin(section.key, module.key),
)
.map((module) => (
<Col
key={module.key}
xs={24}
sm={24}
md={12}
lg={8}
xl={8}
>
<Card
className={`!rounded-xl border border-gray-200 hover:border-blue-300 transition-all duration-200 ${
sidebarModulesUser[section.key]?.enabled
? ''
: 'opacity-50'
}`}
bodyStyle={{ padding: '16px' }}
hoverable
>
<div className='flex justify-between items-center h-full'>
<div className='flex-1 text-left'>
<div className='font-semibold text-sm text-gray-900 mb-1'>
{module.title}
</div>
<Typography.Text
type='secondary'
size='small'
className='block'
style={{
fontSize: '12px',
lineHeight: '1.5',
color: 'var(--semi-color-text-2)',
marginTop: '4px',
}}
>
{module.description}
</Typography.Text>
</div>
<div className='ml-4'>
<Switch
checked={
sidebarModulesUser[section.key]?.[
module.key
]
}
onChange={handleModuleChange(
section.key,
module.key,
)}
size='default'
disabled={
!sidebarModulesUser[section.key]
?.enabled
}
/>
</div>
</div>
</Card>
</Col>
))}
</Row>
</div>
))}
</div>{' '}
{/* 关闭边栏设置功能区域容器 */}
</div>
</TabPane>
)}
</Tabs>
)}
</Form>