| null): NodeEditorValues {
ip: node.ip ?? '',
auto_update_enabled: node.auto_update_enabled ?? false,
geo_manual_override: node.geo_manual_override ?? false,
- geo_region: node.geo_manual_override ? node.geo_name ?? '' : '',
+ geo_region: node.geo_manual_override ? (node.geo_name ?? '') : '',
geo_name: node.geo_name ?? '',
geo_latitude:
node.geo_latitude === undefined || node.geo_latitude === null
diff --git a/openflare_server/web/features/nodes/components/nodes-page.tsx b/openflare_server/web/features/nodes/components/nodes-page.tsx
index 76fce423..1e422639 100644
--- a/openflare_server/web/features/nodes/components/nodes-page.tsx
+++ b/openflare_server/web/features/nodes/components/nodes-page.tsx
@@ -354,10 +354,10 @@ export function NodesPage() {
{isWSConnectedLastSeen(node.last_seen_at)
? 'WS 已连接'
: isMeaningfulTime(node.last_seen_at)
- ? `${formatRelativeTime(
- node.last_seen_at,
- )} · ${formatDateTime(node.last_seen_at)}`
- : '暂无'}
+ ? `${formatRelativeTime(
+ node.last_seen_at,
+ )} · ${formatDateTime(node.last_seen_at)}`
+ : '暂无'}
diff --git a/openflare_server/web/features/nodes/types.ts b/openflare_server/web/features/nodes/types.ts
index f7ae80df..4ad97ee9 100644
--- a/openflare_server/web/features/nodes/types.ts
+++ b/openflare_server/web/features/nodes/types.ts
@@ -1,188 +1,188 @@
-import type { ReleaseChannel } from '@/features/update/types';
-
-export interface NodeItem {
- id: number;
- node_id: string;
- name: string;
- ip: string;
- geo_name: string;
- geo_latitude?: number | null;
- geo_longitude?: number | null;
- geo_manual_override: boolean;
- agent_token: string;
- auto_update_enabled: boolean;
- update_requested: boolean;
- update_channel: ReleaseChannel;
- update_tag: string;
- restart_openresty_requested: boolean;
- agent_version: string;
- nginx_version: string;
- openresty_status: 'healthy' | 'unhealthy' | 'unknown';
- openresty_message: string;
- status: 'online' | 'offline' | 'pending';
- current_version: string;
- last_seen_at: string;
- last_error: string;
- latest_apply_result: 'success' | 'warning' | 'failed' | '';
- latest_apply_message: string;
- latest_apply_checksum: string;
- latest_main_config_checksum: string;
- latest_route_config_checksum: string;
- latest_support_file_count: number;
- latest_apply_at?: string | null;
- created_at: string;
- updated_at: string;
-}
-
-export interface NodeBootstrapToken {
- discovery_token: string;
-}
-
-export interface NodeMutationPayload {
- name: string;
- ip: string;
- auto_update_enabled: boolean;
- geo_name: string;
- geo_latitude?: number | null;
- geo_longitude?: number | null;
- geo_manual_override: boolean;
-}
-
-export interface NodeAgentReleaseInfo {
- tag_name: string;
- body: string;
- html_url: string;
- published_at: string;
- current_version: string;
- has_update: boolean;
- channel: ReleaseChannel;
- prerelease: boolean;
- update_requested: boolean;
- requested_channel: ReleaseChannel;
- requested_tag: string;
-}
-
-export interface NodeAgentUpdatePayload {
- channel?: ReleaseChannel;
- tag_name?: string;
-}
-
-export interface NodeSystemProfile {
- hostname: string;
- os_name: string;
- os_version: string;
- kernel_version: string;
- architecture: string;
- cpu_model: string;
- cpu_cores: number;
- total_memory_bytes: number;
- total_disk_bytes: number;
- uptime_seconds: number;
- reported_at: string;
-}
-
-export interface NodeMetricSnapshot {
- captured_at: string;
- cpu_usage_percent: number;
- memory_used_bytes: number;
- memory_total_bytes: number;
- storage_used_bytes: number;
- storage_total_bytes: number;
- disk_read_bytes: number;
- disk_write_bytes: number;
- network_rx_bytes: number;
- network_tx_bytes: number;
- openresty_rx_bytes: number;
- openresty_tx_bytes: number;
- openresty_connections: number;
-}
-
-export interface NodeTrafficReport {
- window_started_at: string;
- window_ended_at: string;
- request_count: number;
- error_count: number;
- unique_visitor_count: number;
- status_codes_json: string;
- top_domains_json: string;
- source_countries_json: string;
-}
-
-export interface NodeTrafficTrendPoint {
- bucket_started_at: string;
- request_count: number;
- error_count: number;
- unique_visitor_count: number;
-}
-
-export interface NodeCapacityTrendPoint {
- bucket_started_at: string;
- average_cpu_usage_percent: number;
- average_memory_usage_percent: number;
- reported_nodes: number;
-}
-
-export interface NodeNetworkTrendPoint {
- bucket_started_at: string;
- network_rx_bytes: number;
- network_tx_bytes: number;
- openresty_rx_bytes: number;
- openresty_tx_bytes: number;
- reported_nodes: number;
-}
-
-export interface NodeDiskIOTrendPoint {
- bucket_started_at: string;
- disk_read_bytes: number;
- disk_write_bytes: number;
- reported_nodes: number;
-}
-
-export interface NodeDistributionItem {
- key: string;
- value: number;
-}
-
-export interface NodeTrafficDistributions {
- status_codes: NodeDistributionItem[];
- top_domains: NodeDistributionItem[];
- source_countries: NodeDistributionItem[];
-}
-
-export interface NodeTrafficSummary {
- window_started_at: string;
- window_ended_at: string;
- request_count: number;
- unique_visitor_count: number;
- error_count: number;
- estimated_qps: number;
- error_rate_percent: number;
-}
-
-export interface NodeHealthSummary {
- active_alerts: number;
- critical_alerts: number;
- warning_alerts: number;
- info_alerts: number;
- resolved_alerts: number;
- has_capacity_risk: boolean;
- has_traffic_risk: boolean;
- has_runtime_risk: boolean;
-}
-
-export interface NodeObservabilityAnalytics {
- traffic: NodeTrafficSummary;
- distributions: NodeTrafficDistributions;
- health: NodeHealthSummary;
-}
-
-export interface NodeObservabilityTrends {
- traffic_24h: NodeTrafficTrendPoint[];
- capacity_24h: NodeCapacityTrendPoint[];
- network_24h: NodeNetworkTrendPoint[];
- disk_io_24h: NodeDiskIOTrendPoint[];
-}
-
+import type { ReleaseChannel } from '@/features/update/types';
+
+export interface NodeItem {
+ id: number;
+ node_id: string;
+ name: string;
+ ip: string;
+ geo_name: string;
+ geo_latitude?: number | null;
+ geo_longitude?: number | null;
+ geo_manual_override: boolean;
+ agent_token: string;
+ auto_update_enabled: boolean;
+ update_requested: boolean;
+ update_channel: ReleaseChannel;
+ update_tag: string;
+ restart_openresty_requested: boolean;
+ agent_version: string;
+ nginx_version: string;
+ openresty_status: 'healthy' | 'unhealthy' | 'unknown';
+ openresty_message: string;
+ status: 'online' | 'offline' | 'pending';
+ current_version: string;
+ last_seen_at: string;
+ last_error: string;
+ latest_apply_result: 'success' | 'warning' | 'failed' | '';
+ latest_apply_message: string;
+ latest_apply_checksum: string;
+ latest_main_config_checksum: string;
+ latest_route_config_checksum: string;
+ latest_support_file_count: number;
+ latest_apply_at?: string | null;
+ created_at: string;
+ updated_at: string;
+}
+
+export interface NodeBootstrapToken {
+ discovery_token: string;
+}
+
+export interface NodeMutationPayload {
+ name: string;
+ ip: string;
+ auto_update_enabled: boolean;
+ geo_name: string;
+ geo_latitude?: number | null;
+ geo_longitude?: number | null;
+ geo_manual_override: boolean;
+}
+
+export interface NodeAgentReleaseInfo {
+ tag_name: string;
+ body: string;
+ html_url: string;
+ published_at: string;
+ current_version: string;
+ has_update: boolean;
+ channel: ReleaseChannel;
+ prerelease: boolean;
+ update_requested: boolean;
+ requested_channel: ReleaseChannel;
+ requested_tag: string;
+}
+
+export interface NodeAgentUpdatePayload {
+ channel?: ReleaseChannel;
+ tag_name?: string;
+}
+
+export interface NodeSystemProfile {
+ hostname: string;
+ os_name: string;
+ os_version: string;
+ kernel_version: string;
+ architecture: string;
+ cpu_model: string;
+ cpu_cores: number;
+ total_memory_bytes: number;
+ total_disk_bytes: number;
+ uptime_seconds: number;
+ reported_at: string;
+}
+
+export interface NodeMetricSnapshot {
+ captured_at: string;
+ cpu_usage_percent: number;
+ memory_used_bytes: number;
+ memory_total_bytes: number;
+ storage_used_bytes: number;
+ storage_total_bytes: number;
+ disk_read_bytes: number;
+ disk_write_bytes: number;
+ network_rx_bytes: number;
+ network_tx_bytes: number;
+ openresty_rx_bytes: number;
+ openresty_tx_bytes: number;
+ openresty_connections: number;
+}
+
+export interface NodeTrafficReport {
+ window_started_at: string;
+ window_ended_at: string;
+ request_count: number;
+ error_count: number;
+ unique_visitor_count: number;
+ status_codes_json: string;
+ top_domains_json: string;
+ source_countries_json: string;
+}
+
+export interface NodeTrafficTrendPoint {
+ bucket_started_at: string;
+ request_count: number;
+ error_count: number;
+ unique_visitor_count: number;
+}
+
+export interface NodeCapacityTrendPoint {
+ bucket_started_at: string;
+ average_cpu_usage_percent: number;
+ average_memory_usage_percent: number;
+ reported_nodes: number;
+}
+
+export interface NodeNetworkTrendPoint {
+ bucket_started_at: string;
+ network_rx_bytes: number;
+ network_tx_bytes: number;
+ openresty_rx_bytes: number;
+ openresty_tx_bytes: number;
+ reported_nodes: number;
+}
+
+export interface NodeDiskIOTrendPoint {
+ bucket_started_at: string;
+ disk_read_bytes: number;
+ disk_write_bytes: number;
+ reported_nodes: number;
+}
+
+export interface NodeDistributionItem {
+ key: string;
+ value: number;
+}
+
+export interface NodeTrafficDistributions {
+ status_codes: NodeDistributionItem[];
+ top_domains: NodeDistributionItem[];
+ source_countries: NodeDistributionItem[];
+}
+
+export interface NodeTrafficSummary {
+ window_started_at: string;
+ window_ended_at: string;
+ request_count: number;
+ unique_visitor_count: number;
+ error_count: number;
+ estimated_qps: number;
+ error_rate_percent: number;
+}
+
+export interface NodeHealthSummary {
+ active_alerts: number;
+ critical_alerts: number;
+ warning_alerts: number;
+ info_alerts: number;
+ resolved_alerts: number;
+ has_capacity_risk: boolean;
+ has_traffic_risk: boolean;
+ has_runtime_risk: boolean;
+}
+
+export interface NodeObservabilityAnalytics {
+ traffic: NodeTrafficSummary;
+ distributions: NodeTrafficDistributions;
+ health: NodeHealthSummary;
+}
+
+export interface NodeObservabilityTrends {
+ traffic_24h: NodeTrafficTrendPoint[];
+ capacity_24h: NodeCapacityTrendPoint[];
+ network_24h: NodeNetworkTrendPoint[];
+ disk_io_24h: NodeDiskIOTrendPoint[];
+}
+
export interface NodeHealthEvent {
event_type: string;
severity: string;
@@ -194,13 +194,13 @@ export interface NodeHealthEvent {
reported_at: string;
resolved_at?: string | null;
}
-
-export interface NodeObservability {
- node_id: string;
- profile: NodeSystemProfile | null;
- metric_snapshots: NodeMetricSnapshot[];
- traffic_reports: NodeTrafficReport[];
- health_events: NodeHealthEvent[];
- analytics: NodeObservabilityAnalytics;
- trends: NodeObservabilityTrends;
-}
+
+export interface NodeObservability {
+ node_id: string;
+ profile: NodeSystemProfile | null;
+ metric_snapshots: NodeMetricSnapshot[];
+ traffic_reports: NodeTrafficReport[];
+ health_events: NodeHealthEvent[];
+ analytics: NodeObservabilityAnalytics;
+ trends: NodeObservabilityTrends;
+}
diff --git a/openflare_server/web/features/nodes/utils.ts b/openflare_server/web/features/nodes/utils.ts
index 4f1a4997..b3d04ac4 100644
--- a/openflare_server/web/features/nodes/utils.ts
+++ b/openflare_server/web/features/nodes/utils.ts
@@ -1,189 +1,194 @@
-import type { NodeItem } from '@/features/nodes/types';
-
-export const WS_CONNECTED_LAST_SEEN = '__OPENFLARE_WS_CONNECTED__';
-
-export function isWSConnectedLastSeen(value: string | null | undefined) {
- return value === WS_CONNECTED_LAST_SEEN;
-}
-
-export function isMeaningfulTime(value: string | null | undefined) {
- return (
- Boolean(value) &&
- !isWSConnectedLastSeen(value) &&
- !String(value).startsWith('0001-01-01')
- );
-}
-
-export function getNodeStatusVariant(status: NodeItem['status']) {
- if (status === 'online') {
- return 'success';
- }
-
- if (status === 'pending') {
- return 'warning';
- }
-
- return 'danger';
-}
-
-export function getNodeStatusLabel(status: NodeItem['status']) {
- if (status === 'online') {
- return '在线';
- }
-
- if (status === 'pending') {
- return '待接入';
- }
-
- return '离线';
-}
-
-export function getApplyVariant(result: NodeItem['latest_apply_result']) {
- if (result === 'success') {
- return 'success';
- }
-
- if (result === 'warning') {
- return 'warning';
- }
-
- if (result === 'failed') {
- return 'danger';
- }
-
- return 'warning';
-}
-
-export function getApplyLabel(result: NodeItem['latest_apply_result']) {
- if (result === 'success') {
- return '成功';
- }
-
- if (result === 'warning') {
- return '警告';
- }
-
- if (result === 'failed') {
- return '失败';
- }
-
- return '暂无';
-}
-
-export function getUpdateMode(node: NodeItem) {
- if (node.update_requested) {
- if (node.update_channel === 'preview') {
- return { label: '等待预览更新', variant: 'warning' as const };
- }
-
- return { label: '等待更新', variant: 'warning' as const };
- }
-
- if (node.auto_update_enabled) {
- return { label: '自动', variant: 'success' as const };
- }
-
- return { label: '手动', variant: 'info' as const };
-}
-
-export function getOpenrestyStatusVariant(status: NodeItem['openresty_status']) {
- if (status === 'healthy') {
- return 'success';
- }
-
- if (status === 'unhealthy') {
- return 'danger';
- }
-
- return 'warning';
-}
-
-export function getOpenrestyStatusLabel(status: NodeItem['openresty_status']) {
- if (status === 'healthy') {
- return '健康';
- }
-
- if (status === 'unhealthy') {
- return '异常';
- }
-
- return '未知';
-}
-
-function parseVersionParts(version: string) {
- const normalized = version.trim().replace(/^v/i, '');
- if (!normalized || normalized.toLowerCase() === 'unknown') {
- return null;
- }
-
- return normalized.split('.').map((segment) => {
- const matched = segment.trim().match(/^\d+/);
- return matched ? Number.parseInt(matched[0], 10) : 0;
- });
-}
-
-function isOlderVersion(current: string, target: string) {
- const currentParts = parseVersionParts(current);
- const targetParts = parseVersionParts(target);
- if (!currentParts || !targetParts) {
- return false;
- }
-
- const maxLength = Math.max(currentParts.length, targetParts.length);
- for (let index = 0; index < maxLength; index += 1) {
- const currentPart = currentParts[index] ?? 0;
- const targetPart = targetParts[index] ?? 0;
- if (currentPart < targetPart) {
- return true;
- }
- if (currentPart > targetPart) {
- return false;
- }
- }
-
- return false;
-}
-
-export function shouldShowManualUpdate(
- agentVersion: string,
- serverVersion: string,
-) {
- const normalizedServerVersion = serverVersion.trim();
- const normalizedAgentVersion = agentVersion.trim();
-
- if (
- !normalizedServerVersion ||
- normalizedServerVersion.toLowerCase() === 'dev' ||
- !normalizedAgentVersion ||
- normalizedAgentVersion.toLowerCase() === 'unknown'
- ) {
- return false;
- }
-
- return isOlderVersion(normalizedAgentVersion, normalizedServerVersion);
-}
-
-export function getServerUrl(value: string) {
- return value.trim().replace(/\/+$/, '');
-}
-
-const installerScriptUrl =
- 'https://raw.githubusercontent.com/Rain-kl/OpenFlare/main/scripts/install-agent.sh';
-
-export function buildNodeInstallCommand(serverUrl: string, agentToken: string) {
- return [
- `curl -fsSL ${installerScriptUrl} | bash -s -- \\`,
- ` --server-url ${serverUrl} \\`,
- ` --agent-token ${agentToken}`,
- ].join('\n');
-}
-
-export function buildNodeDockerInstallCommand(serverUrl: string, agentToken: string) {
- return [
- `docker run -d --name openflare-agent --restart unless-stopped \\`,
- ` -p 80:80 -p 443:443 \\`,
- ` -e OPENFLARE_SERVER_URL=${serverUrl} \\`,
- ` -e OPENFLARE_AGENT_TOKEN=${agentToken} \\`,
- ` ghcr.io/rain-kl/openflare-agent:latest`
- ].join('\n');
-}
+import type { NodeItem } from '@/features/nodes/types';
+
+export const WS_CONNECTED_LAST_SEEN = '__OPENFLARE_WS_CONNECTED__';
+
+export function isWSConnectedLastSeen(value: string | null | undefined) {
+ return value === WS_CONNECTED_LAST_SEEN;
+}
+
+export function isMeaningfulTime(value: string | null | undefined) {
+ return (
+ Boolean(value) &&
+ !isWSConnectedLastSeen(value) &&
+ !String(value).startsWith('0001-01-01')
+ );
+}
+
+export function getNodeStatusVariant(status: NodeItem['status']) {
+ if (status === 'online') {
+ return 'success';
+ }
+
+ if (status === 'pending') {
+ return 'warning';
+ }
+
+ return 'danger';
+}
+
+export function getNodeStatusLabel(status: NodeItem['status']) {
+ if (status === 'online') {
+ return '在线';
+ }
+
+ if (status === 'pending') {
+ return '待接入';
+ }
+
+ return '离线';
+}
+
+export function getApplyVariant(result: NodeItem['latest_apply_result']) {
+ if (result === 'success') {
+ return 'success';
+ }
+
+ if (result === 'warning') {
+ return 'warning';
+ }
+
+ if (result === 'failed') {
+ return 'danger';
+ }
+
+ return 'warning';
+}
+
+export function getApplyLabel(result: NodeItem['latest_apply_result']) {
+ if (result === 'success') {
+ return '成功';
+ }
+
+ if (result === 'warning') {
+ return '警告';
+ }
+
+ if (result === 'failed') {
+ return '失败';
+ }
+
+ return '暂无';
+}
+
+export function getUpdateMode(node: NodeItem) {
+ if (node.update_requested) {
+ if (node.update_channel === 'preview') {
+ return { label: '等待预览更新', variant: 'warning' as const };
+ }
+
+ return { label: '等待更新', variant: 'warning' as const };
+ }
+
+ if (node.auto_update_enabled) {
+ return { label: '自动', variant: 'success' as const };
+ }
+
+ return { label: '手动', variant: 'info' as const };
+}
+
+export function getOpenrestyStatusVariant(
+ status: NodeItem['openresty_status'],
+) {
+ if (status === 'healthy') {
+ return 'success';
+ }
+
+ if (status === 'unhealthy') {
+ return 'danger';
+ }
+
+ return 'warning';
+}
+
+export function getOpenrestyStatusLabel(status: NodeItem['openresty_status']) {
+ if (status === 'healthy') {
+ return '健康';
+ }
+
+ if (status === 'unhealthy') {
+ return '异常';
+ }
+
+ return '未知';
+}
+
+function parseVersionParts(version: string) {
+ const normalized = version.trim().replace(/^v/i, '');
+ if (!normalized || normalized.toLowerCase() === 'unknown') {
+ return null;
+ }
+
+ return normalized.split('.').map((segment) => {
+ const matched = segment.trim().match(/^\d+/);
+ return matched ? Number.parseInt(matched[0], 10) : 0;
+ });
+}
+
+function isOlderVersion(current: string, target: string) {
+ const currentParts = parseVersionParts(current);
+ const targetParts = parseVersionParts(target);
+ if (!currentParts || !targetParts) {
+ return false;
+ }
+
+ const maxLength = Math.max(currentParts.length, targetParts.length);
+ for (let index = 0; index < maxLength; index += 1) {
+ const currentPart = currentParts[index] ?? 0;
+ const targetPart = targetParts[index] ?? 0;
+ if (currentPart < targetPart) {
+ return true;
+ }
+ if (currentPart > targetPart) {
+ return false;
+ }
+ }
+
+ return false;
+}
+
+export function shouldShowManualUpdate(
+ agentVersion: string,
+ serverVersion: string,
+) {
+ const normalizedServerVersion = serverVersion.trim();
+ const normalizedAgentVersion = agentVersion.trim();
+
+ if (
+ !normalizedServerVersion ||
+ normalizedServerVersion.toLowerCase() === 'dev' ||
+ !normalizedAgentVersion ||
+ normalizedAgentVersion.toLowerCase() === 'unknown'
+ ) {
+ return false;
+ }
+
+ return isOlderVersion(normalizedAgentVersion, normalizedServerVersion);
+}
+
+export function getServerUrl(value: string) {
+ return value.trim().replace(/\/+$/, '');
+}
+
+const installerScriptUrl =
+ 'https://raw.githubusercontent.com/Rain-kl/OpenFlare/main/scripts/install-agent.sh';
+
+export function buildNodeInstallCommand(serverUrl: string, agentToken: string) {
+ return [
+ `curl -fsSL ${installerScriptUrl} | bash -s -- \\`,
+ ` --server-url ${serverUrl} \\`,
+ ` --agent-token ${agentToken}`,
+ ].join('\n');
+}
+
+export function buildNodeDockerInstallCommand(
+ serverUrl: string,
+ agentToken: string,
+) {
+ return [
+ `docker run -d --name openflare-agent --restart unless-stopped \\`,
+ ` -p 80:80 -p 443:443 \\`,
+ ` -e OPENFLARE_SERVER_URL=${serverUrl} \\`,
+ ` -e OPENFLARE_AGENT_TOKEN=${agentToken} \\`,
+ ` ghcr.io/rain-kl/openflare-agent:latest`,
+ ].join('\n');
+}
diff --git a/openflare_server/web/features/performance/components/performance-page.tsx b/openflare_server/web/features/performance/components/performance-page.tsx
index 44e4c73e..532758de 100644
--- a/openflare_server/web/features/performance/components/performance-page.tsx
+++ b/openflare_server/web/features/performance/components/performance-page.tsx
@@ -1,1354 +1,1342 @@
'use client';
-import {useEffect, useState} from 'react';
-import {useQuery, useQueryClient} from '@tanstack/react-query';
+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, updateOptions} from '@/features/settings/api/settings';
-import type {OptionItem} from '@/features/settings/types';
+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, updateOptions } from '@/features/settings/api/settings';
+import type { OptionItem } from '@/features/settings/types';
import {
- CodeBlock,
- PrimaryButton,
- ResourceField,
- ResourceInput,
- ResourceSelect,
- ResourceTextarea,
- SecondaryButton,
- ToggleField,
+ 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 templatePanelClassName =
- 'h-[560px] w-full box-border overflow-auto rounded-2xl border border-[var(--border-default)] bg-[var(--surface-elevated)] px-4 py-3 font-mono text-xs leading-6';
+ 'h-[560px] w-full box-border overflow-auto rounded-2xl border border-[var(--border-default)] bg-[var(--surface-elevated)] px-4 py-3 font-mono text-xs leading-6';
const defaultPerformanceFields = {
- OpenRestyWorkerProcesses: 'auto',
- OpenRestyWorkerConnections: '4096',
- OpenRestyWorkerRlimitNofile: '65535',
- OpenRestyEventsUse: 'epoll',
- OpenRestyEventsMultiAcceptEnabled: true,
- OpenRestyKeepaliveTimeout: '20',
- OpenRestyKeepaliveRequests: '1000',
- OpenRestyClientHeaderTimeout: '15',
- OpenRestyClientBodyTimeout: '15',
- OpenRestyClientMaxBodySize: '64m',
- OpenRestyLargeClientHeaderBuffers: '4 16k',
- OpenRestySendTimeout: '30',
- OpenRestyProxyConnectTimeout: '3',
- OpenRestyProxySendTimeout: '60',
- OpenRestyProxyReadTimeout: '60',
- OpenRestyWebsocketEnabled: true,
- OpenRestyProxyRequestBufferingEnabled: false,
- 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$host$request_uri',
- OpenRestyCacheLockEnabled: true,
- OpenRestyCacheLockTimeout: '5s',
- OpenRestyCacheUseStale:
- 'error timeout updating http_500 http_502 http_503 http_504',
- OpenRestyResolvers: '',
+ OpenRestyWorkerProcesses: 'auto',
+ OpenRestyWorkerConnections: '4096',
+ OpenRestyWorkerRlimitNofile: '65535',
+ OpenRestyEventsUse: 'epoll',
+ OpenRestyEventsMultiAcceptEnabled: true,
+ OpenRestyKeepaliveTimeout: '20',
+ OpenRestyKeepaliveRequests: '1000',
+ OpenRestyClientHeaderTimeout: '15',
+ OpenRestyClientBodyTimeout: '15',
+ OpenRestyClientMaxBodySize: '64m',
+ OpenRestyLargeClientHeaderBuffers: '4 16k',
+ OpenRestySendTimeout: '30',
+ OpenRestyProxyConnectTimeout: '3',
+ OpenRestyProxySendTimeout: '60',
+ OpenRestyProxyReadTimeout: '60',
+ OpenRestyWebsocketEnabled: true,
+ OpenRestyProxyRequestBufferingEnabled: false,
+ 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$host$request_uri',
+ OpenRestyCacheLockEnabled: true,
+ OpenRestyCacheLockTimeout: '5s',
+ OpenRestyCacheUseStale:
+ 'error timeout updating http_500 http_502 http_503 http_504',
+ OpenRestyResolvers: '',
};
const performanceFieldTooltips: Record = {
- worker_processes:
- 'Nginx worker 进程数。通常建议保持 auto,让 OpenResty 按 CPU 核数自动分配。',
- worker_connections:
- '每个 worker 可同时处理的最大连接数。值越大,可承载的并发连接越多。',
- worker_rlimit_nofile:
- '提升 worker 可打开的文件描述符上限,避免高并发下连接或文件句柄不足。',
- events_use:
- '指定事件驱动模型。默认使用 epoll,Linux 高并发场景通常优先选择它。',
- multi_accept:
- '默认开启。worker 会尽可能一次接受多个新连接,适合高吞吐接入场景。',
- keepalive_timeout: '客户端 Keep-Alive 空闲保持时间,单位秒。',
- keepalive_requests: '单个长连接允许复用的最大请求数。',
- client_header_timeout: '读取客户端请求头的超时时间,单位秒。',
- client_body_timeout: '读取客户端请求体的超时时间,单位秒。',
- client_max_body_size:
- '限制客户端请求体大小,常用于上传文件大小控制,例如 64m、128m。',
- large_client_header_buffers:
- '控制大请求头使用的缓冲区数量和大小,例如 4 16k。',
- send_timeout: '向客户端发送响应时的超时时间,单位秒。',
- proxy_connect_timeout: '连接上游源站的超时时间,单位秒。',
- proxy_send_timeout: '向上游发送请求的超时时间,单位秒。',
- proxy_read_timeout: '等待上游返回响应的超时时间,单位秒。',
- websocket:
- '控制是否为反向代理规则自动注入 WebSocket 升级所需的 HTTP/1.1、Upgrade 和 Connection 头。',
- proxy_request_buffering:
- '控制请求体是否先在 Nginx 侧缓冲后再转发给上游,上传和流式场景经常会用到。',
- proxy_buffering:
- '控制是否启用代理响应缓冲。开启后通常有更平滑的吞吐,但会增加内存占用。',
- proxy_buffers: '设置代理响应缓冲区的数量和大小,例如 16 16k。',
- proxy_buffer_size: '保存响应头等小块数据的基础缓冲区大小。',
- proxy_busy_buffers_size: '限制 busy 状态下可同时占用的缓冲区总大小。',
- gzip: '控制是否启用 gzip 压缩响应。',
- gzip_min_length:
- '只有响应体超过该字节数时才会启用 gzip,避免对极小响应做无意义压缩。',
- gzip_comp_level: 'gzip 压缩等级,1 更省 CPU,9 压缩更高但更耗 CPU。',
- proxy_cache_path: '缓存目录路径,对应 proxy_cache_path 指令中的磁盘位置。',
- levels: '缓存目录层级,例如 1:2,可控制缓存文件的目录分布。',
- inactive: '缓存对象在未命中访问时的失活时间,例如 30m。',
- max_size:
- '缓存目录允许占用的最大磁盘空间,会渲染到 proxy_cache_path 的 max_size。',
- cache_key_template: '生成缓存 Key 的模板,决定不同请求如何命中同一缓存对象。',
- proxy_cache_lock:
- '启用后,同一缓存 Key 未命中时只允许一个请求回源,减少击穿。',
- proxy_cache_lock_timeout: '等待缓存锁的最长时间,例如 5s。',
- proxy_cache_use_stale:
- '上游异常时允许返回旧缓存的条件列表,例如 error、timeout、http_500。',
- resolvers:
- '自定义 DNS 解析器,留空表示不配置。若配置,当上游源站需要动态解析且 DNS 服务器失效时,能防止容器异常挂掉。例如:1.1.1.1 8.8.8.8',
+ worker_processes:
+ 'Nginx worker 进程数。通常建议保持 auto,让 OpenResty 按 CPU 核数自动分配。',
+ worker_connections:
+ '每个 worker 可同时处理的最大连接数。值越大,可承载的并发连接越多。',
+ worker_rlimit_nofile:
+ '提升 worker 可打开的文件描述符上限,避免高并发下连接或文件句柄不足。',
+ events_use:
+ '指定事件驱动模型。默认使用 epoll,Linux 高并发场景通常优先选择它。',
+ multi_accept:
+ '默认开启。worker 会尽可能一次接受多个新连接,适合高吞吐接入场景。',
+ keepalive_timeout: '客户端 Keep-Alive 空闲保持时间,单位秒。',
+ keepalive_requests: '单个长连接允许复用的最大请求数。',
+ client_header_timeout: '读取客户端请求头的超时时间,单位秒。',
+ client_body_timeout: '读取客户端请求体的超时时间,单位秒。',
+ client_max_body_size:
+ '限制客户端请求体大小,常用于上传文件大小控制,例如 64m、128m。',
+ large_client_header_buffers:
+ '控制大请求头使用的缓冲区数量和大小,例如 4 16k。',
+ send_timeout: '向客户端发送响应时的超时时间,单位秒。',
+ proxy_connect_timeout: '连接上游源站的超时时间,单位秒。',
+ proxy_send_timeout: '向上游发送请求的超时时间,单位秒。',
+ proxy_read_timeout: '等待上游返回响应的超时时间,单位秒。',
+ websocket:
+ '控制是否为反向代理规则自动注入 WebSocket 升级所需的 HTTP/1.1、Upgrade 和 Connection 头。',
+ proxy_request_buffering:
+ '控制请求体是否先在 Nginx 侧缓冲后再转发给上游,上传和流式场景经常会用到。',
+ proxy_buffering:
+ '控制是否启用代理响应缓冲。开启后通常有更平滑的吞吐,但会增加内存占用。',
+ proxy_buffers: '设置代理响应缓冲区的数量和大小,例如 16 16k。',
+ proxy_buffer_size: '保存响应头等小块数据的基础缓冲区大小。',
+ proxy_busy_buffers_size: '限制 busy 状态下可同时占用的缓冲区总大小。',
+ gzip: '控制是否启用 gzip 压缩响应。',
+ gzip_min_length:
+ '只有响应体超过该字节数时才会启用 gzip,避免对极小响应做无意义压缩。',
+ gzip_comp_level: 'gzip 压缩等级,1 更省 CPU,9 压缩更高但更耗 CPU。',
+ proxy_cache_path: '缓存目录路径,对应 proxy_cache_path 指令中的磁盘位置。',
+ levels: '缓存目录层级,例如 1:2,可控制缓存文件的目录分布。',
+ inactive: '缓存对象在未命中访问时的失活时间,例如 30m。',
+ max_size:
+ '缓存目录允许占用的最大磁盘空间,会渲染到 proxy_cache_path 的 max_size。',
+ cache_key_template: '生成缓存 Key 的模板,决定不同请求如何命中同一缓存对象。',
+ proxy_cache_lock:
+ '启用后,同一缓存 Key 未命中时只允许一个请求回源,减少击穿。',
+ proxy_cache_lock_timeout: '等待缓存锁的最长时间,例如 5s。',
+ proxy_cache_use_stale:
+ '上游异常时允许返回旧缓存的条件列表,例如 error、timeout、http_500。',
+ resolvers:
+ '自定义 DNS 解析器,留空表示不配置。若配置,当上游源站需要动态解析且 DNS 服务器失效时,能防止容器异常挂掉。例如:1.1.1.1 8.8.8.8',
};
type PerformanceTab = 'settings' | 'editor';
type FeedbackState = {
- tone: 'info' | 'success' | 'danger';
- message: string;
+ tone: 'info' | 'success' | 'danger';
+ message: string;
};
function getErrorMessage(error: unknown) {
- return error instanceof Error ? error.message : '请求失败,请稍后重试。';
+ return error instanceof Error ? error.message : '请求失败,请稍后重试。';
}
function optionsToMap(options: OptionItem[] | undefined) {
- return (options ?? []).reduce>(
- (accumulator, option) => {
- accumulator[option.key] = option.value;
- return accumulator;
- },
- {},
- );
+ return (options ?? []).reduce>(
+ (accumulator, option) => {
+ accumulator[option.key] = option.value;
+ return accumulator;
+ },
+ {},
+ );
}
function toBoolean(value: string | undefined, fallback: boolean) {
- if (value === undefined) {
- return fallback;
- }
+ if (value === undefined) {
+ return fallback;
+ }
- return value === 'true';
+ return value === 'true';
}
function isPositiveInteger(value: string) {
- const parsed = Number.parseInt(value, 10);
- return !Number.isNaN(parsed) && parsed > 0;
+ const parsed = Number.parseInt(value, 10);
+ return !Number.isNaN(parsed) && parsed > 0;
}
function isSizeValue(value: string) {
- return /^\d+[kKmMgG]?$/.test(value.trim());
+ return /^\d+[kKmMgG]?$/.test(value.trim());
}
function isProxyBuffersValue(value: string) {
- return /^\d+\s+\d+[kKmMgG]?$/.test(value.trim());
+ return /^\d+\s+\d+[kKmMgG]?$/.test(value.trim());
}
function isDurationToken(value: string) {
- return /^\d+[smhdwSMHDW]$/.test(value.trim());
+ return /^\d+[smhdwSMHDW]$/.test(value.trim());
}
function isCacheLevelsValue(value: string) {
- return /^\d{1,2}(?::\d{1,2}){0,2}$/.test(value.trim());
+ return /^\d{1,2}(?::\d{1,2}){0,2}$/.test(value.trim());
}
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 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 isRoot = (user?.role ?? 0) >= 100;
- const optionsQuery = useQuery({
- queryKey: settingsQueryKey,
- queryFn: getOptions,
- enabled: isRoot,
+ 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 ?? 'epoll',
+ OpenRestyEventsMultiAcceptEnabled: toBoolean(
+ optionMap.OpenRestyEventsMultiAcceptEnabled,
+ true,
+ ),
+ OpenRestyKeepaliveTimeout: optionMap.OpenRestyKeepaliveTimeout ?? '20',
+ OpenRestyKeepaliveRequests:
+ optionMap.OpenRestyKeepaliveRequests ?? '1000',
+ OpenRestyClientHeaderTimeout:
+ optionMap.OpenRestyClientHeaderTimeout ?? '15',
+ OpenRestyClientBodyTimeout: optionMap.OpenRestyClientBodyTimeout ?? '15',
+ OpenRestyClientMaxBodySize: optionMap.OpenRestyClientMaxBodySize ?? '64m',
+ OpenRestyLargeClientHeaderBuffers:
+ optionMap.OpenRestyLargeClientHeaderBuffers ?? '4 16k',
+ OpenRestySendTimeout: optionMap.OpenRestySendTimeout ?? '30',
+ OpenRestyProxyConnectTimeout:
+ optionMap.OpenRestyProxyConnectTimeout ?? '3',
+ OpenRestyProxySendTimeout: optionMap.OpenRestyProxySendTimeout ?? '60',
+ OpenRestyProxyReadTimeout: optionMap.OpenRestyProxyReadTimeout ?? '60',
+ OpenRestyWebsocketEnabled: toBoolean(
+ optionMap.OpenRestyWebsocketEnabled,
+ true,
+ ),
+ OpenRestyProxyRequestBufferingEnabled: toBoolean(
+ optionMap.OpenRestyProxyRequestBufferingEnabled,
+ false,
+ ),
+ 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$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',
+ OpenRestyResolvers: optionMap.OpenRestyResolvers ?? '',
});
+ setTemplateContent(optionMap.OpenRestyMainConfigTemplate ?? '');
+ }, [optionsQuery.data]);
- const previewQuery = useQuery({
- queryKey: previewQueryKey,
- queryFn: getConfigVersionPreview,
- enabled: isRoot,
+ 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,
+ ) => {
+ await updateOptions(entries.map(([key, value]) => ({ 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 的整数。`);
+ }
+ }
+ if (!isSizeValue(performanceFields.OpenRestyClientMaxBodySize)) {
+ throw new Error(
+ 'client_max_body_size 必须为整数或带 k/m/g 单位的大小值。',
+ );
+ }
+ if (
+ !isProxyBuffersValue(
+ performanceFields.OpenRestyLargeClientHeaderBuffers,
+ )
+ ) {
+ throw new Error('large_client_header_buffers 格式必须类似 "4 16k"。');
+ }
+
+ await saveOptionEntries(
+ [
+ [
+ 'OpenRestyWorkerProcesses',
+ performanceFields.OpenRestyWorkerProcesses.trim(),
+ ],
+ ['OpenRestyResolvers', performanceFields.OpenRestyResolvers.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(),
+ ],
+ [
+ 'OpenRestyClientMaxBodySize',
+ performanceFields.OpenRestyClientMaxBodySize.trim(),
+ ],
+ [
+ 'OpenRestyLargeClientHeaderBuffers',
+ performanceFields.OpenRestyLargeClientHeaderBuffers.trim(),
+ ],
+ [
+ 'OpenRestySendTimeout',
+ performanceFields.OpenRestySendTimeout.trim(),
+ ],
+ ],
+ 'OpenResty 连接与事件参数已保存。',
+ );
});
+ };
- useEffect(() => {
- if (!optionsQuery.data) {
- return;
+ 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 的整数秒。');
}
+ }
- const optionMap = optionsToMap(optionsQuery.data);
- setPerformanceFields({
- OpenRestyWorkerProcesses: optionMap.OpenRestyWorkerProcesses ?? 'auto',
- OpenRestyWorkerConnections:
- optionMap.OpenRestyWorkerConnections ?? '4096',
- OpenRestyWorkerRlimitNofile:
- optionMap.OpenRestyWorkerRlimitNofile ?? '65535',
- OpenRestyEventsUse: optionMap.OpenRestyEventsUse ?? 'epoll',
- OpenRestyEventsMultiAcceptEnabled: toBoolean(
- optionMap.OpenRestyEventsMultiAcceptEnabled,
- true,
- ),
- OpenRestyKeepaliveTimeout: optionMap.OpenRestyKeepaliveTimeout ?? '20',
- OpenRestyKeepaliveRequests:
- optionMap.OpenRestyKeepaliveRequests ?? '1000',
- OpenRestyClientHeaderTimeout:
- optionMap.OpenRestyClientHeaderTimeout ?? '15',
- OpenRestyClientBodyTimeout: optionMap.OpenRestyClientBodyTimeout ?? '15',
- OpenRestyClientMaxBodySize: optionMap.OpenRestyClientMaxBodySize ?? '64m',
- OpenRestyLargeClientHeaderBuffers:
- optionMap.OpenRestyLargeClientHeaderBuffers ?? '4 16k',
- OpenRestySendTimeout: optionMap.OpenRestySendTimeout ?? '30',
- OpenRestyProxyConnectTimeout:
- optionMap.OpenRestyProxyConnectTimeout ?? '3',
- OpenRestyProxySendTimeout: optionMap.OpenRestyProxySendTimeout ?? '60',
- OpenRestyProxyReadTimeout: optionMap.OpenRestyProxyReadTimeout ?? '60',
- OpenRestyWebsocketEnabled: toBoolean(
- optionMap.OpenRestyWebsocketEnabled,
- true,
- ),
- OpenRestyProxyRequestBufferingEnabled: toBoolean(
- optionMap.OpenRestyProxyRequestBufferingEnabled,
- false,
- ),
- 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$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',
- OpenRestyResolvers: optionMap.OpenRestyResolvers ?? '',
- });
- setTemplateContent(optionMap.OpenRestyMainConfigTemplate ?? '');
- }, [optionsQuery.data]);
+ if (!isProxyBuffersValue(performanceFields.OpenRestyProxyBuffers)) {
+ throw new Error('proxy_buffers 格式必须类似 "16 16k"。');
+ }
+ if (
+ !isSizeValue(performanceFields.OpenRestyProxyBufferSize) ||
+ !isSizeValue(performanceFields.OpenRestyProxyBusyBuffersSize)
+ ) {
+ throw new Error('缓冲大小必须为整数或带 k/m/g 单位的值。');
+ }
- const runBusyAction = async (key: string, action: () => Promise) => {
- setBusyKey(key);
- setFeedback(null);
+ await saveOptionEntries(
+ [
+ [
+ 'OpenRestyProxyConnectTimeout',
+ performanceFields.OpenRestyProxyConnectTimeout.trim(),
+ ],
+ [
+ 'OpenRestyProxySendTimeout',
+ performanceFields.OpenRestyProxySendTimeout.trim(),
+ ],
+ [
+ 'OpenRestyProxyReadTimeout',
+ performanceFields.OpenRestyProxyReadTimeout.trim(),
+ ],
+ [
+ 'OpenRestyWebsocketEnabled',
+ String(performanceFields.OpenRestyWebsocketEnabled),
+ ],
+ [
+ 'OpenRestyProxyRequestBufferingEnabled',
+ String(performanceFields.OpenRestyProxyRequestBufferingEnabled),
+ ],
+ [
+ 'OpenRestyProxyBufferingEnabled',
+ String(performanceFields.OpenRestyProxyBufferingEnabled),
+ ],
+ [
+ 'OpenRestyProxyBuffers',
+ performanceFields.OpenRestyProxyBuffers.trim(),
+ ],
+ [
+ 'OpenRestyProxyBufferSize',
+ performanceFields.OpenRestyProxyBufferSize.trim(),
+ ],
+ [
+ 'OpenRestyProxyBusyBuffersSize',
+ performanceFields.OpenRestyProxyBusyBuffersSize.trim(),
+ ],
+ ],
+ 'OpenResty 反代缓冲参数已保存。',
+ );
+ });
+ };
- try {
- await action();
- } catch (error) {
- setFeedback({tone: 'danger', message: getErrorMessage(error)});
- } finally {
- setBusyKey(null);
+ 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 模板。');
+ }
+ }
- const saveOptionEntries = async (
- entries: Array<[string, string]>,
- successMessage: string,
- ) => {
- await updateOptions(entries.map(([key, value]) => ({key, value})));
+ 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 压缩与缓存参数已保存。',
+ );
+ });
+ };
- await Promise.all([
- queryClient.invalidateQueries({queryKey: settingsQueryKey}),
- queryClient.invalidateQueries({queryKey: previewQueryKey}),
- queryClient.invalidateQueries({queryKey: ['config-versions']}),
- ]);
- setFeedback({tone: 'success', message: successMessage});
- };
+ 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 之间。');
+ }
- const handleRuntimeSave = () => {
- void runBusyAction('performance-runtime', async () => {
- if (
- performanceFields.OpenRestyWorkerProcesses !== 'auto' &&
- !isPositiveInteger(performanceFields.OpenRestyWorkerProcesses)
- ) {
- throw new Error('worker_processes 必须为 auto 或大于 0 的整数。');
- }
+ await saveOptionEntries(
+ [
+ [
+ 'OpenRestyGzipEnabled',
+ String(performanceFields.OpenRestyGzipEnabled),
+ ],
+ [
+ 'OpenRestyGzipMinLength',
+ performanceFields.OpenRestyGzipMinLength.trim(),
+ ],
+ [
+ 'OpenRestyGzipCompLevel',
+ performanceFields.OpenRestyGzipCompLevel.trim(),
+ ],
+ ],
+ 'OpenResty 压缩参数已保存。',
+ );
+ });
+ };
- 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 的整数。`);
- }
- }
- if (!isSizeValue(performanceFields.OpenRestyClientMaxBodySize)) {
- throw new Error(
- 'client_max_body_size 必须为整数或带 k/m/g 单位的大小值。',
- );
- }
- if (
- !isProxyBuffersValue(
- performanceFields.OpenRestyLargeClientHeaderBuffers,
- )
- ) {
- throw new Error('large_client_header_buffers 格式必须类似 "4 16k"。');
- }
-
- await saveOptionEntries(
- [
- [
- 'OpenRestyWorkerProcesses',
- performanceFields.OpenRestyWorkerProcesses.trim(),
- ],
- [
- 'OpenRestyResolvers',
- performanceFields.OpenRestyResolvers.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(),
- ],
- [
- 'OpenRestyClientMaxBodySize',
- performanceFields.OpenRestyClientMaxBodySize.trim(),
- ],
- [
- 'OpenRestyLargeClientHeaderBuffers',
- performanceFields.OpenRestyLargeClientHeaderBuffers.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(),
- ],
- [
- 'OpenRestyWebsocketEnabled',
- String(performanceFields.OpenRestyWebsocketEnabled),
- ],
- [
- 'OpenRestyProxyRequestBufferingEnabled',
- String(performanceFields.OpenRestyProxyRequestBufferingEnabled),
- ],
- [
- '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 handleTemplateSave = () => {
+ void runBusyAction('performance-template', async () => {
+ await saveOptionEntries(
+ [['OpenRestyMainConfigTemplate', templateContent]],
+ 'OpenResty 主配置模板已保存。',
+ );
+ });
+ };
+ if (!isRoot) {
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,
- OpenRestyResolvers: event.target.value,
- }))
- }
- placeholder="留空表示不配置"
- />
-
-
-
- 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,
- OpenRestyClientMaxBodySize: event.target.value,
- }))
- }
- placeholder="64m"
- />
-
-
-
- setPerformanceFields((previous) => ({
- ...previous,
- OpenRestyLargeClientHeaderBuffers: event.target.value,
- }))
- }
- placeholder="4 16k"
- />
-
-
-
- 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,
- OpenRestyWebsocketEnabled: checked,
- }))
- }
- />
-
- setPerformanceFields((previous) => ({
- ...previous,
- OpenRestyProxyRequestBufferingEnabled: checked,
- }))
- }
- />
-
- 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,
- }))
- }
- />
-
-
- 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'
- ? '保存中...' : '保存'}
-
-
- }
- >
-
-
-
-
- setPerformanceFields((previous) => ({
- ...previous,
- OpenRestyCachePath: event.target.value,
- }))
- }
- placeholder="/var/cache/openresty/openflare"
- />
-
-
-
- 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)}
- spellCheck={false}
- className={`${templatePanelClassName} resize-none`}
- placeholder="请输入受控 nginx.conf 模板"
- />
-
-
-
-
-
-
- {preview.main_config}
-
-
-
-
-
- )}
-
+
);
+ }
+
+ if (optionsQuery.isLoading || previewQuery.isLoading) {
+ return ;
+ }
+
+ if (optionsQuery.isError) {
+ return (
+
+ );
+ }
+
+ if (previewQuery.isError) {
+ return (
+
+ );
+ }
+
+ const preview = previewQuery.data;
+ if (!preview) {
+ return (
+
+ );
+ }
+
+ 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,
+ OpenRestyResolvers: event.target.value,
+ }))
+ }
+ placeholder="留空表示不配置"
+ />
+
+
+
+ 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,
+ OpenRestyClientMaxBodySize: event.target.value,
+ }))
+ }
+ placeholder="64m"
+ />
+
+
+
+ setPerformanceFields((previous) => ({
+ ...previous,
+ OpenRestyLargeClientHeaderBuffers: event.target.value,
+ }))
+ }
+ placeholder="4 16k"
+ />
+
+
+
+ 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,
+ OpenRestyWebsocketEnabled: checked,
+ }))
+ }
+ />
+
+ setPerformanceFields((previous) => ({
+ ...previous,
+ OpenRestyProxyRequestBufferingEnabled: checked,
+ }))
+ }
+ />
+
+ 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,
+ }))
+ }
+ />
+
+
+ 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' ? '保存中...' : '保存'}
+
+
+ }
+ >
+
+
+
+
+ setPerformanceFields((previous) => ({
+ ...previous,
+ OpenRestyCachePath: event.target.value,
+ }))
+ }
+ placeholder="/var/cache/openresty/openflare"
+ />
+
+
+
+ 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)}
+ spellCheck={false}
+ className={`${templatePanelClassName} resize-none`}
+ placeholder="请输入受控 nginx.conf 模板"
+ />
+
+
+
+
+
+
+ {preview.main_config}
+
+
+
+
+
+ )}
+
+ );
}
diff --git a/openflare_server/web/features/proxy-routes/api/proxy-routes.ts b/openflare_server/web/features/proxy-routes/api/proxy-routes.ts
index a188f5e7..3fa7844d 100644
--- a/openflare_server/web/features/proxy-routes/api/proxy-routes.ts
+++ b/openflare_server/web/features/proxy-routes/api/proxy-routes.ts
@@ -1,12 +1,12 @@
-import { apiRequest } from '@/lib/api/client';
-
-import type {
- ManagedDomainMatchResult,
- ProxyRouteItem,
- ProxyRouteMutationPayload,
- TlsCertificateItem,
-} from '@/features/proxy-routes/types';
-
+import { apiRequest } from '@/lib/api/client';
+
+import type {
+ ManagedDomainMatchResult,
+ ProxyRouteItem,
+ ProxyRouteMutationPayload,
+ TlsCertificateItem,
+} from '@/features/proxy-routes/types';
+
export function getProxyRoutes() {
return apiRequest('/proxy-routes/');
}
@@ -17,12 +17,15 @@ export function getProxyRoute(id: number) {
export function createProxyRoute(payload: ProxyRouteMutationPayload) {
return apiRequest('/proxy-routes/', {
- method: 'POST',
- body: JSON.stringify(payload),
- });
-}
-
-export function updateProxyRoute(id: number, payload: ProxyRouteMutationPayload) {
+ method: 'POST',
+ body: JSON.stringify(payload),
+ });
+}
+
+export function updateProxyRoute(
+ id: number,
+ payload: ProxyRouteMutationPayload,
+) {
return apiRequest(`/proxy-routes/${id}/update`, {
method: 'POST',
body: JSON.stringify(payload),
@@ -34,12 +37,14 @@ export function deleteProxyRoute(id: number) {
method: 'POST',
});
}
-
-export function getTlsCertificates() {
- return apiRequest('/tls-certificates/');
-}
-
-export function matchManagedDomainCertificate(domain: string) {
- const searchParams = new URLSearchParams({ domain });
- return apiRequest(`/managed-domains/match?${searchParams.toString()}`);
-}
+
+export function getTlsCertificates() {
+ return apiRequest('/tls-certificates/');
+}
+
+export function matchManagedDomainCertificate(domain: string) {
+ const searchParams = new URLSearchParams({ domain });
+ return apiRequest(
+ `/managed-domains/match?${searchParams.toString()}`,
+ );
+}
diff --git a/openflare_server/web/features/proxy-routes/components/domain-list-input.tsx b/openflare_server/web/features/proxy-routes/components/domain-list-input.tsx
index 47a6df1d..77ba94f2 100644
--- a/openflare_server/web/features/proxy-routes/components/domain-list-input.tsx
+++ b/openflare_server/web/features/proxy-routes/components/domain-list-input.tsx
@@ -100,9 +100,7 @@ export function buildDomainRowsFromRoute(
if (domainCertIDs.length === domains.length) {
return domains.map((domain, index) => ({
domain,
- certificateId: domainCertIDs[index]
- ? String(domainCertIDs[index])
- : '',
+ certificateId: domainCertIDs[index] ? String(domainCertIDs[index]) : '',
}));
}
@@ -185,7 +183,9 @@ export function DomainListInput({
value={row.domain}
list={`${listId}-${index}`}
aria-label={`域名 ${index + 1}`}
- placeholder={index === 0 ? domainPlaceholder : 'www.example.com'}
+ placeholder={
+ index === 0 ? domainPlaceholder : 'www.example.com'
+ }
onBlur={onBlur}
onChange={(event) => {
const nextRows = safeRows.slice();
diff --git a/openflare_server/web/features/proxy-routes/components/proxy-route-config-page.tsx b/openflare_server/web/features/proxy-routes/components/proxy-route-config-page.tsx
index 5ea0ee63..6041a28c 100644
--- a/openflare_server/web/features/proxy-routes/components/proxy-route-config-page.tsx
+++ b/openflare_server/web/features/proxy-routes/components/proxy-route-config-page.tsx
@@ -123,7 +123,10 @@ const rateLimitSchema = z
limit_rate: z.string(),
})
.superRefine((value, context) => {
- for (const field of ['limit_conn_per_server', 'limit_conn_per_ip'] as const) {
+ for (const field of [
+ 'limit_conn_per_server',
+ 'limit_conn_per_ip',
+ ] as const) {
const rawValue = value[field].trim();
if (!rawValue) {
continue;
@@ -369,7 +372,9 @@ function DomainSettingsSection({
-
+
@@ -538,7 +546,9 @@ function ReverseProxySection({
onSubmit={form.handleSubmit((values) => {
const { urls } = parseOriginUrls(values.origin_urls_text);
const primaryOrigin = parseOriginUrl(urls[0]);
- const { headers } = parseCustomHeadersText(values.custom_headers_text);
+ const { headers } = parseCustomHeadersText(
+ values.custom_headers_text,
+ );
onSave(
buildPayloadFromRoute(route, {
@@ -565,7 +575,9 @@ function ReverseProxySection({
@@ -593,7 +605,10 @@ function ReverseProxySection({
/>
-
+
{
form.reset({
cache_enabled: route.cache_enabled,
- cache_policy: (route.cache_policy || 'url') as CacheValues['cache_policy'],
+ cache_policy: (route.cache_policy ||
+ 'url') as CacheValues['cache_policy'],
cache_rules_text: route.cache_rule_list.join('\n'),
});
}, [form, route]);
@@ -650,7 +667,9 @@ function CacheSection({
cache_enabled: values.cache_enabled,
cache_policy: values.cache_enabled ? values.cache_policy : 'url',
cache_rules:
- values.cache_enabled && values.cache_policy !== 'url' ? rules : [],
+ values.cache_enabled && values.cache_policy !== 'url'
+ ? rules
+ : [],
}),
{ message: '缓存设置已保存。' },
);
@@ -770,7 +789,13 @@ type PowValues = z.infer;
function buildPowListFromConfig(
list:
- | { ips?: string[]; ip_cidrs?: string[]; paths?: string[]; path_regexes?: string[]; user_agents?: string[] }
+ | {
+ ips?: string[];
+ ip_cidrs?: string[];
+ paths?: string[];
+ path_regexes?: string[];
+ user_agents?: string[];
+ }
| undefined,
): PowListValues {
return {
@@ -923,7 +948,7 @@ function PowSection({
|