Commit be09188b authored by yangjianbo's avatar yangjianbo
Browse files

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



- 后端新增 Sora2 邀请码与剩余额度探测,并补充对应结果解析
- Sora 测试流程补齐请求头与 Cloudflare 场景提示,完善单测覆盖
- 前端测试弹窗对 Sora 账号改为免选模型流程,并新增中英文提示文案
Co-Authored-By: default avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 5d2219d2
...@@ -34,6 +34,9 @@ const ( ...@@ -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 * ...@@ -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 * ...@@ -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 * ...@@ -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 { ...@@ -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
......
...@@ -61,6 +61,8 @@ func TestAccountTestService_testSoraAccountConnection_WithSubscription(t *testin ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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`)
} }
...@@ -41,7 +41,7 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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[]>([]) ...@@ -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( ...@@ -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 () => { ...@@ -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 () => { ...@@ -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: { ...@@ -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
......
...@@ -41,7 +41,7 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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[]>([]) ...@@ -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( ...@@ -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 () => { ...@@ -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 () => { ...@@ -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 () => { ...@@ -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: { ...@@ -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
......
...@@ -1993,6 +1993,10 @@ export default { ...@@ -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',
......
...@@ -2125,6 +2125,10 @@ export default { ...@@ -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: '使用统计',
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment