refactor(health dashboard & tts server): 优化健康页面展示与服务配置
- 重构健康页面配置状态展示,替换原有资源ID和发音人字段 - 简化错误记录展示逻辑,改为统计展示错误数量并提示查看日志 - 调整HTTP客户端连接池配置与请求体缓冲区大小 - 重构速率限制逻辑位置,优化请求处理流程 - 重写CORS配置,支持环境变量白名单和通配符模式 - 增加静态文件检查与更严格的跨域校验 - 调整服务器超时配置,适配更长时间的请求处理
This commit is contained in:
+14
-16
@@ -316,12 +316,10 @@
|
||||
</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">Resource ID</span>
|
||||
<span class="info-value">{{ healthData.config_status?.resource_id || '-' }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">发音人</span>
|
||||
<span class="info-value">{{ healthData.config_status?.speaker || '-' }}</span>
|
||||
<span class="info-label">配置状态</span>
|
||||
<span class="info-value" :class="healthData.config_status?.config_error ? 'error' : 'success'">
|
||||
{{ healthData.config_status?.config_error ? '异常' : '正常' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">启动时间</span>
|
||||
@@ -366,15 +364,15 @@
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title">
|
||||
<span class="dot" :class="{ error: recentErrors.length > 0 }"></span>
|
||||
错误记录 ({{ recentErrors.length }})
|
||||
<span class="dot" :class="{ error: recentErrorsCount > 0 }"></span>
|
||||
错误记录 ({{ recentErrorsCount }})
|
||||
</div>
|
||||
<div v-if="recentErrors.length === 0" class="no-errors">
|
||||
<div v-if="recentErrorsCount === 0" class="no-errors">
|
||||
暂无错误记录
|
||||
</div>
|
||||
<div v-else class="error-list">
|
||||
<div v-for="(err, index) in recentErrors" :key="index" class="error-item">
|
||||
<div class="error-time">{{ err }}</div>
|
||||
<div class="error-item">
|
||||
<div class="error-time">检测到 {{ recentErrorsCount }} 条最近错误,详细信息请查看服务器日志</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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,
|
||||
|
||||
+114
-48
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user