From 1b84a6c9ee9d56bf80bbff5b89d61d794907d43f Mon Sep 17 00:00:00 2001 From: sun <3371392206@qq.com> Date: Fri, 22 May 2026 23:32:10 +0800 Subject: [PATCH] =?UTF-8?q?refactor(health=20dashboard=20&=20tts=20server)?= =?UTF-8?q?:=20=E4=BC=98=E5=8C=96=E5=81=A5=E5=BA=B7=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E5=B1=95=E7=A4=BA=E4=B8=8E=E6=9C=8D=E5=8A=A1=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 重构健康页面配置状态展示,替换原有资源ID和发音人字段 - 简化错误记录展示逻辑,改为统计展示错误数量并提示查看日志 - 调整HTTP客户端连接池配置与请求体缓冲区大小 - 重构速率限制逻辑位置,优化请求处理流程 - 重写CORS配置,支持环境变量白名单和通配符模式 - 增加静态文件检查与更严格的跨域校验 - 调整服务器超时配置,适配更长时间的请求处理 --- health.html | 30 +++++----- tts_server.go | 162 +++++++++++++++++++++++++++++++++++--------------- 2 files changed, 128 insertions(+), 64 deletions(-) diff --git a/health.html b/health.html index aac9f4c..ff92635 100644 --- a/health.html +++ b/health.html @@ -316,12 +316,10 @@
- Resource ID - {{ healthData.config_status?.resource_id || '-' }} -
-
- 发音人 - {{ healthData.config_status?.speaker || '-' }} + 配置状态 + + {{ healthData.config_status?.config_error ? '异常' : '正常' }} +
启动时间 @@ -366,15 +364,15 @@
- - 错误记录 ({{ recentErrors.length }}) + + 错误记录 ({{ recentErrorsCount }})
-
+
暂无错误记录
-
-
{{ err }}
+
+
检测到 {{ recentErrorsCount }} 条最近错误,详细信息请查看服务器日志
@@ -420,8 +418,8 @@ } catch (e) { if (e.response && e.response.data) { deepUpdate(healthData.value, e.response.data); - if (e.response.status === 503 && e.response.data.config_status?.config_error_message) { - error.value = e.response.data.config_status.config_error_message; + if (e.response.status === 503) { + error.value = '服务配置异常,请检查环境变量配置'; } else { error.value = '服务异常: ' + (e.message || '未知错误'); } @@ -437,8 +435,8 @@ return parseFloat(healthData.value.api_stats?.error_rate_percent || 0); }); - const recentErrors = computed(() => { - return healthData.value.errors?.recent_errors || []; + const recentErrorsCount = computed(() => { + return healthData.value.errors?.recent_errors_count || 0; }); const chartData = computed(() => { @@ -481,7 +479,7 @@ error, fetchHealth, errorRate, - recentErrors, + recentErrorsCount, chartData, maxResponseTime, formatBytes, diff --git a/tts_server.go b/tts_server.go index 4d4159d..5f5f759 100644 --- a/tts_server.go +++ b/tts_server.go @@ -107,7 +107,7 @@ func init() { Timeout: DEFAULT_TIMEOUT, Transport: &http.Transport{ MaxIdleConns: 100, - MaxIdleConnsPerHost: 10, + MaxIdleConnsPerHost: 20, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, }, @@ -344,7 +344,7 @@ func synthesis(text string, speed float64) (*SynthesisResult, error) { var audioData []byte scanner := bufio.NewScanner(resp.Body) - scanner.Buffer(make([]byte, 1024*1024), 1024*1024) + scanner.Buffer(make([]byte, 1024*1024), 8*1024*1024) for scanner.Scan() { line := scanner.Bytes() @@ -472,21 +472,6 @@ func openaiTTSHandler(w http.ResponseWriter, r *http.Request) { return } - clientIP := getClientIP(r) - if !rateLimiter.Allow(clientIP) { - log.Printf("警告: 已超过IP速率限制,拒绝请求 - 客户端IP: %s", clientIP) - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusTooManyRequests) - json.NewEncoder(w).Encode(map[string]interface{}{ - "error": map[string]interface{}{ - "message": "Rate limit exceeded. Please try again later.", - "type": "rate_limit_error", - "code": "rate_limit_exceeded", - }, - }) - return - } - select { case concurrencySem <- struct{}{}: defer func() { <-concurrencySem }() @@ -504,13 +489,28 @@ func openaiTTSHandler(w http.ResponseWriter, r *http.Request) { return } - body, err := io.ReadAll(http.MaxBytesReader(w, r.Body, MAX_REQUEST_BODY_SIZE)) + clientIP := getClientIP(r) + if !rateLimiter.Allow(clientIP) { + log.Printf("警告: 已超过IP速率限制,拒绝请求 - 客户端IP: %s", clientIP) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusTooManyRequests) + json.NewEncoder(w).Encode(map[string]interface{}{ + "error": map[string]interface{}{ + "message": "Rate limit exceeded. Please try again later.", + "type": "rate_limit_error", + "code": "rate_limit_exceeded", + }, + }) + return + } + + r.Body = http.MaxBytesReader(w, r.Body, MAX_REQUEST_BODY_SIZE) + body, err := io.ReadAll(r.Body) if err != nil { if strings.Contains(err.Error(), "request body too large") { - http.Error(w, "Request body too large", http.StatusRequestEntityTooLarge) - } else { - http.Error(w, "Failed to read request body", http.StatusBadRequest) + return } + http.Error(w, "Failed to read request body", http.StatusBadRequest) return } @@ -678,47 +678,112 @@ func (rec *statusRecorder) WriteHeader(code int) { rec.ResponseWriter.WriteHeader(code) } -var allowedOrigins []string +var ( + allowedOrigins []string + allowAllOrigins bool + corsMaxAgeHeader = "86400" +) + +func normalizeOrigin(origin string) string { + origin = strings.TrimSpace(origin) + origin = strings.TrimRight(origin, "/") + return strings.ToLower(origin) +} func initCORSConfig() { origins := os.Getenv("ALLOWED_ORIGINS") - if origins != "" { - allowedOrigins = strings.Split(origins, ",") - for i, origin := range allowedOrigins { - allowedOrigins[i] = strings.TrimSpace(origin) - } - log.Printf("已配置 %d 个允许的跨域来源", len(allowedOrigins)) - } else { + if origins == "" { log.Println("警告: ALLOWED_ORIGINS 环境变量未设置") - log.Println("将使用反射模式允许所有请求来源,生产环境建议配置白名单") + log.Println("出于安全考虑,跨域请求将被拒绝。如需开放跨域请配置 ALLOWED_ORIGINS") + log.Println("开发环境可设置 ALLOWED_ORIGINS=* 允许所有来源(不可与凭据共用)") + return } + + parts := strings.Split(origins, ",") + for _, p := range parts { + o := strings.TrimSpace(p) + if o == "" { + continue + } + if o == "*" { + allowAllOrigins = true + continue + } + allowedOrigins = append(allowedOrigins, normalizeOrigin(o)) + } + + if allowAllOrigins { + log.Println("警告: ALLOWED_ORIGINS=*,将允许所有来源跨域请求(不携带凭据)") + } + if len(allowedOrigins) > 0 { + log.Printf("已配置 %d 个允许的跨域来源白名单", len(allowedOrigins)) + } +} + +func checkStaticFiles() { + if _, err := os.Stat("health.html"); os.IsNotExist(err) { + log.Println("警告: health.html 不存在,/dashboard 路由将返回 404") + } +} + +func isValidOrigin(origin string) bool { + if origin == "" || origin == "null" || origin == "nil" { + return false + } + if !strings.HasPrefix(origin, "http://") && !strings.HasPrefix(origin, "https://") { + return false + } + return true +} + +func matchOrigin(origin string) (string, bool) { + if !isValidOrigin(origin) { + return "", false + } + if allowAllOrigins { + return "*", true + } + normalized := normalizeOrigin(origin) + for _, allowed := range allowedOrigins { + if allowed == normalized { + return origin, true + } + } + return "", false } func corsMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { origin := r.Header.Get("Origin") - allowOrigin := "" - if len(allowedOrigins) == 0 { - allowOrigin = origin - } else { - for _, allowed := range allowedOrigins { - if allowed == origin { - allowOrigin = origin - break + if origin != "" { + allowOrigin, matched := matchOrigin(origin) + if matched { + w.Header().Set("Access-Control-Allow-Origin", allowOrigin) + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") + w.Header().Set("Access-Control-Expose-Headers", "X-Request-Id") + w.Header().Set("Access-Control-Max-Age", corsMaxAgeHeader) + if allowOrigin != "*" { + w.Header().Set("Access-Control-Allow-Credentials", "true") + } + vary := w.Header().Get("Vary") + if vary == "" { + w.Header().Set("Vary", "Origin") + } else if !strings.Contains(vary, "Origin") { + w.Header().Set("Vary", vary+", Origin") } } } - if allowOrigin != "" { - w.Header().Set("Access-Control-Allow-Origin", allowOrigin) - w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS") - w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") - w.Header().Set("Access-Control-Allow-Credentials", "true") - } - if r.Method == http.MethodOptions { - w.WriteHeader(http.StatusOK) + if origin != "" { + if _, matched := matchOrigin(origin); !matched { + w.WriteHeader(http.StatusNoContent) + return + } + } + w.WriteHeader(http.StatusNoContent) return } @@ -734,6 +799,7 @@ func main() { initAPIKeys() initCORSConfig() + checkStaticFiles() ttsConfigErr = initTTSConfig() if ttsConfigErr != nil { @@ -782,8 +848,8 @@ func main() { server := &http.Server{ Addr: ":" + port, Handler: router, - ReadTimeout: 15 * time.Second, - WriteTimeout: 15 * time.Second, + ReadTimeout: 30 * time.Second, + WriteTimeout: 120 * time.Second, IdleTimeout: 60 * time.Second, }