fix(frontend): comprehensive i18n cleanup and Select component hardening
This commit is contained in:
@@ -128,7 +128,7 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
|
||||
// 2. 【新增】Wait后二次检查余额/订阅
|
||||
if err := h.billingCacheService.CheckBillingEligibility(c.Request.Context(), apiKey.User, apiKey, apiKey.Group, subscription); err != nil {
|
||||
log.Printf("Billing eligibility check failed after wait: %v", err)
|
||||
h.handleStreamingAwareError(c, http.StatusForbidden, "billing_error", err.Error(), streamStarted)
|
||||
h.handleStreamingAwareError(c, http.StatusForbidden, "permission_error", "Insufficient balance or active subscription required", streamStarted)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -156,8 +156,9 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
|
||||
for {
|
||||
selection, err := h.gatewayService.SelectAccountWithLoadAwareness(c.Request.Context(), apiKey.GroupID, sessionKey, reqModel, failedAccountIDs)
|
||||
if err != nil {
|
||||
log.Printf("Select account failed: %v", err)
|
||||
if len(failedAccountIDs) == 0 {
|
||||
h.handleStreamingAwareError(c, http.StatusServiceUnavailable, "api_error", "No available accounts: "+err.Error(), streamStarted)
|
||||
h.handleStreamingAwareError(c, http.StatusServiceUnavailable, "api_error", "No available accounts for requested model", streamStarted)
|
||||
return
|
||||
}
|
||||
h.handleFailoverExhausted(c, lastFailoverStatus, streamStarted)
|
||||
@@ -280,8 +281,9 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
|
||||
// 选择支持该模型的账号
|
||||
selection, err := h.gatewayService.SelectAccountWithLoadAwareness(c.Request.Context(), apiKey.GroupID, sessionKey, reqModel, failedAccountIDs)
|
||||
if err != nil {
|
||||
log.Printf("Select account failed: %v", err)
|
||||
if len(failedAccountIDs) == 0 {
|
||||
h.handleStreamingAwareError(c, http.StatusServiceUnavailable, "api_error", "No available accounts: "+err.Error(), streamStarted)
|
||||
h.handleStreamingAwareError(c, http.StatusServiceUnavailable, "api_error", "No available accounts for requested model", streamStarted)
|
||||
return
|
||||
}
|
||||
h.handleFailoverExhausted(c, lastFailoverStatus, streamStarted)
|
||||
@@ -566,32 +568,68 @@ func (h *GatewayHandler) handleFailoverExhausted(c *gin.Context, statusCode int,
|
||||
func (h *GatewayHandler) mapUpstreamError(statusCode int) (int, string, string) {
|
||||
switch statusCode {
|
||||
case 401:
|
||||
return http.StatusBadGateway, "upstream_error", "Upstream authentication failed, please contact administrator"
|
||||
return http.StatusBadGateway, "api_error", "Upstream authentication failed, please contact administrator"
|
||||
case 403:
|
||||
return http.StatusBadGateway, "upstream_error", "Upstream access forbidden, please contact administrator"
|
||||
return http.StatusBadGateway, "api_error", "Upstream access forbidden, please contact administrator"
|
||||
case 429:
|
||||
return http.StatusTooManyRequests, "rate_limit_error", "Upstream rate limit exceeded, please retry later"
|
||||
case 529:
|
||||
return http.StatusServiceUnavailable, "overloaded_error", "Upstream service overloaded, please retry later"
|
||||
case 500, 502, 503, 504:
|
||||
return http.StatusBadGateway, "upstream_error", "Upstream service temporarily unavailable"
|
||||
return http.StatusBadGateway, "api_error", "Upstream service temporarily unavailable"
|
||||
default:
|
||||
return http.StatusBadGateway, "upstream_error", "Upstream request failed"
|
||||
return http.StatusBadGateway, "api_error", "Upstream request failed"
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeAnthropicErrorType(errType string) string {
|
||||
switch errType {
|
||||
case "invalid_request_error",
|
||||
"authentication_error",
|
||||
"permission_error",
|
||||
"not_found_error",
|
||||
"rate_limit_error",
|
||||
"api_error",
|
||||
"overloaded_error":
|
||||
return errType
|
||||
case "billing_error":
|
||||
// Not an Anthropic-standard error type; map to the closest equivalent.
|
||||
return "permission_error"
|
||||
case "upstream_error":
|
||||
// Not an Anthropic-standard error type; keep clients compatible.
|
||||
return "api_error"
|
||||
default:
|
||||
return "api_error"
|
||||
}
|
||||
}
|
||||
|
||||
const maxPublicErrorMessageLen = 512
|
||||
|
||||
func sanitizePublicErrorMessage(message string) string {
|
||||
cleaned := strings.TrimSpace(message)
|
||||
cleaned = strings.ReplaceAll(cleaned, "\r", " ")
|
||||
cleaned = strings.ReplaceAll(cleaned, "\n", " ")
|
||||
if len(cleaned) > maxPublicErrorMessageLen {
|
||||
cleaned = cleaned[:maxPublicErrorMessageLen] + "..."
|
||||
}
|
||||
return cleaned
|
||||
}
|
||||
|
||||
// handleStreamingAwareError handles errors that may occur after streaming has started
|
||||
func (h *GatewayHandler) handleStreamingAwareError(c *gin.Context, status int, errType, message string, streamStarted bool) {
|
||||
normalizedType := normalizeAnthropicErrorType(errType)
|
||||
publicMessage := sanitizePublicErrorMessage(message)
|
||||
|
||||
if streamStarted {
|
||||
// Stream already started, send error as SSE event then close
|
||||
flusher, ok := c.Writer.(http.Flusher)
|
||||
if ok {
|
||||
// Send error event in SSE format with proper JSON marshaling
|
||||
// Anthropic streaming spec: send `event: error` with JSON `data`.
|
||||
errorData := map[string]any{
|
||||
"type": "error",
|
||||
"error": map[string]string{
|
||||
"type": errType,
|
||||
"message": message,
|
||||
"type": normalizedType,
|
||||
"message": publicMessage,
|
||||
},
|
||||
}
|
||||
jsonBytes, err := json.Marshal(errorData)
|
||||
@@ -599,8 +637,11 @@ func (h *GatewayHandler) handleStreamingAwareError(c *gin.Context, status int, e
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
errorEvent := fmt.Sprintf("data: %s\n\n", string(jsonBytes))
|
||||
if _, err := fmt.Fprint(c.Writer, errorEvent); err != nil {
|
||||
if _, err := fmt.Fprintf(c.Writer, "event: error\n"); err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
if _, err := fmt.Fprintf(c.Writer, "data: %s\n\n", string(jsonBytes)); err != nil {
|
||||
_ = c.Error(err)
|
||||
}
|
||||
flusher.Flush()
|
||||
@@ -609,16 +650,19 @@ func (h *GatewayHandler) handleStreamingAwareError(c *gin.Context, status int, e
|
||||
}
|
||||
|
||||
// Normal case: return JSON response with proper status code
|
||||
h.errorResponse(c, status, errType, message)
|
||||
h.errorResponse(c, status, normalizedType, publicMessage)
|
||||
}
|
||||
|
||||
// errorResponse 返回Claude API格式的错误响应
|
||||
func (h *GatewayHandler) errorResponse(c *gin.Context, status int, errType, message string) {
|
||||
normalizedType := normalizeAnthropicErrorType(errType)
|
||||
publicMessage := sanitizePublicErrorMessage(message)
|
||||
|
||||
c.JSON(status, gin.H{
|
||||
"type": "error",
|
||||
"error": gin.H{
|
||||
"type": errType,
|
||||
"message": message,
|
||||
"type": normalizedType,
|
||||
"message": publicMessage,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -674,7 +718,8 @@ func (h *GatewayHandler) CountTokens(c *gin.Context) {
|
||||
// 校验 billing eligibility(订阅/余额)
|
||||
// 【注意】不计算并发,但需要校验订阅/余额
|
||||
if err := h.billingCacheService.CheckBillingEligibility(c.Request.Context(), apiKey.User, apiKey, apiKey.Group, subscription); err != nil {
|
||||
h.errorResponse(c, http.StatusForbidden, "billing_error", err.Error())
|
||||
log.Printf("Billing eligibility check failed: %v", err)
|
||||
h.errorResponse(c, http.StatusForbidden, "permission_error", "Insufficient balance or active subscription required")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -684,7 +729,8 @@ func (h *GatewayHandler) CountTokens(c *gin.Context) {
|
||||
// 选择支持该模型的账号
|
||||
account, err := h.gatewayService.SelectAccountForModel(c.Request.Context(), apiKey.GroupID, sessionHash, parsedReq.Model)
|
||||
if err != nil {
|
||||
h.errorResponse(c, http.StatusServiceUnavailable, "api_error", "No available accounts: "+err.Error())
|
||||
log.Printf("Select account failed: %v", err)
|
||||
h.errorResponse(c, http.StatusServiceUnavailable, "api_error", "No available accounts for requested model")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user