fix(sora): 增强 Cloudflare 挑战识别并收敛 Sora 请求链路
- 在 failover 场景透传上游响应头并识别 Cloudflare challenge/cf-ray - 统一 Sora 任务请求的 UA 与代理使用,sentinel 与业务请求保持一致 - 修复流式错误事件 JSON 转义问题并补充相关单元测试
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"io"
|
||||
"log"
|
||||
"math/rand"
|
||||
@@ -97,6 +98,7 @@ var soraDesktopUserAgents = []string{
|
||||
var soraRand = rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
var soraRandMu sync.Mutex
|
||||
var soraPerfStart = time.Now()
|
||||
var soraPowTokenGenerator = soraGetPowToken
|
||||
|
||||
// SoraClient 定义直连 Sora 的任务操作接口。
|
||||
type SoraClient interface {
|
||||
@@ -224,9 +226,11 @@ func (c *SoraDirectClient) PreflightCheck(ctx context.Context, account *Account,
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
headers := c.buildBaseHeaders(token, c.defaultUserAgent())
|
||||
userAgent := c.taskUserAgent()
|
||||
proxyURL := c.resolveProxyURL(account)
|
||||
headers := c.buildBaseHeaders(token, userAgent)
|
||||
headers.Set("Accept", "application/json")
|
||||
body, _, err := c.doRequest(ctx, account, http.MethodGet, c.buildURL("/nf/check"), headers, nil, false)
|
||||
body, _, err := c.doRequestWithProxy(ctx, account, proxyURL, http.MethodGet, c.buildURL("/nf/check"), headers, nil, false)
|
||||
if err != nil {
|
||||
var upstreamErr *SoraUpstreamError
|
||||
if errors.As(err, &upstreamErr) && upstreamErr.StatusCode == http.StatusNotFound {
|
||||
@@ -264,6 +268,8 @@ func (c *SoraDirectClient) UploadImage(ctx context.Context, account *Account, da
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
userAgent := c.taskUserAgent()
|
||||
proxyURL := c.resolveProxyURL(account)
|
||||
if filename == "" {
|
||||
filename = "image.png"
|
||||
}
|
||||
@@ -290,10 +296,10 @@ func (c *SoraDirectClient) UploadImage(ctx context.Context, account *Account, da
|
||||
return "", err
|
||||
}
|
||||
|
||||
headers := c.buildBaseHeaders(token, c.defaultUserAgent())
|
||||
headers := c.buildBaseHeaders(token, userAgent)
|
||||
headers.Set("Content-Type", writer.FormDataContentType())
|
||||
|
||||
respBody, _, err := c.doRequest(ctx, account, http.MethodPost, c.buildURL("/uploads"), headers, &body, false)
|
||||
respBody, _, err := c.doRequestWithProxy(ctx, account, proxyURL, http.MethodPost, c.buildURL("/uploads"), headers, &body, false)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -309,6 +315,8 @@ func (c *SoraDirectClient) CreateImageTask(ctx context.Context, account *Account
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
userAgent := c.taskUserAgent()
|
||||
proxyURL := c.resolveProxyURL(account)
|
||||
operation := "simple_compose"
|
||||
inpaintItems := []map[string]any{}
|
||||
if strings.TrimSpace(req.MediaID) != "" {
|
||||
@@ -329,7 +337,7 @@ func (c *SoraDirectClient) CreateImageTask(ctx context.Context, account *Account
|
||||
"n_frames": 1,
|
||||
"inpaint_items": inpaintItems,
|
||||
}
|
||||
headers := c.buildBaseHeaders(token, c.defaultUserAgent())
|
||||
headers := c.buildBaseHeaders(token, userAgent)
|
||||
headers.Set("Content-Type", "application/json")
|
||||
headers.Set("Origin", "https://sora.chatgpt.com")
|
||||
headers.Set("Referer", "https://sora.chatgpt.com/")
|
||||
@@ -338,13 +346,13 @@ func (c *SoraDirectClient) CreateImageTask(ctx context.Context, account *Account
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
sentinel, err := c.generateSentinelToken(ctx, account, token)
|
||||
sentinel, err := c.generateSentinelToken(ctx, account, token, userAgent, proxyURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
headers.Set("openai-sentinel-token", sentinel)
|
||||
|
||||
respBody, _, err := c.doRequest(ctx, account, http.MethodPost, c.buildURL("/video_gen"), headers, bytes.NewReader(body), true)
|
||||
respBody, _, err := c.doRequestWithProxy(ctx, account, proxyURL, http.MethodPost, c.buildURL("/video_gen"), headers, bytes.NewReader(body), true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -360,6 +368,8 @@ func (c *SoraDirectClient) CreateVideoTask(ctx context.Context, account *Account
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
userAgent := c.taskUserAgent()
|
||||
proxyURL := c.resolveProxyURL(account)
|
||||
orientation := req.Orientation
|
||||
if orientation == "" {
|
||||
orientation = "landscape"
|
||||
@@ -399,7 +409,7 @@ func (c *SoraDirectClient) CreateVideoTask(ctx context.Context, account *Account
|
||||
payload["cameo_replacements"] = map[string]any{}
|
||||
}
|
||||
|
||||
headers := c.buildBaseHeaders(token, c.defaultUserAgent())
|
||||
headers := c.buildBaseHeaders(token, userAgent)
|
||||
headers.Set("Content-Type", "application/json")
|
||||
headers.Set("Origin", "https://sora.chatgpt.com")
|
||||
headers.Set("Referer", "https://sora.chatgpt.com/")
|
||||
@@ -407,13 +417,13 @@ func (c *SoraDirectClient) CreateVideoTask(ctx context.Context, account *Account
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
sentinel, err := c.generateSentinelToken(ctx, account, token)
|
||||
sentinel, err := c.generateSentinelToken(ctx, account, token, userAgent, proxyURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
headers.Set("openai-sentinel-token", sentinel)
|
||||
|
||||
respBody, _, err := c.doRequest(ctx, account, http.MethodPost, c.buildURL("/nf/create"), headers, bytes.NewReader(body), true)
|
||||
respBody, _, err := c.doRequestWithProxy(ctx, account, proxyURL, http.MethodPost, c.buildURL("/nf/create"), headers, bytes.NewReader(body), true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -429,6 +439,8 @@ func (c *SoraDirectClient) EnhancePrompt(ctx context.Context, account *Account,
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
userAgent := c.taskUserAgent()
|
||||
proxyURL := c.resolveProxyURL(account)
|
||||
if strings.TrimSpace(expansionLevel) == "" {
|
||||
expansionLevel = "medium"
|
||||
}
|
||||
@@ -446,13 +458,13 @@ func (c *SoraDirectClient) EnhancePrompt(ctx context.Context, account *Account,
|
||||
return "", err
|
||||
}
|
||||
|
||||
headers := c.buildBaseHeaders(token, c.defaultUserAgent())
|
||||
headers := c.buildBaseHeaders(token, userAgent)
|
||||
headers.Set("Content-Type", "application/json")
|
||||
headers.Set("Accept", "application/json")
|
||||
headers.Set("Origin", "https://sora.chatgpt.com")
|
||||
headers.Set("Referer", "https://sora.chatgpt.com/")
|
||||
|
||||
respBody, _, err := c.doRequest(ctx, account, http.MethodPost, c.buildURL("/editor/enhance_prompt"), headers, bytes.NewReader(body), false)
|
||||
respBody, _, err := c.doRequestWithProxy(ctx, account, proxyURL, http.MethodPost, c.buildURL("/editor/enhance_prompt"), headers, bytes.NewReader(body), false)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -489,12 +501,14 @@ func (c *SoraDirectClient) fetchRecentImageTask(ctx context.Context, account *Ac
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
headers := c.buildBaseHeaders(token, c.defaultUserAgent())
|
||||
userAgent := c.taskUserAgent()
|
||||
proxyURL := c.resolveProxyURL(account)
|
||||
headers := c.buildBaseHeaders(token, userAgent)
|
||||
if limit <= 0 {
|
||||
limit = 20
|
||||
}
|
||||
endpoint := fmt.Sprintf("/v2/recent_tasks?limit=%d", limit)
|
||||
respBody, _, err := c.doRequest(ctx, account, http.MethodGet, c.buildURL(endpoint), headers, nil, false)
|
||||
respBody, _, err := c.doRequestWithProxy(ctx, account, proxyURL, http.MethodGet, c.buildURL(endpoint), headers, nil, false)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
@@ -551,9 +565,11 @@ func (c *SoraDirectClient) GetVideoTask(ctx context.Context, account *Account, t
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
headers := c.buildBaseHeaders(token, c.defaultUserAgent())
|
||||
userAgent := c.taskUserAgent()
|
||||
proxyURL := c.resolveProxyURL(account)
|
||||
headers := c.buildBaseHeaders(token, userAgent)
|
||||
|
||||
respBody, _, err := c.doRequest(ctx, account, http.MethodGet, c.buildURL("/nf/pending/v2"), headers, nil, false)
|
||||
respBody, _, err := c.doRequestWithProxy(ctx, account, proxyURL, http.MethodGet, c.buildURL("/nf/pending/v2"), headers, nil, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -582,7 +598,7 @@ func (c *SoraDirectClient) GetVideoTask(ctx context.Context, account *Account, t
|
||||
}
|
||||
}
|
||||
|
||||
respBody, _, err = c.doRequest(ctx, account, http.MethodGet, c.buildURL("/project_y/profile/drafts?limit=15"), headers, nil, false)
|
||||
respBody, _, err = c.doRequestWithProxy(ctx, account, proxyURL, http.MethodGet, c.buildURL("/project_y/profile/drafts?limit=15"), headers, nil, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -653,6 +669,25 @@ func (c *SoraDirectClient) defaultUserAgent() string {
|
||||
return ua
|
||||
}
|
||||
|
||||
func (c *SoraDirectClient) taskUserAgent() string {
|
||||
if c != nil && c.cfg != nil {
|
||||
if ua := strings.TrimSpace(c.cfg.Sora.Client.UserAgent); ua != "" {
|
||||
return ua
|
||||
}
|
||||
}
|
||||
if len(soraDesktopUserAgents) > 0 {
|
||||
return soraDesktopUserAgents[0]
|
||||
}
|
||||
return soraDefaultUserAgent
|
||||
}
|
||||
|
||||
func (c *SoraDirectClient) resolveProxyURL(account *Account) string {
|
||||
if account == nil || account.ProxyID == nil || account.Proxy == nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(account.Proxy.URL())
|
||||
}
|
||||
|
||||
func (c *SoraDirectClient) getAccessToken(ctx context.Context, account *Account) (string, error) {
|
||||
if account == nil {
|
||||
return "", errors.New("account is nil")
|
||||
@@ -925,9 +960,26 @@ func (c *SoraDirectClient) buildBaseHeaders(token, userAgent string) http.Header
|
||||
}
|
||||
|
||||
func (c *SoraDirectClient) doRequest(ctx context.Context, account *Account, method, urlStr string, headers http.Header, body io.Reader, allowRetry bool) ([]byte, http.Header, error) {
|
||||
return c.doRequestWithProxy(ctx, account, c.resolveProxyURL(account), method, urlStr, headers, body, allowRetry)
|
||||
}
|
||||
|
||||
func (c *SoraDirectClient) doRequestWithProxy(
|
||||
ctx context.Context,
|
||||
account *Account,
|
||||
proxyURL string,
|
||||
method,
|
||||
urlStr string,
|
||||
headers http.Header,
|
||||
body io.Reader,
|
||||
allowRetry bool,
|
||||
) ([]byte, http.Header, error) {
|
||||
if strings.TrimSpace(urlStr) == "" {
|
||||
return nil, nil, errors.New("empty upstream url")
|
||||
}
|
||||
proxyURL = strings.TrimSpace(proxyURL)
|
||||
if proxyURL == "" {
|
||||
proxyURL = c.resolveProxyURL(account)
|
||||
}
|
||||
timeout := 0
|
||||
if c != nil && c.cfg != nil {
|
||||
timeout = c.cfg.Sora.Client.TimeoutSeconds
|
||||
@@ -968,7 +1020,7 @@ func (c *SoraDirectClient) doRequest(ctx context.Context, account *Account, meth
|
||||
attempts,
|
||||
timeout,
|
||||
len(bodyBytes),
|
||||
account != nil && account.ProxyID != nil && account.Proxy != nil,
|
||||
proxyURL != "",
|
||||
formatSoraHeaders(headers),
|
||||
)
|
||||
}
|
||||
@@ -984,10 +1036,6 @@ func (c *SoraDirectClient) doRequest(ctx context.Context, account *Account, meth
|
||||
req.Header = headers.Clone()
|
||||
start := time.Now()
|
||||
|
||||
proxyURL := ""
|
||||
if account != nil && account.ProxyID != nil && account.Proxy != nil {
|
||||
proxyURL = account.Proxy.URL()
|
||||
}
|
||||
resp, err := c.doHTTP(req, proxyURL, account)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
@@ -1183,10 +1231,13 @@ func soraBaseURLNotFoundHint(requestURL string) string {
|
||||
return "(请检查 sora.client.base_url,建议配置为 https://sora.chatgpt.com/backend)"
|
||||
}
|
||||
|
||||
func (c *SoraDirectClient) generateSentinelToken(ctx context.Context, account *Account, accessToken string) (string, error) {
|
||||
func (c *SoraDirectClient) generateSentinelToken(ctx context.Context, account *Account, accessToken, userAgent, proxyURL string) (string, error) {
|
||||
reqID := uuid.NewString()
|
||||
userAgent := soraRandChoice(soraDesktopUserAgents)
|
||||
powToken := soraGetPowToken(userAgent)
|
||||
userAgent = strings.TrimSpace(userAgent)
|
||||
if userAgent == "" {
|
||||
userAgent = c.taskUserAgent()
|
||||
}
|
||||
powToken := soraPowTokenGenerator(userAgent)
|
||||
payload := map[string]any{
|
||||
"p": powToken,
|
||||
"flow": soraSentinelFlow,
|
||||
@@ -1207,7 +1258,7 @@ func (c *SoraDirectClient) generateSentinelToken(ctx context.Context, account *A
|
||||
}
|
||||
|
||||
urlStr := soraChatGPTBaseURL + "/backend-api/sentinel/req"
|
||||
respBody, _, err := c.doRequest(ctx, account, http.MethodPost, urlStr, headers, bytes.NewReader(body), true)
|
||||
respBody, _, err := c.doRequestWithProxy(ctx, account, proxyURL, http.MethodPost, urlStr, headers, bytes.NewReader(body), true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -1223,16 +1274,6 @@ func (c *SoraDirectClient) generateSentinelToken(ctx context.Context, account *A
|
||||
return sentinel, nil
|
||||
}
|
||||
|
||||
func soraRandChoice(items []string) string {
|
||||
if len(items) == 0 {
|
||||
return ""
|
||||
}
|
||||
soraRandMu.Lock()
|
||||
idx := soraRand.Intn(len(items))
|
||||
soraRandMu.Unlock()
|
||||
return items[idx]
|
||||
}
|
||||
|
||||
func soraGetPowToken(userAgent string) string {
|
||||
configList := soraBuildPowConfig(userAgent)
|
||||
seed := strconv.FormatFloat(soraRandFloat(), 'f', -1, 64)
|
||||
@@ -1248,13 +1289,16 @@ func soraRandFloat() float64 {
|
||||
}
|
||||
|
||||
func soraBuildPowConfig(userAgent string) []any {
|
||||
screen := soraRandChoice([]string{
|
||||
strconv.Itoa(1920 + 1080),
|
||||
strconv.Itoa(2560 + 1440),
|
||||
strconv.Itoa(1920 + 1200),
|
||||
strconv.Itoa(2560 + 1600),
|
||||
})
|
||||
screenVal, _ := strconv.Atoi(screen)
|
||||
userAgent = strings.TrimSpace(userAgent)
|
||||
if userAgent == "" && len(soraDesktopUserAgents) > 0 {
|
||||
userAgent = soraDesktopUserAgents[0]
|
||||
}
|
||||
screenVal := soraStableChoiceInt([]int{
|
||||
1920 + 1080,
|
||||
2560 + 1440,
|
||||
1920 + 1200,
|
||||
2560 + 1600,
|
||||
}, userAgent+"|screen")
|
||||
perfMs := float64(time.Since(soraPerfStart).Milliseconds())
|
||||
wallMs := float64(time.Now().UnixNano()) / 1e6
|
||||
diff := wallMs - perfMs
|
||||
@@ -1264,32 +1308,47 @@ func soraBuildPowConfig(userAgent string) []any {
|
||||
4294705152,
|
||||
0,
|
||||
userAgent,
|
||||
soraRandChoice(soraPowScripts),
|
||||
soraRandChoice(soraPowDPL),
|
||||
soraStableChoice(soraPowScripts, userAgent+"|script"),
|
||||
soraStableChoice(soraPowDPL, userAgent+"|dpl"),
|
||||
"en-US",
|
||||
"en-US,es-US,en,es",
|
||||
0,
|
||||
soraRandChoice(soraPowNavigatorKeys),
|
||||
soraRandChoice(soraPowDocumentKeys),
|
||||
soraRandChoice(soraPowWindowKeys),
|
||||
soraStableChoice(soraPowNavigatorKeys, userAgent+"|navigator"),
|
||||
soraStableChoice(soraPowDocumentKeys, userAgent+"|document"),
|
||||
soraStableChoice(soraPowWindowKeys, userAgent+"|window"),
|
||||
perfMs,
|
||||
uuid.NewString(),
|
||||
"",
|
||||
soraRandChoiceInt(soraPowCores),
|
||||
soraStableChoiceInt(soraPowCores, userAgent+"|cores"),
|
||||
diff,
|
||||
}
|
||||
}
|
||||
|
||||
func soraRandChoiceInt(items []int) int {
|
||||
func soraStableChoice(items []string, seed string) string {
|
||||
if len(items) == 0 {
|
||||
return ""
|
||||
}
|
||||
idx := soraStableIndex(seed, len(items))
|
||||
return items[idx]
|
||||
}
|
||||
|
||||
func soraStableChoiceInt(items []int, seed string) int {
|
||||
if len(items) == 0 {
|
||||
return 0
|
||||
}
|
||||
soraRandMu.Lock()
|
||||
idx := soraRand.Intn(len(items))
|
||||
soraRandMu.Unlock()
|
||||
idx := soraStableIndex(seed, len(items))
|
||||
return items[idx]
|
||||
}
|
||||
|
||||
func soraStableIndex(seed string, size int) int {
|
||||
if size <= 0 {
|
||||
return 0
|
||||
}
|
||||
h := fnv.New32a()
|
||||
_, _ = h.Write([]byte(seed))
|
||||
return int(h.Sum32() % uint32(size))
|
||||
}
|
||||
|
||||
func soraPowParseTime() string {
|
||||
loc := time.FixedZone("EST", -5*3600)
|
||||
return time.Now().In(loc).Format("Mon Jan 02 2006 15:04:05 GMT-0700 (Eastern Standard Time)")
|
||||
|
||||
Reference in New Issue
Block a user