'use client'; import { useEffect, useState } from 'react'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { EmptyState } from '@/components/feedback/empty-state'; import { ErrorState } from '@/components/feedback/error-state'; import { InlineMessage } from '@/components/feedback/inline-message'; import { LoadingState } from '@/components/feedback/loading-state'; import { PageHeader } from '@/components/layout/page-header'; import { useAuth } from '@/components/providers/auth-provider'; import { AppCard } from '@/components/ui/app-card'; import { StatusBadge } from '@/components/ui/status-badge'; import { getConfigVersionPreview } from '@/features/config-versions/api/config-versions'; import { getOptions, updateOption } from '@/features/settings/api/settings'; import type { OptionItem } from '@/features/settings/types'; import { CodeBlock, PrimaryButton, ResourceField, ResourceInput, ResourceSelect, ResourceTextarea, SecondaryButton, ToggleField, } from '@/features/shared/components/resource-primitives'; const settingsQueryKey = ['settings', 'options'] as const; const previewQueryKey = ['performance', 'preview'] as const; const defaultPerformanceFields = { OpenRestyWorkerProcesses: 'auto', OpenRestyWorkerConnections: '4096', OpenRestyWorkerRlimitNofile: '65535', OpenRestyEventsUse: '', OpenRestyEventsMultiAcceptEnabled: false, OpenRestyKeepaliveTimeout: '65', OpenRestyKeepaliveRequests: '1000', OpenRestyClientHeaderTimeout: '15', OpenRestyClientBodyTimeout: '15', OpenRestySendTimeout: '30', OpenRestyProxyConnectTimeout: '5', OpenRestyProxySendTimeout: '60', OpenRestyProxyReadTimeout: '60', OpenRestyProxyBufferingEnabled: true, OpenRestyProxyBuffers: '16 16k', OpenRestyProxyBufferSize: '8k', OpenRestyProxyBusyBuffersSize: '64k', OpenRestyGzipEnabled: true, OpenRestyGzipMinLength: '1024', OpenRestyGzipCompLevel: '5', OpenRestyCacheEnabled: false, OpenRestyCachePath: '', OpenRestyCacheLevels: '1:2', OpenRestyCacheInactive: '30m', OpenRestyCacheMaxSize: '1g', OpenRestyCacheKeyTemplate: '$scheme$proxy_host$request_uri', OpenRestyCacheLockEnabled: true, OpenRestyCacheLockTimeout: '5s', OpenRestyCacheUseStale: 'error timeout updating http_500 http_502 http_503 http_504', }; type PerformanceTab = 'settings' | 'editor'; type FeedbackState = { tone: 'info' | 'success' | 'danger'; message: string; }; function getErrorMessage(error: unknown) { return error instanceof Error ? error.message : '请求失败,请稍后重试。'; } function optionsToMap(options: OptionItem[] | undefined) { return (options ?? []).reduce>( (accumulator, option) => { accumulator[option.key] = option.value; return accumulator; }, {}, ); } function toBoolean(value: string | undefined, fallback: boolean) { if (value === undefined) { return fallback; } return value === 'true'; } function isPositiveInteger(value: string) { const parsed = Number.parseInt(value, 10); return !Number.isNaN(parsed) && parsed > 0; } function isSizeValue(value: string) { return /^\d+[kKmMgG]?$/.test(value.trim()); } function isProxyBuffersValue(value: string) { return /^\d+\s+\d+[kKmMgG]?$/.test(value.trim()); } function isDurationToken(value: string) { return /^\d+[smhdwSMHDW]$/.test(value.trim()); } function isCacheLevelsValue(value: string) { return /^\d{1,2}(?::\d{1,2}){0,2}$/.test(value.trim()); } function extractMainConfigLines(content: string, matcher: RegExp) { return content .split('\n') .map((line) => line.trim()) .filter((line) => line && matcher.test(line)); } export function PerformancePage() { const queryClient = useQueryClient(); const { user } = useAuth(); const [activeTab, setActiveTab] = useState('settings'); const [feedback, setFeedback] = useState(null); const [busyKey, setBusyKey] = useState(null); const [performanceFields, setPerformanceFields] = useState( defaultPerformanceFields, ); const [templateContent, setTemplateContent] = useState(''); const isRoot = (user?.role ?? 0) >= 100; const optionsQuery = useQuery({ queryKey: settingsQueryKey, queryFn: getOptions, enabled: isRoot, }); const previewQuery = useQuery({ queryKey: previewQueryKey, queryFn: getConfigVersionPreview, enabled: isRoot, }); useEffect(() => { if (!optionsQuery.data) { return; } const optionMap = optionsToMap(optionsQuery.data); setPerformanceFields({ OpenRestyWorkerProcesses: optionMap.OpenRestyWorkerProcesses ?? 'auto', OpenRestyWorkerConnections: optionMap.OpenRestyWorkerConnections ?? '4096', OpenRestyWorkerRlimitNofile: optionMap.OpenRestyWorkerRlimitNofile ?? '65535', OpenRestyEventsUse: optionMap.OpenRestyEventsUse ?? '', OpenRestyEventsMultiAcceptEnabled: toBoolean( optionMap.OpenRestyEventsMultiAcceptEnabled, false, ), OpenRestyKeepaliveTimeout: optionMap.OpenRestyKeepaliveTimeout ?? '65', OpenRestyKeepaliveRequests: optionMap.OpenRestyKeepaliveRequests ?? '1000', OpenRestyClientHeaderTimeout: optionMap.OpenRestyClientHeaderTimeout ?? '15', OpenRestyClientBodyTimeout: optionMap.OpenRestyClientBodyTimeout ?? '15', OpenRestySendTimeout: optionMap.OpenRestySendTimeout ?? '30', OpenRestyProxyConnectTimeout: optionMap.OpenRestyProxyConnectTimeout ?? '5', OpenRestyProxySendTimeout: optionMap.OpenRestyProxySendTimeout ?? '60', OpenRestyProxyReadTimeout: optionMap.OpenRestyProxyReadTimeout ?? '60', OpenRestyProxyBufferingEnabled: toBoolean( optionMap.OpenRestyProxyBufferingEnabled, true, ), OpenRestyProxyBuffers: optionMap.OpenRestyProxyBuffers ?? '16 16k', OpenRestyProxyBufferSize: optionMap.OpenRestyProxyBufferSize ?? '8k', OpenRestyProxyBusyBuffersSize: optionMap.OpenRestyProxyBusyBuffersSize ?? '64k', OpenRestyGzipEnabled: toBoolean(optionMap.OpenRestyGzipEnabled, true), OpenRestyGzipMinLength: optionMap.OpenRestyGzipMinLength ?? '1024', OpenRestyGzipCompLevel: optionMap.OpenRestyGzipCompLevel ?? '5', OpenRestyCacheEnabled: toBoolean(optionMap.OpenRestyCacheEnabled, false), OpenRestyCachePath: optionMap.OpenRestyCachePath ?? '', OpenRestyCacheLevels: optionMap.OpenRestyCacheLevels ?? '1:2', OpenRestyCacheInactive: optionMap.OpenRestyCacheInactive ?? '30m', OpenRestyCacheMaxSize: optionMap.OpenRestyCacheMaxSize ?? '1g', OpenRestyCacheKeyTemplate: optionMap.OpenRestyCacheKeyTemplate ?? '$scheme$proxy_host$request_uri', OpenRestyCacheLockEnabled: toBoolean( optionMap.OpenRestyCacheLockEnabled, true, ), OpenRestyCacheLockTimeout: optionMap.OpenRestyCacheLockTimeout ?? '5s', OpenRestyCacheUseStale: optionMap.OpenRestyCacheUseStale ?? 'error timeout updating http_500 http_502 http_503 http_504', }); setTemplateContent(optionMap.OpenRestyMainConfigTemplate ?? ''); }, [optionsQuery.data]); const runBusyAction = async (key: string, action: () => Promise) => { setBusyKey(key); setFeedback(null); try { await action(); } catch (error) { setFeedback({ tone: 'danger', message: getErrorMessage(error) }); } finally { setBusyKey(null); } }; const saveOptionEntries = async ( entries: Array<[string, string]>, successMessage: string, ) => { for (const [key, value] of entries) { await updateOption(key, value); } await Promise.all([ queryClient.invalidateQueries({ queryKey: settingsQueryKey }), queryClient.invalidateQueries({ queryKey: previewQueryKey }), queryClient.invalidateQueries({ queryKey: ['config-versions'] }), ]); setFeedback({ tone: 'success', message: successMessage }); }; const handleRuntimeSave = () => { void runBusyAction('performance-runtime', async () => { if ( performanceFields.OpenRestyWorkerProcesses !== 'auto' && !isPositiveInteger(performanceFields.OpenRestyWorkerProcesses) ) { throw new Error('worker_processes 必须为 auto 或大于 0 的整数。'); } const integerFields = [ ['worker_connections', performanceFields.OpenRestyWorkerConnections], ['worker_rlimit_nofile', performanceFields.OpenRestyWorkerRlimitNofile], ['keepalive_timeout', performanceFields.OpenRestyKeepaliveTimeout], ['keepalive_requests', performanceFields.OpenRestyKeepaliveRequests], [ 'client_header_timeout', performanceFields.OpenRestyClientHeaderTimeout, ], ['client_body_timeout', performanceFields.OpenRestyClientBodyTimeout], ['send_timeout', performanceFields.OpenRestySendTimeout], ] as const; for (const [label, value] of integerFields) { if (!isPositiveInteger(value)) { throw new Error(`${label} 必须为大于 0 的整数。`); } } await saveOptionEntries( [ [ 'OpenRestyWorkerProcesses', performanceFields.OpenRestyWorkerProcesses.trim(), ], [ 'OpenRestyWorkerConnections', performanceFields.OpenRestyWorkerConnections.trim(), ], [ 'OpenRestyWorkerRlimitNofile', performanceFields.OpenRestyWorkerRlimitNofile.trim(), ], ['OpenRestyEventsUse', performanceFields.OpenRestyEventsUse.trim()], [ 'OpenRestyEventsMultiAcceptEnabled', String(performanceFields.OpenRestyEventsMultiAcceptEnabled), ], [ 'OpenRestyKeepaliveTimeout', performanceFields.OpenRestyKeepaliveTimeout.trim(), ], [ 'OpenRestyKeepaliveRequests', performanceFields.OpenRestyKeepaliveRequests.trim(), ], [ 'OpenRestyClientHeaderTimeout', performanceFields.OpenRestyClientHeaderTimeout.trim(), ], [ 'OpenRestyClientBodyTimeout', performanceFields.OpenRestyClientBodyTimeout.trim(), ], [ 'OpenRestySendTimeout', performanceFields.OpenRestySendTimeout.trim(), ], ], 'OpenResty 连接与事件参数已保存。', ); }); }; const handleProxySave = () => { void runBusyAction('performance-proxy', async () => { const timeoutFields = [ performanceFields.OpenRestyProxyConnectTimeout, performanceFields.OpenRestyProxySendTimeout, performanceFields.OpenRestyProxyReadTimeout, ]; for (const value of timeoutFields) { if (!isPositiveInteger(value)) { throw new Error('代理超时参数必须为大于 0 的整数秒。'); } } if (!isProxyBuffersValue(performanceFields.OpenRestyProxyBuffers)) { throw new Error('proxy_buffers 格式必须类似 "16 16k"。'); } if ( !isSizeValue(performanceFields.OpenRestyProxyBufferSize) || !isSizeValue(performanceFields.OpenRestyProxyBusyBuffersSize) ) { throw new Error('缓冲大小必须为整数或带 k/m/g 单位的值。'); } await saveOptionEntries( [ [ 'OpenRestyProxyConnectTimeout', performanceFields.OpenRestyProxyConnectTimeout.trim(), ], [ 'OpenRestyProxySendTimeout', performanceFields.OpenRestyProxySendTimeout.trim(), ], [ 'OpenRestyProxyReadTimeout', performanceFields.OpenRestyProxyReadTimeout.trim(), ], [ 'OpenRestyProxyBufferingEnabled', String(performanceFields.OpenRestyProxyBufferingEnabled), ], [ 'OpenRestyProxyBuffers', performanceFields.OpenRestyProxyBuffers.trim(), ], [ 'OpenRestyProxyBufferSize', performanceFields.OpenRestyProxyBufferSize.trim(), ], [ 'OpenRestyProxyBusyBuffersSize', performanceFields.OpenRestyProxyBusyBuffersSize.trim(), ], ], 'OpenResty 反代缓冲参数已保存。', ); }); }; const handleCacheSave = () => { void runBusyAction('performance-cache', async () => { if (performanceFields.OpenRestyCacheEnabled) { if (!performanceFields.OpenRestyCachePath.trim()) { throw new Error('启用缓存时必须填写 proxy_cache_path 目录。'); } if ( !isCacheLevelsValue(performanceFields.OpenRestyCacheLevels) || !isDurationToken(performanceFields.OpenRestyCacheInactive) || !isSizeValue(performanceFields.OpenRestyCacheMaxSize) || !isDurationToken(performanceFields.OpenRestyCacheLockTimeout) ) { throw new Error( '缓存 levels、inactive、max_size 或 lock_timeout 格式不合法。', ); } if (!performanceFields.OpenRestyCacheKeyTemplate.trim()) { throw new Error('启用缓存时必须填写缓存 Key 模板。'); } } await saveOptionEntries( [ ['OpenRestyCachePath', performanceFields.OpenRestyCachePath.trim()], [ 'OpenRestyCacheLevels', performanceFields.OpenRestyCacheLevels.trim(), ], [ 'OpenRestyCacheInactive', performanceFields.OpenRestyCacheInactive.trim(), ], [ 'OpenRestyCacheMaxSize', performanceFields.OpenRestyCacheMaxSize.trim(), ], [ 'OpenRestyCacheKeyTemplate', performanceFields.OpenRestyCacheKeyTemplate.trim(), ], [ 'OpenRestyCacheLockEnabled', String(performanceFields.OpenRestyCacheLockEnabled), ], [ 'OpenRestyCacheLockTimeout', performanceFields.OpenRestyCacheLockTimeout.trim(), ], [ 'OpenRestyCacheUseStale', performanceFields.OpenRestyCacheUseStale.trim(), ], [ 'OpenRestyCacheEnabled', String(performanceFields.OpenRestyCacheEnabled), ], ], 'OpenResty 压缩与缓存参数已保存。', ); }); }; const handleGzipSave = () => { void runBusyAction('performance-gzip', async () => { if (!isPositiveInteger(performanceFields.OpenRestyGzipMinLength)) { throw new Error('gzip_min_length 必须为大于 0 的整数。'); } const gzipLevel = Number.parseInt( performanceFields.OpenRestyGzipCompLevel, 10, ); if (Number.isNaN(gzipLevel) || gzipLevel < 1 || gzipLevel > 9) { throw new Error('gzip_comp_level 必须在 1 到 9 之间。'); } await saveOptionEntries( [ [ 'OpenRestyGzipEnabled', String(performanceFields.OpenRestyGzipEnabled), ], [ 'OpenRestyGzipMinLength', performanceFields.OpenRestyGzipMinLength.trim(), ], [ 'OpenRestyGzipCompLevel', performanceFields.OpenRestyGzipCompLevel.trim(), ], ], 'OpenResty 压缩参数已保存。', ); }); }; const handleTemplateSave = () => { void runBusyAction('performance-template', async () => { await saveOptionEntries( [['OpenRestyMainConfigTemplate', templateContent]], 'OpenResty 主配置模板已保存。', ); }); }; if (!isRoot) { return (
); } if (optionsQuery.isLoading || previewQuery.isLoading) { return ; } if (optionsQuery.isError) { return ( ); } if (previewQuery.isError) { return ( ); } const preview = previewQuery.data; if (!preview) { return ( ); } const cachePreviewLines = extractMainConfigLines( preview.main_config, /proxy_cache_path|proxy_cache_key|proxy_cache_lock(?:_timeout)?|proxy_cache_use_stale/, ); const gzipPreviewLines = extractMainConfigLines( preview.main_config, /gzip(?:_| )/, ); return (
{feedback ? ( ) : null}
{[ { key: 'settings' as const, label: '设置', description: '维护连接、缓冲、压缩与缓存参数。', }, { key: 'editor' as const, label: '编辑', description: '编辑 nginx.conf 模板并查看当前渲染结果。', }, ].map((tab) => ( ))}
{activeTab === 'settings' ? (

主配置校验

当前预览规则数

{preview.route_count} 条

当前模板

受控模板 + 占位符渲染

{busyKey === 'performance-runtime' ? '保存中...' : '保存运行调优'} } >
setPerformanceFields((previous) => ({ ...previous, OpenRestyWorkerProcesses: event.target.value, })) } placeholder="auto" /> setPerformanceFields((previous) => ({ ...previous, OpenRestyWorkerConnections: event.target.value, })) } /> setPerformanceFields((previous) => ({ ...previous, OpenRestyWorkerRlimitNofile: event.target.value, })) } /> setPerformanceFields((previous) => ({ ...previous, OpenRestyEventsUse: event.target.value, })) } > setPerformanceFields((previous) => ({ ...previous, OpenRestyEventsMultiAcceptEnabled: checked, })) } /> setPerformanceFields((previous) => ({ ...previous, OpenRestyKeepaliveTimeout: event.target.value, })) } /> setPerformanceFields((previous) => ({ ...previous, OpenRestyKeepaliveRequests: event.target.value, })) } /> setPerformanceFields((previous) => ({ ...previous, OpenRestyClientHeaderTimeout: event.target.value, })) } /> setPerformanceFields((previous) => ({ ...previous, OpenRestyClientBodyTimeout: event.target.value, })) } /> setPerformanceFields((previous) => ({ ...previous, OpenRestySendTimeout: event.target.value, })) } />
{busyKey === 'performance-proxy' ? '保存中...' : '保存代理调优'} } >
setPerformanceFields((previous) => ({ ...previous, OpenRestyProxyConnectTimeout: event.target.value, })) } /> setPerformanceFields((previous) => ({ ...previous, OpenRestyProxySendTimeout: event.target.value, })) } /> setPerformanceFields((previous) => ({ ...previous, OpenRestyProxyReadTimeout: event.target.value, })) } /> setPerformanceFields((previous) => ({ ...previous, OpenRestyProxyBufferingEnabled: checked, })) } /> setPerformanceFields((previous) => ({ ...previous, OpenRestyProxyBuffers: event.target.value, })) } placeholder="16 16k" /> setPerformanceFields((previous) => ({ ...previous, OpenRestyProxyBufferSize: event.target.value, })) } placeholder="8k" /> setPerformanceFields((previous) => ({ ...previous, OpenRestyProxyBusyBuffersSize: event.target.value, })) } placeholder="64k" />
{busyKey === 'performance-gzip' ? '保存中...' : '保存压缩设置'} } >
setPerformanceFields((previous) => ({ ...previous, OpenRestyGzipEnabled: checked, })) } />

当前压缩预览

{gzipPreviewLines.length > 0 ? ( {gzipPreviewLines.join('\n')} ) : (

当前主配置预览中没有压缩指令。

)}
setPerformanceFields((previous) => ({ ...previous, OpenRestyGzipMinLength: event.target.value, })) } /> setPerformanceFields((previous) => ({ ...previous, OpenRestyGzipCompLevel: event.target.value, })) } />
setPerformanceFields((previous) => ({ ...previous, OpenRestyCacheEnabled: !previous.OpenRestyCacheEnabled, })) } > {performanceFields.OpenRestyCacheEnabled ? '先关闭缓存' : '先启用缓存'} {busyKey === 'performance-cache' ? '保存中...' : performanceFields.OpenRestyCacheEnabled ? '保存缓存设置' : '保存为关闭状态'}
} >

缓存状态

`max_size` 会跟随 `proxy_cache_path` 一起渲染,不会单独占一行。

当前缓存预览

{cachePreviewLines.length > 0 ? ( {cachePreviewLines.join('\n')} ) : (

当前主配置预览中没有缓存指令。启用缓存并保存后,会出现 `proxy_cache_path ... max_size=...;` 这一行。

)}
setPerformanceFields((previous) => ({ ...previous, OpenRestyCachePath: event.target.value, })) } placeholder="/var/cache/openresty/atsflare" /> setPerformanceFields((previous) => ({ ...previous, OpenRestyCacheLevels: event.target.value, })) } placeholder="1:2" /> setPerformanceFields((previous) => ({ ...previous, OpenRestyCacheInactive: event.target.value, })) } placeholder="30m" /> setPerformanceFields((previous) => ({ ...previous, OpenRestyCacheMaxSize: event.target.value, })) } placeholder="1g" /> setPerformanceFields((previous) => ({ ...previous, OpenRestyCacheKeyTemplate: event.target.value, })) } /> setPerformanceFields((previous) => ({ ...previous, OpenRestyCacheLockEnabled: checked, })) } /> setPerformanceFields((previous) => ({ ...previous, OpenRestyCacheLockTimeout: event.target.value, })) } placeholder="5s" /> setPerformanceFields((previous) => ({ ...previous, OpenRestyCacheUseStale: event.target.value, })) } />
) : (
void queryClient.invalidateQueries({ queryKey: previewQueryKey, }) } > 刷新预览 {busyKey === 'performance-template' ? '保存中...' : '保存模板'}
} >
setTemplateContent(event.target.value)} className="min-h-[560px] font-mono text-xs leading-6" placeholder="请输入受控 nginx.conf 模板" />

当前渲染预览

基于当前结构化参数和模板生成的主配置文件。

{preview.main_config}
)} ); }