Commit 900cce20 authored by yangjianbo's avatar yangjianbo
Browse files

feat(sora): 对齐 Sora OAuth 流程并隔离网关请求路径



- 新增并接通 Sora 专用 OAuth 接口与 ST/RT 换取能力
- 完成前端 Sora 授权、RT/ST 手动导入与账号创建流程
- 强化 Sora token 恢复、转发日志与网关路由隔离行为
- 补充后端服务层与路由层相关测试覆盖
Co-Authored-By: default avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 36bb3270
...@@ -18,6 +18,8 @@ type stubSoraClientForPoll struct { ...@@ -18,6 +18,8 @@ type stubSoraClientForPoll struct {
videoStatus *SoraVideoTaskStatus videoStatus *SoraVideoTaskStatus
imageCalls int imageCalls int
videoCalls int videoCalls int
enhanced string
enhanceErr error
} }
func (s *stubSoraClientForPoll) Enabled() bool { return true } func (s *stubSoraClientForPoll) Enabled() bool { return true }
...@@ -30,6 +32,12 @@ func (s *stubSoraClientForPoll) CreateImageTask(ctx context.Context, account *Ac ...@@ -30,6 +32,12 @@ func (s *stubSoraClientForPoll) CreateImageTask(ctx context.Context, account *Ac
func (s *stubSoraClientForPoll) CreateVideoTask(ctx context.Context, account *Account, req SoraVideoRequest) (string, error) { func (s *stubSoraClientForPoll) CreateVideoTask(ctx context.Context, account *Account, req SoraVideoRequest) (string, error) {
return "task-video", nil return "task-video", nil
} }
func (s *stubSoraClientForPoll) EnhancePrompt(ctx context.Context, account *Account, prompt, expansionLevel string, durationS int) (string, error) {
if s.enhanced != "" {
return s.enhanced, s.enhanceErr
}
return "enhanced prompt", s.enhanceErr
}
func (s *stubSoraClientForPoll) GetImageTask(ctx context.Context, account *Account, taskID string) (*SoraImageTaskStatus, error) { func (s *stubSoraClientForPoll) GetImageTask(ctx context.Context, account *Account, taskID string) (*SoraImageTaskStatus, error) {
s.imageCalls++ s.imageCalls++
return s.imageStatus, nil return s.imageStatus, nil
...@@ -62,6 +70,33 @@ func TestSoraGatewayService_PollImageTaskCompleted(t *testing.T) { ...@@ -62,6 +70,33 @@ func TestSoraGatewayService_PollImageTaskCompleted(t *testing.T) {
require.Equal(t, 1, client.imageCalls) require.Equal(t, 1, client.imageCalls)
} }
func TestSoraGatewayService_ForwardPromptEnhance(t *testing.T) {
client := &stubSoraClientForPoll{
enhanced: "cinematic prompt",
}
cfg := &config.Config{
Sora: config.SoraConfig{
Client: config.SoraClientConfig{
PollIntervalSeconds: 1,
MaxPollAttempts: 1,
},
},
}
svc := NewSoraGatewayService(client, nil, nil, cfg)
account := &Account{
ID: 1,
Platform: PlatformSora,
Status: StatusActive,
}
body := []byte(`{"model":"prompt-enhance-short-10s","messages":[{"role":"user","content":"cat running"}],"stream":false}`)
result, err := svc.Forward(context.Background(), nil, account, body, false)
require.NoError(t, err)
require.NotNil(t, result)
require.Equal(t, "prompt", result.MediaType)
require.Equal(t, "prompt-enhance-short-10s", result.Model)
}
func TestSoraGatewayService_PollVideoTaskFailed(t *testing.T) { func TestSoraGatewayService_PollVideoTaskFailed(t *testing.T) {
client := &stubSoraClientForPoll{ client := &stubSoraClientForPoll{
videoStatus: &SoraVideoTaskStatus{ videoStatus: &SoraVideoTaskStatus{
...@@ -178,6 +213,7 @@ func TestSoraProErrorMessage(t *testing.T) { ...@@ -178,6 +213,7 @@ func TestSoraProErrorMessage(t *testing.T) {
func TestShouldFailoverUpstreamError(t *testing.T) { func TestShouldFailoverUpstreamError(t *testing.T) {
svc := NewSoraGatewayService(nil, nil, nil, &config.Config{}) svc := NewSoraGatewayService(nil, nil, nil, &config.Config{})
require.True(t, svc.shouldFailoverUpstreamError(401)) require.True(t, svc.shouldFailoverUpstreamError(401))
require.True(t, svc.shouldFailoverUpstreamError(404))
require.True(t, svc.shouldFailoverUpstreamError(429)) require.True(t, svc.shouldFailoverUpstreamError(429))
require.True(t, svc.shouldFailoverUpstreamError(500)) require.True(t, svc.shouldFailoverUpstreamError(500))
require.True(t, svc.shouldFailoverUpstreamError(502)) require.True(t, svc.shouldFailoverUpstreamError(502))
......
...@@ -17,6 +17,9 @@ type SoraModelConfig struct { ...@@ -17,6 +17,9 @@ type SoraModelConfig struct {
Model string Model string
Size string Size string
RequirePro bool RequirePro bool
// Prompt-enhance 专用参数
ExpansionLevel string
DurationS int
} }
var soraModelConfigs = map[string]SoraModelConfig{ var soraModelConfigs = map[string]SoraModelConfig{
...@@ -160,31 +163,49 @@ var soraModelConfigs = map[string]SoraModelConfig{ ...@@ -160,31 +163,49 @@ var soraModelConfigs = map[string]SoraModelConfig{
RequirePro: true, RequirePro: true,
}, },
"prompt-enhance-short-10s": { "prompt-enhance-short-10s": {
Type: "prompt_enhance", Type: "prompt_enhance",
ExpansionLevel: "short",
DurationS: 10,
}, },
"prompt-enhance-short-15s": { "prompt-enhance-short-15s": {
Type: "prompt_enhance", Type: "prompt_enhance",
ExpansionLevel: "short",
DurationS: 15,
}, },
"prompt-enhance-short-20s": { "prompt-enhance-short-20s": {
Type: "prompt_enhance", Type: "prompt_enhance",
ExpansionLevel: "short",
DurationS: 20,
}, },
"prompt-enhance-medium-10s": { "prompt-enhance-medium-10s": {
Type: "prompt_enhance", Type: "prompt_enhance",
ExpansionLevel: "medium",
DurationS: 10,
}, },
"prompt-enhance-medium-15s": { "prompt-enhance-medium-15s": {
Type: "prompt_enhance", Type: "prompt_enhance",
ExpansionLevel: "medium",
DurationS: 15,
}, },
"prompt-enhance-medium-20s": { "prompt-enhance-medium-20s": {
Type: "prompt_enhance", Type: "prompt_enhance",
ExpansionLevel: "medium",
DurationS: 20,
}, },
"prompt-enhance-long-10s": { "prompt-enhance-long-10s": {
Type: "prompt_enhance", Type: "prompt_enhance",
ExpansionLevel: "long",
DurationS: 10,
}, },
"prompt-enhance-long-15s": { "prompt-enhance-long-15s": {
Type: "prompt_enhance", Type: "prompt_enhance",
ExpansionLevel: "long",
DurationS: 15,
}, },
"prompt-enhance-long-20s": { "prompt-enhance-long-20s": {
Type: "prompt_enhance", Type: "prompt_enhance",
ExpansionLevel: "long",
DurationS: 20,
}, },
} }
......
...@@ -43,10 +43,13 @@ func NewTokenRefreshService( ...@@ -43,10 +43,13 @@ func NewTokenRefreshService(
stopCh: make(chan struct{}), stopCh: make(chan struct{}),
} }
openAIRefresher := NewOpenAITokenRefresher(openaiOAuthService, accountRepo)
openAIRefresher.SetSyncLinkedSoraAccounts(cfg.TokenRefresh.SyncLinkedSoraAccounts)
// 注册平台特定的刷新器 // 注册平台特定的刷新器
s.refreshers = []TokenRefresher{ s.refreshers = []TokenRefresher{
NewClaudeTokenRefresher(oauthService), NewClaudeTokenRefresher(oauthService),
NewOpenAITokenRefresher(openaiOAuthService, accountRepo), openAIRefresher,
NewGeminiTokenRefresher(geminiOAuthService), NewGeminiTokenRefresher(geminiOAuthService),
NewAntigravityTokenRefresher(antigravityOAuthService), NewAntigravityTokenRefresher(antigravityOAuthService),
} }
......
...@@ -86,6 +86,7 @@ type OpenAITokenRefresher struct { ...@@ -86,6 +86,7 @@ type OpenAITokenRefresher struct {
openaiOAuthService *OpenAIOAuthService openaiOAuthService *OpenAIOAuthService
accountRepo AccountRepository accountRepo AccountRepository
soraAccountRepo SoraAccountRepository // Sora 扩展表仓储,用于双表同步 soraAccountRepo SoraAccountRepository // Sora 扩展表仓储,用于双表同步
syncLinkedSora bool
} }
// NewOpenAITokenRefresher 创建 OpenAI token刷新器 // NewOpenAITokenRefresher 创建 OpenAI token刷新器
...@@ -103,11 +104,15 @@ func (r *OpenAITokenRefresher) SetSoraAccountRepo(repo SoraAccountRepository) { ...@@ -103,11 +104,15 @@ func (r *OpenAITokenRefresher) SetSoraAccountRepo(repo SoraAccountRepository) {
r.soraAccountRepo = repo r.soraAccountRepo = repo
} }
// SetSyncLinkedSoraAccounts 控制是否同步覆盖关联的 Sora 账号 token。
func (r *OpenAITokenRefresher) SetSyncLinkedSoraAccounts(enabled bool) {
r.syncLinkedSora = enabled
}
// CanRefresh 检查是否能处理此账号 // CanRefresh 检查是否能处理此账号
// 只处理 openai 平台的 oauth 类型账号 // 只处理 openai 平台的 oauth 类型账号(不直接刷新 sora 平台账号)
func (r *OpenAITokenRefresher) CanRefresh(account *Account) bool { func (r *OpenAITokenRefresher) CanRefresh(account *Account) bool {
return (account.Platform == PlatformOpenAI || account.Platform == PlatformSora) && return account.Platform == PlatformOpenAI && account.Type == AccountTypeOAuth
account.Type == AccountTypeOAuth
} }
// NeedsRefresh 检查token是否需要刷新 // NeedsRefresh 检查token是否需要刷新
...@@ -141,7 +146,7 @@ func (r *OpenAITokenRefresher) Refresh(ctx context.Context, account *Account) (m ...@@ -141,7 +146,7 @@ func (r *OpenAITokenRefresher) Refresh(ctx context.Context, account *Account) (m
} }
// 异步同步关联的 Sora 账号(不阻塞主流程) // 异步同步关联的 Sora 账号(不阻塞主流程)
if r.accountRepo != nil { if r.accountRepo != nil && r.syncLinkedSora {
go r.syncLinkedSoraAccounts(context.Background(), account.ID, newCredentials) go r.syncLinkedSoraAccounts(context.Background(), account.ID, newCredentials)
} }
......
...@@ -226,3 +226,43 @@ func TestClaudeTokenRefresher_CanRefresh(t *testing.T) { ...@@ -226,3 +226,43 @@ func TestClaudeTokenRefresher_CanRefresh(t *testing.T) {
}) })
} }
} }
func TestOpenAITokenRefresher_CanRefresh(t *testing.T) {
refresher := &OpenAITokenRefresher{}
tests := []struct {
name string
platform string
accType string
want bool
}{
{
name: "openai oauth - can refresh",
platform: PlatformOpenAI,
accType: AccountTypeOAuth,
want: true,
},
{
name: "sora oauth - cannot refresh directly",
platform: PlatformSora,
accType: AccountTypeOAuth,
want: false,
},
{
name: "openai apikey - cannot refresh",
platform: PlatformOpenAI,
accType: AccountTypeAPIKey,
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
account := &Account{
Platform: tt.platform,
Type: tt.accType,
}
require.Equal(t, tt.want, refresher.CanRefresh(account))
})
}
}
...@@ -206,6 +206,18 @@ func ProvideSoraMediaStorage(cfg *config.Config) *SoraMediaStorage { ...@@ -206,6 +206,18 @@ func ProvideSoraMediaStorage(cfg *config.Config) *SoraMediaStorage {
return NewSoraMediaStorage(cfg) return NewSoraMediaStorage(cfg)
} }
func ProvideSoraDirectClient(
cfg *config.Config,
httpUpstream HTTPUpstream,
tokenProvider *OpenAITokenProvider,
accountRepo AccountRepository,
soraAccountRepo SoraAccountRepository,
) *SoraDirectClient {
client := NewSoraDirectClient(cfg, httpUpstream, tokenProvider)
client.SetAccountRepositories(accountRepo, soraAccountRepo)
return client
}
// ProvideSoraMediaCleanupService 创建并启动 Sora 媒体清理服务 // ProvideSoraMediaCleanupService 创建并启动 Sora 媒体清理服务
func ProvideSoraMediaCleanupService(storage *SoraMediaStorage, cfg *config.Config) *SoraMediaCleanupService { func ProvideSoraMediaCleanupService(storage *SoraMediaStorage, cfg *config.Config) *SoraMediaCleanupService {
svc := NewSoraMediaCleanupService(storage, cfg) svc := NewSoraMediaCleanupService(storage, cfg)
...@@ -255,7 +267,7 @@ var ProviderSet = wire.NewSet( ...@@ -255,7 +267,7 @@ var ProviderSet = wire.NewSet(
NewGatewayService, NewGatewayService,
ProvideSoraMediaStorage, ProvideSoraMediaStorage,
ProvideSoraMediaCleanupService, ProvideSoraMediaCleanupService,
NewSoraDirectClient, ProvideSoraDirectClient,
wire.Bind(new(SoraClient), new(*SoraDirectClient)), wire.Bind(new(SoraClient), new(*SoraDirectClient)),
NewSoraGatewayService, NewSoraGatewayService,
NewOpenAIGatewayService, NewOpenAIGatewayService,
......
...@@ -86,6 +86,7 @@ func (s *FrontendServer) Middleware() gin.HandlerFunc { ...@@ -86,6 +86,7 @@ func (s *FrontendServer) Middleware() gin.HandlerFunc {
if strings.HasPrefix(path, "/api/") || if strings.HasPrefix(path, "/api/") ||
strings.HasPrefix(path, "/v1/") || strings.HasPrefix(path, "/v1/") ||
strings.HasPrefix(path, "/v1beta/") || strings.HasPrefix(path, "/v1beta/") ||
strings.HasPrefix(path, "/sora/") ||
strings.HasPrefix(path, "/antigravity/") || strings.HasPrefix(path, "/antigravity/") ||
strings.HasPrefix(path, "/setup/") || strings.HasPrefix(path, "/setup/") ||
path == "/health" || path == "/health" ||
...@@ -209,6 +210,7 @@ func ServeEmbeddedFrontend() gin.HandlerFunc { ...@@ -209,6 +210,7 @@ func ServeEmbeddedFrontend() gin.HandlerFunc {
if strings.HasPrefix(path, "/api/") || if strings.HasPrefix(path, "/api/") ||
strings.HasPrefix(path, "/v1/") || strings.HasPrefix(path, "/v1/") ||
strings.HasPrefix(path, "/v1beta/") || strings.HasPrefix(path, "/v1beta/") ||
strings.HasPrefix(path, "/sora/") ||
strings.HasPrefix(path, "/antigravity/") || strings.HasPrefix(path, "/antigravity/") ||
strings.HasPrefix(path, "/setup/") || strings.HasPrefix(path, "/setup/") ||
path == "/health" || path == "/health" ||
......
...@@ -362,6 +362,7 @@ func TestFrontendServer_Middleware(t *testing.T) { ...@@ -362,6 +362,7 @@ func TestFrontendServer_Middleware(t *testing.T) {
"/api/v1/users", "/api/v1/users",
"/v1/models", "/v1/models",
"/v1beta/chat", "/v1beta/chat",
"/sora/v1/models",
"/antigravity/test", "/antigravity/test",
"/setup/init", "/setup/init",
"/health", "/health",
...@@ -537,6 +538,7 @@ func TestServeEmbeddedFrontend(t *testing.T) { ...@@ -537,6 +538,7 @@ func TestServeEmbeddedFrontend(t *testing.T) {
"/api/users", "/api/users",
"/v1/models", "/v1/models",
"/v1beta/chat", "/v1beta/chat",
"/sora/v1/models",
"/antigravity/test", "/antigravity/test",
"/setup/init", "/setup/init",
"/health", "/health",
......
...@@ -388,7 +388,11 @@ sora: ...@@ -388,7 +388,11 @@ sora:
recent_task_limit_max: 200 recent_task_limit_max: 200
# Enable debug logs for Sora upstream requests # Enable debug logs for Sora upstream requests
# 启用 Sora 直连调试日志 # 启用 Sora 直连调试日志
# 调试日志会输出上游请求尝试、重试、响应摘要;Authorization/openai-sentinel-token 等敏感头会自动脱敏
debug: false debug: false
# Allow Sora client to fetch token via OpenAI token provider
# 是否允许 Sora 客户端通过 OpenAI token provider 取 token(默认 false,避免误走 OpenAI 刷新链路)
use_openai_token_provider: false
# Optional custom headers (key-value) # Optional custom headers (key-value)
# 额外请求头(键值对) # 额外请求头(键值对)
headers: {} headers: {}
...@@ -431,6 +435,13 @@ sora: ...@@ -431,6 +435,13 @@ sora:
# Cron 调度表达式 # Cron 调度表达式
schedule: "0 3 * * *" schedule: "0 3 * * *"
# Token refresh behavior
# token 刷新行为控制
token_refresh:
# Whether OpenAI refresh flow is allowed to sync linked Sora accounts
# 是否允许 OpenAI 刷新流程同步覆盖 linked_openai_account_id 关联的 Sora 账号 token
sync_linked_sora_accounts: false
# ============================================================================= # =============================================================================
# API Key Auth Cache Configuration # API Key Auth Cache Configuration
# API Key 认证缓存配置 # API Key 认证缓存配置
......
...@@ -220,7 +220,7 @@ export async function generateAuthUrl( ...@@ -220,7 +220,7 @@ export async function generateAuthUrl(
*/ */
export async function exchangeCode( export async function exchangeCode(
endpoint: string, endpoint: string,
exchangeData: { session_id: string; code: string; proxy_id?: number } exchangeData: { session_id: string; code: string; state?: string; proxy_id?: number }
): Promise<Record<string, unknown>> { ): Promise<Record<string, unknown>> {
const { data } = await apiClient.post<Record<string, unknown>>(endpoint, exchangeData) const { data } = await apiClient.post<Record<string, unknown>>(endpoint, exchangeData)
return data return data
...@@ -442,7 +442,8 @@ export async function getAntigravityDefaultModelMapping(): Promise<Record<string ...@@ -442,7 +442,8 @@ export async function getAntigravityDefaultModelMapping(): Promise<Record<string
*/ */
export async function refreshOpenAIToken( export async function refreshOpenAIToken(
refreshToken: string, refreshToken: string,
proxyId?: number | null proxyId?: number | null,
endpoint: string = '/admin/openai/refresh-token'
): Promise<Record<string, unknown>> { ): Promise<Record<string, unknown>> {
const payload: { refresh_token: string; proxy_id?: number } = { const payload: { refresh_token: string; proxy_id?: number } = {
refresh_token: refreshToken refresh_token: refreshToken
...@@ -450,7 +451,29 @@ export async function refreshOpenAIToken( ...@@ -450,7 +451,29 @@ export async function refreshOpenAIToken(
if (proxyId) { if (proxyId) {
payload.proxy_id = proxyId payload.proxy_id = proxyId
} }
const { data } = await apiClient.post<Record<string, unknown>>('/admin/openai/refresh-token', payload) const { data } = await apiClient.post<Record<string, unknown>>(endpoint, payload)
return data
}
/**
* Validate Sora session token and exchange to access token
* @param sessionToken - Sora session token
* @param proxyId - Optional proxy ID
* @param endpoint - API endpoint path
* @returns Token information including access_token
*/
export async function validateSoraSessionToken(
sessionToken: string,
proxyId?: number | null,
endpoint: string = '/admin/sora/st2at'
): Promise<Record<string, unknown>> {
const payload: { session_token: string; proxy_id?: number } = {
session_token: sessionToken
}
if (proxyId) {
payload.proxy_id = proxyId
}
const { data } = await apiClient.post<Record<string, unknown>>(endpoint, payload)
return data return data
} }
...@@ -475,6 +498,7 @@ export const accountsAPI = { ...@@ -475,6 +498,7 @@ export const accountsAPI = {
generateAuthUrl, generateAuthUrl,
exchangeCode, exchangeCode,
refreshOpenAIToken, refreshOpenAIToken,
validateSoraSessionToken,
batchCreate, batchCreate,
batchUpdateCredentials, batchUpdateCredentials,
bulkUpdate, bulkUpdate,
......
...@@ -48,6 +48,17 @@ ...@@ -48,6 +48,17 @@
t(getOAuthKey('refreshTokenAuth')) t(getOAuthKey('refreshTokenAuth'))
}}</span> }}</span>
</label> </label>
<label v-if="showSessionTokenOption" class="flex cursor-pointer items-center gap-2">
<input
v-model="inputMethod"
type="radio"
value="session_token"
class="text-blue-600 focus:ring-blue-500"
/>
<span class="text-sm text-blue-900 dark:text-blue-200">{{
t(getOAuthKey('sessionTokenAuth'))
}}</span>
</label>
</div> </div>
</div> </div>
...@@ -135,6 +146,87 @@ ...@@ -135,6 +146,87 @@
</div> </div>
</div> </div>
<!-- Session Token Input (Sora) -->
<div v-if="inputMethod === 'session_token'" class="space-y-4">
<div
class="rounded-lg border border-blue-300 bg-white/80 p-4 dark:border-blue-600 dark:bg-gray-800/80"
>
<p class="mb-3 text-sm text-blue-700 dark:text-blue-300">
{{ t(getOAuthKey('sessionTokenDesc')) }}
</p>
<div class="mb-4">
<label
class="mb-2 flex items-center gap-2 text-sm font-semibold text-gray-700 dark:text-gray-300"
>
<Icon name="key" size="sm" class="text-blue-500" />
Session Token
<span
v-if="parsedSessionTokenCount > 1"
class="rounded-full bg-blue-500 px-2 py-0.5 text-xs text-white"
>
{{ t('admin.accounts.oauth.keysCount', { count: parsedSessionTokenCount }) }}
</span>
</label>
<textarea
v-model="sessionTokenInput"
rows="3"
class="input w-full resize-y font-mono text-sm"
:placeholder="t(getOAuthKey('sessionTokenPlaceholder'))"
></textarea>
<p
v-if="parsedSessionTokenCount > 1"
class="mt-1 text-xs text-blue-600 dark:text-blue-400"
>
{{ t('admin.accounts.oauth.batchCreateAccounts', { count: parsedSessionTokenCount }) }}
</p>
</div>
<div
v-if="error"
class="mb-4 rounded-lg border border-red-200 bg-red-50 p-3 dark:border-red-700 dark:bg-red-900/30"
>
<p class="whitespace-pre-line text-sm text-red-600 dark:text-red-400">
{{ error }}
</p>
</div>
<button
type="button"
class="btn btn-primary w-full"
:disabled="loading || !sessionTokenInput.trim()"
@click="handleValidateSessionToken"
>
<svg
v-if="loading"
class="-ml-1 mr-2 h-4 w-4 animate-spin"
fill="none"
viewBox="0 0 24 24"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
></circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
<Icon v-else name="sparkles" size="sm" class="mr-2" />
{{
loading
? t(getOAuthKey('validating'))
: t(getOAuthKey('validateAndCreate'))
}}
</button>
</div>
</div>
<!-- Cookie Auto-Auth Form --> <!-- Cookie Auto-Auth Form -->
<div v-if="inputMethod === 'cookie'" class="space-y-4"> <div v-if="inputMethod === 'cookie'" class="space-y-4">
<div <div
...@@ -521,13 +613,14 @@ interface Props { ...@@ -521,13 +613,14 @@ interface Props {
error?: string error?: string
showHelp?: boolean showHelp?: boolean
showProxyWarning?: boolean showProxyWarning?: boolean
allowMultiple?: boolean allowMultiple?: boolean
methodLabel?: string methodLabel?: string
showCookieOption?: boolean // Whether to show cookie auto-auth option showCookieOption?: boolean // Whether to show cookie auto-auth option
showRefreshTokenOption?: boolean // Whether to show refresh token input option (OpenAI only) showRefreshTokenOption?: boolean // Whether to show refresh token input option (OpenAI only)
platform?: AccountPlatform // Platform type for different UI/text showSessionTokenOption?: boolean // Whether to show session token input option (Sora only)
showProjectId?: boolean // New prop to control project ID visibility platform?: AccountPlatform // Platform type for different UI/text
} showProjectId?: boolean // New prop to control project ID visibility
}
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
authUrl: '', authUrl: '',
...@@ -540,6 +633,7 @@ const props = withDefaults(defineProps<Props>(), { ...@@ -540,6 +633,7 @@ const props = withDefaults(defineProps<Props>(), {
methodLabel: 'Authorization Method', methodLabel: 'Authorization Method',
showCookieOption: true, showCookieOption: true,
showRefreshTokenOption: false, showRefreshTokenOption: false,
showSessionTokenOption: false,
platform: 'anthropic', platform: 'anthropic',
showProjectId: true showProjectId: true
}) })
...@@ -549,6 +643,7 @@ const emit = defineEmits<{ ...@@ -549,6 +643,7 @@ const emit = defineEmits<{
'exchange-code': [code: string] 'exchange-code': [code: string]
'cookie-auth': [sessionKey: string] 'cookie-auth': [sessionKey: string]
'validate-refresh-token': [refreshToken: string] 'validate-refresh-token': [refreshToken: string]
'validate-session-token': [sessionToken: string]
'update:inputMethod': [method: AuthInputMethod] 'update:inputMethod': [method: AuthInputMethod]
}>() }>()
...@@ -587,12 +682,13 @@ const inputMethod = ref<AuthInputMethod>(props.showCookieOption ? 'manual' : 'ma ...@@ -587,12 +682,13 @@ const inputMethod = ref<AuthInputMethod>(props.showCookieOption ? 'manual' : 'ma
const authCodeInput = ref('') const authCodeInput = ref('')
const sessionKeyInput = ref('') const sessionKeyInput = ref('')
const refreshTokenInput = ref('') const refreshTokenInput = ref('')
const sessionTokenInput = ref('')
const showHelpDialog = ref(false) const showHelpDialog = ref(false)
const oauthState = ref('') const oauthState = ref('')
const projectId = ref('') const projectId = ref('')
// Computed: show method selection when either cookie or refresh token option is enabled // Computed: show method selection when either cookie or refresh token option is enabled
const showMethodSelection = computed(() => props.showCookieOption || props.showRefreshTokenOption) const showMethodSelection = computed(() => props.showCookieOption || props.showRefreshTokenOption || props.showSessionTokenOption)
// Clipboard // Clipboard
const { copied, copyToClipboard } = useClipboard() const { copied, copyToClipboard } = useClipboard()
...@@ -613,6 +709,13 @@ const parsedRefreshTokenCount = computed(() => { ...@@ -613,6 +709,13 @@ const parsedRefreshTokenCount = computed(() => {
.filter((rt) => rt).length .filter((rt) => rt).length
}) })
const parsedSessionTokenCount = computed(() => {
return sessionTokenInput.value
.split('\n')
.map((st) => st.trim())
.filter((st) => st).length
})
// Watchers // Watchers
watch(inputMethod, (newVal) => { watch(inputMethod, (newVal) => {
emit('update:inputMethod', newVal) emit('update:inputMethod', newVal)
...@@ -631,7 +734,7 @@ watch(authCodeInput, (newVal) => { ...@@ -631,7 +734,7 @@ watch(authCodeInput, (newVal) => {
const url = new URL(trimmed) const url = new URL(trimmed)
const code = url.searchParams.get('code') const code = url.searchParams.get('code')
const stateParam = url.searchParams.get('state') const stateParam = url.searchParams.get('state')
if ((props.platform === 'gemini' || props.platform === 'antigravity') && stateParam) { if ((props.platform === 'openai' || props.platform === 'sora' || props.platform === 'gemini' || props.platform === 'antigravity') && stateParam) {
oauthState.value = stateParam oauthState.value = stateParam
} }
if (code && code !== trimmed) { if (code && code !== trimmed) {
...@@ -642,7 +745,7 @@ watch(authCodeInput, (newVal) => { ...@@ -642,7 +745,7 @@ watch(authCodeInput, (newVal) => {
// If URL parsing fails, try regex extraction // If URL parsing fails, try regex extraction
const match = trimmed.match(/[?&]code=([^&]+)/) const match = trimmed.match(/[?&]code=([^&]+)/)
const stateMatch = trimmed.match(/[?&]state=([^&]+)/) const stateMatch = trimmed.match(/[?&]state=([^&]+)/)
if ((props.platform === 'gemini' || props.platform === 'antigravity') && stateMatch && stateMatch[1]) { if ((props.platform === 'openai' || props.platform === 'sora' || props.platform === 'gemini' || props.platform === 'antigravity') && stateMatch && stateMatch[1]) {
oauthState.value = stateMatch[1] oauthState.value = stateMatch[1]
} }
if (match && match[1] && match[1] !== trimmed) { if (match && match[1] && match[1] !== trimmed) {
...@@ -680,6 +783,12 @@ const handleValidateRefreshToken = () => { ...@@ -680,6 +783,12 @@ const handleValidateRefreshToken = () => {
} }
} }
const handleValidateSessionToken = () => {
if (sessionTokenInput.value.trim()) {
emit('validate-session-token', sessionTokenInput.value.trim())
}
}
// Expose methods and state // Expose methods and state
defineExpose({ defineExpose({
authCode: authCodeInput, authCode: authCodeInput,
...@@ -687,6 +796,7 @@ defineExpose({ ...@@ -687,6 +796,7 @@ defineExpose({
projectId, projectId,
sessionKey: sessionKeyInput, sessionKey: sessionKeyInput,
refreshToken: refreshTokenInput, refreshToken: refreshTokenInput,
sessionToken: sessionTokenInput,
inputMethod, inputMethod,
reset: () => { reset: () => {
authCodeInput.value = '' authCodeInput.value = ''
...@@ -694,6 +804,7 @@ defineExpose({ ...@@ -694,6 +804,7 @@ defineExpose({
projectId.value = '' projectId.value = ''
sessionKeyInput.value = '' sessionKeyInput.value = ''
refreshTokenInput.value = '' refreshTokenInput.value = ''
sessionTokenInput.value = ''
inputMethod.value = 'manual' inputMethod.value = 'manual'
showHelpDialog.value = false showHelpDialog.value = false
} }
......
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
<div <div
:class="[ :class="[
'flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-br', 'flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-br',
isOpenAI isOpenAILike
? 'from-green-500 to-green-600' ? 'from-green-500 to-green-600'
: isGemini : isGemini
? 'from-blue-500 to-blue-600' ? 'from-blue-500 to-blue-600'
...@@ -33,6 +33,8 @@ ...@@ -33,6 +33,8 @@
{{ {{
isOpenAI isOpenAI
? t('admin.accounts.openaiAccount') ? t('admin.accounts.openaiAccount')
: isSora
? t('admin.accounts.soraAccount')
: isGemini : isGemini
? t('admin.accounts.geminiAccount') ? t('admin.accounts.geminiAccount')
: isAntigravity : isAntigravity
...@@ -128,7 +130,7 @@ ...@@ -128,7 +130,7 @@
:show-cookie-option="isAnthropic" :show-cookie-option="isAnthropic"
:allow-multiple="false" :allow-multiple="false"
:method-label="t('admin.accounts.inputMethod')" :method-label="t('admin.accounts.inputMethod')"
:platform="isOpenAI ? 'openai' : isGemini ? 'gemini' : isAntigravity ? 'antigravity' : 'anthropic'" :platform="isOpenAI ? 'openai' : isSora ? 'sora' : isGemini ? 'gemini' : isAntigravity ? 'antigravity' : 'anthropic'"
:show-project-id="isGemini && geminiOAuthType === 'code_assist'" :show-project-id="isGemini && geminiOAuthType === 'code_assist'"
@generate-url="handleGenerateUrl" @generate-url="handleGenerateUrl"
@cookie-auth="handleCookieAuth" @cookie-auth="handleCookieAuth"
...@@ -224,7 +226,8 @@ const { t } = useI18n() ...@@ -224,7 +226,8 @@ const { t } = useI18n()
// OAuth composables // OAuth composables
const claudeOAuth = useAccountOAuth() const claudeOAuth = useAccountOAuth()
const openaiOAuth = useOpenAIOAuth() const openaiOAuth = useOpenAIOAuth({ platform: 'openai' })
const soraOAuth = useOpenAIOAuth({ platform: 'sora' })
const geminiOAuth = useGeminiOAuth() const geminiOAuth = useGeminiOAuth()
const antigravityOAuth = useAntigravityOAuth() const antigravityOAuth = useAntigravityOAuth()
...@@ -237,31 +240,34 @@ const geminiOAuthType = ref<'code_assist' | 'google_one' | 'ai_studio'>('code_as ...@@ -237,31 +240,34 @@ const geminiOAuthType = ref<'code_assist' | 'google_one' | 'ai_studio'>('code_as
// Computed - check platform // Computed - check platform
const isOpenAI = computed(() => props.account?.platform === 'openai') const isOpenAI = computed(() => props.account?.platform === 'openai')
const isSora = computed(() => props.account?.platform === 'sora')
const isOpenAILike = computed(() => isOpenAI.value || isSora.value)
const isGemini = computed(() => props.account?.platform === 'gemini') const isGemini = computed(() => props.account?.platform === 'gemini')
const isAnthropic = computed(() => props.account?.platform === 'anthropic') const isAnthropic = computed(() => props.account?.platform === 'anthropic')
const isAntigravity = computed(() => props.account?.platform === 'antigravity') const isAntigravity = computed(() => props.account?.platform === 'antigravity')
const activeOpenAIOAuth = computed(() => (isSora.value ? soraOAuth : openaiOAuth))
// Computed - current OAuth state based on platform // Computed - current OAuth state based on platform
const currentAuthUrl = computed(() => { const currentAuthUrl = computed(() => {
if (isOpenAI.value) return openaiOAuth.authUrl.value if (isOpenAILike.value) return activeOpenAIOAuth.value.authUrl.value
if (isGemini.value) return geminiOAuth.authUrl.value if (isGemini.value) return geminiOAuth.authUrl.value
if (isAntigravity.value) return antigravityOAuth.authUrl.value if (isAntigravity.value) return antigravityOAuth.authUrl.value
return claudeOAuth.authUrl.value return claudeOAuth.authUrl.value
}) })
const currentSessionId = computed(() => { const currentSessionId = computed(() => {
if (isOpenAI.value) return openaiOAuth.sessionId.value if (isOpenAILike.value) return activeOpenAIOAuth.value.sessionId.value
if (isGemini.value) return geminiOAuth.sessionId.value if (isGemini.value) return geminiOAuth.sessionId.value
if (isAntigravity.value) return antigravityOAuth.sessionId.value if (isAntigravity.value) return antigravityOAuth.sessionId.value
return claudeOAuth.sessionId.value return claudeOAuth.sessionId.value
}) })
const currentLoading = computed(() => { const currentLoading = computed(() => {
if (isOpenAI.value) return openaiOAuth.loading.value if (isOpenAILike.value) return activeOpenAIOAuth.value.loading.value
if (isGemini.value) return geminiOAuth.loading.value if (isGemini.value) return geminiOAuth.loading.value
if (isAntigravity.value) return antigravityOAuth.loading.value if (isAntigravity.value) return antigravityOAuth.loading.value
return claudeOAuth.loading.value return claudeOAuth.loading.value
}) })
const currentError = computed(() => { const currentError = computed(() => {
if (isOpenAI.value) return openaiOAuth.error.value if (isOpenAILike.value) return activeOpenAIOAuth.value.error.value
if (isGemini.value) return geminiOAuth.error.value if (isGemini.value) return geminiOAuth.error.value
if (isAntigravity.value) return antigravityOAuth.error.value if (isAntigravity.value) return antigravityOAuth.error.value
return claudeOAuth.error.value return claudeOAuth.error.value
...@@ -269,8 +275,8 @@ const currentError = computed(() => { ...@@ -269,8 +275,8 @@ const currentError = computed(() => {
// Computed // Computed
const isManualInputMethod = computed(() => { const isManualInputMethod = computed(() => {
// OpenAI/Gemini/Antigravity always use manual input (no cookie auth option) // OpenAI/Sora/Gemini/Antigravity always use manual input (no cookie auth option)
return isOpenAI.value || isGemini.value || isAntigravity.value || oauthFlowRef.value?.inputMethod === 'manual' return isOpenAILike.value || isGemini.value || isAntigravity.value || oauthFlowRef.value?.inputMethod === 'manual'
}) })
const canExchangeCode = computed(() => { const canExchangeCode = computed(() => {
...@@ -313,6 +319,7 @@ const resetState = () => { ...@@ -313,6 +319,7 @@ const resetState = () => {
geminiOAuthType.value = 'code_assist' geminiOAuthType.value = 'code_assist'
claudeOAuth.resetState() claudeOAuth.resetState()
openaiOAuth.resetState() openaiOAuth.resetState()
soraOAuth.resetState()
geminiOAuth.resetState() geminiOAuth.resetState()
antigravityOAuth.resetState() antigravityOAuth.resetState()
oauthFlowRef.value?.reset() oauthFlowRef.value?.reset()
...@@ -325,8 +332,8 @@ const handleClose = () => { ...@@ -325,8 +332,8 @@ const handleClose = () => {
const handleGenerateUrl = async () => { const handleGenerateUrl = async () => {
if (!props.account) return if (!props.account) return
if (isOpenAI.value) { if (isOpenAILike.value) {
await openaiOAuth.generateAuthUrl(props.account.proxy_id) await activeOpenAIOAuth.value.generateAuthUrl(props.account.proxy_id)
} else if (isGemini.value) { } else if (isGemini.value) {
const creds = (props.account.credentials || {}) as Record<string, unknown> const creds = (props.account.credentials || {}) as Record<string, unknown>
const tierId = typeof creds.tier_id === 'string' ? creds.tier_id : undefined const tierId = typeof creds.tier_id === 'string' ? creds.tier_id : undefined
...@@ -345,21 +352,29 @@ const handleExchangeCode = async () => { ...@@ -345,21 +352,29 @@ const handleExchangeCode = async () => {
const authCode = oauthFlowRef.value?.authCode || '' const authCode = oauthFlowRef.value?.authCode || ''
if (!authCode.trim()) return if (!authCode.trim()) return
if (isOpenAI.value) { if (isOpenAILike.value) {
// OpenAI OAuth flow // OpenAI OAuth flow
const sessionId = openaiOAuth.sessionId.value const oauthClient = activeOpenAIOAuth.value
const sessionId = oauthClient.sessionId.value
if (!sessionId) return if (!sessionId) return
const stateToUse = (oauthFlowRef.value?.oauthState || oauthClient.oauthState.value || '').trim()
if (!stateToUse) {
oauthClient.error.value = t('admin.accounts.oauth.authFailed')
appStore.showError(oauthClient.error.value)
return
}
const tokenInfo = await openaiOAuth.exchangeAuthCode( const tokenInfo = await oauthClient.exchangeAuthCode(
authCode.trim(), authCode.trim(),
sessionId, sessionId,
stateToUse,
props.account.proxy_id props.account.proxy_id
) )
if (!tokenInfo) return if (!tokenInfo) return
// Build credentials and extra info // Build credentials and extra info
const credentials = openaiOAuth.buildCredentials(tokenInfo) const credentials = oauthClient.buildCredentials(tokenInfo)
const extra = openaiOAuth.buildExtraInfo(tokenInfo) const extra = oauthClient.buildExtraInfo(tokenInfo)
try { try {
// Update account with new credentials // Update account with new credentials
...@@ -376,8 +391,8 @@ const handleExchangeCode = async () => { ...@@ -376,8 +391,8 @@ const handleExchangeCode = async () => {
emit('reauthorized') emit('reauthorized')
handleClose() handleClose()
} catch (error: any) { } catch (error: any) {
openaiOAuth.error.value = error.response?.data?.detail || t('admin.accounts.oauth.authFailed') oauthClient.error.value = error.response?.data?.detail || t('admin.accounts.oauth.authFailed')
appStore.showError(openaiOAuth.error.value) appStore.showError(oauthClient.error.value)
} }
} else if (isGemini.value) { } else if (isGemini.value) {
const sessionId = geminiOAuth.sessionId.value const sessionId = geminiOAuth.sessionId.value
...@@ -490,7 +505,7 @@ const handleExchangeCode = async () => { ...@@ -490,7 +505,7 @@ const handleExchangeCode = async () => {
} }
const handleCookieAuth = async (sessionKey: string) => { const handleCookieAuth = async (sessionKey: string) => {
if (!props.account || isOpenAI.value) return if (!props.account || isOpenAILike.value) return
claudeOAuth.loading.value = true claudeOAuth.loading.value = true
claudeOAuth.error.value = '' claudeOAuth.error.value = ''
......
...@@ -238,6 +238,11 @@ const loadAvailableModels = async () => { ...@@ -238,6 +238,11 @@ 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'))
......
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
<div <div
:class="[ :class="[
'flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-br', 'flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-br',
isOpenAI isOpenAILike
? 'from-green-500 to-green-600' ? 'from-green-500 to-green-600'
: isGemini : isGemini
? 'from-blue-500 to-blue-600' ? 'from-blue-500 to-blue-600'
...@@ -33,6 +33,8 @@ ...@@ -33,6 +33,8 @@
{{ {{
isOpenAI isOpenAI
? t('admin.accounts.openaiAccount') ? t('admin.accounts.openaiAccount')
: isSora
? t('admin.accounts.soraAccount')
: isGemini : isGemini
? t('admin.accounts.geminiAccount') ? t('admin.accounts.geminiAccount')
: isAntigravity : isAntigravity
...@@ -128,7 +130,7 @@ ...@@ -128,7 +130,7 @@
:show-cookie-option="isAnthropic" :show-cookie-option="isAnthropic"
:allow-multiple="false" :allow-multiple="false"
:method-label="t('admin.accounts.inputMethod')" :method-label="t('admin.accounts.inputMethod')"
:platform="isOpenAI ? 'openai' : isGemini ? 'gemini' : isAntigravity ? 'antigravity' : 'anthropic'" :platform="isOpenAI ? 'openai' : isSora ? 'sora' : isGemini ? 'gemini' : isAntigravity ? 'antigravity' : 'anthropic'"
:show-project-id="isGemini && geminiOAuthType === 'code_assist'" :show-project-id="isGemini && geminiOAuthType === 'code_assist'"
@generate-url="handleGenerateUrl" @generate-url="handleGenerateUrl"
@cookie-auth="handleCookieAuth" @cookie-auth="handleCookieAuth"
...@@ -224,7 +226,8 @@ const { t } = useI18n() ...@@ -224,7 +226,8 @@ const { t } = useI18n()
// OAuth composables // OAuth composables
const claudeOAuth = useAccountOAuth() const claudeOAuth = useAccountOAuth()
const openaiOAuth = useOpenAIOAuth() const openaiOAuth = useOpenAIOAuth({ platform: 'openai' })
const soraOAuth = useOpenAIOAuth({ platform: 'sora' })
const geminiOAuth = useGeminiOAuth() const geminiOAuth = useGeminiOAuth()
const antigravityOAuth = useAntigravityOAuth() const antigravityOAuth = useAntigravityOAuth()
...@@ -237,31 +240,34 @@ const geminiOAuthType = ref<'code_assist' | 'google_one' | 'ai_studio'>('code_as ...@@ -237,31 +240,34 @@ const geminiOAuthType = ref<'code_assist' | 'google_one' | 'ai_studio'>('code_as
// Computed - check platform // Computed - check platform
const isOpenAI = computed(() => props.account?.platform === 'openai') const isOpenAI = computed(() => props.account?.platform === 'openai')
const isSora = computed(() => props.account?.platform === 'sora')
const isOpenAILike = computed(() => isOpenAI.value || isSora.value)
const isGemini = computed(() => props.account?.platform === 'gemini') const isGemini = computed(() => props.account?.platform === 'gemini')
const isAnthropic = computed(() => props.account?.platform === 'anthropic') const isAnthropic = computed(() => props.account?.platform === 'anthropic')
const isAntigravity = computed(() => props.account?.platform === 'antigravity') const isAntigravity = computed(() => props.account?.platform === 'antigravity')
const activeOpenAIOAuth = computed(() => (isSora.value ? soraOAuth : openaiOAuth))
// Computed - current OAuth state based on platform // Computed - current OAuth state based on platform
const currentAuthUrl = computed(() => { const currentAuthUrl = computed(() => {
if (isOpenAI.value) return openaiOAuth.authUrl.value if (isOpenAILike.value) return activeOpenAIOAuth.value.authUrl.value
if (isGemini.value) return geminiOAuth.authUrl.value if (isGemini.value) return geminiOAuth.authUrl.value
if (isAntigravity.value) return antigravityOAuth.authUrl.value if (isAntigravity.value) return antigravityOAuth.authUrl.value
return claudeOAuth.authUrl.value return claudeOAuth.authUrl.value
}) })
const currentSessionId = computed(() => { const currentSessionId = computed(() => {
if (isOpenAI.value) return openaiOAuth.sessionId.value if (isOpenAILike.value) return activeOpenAIOAuth.value.sessionId.value
if (isGemini.value) return geminiOAuth.sessionId.value if (isGemini.value) return geminiOAuth.sessionId.value
if (isAntigravity.value) return antigravityOAuth.sessionId.value if (isAntigravity.value) return antigravityOAuth.sessionId.value
return claudeOAuth.sessionId.value return claudeOAuth.sessionId.value
}) })
const currentLoading = computed(() => { const currentLoading = computed(() => {
if (isOpenAI.value) return openaiOAuth.loading.value if (isOpenAILike.value) return activeOpenAIOAuth.value.loading.value
if (isGemini.value) return geminiOAuth.loading.value if (isGemini.value) return geminiOAuth.loading.value
if (isAntigravity.value) return antigravityOAuth.loading.value if (isAntigravity.value) return antigravityOAuth.loading.value
return claudeOAuth.loading.value return claudeOAuth.loading.value
}) })
const currentError = computed(() => { const currentError = computed(() => {
if (isOpenAI.value) return openaiOAuth.error.value if (isOpenAILike.value) return activeOpenAIOAuth.value.error.value
if (isGemini.value) return geminiOAuth.error.value if (isGemini.value) return geminiOAuth.error.value
if (isAntigravity.value) return antigravityOAuth.error.value if (isAntigravity.value) return antigravityOAuth.error.value
return claudeOAuth.error.value return claudeOAuth.error.value
...@@ -269,8 +275,8 @@ const currentError = computed(() => { ...@@ -269,8 +275,8 @@ const currentError = computed(() => {
// Computed // Computed
const isManualInputMethod = computed(() => { const isManualInputMethod = computed(() => {
// OpenAI/Gemini/Antigravity always use manual input (no cookie auth option) // OpenAI/Sora/Gemini/Antigravity always use manual input (no cookie auth option)
return isOpenAI.value || isGemini.value || isAntigravity.value || oauthFlowRef.value?.inputMethod === 'manual' return isOpenAILike.value || isGemini.value || isAntigravity.value || oauthFlowRef.value?.inputMethod === 'manual'
}) })
const canExchangeCode = computed(() => { const canExchangeCode = computed(() => {
...@@ -313,6 +319,7 @@ const resetState = () => { ...@@ -313,6 +319,7 @@ const resetState = () => {
geminiOAuthType.value = 'code_assist' geminiOAuthType.value = 'code_assist'
claudeOAuth.resetState() claudeOAuth.resetState()
openaiOAuth.resetState() openaiOAuth.resetState()
soraOAuth.resetState()
geminiOAuth.resetState() geminiOAuth.resetState()
antigravityOAuth.resetState() antigravityOAuth.resetState()
oauthFlowRef.value?.reset() oauthFlowRef.value?.reset()
...@@ -325,8 +332,8 @@ const handleClose = () => { ...@@ -325,8 +332,8 @@ const handleClose = () => {
const handleGenerateUrl = async () => { const handleGenerateUrl = async () => {
if (!props.account) return if (!props.account) return
if (isOpenAI.value) { if (isOpenAILike.value) {
await openaiOAuth.generateAuthUrl(props.account.proxy_id) await activeOpenAIOAuth.value.generateAuthUrl(props.account.proxy_id)
} else if (isGemini.value) { } else if (isGemini.value) {
const creds = (props.account.credentials || {}) as Record<string, unknown> const creds = (props.account.credentials || {}) as Record<string, unknown>
const tierId = typeof creds.tier_id === 'string' ? creds.tier_id : undefined const tierId = typeof creds.tier_id === 'string' ? creds.tier_id : undefined
...@@ -345,21 +352,29 @@ const handleExchangeCode = async () => { ...@@ -345,21 +352,29 @@ const handleExchangeCode = async () => {
const authCode = oauthFlowRef.value?.authCode || '' const authCode = oauthFlowRef.value?.authCode || ''
if (!authCode.trim()) return if (!authCode.trim()) return
if (isOpenAI.value) { if (isOpenAILike.value) {
// OpenAI OAuth flow // OpenAI OAuth flow
const sessionId = openaiOAuth.sessionId.value const oauthClient = activeOpenAIOAuth.value
const sessionId = oauthClient.sessionId.value
if (!sessionId) return if (!sessionId) return
const stateToUse = (oauthFlowRef.value?.oauthState || oauthClient.oauthState.value || '').trim()
if (!stateToUse) {
oauthClient.error.value = t('admin.accounts.oauth.authFailed')
appStore.showError(oauthClient.error.value)
return
}
const tokenInfo = await openaiOAuth.exchangeAuthCode( const tokenInfo = await oauthClient.exchangeAuthCode(
authCode.trim(), authCode.trim(),
sessionId, sessionId,
stateToUse,
props.account.proxy_id props.account.proxy_id
) )
if (!tokenInfo) return if (!tokenInfo) return
// Build credentials and extra info // Build credentials and extra info
const credentials = openaiOAuth.buildCredentials(tokenInfo) const credentials = oauthClient.buildCredentials(tokenInfo)
const extra = openaiOAuth.buildExtraInfo(tokenInfo) const extra = oauthClient.buildExtraInfo(tokenInfo)
try { try {
// Update account with new credentials // Update account with new credentials
...@@ -376,8 +391,8 @@ const handleExchangeCode = async () => { ...@@ -376,8 +391,8 @@ const handleExchangeCode = async () => {
emit('reauthorized', updatedAccount) emit('reauthorized', updatedAccount)
handleClose() handleClose()
} catch (error: any) { } catch (error: any) {
openaiOAuth.error.value = error.response?.data?.detail || t('admin.accounts.oauth.authFailed') oauthClient.error.value = error.response?.data?.detail || t('admin.accounts.oauth.authFailed')
appStore.showError(openaiOAuth.error.value) appStore.showError(oauthClient.error.value)
} }
} else if (isGemini.value) { } else if (isGemini.value) {
const sessionId = geminiOAuth.sessionId.value const sessionId = geminiOAuth.sessionId.value
...@@ -490,7 +505,7 @@ const handleExchangeCode = async () => { ...@@ -490,7 +505,7 @@ const handleExchangeCode = async () => {
} }
const handleCookieAuth = async (sessionKey: string) => { const handleCookieAuth = async (sessionKey: string) => {
if (!props.account || isOpenAI.value) return if (!props.account || isOpenAILike.value) return
claudeOAuth.loading.value = true claudeOAuth.loading.value = true
claudeOAuth.error.value = '' claudeOAuth.error.value = ''
......
...@@ -3,7 +3,7 @@ import { useAppStore } from '@/stores/app' ...@@ -3,7 +3,7 @@ import { useAppStore } from '@/stores/app'
import { adminAPI } from '@/api/admin' import { adminAPI } from '@/api/admin'
export type AddMethod = 'oauth' | 'setup-token' export type AddMethod = 'oauth' | 'setup-token'
export type AuthInputMethod = 'manual' | 'cookie' | 'refresh_token' export type AuthInputMethod = 'manual' | 'cookie' | 'refresh_token' | 'session_token'
export interface OAuthState { export interface OAuthState {
authUrl: string authUrl: string
......
...@@ -19,12 +19,21 @@ export interface OpenAITokenInfo { ...@@ -19,12 +19,21 @@ export interface OpenAITokenInfo {
[key: string]: unknown [key: string]: unknown
} }
export function useOpenAIOAuth() { export type OpenAIOAuthPlatform = 'openai' | 'sora'
interface UseOpenAIOAuthOptions {
platform?: OpenAIOAuthPlatform
}
export function useOpenAIOAuth(options?: UseOpenAIOAuthOptions) {
const appStore = useAppStore() const appStore = useAppStore()
const oauthPlatform = options?.platform ?? 'openai'
const endpointPrefix = oauthPlatform === 'sora' ? '/admin/sora' : '/admin/openai'
// State // State
const authUrl = ref('') const authUrl = ref('')
const sessionId = ref('') const sessionId = ref('')
const oauthState = ref('')
const loading = ref(false) const loading = ref(false)
const error = ref('') const error = ref('')
...@@ -32,6 +41,7 @@ export function useOpenAIOAuth() { ...@@ -32,6 +41,7 @@ export function useOpenAIOAuth() {
const resetState = () => { const resetState = () => {
authUrl.value = '' authUrl.value = ''
sessionId.value = '' sessionId.value = ''
oauthState.value = ''
loading.value = false loading.value = false
error.value = '' error.value = ''
} }
...@@ -44,6 +54,7 @@ export function useOpenAIOAuth() { ...@@ -44,6 +54,7 @@ export function useOpenAIOAuth() {
loading.value = true loading.value = true
authUrl.value = '' authUrl.value = ''
sessionId.value = '' sessionId.value = ''
oauthState.value = ''
error.value = '' error.value = ''
try { try {
...@@ -56,11 +67,17 @@ export function useOpenAIOAuth() { ...@@ -56,11 +67,17 @@ export function useOpenAIOAuth() {
} }
const response = await adminAPI.accounts.generateAuthUrl( const response = await adminAPI.accounts.generateAuthUrl(
'/admin/openai/generate-auth-url', `${endpointPrefix}/generate-auth-url`,
payload payload
) )
authUrl.value = response.auth_url authUrl.value = response.auth_url
sessionId.value = response.session_id sessionId.value = response.session_id
try {
const parsed = new URL(response.auth_url)
oauthState.value = parsed.searchParams.get('state') || ''
} catch {
oauthState.value = ''
}
return true return true
} catch (err: any) { } catch (err: any) {
error.value = err.response?.data?.detail || 'Failed to generate OpenAI auth URL' error.value = err.response?.data?.detail || 'Failed to generate OpenAI auth URL'
...@@ -75,10 +92,11 @@ export function useOpenAIOAuth() { ...@@ -75,10 +92,11 @@ export function useOpenAIOAuth() {
const exchangeAuthCode = async ( const exchangeAuthCode = async (
code: string, code: string,
currentSessionId: string, currentSessionId: string,
state: string,
proxyId?: number | null proxyId?: number | null
): Promise<OpenAITokenInfo | null> => { ): Promise<OpenAITokenInfo | null> => {
if (!code.trim() || !currentSessionId) { if (!code.trim() || !currentSessionId || !state.trim()) {
error.value = 'Missing auth code or session ID' error.value = 'Missing auth code, session ID, or state'
return null return null
} }
...@@ -86,15 +104,16 @@ export function useOpenAIOAuth() { ...@@ -86,15 +104,16 @@ export function useOpenAIOAuth() {
error.value = '' error.value = ''
try { try {
const payload: { session_id: string; code: string; proxy_id?: number } = { const payload: { session_id: string; code: string; state: string; proxy_id?: number } = {
session_id: currentSessionId, session_id: currentSessionId,
code: code.trim() code: code.trim(),
state: state.trim()
} }
if (proxyId) { if (proxyId) {
payload.proxy_id = proxyId payload.proxy_id = proxyId
} }
const tokenInfo = await adminAPI.accounts.exchangeCode('/admin/openai/exchange-code', payload) const tokenInfo = await adminAPI.accounts.exchangeCode(`${endpointPrefix}/exchange-code`, payload)
return tokenInfo as OpenAITokenInfo return tokenInfo as OpenAITokenInfo
} catch (err: any) { } catch (err: any) {
error.value = err.response?.data?.detail || 'Failed to exchange OpenAI auth code' error.value = err.response?.data?.detail || 'Failed to exchange OpenAI auth code'
...@@ -120,7 +139,11 @@ export function useOpenAIOAuth() { ...@@ -120,7 +139,11 @@ export function useOpenAIOAuth() {
try { try {
// Use dedicated refresh-token endpoint // Use dedicated refresh-token endpoint
const tokenInfo = await adminAPI.accounts.refreshOpenAIToken(refreshToken.trim(), proxyId) const tokenInfo = await adminAPI.accounts.refreshOpenAIToken(
refreshToken.trim(),
proxyId,
`${endpointPrefix}/refresh-token`
)
return tokenInfo as OpenAITokenInfo return tokenInfo as OpenAITokenInfo
} catch (err: any) { } catch (err: any) {
error.value = err.response?.data?.detail || 'Failed to validate refresh token' error.value = err.response?.data?.detail || 'Failed to validate refresh token'
...@@ -131,6 +154,33 @@ export function useOpenAIOAuth() { ...@@ -131,6 +154,33 @@ export function useOpenAIOAuth() {
} }
} }
// Validate Sora session token and get access token
const validateSessionToken = async (
sessionToken: string,
proxyId?: number | null
): Promise<OpenAITokenInfo | null> => {
if (!sessionToken.trim()) {
error.value = 'Missing session token'
return null
}
loading.value = true
error.value = ''
try {
const tokenInfo = await adminAPI.accounts.validateSoraSessionToken(
sessionToken.trim(),
proxyId,
`${endpointPrefix}/st2at`
)
return tokenInfo as OpenAITokenInfo
} catch (err: any) {
error.value = err.response?.data?.detail || 'Failed to validate session token'
appStore.showError(error.value)
return null
} finally {
loading.value = false
}
}
// Build credentials for OpenAI OAuth account // Build credentials for OpenAI OAuth account
const buildCredentials = (tokenInfo: OpenAITokenInfo): Record<string, unknown> => { const buildCredentials = (tokenInfo: OpenAITokenInfo): Record<string, unknown> => {
const creds: Record<string, unknown> = { const creds: Record<string, unknown> = {
...@@ -172,6 +222,7 @@ export function useOpenAIOAuth() { ...@@ -172,6 +222,7 @@ export function useOpenAIOAuth() {
// State // State
authUrl, authUrl,
sessionId, sessionId,
oauthState,
loading, loading,
error, error,
// Methods // Methods
...@@ -179,6 +230,7 @@ export function useOpenAIOAuth() { ...@@ -179,6 +230,7 @@ export function useOpenAIOAuth() {
generateAuthUrl, generateAuthUrl,
exchangeAuthCode, exchangeAuthCode,
validateRefreshToken, validateRefreshToken,
validateSessionToken,
buildCredentials, buildCredentials,
buildExtraInfo buildExtraInfo
} }
......
...@@ -1740,9 +1740,13 @@ export default { ...@@ -1740,9 +1740,13 @@ export default {
refreshTokenAuth: 'Manual RT Input', refreshTokenAuth: 'Manual RT Input',
refreshTokenDesc: 'Enter your existing OpenAI Refresh Token(s). Supports batch input (one per line). The system will automatically validate and create accounts.', refreshTokenDesc: 'Enter your existing OpenAI Refresh Token(s). Supports batch input (one per line). The system will automatically validate and create accounts.',
refreshTokenPlaceholder: 'Paste your OpenAI Refresh Token...\nSupports multiple, one per line', refreshTokenPlaceholder: 'Paste your OpenAI Refresh Token...\nSupports multiple, one per line',
sessionTokenAuth: 'Manual ST Input',
sessionTokenDesc: 'Enter your existing Sora Session Token(s). Supports batch input (one per line). The system will automatically validate and create accounts.',
sessionTokenPlaceholder: 'Paste your Sora Session Token...\nSupports multiple, one per line',
validating: 'Validating...', validating: 'Validating...',
validateAndCreate: 'Validate & Create Account', validateAndCreate: 'Validate & Create Account',
pleaseEnterRefreshToken: 'Please enter Refresh Token' pleaseEnterRefreshToken: 'Please enter Refresh Token',
pleaseEnterSessionToken: 'Please enter Session Token'
}, },
// Gemini specific // Gemini specific
gemini: { gemini: {
...@@ -1963,6 +1967,7 @@ export default { ...@@ -1963,6 +1967,7 @@ export default {
reAuthorizeAccount: 'Re-Authorize Account', reAuthorizeAccount: 'Re-Authorize Account',
claudeCodeAccount: 'Claude Code Account', claudeCodeAccount: 'Claude Code Account',
openaiAccount: 'OpenAI Account', openaiAccount: 'OpenAI Account',
soraAccount: 'Sora Account',
geminiAccount: 'Gemini Account', geminiAccount: 'Gemini Account',
antigravityAccount: 'Antigravity Account', antigravityAccount: 'Antigravity Account',
inputMethod: 'Input Method', inputMethod: 'Input Method',
......
...@@ -1879,9 +1879,13 @@ export default { ...@@ -1879,9 +1879,13 @@ export default {
refreshTokenAuth: '手动输入 RT', refreshTokenAuth: '手动输入 RT',
refreshTokenDesc: '输入您已有的 OpenAI Refresh Token,支持批量输入(每行一个),系统将自动验证并创建账号。', refreshTokenDesc: '输入您已有的 OpenAI Refresh Token,支持批量输入(每行一个),系统将自动验证并创建账号。',
refreshTokenPlaceholder: '粘贴您的 OpenAI Refresh Token...\n支持多个,每行一个', refreshTokenPlaceholder: '粘贴您的 OpenAI Refresh Token...\n支持多个,每行一个',
sessionTokenAuth: '手动输入 ST',
sessionTokenDesc: '输入您已有的 Sora Session Token,支持批量输入(每行一个),系统将自动验证并创建账号。',
sessionTokenPlaceholder: '粘贴您的 Sora Session Token...\n支持多个,每行一个',
validating: '验证中...', validating: '验证中...',
validateAndCreate: '验证并创建账号', validateAndCreate: '验证并创建账号',
pleaseEnterRefreshToken: '请输入 Refresh Token' pleaseEnterRefreshToken: '请输入 Refresh Token',
pleaseEnterSessionToken: '请输入 Session Token'
}, },
// Gemini specific // Gemini specific
gemini: { gemini: {
...@@ -2097,6 +2101,7 @@ export default { ...@@ -2097,6 +2101,7 @@ export default {
reAuthorizeAccount: '重新授权账号', reAuthorizeAccount: '重新授权账号',
claudeCodeAccount: 'Claude Code 账号', claudeCodeAccount: 'Claude Code 账号',
openaiAccount: 'OpenAI 账号', openaiAccount: 'OpenAI 账号',
soraAccount: 'Sora 账号',
geminiAccount: 'Gemini 账号', geminiAccount: 'Gemini 账号',
antigravityAccount: 'Antigravity 账号', antigravityAccount: 'Antigravity 账号',
inputMethod: '输入方式', inputMethod: '输入方式',
......
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