mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-30 22:26:38 +08:00
feat: split openresty settings into performance page
This commit is contained in:
@@ -85,6 +85,39 @@ var OpenRestyCacheKeyTemplate = "$scheme$proxy_host$request_uri"
|
||||
var OpenRestyCacheLockEnabled = true
|
||||
var OpenRestyCacheLockTimeout = "5s"
|
||||
var OpenRestyCacheUseStale = "error timeout updating http_500 http_502 http_503 http_504"
|
||||
var OpenRestyMainConfigTemplate = `# This file is generated by ATSFlare. Do not edit manually.
|
||||
worker_processes {{OpenRestyWorkerProcesses}};
|
||||
worker_rlimit_nofile {{OpenRestyWorkerRlimitNofile}};
|
||||
pid logs/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections {{OpenRestyWorkerConnections}};
|
||||
{{OpenRestyEventsUseDirective}}{{OpenRestyEventsMultiAcceptDirective}}}
|
||||
|
||||
http {
|
||||
include mime.types;
|
||||
default_type application/octet-stream;
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout {{OpenRestyKeepaliveTimeout}};
|
||||
keepalive_requests {{OpenRestyKeepaliveRequests}};
|
||||
client_header_timeout {{OpenRestyClientHeaderTimeout}};
|
||||
client_body_timeout {{OpenRestyClientBodyTimeout}};
|
||||
send_timeout {{OpenRestySendTimeout}};
|
||||
proxy_connect_timeout {{OpenRestyProxyConnectTimeout}};
|
||||
proxy_send_timeout {{OpenRestyProxySendTimeout}};
|
||||
proxy_read_timeout {{OpenRestyProxyReadTimeout}};
|
||||
proxy_buffering {{OpenRestyProxyBuffering}};
|
||||
proxy_buffers {{OpenRestyProxyBuffers}};
|
||||
proxy_buffer_size {{OpenRestyProxyBufferSize}};
|
||||
proxy_busy_buffers_size {{OpenRestyProxyBusyBuffersSize}};
|
||||
gzip {{OpenRestyGzip}};
|
||||
gzip_min_length {{OpenRestyGzipMinLength}};
|
||||
gzip_comp_level {{OpenRestyGzipCompLevel}};
|
||||
{{OpenRestyCacheBlock}} include {{OpenRestyRouteConfigInclude}};
|
||||
}
|
||||
`
|
||||
|
||||
const (
|
||||
RoleGuestUser = 0
|
||||
|
||||
@@ -3,6 +3,7 @@ package controller
|
||||
import (
|
||||
"atsflare/common"
|
||||
"atsflare/model"
|
||||
"atsflare/service"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -154,6 +155,8 @@ func validateOpenRestyOption(key string, value string) error {
|
||||
}
|
||||
}
|
||||
return nil
|
||||
case "OpenRestyMainConfigTemplate":
|
||||
return service.ValidateOpenRestyMainConfigTemplate(value)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -84,6 +84,7 @@ func InitOptionMap() {
|
||||
common.OptionMap["OpenRestyCacheLockEnabled"] = strconv.FormatBool(common.OpenRestyCacheLockEnabled)
|
||||
common.OptionMap["OpenRestyCacheLockTimeout"] = common.OpenRestyCacheLockTimeout
|
||||
common.OptionMap["OpenRestyCacheUseStale"] = common.OpenRestyCacheUseStale
|
||||
common.OptionMap["OpenRestyMainConfigTemplate"] = common.OpenRestyMainConfigTemplate
|
||||
common.OptionMap["GlobalApiRateLimitNum"] = strconv.Itoa(common.GlobalApiRateLimitNum)
|
||||
common.OptionMap["GlobalApiRateLimitDuration"] = strconv.FormatInt(common.GlobalApiRateLimitDuration, 10)
|
||||
common.OptionMap["GlobalWebRateLimitNum"] = strconv.Itoa(common.GlobalWebRateLimitNum)
|
||||
@@ -309,6 +310,10 @@ func updateOptionMap(key string, value string) {
|
||||
if strings.TrimSpace(value) != "" {
|
||||
common.OpenRestyCacheUseStale = value
|
||||
}
|
||||
case "OpenRestyMainConfigTemplate":
|
||||
if strings.TrimSpace(value) != "" {
|
||||
common.OpenRestyMainConfigTemplate = value
|
||||
}
|
||||
case "GlobalApiRateLimitNum":
|
||||
if v, err := strconv.Atoi(value); err == nil && v > 0 {
|
||||
common.GlobalApiRateLimitNum = v
|
||||
|
||||
@@ -116,6 +116,31 @@ const (
|
||||
nginxRouteConfigPlaceholder = "__ATSF_ROUTE_CONFIG__"
|
||||
)
|
||||
|
||||
var requiredMainConfigTemplatePlaceholders = []string{
|
||||
"{{OpenRestyWorkerProcesses}}",
|
||||
"{{OpenRestyWorkerConnections}}",
|
||||
"{{OpenRestyWorkerRlimitNofile}}",
|
||||
"{{OpenRestyEventsUseDirective}}",
|
||||
"{{OpenRestyEventsMultiAcceptDirective}}",
|
||||
"{{OpenRestyKeepaliveTimeout}}",
|
||||
"{{OpenRestyKeepaliveRequests}}",
|
||||
"{{OpenRestyClientHeaderTimeout}}",
|
||||
"{{OpenRestyClientBodyTimeout}}",
|
||||
"{{OpenRestySendTimeout}}",
|
||||
"{{OpenRestyProxyConnectTimeout}}",
|
||||
"{{OpenRestyProxySendTimeout}}",
|
||||
"{{OpenRestyProxyReadTimeout}}",
|
||||
"{{OpenRestyProxyBuffering}}",
|
||||
"{{OpenRestyProxyBuffers}}",
|
||||
"{{OpenRestyProxyBufferSize}}",
|
||||
"{{OpenRestyProxyBusyBuffersSize}}",
|
||||
"{{OpenRestyGzip}}",
|
||||
"{{OpenRestyGzipMinLength}}",
|
||||
"{{OpenRestyGzipCompLevel}}",
|
||||
"{{OpenRestyCacheBlock}}",
|
||||
"{{OpenRestyRouteConfigInclude}}",
|
||||
}
|
||||
|
||||
func ListConfigVersions() ([]*model.ConfigVersion, error) {
|
||||
return model.ListConfigVersions()
|
||||
}
|
||||
@@ -569,51 +594,77 @@ func renderRouteConfig(routes []*model.ProxyRoute) (string, []SupportFile, error
|
||||
}
|
||||
|
||||
func renderMainConfig(cfg openRestyConfigSnapshot) string {
|
||||
var builder strings.Builder
|
||||
builder.WriteString("# This file is generated by ATSFlare. Do not edit manually.\n")
|
||||
builder.WriteString(fmt.Sprintf("worker_processes %s;\n", cfg.WorkerProcesses))
|
||||
builder.WriteString(fmt.Sprintf("worker_rlimit_nofile %d;\n", cfg.WorkerRlimitNofile))
|
||||
builder.WriteString("pid logs/nginx.pid;\n\n")
|
||||
builder.WriteString("events {\n")
|
||||
builder.WriteString(fmt.Sprintf(" worker_connections %d;\n", cfg.WorkerConnections))
|
||||
if strings.TrimSpace(cfg.EventsUse) != "" {
|
||||
builder.WriteString(fmt.Sprintf(" use %s;\n", cfg.EventsUse))
|
||||
templateText := common.OpenRestyMainConfigTemplate
|
||||
if strings.TrimSpace(templateText) == "" {
|
||||
templateText = defaultOpenRestyMainConfigTemplate()
|
||||
}
|
||||
if cfg.EventsMultiAcceptEnabled {
|
||||
builder.WriteString(" multi_accept on;\n")
|
||||
return renderMainConfigTemplate(templateText, cfg)
|
||||
}
|
||||
|
||||
func ValidateOpenRestyMainConfigTemplate(templateText string) error {
|
||||
trimmed := strings.TrimSpace(templateText)
|
||||
if trimmed == "" {
|
||||
return errors.New("OpenRestyMainConfigTemplate 不能为空")
|
||||
}
|
||||
builder.WriteString("}\n\n")
|
||||
builder.WriteString("http {\n")
|
||||
builder.WriteString(" include mime.types;\n")
|
||||
builder.WriteString(" default_type application/octet-stream;\n")
|
||||
builder.WriteString(" sendfile on;\n")
|
||||
builder.WriteString(" tcp_nopush on;\n")
|
||||
builder.WriteString(" tcp_nodelay on;\n")
|
||||
builder.WriteString(fmt.Sprintf(" keepalive_timeout %d;\n", cfg.KeepaliveTimeout))
|
||||
builder.WriteString(fmt.Sprintf(" keepalive_requests %d;\n", cfg.KeepaliveRequests))
|
||||
builder.WriteString(fmt.Sprintf(" client_header_timeout %d;\n", cfg.ClientHeaderTimeout))
|
||||
builder.WriteString(fmt.Sprintf(" client_body_timeout %d;\n", cfg.ClientBodyTimeout))
|
||||
builder.WriteString(fmt.Sprintf(" send_timeout %d;\n", cfg.SendTimeout))
|
||||
builder.WriteString(fmt.Sprintf(" proxy_connect_timeout %d;\n", cfg.ProxyConnectTimeout))
|
||||
builder.WriteString(fmt.Sprintf(" proxy_send_timeout %d;\n", cfg.ProxySendTimeout))
|
||||
builder.WriteString(fmt.Sprintf(" proxy_read_timeout %d;\n", cfg.ProxyReadTimeout))
|
||||
builder.WriteString(fmt.Sprintf(" proxy_buffering %s;\n", onOff(cfg.ProxyBufferingEnabled)))
|
||||
builder.WriteString(fmt.Sprintf(" proxy_buffers %s;\n", cfg.ProxyBuffers))
|
||||
builder.WriteString(fmt.Sprintf(" proxy_buffer_size %s;\n", cfg.ProxyBufferSize))
|
||||
builder.WriteString(fmt.Sprintf(" proxy_busy_buffers_size %s;\n", cfg.ProxyBusyBuffersSize))
|
||||
builder.WriteString(fmt.Sprintf(" gzip %s;\n", onOff(cfg.GzipEnabled)))
|
||||
builder.WriteString(fmt.Sprintf(" gzip_min_length %d;\n", cfg.GzipMinLength))
|
||||
builder.WriteString(fmt.Sprintf(" gzip_comp_level %d;\n", cfg.GzipCompLevel))
|
||||
if cfg.CacheEnabled {
|
||||
builder.WriteString(fmt.Sprintf(" proxy_cache_path %s levels=%s keys_zone=atsflare_cache:10m inactive=%s max_size=%s;\n", cfg.CachePath, cfg.CacheLevels, cfg.CacheInactive, cfg.CacheMaxSize))
|
||||
builder.WriteString(fmt.Sprintf(" proxy_cache_key \"%s\";\n", cfg.CacheKeyTemplate))
|
||||
builder.WriteString(fmt.Sprintf(" proxy_cache_lock %s;\n", onOff(cfg.CacheLockEnabled)))
|
||||
builder.WriteString(fmt.Sprintf(" proxy_cache_lock_timeout %s;\n", cfg.CacheLockTimeout))
|
||||
builder.WriteString(fmt.Sprintf(" proxy_cache_use_stale %s;\n", cfg.CacheUseStale))
|
||||
for _, placeholder := range requiredMainConfigTemplatePlaceholders {
|
||||
if !strings.Contains(trimmed, placeholder) {
|
||||
return fmt.Errorf("OpenRestyMainConfigTemplate 必须保留占位符 %s", placeholder)
|
||||
}
|
||||
}
|
||||
builder.WriteString(fmt.Sprintf(" include %s;\n", nginxRouteConfigPlaceholder))
|
||||
builder.WriteString("}\n")
|
||||
return builder.String()
|
||||
return nil
|
||||
}
|
||||
|
||||
func defaultOpenRestyMainConfigTemplate() string {
|
||||
return common.OpenRestyMainConfigTemplate
|
||||
}
|
||||
|
||||
func renderMainConfigTemplate(templateText string, cfg openRestyConfigSnapshot) string {
|
||||
replacer := strings.NewReplacer(
|
||||
"{{OpenRestyWorkerProcesses}}", cfg.WorkerProcesses,
|
||||
"{{OpenRestyWorkerConnections}}", fmt.Sprintf("%d", cfg.WorkerConnections),
|
||||
"{{OpenRestyWorkerRlimitNofile}}", fmt.Sprintf("%d", cfg.WorkerRlimitNofile),
|
||||
"{{OpenRestyEventsUseDirective}}", renderTemplateDirective(cfg.EventsUse != "", fmt.Sprintf("use %s;", cfg.EventsUse)),
|
||||
"{{OpenRestyEventsMultiAcceptDirective}}", renderTemplateDirective(cfg.EventsMultiAcceptEnabled, "multi_accept on;"),
|
||||
"{{OpenRestyKeepaliveTimeout}}", fmt.Sprintf("%d", cfg.KeepaliveTimeout),
|
||||
"{{OpenRestyKeepaliveRequests}}", fmt.Sprintf("%d", cfg.KeepaliveRequests),
|
||||
"{{OpenRestyClientHeaderTimeout}}", fmt.Sprintf("%d", cfg.ClientHeaderTimeout),
|
||||
"{{OpenRestyClientBodyTimeout}}", fmt.Sprintf("%d", cfg.ClientBodyTimeout),
|
||||
"{{OpenRestySendTimeout}}", fmt.Sprintf("%d", cfg.SendTimeout),
|
||||
"{{OpenRestyProxyConnectTimeout}}", fmt.Sprintf("%d", cfg.ProxyConnectTimeout),
|
||||
"{{OpenRestyProxySendTimeout}}", fmt.Sprintf("%d", cfg.ProxySendTimeout),
|
||||
"{{OpenRestyProxyReadTimeout}}", fmt.Sprintf("%d", cfg.ProxyReadTimeout),
|
||||
"{{OpenRestyProxyBuffering}}", onOff(cfg.ProxyBufferingEnabled),
|
||||
"{{OpenRestyProxyBuffers}}", cfg.ProxyBuffers,
|
||||
"{{OpenRestyProxyBufferSize}}", cfg.ProxyBufferSize,
|
||||
"{{OpenRestyProxyBusyBuffersSize}}", cfg.ProxyBusyBuffersSize,
|
||||
"{{OpenRestyGzip}}", onOff(cfg.GzipEnabled),
|
||||
"{{OpenRestyGzipMinLength}}", fmt.Sprintf("%d", cfg.GzipMinLength),
|
||||
"{{OpenRestyGzipCompLevel}}", fmt.Sprintf("%d", cfg.GzipCompLevel),
|
||||
"{{OpenRestyCacheBlock}}", renderOpenRestyCacheTemplateBlock(cfg),
|
||||
"{{OpenRestyRouteConfigInclude}}", nginxRouteConfigPlaceholder,
|
||||
)
|
||||
return replacer.Replace(templateText)
|
||||
}
|
||||
|
||||
func renderTemplateDirective(enabled bool, statement string) string {
|
||||
if !enabled {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf(" %s\n", statement)
|
||||
}
|
||||
|
||||
func renderOpenRestyCacheTemplateBlock(cfg openRestyConfigSnapshot) string {
|
||||
if !cfg.CacheEnabled {
|
||||
return ""
|
||||
}
|
||||
return strings.Join([]string{
|
||||
fmt.Sprintf(" proxy_cache_path %s levels=%s keys_zone=atsflare_cache:10m inactive=%s max_size=%s;", cfg.CachePath, cfg.CacheLevels, cfg.CacheInactive, cfg.CacheMaxSize),
|
||||
fmt.Sprintf(" proxy_cache_key \"%s\";", cfg.CacheKeyTemplate),
|
||||
fmt.Sprintf(" proxy_cache_lock %s;", onOff(cfg.CacheLockEnabled)),
|
||||
fmt.Sprintf(" proxy_cache_lock_timeout %s;", cfg.CacheLockTimeout),
|
||||
fmt.Sprintf(" proxy_cache_use_stale %s;", cfg.CacheUseStale),
|
||||
"",
|
||||
}, "\n")
|
||||
}
|
||||
|
||||
func onOff(value bool) string {
|
||||
|
||||
@@ -251,6 +251,45 @@ func TestCreateTLSCertificateRejectsInvalidPEM(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenRestyMainConfigTemplateRenderAndValidate(t *testing.T) {
|
||||
setupServiceTestDB(t)
|
||||
|
||||
customTemplate := strings.ReplaceAll(
|
||||
common.OpenRestyMainConfigTemplate,
|
||||
"pid logs/nginx.pid;",
|
||||
"pid logs/nginx.pid;\nworker_shutdown_timeout 10s;",
|
||||
)
|
||||
if err := ValidateOpenRestyMainConfigTemplate(customTemplate); err != nil {
|
||||
t.Fatalf("ValidateOpenRestyMainConfigTemplate failed: %v", err)
|
||||
}
|
||||
if err := model.UpdateOption("OpenRestyMainConfigTemplate", customTemplate); err != nil {
|
||||
t.Fatalf("UpdateOption OpenRestyMainConfigTemplate failed: %v", err)
|
||||
}
|
||||
|
||||
preview, err := PreviewConfigVersion()
|
||||
if err != nil {
|
||||
t.Fatalf("PreviewConfigVersion failed: %v", err)
|
||||
}
|
||||
if !strings.Contains(preview.MainConfig, "worker_shutdown_timeout 10s;") {
|
||||
t.Fatal("expected preview main config to include custom template content")
|
||||
}
|
||||
if strings.Contains(preview.MainConfig, "{{OpenRestyWorkerProcesses}}") {
|
||||
t.Fatal("expected preview main config placeholders to be rendered")
|
||||
}
|
||||
if !strings.Contains(preview.MainConfig, "include __ATSF_ROUTE_CONFIG__;") {
|
||||
t.Fatal("expected preview main config to preserve managed route include")
|
||||
}
|
||||
|
||||
invalidTemplate := strings.ReplaceAll(
|
||||
common.OpenRestyMainConfigTemplate,
|
||||
"{{OpenRestyRouteConfigInclude}}",
|
||||
"",
|
||||
)
|
||||
if err := ValidateOpenRestyMainConfigTemplate(invalidTemplate); err == nil {
|
||||
t.Fatal("expected template without managed route placeholder to fail validation")
|
||||
}
|
||||
}
|
||||
|
||||
func setupServiceTestDB(t *testing.T) {
|
||||
t.Helper()
|
||||
common.SQLitePath = filepath.Join(t.TempDir(), "service.db")
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { PerformancePage } from '@/features/performance/components/performance-page';
|
||||
|
||||
export default function PerformanceRoute() {
|
||||
return <PerformancePage />;
|
||||
}
|
||||
@@ -1,252 +1,270 @@
|
||||
'use client';
|
||||
|
||||
import {useEffect} from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import Link from 'next/link';
|
||||
import {usePathname} from 'next/navigation';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
import {dashboardNavigation} from '@/lib/constants/navigation';
|
||||
import {cn} from '@/lib/utils/cn';
|
||||
import {isNavigationItemActive} from '@/lib/utils/navigation';
|
||||
import {useAppShellStore} from '@/store/app-shell';
|
||||
import type {NavigationIconKey, NavigationItem} from '@/types/navigation';
|
||||
import { dashboardNavigation } from '@/lib/constants/navigation';
|
||||
import { cn } from '@/lib/utils/cn';
|
||||
import { isNavigationItemActive } from '@/lib/utils/navigation';
|
||||
import { useAppShellStore } from '@/store/app-shell';
|
||||
import type { NavigationIconKey, NavigationItem } from '@/types/navigation';
|
||||
|
||||
function SidebarIcon({icon}: { icon: NavigationIconKey }) {
|
||||
const commonProps = {
|
||||
className: 'h-[18px] w-[18px]',
|
||||
fill: 'none',
|
||||
stroke: 'currentColor',
|
||||
strokeWidth: 1.8,
|
||||
strokeLinecap: 'round' as const,
|
||||
strokeLinejoin: 'round' as const,
|
||||
viewBox: '0 0 24 24',
|
||||
};
|
||||
function SidebarIcon({ icon }: { icon: NavigationIconKey }) {
|
||||
const commonProps = {
|
||||
className: 'h-[18px] w-[18px]',
|
||||
fill: 'none',
|
||||
stroke: 'currentColor',
|
||||
strokeWidth: 1.8,
|
||||
strokeLinecap: 'round' as const,
|
||||
strokeLinejoin: 'round' as const,
|
||||
viewBox: '0 0 24 24',
|
||||
};
|
||||
|
||||
switch (icon) {
|
||||
case 'home':
|
||||
return (
|
||||
<svg {...commonProps}>
|
||||
<path d='M3 10.5 12 3l9 7.5'/>
|
||||
<path d='M5.5 9.5V21h13V9.5'/>
|
||||
<path d='M9.5 21v-6h5v6'/>
|
||||
</svg>
|
||||
);
|
||||
case 'node':
|
||||
return (
|
||||
<svg {...commonProps}>
|
||||
<rect x='4' y='4' width='6' height='6' rx='1.5'/>
|
||||
<rect x='14' y='4' width='6' height='6' rx='1.5'/>
|
||||
<rect x='9' y='14' width='6' height='6' rx='1.5'/>
|
||||
<path d='M10 7h4M12 10v4'/>
|
||||
</svg>
|
||||
);
|
||||
case 'website':
|
||||
return (
|
||||
<svg {...commonProps}>
|
||||
<circle cx='12' cy='12' r='8.5'/>
|
||||
<path d='M3.5 12h17'/>
|
||||
<path
|
||||
d='M12 3.5c2.4 2.2 3.8 5.2 3.8 8.5S14.4 18.3 12 20.5C9.6 18.3 8.2 15.3 8.2 12S9.6 5.7 12 3.5Z'/>
|
||||
</svg>
|
||||
);
|
||||
case 'domain':
|
||||
return (
|
||||
<svg {...commonProps}>
|
||||
<path d='M4 8.5h16'/>
|
||||
<path d='M4 15.5h16'/>
|
||||
<rect x='3.5' y='5' width='17' height='14' rx='3'/>
|
||||
</svg>
|
||||
);
|
||||
case 'certificate':
|
||||
return (
|
||||
<svg {...commonProps}>
|
||||
<path d='M7 4.5h10l2 2V14a7 7 0 1 1-14 0V4.5Z'/>
|
||||
<path d='M9 10h6'/>
|
||||
<path d='M10 14h4'/>
|
||||
</svg>
|
||||
);
|
||||
case 'proxy':
|
||||
return (
|
||||
<svg {...commonProps}>
|
||||
<path d='M7 7h10'/>
|
||||
<path d='M7 12h10'/>
|
||||
<path d='M7 17h6'/>
|
||||
<path d='m14 15 3 2-3 2'/>
|
||||
</svg>
|
||||
);
|
||||
case 'release':
|
||||
return (
|
||||
<svg {...commonProps}>
|
||||
<path d='M12 4v10'/>
|
||||
<path d='m8.5 7.5 3.5-3.5 3.5 3.5'/>
|
||||
<path d='M5 19.5h14'/>
|
||||
</svg>
|
||||
);
|
||||
case 'user':
|
||||
return (
|
||||
<svg {...commonProps}>
|
||||
<circle cx='12' cy='8' r='3.25'/>
|
||||
<path d='M5 19.5c1.7-3 4.1-4.5 7-4.5s5.3 1.5 7 4.5'/>
|
||||
</svg>
|
||||
);
|
||||
case 'setting':
|
||||
return (
|
||||
<svg {...commonProps}>
|
||||
<circle cx='12' cy='12' r='3.5'/>
|
||||
<path
|
||||
d='M12 3.5v2.2M12 18.3v2.2M20.5 12h-2.2M5.7 12H3.5M18.1 5.9l-1.6 1.6M7.5 16.5l-1.6 1.6M18.1 18.1l-1.6-1.6M7.5 7.5 5.9 5.9'/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
switch (icon) {
|
||||
case 'home':
|
||||
return (
|
||||
<svg {...commonProps}>
|
||||
<path d="M3 10.5 12 3l9 7.5" />
|
||||
<path d="M5.5 9.5V21h13V9.5" />
|
||||
<path d="M9.5 21v-6h5v6" />
|
||||
</svg>
|
||||
);
|
||||
case 'node':
|
||||
return (
|
||||
<svg {...commonProps}>
|
||||
<rect x="4" y="4" width="6" height="6" rx="1.5" />
|
||||
<rect x="14" y="4" width="6" height="6" rx="1.5" />
|
||||
<rect x="9" y="14" width="6" height="6" rx="1.5" />
|
||||
<path d="M10 7h4M12 10v4" />
|
||||
</svg>
|
||||
);
|
||||
case 'website':
|
||||
return (
|
||||
<svg {...commonProps}>
|
||||
<circle cx="12" cy="12" r="8.5" />
|
||||
<path d="M3.5 12h17" />
|
||||
<path d="M12 3.5c2.4 2.2 3.8 5.2 3.8 8.5S14.4 18.3 12 20.5C9.6 18.3 8.2 15.3 8.2 12S9.6 5.7 12 3.5Z" />
|
||||
</svg>
|
||||
);
|
||||
case 'domain':
|
||||
return (
|
||||
<svg {...commonProps}>
|
||||
<path d="M4 8.5h16" />
|
||||
<path d="M4 15.5h16" />
|
||||
<rect x="3.5" y="5" width="17" height="14" rx="3" />
|
||||
</svg>
|
||||
);
|
||||
case 'certificate':
|
||||
return (
|
||||
<svg {...commonProps}>
|
||||
<path d="M7 4.5h10l2 2V14a7 7 0 1 1-14 0V4.5Z" />
|
||||
<path d="M9 10h6" />
|
||||
<path d="M10 14h4" />
|
||||
</svg>
|
||||
);
|
||||
case 'proxy':
|
||||
return (
|
||||
<svg {...commonProps}>
|
||||
<path d="M7 7h10" />
|
||||
<path d="M7 12h10" />
|
||||
<path d="M7 17h6" />
|
||||
<path d="m14 15 3 2-3 2" />
|
||||
</svg>
|
||||
);
|
||||
case 'release':
|
||||
return (
|
||||
<svg {...commonProps}>
|
||||
<path d="M12 4v10" />
|
||||
<path d="m8.5 7.5 3.5-3.5 3.5 3.5" />
|
||||
<path d="M5 19.5h14" />
|
||||
</svg>
|
||||
);
|
||||
case 'performance':
|
||||
return (
|
||||
<svg {...commonProps}>
|
||||
<path d="M4.5 19.5h15" />
|
||||
<path d="M7 16l3-4 3 2 4-6" />
|
||||
<path d="m15.5 8 1.5 0 0 1.5" />
|
||||
</svg>
|
||||
);
|
||||
case 'user':
|
||||
return (
|
||||
<svg {...commonProps}>
|
||||
<circle cx="12" cy="8" r="3.25" />
|
||||
<path d="M5 19.5c1.7-3 4.1-4.5 7-4.5s5.3 1.5 7 4.5" />
|
||||
</svg>
|
||||
);
|
||||
case 'setting':
|
||||
return (
|
||||
<svg {...commonProps}>
|
||||
<circle cx="12" cy="12" r="3.5" />
|
||||
<path d="M12 3.5v2.2M12 18.3v2.2M20.5 12h-2.2M5.7 12H3.5M18.1 5.9l-1.6 1.6M7.5 16.5l-1.6 1.6M18.1 18.1l-1.6-1.6M7.5 7.5 5.9 5.9" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function SidebarNavItem({
|
||||
item,
|
||||
currentPath,
|
||||
isSidebarCollapsed,
|
||||
forceExpanded,
|
||||
onNavigate,
|
||||
depth = 0,
|
||||
}: {
|
||||
item: NavigationItem;
|
||||
currentPath: string;
|
||||
isSidebarCollapsed: boolean;
|
||||
forceExpanded?: boolean;
|
||||
onNavigate?: () => void;
|
||||
depth?: number;
|
||||
item,
|
||||
currentPath,
|
||||
isSidebarCollapsed,
|
||||
forceExpanded,
|
||||
onNavigate,
|
||||
depth = 0,
|
||||
}: {
|
||||
item: NavigationItem;
|
||||
currentPath: string;
|
||||
isSidebarCollapsed: boolean;
|
||||
forceExpanded?: boolean;
|
||||
onNavigate?: () => void;
|
||||
depth?: number;
|
||||
}) {
|
||||
const active = isNavigationItemActive(currentPath, item);
|
||||
const hasChildren = Boolean(item.children?.length);
|
||||
const showLabel = forceExpanded || !isSidebarCollapsed;
|
||||
const active = isNavigationItemActive(currentPath, item);
|
||||
const hasChildren = Boolean(item.children?.length);
|
||||
const showLabel = forceExpanded || !isSidebarCollapsed;
|
||||
|
||||
return (
|
||||
<div className='space-y-2'>
|
||||
<Link
|
||||
href={item.href}
|
||||
onClick={onNavigate}
|
||||
className={cn(
|
||||
'flex min-h-[50px] items-center gap-3 rounded-2xl border px-3 py-2.5 transition-colors',
|
||||
depth > 0 && 'ml-3 rounded-xl',
|
||||
active
|
||||
? 'border-[var(--border-strong)] bg-[var(--accent-soft)] text-[var(--foreground-primary)]'
|
||||
: 'border-transparent text-[var(--foreground-secondary)] hover:border-[var(--border-default)] hover:bg-[var(--surface-muted)] hover:text-[var(--foreground-primary)]',
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className='flex h-8 w-8 shrink-0 items-center justify-center rounded-xl bg-[var(--control-background)] text-[var(--foreground-primary)]'>
|
||||
<SidebarIcon icon={item.icon}/>
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<Link
|
||||
href={item.href}
|
||||
onClick={onNavigate}
|
||||
className={cn(
|
||||
'flex min-h-[50px] items-center gap-3 rounded-2xl border px-3 py-2.5 transition-colors',
|
||||
depth > 0 && 'ml-3 rounded-xl',
|
||||
active
|
||||
? 'border-[var(--border-strong)] bg-[var(--accent-soft)] text-[var(--foreground-primary)]'
|
||||
: 'border-transparent text-[var(--foreground-secondary)] hover:border-[var(--border-default)] hover:bg-[var(--surface-muted)] hover:text-[var(--foreground-primary)]',
|
||||
)}
|
||||
>
|
||||
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-xl bg-[var(--control-background)] text-[var(--foreground-primary)]">
|
||||
<SidebarIcon icon={item.icon} />
|
||||
</span>
|
||||
{showLabel ? <span className='min-w-0 flex-1 text-sm font-medium'>{item.label}</span> : null}
|
||||
</Link>
|
||||
{showLabel && hasChildren ? (
|
||||
<div className='space-y-2'>
|
||||
{item.children?.map((child) => (
|
||||
<SidebarNavItem
|
||||
key={child.href}
|
||||
item={child}
|
||||
currentPath={currentPath}
|
||||
isSidebarCollapsed={isSidebarCollapsed}
|
||||
forceExpanded={forceExpanded}
|
||||
onNavigate={onNavigate}
|
||||
depth={depth + 1}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
{showLabel ? (
|
||||
<span className="min-w-0 flex-1 text-sm font-medium">
|
||||
{item.label}
|
||||
</span>
|
||||
) : null}
|
||||
</Link>
|
||||
{showLabel && hasChildren ? (
|
||||
<div className="space-y-2">
|
||||
{item.children?.map((child) => (
|
||||
<SidebarNavItem
|
||||
key={child.href}
|
||||
item={child}
|
||||
currentPath={currentPath}
|
||||
isSidebarCollapsed={isSidebarCollapsed}
|
||||
forceExpanded={forceExpanded}
|
||||
onNavigate={onNavigate}
|
||||
depth={depth + 1}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarContent({
|
||||
currentPath,
|
||||
isSidebarCollapsed,
|
||||
forceExpanded = false,
|
||||
onNavigate,
|
||||
}: {
|
||||
currentPath: string;
|
||||
isSidebarCollapsed: boolean;
|
||||
forceExpanded?: boolean;
|
||||
onNavigate?: () => void;
|
||||
currentPath,
|
||||
isSidebarCollapsed,
|
||||
forceExpanded = false,
|
||||
onNavigate,
|
||||
}: {
|
||||
currentPath: string;
|
||||
isSidebarCollapsed: boolean;
|
||||
forceExpanded?: boolean;
|
||||
onNavigate?: () => void;
|
||||
}) {
|
||||
const showLabel = forceExpanded || !isSidebarCollapsed;
|
||||
const showLabel = forceExpanded || !isSidebarCollapsed;
|
||||
|
||||
return (
|
||||
<div className='flex h-full flex-col gap-5'>
|
||||
<div
|
||||
className='flex items-center gap-3 rounded-2xl border border-[var(--border-default)] bg-[var(--surface-muted)] px-3 py-3'>
|
||||
<div
|
||||
className='flex h-10 w-10 items-center justify-center rounded-2xl bg-[var(--brand-primary-soft)] text-sm font-semibold text-[var(--brand-primary)]'>
|
||||
AF
|
||||
</div>
|
||||
{showLabel ? (
|
||||
<div>
|
||||
<p className='text-sm font-semibold text-[var(--foreground-primary)]'>ATSFlare</p>
|
||||
<p className='text-xs text-[var(--foreground-secondary)]'>控制面</p>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<nav className='flex-1 space-y-2'>
|
||||
<div className='flex max-h-full min-h-0 flex-col gap-2 overflow-y-auto pr-1'>
|
||||
{dashboardNavigation.map((item) => (
|
||||
<SidebarNavItem
|
||||
key={item.href}
|
||||
item={item}
|
||||
currentPath={currentPath}
|
||||
isSidebarCollapsed={isSidebarCollapsed}
|
||||
forceExpanded={forceExpanded}
|
||||
onNavigate={onNavigate}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</nav>
|
||||
return (
|
||||
<div className="flex h-full flex-col gap-5">
|
||||
<div className="flex items-center gap-3 rounded-2xl border border-[var(--border-default)] bg-[var(--surface-muted)] px-3 py-3">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-2xl bg-[var(--brand-primary-soft)] text-sm font-semibold text-[var(--brand-primary)]">
|
||||
AF
|
||||
</div>
|
||||
);
|
||||
{showLabel ? (
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-[var(--foreground-primary)]">
|
||||
ATSFlare
|
||||
</p>
|
||||
<p className="text-xs text-[var(--foreground-secondary)]">控制面</p>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<nav className="flex-1 space-y-2">
|
||||
<div className="flex max-h-full min-h-0 flex-col gap-2 overflow-y-auto pr-1">
|
||||
{dashboardNavigation.map((item) => (
|
||||
<SidebarNavItem
|
||||
key={item.href}
|
||||
item={item}
|
||||
currentPath={currentPath}
|
||||
isSidebarCollapsed={isSidebarCollapsed}
|
||||
forceExpanded={forceExpanded}
|
||||
onNavigate={onNavigate}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function DashboardSidebar() {
|
||||
const pathname = usePathname();
|
||||
const currentPath = pathname ?? '/';
|
||||
const isSidebarCollapsed = useAppShellStore((state) => state.isSidebarCollapsed);
|
||||
const isMobileSidebarOpen = useAppShellStore((state) => state.isMobileSidebarOpen);
|
||||
const setMobileSidebarOpen = useAppShellStore((state) => state.setMobileSidebarOpen);
|
||||
const pathname = usePathname();
|
||||
const currentPath = pathname ?? '/';
|
||||
const isSidebarCollapsed = useAppShellStore(
|
||||
(state) => state.isSidebarCollapsed,
|
||||
);
|
||||
const isMobileSidebarOpen = useAppShellStore(
|
||||
(state) => state.isMobileSidebarOpen,
|
||||
);
|
||||
const setMobileSidebarOpen = useAppShellStore(
|
||||
(state) => state.setMobileSidebarOpen,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setMobileSidebarOpen(false);
|
||||
}, [currentPath, setMobileSidebarOpen]);
|
||||
useEffect(() => {
|
||||
setMobileSidebarOpen(false);
|
||||
}, [currentPath, setMobileSidebarOpen]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={cn(
|
||||
'fixed inset-0 z-30 bg-black/35 transition-opacity duration-200 min-[1000px]:hidden',
|
||||
isMobileSidebarOpen ? 'opacity-100' : 'pointer-events-none opacity-0',
|
||||
)}
|
||||
onClick={() => setMobileSidebarOpen(false)}
|
||||
aria-hidden='true'
|
||||
/>
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={cn(
|
||||
'fixed inset-0 z-30 bg-black/35 transition-opacity duration-200 min-[1000px]:hidden',
|
||||
isMobileSidebarOpen ? 'opacity-100' : 'pointer-events-none opacity-0',
|
||||
)}
|
||||
onClick={() => setMobileSidebarOpen(false)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
<aside
|
||||
className={cn(
|
||||
'fixed left-0 top-0 z-40 h-screen w-[200px] overflow-hidden border-r border-[var(--border-default)] bg-[var(--surface-panel)]/95 px-3 py-5 backdrop-blur transition-transform duration-200 min-[1000px]:hidden',
|
||||
isMobileSidebarOpen ? 'translate-x-0' : '-translate-x-full',
|
||||
)}
|
||||
>
|
||||
<SidebarContent
|
||||
currentPath={currentPath}
|
||||
isSidebarCollapsed={false}
|
||||
forceExpanded
|
||||
onNavigate={() => setMobileSidebarOpen(false)}
|
||||
/>
|
||||
</aside>
|
||||
<aside
|
||||
className={cn(
|
||||
'fixed top-0 left-0 z-40 h-screen w-[200px] overflow-hidden border-r border-[var(--border-default)] bg-[var(--surface-panel)]/95 px-3 py-5 backdrop-blur transition-transform duration-200 min-[1000px]:hidden',
|
||||
isMobileSidebarOpen ? 'translate-x-0' : '-translate-x-full',
|
||||
)}
|
||||
>
|
||||
<SidebarContent
|
||||
currentPath={currentPath}
|
||||
isSidebarCollapsed={false}
|
||||
forceExpanded
|
||||
onNavigate={() => setMobileSidebarOpen(false)}
|
||||
/>
|
||||
</aside>
|
||||
|
||||
<aside
|
||||
className={cn(
|
||||
'sticky top-0 z-10 hidden h-screen shrink-0 overflow-hidden border-r border-[var(--border-default)] bg-[var(--surface-panel)]/95 px-3 py-5 backdrop-blur min-[1000px]:block',
|
||||
isSidebarCollapsed ? 'w-[76px]' : 'w-[200px]',
|
||||
)}
|
||||
>
|
||||
<SidebarContent currentPath={currentPath} isSidebarCollapsed={isSidebarCollapsed}/>
|
||||
</aside>
|
||||
</>
|
||||
);
|
||||
<aside
|
||||
className={cn(
|
||||
'sticky top-0 z-10 hidden h-screen shrink-0 overflow-hidden border-r border-[var(--border-default)] bg-[var(--surface-panel)]/95 px-3 py-5 backdrop-blur min-[1000px]:block',
|
||||
isSidebarCollapsed ? 'w-[76px]' : 'w-[200px]',
|
||||
)}
|
||||
>
|
||||
<SidebarContent
|
||||
currentPath={currentPath}
|
||||
isSidebarCollapsed={isSidebarCollapsed}
|
||||
/>
|
||||
</aside>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -35,7 +35,6 @@ import {
|
||||
PrimaryButton,
|
||||
ResourceField,
|
||||
ResourceInput,
|
||||
ResourceSelect,
|
||||
ResourceTextarea,
|
||||
SecondaryButton,
|
||||
ToggleField,
|
||||
@@ -201,27 +200,6 @@ function formatSecondsLabel(value: string) {
|
||||
return `${seconds} 秒`;
|
||||
}
|
||||
|
||||
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 buildDiscoveryCommand(serverUrl: string, discoveryToken: string) {
|
||||
return [
|
||||
`curl -fsSL ${installerScriptUrl} | bash -s -- \\`,
|
||||
@@ -487,7 +465,7 @@ export function SettingsPage() {
|
||||
{
|
||||
key: 'system' as const,
|
||||
label: '系统设置',
|
||||
description: '登录注册、SMTP、OAuth 与风控开关。',
|
||||
description: '登录注册、SMTP、OAuth、限流与风控开关。',
|
||||
},
|
||||
{
|
||||
key: 'other' as const,
|
||||
@@ -1087,247 +1065,21 @@ export function SettingsPage() {
|
||||
</ResourceField>
|
||||
</AppCard>
|
||||
<AppCard
|
||||
title="OpenResty 连接与事件"
|
||||
description="第五版第一批结构化性能项。保存后进入统一发布链路,不会在节点即时生效。"
|
||||
title="性能页入口"
|
||||
description="OpenResty 连接、缓冲、压缩、缓存以及主配置模板编辑已迁移到侧边栏“性能”页面。"
|
||||
action={
|
||||
<PrimaryButton
|
||||
type="button"
|
||||
onClick={() =>
|
||||
void runBusyAction(
|
||||
'operation-openresty-runtime',
|
||||
async () => {
|
||||
if (
|
||||
operationFields.OpenRestyWorkerProcesses !== 'auto' &&
|
||||
!isPositiveInteger(
|
||||
operationFields.OpenRestyWorkerProcesses,
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
'worker_processes 必须为 auto 或大于 0 的整数。',
|
||||
);
|
||||
}
|
||||
|
||||
const integerFields = [
|
||||
[
|
||||
'worker_connections',
|
||||
operationFields.OpenRestyWorkerConnections,
|
||||
],
|
||||
[
|
||||
'worker_rlimit_nofile',
|
||||
operationFields.OpenRestyWorkerRlimitNofile,
|
||||
],
|
||||
[
|
||||
'keepalive_timeout',
|
||||
operationFields.OpenRestyKeepaliveTimeout,
|
||||
],
|
||||
[
|
||||
'keepalive_requests',
|
||||
operationFields.OpenRestyKeepaliveRequests,
|
||||
],
|
||||
[
|
||||
'client_header_timeout',
|
||||
operationFields.OpenRestyClientHeaderTimeout,
|
||||
],
|
||||
[
|
||||
'client_body_timeout',
|
||||
operationFields.OpenRestyClientBodyTimeout,
|
||||
],
|
||||
[
|
||||
'send_timeout',
|
||||
operationFields.OpenRestySendTimeout,
|
||||
],
|
||||
] as const;
|
||||
|
||||
for (const [label, value] of integerFields) {
|
||||
if (!isPositiveInteger(value)) {
|
||||
throw new Error(`${label} 必须为大于 0 的整数。`);
|
||||
}
|
||||
}
|
||||
|
||||
await saveOptionEntries(
|
||||
[
|
||||
[
|
||||
'OpenRestyWorkerProcesses',
|
||||
operationFields.OpenRestyWorkerProcesses.trim(),
|
||||
],
|
||||
[
|
||||
'OpenRestyWorkerConnections',
|
||||
operationFields.OpenRestyWorkerConnections.trim(),
|
||||
],
|
||||
[
|
||||
'OpenRestyWorkerRlimitNofile',
|
||||
operationFields.OpenRestyWorkerRlimitNofile.trim(),
|
||||
],
|
||||
[
|
||||
'OpenRestyEventsUse',
|
||||
operationFields.OpenRestyEventsUse.trim(),
|
||||
],
|
||||
[
|
||||
'OpenRestyEventsMultiAcceptEnabled',
|
||||
String(
|
||||
operationFields.OpenRestyEventsMultiAcceptEnabled,
|
||||
),
|
||||
],
|
||||
[
|
||||
'OpenRestyKeepaliveTimeout',
|
||||
operationFields.OpenRestyKeepaliveTimeout.trim(),
|
||||
],
|
||||
[
|
||||
'OpenRestyKeepaliveRequests',
|
||||
operationFields.OpenRestyKeepaliveRequests.trim(),
|
||||
],
|
||||
[
|
||||
'OpenRestyClientHeaderTimeout',
|
||||
operationFields.OpenRestyClientHeaderTimeout.trim(),
|
||||
],
|
||||
[
|
||||
'OpenRestyClientBodyTimeout',
|
||||
operationFields.OpenRestyClientBodyTimeout.trim(),
|
||||
],
|
||||
[
|
||||
'OpenRestySendTimeout',
|
||||
operationFields.OpenRestySendTimeout.trim(),
|
||||
],
|
||||
],
|
||||
'OpenResty 运行参数已保存。',
|
||||
);
|
||||
},
|
||||
)
|
||||
}
|
||||
disabled={busyKey === 'operation-openresty-runtime'}
|
||||
onClick={() => window.location.assign('/performance')}
|
||||
>
|
||||
{busyKey === 'operation-openresty-runtime'
|
||||
? '保存中...'
|
||||
: '保存运行调优'}
|
||||
打开性能页
|
||||
</PrimaryButton>
|
||||
}
|
||||
>
|
||||
<div className="grid gap-5 md:grid-cols-2 xl:grid-cols-3">
|
||||
<ResourceField label="worker_processes">
|
||||
<ResourceInput
|
||||
value={operationFields.OpenRestyWorkerProcesses}
|
||||
onChange={(event) =>
|
||||
setOperationFields((previous) => ({
|
||||
...previous,
|
||||
OpenRestyWorkerProcesses: event.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="auto"
|
||||
/>
|
||||
</ResourceField>
|
||||
<ResourceField label="worker_connections">
|
||||
<ResourceInput
|
||||
type="number"
|
||||
value={operationFields.OpenRestyWorkerConnections}
|
||||
onChange={(event) =>
|
||||
setOperationFields((previous) => ({
|
||||
...previous,
|
||||
OpenRestyWorkerConnections: event.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</ResourceField>
|
||||
<ResourceField label="worker_rlimit_nofile">
|
||||
<ResourceInput
|
||||
type="number"
|
||||
value={operationFields.OpenRestyWorkerRlimitNofile}
|
||||
onChange={(event) =>
|
||||
setOperationFields((previous) => ({
|
||||
...previous,
|
||||
OpenRestyWorkerRlimitNofile: event.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</ResourceField>
|
||||
<ResourceField label="events use" hint="留空表示不显式渲染。">
|
||||
<ResourceSelect
|
||||
value={operationFields.OpenRestyEventsUse}
|
||||
onChange={(event) =>
|
||||
setOperationFields((previous) => ({
|
||||
...previous,
|
||||
OpenRestyEventsUse: event.target.value,
|
||||
}))
|
||||
}
|
||||
>
|
||||
<option value="">默认</option>
|
||||
<option value="epoll">epoll</option>
|
||||
<option value="kqueue">kqueue</option>
|
||||
<option value="poll">poll</option>
|
||||
<option value="select">select</option>
|
||||
</ResourceSelect>
|
||||
</ResourceField>
|
||||
<ToggleField
|
||||
label="multi_accept"
|
||||
description="是否启用 multi_accept on。"
|
||||
checked={operationFields.OpenRestyEventsMultiAcceptEnabled}
|
||||
onChange={(checked) =>
|
||||
setOperationFields((previous) => ({
|
||||
...previous,
|
||||
OpenRestyEventsMultiAcceptEnabled: checked,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<ResourceField label="keepalive_timeout (秒)">
|
||||
<ResourceInput
|
||||
type="number"
|
||||
value={operationFields.OpenRestyKeepaliveTimeout}
|
||||
onChange={(event) =>
|
||||
setOperationFields((previous) => ({
|
||||
...previous,
|
||||
OpenRestyKeepaliveTimeout: event.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</ResourceField>
|
||||
<ResourceField label="keepalive_requests">
|
||||
<ResourceInput
|
||||
type="number"
|
||||
value={operationFields.OpenRestyKeepaliveRequests}
|
||||
onChange={(event) =>
|
||||
setOperationFields((previous) => ({
|
||||
...previous,
|
||||
OpenRestyKeepaliveRequests: event.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</ResourceField>
|
||||
<ResourceField label="client_header_timeout (秒)">
|
||||
<ResourceInput
|
||||
type="number"
|
||||
value={operationFields.OpenRestyClientHeaderTimeout}
|
||||
onChange={(event) =>
|
||||
setOperationFields((previous) => ({
|
||||
...previous,
|
||||
OpenRestyClientHeaderTimeout: event.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</ResourceField>
|
||||
<ResourceField label="client_body_timeout (秒)">
|
||||
<ResourceInput
|
||||
type="number"
|
||||
value={operationFields.OpenRestyClientBodyTimeout}
|
||||
onChange={(event) =>
|
||||
setOperationFields((previous) => ({
|
||||
...previous,
|
||||
OpenRestyClientBodyTimeout: event.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</ResourceField>
|
||||
<ResourceField label="send_timeout (秒)">
|
||||
<ResourceInput
|
||||
type="number"
|
||||
value={operationFields.OpenRestySendTimeout}
|
||||
onChange={(event) =>
|
||||
setOperationFields((previous) => ({
|
||||
...previous,
|
||||
OpenRestySendTimeout: event.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</ResourceField>
|
||||
</div>
|
||||
<p className="text-sm leading-6 text-[var(--foreground-secondary)]">
|
||||
设置页保留 Agent、系统、限流和品牌配置;OpenResty
|
||||
相关项已拆分为独立工作台,支持“设置 / 编辑”双 Tab 操作。
|
||||
</p>
|
||||
</AppCard>
|
||||
<AppCard
|
||||
title="Discovery Token 与部署命令"
|
||||
@@ -1775,462 +1527,23 @@ export function SettingsPage() {
|
||||
</div>
|
||||
</AppCard>
|
||||
</div>
|
||||
<div className="grid gap-6 xl:grid-cols-[1fr_1fr]">
|
||||
<AppCard
|
||||
title="OpenResty 反代缓冲与超时"
|
||||
description="用于控制 upstream 连接、发送、读取超时,以及常用代理缓冲参数。"
|
||||
action={
|
||||
<PrimaryButton
|
||||
type="button"
|
||||
onClick={() =>
|
||||
void runBusyAction(
|
||||
'operation-openresty-proxy',
|
||||
async () => {
|
||||
const timeoutFields = [
|
||||
operationFields.OpenRestyProxyConnectTimeout,
|
||||
operationFields.OpenRestyProxySendTimeout,
|
||||
operationFields.OpenRestyProxyReadTimeout,
|
||||
];
|
||||
|
||||
for (const value of timeoutFields) {
|
||||
if (!isPositiveInteger(value)) {
|
||||
throw new Error(
|
||||
'代理超时参数必须为大于 0 的整数秒。',
|
||||
);
|
||||
}
|
||||
}
|
||||
if (
|
||||
!isProxyBuffersValue(
|
||||
operationFields.OpenRestyProxyBuffers,
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
'proxy_buffers 格式必须类似 "16 16k"。',
|
||||
);
|
||||
}
|
||||
if (
|
||||
!isSizeValue(
|
||||
operationFields.OpenRestyProxyBufferSize,
|
||||
) ||
|
||||
!isSizeValue(
|
||||
operationFields.OpenRestyProxyBusyBuffersSize,
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
'缓冲大小必须为整数或带 k/m/g 单位的值。',
|
||||
);
|
||||
}
|
||||
|
||||
await saveOptionEntries(
|
||||
[
|
||||
[
|
||||
'OpenRestyProxyConnectTimeout',
|
||||
operationFields.OpenRestyProxyConnectTimeout.trim(),
|
||||
],
|
||||
[
|
||||
'OpenRestyProxySendTimeout',
|
||||
operationFields.OpenRestyProxySendTimeout.trim(),
|
||||
],
|
||||
[
|
||||
'OpenRestyProxyReadTimeout',
|
||||
operationFields.OpenRestyProxyReadTimeout.trim(),
|
||||
],
|
||||
[
|
||||
'OpenRestyProxyBufferingEnabled',
|
||||
String(
|
||||
operationFields.OpenRestyProxyBufferingEnabled,
|
||||
),
|
||||
],
|
||||
[
|
||||
'OpenRestyProxyBuffers',
|
||||
operationFields.OpenRestyProxyBuffers.trim(),
|
||||
],
|
||||
[
|
||||
'OpenRestyProxyBufferSize',
|
||||
operationFields.OpenRestyProxyBufferSize.trim(),
|
||||
],
|
||||
[
|
||||
'OpenRestyProxyBusyBuffersSize',
|
||||
operationFields.OpenRestyProxyBusyBuffersSize.trim(),
|
||||
],
|
||||
],
|
||||
'OpenResty 反代缓冲参数已保存。',
|
||||
);
|
||||
},
|
||||
)
|
||||
}
|
||||
disabled={busyKey === 'operation-openresty-proxy'}
|
||||
>
|
||||
{busyKey === 'operation-openresty-proxy'
|
||||
? '保存中...'
|
||||
: '保存代理调优'}
|
||||
</PrimaryButton>
|
||||
}
|
||||
>
|
||||
<div className="grid gap-5 md:grid-cols-2">
|
||||
<ResourceField label="proxy_connect_timeout (秒)">
|
||||
<ResourceInput
|
||||
type="number"
|
||||
value={operationFields.OpenRestyProxyConnectTimeout}
|
||||
onChange={(event) =>
|
||||
setOperationFields((previous) => ({
|
||||
...previous,
|
||||
OpenRestyProxyConnectTimeout: event.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</ResourceField>
|
||||
<ResourceField label="proxy_send_timeout (秒)">
|
||||
<ResourceInput
|
||||
type="number"
|
||||
value={operationFields.OpenRestyProxySendTimeout}
|
||||
onChange={(event) =>
|
||||
setOperationFields((previous) => ({
|
||||
...previous,
|
||||
OpenRestyProxySendTimeout: event.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</ResourceField>
|
||||
<ResourceField label="proxy_read_timeout (秒)">
|
||||
<ResourceInput
|
||||
type="number"
|
||||
value={operationFields.OpenRestyProxyReadTimeout}
|
||||
onChange={(event) =>
|
||||
setOperationFields((previous) => ({
|
||||
...previous,
|
||||
OpenRestyProxyReadTimeout: event.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</ResourceField>
|
||||
<ToggleField
|
||||
label="proxy_buffering"
|
||||
description="是否启用 proxy_buffering。"
|
||||
checked={operationFields.OpenRestyProxyBufferingEnabled}
|
||||
onChange={(checked) =>
|
||||
setOperationFields((previous) => ({
|
||||
...previous,
|
||||
OpenRestyProxyBufferingEnabled: checked,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<ResourceField label="proxy_buffers">
|
||||
<ResourceInput
|
||||
value={operationFields.OpenRestyProxyBuffers}
|
||||
onChange={(event) =>
|
||||
setOperationFields((previous) => ({
|
||||
...previous,
|
||||
OpenRestyProxyBuffers: event.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="16 16k"
|
||||
/>
|
||||
</ResourceField>
|
||||
<ResourceField label="proxy_buffer_size">
|
||||
<ResourceInput
|
||||
value={operationFields.OpenRestyProxyBufferSize}
|
||||
onChange={(event) =>
|
||||
setOperationFields((previous) => ({
|
||||
...previous,
|
||||
OpenRestyProxyBufferSize: event.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="8k"
|
||||
/>
|
||||
</ResourceField>
|
||||
<ResourceField label="proxy_busy_buffers_size">
|
||||
<ResourceInput
|
||||
value={operationFields.OpenRestyProxyBusyBuffersSize}
|
||||
onChange={(event) =>
|
||||
setOperationFields((previous) => ({
|
||||
...previous,
|
||||
OpenRestyProxyBusyBuffersSize: event.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="64k"
|
||||
/>
|
||||
</ResourceField>
|
||||
</div>
|
||||
</AppCard>
|
||||
<AppCard
|
||||
title="OpenResty 压缩与缓存"
|
||||
description="缓存能力仍限定在单节点反代优化场景,不扩展为独立缓存产品。"
|
||||
action={
|
||||
<PrimaryButton
|
||||
type="button"
|
||||
onClick={() =>
|
||||
void runBusyAction(
|
||||
'operation-openresty-cache',
|
||||
async () => {
|
||||
if (
|
||||
!isPositiveInteger(
|
||||
operationFields.OpenRestyGzipMinLength,
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
'gzip_min_length 必须为大于 0 的整数。',
|
||||
);
|
||||
}
|
||||
const gzipLevel = Number.parseInt(
|
||||
operationFields.OpenRestyGzipCompLevel,
|
||||
10,
|
||||
);
|
||||
if (
|
||||
Number.isNaN(gzipLevel) ||
|
||||
gzipLevel < 1 ||
|
||||
gzipLevel > 9
|
||||
) {
|
||||
throw new Error(
|
||||
'gzip_comp_level 必须在 1 到 9 之间。',
|
||||
);
|
||||
}
|
||||
if (operationFields.OpenRestyCacheEnabled) {
|
||||
if (!operationFields.OpenRestyCachePath.trim()) {
|
||||
throw new Error(
|
||||
'启用缓存时必须填写 proxy_cache_path 目录。',
|
||||
);
|
||||
}
|
||||
if (
|
||||
!isCacheLevelsValue(
|
||||
operationFields.OpenRestyCacheLevels,
|
||||
) ||
|
||||
!isDurationToken(
|
||||
operationFields.OpenRestyCacheInactive,
|
||||
) ||
|
||||
!isSizeValue(
|
||||
operationFields.OpenRestyCacheMaxSize,
|
||||
) ||
|
||||
!isDurationToken(
|
||||
operationFields.OpenRestyCacheLockTimeout,
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
'缓存 levels、inactive、max_size 或 lock_timeout 格式不合法。',
|
||||
);
|
||||
}
|
||||
if (
|
||||
!operationFields.OpenRestyCacheKeyTemplate.trim()
|
||||
) {
|
||||
throw new Error(
|
||||
'启用缓存时必须填写缓存 Key 模板。',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await saveOptionEntries(
|
||||
[
|
||||
[
|
||||
'OpenRestyGzipEnabled',
|
||||
String(operationFields.OpenRestyGzipEnabled),
|
||||
],
|
||||
[
|
||||
'OpenRestyGzipMinLength',
|
||||
operationFields.OpenRestyGzipMinLength.trim(),
|
||||
],
|
||||
[
|
||||
'OpenRestyGzipCompLevel',
|
||||
operationFields.OpenRestyGzipCompLevel.trim(),
|
||||
],
|
||||
[
|
||||
'OpenRestyCachePath',
|
||||
operationFields.OpenRestyCachePath.trim(),
|
||||
],
|
||||
[
|
||||
'OpenRestyCacheLevels',
|
||||
operationFields.OpenRestyCacheLevels.trim(),
|
||||
],
|
||||
[
|
||||
'OpenRestyCacheInactive',
|
||||
operationFields.OpenRestyCacheInactive.trim(),
|
||||
],
|
||||
[
|
||||
'OpenRestyCacheMaxSize',
|
||||
operationFields.OpenRestyCacheMaxSize.trim(),
|
||||
],
|
||||
[
|
||||
'OpenRestyCacheKeyTemplate',
|
||||
operationFields.OpenRestyCacheKeyTemplate.trim(),
|
||||
],
|
||||
[
|
||||
'OpenRestyCacheLockEnabled',
|
||||
String(operationFields.OpenRestyCacheLockEnabled),
|
||||
],
|
||||
[
|
||||
'OpenRestyCacheLockTimeout',
|
||||
operationFields.OpenRestyCacheLockTimeout.trim(),
|
||||
],
|
||||
[
|
||||
'OpenRestyCacheUseStale',
|
||||
operationFields.OpenRestyCacheUseStale.trim(),
|
||||
],
|
||||
[
|
||||
'OpenRestyCacheEnabled',
|
||||
String(operationFields.OpenRestyCacheEnabled),
|
||||
],
|
||||
],
|
||||
'OpenResty 压缩与缓存参数已保存。',
|
||||
);
|
||||
},
|
||||
)
|
||||
}
|
||||
disabled={busyKey === 'operation-openresty-cache'}
|
||||
>
|
||||
{busyKey === 'operation-openresty-cache'
|
||||
? '保存中...'
|
||||
: '保存压缩与缓存'}
|
||||
</PrimaryButton>
|
||||
}
|
||||
>
|
||||
<div className="space-y-5">
|
||||
<div className="grid gap-5 md:grid-cols-2">
|
||||
<ToggleField
|
||||
label="gzip"
|
||||
description="是否启用 gzip on。"
|
||||
checked={operationFields.OpenRestyGzipEnabled}
|
||||
onChange={(checked) =>
|
||||
setOperationFields((previous) => ({
|
||||
...previous,
|
||||
OpenRestyGzipEnabled: checked,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<ToggleField
|
||||
label="proxy cache"
|
||||
description="是否启用单节点代理缓存。"
|
||||
checked={operationFields.OpenRestyCacheEnabled}
|
||||
onChange={(checked) =>
|
||||
setOperationFields((previous) => ({
|
||||
...previous,
|
||||
OpenRestyCacheEnabled: checked,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<ResourceField label="gzip_min_length">
|
||||
<ResourceInput
|
||||
type="number"
|
||||
value={operationFields.OpenRestyGzipMinLength}
|
||||
onChange={(event) =>
|
||||
setOperationFields((previous) => ({
|
||||
...previous,
|
||||
OpenRestyGzipMinLength: event.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</ResourceField>
|
||||
<ResourceField label="gzip_comp_level">
|
||||
<ResourceInput
|
||||
type="number"
|
||||
min="1"
|
||||
max="9"
|
||||
value={operationFields.OpenRestyGzipCompLevel}
|
||||
onChange={(event) =>
|
||||
setOperationFields((previous) => ({
|
||||
...previous,
|
||||
OpenRestyGzipCompLevel: event.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</ResourceField>
|
||||
</div>
|
||||
<div className="grid gap-5 md:grid-cols-2">
|
||||
<ResourceField label="proxy_cache_path">
|
||||
<ResourceInput
|
||||
value={operationFields.OpenRestyCachePath}
|
||||
onChange={(event) =>
|
||||
setOperationFields((previous) => ({
|
||||
...previous,
|
||||
OpenRestyCachePath: event.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="/var/cache/openresty/atsflare"
|
||||
/>
|
||||
</ResourceField>
|
||||
<ResourceField label="levels">
|
||||
<ResourceInput
|
||||
value={operationFields.OpenRestyCacheLevels}
|
||||
onChange={(event) =>
|
||||
setOperationFields((previous) => ({
|
||||
...previous,
|
||||
OpenRestyCacheLevels: event.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="1:2"
|
||||
/>
|
||||
</ResourceField>
|
||||
<ResourceField label="inactive">
|
||||
<ResourceInput
|
||||
value={operationFields.OpenRestyCacheInactive}
|
||||
onChange={(event) =>
|
||||
setOperationFields((previous) => ({
|
||||
...previous,
|
||||
OpenRestyCacheInactive: event.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="30m"
|
||||
/>
|
||||
</ResourceField>
|
||||
<ResourceField label="max_size">
|
||||
<ResourceInput
|
||||
value={operationFields.OpenRestyCacheMaxSize}
|
||||
onChange={(event) =>
|
||||
setOperationFields((previous) => ({
|
||||
...previous,
|
||||
OpenRestyCacheMaxSize: event.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="1g"
|
||||
/>
|
||||
</ResourceField>
|
||||
<ResourceField label="cache key template">
|
||||
<ResourceInput
|
||||
value={operationFields.OpenRestyCacheKeyTemplate}
|
||||
onChange={(event) =>
|
||||
setOperationFields((previous) => ({
|
||||
...previous,
|
||||
OpenRestyCacheKeyTemplate: event.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</ResourceField>
|
||||
<ToggleField
|
||||
label="proxy_cache_lock"
|
||||
description="是否启用 proxy_cache_lock。"
|
||||
checked={operationFields.OpenRestyCacheLockEnabled}
|
||||
onChange={(checked) =>
|
||||
setOperationFields((previous) => ({
|
||||
...previous,
|
||||
OpenRestyCacheLockEnabled: checked,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<ResourceField label="proxy_cache_lock_timeout">
|
||||
<ResourceInput
|
||||
value={operationFields.OpenRestyCacheLockTimeout}
|
||||
onChange={(event) =>
|
||||
setOperationFields((previous) => ({
|
||||
...previous,
|
||||
OpenRestyCacheLockTimeout: event.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="5s"
|
||||
/>
|
||||
</ResourceField>
|
||||
<ResourceField label="proxy_cache_use_stale">
|
||||
<ResourceInput
|
||||
value={operationFields.OpenRestyCacheUseStale}
|
||||
onChange={(event) =>
|
||||
setOperationFields((previous) => ({
|
||||
...previous,
|
||||
OpenRestyCacheUseStale: event.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</ResourceField>
|
||||
</div>
|
||||
</div>
|
||||
</AppCard>
|
||||
</div>
|
||||
<AppCard
|
||||
title="性能页说明"
|
||||
description="OpenResty 反代缓冲、压缩、缓存和主配置模板编辑已迁移到独立“性能”页面。"
|
||||
action={
|
||||
<PrimaryButton
|
||||
type="button"
|
||||
onClick={() => window.location.assign('/performance')}
|
||||
>
|
||||
前往性能页
|
||||
</PrimaryButton>
|
||||
}
|
||||
>
|
||||
<p className="text-sm leading-6 text-[var(--foreground-secondary)]">
|
||||
这样可以把 Agent 运行参数、系统开关与 OpenResty
|
||||
专项配置拆开,减少设置页的混杂信息量。
|
||||
</p>
|
||||
</AppCard>
|
||||
<AppCard
|
||||
title="请求限流设置"
|
||||
description="按来源 IP 生效,保存后立即影响 Web、API、上传下载及登录注册等敏感接口。时间单位均为秒。"
|
||||
@@ -2617,7 +1930,7 @@ export function SettingsPage() {
|
||||
<div className="space-y-6">
|
||||
<PageHeader
|
||||
title="设置"
|
||||
description="第五版从这里开始补齐 OpenResty 性能参数、Agent 运行参数和系统级运维配置。"
|
||||
description="集中维护账号、Agent、系统、限流与品牌配置。OpenResty 专项能力已迁移到侧边栏“性能”页面。"
|
||||
/>
|
||||
|
||||
{feedback ? (
|
||||
|
||||
@@ -1,44 +1,49 @@
|
||||
import type {NavigationItem} from '@/types/navigation';
|
||||
import type { NavigationItem } from '@/types/navigation';
|
||||
|
||||
export const dashboardNavigation: NavigationItem[] = [
|
||||
{
|
||||
href: '/',
|
||||
label: '总览',
|
||||
icon: 'home',
|
||||
},
|
||||
{
|
||||
href: '/node',
|
||||
label: '节点',
|
||||
icon: 'node',
|
||||
},
|
||||
{
|
||||
href: '/proxy-route',
|
||||
label: '规则',
|
||||
icon: 'proxy',
|
||||
},
|
||||
{
|
||||
href: '/config-version',
|
||||
label: '发布',
|
||||
icon: 'release',
|
||||
},
|
||||
{
|
||||
href: '/managed-domain',
|
||||
label: '域名',
|
||||
icon: 'domain',
|
||||
},
|
||||
{
|
||||
href: '/tls-certificate',
|
||||
label: '证书',
|
||||
icon: 'certificate',
|
||||
},
|
||||
{
|
||||
href: '/user',
|
||||
label: '用户',
|
||||
icon: 'user',
|
||||
},
|
||||
{
|
||||
href: '/setting',
|
||||
label: '设置',
|
||||
icon: 'setting',
|
||||
},
|
||||
{
|
||||
href: '/',
|
||||
label: '总览',
|
||||
icon: 'home',
|
||||
},
|
||||
{
|
||||
href: '/node',
|
||||
label: '节点',
|
||||
icon: 'node',
|
||||
},
|
||||
{
|
||||
href: '/proxy-route',
|
||||
label: '规则',
|
||||
icon: 'proxy',
|
||||
},
|
||||
{
|
||||
href: '/config-version',
|
||||
label: '发布',
|
||||
icon: 'release',
|
||||
},
|
||||
{
|
||||
href: '/managed-domain',
|
||||
label: '域名',
|
||||
icon: 'domain',
|
||||
},
|
||||
{
|
||||
href: '/tls-certificate',
|
||||
label: '证书',
|
||||
icon: 'certificate',
|
||||
},
|
||||
{
|
||||
href: '/user',
|
||||
label: '用户',
|
||||
icon: 'user',
|
||||
},
|
||||
{
|
||||
href: '/performance',
|
||||
label: '性能',
|
||||
icon: 'performance',
|
||||
},
|
||||
{
|
||||
href: '/setting',
|
||||
label: '设置',
|
||||
icon: 'setting',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { getCurrentNavigationItem, isPathActive } from '@/lib/utils/navigation';
|
||||
|
||||
describe('navigation utils', () => {
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { getCurrentNavigationItem, isPathActive } from '@/lib/utils/navigation';
|
||||
|
||||
describe('navigation utils', () => {
|
||||
it('marks root path as active only for home', () => {
|
||||
expect(isPathActive('/', '/')).toBe(true);
|
||||
expect(isPathActive('/node', '/')).toBe(false);
|
||||
@@ -11,6 +11,7 @@ describe('navigation utils', () => {
|
||||
it('resolves current navigation item for nested paths', () => {
|
||||
expect(getCurrentNavigationItem('/node/abc')?.label).toBe('节点');
|
||||
expect(getCurrentNavigationItem('/managed-domain')?.label).toBe('域名');
|
||||
expect(getCurrentNavigationItem('/performance')?.label).toBe('性能');
|
||||
expect(getCurrentNavigationItem('/setting')?.label).toBe('设置');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ export type NavigationIconKey =
|
||||
| 'certificate'
|
||||
| 'proxy'
|
||||
| 'release'
|
||||
| 'performance'
|
||||
| 'user'
|
||||
| 'setting';
|
||||
|
||||
|
||||
+4
-2
@@ -63,6 +63,7 @@ ATSFlare 当前定位为内部自用的反向代理控制面,不面向外部
|
||||
* 主配置文件由 Server 统一生成并由 Agent 受控落地,不开放节点侧手改后再回传合并
|
||||
* 性能优化参数统一在 Server 配置,不支持按节点分叉不同性能模板
|
||||
* 第五版不开放任意自定义 Nginx/OpenResty 片段上传,不提供任意指令执行入口, 但是需要保留拓展能力, 为后续开放准备
|
||||
* 允许在管理端提供 OpenResty 主配置模板编辑能力,但模板必须保留 ATSFlare 预留占位符,确保性能参数、证书目录与受管路由 include 继续由 Server 统一渲染
|
||||
|
||||
新增能力超出上述边界时,必须先更新本文档,再进入实现。
|
||||
|
||||
@@ -122,7 +123,7 @@ ATSFlare Agent (register / heartbeat / sync / apply / update)
|
||||
|
||||
职责分工:
|
||||
|
||||
* Server 负责配置、版本、节点、设置、管理端 UI 以及 OpenResty 主配置渲染
|
||||
* Server 负责配置、版本、节点、设置、管理端 UI 以及 OpenResty 主配置模板渲染
|
||||
* Agent 负责本地落盘、校验、reload、回滚、自更新,不负责维护独立于 Server 的主配置真相
|
||||
* 发布通过“生成完整版本并激活”完成
|
||||
* 历史版本不可变
|
||||
@@ -152,6 +153,7 @@ ATSFlare Agent (register / heartbeat / sync / apply / update)
|
||||
* 域名与证书匹配同时支持精确匹配与通配符匹配
|
||||
* 节点专属 `agent_token` 必须可立即失效
|
||||
* OpenResty 性能优化参数与缓存参数统一由 Server 设置管理,不允许节点侧形成额外配置源
|
||||
* OpenResty 主配置模板允许编辑,但必须保留系统要求的占位符,不能绕过结构化参数校验与受管 include 注入
|
||||
* Agent 只应用受控主配置文件,不提供任意配置片段拼接入口
|
||||
|
||||
---
|
||||
@@ -215,7 +217,7 @@ ATSFlare Agent (register / heartbeat / sync / apply / update)
|
||||
负责:
|
||||
|
||||
* 管理端页面、布局、交互与主题
|
||||
* 规则、版本、节点、证书、域名、用户、设置等页面
|
||||
* 规则、版本、节点、证书、域名、用户、设置、性能等页面
|
||||
* 统一请求层与前端状态管理
|
||||
|
||||
---
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
|
||||
* 默认不引入 Redis、MQ、对象存储等新基础设施
|
||||
* 不为未确认的平台化能力预埋复杂抽象
|
||||
* OpenResty 性能参数与缓存参数优先复用现有 `Option` 体系管理,不为单一版本额外引入配置中心
|
||||
* OpenResty 性能参数、缓存参数与主配置模板优先复用现有 `Option` 体系管理,不为单一版本额外引入配置中心
|
||||
|
||||
### 2.2 Agent
|
||||
|
||||
@@ -179,6 +179,7 @@ Agent:
|
||||
* 用通用 shell/命令执行方式替代受限节点操作接口
|
||||
* 在日志中打印完整 Token
|
||||
* 为性能优化需求开放任意 OpenResty 文本片段上传或任意指令下发
|
||||
* 允许绕过系统占位符约束直接保存不可渲染的 OpenResty 主配置模板
|
||||
|
||||
---
|
||||
|
||||
@@ -199,6 +200,7 @@ Agent:
|
||||
* 主配置文件的真相源在 Server,Agent 只负责受控写入、校验与回滚
|
||||
* 性能优化参数必须通过结构化字段渲染,禁止直接拼接未经校验的自由文本
|
||||
* 新增参数命名统一采用 `OpenResty...` 前缀,布尔值、整数、大小单位和时间单位必须在更新入口做校验
|
||||
* 主配置模板编辑必须保留系统要求的占位符,由 Server 在发布与预览时再渲染为最终 `nginx.conf`
|
||||
|
||||
版本号格式保持:
|
||||
|
||||
|
||||
@@ -47,13 +47,14 @@
|
||||
|
||||
* 平台化缓存产品、Purge 系统、分层缓存、节点差异化缓存策略
|
||||
* 任意文本片段注入、任意 OpenResty 指令执行、节点侧自定义模板合并
|
||||
* 绕过占位符约束的主配置模板直写
|
||||
* 引入 Redis、Prometheus、消息队列等新基础设施作为第五版前置条件
|
||||
|
||||
## 4. 第五版实施顺序
|
||||
|
||||
建议按以下顺序推进:
|
||||
|
||||
1. 补齐 Server 侧 OpenResty 性能参数模型、默认值、校验规则与设置页入口
|
||||
1. 补齐 Server 侧 OpenResty 性能参数模型、默认值、校验规则与性能页入口,并补充主配置模板编辑能力
|
||||
2. 扩展配置渲染结果,使版本快照覆盖主配置文件与路由配置文件
|
||||
3. 调整 Agent 本地应用链路,支持主配置写入、校验、reload、失败回滚
|
||||
4. 补齐预览、diff、应用结果与日志,确保第五版能力可观测
|
||||
@@ -64,6 +65,7 @@
|
||||
完成第五版时至少满足:
|
||||
|
||||
* 管理端可以查看并修改第一批 OpenResty 性能优化项
|
||||
* 管理端提供独立“性能”页面,支持结构化设置与主配置模板编辑/预览
|
||||
* 性能优化项保存后进入统一发布链路,而不是节点即时生效
|
||||
* Agent 可以接管主配置文件,并在 `openresty -t` 失败时完整回滚
|
||||
* 配置预览或 diff 能体现主配置与关键性能参数变化
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
* TLS 证书
|
||||
* 用户管理
|
||||
* 设置
|
||||
* 性能
|
||||
* 登录、注册、重置密码、GitHub OAuth、关于页
|
||||
|
||||
---
|
||||
|
||||
Reference in New Issue
Block a user