feat(account-test): 增强 Sora 账号测试能力探测与弹窗交互

- 后端新增 Sora2 邀请码与剩余额度探测,并补充对应结果解析
- Sora 测试流程补齐请求头与 Cloudflare 场景提示,完善单测覆盖
- 前端测试弹窗对 Sora 账号改为免选模型流程,并新增中英文提示文案

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
yangjianbo
2026-02-19 08:29:51 +08:00
parent 5d2219d299
commit be09188bda
6 changed files with 251 additions and 26 deletions

View File

@@ -34,6 +34,9 @@ const (
chatgptCodexAPIURL = "https://chatgpt.com/backend-api/codex/responses" chatgptCodexAPIURL = "https://chatgpt.com/backend-api/codex/responses"
soraMeAPIURL = "https://sora.chatgpt.com/backend/me" // Sora 用户信息接口,用于测试连接 soraMeAPIURL = "https://sora.chatgpt.com/backend/me" // Sora 用户信息接口,用于测试连接
soraBillingAPIURL = "https://sora.chatgpt.com/backend/billing/subscriptions" 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 // 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("Authorization", "Bearer "+authToken)
req.Header.Set("User-Agent", "Sora/1.2026.007 (Android 15; 24122RKC7C; build 2600700)") req.Header.Set("User-Agent", "Sora/1.2026.007 (Android 15; 24122RKC7C; build 2600700)")
req.Header.Set("Accept", "application/json") 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 // Get proxy URL
proxyURL := "" proxyURL := ""
@@ -543,6 +549,9 @@ func (s *AccountTestService) testSoraAccountConnection(c *gin.Context, account *
subReq.Header.Set("Authorization", "Bearer "+authToken) subReq.Header.Set("Authorization", "Bearer "+authToken)
subReq.Header.Set("User-Agent", "Sora/1.2026.007 (Android 15; 24122RKC7C; build 2600700)") subReq.Header.Set("User-Agent", "Sora/1.2026.007 (Android 15; 24122RKC7C; build 2600700)")
subReq.Header.Set("Accept", "application/json") 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) subResp, subErr := s.httpUpstream.DoWithTLS(subReq, proxyURL, account.ID, account.Concurrency, enableSoraTLSFingerprint)
if subErr != nil { 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}) s.sendEvent(c, TestEvent{Type: "test_complete", Success: true})
return nil 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 { func parseSoraSubscriptionSummary(body []byte) string {
var subResp struct { var subResp struct {
Data []struct { Data []struct {
@@ -604,6 +737,48 @@ func parseSoraSubscriptionSummary(body []byte) string {
return "Subscription: " + strings.Join(parts, " | ") 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 { func (s *AccountTestService) shouldEnableSoraTLSFingerprint() bool {
if s == nil || s.cfg == nil { if s == nil || s.cfg == nil {
return false return false

View File

@@ -61,6 +61,8 @@ func TestAccountTestService_testSoraAccountConnection_WithSubscription(t *testin
responses: []*http.Response{ responses: []*http.Response{
newJSONResponse(http.StatusOK, `{"email":"demo@example.com"}`), 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, `{"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{ svc := &AccountTestService{
@@ -92,17 +94,21 @@ func TestAccountTestService_testSoraAccountConnection_WithSubscription(t *testin
err := svc.testSoraAccountConnection(c, account) err := svc.testSoraAccountConnection(c, account)
require.NoError(t, err) 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, soraMeAPIURL, upstream.requests[0].URL.String())
require.Equal(t, soraBillingAPIURL, upstream.requests[1].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[0].Header.Get("Authorization"))
require.Equal(t, "Bearer test_token", upstream.requests[1].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() body := rec.Body.String()
require.Contains(t, body, `"type":"test_start"`) require.Contains(t, body, `"type":"test_start"`)
require.Contains(t, body, "Sora connection OK - Email: demo@example.com") 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, "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`) require.Contains(t, body, `"type":"test_complete","success":true`)
} }
@@ -111,6 +117,8 @@ func TestAccountTestService_testSoraAccountConnection_SubscriptionFailedStillSuc
responses: []*http.Response{ responses: []*http.Response{
newJSONResponse(http.StatusOK, `{"name":"demo-user"}`), newJSONResponse(http.StatusOK, `{"name":"demo-user"}`),
newJSONResponse(http.StatusForbidden, `{"error":{"message":"forbidden"}}`), newJSONResponse(http.StatusForbidden, `{"error":{"message":"forbidden"}}`),
newJSONResponse(http.StatusUnauthorized, `{"error":{"message":"Unauthorized"}}`),
newJSONResponse(http.StatusForbidden, `{"error":{"message":"forbidden"}}`),
}, },
} }
svc := &AccountTestService{httpUpstream: upstream} svc := &AccountTestService{httpUpstream: upstream}
@@ -128,10 +136,11 @@ func TestAccountTestService_testSoraAccountConnection_SubscriptionFailedStillSuc
err := svc.testSoraAccountConnection(c, account) err := svc.testSoraAccountConnection(c, account)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, upstream.requests, 2) require.Len(t, upstream.requests, 4)
body := rec.Body.String() body := rec.Body.String()
require.Contains(t, body, "Sora connection OK - User: demo-user") require.Contains(t, body, "Sora connection OK - User: demo-user")
require.Contains(t, body, "Subscription check returned 403") 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`) require.Contains(t, body, `"type":"test_complete","success":true`)
} }
@@ -169,6 +178,7 @@ func TestAccountTestService_testSoraAccountConnection_SubscriptionCloudflareChal
responses: []*http.Response{ responses: []*http.Response{
newJSONResponse(http.StatusOK, `{"name":"demo-user"}`), 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>`),
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} svc := &AccountTestService{httpUpstream: upstream}
@@ -188,6 +198,7 @@ func TestAccountTestService_testSoraAccountConnection_SubscriptionCloudflareChal
require.NoError(t, err) require.NoError(t, err)
body := rec.Body.String() body := rec.Body.String()
require.Contains(t, body, "Subscription check blocked by Cloudflare challenge (HTTP 403)") 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, "cf-ray: 9cff2d62d83bb98d")
require.Contains(t, body, `"type":"test_complete","success":true`) require.Contains(t, body, `"type":"test_complete","success":true`)
} }

View File

@@ -41,7 +41,7 @@
</span> </span>
</div> </div>
<div class="space-y-1.5"> <div v-if="!isSoraAccount" class="space-y-1.5">
<label class="text-sm font-medium text-gray-700 dark:text-gray-300"> <label class="text-sm font-medium text-gray-700 dark:text-gray-300">
{{ t('admin.accounts.selectTestModel') }} {{ t('admin.accounts.selectTestModel') }}
</label> </label>
@@ -54,6 +54,12 @@
:placeholder="loadingModels ? t('common.loading') + '...' : t('admin.accounts.selectTestModel')" :placeholder="loadingModels ? t('common.loading') + '...' : t('admin.accounts.selectTestModel')"
/> />
</div> </div>
<div
v-else
class="rounded-lg border border-blue-200 bg-blue-50 px-3 py-2 text-xs text-blue-700 dark:border-blue-700 dark:bg-blue-900/20 dark:text-blue-300"
>
{{ t('admin.accounts.soraTestHint') }}
</div>
<!-- Terminal Output --> <!-- Terminal Output -->
<div class="group relative"> <div class="group relative">
@@ -135,12 +141,12 @@
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<span class="flex items-center gap-1"> <span class="flex items-center gap-1">
<Icon name="cpu" size="sm" :stroke-width="2" /> <Icon name="cpu" size="sm" :stroke-width="2" />
{{ t('admin.accounts.testModel') }} {{ isSoraAccount ? t('admin.accounts.soraTestTarget') : t('admin.accounts.testModel') }}
</span> </span>
</div> </div>
<span class="flex items-center gap-1"> <span class="flex items-center gap-1">
<Icon name="chatBubble" size="sm" :stroke-width="2" /> <Icon name="chatBubble" size="sm" :stroke-width="2" />
{{ t('admin.accounts.testPrompt') }} {{ isSoraAccount ? t('admin.accounts.soraTestMode') : t('admin.accounts.testPrompt') }}
</span> </span>
</div> </div>
</div> </div>
@@ -156,10 +162,10 @@
</button> </button>
<button <button
@click="startTest" @click="startTest"
:disabled="status === 'connecting' || !selectedModelId" :disabled="status === 'connecting' || (!isSoraAccount && !selectedModelId)"
:class="[ :class="[
'flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition-all', 'flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition-all',
status === 'connecting' || !selectedModelId status === 'connecting' || (!isSoraAccount && !selectedModelId)
? 'cursor-not-allowed bg-primary-400 text-white' ? 'cursor-not-allowed bg-primary-400 text-white'
: status === 'success' : status === 'success'
? 'bg-green-500 text-white hover:bg-green-600' ? 'bg-green-500 text-white hover:bg-green-600'
@@ -232,7 +238,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch, nextTick } from 'vue' import { computed, ref, watch, nextTick } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import BaseDialog from '@/components/common/BaseDialog.vue' import BaseDialog from '@/components/common/BaseDialog.vue'
import Select from '@/components/common/Select.vue' import Select from '@/components/common/Select.vue'
@@ -267,6 +273,7 @@ const availableModels = ref<ClaudeModel[]>([])
const selectedModelId = ref('') const selectedModelId = ref('')
const loadingModels = ref(false) const loadingModels = ref(false)
let eventSource: EventSource | null = null let eventSource: EventSource | null = null
const isSoraAccount = computed(() => props.account?.platform === 'sora')
// Load available models when modal opens // Load available models when modal opens
watch( watch(
@@ -283,6 +290,12 @@ watch(
const loadAvailableModels = async () => { const loadAvailableModels = async () => {
if (!props.account) return if (!props.account) return
if (props.account.platform === 'sora') {
availableModels.value = []
selectedModelId.value = ''
loadingModels.value = false
return
}
loadingModels.value = true loadingModels.value = true
selectedModelId.value = '' // Reset selection before loading selectedModelId.value = '' // Reset selection before loading
@@ -350,7 +363,7 @@ const scrollToBottom = async () => {
} }
const startTest = async () => { const startTest = async () => {
if (!props.account || !selectedModelId.value) return if (!props.account || (!isSoraAccount.value && !selectedModelId.value)) return
resetState() resetState()
status.value = 'connecting' status.value = 'connecting'
@@ -371,7 +384,9 @@ const startTest = async () => {
Authorization: `Bearer ${localStorage.getItem('auth_token')}`, Authorization: `Bearer ${localStorage.getItem('auth_token')}`,
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
body: JSON.stringify({ model_id: selectedModelId.value }) body: JSON.stringify(
isSoraAccount.value ? {} : { model_id: selectedModelId.value }
)
}) })
if (!response.ok) { if (!response.ok) {
@@ -428,7 +443,10 @@ const handleEvent = (event: {
if (event.model) { if (event.model) {
addLine(t('admin.accounts.usingModel', { model: event.model }), 'text-cyan-400') addLine(t('admin.accounts.usingModel', { model: event.model }), 'text-cyan-400')
} }
addLine(t('admin.accounts.sendingTestMessage'), 'text-gray-400') addLine(
isSoraAccount.value ? t('admin.accounts.soraTestingFlow') : t('admin.accounts.sendingTestMessage'),
'text-gray-400'
)
addLine('', 'text-gray-300') addLine('', 'text-gray-300')
addLine(t('admin.accounts.response'), 'text-yellow-400') addLine(t('admin.accounts.response'), 'text-yellow-400')
break break

View File

@@ -41,7 +41,7 @@
</span> </span>
</div> </div>
<div class="space-y-1.5"> <div v-if="!isSoraAccount" class="space-y-1.5">
<label class="text-sm font-medium text-gray-700 dark:text-gray-300"> <label class="text-sm font-medium text-gray-700 dark:text-gray-300">
{{ t('admin.accounts.selectTestModel') }} {{ t('admin.accounts.selectTestModel') }}
</label> </label>
@@ -54,6 +54,12 @@
:placeholder="loadingModels ? t('common.loading') + '...' : t('admin.accounts.selectTestModel')" :placeholder="loadingModels ? t('common.loading') + '...' : t('admin.accounts.selectTestModel')"
/> />
</div> </div>
<div
v-else
class="rounded-lg border border-blue-200 bg-blue-50 px-3 py-2 text-xs text-blue-700 dark:border-blue-700 dark:bg-blue-900/20 dark:text-blue-300"
>
{{ t('admin.accounts.soraTestHint') }}
</div>
<!-- Terminal Output --> <!-- Terminal Output -->
<div class="group relative"> <div class="group relative">
@@ -114,12 +120,12 @@
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<span class="flex items-center gap-1"> <span class="flex items-center gap-1">
<Icon name="grid" size="sm" :stroke-width="2" /> <Icon name="grid" size="sm" :stroke-width="2" />
{{ t('admin.accounts.testModel') }} {{ isSoraAccount ? t('admin.accounts.soraTestTarget') : t('admin.accounts.testModel') }}
</span> </span>
</div> </div>
<span class="flex items-center gap-1"> <span class="flex items-center gap-1">
<Icon name="chat" size="sm" :stroke-width="2" /> <Icon name="chat" size="sm" :stroke-width="2" />
{{ t('admin.accounts.testPrompt') }} {{ isSoraAccount ? t('admin.accounts.soraTestMode') : t('admin.accounts.testPrompt') }}
</span> </span>
</div> </div>
</div> </div>
@@ -135,10 +141,10 @@
</button> </button>
<button <button
@click="startTest" @click="startTest"
:disabled="status === 'connecting' || !selectedModelId" :disabled="status === 'connecting' || (!isSoraAccount && !selectedModelId)"
:class="[ :class="[
'flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition-all', 'flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition-all',
status === 'connecting' || !selectedModelId status === 'connecting' || (!isSoraAccount && !selectedModelId)
? 'cursor-not-allowed bg-primary-400 text-white' ? 'cursor-not-allowed bg-primary-400 text-white'
: status === 'success' : status === 'success'
? 'bg-green-500 text-white hover:bg-green-600' ? 'bg-green-500 text-white hover:bg-green-600'
@@ -172,7 +178,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch, nextTick } from 'vue' import { computed, ref, watch, nextTick } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import BaseDialog from '@/components/common/BaseDialog.vue' import BaseDialog from '@/components/common/BaseDialog.vue'
import Select from '@/components/common/Select.vue' import Select from '@/components/common/Select.vue'
@@ -207,6 +213,7 @@ const availableModels = ref<ClaudeModel[]>([])
const selectedModelId = ref('') const selectedModelId = ref('')
const loadingModels = ref(false) const loadingModels = ref(false)
let eventSource: EventSource | null = null let eventSource: EventSource | null = null
const isSoraAccount = computed(() => props.account?.platform === 'sora')
// Load available models when modal opens // Load available models when modal opens
watch( watch(
@@ -223,6 +230,12 @@ watch(
const loadAvailableModels = async () => { const loadAvailableModels = async () => {
if (!props.account) return if (!props.account) return
if (props.account.platform === 'sora') {
availableModels.value = []
selectedModelId.value = ''
loadingModels.value = false
return
}
loadingModels.value = true loadingModels.value = true
selectedModelId.value = '' // Reset selection before loading selectedModelId.value = '' // Reset selection before loading
@@ -238,11 +251,6 @@ const loadAvailableModels = async () => {
availableModels.value.find((m) => m.id === 'gemini-3-flash-preview') || availableModels.value.find((m) => m.id === 'gemini-3-flash-preview') ||
availableModels.value.find((m) => m.id === 'gemini-3-pro-preview') availableModels.value.find((m) => m.id === 'gemini-3-pro-preview')
selectedModelId.value = preferred?.id || availableModels.value[0].id selectedModelId.value = preferred?.id || availableModels.value[0].id
} else if (props.account.platform === 'sora') {
const preferred =
availableModels.value.find((m) => m.id === 'gpt-image') ||
availableModels.value.find((m) => !m.id.startsWith('prompt-enhance'))
selectedModelId.value = preferred?.id || availableModels.value[0].id
} else { } else {
// Try to select Sonnet as default, otherwise use first model // Try to select Sonnet as default, otherwise use first model
const sonnetModel = availableModels.value.find((m) => m.id.includes('sonnet')) const sonnetModel = availableModels.value.find((m) => m.id.includes('sonnet'))
@@ -295,7 +303,7 @@ const scrollToBottom = async () => {
} }
const startTest = async () => { const startTest = async () => {
if (!props.account || !selectedModelId.value) return if (!props.account || (!isSoraAccount.value && !selectedModelId.value)) return
resetState() resetState()
status.value = 'connecting' status.value = 'connecting'
@@ -316,7 +324,9 @@ const startTest = async () => {
Authorization: `Bearer ${localStorage.getItem('auth_token')}`, Authorization: `Bearer ${localStorage.getItem('auth_token')}`,
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
body: JSON.stringify({ model_id: selectedModelId.value }) body: JSON.stringify(
isSoraAccount.value ? {} : { model_id: selectedModelId.value }
)
}) })
if (!response.ok) { if (!response.ok) {
@@ -373,7 +383,10 @@ const handleEvent = (event: {
if (event.model) { if (event.model) {
addLine(t('admin.accounts.usingModel', { model: event.model }), 'text-cyan-400') addLine(t('admin.accounts.usingModel', { model: event.model }), 'text-cyan-400')
} }
addLine(t('admin.accounts.sendingTestMessage'), 'text-gray-400') addLine(
isSoraAccount.value ? t('admin.accounts.soraTestingFlow') : t('admin.accounts.sendingTestMessage'),
'text-gray-400'
)
addLine('', 'text-gray-300') addLine('', 'text-gray-300')
addLine(t('admin.accounts.response'), 'text-yellow-400') addLine(t('admin.accounts.response'), 'text-yellow-400')
break break

View File

@@ -1993,6 +1993,10 @@ export default {
selectTestModel: 'Select Test Model', selectTestModel: 'Select Test Model',
testModel: 'Test model', testModel: 'Test model',
testPrompt: 'Prompt: "hi"', testPrompt: 'Prompt: "hi"',
soraTestHint: 'Sora test runs connectivity and capability checks (/backend/me, subscription, Sora2 invite and remaining quota).',
soraTestTarget: 'Target: Sora account capability',
soraTestMode: 'Mode: Connectivity + Capability checks',
soraTestingFlow: 'Running Sora connectivity and capability checks...',
// Stats Modal // Stats Modal
viewStats: 'View Stats', viewStats: 'View Stats',
usageStatistics: 'Usage Statistics', usageStatistics: 'Usage Statistics',

View File

@@ -2125,6 +2125,10 @@ export default {
selectTestModel: '选择测试模型', selectTestModel: '选择测试模型',
testModel: '测试模型', testModel: '测试模型',
testPrompt: '提示词:"hi"', testPrompt: '提示词:"hi"',
soraTestHint: 'Sora 测试将执行连通性与能力检测(/backend/me、订阅信息、Sora2 邀请码与剩余额度)。',
soraTestTarget: '检测目标Sora 账号能力',
soraTestMode: '模式:连通性 + 能力探测',
soraTestingFlow: '执行 Sora 连通性与能力检测...',
// Stats Modal // Stats Modal
viewStats: '查看统计', viewStats: '查看统计',
usageStatistics: '使用统计', usageStatistics: '使用统计',