Răsfoiți Sursa

feat: add circuit breaker configuration for network errors

- Introduced ENABLE_CIRCUIT_BREAKER_ON_NETWORK_ERRORS to .env.example and CLAUDE.md for controlling whether network errors count towards the circuit breaker.
- Updated ProxyForwarder to log and handle network errors based on the new configuration.
- Enhanced env.schema to include the new circuit breaker setting.
ding113 3 luni în urmă
părinte
comite
deafa29587

+ 11 - 3
.env.example

@@ -38,10 +38,18 @@ SESSION_TTL=300                         # Session 过期时间(秒,默认 30
 STORE_SESSION_MESSAGES=false            # 是否存储请求 messages 到 Redis(用于实时监控页面查看详情,默认:false)
                                         # 警告:启用后会增加 Redis 内存使用,且可能包含敏感信息
 
+# 熔断器配置
+# 功能说明:控制网络错误是否计入熔断器失败计数
+# - false (默认):网络错误(DNS 解析失败、连接超时、代理连接失败等)不计入熔断器,仅供应商错误(4xx/5xx HTTP 响应)计入
+# - true:所有错误(包括网络错误)都计入熔断器失败计数
+# 使用场景:
+# - 默认关闭:适用于网络不稳定环境(如使用代理),避免因临时网络抖动触发熔断器
+# - 启用:适用于网络稳定环境,连续网络错误也应触发熔断保护,避免持续请求不可达的供应商
+ENABLE_CIRCUIT_BREAKER_ON_NETWORK_ERRORS=false
+
 # 多提供商类型支持(实验性功能)
-# 功能说明:启用除 Claude 以外的其他供应商类型支持
-# - false (默认):仅支持 Claude 类型供应商
-# - true:支持 Codex、Gemini CLI、OpenAI Compatible 等其他类型
+# - false (默认):仅支持 Claude、Codex类型供应商
+# - true:支持 Gemini CLI、OpenAI Compatible 等其他类型
 # 警告:其他类型功能仍在开发中,暂不建议启用
 ENABLE_MULTI_PROVIDER_TYPES=false
 

+ 5 - 0
CLAUDE.md

@@ -309,6 +309,11 @@ ENABLE_RATE_LIMIT=true             # 启用限流功能
 SESSION_TTL=300                    # Session 缓存过期时间(秒)
 STORE_SESSION_MESSAGES=false       # 是否存储请求 messages(用于实时监控)
 
+# 熔断器配置
+ENABLE_CIRCUIT_BREAKER_ON_NETWORK_ERRORS=false  # 网络错误是否计入熔断器(默认:false)
+                                                # false: 仅 HTTP 4xx/5xx 错误计入熔断器
+                                                # true: 网络错误(DNS 失败、连接超时等)也计入熔断器
+
 # Cookie 安全策略
 ENABLE_SECURE_COOKIES=true         # 是否强制 HTTPS Cookie(默认:true)
                                    # 设置为 false 允许 HTTP 访问,但会降低安全性

+ 1 - 15
src/app/internal/data-gen/_components/data-generator-page.tsx

@@ -492,14 +492,6 @@ export function DataGeneratorPage() {
                 </CardTitle>
               </CardHeader>
             </Card>
-            <Card>
-              <CardHeader className="pb-2">
-                <CardDescription>总成本</CardDescription>
-                <CardTitle className="text-2xl">
-                  ${userBreakdownResult.summary.totalCost.toFixed(4)}
-                </CardTitle>
-              </CardHeader>
-            </Card>
             <Card>
               <CardHeader className="pb-2">
                 <CardDescription>总成本(人民币)</CardDescription>
@@ -555,10 +547,7 @@ export function DataGeneratorPage() {
                               {item.totalCalls.toLocaleString()}
                             </TableCell>
                             <TableCell className="text-right font-mono text-xs">
-                              <div>${item.totalCost.toFixed(6)}</div>
-                              <div className="text-muted-foreground">
                                 ¥{(item.totalCost * 7.1).toFixed(2)}
-                              </div>
                             </TableCell>
                           </TableRow>
                         ))
@@ -573,10 +562,7 @@ export function DataGeneratorPage() {
                               {item.totalCalls.toLocaleString()}
                             </TableCell>
                             <TableCell className="text-right font-mono text-xs">
-                              <div>${item.totalCost.toFixed(6)}</div>
-                              <div className="text-muted-foreground">
-                                ¥{(item.totalCost * 7.1).toFixed(2)}
-                              </div>
+                            ¥{(item.totalCost * 7.1).toFixed(2)}
                             </TableCell>
                           </TableRow>
                         ))}

+ 29 - 0
src/app/v1/_lib/proxy/forwarder.ts

@@ -18,6 +18,7 @@ import { mapClientFormatToTransformer, mapProviderTypeToTransformer } from "./fo
 import { isOfficialCodexClient, sanitizeCodexRequest } from "../codex/utils/request-sanitizer";
 import { createProxyAgentForProvider } from "@/lib/proxy-agent";
 import type { Dispatcher } from "undici";
+import { getEnvConfig } from "@/lib/config/env.schema";
 
 const MAX_RETRY_ATTEMPTS = 3;
 
@@ -179,6 +180,34 @@ export class ProxyForwarder {
             providerId: currentProvider.id,
             providerName: currentProvider.name,
           });
+
+          // ⭐ 检查是否启用了网络错误计入熔断器
+          const env = getEnvConfig();
+          if (env.ENABLE_CIRCUIT_BREAKER_ON_NETWORK_ERRORS) {
+            logger.warn(
+              "ProxyForwarder: Network error will be counted towards circuit breaker (enabled by config)",
+              {
+                providerId: currentProvider.id,
+                providerName: currentProvider.name,
+                errorType: err.constructor.name,
+                errorCode: err.code,
+              }
+            );
+
+            // 记录到失败列表(避免重新选择)
+            failedProviderIds.push(currentProvider.id);
+
+            // 计入熔断器
+            await recordFailure(currentProvider.id, lastError);
+          } else {
+            logger.debug(
+              "ProxyForwarder: Network error not counted towards circuit breaker (disabled by default)",
+              {
+                providerId: currentProvider.id,
+                providerName: currentProvider.name,
+              }
+            );
+          }
         }
 
         // ⭐ 4. 供应商错误处理(所有 4xx/5xx HTTP 错误,计入熔断器,直接切换)

+ 1 - 0
src/lib/config/env.schema.ts

@@ -37,6 +37,7 @@ export const EnvSchema = z.object({
   LOG_LEVEL: z.enum(["fatal", "error", "warn", "info", "debug", "trace"]).default("info"),
   TZ: z.string().default("Asia/Shanghai"),
   ENABLE_MULTI_PROVIDER_TYPES: z.string().default("false").transform(booleanTransform),
+  ENABLE_CIRCUIT_BREAKER_ON_NETWORK_ERRORS: z.string().default("false").transform(booleanTransform),
 });
 
 /**