feat: optimize admin settings page
This commit is contained in:
+178
-37
@@ -574,6 +574,70 @@ export default function Settings() {
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: 10,
|
gap: 10,
|
||||||
};
|
};
|
||||||
|
const settingsModernSectionStyle: React.CSSProperties = {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 16,
|
||||||
|
scrollMarginTop: 96,
|
||||||
|
};
|
||||||
|
const settingsSummaryGridStyle: React.CSSProperties = {
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: isMobile ? '1fr' : 'repeat(4, minmax(0, 1fr))',
|
||||||
|
gap: 12,
|
||||||
|
maxWidth: 1120,
|
||||||
|
marginBottom: 16,
|
||||||
|
};
|
||||||
|
const settingsSummaryCardStyle: React.CSSProperties = {
|
||||||
|
display: 'grid',
|
||||||
|
gap: 8,
|
||||||
|
padding: '14px 16px',
|
||||||
|
borderRadius: 'var(--radius-md)',
|
||||||
|
border: '1px solid var(--color-border-light)',
|
||||||
|
background: 'color-mix(in srgb, var(--color-bg-card) 92%, var(--color-bg))',
|
||||||
|
boxShadow: 'var(--shadow-card)',
|
||||||
|
minWidth: 0,
|
||||||
|
};
|
||||||
|
const settingsShellStyle: React.CSSProperties = {
|
||||||
|
maxWidth: 1120,
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: isMobile ? '1fr' : '220px minmax(0, 1fr)',
|
||||||
|
alignItems: 'start',
|
||||||
|
gap: isMobile ? 14 : 18,
|
||||||
|
};
|
||||||
|
const settingsNavStyle: React.CSSProperties = {
|
||||||
|
display: 'grid',
|
||||||
|
gap: 6,
|
||||||
|
padding: 12,
|
||||||
|
borderRadius: 'var(--radius-md)',
|
||||||
|
border: '1px solid var(--color-border-light)',
|
||||||
|
background: 'color-mix(in srgb, var(--color-bg-card) 94%, var(--color-bg))',
|
||||||
|
boxShadow: 'var(--shadow-card)',
|
||||||
|
position: isMobile ? 'static' : 'sticky',
|
||||||
|
top: 88,
|
||||||
|
maxHeight: isMobile ? undefined : 'calc(100dvh - 112px)',
|
||||||
|
overflowY: 'auto',
|
||||||
|
};
|
||||||
|
const settingsNavLinkStyle: React.CSSProperties = {
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 10,
|
||||||
|
padding: '8px 10px',
|
||||||
|
borderRadius: 'var(--radius-sm)',
|
||||||
|
color: 'var(--color-text-secondary)',
|
||||||
|
textDecoration: 'none',
|
||||||
|
fontSize: 13,
|
||||||
|
lineHeight: 1.35,
|
||||||
|
};
|
||||||
|
const settingsContentStyle: React.CSSProperties = {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 16,
|
||||||
|
minWidth: 0,
|
||||||
|
};
|
||||||
|
const settingsAnchorCardStyle: React.CSSProperties = {
|
||||||
|
scrollMarginTop: 96,
|
||||||
|
};
|
||||||
|
|
||||||
const getSettingsPillStyle = (tone: SettingsPillTone): React.CSSProperties => {
|
const getSettingsPillStyle = (tone: SettingsPillTone): React.CSSProperties => {
|
||||||
const toneStyles: Record<SettingsPillTone, React.CSSProperties> = {
|
const toneStyles: Record<SettingsPillTone, React.CSSProperties> = {
|
||||||
@@ -627,6 +691,36 @@ export default function Settings() {
|
|||||||
: savedModelAvailabilityProbeEnabled
|
: savedModelAvailabilityProbeEnabled
|
||||||
? '已启用'
|
? '已启用'
|
||||||
: '已关闭';
|
: '已关闭';
|
||||||
|
const activeRoutingProfileLabel = activeRoutingProfile === 'stable'
|
||||||
|
? '稳定优先'
|
||||||
|
: activeRoutingProfile === 'cost'
|
||||||
|
? '成本优先'
|
||||||
|
: '均衡';
|
||||||
|
const runtimeDatabaseSummary = runtimeDatabaseState
|
||||||
|
? `${runtimeDatabaseState.active.dialect}${runtimeDatabaseState.restartRequired ? ' · 待重启' : ''}`
|
||||||
|
: '加载中';
|
||||||
|
const systemProxySummary = runtime.systemProxyUrl.trim() ? '已配置' : '未配置';
|
||||||
|
const securitySummary = runtime.adminIpAllowlist?.length
|
||||||
|
? `${runtime.adminIpAllowlist.length} 条白名单`
|
||||||
|
: '未限制 IP';
|
||||||
|
const modelGovernanceSummary = [
|
||||||
|
blockedBrands.length > 0 ? `${blockedBrands.length} 个品牌屏蔽` : '',
|
||||||
|
allowedModels.length > 0 ? `${allowedModels.length} 个模型白名单` : '',
|
||||||
|
].filter(Boolean).join(' / ') || '全部模型开放';
|
||||||
|
const settingsSections = [
|
||||||
|
{ id: 'settings-access', label: '管理员令牌', badge: maskedToken ? '已配置' : '检查' },
|
||||||
|
{ id: 'settings-schedule', label: '定时任务', badge: runtime.checkinScheduleMode === 'interval' ? `${runtime.checkinIntervalHours}h` : 'Cron' },
|
||||||
|
{ id: 'settings-proxy', label: '代理与失败判定', badge: systemProxySummary },
|
||||||
|
{ id: 'settings-payload', label: 'Payload 规则', badge: configuredPayloadRuleCount > 0 ? `${configuredPayloadRuleCount}` : '0' },
|
||||||
|
{ id: 'settings-transport', label: '上游传输', badge: runtime.codexUpstreamWebsocketEnabled ? 'WS' : 'HTTP' },
|
||||||
|
{ id: 'settings-probe', label: '批量测活', badge: modelAvailabilityProbeStatusLabel },
|
||||||
|
{ id: 'settings-downstream-token', label: '下游令牌', badge: runtime.proxyTokenMasked ? '已配置' : '未设置' },
|
||||||
|
{ id: 'settings-routing', label: '路由策略', badge: activeRoutingProfileLabel },
|
||||||
|
{ id: 'settings-governance', label: '模型治理', badge: blockedBrands.length + allowedModels.length > 0 ? '已配置' : '开放' },
|
||||||
|
{ id: 'settings-database', label: '数据库', badge: runtimeDatabaseSummary },
|
||||||
|
{ id: 'settings-maintenance', label: '维护与危险操作', badge: '谨慎' },
|
||||||
|
{ id: 'settings-security', label: '会话与安全', badge: securitySummary },
|
||||||
|
];
|
||||||
|
|
||||||
const syncPayloadRuleDraftsFromObject = (value: unknown) => {
|
const syncPayloadRuleDraftsFromObject = (value: unknown) => {
|
||||||
setPayloadRuleDrafts(normalizePayloadRulesForEditor(value));
|
setPayloadRuleDrafts(normalizePayloadRulesForEditor(value));
|
||||||
@@ -824,7 +918,7 @@ export default function Settings() {
|
|||||||
const res = await api.updateRuntimeSettings({ proxyToken: `${PROXY_TOKEN_PREFIX}${suffix}` });
|
const res = await api.updateRuntimeSettings({ proxyToken: `${PROXY_TOKEN_PREFIX}${suffix}` });
|
||||||
setRuntime((prev) => ({ ...prev, proxyTokenMasked: res.proxyTokenMasked || prev.proxyTokenMasked }));
|
setRuntime((prev) => ({ ...prev, proxyTokenMasked: res.proxyTokenMasked || prev.proxyTokenMasked }));
|
||||||
setProxyTokenSuffix('');
|
setProxyTokenSuffix('');
|
||||||
toast.success('Proxy token updated');
|
toast.success('下游访问令牌已更新');
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
toast.error(err?.message || '保存失败');
|
toast.error(err?.message || '保存失败');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -1070,7 +1164,7 @@ export default function Settings() {
|
|||||||
),
|
),
|
||||||
disableCrossProtocolFallback: runtime.disableCrossProtocolFallback,
|
disableCrossProtocolFallback: runtime.disableCrossProtocolFallback,
|
||||||
});
|
});
|
||||||
toast.success('Routing weights saved');
|
toast.success('路由策略已保存');
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
toast.error(err?.message || '保存失败');
|
toast.error(err?.message || '保存失败');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -1144,7 +1238,7 @@ export default function Settings() {
|
|||||||
? res.currentAdminIp
|
? res.currentAdminIp
|
||||||
: prev.currentAdminIp,
|
: prev.currentAdminIp,
|
||||||
}));
|
}));
|
||||||
toast.success('Security settings saved');
|
toast.success('安全设置已保存');
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
toast.error(err?.message || '保存失败');
|
toast.error(err?.message || '保存失败');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -1209,7 +1303,7 @@ export default function Settings() {
|
|||||||
|
|
||||||
const handleTestExternalDatabaseConnection = async () => {
|
const handleTestExternalDatabaseConnection = async () => {
|
||||||
if (!effectiveMigrationConnectionString) {
|
if (!effectiveMigrationConnectionString) {
|
||||||
toast.info('Please fill target database connection first');
|
toast.info('请先填写目标数据库连接');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1226,9 +1320,9 @@ export default function Settings() {
|
|||||||
connectionString: effectiveMigrationConnectionString,
|
connectionString: effectiveMigrationConnectionString,
|
||||||
ssl: migrationSsl,
|
ssl: migrationSsl,
|
||||||
});
|
});
|
||||||
toast.success(`Connection success: ${res.connection || migrationDialect}`);
|
toast.success(`连接测试成功:${res.connection || migrationDialect}`);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
toast.error(err?.message || 'Target database connection failed');
|
toast.error(err?.message || '目标数据库连接失败');
|
||||||
} finally {
|
} finally {
|
||||||
setTestingMigrationConnection(false);
|
setTestingMigrationConnection(false);
|
||||||
}
|
}
|
||||||
@@ -1236,7 +1330,7 @@ export default function Settings() {
|
|||||||
|
|
||||||
const handleMigrateToExternalDatabase = async () => {
|
const handleMigrateToExternalDatabase = async () => {
|
||||||
if (!effectiveMigrationConnectionString) {
|
if (!effectiveMigrationConnectionString) {
|
||||||
toast.info('Please fill target database connection first');
|
toast.info('请先填写目标数据库连接');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1247,8 +1341,8 @@ export default function Settings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const warning = migrationOverwrite
|
const warning = migrationOverwrite
|
||||||
? 'Confirm migration and overwrite existing data in target database?'
|
? '确认迁移并覆盖目标数据库现有数据?'
|
||||||
: 'Confirm migration to target database? If target has data, migration may fail.';
|
: '确认迁移到目标数据库?如果目标库已有数据,迁移可能失败。';
|
||||||
if (!window.confirm(warning)) return;
|
if (!window.confirm(warning)) return;
|
||||||
|
|
||||||
setMigratingDatabase(true);
|
setMigratingDatabase(true);
|
||||||
@@ -1260,9 +1354,9 @@ export default function Settings() {
|
|||||||
ssl: migrationSsl,
|
ssl: migrationSsl,
|
||||||
});
|
});
|
||||||
setMigrationSummary(res);
|
setMigrationSummary(res);
|
||||||
toast.success(res?.message || 'Database migration completed');
|
toast.success(res?.message || '数据库迁移已完成');
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
toast.error(err?.message || 'Database migration failed');
|
toast.error(err?.message || '数据库迁移失败');
|
||||||
} finally {
|
} finally {
|
||||||
setMigratingDatabase(false);
|
setMigratingDatabase(false);
|
||||||
}
|
}
|
||||||
@@ -1270,7 +1364,7 @@ export default function Settings() {
|
|||||||
|
|
||||||
const handleSaveRuntimeDatabaseConfig = async () => {
|
const handleSaveRuntimeDatabaseConfig = async () => {
|
||||||
if (!effectiveMigrationConnectionString) {
|
if (!effectiveMigrationConnectionString) {
|
||||||
toast.info('Please fill target database connection first');
|
toast.info('请先填写目标数据库连接');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1302,9 +1396,9 @@ export default function Settings() {
|
|||||||
: null,
|
: null,
|
||||||
restartRequired: !!res?.restartRequired,
|
restartRequired: !!res?.restartRequired,
|
||||||
});
|
});
|
||||||
toast.success(res?.message || 'Runtime database config saved');
|
toast.success(res?.message || '运行数据库配置已保存');
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
toast.error(err?.message || 'Runtime database config save failed');
|
toast.error(err?.message || '运行数据库配置保存失败');
|
||||||
} finally {
|
} finally {
|
||||||
setSavingRuntimeDatabase(false);
|
setSavingRuntimeDatabase(false);
|
||||||
}
|
}
|
||||||
@@ -1322,11 +1416,54 @@ export default function Settings() {
|
|||||||
return (
|
return (
|
||||||
<div className="animate-fade-in">
|
<div className="animate-fade-in">
|
||||||
<div className="page-header">
|
<div className="page-header">
|
||||||
<h2 className="page-title">系统设置</h2>
|
<div>
|
||||||
|
<h2 className="page-title">系统设置</h2>
|
||||||
|
<div style={{ fontSize: 13, color: 'var(--color-text-muted)', lineHeight: 1.7, marginTop: 6 }}>
|
||||||
|
管理员运行台:集中配置访问令牌、代理、路由、模型治理、数据库和安全策略。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ maxWidth: 720, display: 'flex', flexDirection: 'column', gap: 16 }}>
|
<div style={settingsSummaryGridStyle}>
|
||||||
<div className="card animate-slide-up stagger-1" style={{ padding: 20 }}>
|
<div style={settingsSummaryCardStyle}>
|
||||||
|
<div style={settingsModernFieldLabelStyle}>运行数据库</div>
|
||||||
|
<div style={{ fontSize: 18, fontWeight: 700, color: 'var(--color-text-primary)' }}>{runtimeDatabaseSummary}</div>
|
||||||
|
<div style={settingsModernFieldHintStyle}>
|
||||||
|
{runtimeDatabaseState?.active.connection || '使用默认本地数据库'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={settingsSummaryCardStyle}>
|
||||||
|
<div style={settingsModernFieldLabelStyle}>上游传输</div>
|
||||||
|
<div style={{ fontSize: 18, fontWeight: 700, color: 'var(--color-text-primary)' }}>{proxyTransportModeLabel}</div>
|
||||||
|
<div style={settingsModernFieldHintStyle}>{proxyTransportQueueLabel}</div>
|
||||||
|
</div>
|
||||||
|
<div style={settingsSummaryCardStyle}>
|
||||||
|
<div style={settingsModernFieldLabelStyle}>路由策略</div>
|
||||||
|
<div style={{ fontSize: 18, fontWeight: 700, color: 'var(--color-text-primary)' }}>{activeRoutingProfileLabel}</div>
|
||||||
|
<div style={settingsModernFieldHintStyle}>失败冷却上限 {runtime.routeFailureCooldownMaxValue} {ROUTE_COOLDOWN_UNIT_OPTIONS.find((item) => item.value === runtime.routeFailureCooldownMaxUnit)?.label || ''}</div>
|
||||||
|
</div>
|
||||||
|
<div style={settingsSummaryCardStyle}>
|
||||||
|
<div style={settingsModernFieldLabelStyle}>模型治理</div>
|
||||||
|
<div style={{ fontSize: 18, fontWeight: 700, color: 'var(--color-text-primary)' }}>{modelGovernanceSummary}</div>
|
||||||
|
<div style={settingsModernFieldHintStyle}>批量测活:{modelAvailabilityProbeStatusLabel}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={settingsShellStyle}>
|
||||||
|
<aside style={settingsNavStyle} aria-label="设置分区导航">
|
||||||
|
<div style={{ fontSize: 12, fontWeight: 700, color: 'var(--color-text-muted)', padding: '2px 10px 6px' }}>
|
||||||
|
设置分区
|
||||||
|
</div>
|
||||||
|
{settingsSections.map((section) => (
|
||||||
|
<a key={section.id} href={`#${section.id}`} style={settingsNavLinkStyle}>
|
||||||
|
<span>{section.label}</span>
|
||||||
|
<span style={getSettingsPillStyle(section.badge === '谨慎' ? 'danger' : 'neutral')}>{section.badge}</span>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<div style={settingsContentStyle}>
|
||||||
|
<div id="settings-access" className="card animate-slide-up stagger-1" style={{ ...settingsModernCardStyle, ...settingsAnchorCardStyle }}>
|
||||||
<div style={{ fontWeight: 600, fontSize: 14, marginBottom: 12 }}>管理员登录令牌</div>
|
<div style={{ fontWeight: 600, fontSize: 14, marginBottom: 12 }}>管理员登录令牌</div>
|
||||||
<code style={{ display: 'block', padding: '10px 14px', background: 'var(--color-bg)', borderRadius: 'var(--radius-sm)', fontSize: 13, fontFamily: 'var(--font-mono)', color: 'var(--color-text-secondary)', border: '1px solid var(--color-border-light)', marginBottom: 12 }}>
|
<code style={{ display: 'block', padding: '10px 14px', background: 'var(--color-bg)', borderRadius: 'var(--radius-sm)', fontSize: 13, fontFamily: 'var(--font-mono)', color: 'var(--color-text-secondary)', border: '1px solid var(--color-border-light)', marginBottom: 12 }}>
|
||||||
{maskedToken || '****'}
|
{maskedToken || '****'}
|
||||||
@@ -1341,7 +1478,7 @@ export default function Settings() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="card animate-slide-up stagger-2" style={{ padding: 20 }}>
|
<div id="settings-schedule" className="card animate-slide-up stagger-2" style={{ ...settingsModernCardStyle, ...settingsAnchorCardStyle }}>
|
||||||
<div style={{ fontWeight: 600, fontSize: 14, marginBottom: 12 }}>定时任务</div>
|
<div style={{ fontWeight: 600, fontSize: 14, marginBottom: 12 }}>定时任务</div>
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '180px 180px auto', gap: 12, alignItems: 'end', marginBottom: 12 }}>
|
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '180px 180px auto', gap: 12, alignItems: 'end', marginBottom: 12 }}>
|
||||||
<div>
|
<div>
|
||||||
@@ -1462,7 +1599,7 @@ export default function Settings() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="card animate-slide-up stagger-3" style={{ padding: 20 }}>
|
<div id="settings-proxy" className="card animate-slide-up stagger-3" style={{ ...settingsModernCardStyle, ...settingsAnchorCardStyle }}>
|
||||||
<div style={{ fontWeight: 600, fontSize: 14, marginBottom: 8 }}>系统代理</div>
|
<div style={{ fontWeight: 600, fontSize: 14, marginBottom: 8 }}>系统代理</div>
|
||||||
<div style={{ fontSize: 12, color: 'var(--color-text-muted)', marginBottom: 12 }}>
|
<div style={{ fontSize: 12, color: 'var(--color-text-muted)', marginBottom: 12 }}>
|
||||||
配置一个全局出站代理地址,站点页可按站点决定是否启用系统代理。
|
配置一个全局出站代理地址,站点页可按站点决定是否启用系统代理。
|
||||||
@@ -1504,7 +1641,7 @@ export default function Settings() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="card animate-slide-up stagger-4" style={{ padding: 20 }}>
|
<div className="card animate-slide-up stagger-4" style={settingsModernCardStyle}>
|
||||||
<div style={{ fontWeight: 600, fontSize: 14, marginBottom: 8 }}>代理失败判定</div>
|
<div style={{ fontWeight: 600, fontSize: 14, marginBottom: 8 }}>代理失败判定</div>
|
||||||
<div style={{ fontSize: 12, color: 'var(--color-text-muted)', marginBottom: 12 }}>
|
<div style={{ fontSize: 12, color: 'var(--color-text-muted)', marginBottom: 12 }}>
|
||||||
命中任一关键词或空内容时判定失败,可触发重试。
|
命中任一关键词或空内容时判定失败,可触发重试。
|
||||||
@@ -1536,7 +1673,7 @@ export default function Settings() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="card animate-slide-up stagger-4" style={settingsModernCardStyle} data-settings-card="payload-rules">
|
<div id="settings-payload" className="card animate-slide-up stagger-4" style={{ ...settingsModernCardStyle, ...settingsAnchorCardStyle }} data-settings-card="payload-rules">
|
||||||
<div style={settingsModernHeaderStyle}>
|
<div style={settingsModernHeaderStyle}>
|
||||||
<div style={settingsModernTitleBlockStyle}>
|
<div style={settingsModernTitleBlockStyle}>
|
||||||
<div style={settingsModernTitleStyle}>Payload 规则</div>
|
<div style={settingsModernTitleStyle}>Payload 规则</div>
|
||||||
@@ -1794,7 +1931,7 @@ export default function Settings() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="card animate-slide-up stagger-4" style={settingsModernCardStyle} data-settings-card="proxy-transport">
|
<div id="settings-transport" className="card animate-slide-up stagger-4" style={{ ...settingsModernCardStyle, ...settingsAnchorCardStyle }} data-settings-card="proxy-transport">
|
||||||
<div style={settingsModernHeaderStyle}>
|
<div style={settingsModernHeaderStyle}>
|
||||||
<div style={settingsModernTitleBlockStyle}>
|
<div style={settingsModernTitleBlockStyle}>
|
||||||
<div style={settingsModernTitleStyle}>Codex 上游传输与会话并发</div>
|
<div style={settingsModernTitleStyle}>Codex 上游传输与会话并发</div>
|
||||||
@@ -1891,7 +2028,7 @@ export default function Settings() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="card animate-slide-up stagger-4" style={settingsModernDangerCardStyle} data-settings-card="model-availability-probe">
|
<div id="settings-probe" className="card animate-slide-up stagger-4" style={{ ...settingsModernDangerCardStyle, ...settingsAnchorCardStyle }} data-settings-card="model-availability-probe">
|
||||||
<div style={settingsModernHeaderStyle}>
|
<div style={settingsModernHeaderStyle}>
|
||||||
<div style={settingsModernTitleBlockStyle}>
|
<div style={settingsModernTitleBlockStyle}>
|
||||||
<div style={{ ...settingsModernTitleStyle, color: 'var(--color-danger)' }}>批量测活</div>
|
<div style={{ ...settingsModernTitleStyle, color: 'var(--color-danger)' }}>批量测活</div>
|
||||||
@@ -1962,7 +2099,7 @@ export default function Settings() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="card animate-slide-up stagger-4" style={{ padding: 20 }}>
|
<div id="settings-downstream-token" className="card animate-slide-up stagger-4" style={{ ...settingsModernCardStyle, ...settingsAnchorCardStyle }}>
|
||||||
<div style={{ fontWeight: 600, fontSize: 14, marginBottom: 8 }}>下游访问令牌(PROXY_TOKEN)</div>
|
<div style={{ fontWeight: 600, fontSize: 14, marginBottom: 8 }}>下游访问令牌(PROXY_TOKEN)</div>
|
||||||
<div style={{ fontSize: 12, color: 'var(--color-text-muted)', marginBottom: 12 }}>
|
<div style={{ fontSize: 12, color: 'var(--color-text-muted)', marginBottom: 12 }}>
|
||||||
用于下游站点或客户端访问本服务代理接口。前缀 sk- 固定不可修改,只需填写后缀。
|
用于下游站点或客户端访问本服务代理接口。前缀 sk- 固定不可修改,只需填写后缀。
|
||||||
@@ -2055,7 +2192,7 @@ export default function Settings() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="card animate-slide-up stagger-5" style={{ padding: 20 }}>
|
<div id="settings-routing" className="card animate-slide-up stagger-5" style={{ ...settingsModernCardStyle, ...settingsAnchorCardStyle }}>
|
||||||
<div style={{ fontWeight: 600, fontSize: 14, marginBottom: 10 }}>路由策略</div>
|
<div style={{ fontWeight: 600, fontSize: 14, marginBottom: 10 }}>路由策略</div>
|
||||||
<div style={{ fontSize: 12, color: 'var(--color-text-muted)', marginBottom: 12 }}>
|
<div style={{ fontSize: 12, color: 'var(--color-text-muted)', marginBottom: 12 }}>
|
||||||
先选择预设策略,只有需要精调时再展开高级参数。
|
先选择预设策略,只有需要精调时再展开高级参数。
|
||||||
@@ -2252,7 +2389,7 @@ export default function Settings() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Global Brand Filter */}
|
{/* Global Brand Filter */}
|
||||||
<div className="card animate-slide-up stagger-6" style={{ padding: 20 }}>
|
<div id="settings-governance" className="card animate-slide-up stagger-6" style={{ ...settingsModernCardStyle, ...settingsAnchorCardStyle }}>
|
||||||
<div style={{ fontWeight: 600, fontSize: 14, marginBottom: 8 }}>全局品牌屏蔽</div>
|
<div style={{ fontWeight: 600, fontSize: 14, marginBottom: 8 }}>全局品牌屏蔽</div>
|
||||||
<div style={{ fontSize: 12, color: 'var(--color-text-muted)', marginBottom: 12, lineHeight: 1.6 }}>
|
<div style={{ fontSize: 12, color: 'var(--color-text-muted)', marginBottom: 12, lineHeight: 1.6 }}>
|
||||||
屏蔽选定品牌后,路由重建时将自动跳过匹配该品牌的所有模型。点击品牌切换屏蔽状态,保存后自动触发路由重建。
|
屏蔽选定品牌后,路由重建时将自动跳过匹配该品牌的所有模型。点击品牌切换屏蔽状态,保存后自动触发路由重建。
|
||||||
@@ -2301,7 +2438,7 @@ export default function Settings() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Global Allowed Models Whitelist */}
|
{/* Global Allowed Models Whitelist */}
|
||||||
<div className="card animate-slide-up stagger-7" style={{ padding: 20 }}>
|
<div className="card animate-slide-up stagger-7" style={settingsModernCardStyle}>
|
||||||
<div style={{ fontWeight: 600, fontSize: 14, marginBottom: 8 }}>全局模型白名单</div>
|
<div style={{ fontWeight: 600, fontSize: 14, marginBottom: 8 }}>全局模型白名单</div>
|
||||||
<div style={{ fontSize: 12, color: 'var(--color-text-muted)', marginBottom: 12, lineHeight: 1.6 }}>
|
<div style={{ fontSize: 12, color: 'var(--color-text-muted)', marginBottom: 12, lineHeight: 1.6 }}>
|
||||||
配置白名单后,路由重建和候选生成将只针对白名单中的模型。留空表示允许所有模型(向后兼容)。保存后自动触发路由重建。
|
配置白名单后,路由重建和候选生成将只针对白名单中的模型。留空表示允许所有模型(向后兼容)。保存后自动触发路由重建。
|
||||||
@@ -2411,7 +2548,7 @@ export default function Settings() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="card animate-slide-up stagger-8" style={{ padding: 20 }}>
|
<div id="settings-database" className="card animate-slide-up stagger-8" style={{ ...settingsModernCardStyle, ...settingsAnchorCardStyle }}>
|
||||||
<div style={{ fontWeight: 600, fontSize: 14, marginBottom: 10 }}>数据库迁移(SQLite / MySQL / PostgreSQL)</div>
|
<div style={{ fontWeight: 600, fontSize: 14, marginBottom: 10 }}>数据库迁移(SQLite / MySQL / PostgreSQL)</div>
|
||||||
<div style={{ fontSize: 12, color: 'var(--color-text-muted)', marginBottom: 12 }}>
|
<div style={{ fontSize: 12, color: 'var(--color-text-muted)', marginBottom: 12 }}>
|
||||||
可先测试连接,再迁移数据;迁移完成后可保存为运行数据库配置(重启容器后生效)。
|
可先测试连接,再迁移数据;迁移完成后可保存为运行数据库配置(重启容器后生效)。
|
||||||
@@ -2462,19 +2599,19 @@ export default function Settings() {
|
|||||||
<input
|
<input
|
||||||
value={shorthandConnection.host}
|
value={shorthandConnection.host}
|
||||||
onChange={(e) => setShorthandConnection((prev) => ({ ...prev, host: e.target.value }))}
|
onChange={(e) => setShorthandConnection((prev) => ({ ...prev, host: e.target.value }))}
|
||||||
placeholder="Host (required)"
|
placeholder="主机地址(必填)"
|
||||||
style={inputStyle}
|
style={inputStyle}
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
value={shorthandConnection.user}
|
value={shorthandConnection.user}
|
||||||
onChange={(e) => setShorthandConnection((prev) => ({ ...prev, user: e.target.value }))}
|
onChange={(e) => setShorthandConnection((prev) => ({ ...prev, user: e.target.value }))}
|
||||||
placeholder="User (required)"
|
placeholder="用户名(必填)"
|
||||||
style={inputStyle}
|
style={inputStyle}
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
value={shorthandConnection.password}
|
value={shorthandConnection.password}
|
||||||
onChange={(e) => setShorthandConnection((prev) => ({ ...prev, password: e.target.value }))}
|
onChange={(e) => setShorthandConnection((prev) => ({ ...prev, password: e.target.value }))}
|
||||||
placeholder="Password (required)"
|
placeholder="密码(必填)"
|
||||||
type="password"
|
type="password"
|
||||||
style={inputStyle}
|
style={inputStyle}
|
||||||
/>
|
/>
|
||||||
@@ -2505,7 +2642,7 @@ export default function Settings() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<code style={{ display: 'block', padding: '10px 14px', background: 'var(--color-bg)', borderRadius: 'var(--radius-sm)', fontSize: 12, fontFamily: 'var(--font-mono)', color: 'var(--color-text-secondary)', border: '1px solid var(--color-border-light)' }}>
|
<code style={{ display: 'block', padding: '10px 14px', background: 'var(--color-bg)', borderRadius: 'var(--radius-sm)', fontSize: 12, fontFamily: 'var(--font-mono)', color: 'var(--color-text-secondary)', border: '1px solid var(--color-border-light)' }}>
|
||||||
{generatedConnectionString || 'Fill host/user/password to generate connection string'}
|
{generatedConnectionString || '填写主机、用户名、密码后自动生成连接串'}
|
||||||
</code>
|
</code>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -2560,11 +2697,11 @@ export default function Settings() {
|
|||||||
|
|
||||||
{runtimeDatabaseState && (
|
{runtimeDatabaseState && (
|
||||||
<div style={{ border: '1px solid var(--color-border-light)', borderRadius: 'var(--radius-sm)', padding: 10, fontSize: 12, color: 'var(--color-text-secondary)', lineHeight: 1.8, marginBottom: migrationSummary ? 12 : 0 }}>
|
<div style={{ border: '1px solid var(--color-border-light)', borderRadius: 'var(--radius-sm)', padding: 10, fontSize: 12, color: 'var(--color-text-secondary)', lineHeight: 1.8, marginBottom: migrationSummary ? 12 : 0 }}>
|
||||||
<div>当前运行:{runtimeDatabaseState.active.dialect}({runtimeDatabaseState.active.connection || '(empty)' }){runtimeDatabaseState.active.ssl && ' [SSL]'}</div>
|
<div>当前运行:{runtimeDatabaseState.active.dialect}({runtimeDatabaseState.active.connection || '未配置'}){runtimeDatabaseState.active.ssl && '(已启用 SSL)'}</div>
|
||||||
<div>
|
<div>
|
||||||
已保存待生效:
|
已保存待生效:
|
||||||
{runtimeDatabaseState.saved
|
{runtimeDatabaseState.saved
|
||||||
? ` ${runtimeDatabaseState.saved.dialect}(${runtimeDatabaseState.saved.connection})${runtimeDatabaseState.saved.ssl ? ' [SSL]' : ''}`
|
? ` ${runtimeDatabaseState.saved.dialect}(${runtimeDatabaseState.saved.connection})${runtimeDatabaseState.saved.ssl ? '(已启用 SSL)' : ''}`
|
||||||
: ' 未保存'}
|
: ' 未保存'}
|
||||||
</div>
|
</div>
|
||||||
{runtimeDatabaseState.restartRequired && (
|
{runtimeDatabaseState.restartRequired && (
|
||||||
@@ -2582,9 +2719,10 @@ export default function Settings() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="settings-maintenance" style={settingsModernSectionStyle}>
|
||||||
<UpdateCenterSection />
|
<UpdateCenterSection />
|
||||||
|
|
||||||
<div className="card animate-slide-up stagger-6" style={{ padding: 20 }}>
|
<div className="card animate-slide-up stagger-6" style={settingsModernCardStyle}>
|
||||||
<div style={{ fontWeight: 600, fontSize: 14, marginBottom: 12 }}>维护工具</div>
|
<div style={{ fontWeight: 600, fontSize: 14, marginBottom: 12 }}>维护工具</div>
|
||||||
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
||||||
<button onClick={handleClearCache} disabled={clearingCache} className="btn btn-ghost" style={{ border: '1px solid var(--color-border)' }}>
|
<button onClick={handleClearCache} disabled={clearingCache} className="btn btn-ghost" style={{ border: '1px solid var(--color-border)' }}>
|
||||||
@@ -2596,7 +2734,7 @@ export default function Settings() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="card animate-slide-up stagger-7" style={{ padding: 20, border: '1px solid color-mix(in srgb, var(--color-danger) 30%, var(--color-border))' }}>
|
<div className="card animate-slide-up stagger-7" style={settingsModernDangerCardStyle}>
|
||||||
<div style={{ fontWeight: 600, fontSize: 14, marginBottom: 10, color: 'var(--color-danger)' }}>危险操作</div>
|
<div style={{ fontWeight: 600, fontSize: 14, marginBottom: 10, color: 'var(--color-danger)' }}>危险操作</div>
|
||||||
<div style={{ fontSize: 12, color: 'var(--color-text-muted)', lineHeight: 1.8, marginBottom: 12 }}>
|
<div style={{ fontSize: 12, color: 'var(--color-text-muted)', lineHeight: 1.8, marginBottom: 12 }}>
|
||||||
重新初始化系统会清空当前 BoosAPI 使用中的全部数据库内容;若当前运行在外部 MySQL/Postgres,也会先清空该外部库中的 BoosAPI 数据,然后切回默认 SQLite。
|
重新初始化系统会清空当前 BoosAPI 使用中的全部数据库内容;若当前运行在外部 MySQL/Postgres,也会先清空该外部库中的 BoosAPI 数据,然后切回默认 SQLite。
|
||||||
@@ -2609,7 +2747,9 @@ export default function Settings() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="card animate-slide-up stagger-7" style={{ padding: 20 }}>
|
</div>
|
||||||
|
|
||||||
|
<div id="settings-security" className="card animate-slide-up stagger-7" style={{ ...settingsModernCardStyle, ...settingsAnchorCardStyle }}>
|
||||||
<div style={{ fontWeight: 600, fontSize: 14, marginBottom: 10 }}>会话与安全</div>
|
<div style={{ fontWeight: 600, fontSize: 14, marginBottom: 10 }}>会话与安全</div>
|
||||||
<div style={{ fontSize: 12, color: 'var(--color-text-muted)', marginBottom: 12 }}>
|
<div style={{ fontSize: 12, color: 'var(--color-text-muted)', marginBottom: 12 }}>
|
||||||
登录会话默认 12 小时自动过期。可选配置管理端 IP 白名单,支持每行一个 IP 或 IPv4 CIDR 网段。
|
登录会话默认 12 小时自动过期。可选配置管理端 IP 白名单,支持每行一个 IP 或 IPv4 CIDR 网段。
|
||||||
@@ -2643,6 +2783,7 @@ export default function Settings() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<FactoryResetModal
|
<FactoryResetModal
|
||||||
presence={factoryResetPresence}
|
presence={factoryResetPresence}
|
||||||
factoryResetting={factoryResetting}
|
factoryResetting={factoryResetting}
|
||||||
|
|||||||
Reference in New Issue
Block a user