From 6a73f6e333966b40fa22aad42fb197dbd90f8dcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=97=E5=AD=90?= Date: Thu, 29 Jan 2026 20:06:44 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=8F=8D=E5=90=91=E4=BB=A3=E7=90=86?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E6=9B=B4=E5=A4=9A=E8=AE=BE=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/webserver/nginx/proxy.go | 270 +++++++++++++++- pkg/webserver/types/proxy.go | 73 ++++- web/src/views/website/EditView.vue | 481 ++++++++++++++++++++++++++++- 3 files changed, 812 insertions(+), 12 deletions(-) diff --git a/pkg/webserver/nginx/proxy.go b/pkg/webserver/nginx/proxy.go index a97eb966..1fdbb21c 100644 --- a/pkg/webserver/nginx/proxy.go +++ b/pkg/webserver/nginx/proxy.go @@ -17,6 +17,66 @@ import ( // proxyFilePattern 匹配代理配置文件名 (200-299) var proxyFilePattern = regexp.MustCompile(`^(\d{3})-proxy\.conf$`) +// parseDurationFromNginx 从 Nginx 时间格式解析为 time.Duration +func parseDurationFromNginx(valueStr, unit string) time.Duration { + value, _ := strconv.Atoi(valueStr) + switch unit { + case "m": + return time.Duration(value) * time.Minute + case "h": + return time.Duration(value) * time.Hour + default: + return time.Duration(value) * time.Second + } +} + +// parseSizeToBytes 解析大小字符串为字节数 +func parseSizeToBytes(valueStr, unit string) int64 { + value, _ := strconv.ParseInt(valueStr, 10, 64) + switch strings.ToLower(unit) { + case "k": + return value * 1024 + case "m": + return value * 1024 * 1024 + case "g": + return value * 1024 * 1024 * 1024 + default: + return value + } +} + +// formatBytesToNginx 格式化字节数为 Nginx 大小格式 +func formatBytesToNginx(bytes int64) string { + if bytes == 0 { + return "0" + } + if bytes%(1024*1024*1024) == 0 { + return fmt.Sprintf("%dg", bytes/(1024*1024*1024)) + } + if bytes%(1024*1024) == 0 { + return fmt.Sprintf("%dm", bytes/(1024*1024)) + } + if bytes%1024 == 0 { + return fmt.Sprintf("%dk", bytes/1024) + } + return fmt.Sprintf("%d", bytes) +} + +// formatDurationToNginx 格式化 time.Duration 为 Nginx 时间格式 +func formatDurationToNginx(d time.Duration) string { + if d == 0 { + return "0s" + } + seconds := int(d.Seconds()) + if seconds%3600 == 0 { + return fmt.Sprintf("%dh", seconds/3600) + } + if seconds%60 == 0 { + return fmt.Sprintf("%dm", seconds/60) + } + return fmt.Sprintf("%ds", seconds) +} + // parseProxyFiles 从 site 目录解析所有代理配置 func parseProxyFiles(siteDir string) ([]types.Proxy, error) { entries, err := os.ReadDir(siteDir) @@ -227,6 +287,146 @@ func parseProxyFile(filePath string) (*types.Proxy, error) { } } + // 解析 proxy_http_version + httpVersionPattern := regexp.MustCompile(`proxy_http_version\s+([\d.]+);`) + if hvm := httpVersionPattern.FindStringSubmatch(blockContent); hvm != nil { + proxy.HTTPVersion = strings.TrimSpace(hvm[1]) + } + + // 解析超时配置 + connectTimeoutPattern := regexp.MustCompile(`proxy_connect_timeout\s+(\d+)([smh]?);`) + readTimeoutPattern := regexp.MustCompile(`proxy_read_timeout\s+(\d+)([smh]?);`) + sendTimeoutPattern := regexp.MustCompile(`proxy_send_timeout\s+(\d+)([smh]?);`) + + var timeout types.TimeoutConfig + hasTimeout := false + + if ctm := connectTimeoutPattern.FindStringSubmatch(blockContent); ctm != nil { + timeout.Connect = parseDurationFromNginx(ctm[1], ctm[2]) + hasTimeout = true + } + if rtm := readTimeoutPattern.FindStringSubmatch(blockContent); rtm != nil { + timeout.Read = parseDurationFromNginx(rtm[1], rtm[2]) + hasTimeout = true + } + if stm := sendTimeoutPattern.FindStringSubmatch(blockContent); stm != nil { + timeout.Send = parseDurationFromNginx(stm[1], stm[2]) + hasTimeout = true + } + if hasTimeout { + proxy.Timeout = &timeout + } + + // 解析重试配置 + nextUpstreamPattern := regexp.MustCompile(`proxy_next_upstream\s+([^;]+);`) + nextUpstreamTriesPattern := regexp.MustCompile(`proxy_next_upstream_tries\s+(\d+);`) + nextUpstreamTimeoutPattern := regexp.MustCompile(`proxy_next_upstream_timeout\s+(\d+)([smh]?);`) + + var retry types.RetryConfig + hasRetry := false + + if num := nextUpstreamPattern.FindStringSubmatch(blockContent); num != nil { + retry.Conditions = strings.Fields(num[1]) + hasRetry = true + } + if nutm := nextUpstreamTriesPattern.FindStringSubmatch(blockContent); nutm != nil { + retry.Tries, _ = strconv.Atoi(nutm[1]) + hasRetry = true + } + if nutom := nextUpstreamTimeoutPattern.FindStringSubmatch(blockContent); nutom != nil { + retry.Timeout = parseDurationFromNginx(nutom[1], nutom[2]) + hasRetry = true + } + if hasRetry { + proxy.Retry = &retry + } + + // 解析 client_max_body_size + clientMaxBodySizePattern := regexp.MustCompile(`client_max_body_size\s+(\d+)([kmgKMG]?);`) + if cmbsm := clientMaxBodySizePattern.FindStringSubmatch(blockContent); cmbsm != nil { + proxy.ClientMaxBodySize = parseSizeToBytes(cmbsm[1], cmbsm[2]) + } + + // 解析 SSL 后端验证配置 + sslVerifyPattern := regexp.MustCompile(`proxy_ssl_verify\s+(on|off);`) + sslTrustedCertPattern := regexp.MustCompile(`proxy_ssl_trusted_certificate\s+([^;]+);`) + sslVerifyDepthPattern := regexp.MustCompile(`proxy_ssl_verify_depth\s+(\d+);`) + + var sslBackend types.SSLBackendConfig + hasSSLBackend := false + + if svm := sslVerifyPattern.FindStringSubmatch(blockContent); svm != nil { + sslBackend.Verify = svm[1] == "on" + hasSSLBackend = true + } + if stcm := sslTrustedCertPattern.FindStringSubmatch(blockContent); stcm != nil { + sslBackend.TrustedCertificate = strings.TrimSpace(stcm[1]) + hasSSLBackend = true + } + if svdm := sslVerifyDepthPattern.FindStringSubmatch(blockContent); svdm != nil { + sslBackend.VerifyDepth, _ = strconv.Atoi(svdm[1]) + hasSSLBackend = true + } + if hasSSLBackend { + proxy.SSLBackend = &sslBackend + } + + // 解析响应头配置 + hideHeaderPattern := regexp.MustCompile(`proxy_hide_header\s+([^;]+);`) + addHeaderPattern := regexp.MustCompile(`add_header\s+(\S+)\s+"?([^";]+)"?(?:\s+always)?;`) + + var responseHeaders types.ResponseHeaderConfig + hasResponseHeaders := false + + hideHeaderMatches := hideHeaderPattern.FindAllStringSubmatch(blockContent, -1) + if len(hideHeaderMatches) > 0 { + responseHeaders.Hide = []string{} + for _, hhm := range hideHeaderMatches { + responseHeaders.Hide = append(responseHeaders.Hide, strings.TrimSpace(hhm[1])) + } + hasResponseHeaders = true + } + + addHeaderMatches := addHeaderPattern.FindAllStringSubmatch(blockContent, -1) + if len(addHeaderMatches) > 0 { + responseHeaders.Add = make(map[string]string) + for _, ahm := range addHeaderMatches { + responseHeaders.Add[strings.TrimSpace(ahm[1])] = strings.TrimSpace(ahm[2]) + } + hasResponseHeaders = true + } + if hasResponseHeaders { + proxy.ResponseHeaders = &responseHeaders + } + + // 解析 IP 访问控制 + allowPattern := regexp.MustCompile(`allow\s+([^;]+);`) + denyPattern := regexp.MustCompile(`deny\s+([^;]+);`) + + var accessControl types.AccessControlConfig + hasAccessControl := false + + allowMatches := allowPattern.FindAllStringSubmatch(blockContent, -1) + if len(allowMatches) > 0 { + accessControl.Allow = []string{} + for _, am := range allowMatches { + accessControl.Allow = append(accessControl.Allow, strings.TrimSpace(am[1])) + } + hasAccessControl = true + } + + denyMatches := denyPattern.FindAllStringSubmatch(blockContent, -1) + if len(denyMatches) > 0 { + accessControl.Deny = []string{} + for _, dm := range denyMatches { + accessControl.Deny = append(accessControl.Deny, strings.TrimSpace(dm[1])) + } + hasAccessControl = true + } + if hasAccessControl { + proxy.AccessControl = &accessControl + } + return proxy, nil } @@ -298,6 +498,21 @@ func generateProxyConfig(proxy types.Proxy) string { sb.WriteString(fmt.Sprintf("# Reverse proxy: %s -> %s\n", location, proxy.Pass)) sb.WriteString(fmt.Sprintf("location %s {\n", location)) + // IP 访问控制 + if proxy.AccessControl != nil { + for _, ip := range proxy.AccessControl.Allow { + sb.WriteString(fmt.Sprintf(" allow %s;\n", ip)) + } + for _, ip := range proxy.AccessControl.Deny { + sb.WriteString(fmt.Sprintf(" deny %s;\n", ip)) + } + } + + // 请求体大小限制 + if proxy.ClientMaxBodySize > 0 { + sb.WriteString(fmt.Sprintf(" client_max_body_size %s;\n", formatBytesToNginx(proxy.ClientMaxBodySize))) + } + // resolver 配置 if len(proxy.Resolver) > 0 { sb.WriteString(fmt.Sprintf(" resolver %s;\n", strings.Join(proxy.Resolver, " "))) @@ -307,7 +522,10 @@ func generateProxyConfig(proxy types.Proxy) string { } sb.WriteString(fmt.Sprintf(" proxy_pass %s;\n", proxy.Pass)) - sb.WriteString(" proxy_http_version 1.1;\n") + + // HTTP 协议版本 + httpVersion := lo.If(proxy.HTTPVersion != "", proxy.HTTPVersion).Else("1.1") + sb.WriteString(fmt.Sprintf(" proxy_http_version %s;\n", httpVersion)) // Host 头 host := lo.If(proxy.Host == "" || proxy.Host == "$proxy_host", "$proxy_host").ElseF(func() string { @@ -329,6 +547,43 @@ func generateProxyConfig(proxy types.Proxy) string { sb.WriteString(" proxy_ssl_session_reuse off;\n") sb.WriteString(" proxy_ssl_server_name on;\n") sb.WriteString(fmt.Sprintf(" proxy_ssl_name %s;\n", lo.If(proxy.SNI != "", proxy.SNI).Else("$proxy_host"))) + + // SSL 后端验证 + if proxy.SSLBackend != nil && proxy.SSLBackend.Verify { + sb.WriteString(" proxy_ssl_verify on;\n") + if proxy.SSLBackend.VerifyDepth > 0 { + sb.WriteString(fmt.Sprintf(" proxy_ssl_verify_depth %d;\n", proxy.SSLBackend.VerifyDepth)) + } + if proxy.SSLBackend.TrustedCertificate != "" { + sb.WriteString(fmt.Sprintf(" proxy_ssl_trusted_certificate %s;\n", proxy.SSLBackend.TrustedCertificate)) + } + } + } + + // 超时配置 + if proxy.Timeout != nil { + if proxy.Timeout.Connect > 0 { + sb.WriteString(fmt.Sprintf(" proxy_connect_timeout %s;\n", formatDurationToNginx(proxy.Timeout.Connect))) + } + if proxy.Timeout.Read > 0 { + sb.WriteString(fmt.Sprintf(" proxy_read_timeout %s;\n", formatDurationToNginx(proxy.Timeout.Read))) + } + if proxy.Timeout.Send > 0 { + sb.WriteString(fmt.Sprintf(" proxy_send_timeout %s;\n", formatDurationToNginx(proxy.Timeout.Send))) + } + } + + // 重试配置 + if proxy.Retry != nil { + if len(proxy.Retry.Conditions) > 0 { + sb.WriteString(fmt.Sprintf(" proxy_next_upstream %s;\n", strings.Join(proxy.Retry.Conditions, " "))) + } + if proxy.Retry.Tries > 0 { + sb.WriteString(fmt.Sprintf(" proxy_next_upstream_tries %d;\n", proxy.Retry.Tries)) + } + if proxy.Retry.Timeout > 0 { + sb.WriteString(fmt.Sprintf(" proxy_next_upstream_timeout %s;\n", formatDurationToNginx(proxy.Retry.Timeout))) + } } // Buffering 配置 @@ -405,6 +660,19 @@ func generateProxyConfig(proxy types.Proxy) string { } } + // 响应头修改 + if proxy.ResponseHeaders != nil { + // 隐藏响应头 + for _, header := range proxy.ResponseHeaders.Hide { + sb.WriteString(fmt.Sprintf(" proxy_hide_header %s;\n", header)) + } + // 添加响应头 + for name, value := range proxy.ResponseHeaders.Add { + formattedValue := lo.If(strings.HasPrefix(value, "$"), value).Else("\"" + value + "\"") + sb.WriteString(fmt.Sprintf(" add_header %s %s always;\n", name, formattedValue)) + } + } + sb.WriteString("}\n") return sb.String() diff --git a/pkg/webserver/types/proxy.go b/pkg/webserver/types/proxy.go index 82fe0c7c..8ce5f9d9 100644 --- a/pkg/webserver/types/proxy.go +++ b/pkg/webserver/types/proxy.go @@ -31,18 +31,71 @@ type CacheConfig struct { Key string `form:"key" json:"key"` } +// TimeoutConfig 超时配置 +type TimeoutConfig struct { + Connect time.Duration `form:"connect" json:"connect"` // proxy_connect_timeout,默认 60s + Read time.Duration `form:"read" json:"read"` // proxy_read_timeout,默认 60s + Send time.Duration `form:"send" json:"send"` // proxy_send_timeout,默认 60s +} + +// RetryConfig 重试配置 +type RetryConfig struct { + // 触发重试的条件 (proxy_next_upstream) + // 可选值: "error", "timeout", "invalid_header", "http_500", "http_502", "http_503", "http_504", "http_429", "non_idempotent", "off" + Conditions []string `form:"conditions" json:"conditions"` + + // 最大重试次数 (proxy_next_upstream_tries),0 表示不限制 + Tries int `form:"tries" json:"tries"` + + // 重试超时时间 (proxy_next_upstream_timeout),0 表示不限制 + Timeout time.Duration `form:"timeout" json:"timeout"` +} + +// SSLBackendConfig SSL 后端验证配置 +type SSLBackendConfig struct { + Verify bool `form:"verify" json:"verify"` // proxy_ssl_verify on/off + TrustedCertificate string `form:"trusted_certificate" json:"trusted_certificate"` // proxy_ssl_trusted_certificate 路径 + VerifyDepth int `form:"verify_depth" json:"verify_depth"` // proxy_ssl_verify_depth,默认 1 +} + +// ResponseHeaderConfig 响应头修改配置 +type ResponseHeaderConfig struct { + // 隐藏的响应头 (proxy_hide_header) + Hide []string `form:"hide" json:"hide"` + + // 添加的响应头 (add_header),key -> value + // 值可以包含变量,如 $upstream_cache_status + Add map[string]string `form:"add" json:"add"` +} + +// AccessControlConfig IP 访问控制配置 +type AccessControlConfig struct { + // 允许的 IP/CIDR 列表 (allow) + Allow []string `form:"allow" json:"allow"` + + // 拒绝的 IP/CIDR 列表 (deny) + Deny []string `form:"deny" json:"deny"` +} + // Proxy 反向代理配置 type Proxy struct { - Location string `form:"location" json:"location" validate:"required"` // 匹配路径,如: "/", "/api", "~ ^/api/v[0-9]+/" - Pass string `form:"pass" json:"pass" validate:"required"` // 代理地址,如: "http://example.com", "http://backend" - Host string `form:"host" json:"host"` // 代理 Host,如: "example.com" - SNI string `form:"sni" json:"sni"` // 代理 SNI,如: "example.com" - Cache *CacheConfig `form:"cache" json:"cache"` // 缓存配置,nil 表示禁用缓存 - Buffering bool `form:"buffering" json:"buffering"` // 是否启用缓冲 - Resolver []string `form:"resolver" json:"resolver"` // 自定义 DNS 解析器配置,如: ["8.8.8.8", "ipv6=off"] - ResolverTimeout time.Duration `form:"resolver_timeout" json:"resolver_timeout"` // DNS 解析超时时间,如: 5 * time.Second - Headers map[string]string `form:"headers" json:"headers"` // 自定义请求头,如: map["X-Custom-Header"] = "value" - Replaces map[string]string `form:"replaces" json:"replaces"` // 响应内容替换,如: map["/old"] = "/new" + Location string `form:"location" json:"location" validate:"required"` // 匹配路径,如: "/", "/api", "~ ^/api/v[0-9]+/" + Pass string `form:"pass" json:"pass" validate:"required"` // 代理地址,如: "http://example.com", "http://backend" + Host string `form:"host" json:"host"` // 代理 Host,如: "example.com" + SNI string `form:"sni" json:"sni"` // 代理 SNI,如: "example.com" + Cache *CacheConfig `form:"cache" json:"cache"` // 缓存配置,nil 表示禁用缓存 + Buffering bool `form:"buffering" json:"buffering"` // 是否启用缓冲 + Resolver []string `form:"resolver" json:"resolver"` // 自定义 DNS 解析器配置,如: ["8.8.8.8", "ipv6=off"] + ResolverTimeout time.Duration `form:"resolver_timeout" json:"resolver_timeout"` // DNS 解析超时时间,如: 5 * time.Second + Headers map[string]string `form:"headers" json:"headers"` // 自定义请求头,如: map["X-Custom-Header"] = "value" + Replaces map[string]string `form:"replaces" json:"replaces"` // 响应内容替换,如: map["/old"] = "/new" + HTTPVersion string `form:"http_version" json:"http_version"` // HTTP 协议版本 (proxy_http_version),可选: "1.0", "1.1", "2" + Timeout *TimeoutConfig `form:"timeout" json:"timeout"` // 超时配置 + Retry *RetryConfig `form:"retry" json:"retry"` // 重试配置 + ClientMaxBodySize int64 `form:"client_max_body_size" json:"client_max_body_size"` // 请求体大小限制 (client_max_body_size),单位字节,0 表示使用全局配置 + SSLBackend *SSLBackendConfig `form:"ssl_backend" json:"ssl_backend"` // SSL 后端验证配置 + ResponseHeaders *ResponseHeaderConfig `form:"response_headers" json:"response_headers"` // 响应头修改配置 + AccessControl *AccessControlConfig `form:"access_control" json:"access_control"` // IP 访问控制配置 } // Upstream 上游服务器配置 diff --git a/web/src/views/website/EditView.vue b/web/src/views/website/EditView.vue index 038d018d..e45fb0bc 100644 --- a/web/src/views/website/EditView.vue +++ b/web/src/views/website/EditView.vue @@ -335,7 +335,14 @@ const addProxy = () => { resolver: [], resolver_timeout: 5 * 1000000000, // 5秒,以纳秒为单位 headers: {}, - replaces: {} + replaces: {}, + http_version: '1.1', + timeout: null, + retry: null, + client_max_body_size: 0, + ssl_backend: null, + response_headers: null, + access_control: null }) } @@ -408,6 +415,200 @@ const cacheMethodOptions = [ { label: 'POST', value: 'POST' } ] +// HTTP 协议版本选项 +const httpVersionOptions = [ + { label: 'HTTP/1.0', value: '1.0' }, + { label: 'HTTP/1.1', value: '1.1' }, + { label: 'HTTP/2', value: '2' } +] + +// 大小单位选项 +const sizeUnitOptions = [ + { label: 'KB', value: 'k' }, + { label: 'MB', value: 'm' }, + { label: 'GB', value: 'g' } +] + +// 从字节解析为 {value, unit} 格式 +const parseSize = (bytes: number): { value: number; unit: string } => { + if (!bytes || bytes <= 0) return { value: 0, unit: 'm' } + + if (bytes >= 1024 * 1024 * 1024 && bytes % (1024 * 1024 * 1024) === 0) { + return { value: bytes / (1024 * 1024 * 1024), unit: 'g' } + } + if (bytes >= 1024 * 1024 && bytes % (1024 * 1024) === 0) { + return { value: bytes / (1024 * 1024), unit: 'm' } + } + if (bytes >= 1024 && bytes % 1024 === 0) { + return { value: bytes / 1024, unit: 'k' } + } + return { value: bytes, unit: '' } +} + +// 将 {value, unit} 转换为字节 +const buildSize = (value: number, unit: string): number => { + if (!value || value <= 0) return 0 + switch (unit) { + case 'g': + return value * 1024 * 1024 * 1024 + case 'm': + return value * 1024 * 1024 + case 'k': + return value * 1024 + default: + return value + } +} + +// 重试条件选项 +const retryConditionOptions = [ + { label: 'error', value: 'error' }, + { label: 'timeout', value: 'timeout' }, + { label: 'invalid_header', value: 'invalid_header' }, + { label: 'http_500', value: 'http_500' }, + { label: 'http_502', value: 'http_502' }, + { label: 'http_503', value: 'http_503' }, + { label: 'http_504', value: 'http_504' }, + { label: 'http_429', value: 'http_429' }, + { label: 'non_idempotent', value: 'non_idempotent' }, + { label: 'off', value: 'off' } +] + +// 常见隐藏响应头选项 +const hideHeaderOptions = [ + { label: 'X-Powered-By', value: 'X-Powered-By' }, + { label: 'Server', value: 'Server' }, + { label: 'X-AspNet-Version', value: 'X-AspNet-Version' }, + { label: 'X-AspNetMvc-Version', value: 'X-AspNetMvc-Version' }, + { label: 'X-Runtime', value: 'X-Runtime' }, + { label: 'X-Version', value: 'X-Version' } +] + +// 创建默认超时配置 +const createDefaultTimeoutConfig = () => ({ + connect: 60 * SECOND, + read: 60 * SECOND, + send: 60 * SECOND +}) + +// 创建默认重试配置 +const createDefaultRetryConfig = () => ({ + conditions: ['error', 'timeout'], + tries: 0, + timeout: 0 +}) + +// 创建默认 SSL 后端配置 +const createDefaultSSLBackendConfig = () => ({ + verify: false, + trusted_certificate: '', + verify_depth: 1 +}) + +// 创建默认响应头配置 +const createDefaultResponseHeadersConfig = () => ({ + hide: [], + add: {} +}) + +// 创建默认访问控制配置 +const createDefaultAccessControlConfig = () => ({ + allow: [], + deny: [] +}) + +// 切换超时配置启用状态 +const toggleProxyTimeout = (proxy: any, enabled: boolean) => { + if (enabled) { + proxy.timeout = createDefaultTimeoutConfig() + } else { + proxy.timeout = null + } +} + +// 切换重试配置启用状态 +const toggleProxyRetry = (proxy: any, enabled: boolean) => { + if (enabled) { + proxy.retry = createDefaultRetryConfig() + } else { + proxy.retry = null + } +} + +// 切换 SSL 后端验证启用状态 +const toggleProxySSLBackend = (proxy: any, enabled: boolean) => { + if (enabled) { + proxy.ssl_backend = createDefaultSSLBackendConfig() + } else { + proxy.ssl_backend = null + } +} + +// 切换响应头配置启用状态 +const toggleProxyResponseHeaders = (proxy: any, enabled: boolean) => { + if (enabled) { + proxy.response_headers = createDefaultResponseHeadersConfig() + } else { + proxy.response_headers = null + } +} + +// 切换访问控制启用状态 +const toggleProxyAccessControl = (proxy: any, enabled: boolean) => { + if (enabled) { + proxy.access_control = createDefaultAccessControlConfig() + } else { + proxy.access_control = null + } +} + +// 更新超时时间值 +const updateProxyTimeoutValue = (proxy: any, field: string, value: number) => { + if (!proxy.timeout) return + const parsed = parseDuration(proxy.timeout[field]) + proxy.timeout[field] = buildDuration(value, parsed.unit) +} + +// 更新超时时间单位 +const updateProxyTimeoutUnit = (proxy: any, field: string, unit: string) => { + if (!proxy.timeout) return + const parsed = parseDuration(proxy.timeout[field]) + proxy.timeout[field] = buildDuration(parsed.value, unit) +} + +// 更新请求体大小值 +const updateClientMaxBodySizeValue = (proxy: any, value: number) => { + const parsed = parseSize(proxy.client_max_body_size) + proxy.client_max_body_size = buildSize(value, parsed.unit || 'm') +} + +// 更新请求体大小单位 +const updateClientMaxBodySizeUnit = (proxy: any, unit: string) => { + const parsed = parseSize(proxy.client_max_body_size) + proxy.client_max_body_size = buildSize(parsed.value || 0, unit) +} + +// 更新重试超时值 +const updateRetryTimeoutValue = (proxy: any, value: number) => { + if (!proxy.retry) return + const parsed = parseDuration(proxy.retry.timeout) + proxy.retry.timeout = buildDuration(value, parsed.unit) +} + +// 更新重试超时单位 +const updateRetryTimeoutUnit = (proxy: any, unit: string) => { + if (!proxy.retry) return + const parsed = parseDuration(proxy.retry.timeout) + proxy.retry.timeout = buildDuration(parsed.value, unit) +} + +// 添加响应头 +const addResponseHeader = (proxy: any) => { + if (!proxy.response_headers) return + if (!proxy.response_headers.add) proxy.response_headers.add = {} + proxy.response_headers.add['X-Custom-Header'] = 'value' +} + // 删除代理 const removeProxy = (index: number) => { if (setting.value.proxies) { @@ -1182,6 +1383,284 @@ const removeCustomConfig = (index: number) => { {{ $gettext('Add Replacement Rule') }} + + +