diff --git a/backend/internal/service/sora_sdk_client.go b/backend/internal/service/sora_sdk_client.go index f9221c5b..a8812ff5 100644 --- a/backend/internal/service/sora_sdk_client.go +++ b/backend/internal/service/sora_sdk_client.go @@ -478,7 +478,7 @@ func (c *SoraSDKClient) GetWatermarkFreeURLCustom(ctx context.Context, account * } var resp *http.Response if c.httpUpstream != nil { - resp, err = c.httpUpstream.Do(req, proxyURL, accountID, accountConcurrency) + resp, err = c.doSoraHTTP(req, proxyURL, accountID, accountConcurrency) } else { resp, err = http.DefaultClient.Do(req) } @@ -901,7 +901,7 @@ func (c *SoraSDKClient) exchangeSessionToken(ctx context.Context, account *Accou var resp *http.Response if c.httpUpstream != nil { - resp, err = c.httpUpstream.Do(req, proxyURL, accountID, accountConcurrency) + resp, err = c.doSoraHTTP(req, proxyURL, accountID, accountConcurrency) } else { resp, err = http.DefaultClient.Do(req) } @@ -1024,3 +1024,70 @@ func (c *SoraSDKClient) debugLogf(format string, args ...any) { log.Printf("[SoraSDK] "+format, args...) } } + +// doSoraHTTP 执行 Sora HTTP 请求,优先走 curl_cffi sidecar(Chrome TLS 指纹绕过 Cloudflare) +// 如果 sidecar 未启用或不可用,回退到 httpUpstream.Do() / http.DefaultClient +func (c *SoraSDKClient) doSoraHTTP(req *http.Request, proxyURL string, accountID int64, accountConcurrency int) (*http.Response, error) { + // 检查 sidecar 是否启用 + if c.cfg != nil && c.cfg.Sora.Client.CurlCFFISidecar.Enabled && c.cfg.Sora.Client.CurlCFFISidecar.BaseURL != "" { + resp, err := c.doViaSidecar(req) + if err == nil { + return resp, nil + } + // sidecar 失败,回退到直连 + logger.LegacyPrintf("service.sora", "Warning: sidecar failed, falling back to direct: %v", err) + } + + // 回退路径 + if c.httpUpstream != nil { + return c.httpUpstream.Do(req, proxyURL, accountID, accountConcurrency) + } + return http.DefaultClient.Do(req) +} + +// doViaSidecar 通过 curl_cffi sidecar 发送请求(Chrome TLS 指纹) +func (c *SoraSDKClient) doViaSidecar(originalReq *http.Request) (*http.Response, error) { + sidecarURL := strings.TrimRight(c.cfg.Sora.Client.CurlCFFISidecar.BaseURL, "/") + "/proxy" + + // 读取原始请求体 + var bodyStr string + if originalReq.Body != nil { + bodyBytes, err := io.ReadAll(originalReq.Body) + if err != nil { + return nil, fmt.Errorf("read request body: %w", err) + } + bodyStr = string(bodyBytes) + // 恢复 body 以备回退 + originalReq.Body = io.NopCloser(bytes.NewReader(bodyBytes)) + } + + // 构建 sidecar 请求 + headers := make(map[string]string) + for k, vs := range originalReq.Header { + if len(vs) > 0 { + headers[k] = vs[0] + } + } + + payload := map[string]any{ + "url": originalReq.URL.String(), + "method": originalReq.Method, + "headers": headers, + "body": bodyStr, + "session_key": fmt.Sprintf("account_%d", 0), // 简单的 session key + } + + payloadBytes, err := json.Marshal(payload) + if err != nil { + return nil, fmt.Errorf("marshal sidecar payload: %w", err) + } + + ctx := originalReq.Context() + sidecarReq, err := http.NewRequestWithContext(ctx, http.MethodPost, sidecarURL, bytes.NewReader(payloadBytes)) + if err != nil { + return nil, fmt.Errorf("create sidecar request: %w", err) + } + sidecarReq.Header.Set("Content-Type", "application/json") + + return http.DefaultClient.Do(sidecarReq) +}