mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-07 16:16:37 +08:00
feat: Add OpenResty performance tuning options and validation
- Implemented validation for OpenResty configuration options including worker processes, connections, timeouts, and caching parameters. - Added new validation functions for positive integers, booleans, and specific formats (e.g., size values, proxy buffers, cache levels). - Updated the option management to include OpenResty parameters in the database and ensure they can be modified via the management interface. - Enhanced the settings page to allow users to configure OpenResty parameters with appropriate validation and error handling. - Added unit tests for the new validation logic to ensure correctness. - Updated documentation to reflect the new OpenResty configuration options and their usage.
This commit is contained in:
@@ -55,6 +55,37 @@ var AgentHeartbeatInterval = 30000 // milliseconds
|
|||||||
var AgentSyncInterval = 30000 // milliseconds
|
var AgentSyncInterval = 30000 // milliseconds
|
||||||
var AgentUpdateRepo = "Rain-kl/ATSFlare"
|
var AgentUpdateRepo = "Rain-kl/ATSFlare"
|
||||||
|
|
||||||
|
// V5 OpenResty performance settings (hot-reloadable via Option table)
|
||||||
|
var OpenRestyWorkerProcesses = "auto"
|
||||||
|
var OpenRestyWorkerConnections = 4096
|
||||||
|
var OpenRestyWorkerRlimitNofile = 65535
|
||||||
|
var OpenRestyEventsUse = ""
|
||||||
|
var OpenRestyEventsMultiAcceptEnabled = false
|
||||||
|
var OpenRestyKeepaliveTimeout = 65
|
||||||
|
var OpenRestyKeepaliveRequests = 1000
|
||||||
|
var OpenRestyClientHeaderTimeout = 15
|
||||||
|
var OpenRestyClientBodyTimeout = 15
|
||||||
|
var OpenRestySendTimeout = 30
|
||||||
|
var OpenRestyProxyConnectTimeout = 5
|
||||||
|
var OpenRestyProxySendTimeout = 60
|
||||||
|
var OpenRestyProxyReadTimeout = 60
|
||||||
|
var OpenRestyProxyBufferingEnabled = true
|
||||||
|
var OpenRestyProxyBuffers = "16 16k"
|
||||||
|
var OpenRestyProxyBufferSize = "8k"
|
||||||
|
var OpenRestyProxyBusyBuffersSize = "64k"
|
||||||
|
var OpenRestyGzipEnabled = true
|
||||||
|
var OpenRestyGzipMinLength = 1024
|
||||||
|
var OpenRestyGzipCompLevel = 5
|
||||||
|
var OpenRestyCacheEnabled = false
|
||||||
|
var OpenRestyCachePath = ""
|
||||||
|
var OpenRestyCacheLevels = "1:2"
|
||||||
|
var OpenRestyCacheInactive = "30m"
|
||||||
|
var OpenRestyCacheMaxSize = "1g"
|
||||||
|
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"
|
||||||
|
|
||||||
const (
|
const (
|
||||||
RoleGuestUser = 0
|
RoleGuestUser = 0
|
||||||
RoleCommonUser = 1
|
RoleCommonUser = 1
|
||||||
|
|||||||
@@ -7,10 +7,18 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
openRestySizePattern = regexp.MustCompile(`^\d+[kKmMgG]?$`)
|
||||||
|
openRestyProxyBuffersPattern = regexp.MustCompile(`^\d+\s+\d+[kKmMgG]?$`)
|
||||||
|
openRestyCacheLevelsPattern = regexp.MustCompile(`^\d{1,2}(?::\d{1,2}){0,2}$`)
|
||||||
|
openRestyDurationTokenPattern = regexp.MustCompile(`^\d+[smhdwSMHDW]$`)
|
||||||
|
)
|
||||||
|
|
||||||
func validateRateLimitOption(key string, value string) error {
|
func validateRateLimitOption(key string, value string) error {
|
||||||
maxDurationSeconds := int(common.RateLimitKeyExpirationDuration.Seconds())
|
maxDurationSeconds := int(common.RateLimitKeyExpirationDuration.Seconds())
|
||||||
|
|
||||||
@@ -35,6 +43,122 @@ func validateRateLimitOption(key string, value string) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validatePositiveIntegerOption(key string, value string) error {
|
||||||
|
intValue, err := strconv.Atoi(value)
|
||||||
|
if err != nil || intValue <= 0 {
|
||||||
|
return fmt.Errorf("%s 必须为大于 0 的整数", key)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateBooleanOption(key string, value string) error {
|
||||||
|
switch value {
|
||||||
|
case "true", "false":
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("%s 必须为 true 或 false", key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateOpenRestyOption(key string, value string) error {
|
||||||
|
trimmed := strings.TrimSpace(value)
|
||||||
|
|
||||||
|
switch key {
|
||||||
|
case "OpenRestyWorkerProcesses":
|
||||||
|
if trimmed == "auto" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return validatePositiveIntegerOption(key, trimmed)
|
||||||
|
case "OpenRestyWorkerConnections",
|
||||||
|
"OpenRestyWorkerRlimitNofile",
|
||||||
|
"OpenRestyKeepaliveTimeout",
|
||||||
|
"OpenRestyKeepaliveRequests",
|
||||||
|
"OpenRestyClientHeaderTimeout",
|
||||||
|
"OpenRestyClientBodyTimeout",
|
||||||
|
"OpenRestySendTimeout",
|
||||||
|
"OpenRestyProxyConnectTimeout",
|
||||||
|
"OpenRestyProxySendTimeout",
|
||||||
|
"OpenRestyProxyReadTimeout",
|
||||||
|
"OpenRestyGzipMinLength":
|
||||||
|
return validatePositiveIntegerOption(key, trimmed)
|
||||||
|
case "OpenRestyGzipCompLevel":
|
||||||
|
if err := validatePositiveIntegerOption(key, trimmed); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
level, _ := strconv.Atoi(trimmed)
|
||||||
|
if level > 9 {
|
||||||
|
return fmt.Errorf("%s 不能大于 9", key)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
case "OpenRestyEventsUse":
|
||||||
|
if trimmed == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
switch trimmed {
|
||||||
|
case "epoll", "kqueue", "poll", "select", "rtsig", "/dev/poll", "eventport":
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("%s 仅支持 epoll、kqueue、poll、select、rtsig、/dev/poll、eventport 或留空", key)
|
||||||
|
}
|
||||||
|
case "OpenRestyEventsMultiAcceptEnabled",
|
||||||
|
"OpenRestyProxyBufferingEnabled",
|
||||||
|
"OpenRestyGzipEnabled",
|
||||||
|
"OpenRestyCacheEnabled",
|
||||||
|
"OpenRestyCacheLockEnabled":
|
||||||
|
return validateBooleanOption(key, trimmed)
|
||||||
|
case "OpenRestyProxyBuffers":
|
||||||
|
if openRestyProxyBuffersPattern.MatchString(trimmed) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("%s 格式必须类似 \"16 16k\"", key)
|
||||||
|
case "OpenRestyProxyBufferSize", "OpenRestyProxyBusyBuffersSize", "OpenRestyCacheMaxSize":
|
||||||
|
if openRestySizePattern.MatchString(trimmed) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("%s 格式必须为整数或带 k/m/g 单位的大小值", key)
|
||||||
|
case "OpenRestyCachePath":
|
||||||
|
if strings.ContainsAny(trimmed, "\r\n\t") {
|
||||||
|
return fmt.Errorf("%s 不能包含换行或制表符", key)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
case "OpenRestyCacheLevels":
|
||||||
|
if openRestyCacheLevelsPattern.MatchString(trimmed) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("%s 格式必须类似 \"1:2\" 或 \"1:2:2\"", key)
|
||||||
|
case "OpenRestyCacheInactive", "OpenRestyCacheLockTimeout":
|
||||||
|
if openRestyDurationTokenPattern.MatchString(trimmed) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("%s 格式必须为带单位的时长,例如 30m 或 5s", key)
|
||||||
|
case "OpenRestyCacheKeyTemplate":
|
||||||
|
if trimmed == "" {
|
||||||
|
return fmt.Errorf("%s 不能为空", key)
|
||||||
|
}
|
||||||
|
if strings.ContainsAny(trimmed, "\r\n") {
|
||||||
|
return fmt.Errorf("%s 不能包含换行", key)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
case "OpenRestyCacheUseStale":
|
||||||
|
if trimmed == "" {
|
||||||
|
return fmt.Errorf("%s 不能为空", key)
|
||||||
|
}
|
||||||
|
allowedTokens := map[string]struct{}{
|
||||||
|
"error": {}, "timeout": {}, "invalid_header": {}, "updating": {},
|
||||||
|
"http_500": {}, "http_502": {}, "http_503": {}, "http_504": {},
|
||||||
|
"http_403": {}, "http_404": {}, "http_429": {}, "off": {},
|
||||||
|
}
|
||||||
|
for _, token := range strings.Fields(trimmed) {
|
||||||
|
if _, ok := allowedTokens[token]; !ok {
|
||||||
|
return fmt.Errorf("%s 包含不支持的值 %q", key, token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// GetOptions godoc
|
// GetOptions godoc
|
||||||
// @Summary List editable options
|
// @Summary List editable options
|
||||||
// @Tags Options
|
// @Tags Options
|
||||||
@@ -114,6 +238,13 @@ func UpdateOption(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if err = validateOpenRestyOption(option.Key, option.Value); err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
err = model.UpdateOption(option.Key, option.Value)
|
err = model.UpdateOption(option.Key, option.Value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestValidateOpenRestyOption(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
key string
|
||||||
|
value string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{name: "worker processes auto", key: "OpenRestyWorkerProcesses", value: "auto"},
|
||||||
|
{name: "worker processes number", key: "OpenRestyWorkerProcesses", value: "8"},
|
||||||
|
{name: "worker processes invalid", key: "OpenRestyWorkerProcesses", value: "0", wantErr: true},
|
||||||
|
{name: "events use empty", key: "OpenRestyEventsUse", value: ""},
|
||||||
|
{name: "events use invalid", key: "OpenRestyEventsUse", value: "io_uring", wantErr: true},
|
||||||
|
{name: "proxy buffers valid", key: "OpenRestyProxyBuffers", value: "16 16k"},
|
||||||
|
{name: "proxy buffers invalid", key: "OpenRestyProxyBuffers", value: "16x16k", wantErr: true},
|
||||||
|
{name: "cache max size valid", key: "OpenRestyCacheMaxSize", value: "2g"},
|
||||||
|
{name: "cache max size invalid", key: "OpenRestyCacheMaxSize", value: "2gb", wantErr: true},
|
||||||
|
{name: "cache inactive valid", key: "OpenRestyCacheInactive", value: "30m"},
|
||||||
|
{name: "cache inactive invalid", key: "OpenRestyCacheInactive", value: "30", wantErr: true},
|
||||||
|
{name: "cache use stale valid", key: "OpenRestyCacheUseStale", value: "error timeout http_500"},
|
||||||
|
{name: "cache use stale invalid", key: "OpenRestyCacheUseStale", value: "error whatever", wantErr: true},
|
||||||
|
{name: "gzip level valid", key: "OpenRestyGzipCompLevel", value: "9"},
|
||||||
|
{name: "gzip level invalid", key: "OpenRestyGzipCompLevel", value: "10", wantErr: true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
err := validateOpenRestyOption(testCase.key, testCase.value)
|
||||||
|
if testCase.wantErr && err == nil {
|
||||||
|
t.Fatalf("%s: expected error", testCase.name)
|
||||||
|
}
|
||||||
|
if !testCase.wantErr && err != nil {
|
||||||
|
t.Fatalf("%s: unexpected error: %v", testCase.name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -55,6 +55,35 @@ func InitOptionMap() {
|
|||||||
common.OptionMap["AgentSyncInterval"] = strconv.Itoa(common.AgentSyncInterval)
|
common.OptionMap["AgentSyncInterval"] = strconv.Itoa(common.AgentSyncInterval)
|
||||||
common.OptionMap["NodeOfflineThreshold"] = strconv.Itoa(int(common.NodeOfflineThreshold.Milliseconds()))
|
common.OptionMap["NodeOfflineThreshold"] = strconv.Itoa(int(common.NodeOfflineThreshold.Milliseconds()))
|
||||||
common.OptionMap["AgentUpdateRepo"] = common.AgentUpdateRepo
|
common.OptionMap["AgentUpdateRepo"] = common.AgentUpdateRepo
|
||||||
|
common.OptionMap["OpenRestyWorkerProcesses"] = common.OpenRestyWorkerProcesses
|
||||||
|
common.OptionMap["OpenRestyWorkerConnections"] = strconv.Itoa(common.OpenRestyWorkerConnections)
|
||||||
|
common.OptionMap["OpenRestyWorkerRlimitNofile"] = strconv.Itoa(common.OpenRestyWorkerRlimitNofile)
|
||||||
|
common.OptionMap["OpenRestyEventsUse"] = common.OpenRestyEventsUse
|
||||||
|
common.OptionMap["OpenRestyEventsMultiAcceptEnabled"] = strconv.FormatBool(common.OpenRestyEventsMultiAcceptEnabled)
|
||||||
|
common.OptionMap["OpenRestyKeepaliveTimeout"] = strconv.Itoa(common.OpenRestyKeepaliveTimeout)
|
||||||
|
common.OptionMap["OpenRestyKeepaliveRequests"] = strconv.Itoa(common.OpenRestyKeepaliveRequests)
|
||||||
|
common.OptionMap["OpenRestyClientHeaderTimeout"] = strconv.Itoa(common.OpenRestyClientHeaderTimeout)
|
||||||
|
common.OptionMap["OpenRestyClientBodyTimeout"] = strconv.Itoa(common.OpenRestyClientBodyTimeout)
|
||||||
|
common.OptionMap["OpenRestySendTimeout"] = strconv.Itoa(common.OpenRestySendTimeout)
|
||||||
|
common.OptionMap["OpenRestyProxyConnectTimeout"] = strconv.Itoa(common.OpenRestyProxyConnectTimeout)
|
||||||
|
common.OptionMap["OpenRestyProxySendTimeout"] = strconv.Itoa(common.OpenRestyProxySendTimeout)
|
||||||
|
common.OptionMap["OpenRestyProxyReadTimeout"] = strconv.Itoa(common.OpenRestyProxyReadTimeout)
|
||||||
|
common.OptionMap["OpenRestyProxyBufferingEnabled"] = strconv.FormatBool(common.OpenRestyProxyBufferingEnabled)
|
||||||
|
common.OptionMap["OpenRestyProxyBuffers"] = common.OpenRestyProxyBuffers
|
||||||
|
common.OptionMap["OpenRestyProxyBufferSize"] = common.OpenRestyProxyBufferSize
|
||||||
|
common.OptionMap["OpenRestyProxyBusyBuffersSize"] = common.OpenRestyProxyBusyBuffersSize
|
||||||
|
common.OptionMap["OpenRestyGzipEnabled"] = strconv.FormatBool(common.OpenRestyGzipEnabled)
|
||||||
|
common.OptionMap["OpenRestyGzipMinLength"] = strconv.Itoa(common.OpenRestyGzipMinLength)
|
||||||
|
common.OptionMap["OpenRestyGzipCompLevel"] = strconv.Itoa(common.OpenRestyGzipCompLevel)
|
||||||
|
common.OptionMap["OpenRestyCacheEnabled"] = strconv.FormatBool(common.OpenRestyCacheEnabled)
|
||||||
|
common.OptionMap["OpenRestyCachePath"] = common.OpenRestyCachePath
|
||||||
|
common.OptionMap["OpenRestyCacheLevels"] = common.OpenRestyCacheLevels
|
||||||
|
common.OptionMap["OpenRestyCacheInactive"] = common.OpenRestyCacheInactive
|
||||||
|
common.OptionMap["OpenRestyCacheMaxSize"] = common.OpenRestyCacheMaxSize
|
||||||
|
common.OptionMap["OpenRestyCacheKeyTemplate"] = common.OpenRestyCacheKeyTemplate
|
||||||
|
common.OptionMap["OpenRestyCacheLockEnabled"] = strconv.FormatBool(common.OpenRestyCacheLockEnabled)
|
||||||
|
common.OptionMap["OpenRestyCacheLockTimeout"] = common.OpenRestyCacheLockTimeout
|
||||||
|
common.OptionMap["OpenRestyCacheUseStale"] = common.OpenRestyCacheUseStale
|
||||||
common.OptionMap["GlobalApiRateLimitNum"] = strconv.Itoa(common.GlobalApiRateLimitNum)
|
common.OptionMap["GlobalApiRateLimitNum"] = strconv.Itoa(common.GlobalApiRateLimitNum)
|
||||||
common.OptionMap["GlobalApiRateLimitDuration"] = strconv.FormatInt(common.GlobalApiRateLimitDuration, 10)
|
common.OptionMap["GlobalApiRateLimitDuration"] = strconv.FormatInt(common.GlobalApiRateLimitDuration, 10)
|
||||||
common.OptionMap["GlobalWebRateLimitNum"] = strconv.Itoa(common.GlobalWebRateLimitNum)
|
common.OptionMap["GlobalWebRateLimitNum"] = strconv.Itoa(common.GlobalWebRateLimitNum)
|
||||||
@@ -175,6 +204,108 @@ func updateOptionMap(key string, value string) {
|
|||||||
if value != "" {
|
if value != "" {
|
||||||
common.AgentUpdateRepo = value
|
common.AgentUpdateRepo = value
|
||||||
}
|
}
|
||||||
|
case "OpenRestyWorkerProcesses":
|
||||||
|
if strings.TrimSpace(value) != "" {
|
||||||
|
common.OpenRestyWorkerProcesses = value
|
||||||
|
}
|
||||||
|
case "OpenRestyWorkerConnections":
|
||||||
|
if v, err := strconv.Atoi(value); err == nil && v > 0 {
|
||||||
|
common.OpenRestyWorkerConnections = v
|
||||||
|
}
|
||||||
|
case "OpenRestyWorkerRlimitNofile":
|
||||||
|
if v, err := strconv.Atoi(value); err == nil && v > 0 {
|
||||||
|
common.OpenRestyWorkerRlimitNofile = v
|
||||||
|
}
|
||||||
|
case "OpenRestyEventsUse":
|
||||||
|
common.OpenRestyEventsUse = value
|
||||||
|
case "OpenRestyEventsMultiAcceptEnabled":
|
||||||
|
common.OpenRestyEventsMultiAcceptEnabled = value == "true"
|
||||||
|
case "OpenRestyKeepaliveTimeout":
|
||||||
|
if v, err := strconv.Atoi(value); err == nil && v > 0 {
|
||||||
|
common.OpenRestyKeepaliveTimeout = v
|
||||||
|
}
|
||||||
|
case "OpenRestyKeepaliveRequests":
|
||||||
|
if v, err := strconv.Atoi(value); err == nil && v > 0 {
|
||||||
|
common.OpenRestyKeepaliveRequests = v
|
||||||
|
}
|
||||||
|
case "OpenRestyClientHeaderTimeout":
|
||||||
|
if v, err := strconv.Atoi(value); err == nil && v > 0 {
|
||||||
|
common.OpenRestyClientHeaderTimeout = v
|
||||||
|
}
|
||||||
|
case "OpenRestyClientBodyTimeout":
|
||||||
|
if v, err := strconv.Atoi(value); err == nil && v > 0 {
|
||||||
|
common.OpenRestyClientBodyTimeout = v
|
||||||
|
}
|
||||||
|
case "OpenRestySendTimeout":
|
||||||
|
if v, err := strconv.Atoi(value); err == nil && v > 0 {
|
||||||
|
common.OpenRestySendTimeout = v
|
||||||
|
}
|
||||||
|
case "OpenRestyProxyConnectTimeout":
|
||||||
|
if v, err := strconv.Atoi(value); err == nil && v > 0 {
|
||||||
|
common.OpenRestyProxyConnectTimeout = v
|
||||||
|
}
|
||||||
|
case "OpenRestyProxySendTimeout":
|
||||||
|
if v, err := strconv.Atoi(value); err == nil && v > 0 {
|
||||||
|
common.OpenRestyProxySendTimeout = v
|
||||||
|
}
|
||||||
|
case "OpenRestyProxyReadTimeout":
|
||||||
|
if v, err := strconv.Atoi(value); err == nil && v > 0 {
|
||||||
|
common.OpenRestyProxyReadTimeout = v
|
||||||
|
}
|
||||||
|
case "OpenRestyProxyBufferingEnabled":
|
||||||
|
common.OpenRestyProxyBufferingEnabled = value == "true"
|
||||||
|
case "OpenRestyProxyBuffers":
|
||||||
|
if strings.TrimSpace(value) != "" {
|
||||||
|
common.OpenRestyProxyBuffers = value
|
||||||
|
}
|
||||||
|
case "OpenRestyProxyBufferSize":
|
||||||
|
if strings.TrimSpace(value) != "" {
|
||||||
|
common.OpenRestyProxyBufferSize = value
|
||||||
|
}
|
||||||
|
case "OpenRestyProxyBusyBuffersSize":
|
||||||
|
if strings.TrimSpace(value) != "" {
|
||||||
|
common.OpenRestyProxyBusyBuffersSize = value
|
||||||
|
}
|
||||||
|
case "OpenRestyGzipEnabled":
|
||||||
|
common.OpenRestyGzipEnabled = value == "true"
|
||||||
|
case "OpenRestyGzipMinLength":
|
||||||
|
if v, err := strconv.Atoi(value); err == nil && v > 0 {
|
||||||
|
common.OpenRestyGzipMinLength = v
|
||||||
|
}
|
||||||
|
case "OpenRestyGzipCompLevel":
|
||||||
|
if v, err := strconv.Atoi(value); err == nil && v > 0 {
|
||||||
|
common.OpenRestyGzipCompLevel = v
|
||||||
|
}
|
||||||
|
case "OpenRestyCacheEnabled":
|
||||||
|
common.OpenRestyCacheEnabled = value == "true"
|
||||||
|
case "OpenRestyCachePath":
|
||||||
|
common.OpenRestyCachePath = value
|
||||||
|
case "OpenRestyCacheLevels":
|
||||||
|
if strings.TrimSpace(value) != "" {
|
||||||
|
common.OpenRestyCacheLevels = value
|
||||||
|
}
|
||||||
|
case "OpenRestyCacheInactive":
|
||||||
|
if strings.TrimSpace(value) != "" {
|
||||||
|
common.OpenRestyCacheInactive = value
|
||||||
|
}
|
||||||
|
case "OpenRestyCacheMaxSize":
|
||||||
|
if strings.TrimSpace(value) != "" {
|
||||||
|
common.OpenRestyCacheMaxSize = value
|
||||||
|
}
|
||||||
|
case "OpenRestyCacheKeyTemplate":
|
||||||
|
if strings.TrimSpace(value) != "" {
|
||||||
|
common.OpenRestyCacheKeyTemplate = value
|
||||||
|
}
|
||||||
|
case "OpenRestyCacheLockEnabled":
|
||||||
|
common.OpenRestyCacheLockEnabled = value == "true"
|
||||||
|
case "OpenRestyCacheLockTimeout":
|
||||||
|
if strings.TrimSpace(value) != "" {
|
||||||
|
common.OpenRestyCacheLockTimeout = value
|
||||||
|
}
|
||||||
|
case "OpenRestyCacheUseStale":
|
||||||
|
if strings.TrimSpace(value) != "" {
|
||||||
|
common.OpenRestyCacheUseStale = value
|
||||||
|
}
|
||||||
case "GlobalApiRateLimitNum":
|
case "GlobalApiRateLimitNum":
|
||||||
if v, err := strconv.Atoi(value); err == nil && v > 0 {
|
if v, err := strconv.Atoi(value); err == nil && v > 0 {
|
||||||
common.GlobalApiRateLimitNum = v
|
common.GlobalApiRateLimitNum = v
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import {
|
|||||||
PrimaryButton,
|
PrimaryButton,
|
||||||
ResourceField,
|
ResourceField,
|
||||||
ResourceInput,
|
ResourceInput,
|
||||||
|
ResourceSelect,
|
||||||
ResourceTextarea,
|
ResourceTextarea,
|
||||||
SecondaryButton,
|
SecondaryButton,
|
||||||
ToggleField,
|
ToggleField,
|
||||||
@@ -72,6 +73,36 @@ const defaultOperationFields = {
|
|||||||
AgentSyncInterval: '30000',
|
AgentSyncInterval: '30000',
|
||||||
NodeOfflineThreshold: '120000',
|
NodeOfflineThreshold: '120000',
|
||||||
AgentUpdateRepo: 'Rain-kl/ATSFlare',
|
AgentUpdateRepo: 'Rain-kl/ATSFlare',
|
||||||
|
OpenRestyWorkerProcesses: 'auto',
|
||||||
|
OpenRestyWorkerConnections: '4096',
|
||||||
|
OpenRestyWorkerRlimitNofile: '65535',
|
||||||
|
OpenRestyEventsUse: '',
|
||||||
|
OpenRestyEventsMultiAcceptEnabled: false,
|
||||||
|
OpenRestyKeepaliveTimeout: '65',
|
||||||
|
OpenRestyKeepaliveRequests: '1000',
|
||||||
|
OpenRestyClientHeaderTimeout: '15',
|
||||||
|
OpenRestyClientBodyTimeout: '15',
|
||||||
|
OpenRestySendTimeout: '30',
|
||||||
|
OpenRestyProxyConnectTimeout: '5',
|
||||||
|
OpenRestyProxySendTimeout: '60',
|
||||||
|
OpenRestyProxyReadTimeout: '60',
|
||||||
|
OpenRestyProxyBufferingEnabled: true,
|
||||||
|
OpenRestyProxyBuffers: '16 16k',
|
||||||
|
OpenRestyProxyBufferSize: '8k',
|
||||||
|
OpenRestyProxyBusyBuffersSize: '64k',
|
||||||
|
OpenRestyGzipEnabled: true,
|
||||||
|
OpenRestyGzipMinLength: '1024',
|
||||||
|
OpenRestyGzipCompLevel: '5',
|
||||||
|
OpenRestyCacheEnabled: false,
|
||||||
|
OpenRestyCachePath: '',
|
||||||
|
OpenRestyCacheLevels: '1:2',
|
||||||
|
OpenRestyCacheInactive: '30m',
|
||||||
|
OpenRestyCacheMaxSize: '1g',
|
||||||
|
OpenRestyCacheKeyTemplate: '$scheme$proxy_host$request_uri',
|
||||||
|
OpenRestyCacheLockEnabled: true,
|
||||||
|
OpenRestyCacheLockTimeout: '5s',
|
||||||
|
OpenRestyCacheUseStale:
|
||||||
|
'error timeout updating http_500 http_502 http_503 http_504',
|
||||||
GlobalApiRateLimitNum: '300',
|
GlobalApiRateLimitNum: '300',
|
||||||
GlobalApiRateLimitDuration: '180',
|
GlobalApiRateLimitDuration: '180',
|
||||||
GlobalWebRateLimitNum: '300',
|
GlobalWebRateLimitNum: '300',
|
||||||
@@ -170,6 +201,27 @@ function formatSecondsLabel(value: string) {
|
|||||||
return `${seconds} 秒`;
|
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) {
|
function buildDiscoveryCommand(serverUrl: string, discoveryToken: string) {
|
||||||
return [
|
return [
|
||||||
`curl -fsSL ${installerScriptUrl} | bash -s -- \\`,
|
`curl -fsSL ${installerScriptUrl} | bash -s -- \\`,
|
||||||
@@ -309,6 +361,53 @@ export function SettingsPage() {
|
|||||||
AgentSyncInterval: optionMap.AgentSyncInterval ?? '30000',
|
AgentSyncInterval: optionMap.AgentSyncInterval ?? '30000',
|
||||||
NodeOfflineThreshold: optionMap.NodeOfflineThreshold ?? '120000',
|
NodeOfflineThreshold: optionMap.NodeOfflineThreshold ?? '120000',
|
||||||
AgentUpdateRepo: optionMap.AgentUpdateRepo ?? 'Rain-kl/ATSFlare',
|
AgentUpdateRepo: optionMap.AgentUpdateRepo ?? 'Rain-kl/ATSFlare',
|
||||||
|
OpenRestyWorkerProcesses: optionMap.OpenRestyWorkerProcesses ?? 'auto',
|
||||||
|
OpenRestyWorkerConnections:
|
||||||
|
optionMap.OpenRestyWorkerConnections ?? '4096',
|
||||||
|
OpenRestyWorkerRlimitNofile:
|
||||||
|
optionMap.OpenRestyWorkerRlimitNofile ?? '65535',
|
||||||
|
OpenRestyEventsUse: optionMap.OpenRestyEventsUse ?? '',
|
||||||
|
OpenRestyEventsMultiAcceptEnabled: toBoolean(
|
||||||
|
optionMap.OpenRestyEventsMultiAcceptEnabled,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
OpenRestyKeepaliveTimeout: optionMap.OpenRestyKeepaliveTimeout ?? '65',
|
||||||
|
OpenRestyKeepaliveRequests:
|
||||||
|
optionMap.OpenRestyKeepaliveRequests ?? '1000',
|
||||||
|
OpenRestyClientHeaderTimeout:
|
||||||
|
optionMap.OpenRestyClientHeaderTimeout ?? '15',
|
||||||
|
OpenRestyClientBodyTimeout: optionMap.OpenRestyClientBodyTimeout ?? '15',
|
||||||
|
OpenRestySendTimeout: optionMap.OpenRestySendTimeout ?? '30',
|
||||||
|
OpenRestyProxyConnectTimeout:
|
||||||
|
optionMap.OpenRestyProxyConnectTimeout ?? '5',
|
||||||
|
OpenRestyProxySendTimeout: optionMap.OpenRestyProxySendTimeout ?? '60',
|
||||||
|
OpenRestyProxyReadTimeout: optionMap.OpenRestyProxyReadTimeout ?? '60',
|
||||||
|
OpenRestyProxyBufferingEnabled: toBoolean(
|
||||||
|
optionMap.OpenRestyProxyBufferingEnabled,
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
OpenRestyProxyBuffers: optionMap.OpenRestyProxyBuffers ?? '16 16k',
|
||||||
|
OpenRestyProxyBufferSize: optionMap.OpenRestyProxyBufferSize ?? '8k',
|
||||||
|
OpenRestyProxyBusyBuffersSize:
|
||||||
|
optionMap.OpenRestyProxyBusyBuffersSize ?? '64k',
|
||||||
|
OpenRestyGzipEnabled: toBoolean(optionMap.OpenRestyGzipEnabled, true),
|
||||||
|
OpenRestyGzipMinLength: optionMap.OpenRestyGzipMinLength ?? '1024',
|
||||||
|
OpenRestyGzipCompLevel: optionMap.OpenRestyGzipCompLevel ?? '5',
|
||||||
|
OpenRestyCacheEnabled: toBoolean(optionMap.OpenRestyCacheEnabled, false),
|
||||||
|
OpenRestyCachePath: optionMap.OpenRestyCachePath ?? '',
|
||||||
|
OpenRestyCacheLevels: optionMap.OpenRestyCacheLevels ?? '1:2',
|
||||||
|
OpenRestyCacheInactive: optionMap.OpenRestyCacheInactive ?? '30m',
|
||||||
|
OpenRestyCacheMaxSize: optionMap.OpenRestyCacheMaxSize ?? '1g',
|
||||||
|
OpenRestyCacheKeyTemplate:
|
||||||
|
optionMap.OpenRestyCacheKeyTemplate ?? '$scheme$proxy_host$request_uri',
|
||||||
|
OpenRestyCacheLockEnabled: toBoolean(
|
||||||
|
optionMap.OpenRestyCacheLockEnabled,
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
OpenRestyCacheLockTimeout: optionMap.OpenRestyCacheLockTimeout ?? '5s',
|
||||||
|
OpenRestyCacheUseStale:
|
||||||
|
optionMap.OpenRestyCacheUseStale ??
|
||||||
|
'error timeout updating http_500 http_502 http_503 http_504',
|
||||||
GlobalApiRateLimitNum: optionMap.GlobalApiRateLimitNum ?? '300',
|
GlobalApiRateLimitNum: optionMap.GlobalApiRateLimitNum ?? '300',
|
||||||
GlobalApiRateLimitDuration: optionMap.GlobalApiRateLimitDuration ?? '180',
|
GlobalApiRateLimitDuration: optionMap.GlobalApiRateLimitDuration ?? '180',
|
||||||
GlobalWebRateLimitNum: optionMap.GlobalWebRateLimitNum ?? '300',
|
GlobalWebRateLimitNum: optionMap.GlobalWebRateLimitNum ?? '300',
|
||||||
@@ -987,6 +1086,249 @@ export function SettingsPage() {
|
|||||||
/>
|
/>
|
||||||
</ResourceField>
|
</ResourceField>
|
||||||
</AppCard>
|
</AppCard>
|
||||||
|
<AppCard
|
||||||
|
title="OpenResty 连接与事件"
|
||||||
|
description="第五版第一批结构化性能项。保存后进入统一发布链路,不会在节点即时生效。"
|
||||||
|
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'}
|
||||||
|
>
|
||||||
|
{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>
|
||||||
|
</AppCard>
|
||||||
<AppCard
|
<AppCard
|
||||||
title="Discovery Token 与部署命令"
|
title="Discovery Token 与部署命令"
|
||||||
description="适用于新节点首次接入。可直接复制一键安装命令。"
|
description="适用于新节点首次接入。可直接复制一键安装命令。"
|
||||||
@@ -1036,10 +1378,10 @@ export function SettingsPage() {
|
|||||||
/>
|
/>
|
||||||
</ResourceField>
|
</ResourceField>
|
||||||
<div className="rounded-2xl border border-[var(--border-default)] bg-[var(--surface-elevated)] px-4 py-4">
|
<div className="rounded-2xl border border-[var(--border-default)] bg-[var(--surface-elevated)] px-4 py-4">
|
||||||
<p className="text-xs uppercase tracking-[0.2em] text-[var(--foreground-muted)]">
|
<p className="text-xs tracking-[0.2em] text-[var(--foreground-muted)] uppercase">
|
||||||
Discovery Token
|
Discovery Token
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-2 break-all text-sm text-[var(--foreground-primary)]">
|
<p className="mt-2 text-sm break-all text-[var(--foreground-primary)]">
|
||||||
{discoveryToken || '未生成'}
|
{discoveryToken || '未生成'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -1433,6 +1775,462 @@ export function SettingsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</AppCard>
|
</AppCard>
|
||||||
</div>
|
</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
|
<AppCard
|
||||||
title="请求限流设置"
|
title="请求限流设置"
|
||||||
description="按来源 IP 生效,保存后立即影响 Web、API、上传下载及登录注册等敏感接口。时间单位均为秒。"
|
description="按来源 IP 生效,保存后立即影响 Web、API、上传下载及登录注册等敏感接口。时间单位均为秒。"
|
||||||
@@ -1819,7 +2617,7 @@ export function SettingsPage() {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="设置"
|
title="设置"
|
||||||
description="阶段 4 已迁移个人设置、系统设置与运维设置入口,并补齐运行参数、限流和品牌内容配置。"
|
description="第五版从这里开始补齐 OpenResty 性能参数、Agent 运行参数和系统级运维配置。"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{feedback ? (
|
{feedback ? (
|
||||||
|
|||||||
+4
-4
@@ -13,7 +13,7 @@ Server 当前支持两类启动配置:
|
|||||||
|
|
||||||
此外,部分运行时参数已迁入数据库 `Option` 表,可在管理端设置页中热更新,例如 Agent 运行参数与限流阈值。
|
此外,部分运行时参数已迁入数据库 `Option` 表,可在管理端设置页中热更新,例如 Agent 运行参数与限流阈值。
|
||||||
|
|
||||||
第五版(0.5.x)计划继续复用 `Option` 表承载 OpenResty 性能优化参数与缓存参数,不单独引入新的配置中心。
|
第五版(0.5.x)继续复用 `Option` 表承载 OpenResty 性能优化参数与缓存参数,不单独引入新的配置中心。
|
||||||
|
|
||||||
### 1.1 Server 命令行参数
|
### 1.1 Server 命令行参数
|
||||||
|
|
||||||
@@ -119,9 +119,9 @@ volumes:
|
|||||||
* 限流窗口上限不能超过 `RateLimitKeyExpirationDuration`,当前为 20 分钟
|
* 限流窗口上限不能超过 `RateLimitKeyExpirationDuration`,当前为 20 分钟
|
||||||
* 限流按来源 IP 统计,若前置了 Nginx/CDN/LB,应正确透传真实客户端 IP
|
* 限流按来源 IP 统计,若前置了 Nginx/CDN/LB,应正确透传真实客户端 IP
|
||||||
|
|
||||||
### 1.2.2 第五版规划中的 OpenResty 优化配置项
|
### 1.2.2 第五版当前支持的 OpenResty 优化配置项
|
||||||
|
|
||||||
以下配置项用于第五版开发文档定义,目标是在管理端「运维设置」中统一维护,并参与版本渲染。除特别说明外,均计划保存在 `Option` 表中。
|
以下配置项当前已接入管理端「运维设置」,并统一保存在 `Option` 表中。它们会参与第五版后续的配置渲染与发布链路;当前阶段优先完成参数录入、默认值和校验能力。
|
||||||
|
|
||||||
| 配置项 | 作用 | 计划默认值 |
|
| 配置项 | 作用 | 计划默认值 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
@@ -157,7 +157,7 @@ volumes:
|
|||||||
|
|
||||||
说明:
|
说明:
|
||||||
|
|
||||||
* 第五版第一批仅开放稳定、可校验、可回滚的常用性能项;更多指令后续按相同模式扩展
|
* 第五版第一批当前仅开放稳定、可校验、可回滚的常用性能项;更多指令后续按相同模式扩展
|
||||||
* 所有大小、时长、布尔和整数参数都应在 Server 保存前完成校验
|
* 所有大小、时长、布尔和整数参数都应在 Server 保存前完成校验
|
||||||
* `OpenRestyCacheEnabled=false` 时,缓存目录与缓存参数应允许留空或回退到默认值
|
* `OpenRestyCacheEnabled=false` 时,缓存目录与缓存参数应允许留空或回退到默认值
|
||||||
* 任何包含路径的配置项都必须在 Agent 落盘前再次校验可写性与安全边界
|
* 任何包含路径的配置项都必须在 Agent 落盘前再次校验可写性与安全边界
|
||||||
|
|||||||
Reference in New Issue
Block a user