feat(account-test): 增强 Sora 账号测试能力探测与弹窗交互
- 后端新增 Sora2 邀请码与剩余额度探测,并补充对应结果解析 - Sora 测试流程补齐请求头与 Cloudflare 场景提示,完善单测覆盖 - 前端测试弹窗对 Sora 账号改为免选模型流程,并新增中英文提示文案 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -34,6 +34,9 @@ const (
|
||||
chatgptCodexAPIURL = "https://chatgpt.com/backend-api/codex/responses"
|
||||
soraMeAPIURL = "https://sora.chatgpt.com/backend/me" // Sora 用户信息接口,用于测试连接
|
||||
soraBillingAPIURL = "https://sora.chatgpt.com/backend/billing/subscriptions"
|
||||
soraInviteMineURL = "https://sora.chatgpt.com/backend/project_y/invite/mine"
|
||||
soraBootstrapURL = "https://sora.chatgpt.com/backend/m/bootstrap"
|
||||
soraRemainingURL = "https://sora.chatgpt.com/backend/nf/check"
|
||||
)
|
||||
|
||||
// TestEvent represents a SSE event for account testing
|
||||
@@ -498,6 +501,9 @@ func (s *AccountTestService) testSoraAccountConnection(c *gin.Context, account *
|
||||
req.Header.Set("Authorization", "Bearer "+authToken)
|
||||
req.Header.Set("User-Agent", "Sora/1.2026.007 (Android 15; 24122RKC7C; build 2600700)")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("Accept-Language", "en-US,en;q=0.9")
|
||||
req.Header.Set("Origin", "https://sora.chatgpt.com")
|
||||
req.Header.Set("Referer", "https://sora.chatgpt.com/")
|
||||
|
||||
// Get proxy URL
|
||||
proxyURL := ""
|
||||
@@ -543,6 +549,9 @@ func (s *AccountTestService) testSoraAccountConnection(c *gin.Context, account *
|
||||
subReq.Header.Set("Authorization", "Bearer "+authToken)
|
||||
subReq.Header.Set("User-Agent", "Sora/1.2026.007 (Android 15; 24122RKC7C; build 2600700)")
|
||||
subReq.Header.Set("Accept", "application/json")
|
||||
subReq.Header.Set("Accept-Language", "en-US,en;q=0.9")
|
||||
subReq.Header.Set("Origin", "https://sora.chatgpt.com")
|
||||
subReq.Header.Set("Referer", "https://sora.chatgpt.com/")
|
||||
|
||||
subResp, subErr := s.httpUpstream.DoWithTLS(subReq, proxyURL, account.ID, account.Concurrency, enableSoraTLSFingerprint)
|
||||
if subErr != nil {
|
||||
@@ -566,10 +575,134 @@ func (s *AccountTestService) testSoraAccountConnection(c *gin.Context, account *
|
||||
}
|
||||
}
|
||||
|
||||
// 追加 Sora2 能力探测(对齐 sora2api 的测试思路):邀请码 + 剩余额度。
|
||||
s.testSora2Capabilities(c, ctx, account, authToken, proxyURL, enableSoraTLSFingerprint)
|
||||
|
||||
s.sendEvent(c, TestEvent{Type: "test_complete", Success: true})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *AccountTestService) testSora2Capabilities(
|
||||
c *gin.Context,
|
||||
ctx context.Context,
|
||||
account *Account,
|
||||
authToken string,
|
||||
proxyURL string,
|
||||
enableTLSFingerprint bool,
|
||||
) {
|
||||
inviteStatus, inviteHeader, inviteBody, err := s.fetchSoraTestEndpoint(
|
||||
ctx,
|
||||
account,
|
||||
authToken,
|
||||
soraInviteMineURL,
|
||||
proxyURL,
|
||||
enableTLSFingerprint,
|
||||
)
|
||||
if err != nil {
|
||||
s.sendEvent(c, TestEvent{Type: "content", Text: fmt.Sprintf("Sora2 invite check skipped: %s", err.Error())})
|
||||
return
|
||||
}
|
||||
|
||||
if inviteStatus == http.StatusUnauthorized {
|
||||
bootstrapStatus, _, _, bootstrapErr := s.fetchSoraTestEndpoint(
|
||||
ctx,
|
||||
account,
|
||||
authToken,
|
||||
soraBootstrapURL,
|
||||
proxyURL,
|
||||
enableTLSFingerprint,
|
||||
)
|
||||
if bootstrapErr == nil && bootstrapStatus == http.StatusOK {
|
||||
s.sendEvent(c, TestEvent{Type: "content", Text: "Sora2 bootstrap OK, retry invite check"})
|
||||
inviteStatus, inviteHeader, inviteBody, err = s.fetchSoraTestEndpoint(
|
||||
ctx,
|
||||
account,
|
||||
authToken,
|
||||
soraInviteMineURL,
|
||||
proxyURL,
|
||||
enableTLSFingerprint,
|
||||
)
|
||||
if err != nil {
|
||||
s.sendEvent(c, TestEvent{Type: "content", Text: fmt.Sprintf("Sora2 invite retry failed: %s", err.Error())})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if inviteStatus != http.StatusOK {
|
||||
if isCloudflareChallengeResponse(inviteStatus, inviteBody) {
|
||||
s.sendEvent(c, TestEvent{Type: "content", Text: formatCloudflareChallengeMessage("Sora2 invite check blocked by Cloudflare challenge (HTTP 403)", inviteHeader, inviteBody)})
|
||||
return
|
||||
}
|
||||
s.sendEvent(c, TestEvent{Type: "content", Text: fmt.Sprintf("Sora2 invite check returned %d", inviteStatus)})
|
||||
return
|
||||
}
|
||||
|
||||
if summary := parseSoraInviteSummary(inviteBody); summary != "" {
|
||||
s.sendEvent(c, TestEvent{Type: "content", Text: summary})
|
||||
} else {
|
||||
s.sendEvent(c, TestEvent{Type: "content", Text: "Sora2 invite check OK"})
|
||||
}
|
||||
|
||||
remainingStatus, remainingHeader, remainingBody, remainingErr := s.fetchSoraTestEndpoint(
|
||||
ctx,
|
||||
account,
|
||||
authToken,
|
||||
soraRemainingURL,
|
||||
proxyURL,
|
||||
enableTLSFingerprint,
|
||||
)
|
||||
if remainingErr != nil {
|
||||
s.sendEvent(c, TestEvent{Type: "content", Text: fmt.Sprintf("Sora2 remaining check skipped: %s", remainingErr.Error())})
|
||||
return
|
||||
}
|
||||
if remainingStatus != http.StatusOK {
|
||||
if isCloudflareChallengeResponse(remainingStatus, remainingBody) {
|
||||
s.sendEvent(c, TestEvent{Type: "content", Text: formatCloudflareChallengeMessage("Sora2 remaining check blocked by Cloudflare challenge (HTTP 403)", remainingHeader, remainingBody)})
|
||||
return
|
||||
}
|
||||
s.sendEvent(c, TestEvent{Type: "content", Text: fmt.Sprintf("Sora2 remaining check returned %d", remainingStatus)})
|
||||
return
|
||||
}
|
||||
if summary := parseSoraRemainingSummary(remainingBody); summary != "" {
|
||||
s.sendEvent(c, TestEvent{Type: "content", Text: summary})
|
||||
} else {
|
||||
s.sendEvent(c, TestEvent{Type: "content", Text: "Sora2 remaining check OK"})
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AccountTestService) fetchSoraTestEndpoint(
|
||||
ctx context.Context,
|
||||
account *Account,
|
||||
authToken string,
|
||||
url string,
|
||||
proxyURL string,
|
||||
enableTLSFingerprint bool,
|
||||
) (int, http.Header, []byte, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
return 0, nil, nil, err
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+authToken)
|
||||
req.Header.Set("User-Agent", "Sora/1.2026.007 (Android 15; 24122RKC7C; build 2600700)")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("Accept-Language", "en-US,en;q=0.9")
|
||||
req.Header.Set("Origin", "https://sora.chatgpt.com")
|
||||
req.Header.Set("Referer", "https://sora.chatgpt.com/")
|
||||
|
||||
resp, err := s.httpUpstream.DoWithTLS(req, proxyURL, account.ID, account.Concurrency, enableTLSFingerprint)
|
||||
if err != nil {
|
||||
return 0, nil, nil, err
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
body, readErr := io.ReadAll(resp.Body)
|
||||
if readErr != nil {
|
||||
return resp.StatusCode, resp.Header, nil, readErr
|
||||
}
|
||||
return resp.StatusCode, resp.Header, body, nil
|
||||
}
|
||||
|
||||
func parseSoraSubscriptionSummary(body []byte) string {
|
||||
var subResp struct {
|
||||
Data []struct {
|
||||
@@ -604,6 +737,48 @@ func parseSoraSubscriptionSummary(body []byte) string {
|
||||
return "Subscription: " + strings.Join(parts, " | ")
|
||||
}
|
||||
|
||||
func parseSoraInviteSummary(body []byte) string {
|
||||
var inviteResp struct {
|
||||
InviteCode string `json:"invite_code"`
|
||||
RedeemedCount int64 `json:"redeemed_count"`
|
||||
TotalCount int64 `json:"total_count"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &inviteResp); err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
parts := []string{"Sora2: supported"}
|
||||
if inviteResp.InviteCode != "" {
|
||||
parts = append(parts, "invite="+inviteResp.InviteCode)
|
||||
}
|
||||
if inviteResp.TotalCount > 0 {
|
||||
parts = append(parts, fmt.Sprintf("used=%d/%d", inviteResp.RedeemedCount, inviteResp.TotalCount))
|
||||
}
|
||||
return strings.Join(parts, " | ")
|
||||
}
|
||||
|
||||
func parseSoraRemainingSummary(body []byte) string {
|
||||
var remainingResp struct {
|
||||
RateLimitAndCreditBalance struct {
|
||||
EstimatedNumVideosRemaining int64 `json:"estimated_num_videos_remaining"`
|
||||
RateLimitReached bool `json:"rate_limit_reached"`
|
||||
AccessResetsInSeconds int64 `json:"access_resets_in_seconds"`
|
||||
} `json:"rate_limit_and_credit_balance"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &remainingResp); err != nil {
|
||||
return ""
|
||||
}
|
||||
info := remainingResp.RateLimitAndCreditBalance
|
||||
parts := []string{fmt.Sprintf("Sora2 remaining: %d", info.EstimatedNumVideosRemaining)}
|
||||
if info.RateLimitReached {
|
||||
parts = append(parts, "rate_limited=true")
|
||||
}
|
||||
if info.AccessResetsInSeconds > 0 {
|
||||
parts = append(parts, fmt.Sprintf("reset_in=%ds", info.AccessResetsInSeconds))
|
||||
}
|
||||
return strings.Join(parts, " | ")
|
||||
}
|
||||
|
||||
func (s *AccountTestService) shouldEnableSoraTLSFingerprint() bool {
|
||||
if s == nil || s.cfg == nil {
|
||||
return false
|
||||
|
||||
@@ -61,6 +61,8 @@ func TestAccountTestService_testSoraAccountConnection_WithSubscription(t *testin
|
||||
responses: []*http.Response{
|
||||
newJSONResponse(http.StatusOK, `{"email":"demo@example.com"}`),
|
||||
newJSONResponse(http.StatusOK, `{"data":[{"plan":{"id":"chatgpt_plus","title":"ChatGPT Plus"},"end_ts":"2026-12-31T00:00:00Z"}]}`),
|
||||
newJSONResponse(http.StatusOK, `{"invite_code":"inv_abc","redeemed_count":3,"total_count":50}`),
|
||||
newJSONResponse(http.StatusOK, `{"rate_limit_and_credit_balance":{"estimated_num_videos_remaining":27,"rate_limit_reached":false,"access_resets_in_seconds":46833}}`),
|
||||
},
|
||||
}
|
||||
svc := &AccountTestService{
|
||||
@@ -92,17 +94,21 @@ func TestAccountTestService_testSoraAccountConnection_WithSubscription(t *testin
|
||||
err := svc.testSoraAccountConnection(c, account)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Len(t, upstream.requests, 2)
|
||||
require.Len(t, upstream.requests, 4)
|
||||
require.Equal(t, soraMeAPIURL, upstream.requests[0].URL.String())
|
||||
require.Equal(t, soraBillingAPIURL, upstream.requests[1].URL.String())
|
||||
require.Equal(t, soraInviteMineURL, upstream.requests[2].URL.String())
|
||||
require.Equal(t, soraRemainingURL, upstream.requests[3].URL.String())
|
||||
require.Equal(t, "Bearer test_token", upstream.requests[0].Header.Get("Authorization"))
|
||||
require.Equal(t, "Bearer test_token", upstream.requests[1].Header.Get("Authorization"))
|
||||
require.Equal(t, []bool{true, true}, upstream.tlsFlags)
|
||||
require.Equal(t, []bool{true, true, true, true}, upstream.tlsFlags)
|
||||
|
||||
body := rec.Body.String()
|
||||
require.Contains(t, body, `"type":"test_start"`)
|
||||
require.Contains(t, body, "Sora connection OK - Email: demo@example.com")
|
||||
require.Contains(t, body, "Subscription: ChatGPT Plus | chatgpt_plus | end=2026-12-31T00:00:00Z")
|
||||
require.Contains(t, body, "Sora2: supported | invite=inv_abc | used=3/50")
|
||||
require.Contains(t, body, "Sora2 remaining: 27 | reset_in=46833s")
|
||||
require.Contains(t, body, `"type":"test_complete","success":true`)
|
||||
}
|
||||
|
||||
@@ -111,6 +117,8 @@ func TestAccountTestService_testSoraAccountConnection_SubscriptionFailedStillSuc
|
||||
responses: []*http.Response{
|
||||
newJSONResponse(http.StatusOK, `{"name":"demo-user"}`),
|
||||
newJSONResponse(http.StatusForbidden, `{"error":{"message":"forbidden"}}`),
|
||||
newJSONResponse(http.StatusUnauthorized, `{"error":{"message":"Unauthorized"}}`),
|
||||
newJSONResponse(http.StatusForbidden, `{"error":{"message":"forbidden"}}`),
|
||||
},
|
||||
}
|
||||
svc := &AccountTestService{httpUpstream: upstream}
|
||||
@@ -128,10 +136,11 @@ func TestAccountTestService_testSoraAccountConnection_SubscriptionFailedStillSuc
|
||||
err := svc.testSoraAccountConnection(c, account)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Len(t, upstream.requests, 2)
|
||||
require.Len(t, upstream.requests, 4)
|
||||
body := rec.Body.String()
|
||||
require.Contains(t, body, "Sora connection OK - User: demo-user")
|
||||
require.Contains(t, body, "Subscription check returned 403")
|
||||
require.Contains(t, body, "Sora2 invite check returned 401")
|
||||
require.Contains(t, body, `"type":"test_complete","success":true`)
|
||||
}
|
||||
|
||||
@@ -169,6 +178,7 @@ func TestAccountTestService_testSoraAccountConnection_SubscriptionCloudflareChal
|
||||
responses: []*http.Response{
|
||||
newJSONResponse(http.StatusOK, `{"name":"demo-user"}`),
|
||||
newJSONResponse(http.StatusForbidden, `<!DOCTYPE html><html><head><title>Just a moment...</title></head><body><script>window._cf_chl_opt={cRay: '9cff2d62d83bb98d'};</script><noscript>Enable JavaScript and cookies to continue</noscript></body></html>`),
|
||||
newJSONResponse(http.StatusForbidden, `<!DOCTYPE html><html><head><title>Just a moment...</title></head><body><script>window._cf_chl_opt={cRay: '9cff2d62d83bb98d'};</script><noscript>Enable JavaScript and cookies to continue</noscript></body></html>`),
|
||||
},
|
||||
}
|
||||
svc := &AccountTestService{httpUpstream: upstream}
|
||||
@@ -188,6 +198,7 @@ func TestAccountTestService_testSoraAccountConnection_SubscriptionCloudflareChal
|
||||
require.NoError(t, err)
|
||||
body := rec.Body.String()
|
||||
require.Contains(t, body, "Subscription check blocked by Cloudflare challenge (HTTP 403)")
|
||||
require.Contains(t, body, "Sora2 invite check blocked by Cloudflare challenge (HTTP 403)")
|
||||
require.Contains(t, body, "cf-ray: 9cff2d62d83bb98d")
|
||||
require.Contains(t, body, `"type":"test_complete","success":true`)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user