Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
25d961d4
Unverified
Commit
25d961d4
authored
Mar 24, 2026
by
Wesley Liddick
Committed by
GitHub
Mar 24, 2026
Browse files
Merge pull request #1252 from DaydreamCoding/feat/openai-mobile-rt
feat(openai): 支持 Mobile Refresh Token 导入,自动补全 plan_type
parents
995bee14
91b1d812
Changes
9
Hide whitespace changes
Inline
Side-by-side
backend/cmd/server/wire_gen.go
View file @
25d961d4
...
@@ -114,6 +114,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
...
@@ -114,6 +114,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
oAuthService
:=
service
.
NewOAuthService
(
proxyRepository
,
claudeOAuthClient
)
oAuthService
:=
service
.
NewOAuthService
(
proxyRepository
,
claudeOAuthClient
)
openAIOAuthClient
:=
repository
.
NewOpenAIOAuthClient
()
openAIOAuthClient
:=
repository
.
NewOpenAIOAuthClient
()
openAIOAuthService
:=
service
.
NewOpenAIOAuthService
(
proxyRepository
,
openAIOAuthClient
)
openAIOAuthService
:=
service
.
NewOpenAIOAuthService
(
proxyRepository
,
openAIOAuthClient
)
openAIOAuthService
.
SetPrivacyClientFactory
(
privacyClientFactory
)
geminiOAuthClient
:=
repository
.
NewGeminiOAuthClient
(
configConfig
)
geminiOAuthClient
:=
repository
.
NewGeminiOAuthClient
(
configConfig
)
geminiCliCodeAssistClient
:=
repository
.
NewGeminiCliCodeAssistClient
()
geminiCliCodeAssistClient
:=
repository
.
NewGeminiCliCodeAssistClient
()
driveClient
:=
repository
.
NewGeminiDriveClient
()
driveClient
:=
repository
.
NewGeminiDriveClient
()
...
...
backend/internal/pkg/openai/oauth.go
View file @
25d961d4
...
@@ -270,6 +270,7 @@ type OpenAIAuthClaims struct {
...
@@ -270,6 +270,7 @@ type OpenAIAuthClaims struct {
ChatGPTUserID
string
`json:"chatgpt_user_id"`
ChatGPTUserID
string
`json:"chatgpt_user_id"`
ChatGPTPlanType
string
`json:"chatgpt_plan_type"`
ChatGPTPlanType
string
`json:"chatgpt_plan_type"`
UserID
string
`json:"user_id"`
UserID
string
`json:"user_id"`
POID
string
`json:"poid"`
// organization ID in access_token JWT
Organizations
[]
OrganizationClaim
`json:"organizations"`
Organizations
[]
OrganizationClaim
`json:"organizations"`
}
}
...
...
backend/internal/service/openai_oauth_service.go
View file @
25d961d4
...
@@ -29,9 +29,10 @@ type soraSessionChunk struct {
...
@@ -29,9 +29,10 @@ type soraSessionChunk struct {
// OpenAIOAuthService handles OpenAI OAuth authentication flows
// OpenAIOAuthService handles OpenAI OAuth authentication flows
type
OpenAIOAuthService
struct
{
type
OpenAIOAuthService
struct
{
sessionStore
*
openai
.
SessionStore
sessionStore
*
openai
.
SessionStore
proxyRepo
ProxyRepository
proxyRepo
ProxyRepository
oauthClient
OpenAIOAuthClient
oauthClient
OpenAIOAuthClient
privacyClientFactory
PrivacyClientFactory
// 用于调用 chatgpt.com/backend-api(ImpersonateChrome)
}
}
// NewOpenAIOAuthService creates a new OpenAI OAuth service
// NewOpenAIOAuthService creates a new OpenAI OAuth service
...
@@ -43,6 +44,12 @@ func NewOpenAIOAuthService(proxyRepo ProxyRepository, oauthClient OpenAIOAuthCli
...
@@ -43,6 +44,12 @@ func NewOpenAIOAuthService(proxyRepo ProxyRepository, oauthClient OpenAIOAuthCli
}
}
}
}
// SetPrivacyClientFactory 注入 ImpersonateChrome 客户端工厂,
// 用于调用 chatgpt.com/backend-api 获取账号信息(plan_type 等)。
func
(
s
*
OpenAIOAuthService
)
SetPrivacyClientFactory
(
factory
PrivacyClientFactory
)
{
s
.
privacyClientFactory
=
factory
}
// OpenAIAuthURLResult contains the authorization URL and session info
// OpenAIAuthURLResult contains the authorization URL and session info
type
OpenAIAuthURLResult
struct
{
type
OpenAIAuthURLResult
struct
{
AuthURL
string
`json:"auth_url"`
AuthURL
string
`json:"auth_url"`
...
@@ -131,6 +138,7 @@ type OpenAITokenInfo struct {
...
@@ -131,6 +138,7 @@ type OpenAITokenInfo struct {
ChatGPTUserID
string
`json:"chatgpt_user_id,omitempty"`
ChatGPTUserID
string
`json:"chatgpt_user_id,omitempty"`
OrganizationID
string
`json:"organization_id,omitempty"`
OrganizationID
string
`json:"organization_id,omitempty"`
PlanType
string
`json:"plan_type,omitempty"`
PlanType
string
`json:"plan_type,omitempty"`
PrivacyMode
string
`json:"privacy_mode,omitempty"`
}
}
// ExchangeCode exchanges authorization code for tokens
// ExchangeCode exchanges authorization code for tokens
...
@@ -251,6 +259,30 @@ func (s *OpenAIOAuthService) RefreshTokenWithClientID(ctx context.Context, refre
...
@@ -251,6 +259,30 @@ func (s *OpenAIOAuthService) RefreshTokenWithClientID(ctx context.Context, refre
tokenInfo
.
PlanType
=
userInfo
.
PlanType
tokenInfo
.
PlanType
=
userInfo
.
PlanType
}
}
// id_token 中缺少 plan_type 时(如 Mobile RT),尝试通过 ChatGPT backend-api 补全
if
tokenInfo
.
PlanType
==
""
&&
tokenInfo
.
AccessToken
!=
""
&&
s
.
privacyClientFactory
!=
nil
{
// 从 access_token JWT 中提取 orgID(poid),用于匹配正确的账号
orgID
:=
tokenInfo
.
OrganizationID
if
orgID
==
""
{
if
atClaims
,
err
:=
openai
.
DecodeIDToken
(
tokenInfo
.
AccessToken
);
err
==
nil
&&
atClaims
.
OpenAIAuth
!=
nil
{
orgID
=
atClaims
.
OpenAIAuth
.
POID
}
}
if
info
:=
fetchChatGPTAccountInfo
(
ctx
,
s
.
privacyClientFactory
,
tokenInfo
.
AccessToken
,
proxyURL
,
orgID
);
info
!=
nil
{
if
tokenInfo
.
PlanType
==
""
&&
info
.
PlanType
!=
""
{
tokenInfo
.
PlanType
=
info
.
PlanType
}
if
tokenInfo
.
Email
==
""
&&
info
.
Email
!=
""
{
tokenInfo
.
Email
=
info
.
Email
}
}
}
// 尝试设置隐私(关闭训练数据共享),best-effort
if
tokenInfo
.
AccessToken
!=
""
&&
s
.
privacyClientFactory
!=
nil
{
tokenInfo
.
PrivacyMode
=
disableOpenAITraining
(
ctx
,
s
.
privacyClientFactory
,
tokenInfo
.
AccessToken
,
proxyURL
)
}
return
tokenInfo
,
nil
return
tokenInfo
,
nil
}
}
...
...
backend/internal/service/openai_privacy_service.go
View file @
25d961d4
...
@@ -69,6 +69,139 @@ func disableOpenAITraining(ctx context.Context, clientFactory PrivacyClientFacto
...
@@ -69,6 +69,139 @@ func disableOpenAITraining(ctx context.Context, clientFactory PrivacyClientFacto
return
PrivacyModeTrainingOff
return
PrivacyModeTrainingOff
}
}
// ChatGPTAccountInfo 从 chatgpt.com/backend-api/accounts/check 获取的账号信息
type
ChatGPTAccountInfo
struct
{
PlanType
string
Email
string
}
const
chatGPTAccountsCheckURL
=
"https://chatgpt.com/backend-api/accounts/check/v4-2023-04-27"
// fetchChatGPTAccountInfo calls ChatGPT backend-api to get account info (plan_type, etc.).
// Used as fallback when id_token doesn't contain these fields (e.g., Mobile RT).
// orgID is used to match the correct account when multiple accounts exist (e.g., personal + team).
// Returns nil on any failure (best-effort, non-blocking).
func
fetchChatGPTAccountInfo
(
ctx
context
.
Context
,
clientFactory
PrivacyClientFactory
,
accessToken
,
proxyURL
,
orgID
string
)
*
ChatGPTAccountInfo
{
if
accessToken
==
""
||
clientFactory
==
nil
{
return
nil
}
ctx
,
cancel
:=
context
.
WithTimeout
(
ctx
,
15
*
time
.
Second
)
defer
cancel
()
client
,
err
:=
clientFactory
(
proxyURL
)
if
err
!=
nil
{
slog
.
Debug
(
"chatgpt_account_check_client_error"
,
"error"
,
err
.
Error
())
return
nil
}
var
result
map
[
string
]
any
resp
,
err
:=
client
.
R
()
.
SetContext
(
ctx
)
.
SetHeader
(
"Authorization"
,
"Bearer "
+
accessToken
)
.
SetHeader
(
"Origin"
,
"https://chatgpt.com"
)
.
SetHeader
(
"Referer"
,
"https://chatgpt.com/"
)
.
SetHeader
(
"Accept"
,
"application/json"
)
.
SetSuccessResult
(
&
result
)
.
Get
(
chatGPTAccountsCheckURL
)
if
err
!=
nil
{
slog
.
Debug
(
"chatgpt_account_check_request_error"
,
"error"
,
err
.
Error
())
return
nil
}
if
!
resp
.
IsSuccessState
()
{
slog
.
Debug
(
"chatgpt_account_check_failed"
,
"status"
,
resp
.
StatusCode
,
"body"
,
truncate
(
resp
.
String
(),
200
))
return
nil
}
info
:=
&
ChatGPTAccountInfo
{}
accounts
,
ok
:=
result
[
"accounts"
]
.
(
map
[
string
]
any
)
if
!
ok
{
slog
.
Debug
(
"chatgpt_account_check_no_accounts"
,
"body"
,
truncate
(
resp
.
String
(),
300
))
return
nil
}
// 优先匹配 orgID 对应的账号(access_token JWT 中的 poid)
if
orgID
!=
""
{
if
matched
:=
extractPlanFromAccount
(
accounts
,
orgID
);
matched
!=
""
{
info
.
PlanType
=
matched
}
}
// 未匹配到时,遍历所有账号:优先 is_default,次选非 free
if
info
.
PlanType
==
""
{
var
defaultPlan
,
paidPlan
,
anyPlan
string
for
_
,
acctRaw
:=
range
accounts
{
acct
,
ok
:=
acctRaw
.
(
map
[
string
]
any
)
if
!
ok
{
continue
}
planType
:=
extractPlanType
(
acct
)
if
planType
==
""
{
continue
}
if
anyPlan
==
""
{
anyPlan
=
planType
}
if
account
,
ok
:=
acct
[
"account"
]
.
(
map
[
string
]
any
);
ok
{
if
isDefault
,
_
:=
account
[
"is_default"
]
.
(
bool
);
isDefault
{
defaultPlan
=
planType
}
}
if
!
strings
.
EqualFold
(
planType
,
"free"
)
&&
paidPlan
==
""
{
paidPlan
=
planType
}
}
// 优先级:default > 非 free > 任意
switch
{
case
defaultPlan
!=
""
:
info
.
PlanType
=
defaultPlan
case
paidPlan
!=
""
:
info
.
PlanType
=
paidPlan
default
:
info
.
PlanType
=
anyPlan
}
}
if
info
.
PlanType
==
""
{
slog
.
Debug
(
"chatgpt_account_check_no_plan_type"
,
"body"
,
truncate
(
resp
.
String
(),
300
))
return
nil
}
slog
.
Info
(
"chatgpt_account_check_success"
,
"plan_type"
,
info
.
PlanType
,
"org_id"
,
orgID
)
return
info
}
// extractPlanFromAccount 从 accounts map 中按 key(account_id)精确匹配并提取 plan_type
func
extractPlanFromAccount
(
accounts
map
[
string
]
any
,
accountKey
string
)
string
{
acctRaw
,
ok
:=
accounts
[
accountKey
]
if
!
ok
{
return
""
}
acct
,
ok
:=
acctRaw
.
(
map
[
string
]
any
)
if
!
ok
{
return
""
}
return
extractPlanType
(
acct
)
}
// extractPlanType 从单个 account 对象中提取 plan_type
func
extractPlanType
(
acct
map
[
string
]
any
)
string
{
if
account
,
ok
:=
acct
[
"account"
]
.
(
map
[
string
]
any
);
ok
{
if
planType
,
ok
:=
account
[
"plan_type"
]
.
(
string
);
ok
&&
planType
!=
""
{
return
planType
}
}
if
entitlement
,
ok
:=
acct
[
"entitlement"
]
.
(
map
[
string
]
any
);
ok
{
if
subPlan
,
ok
:=
entitlement
[
"subscription_plan"
]
.
(
string
);
ok
&&
subPlan
!=
""
{
return
subPlan
}
}
return
""
}
func
truncate
(
s
string
,
n
int
)
string
{
func
truncate
(
s
string
,
n
int
)
string
{
if
len
(
s
)
<=
n
{
if
len
(
s
)
<=
n
{
return
s
return
s
...
...
frontend/src/api/admin/accounts.ts
View file @
25d961d4
...
@@ -550,14 +550,18 @@ export async function getAntigravityDefaultModelMapping(): Promise<Record<string
...
@@ -550,14 +550,18 @@ 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
'
endpoint
:
string
=
'
/admin/openai/refresh-token
'
,
clientId
?:
string
):
Promise
<
Record
<
string
,
unknown
>>
{
):
Promise
<
Record
<
string
,
unknown
>>
{
const
payload
:
{
refresh_token
:
string
;
proxy_id
?:
number
}
=
{
const
payload
:
{
refresh_token
:
string
;
proxy_id
?:
number
;
client_id
?:
string
}
=
{
refresh_token
:
refreshToken
refresh_token
:
refreshToken
}
}
if
(
proxyId
)
{
if
(
proxyId
)
{
payload
.
proxy_id
=
proxyId
payload
.
proxy_id
=
proxyId
}
}
if
(
clientId
)
{
payload
.
client_id
=
clientId
}
const
{
data
}
=
await
apiClient
.
post
<
Record
<
string
,
unknown
>>
(
endpoint
,
payload
)
const
{
data
}
=
await
apiClient
.
post
<
Record
<
string
,
unknown
>>
(
endpoint
,
payload
)
return
data
return
data
}
}
...
...
frontend/src/components/account/CreateAccountModal.vue
View file @
25d961d4
...
@@ -2504,6 +2504,7 @@
...
@@ -2504,6 +2504,7 @@
:
allow
-
multiple
=
"
form.platform === 'anthropic'
"
:
allow
-
multiple
=
"
form.platform === 'anthropic'
"
:
show
-
cookie
-
option
=
"
form.platform === 'anthropic'
"
:
show
-
cookie
-
option
=
"
form.platform === 'anthropic'
"
:
show
-
refresh
-
token
-
option
=
"
form.platform === 'openai' || form.platform === 'sora' || form.platform === 'antigravity'
"
:
show
-
refresh
-
token
-
option
=
"
form.platform === 'openai' || form.platform === 'sora' || form.platform === 'antigravity'
"
:
show
-
mobile
-
refresh
-
token
-
option
=
"
form.platform === 'openai'
"
:
show
-
session
-
token
-
option
=
"
form.platform === 'sora'
"
:
show
-
session
-
token
-
option
=
"
form.platform === 'sora'
"
:
show
-
access
-
token
-
option
=
"
form.platform === 'sora'
"
:
show
-
access
-
token
-
option
=
"
form.platform === 'sora'
"
:
platform
=
"
form.platform
"
:
platform
=
"
form.platform
"
...
@@ -2511,6 +2512,7 @@
...
@@ -2511,6 +2512,7 @@
@
generate
-
url
=
"
handleGenerateUrl
"
@
generate
-
url
=
"
handleGenerateUrl
"
@
cookie
-
auth
=
"
handleCookieAuth
"
@
cookie
-
auth
=
"
handleCookieAuth
"
@
validate
-
refresh
-
token
=
"
handleValidateRefreshToken
"
@
validate
-
refresh
-
token
=
"
handleValidateRefreshToken
"
@
validate
-
mobile
-
refresh
-
token
=
"
handleOpenAIValidateMobileRT
"
@
validate
-
session
-
token
=
"
handleValidateSessionToken
"
@
validate
-
session
-
token
=
"
handleValidateSessionToken
"
@
import
-
access
-
token
=
"
handleImportAccessToken
"
@
import
-
access
-
token
=
"
handleImportAccessToken
"
/>
/>
...
@@ -4360,11 +4362,14 @@ const handleOpenAIExchange = async (authCode: string) => {
...
@@ -4360,11 +4362,14 @@ const handleOpenAIExchange = async (authCode: string) => {
}
}
// OpenAI 手动 RT 批量验证和创建
// OpenAI 手动 RT 批量验证和创建
const
handleOpenAIValidateRT
=
async
(
refreshTokenInput
:
string
)
=>
{
// OpenAI Mobile RT 使用的 client_id(与后端 openai.SoraClientID 一致)
const
OPENAI_MOBILE_RT_CLIENT_ID
=
'
app_LlGpXReQgckcGGUo2JrYvtJK
'
// OpenAI/Sora RT 批量验证和创建(共享逻辑)
const
handleOpenAIBatchRT
=
async
(
refreshTokenInput
:
string
,
clientId
?:
string
)
=>
{
const
oauthClient
=
activeOpenAIOAuth
.
value
const
oauthClient
=
activeOpenAIOAuth
.
value
if
(
!
refreshTokenInput
.
trim
())
return
if
(
!
refreshTokenInput
.
trim
())
return
// Parse multiple refresh tokens (one per line)
const
refreshTokens
=
refreshTokenInput
const
refreshTokens
=
refreshTokenInput
.
split
(
'
\n
'
)
.
split
(
'
\n
'
)
.
map
((
rt
)
=>
rt
.
trim
())
.
map
((
rt
)
=>
rt
.
trim
())
...
@@ -4389,7 +4394,8 @@ const handleOpenAIValidateRT = async (refreshTokenInput: string) => {
...
@@ -4389,7 +4394,8 @@ const handleOpenAIValidateRT = async (refreshTokenInput: string) => {
try
{
try
{
const
tokenInfo
=
await
oauthClient
.
validateRefreshToken
(
const
tokenInfo
=
await
oauthClient
.
validateRefreshToken
(
refreshTokens
[
i
],
refreshTokens
[
i
],
form
.
proxy_id
form
.
proxy_id
,
clientId
)
)
if
(
!
tokenInfo
)
{
if
(
!
tokenInfo
)
{
failedCount
++
failedCount
++
...
@@ -4399,6 +4405,9 @@ const handleOpenAIValidateRT = async (refreshTokenInput: string) => {
...
@@ -4399,6 +4405,9 @@ const handleOpenAIValidateRT = async (refreshTokenInput: string) => {
}
}
const
credentials
=
oauthClient
.
buildCredentials
(
tokenInfo
)
const
credentials
=
oauthClient
.
buildCredentials
(
tokenInfo
)
if
(
clientId
)
{
credentials
.
client_id
=
clientId
}
const
oauthExtra
=
oauthClient
.
buildExtraInfo
(
tokenInfo
)
as
Record
<
string
,
unknown
>
|
undefined
const
oauthExtra
=
oauthClient
.
buildExtraInfo
(
tokenInfo
)
as
Record
<
string
,
unknown
>
|
undefined
const
extra
=
buildOpenAIExtra
(
oauthExtra
)
const
extra
=
buildOpenAIExtra
(
oauthExtra
)
...
@@ -4410,8 +4419,9 @@ const handleOpenAIValidateRT = async (refreshTokenInput: string) => {
...
@@ -4410,8 +4419,9 @@ const handleOpenAIValidateRT = async (refreshTokenInput: string) => {
}
}
}
}
// Generate account name with index for batch
// Generate account name; fallback to email if name is empty (ent schema requires NotEmpty)
const
accountName
=
refreshTokens
.
length
>
1
?
`${form.name
}
#${i + 1
}
`
:
form
.
name
const
baseName
=
form
.
name
||
tokenInfo
.
email
||
'
OpenAI OAuth Account
'
const
accountName
=
refreshTokens
.
length
>
1
?
`${baseName
}
#${i + 1
}
`
:
baseName
let
openaiAccountId
:
string
|
number
|
undefined
let
openaiAccountId
:
string
|
number
|
undefined
...
@@ -4494,6 +4504,12 @@ const handleOpenAIValidateRT = async (refreshTokenInput: string) => {
...
@@ -4494,6 +4504,12 @@ const handleOpenAIValidateRT = async (refreshTokenInput: string) => {
}
}
}
}
// 手动输入 RT(Codex CLI client_id,默认)
const
handleOpenAIValidateRT
=
(
rt
:
string
)
=>
handleOpenAIBatchRT
(
rt
)
// 手动输入 Mobile RT(SoraClientID)
const
handleOpenAIValidateMobileRT
=
(
rt
:
string
)
=>
handleOpenAIBatchRT
(
rt
,
OPENAI_MOBILE_RT_CLIENT_ID
)
// Sora 手动 ST 批量验证和创建
// Sora 手动 ST 批量验证和创建
const
handleSoraValidateST
=
async
(
sessionTokenInput
:
string
)
=>
{
const
handleSoraValidateST
=
async
(
sessionTokenInput
:
string
)
=>
{
const
oauthClient
=
activeOpenAIOAuth
.
value
const
oauthClient
=
activeOpenAIOAuth
.
value
...
...
frontend/src/components/account/OAuthAuthorizationFlow.vue
View file @
25d961d4
...
@@ -48,6 +48,17 @@
...
@@ -48,6 +48,17 @@
t
(
getOAuthKey
(
'
refreshTokenAuth
'
))
t
(
getOAuthKey
(
'
refreshTokenAuth
'
))
}}
</span>
}}
</span>
</label>
</label>
<label
v-if=
"showMobileRefreshTokenOption"
class=
"flex cursor-pointer items-center gap-2"
>
<input
v-model=
"inputMethod"
type=
"radio"
value=
"mobile_refresh_token"
class=
"text-blue-600 focus:ring-blue-500"
/>
<span
class=
"text-sm text-blue-900 dark:text-blue-200"
>
{{
t
(
'
admin.accounts.oauth.openai.mobileRefreshTokenAuth
'
,
'
手动输入 Mobile RT
'
)
}}
</span>
</label>
<label
v-if=
"showSessionTokenOption"
class=
"flex cursor-pointer items-center gap-2"
>
<label
v-if=
"showSessionTokenOption"
class=
"flex cursor-pointer items-center gap-2"
>
<input
<input
v-model=
"inputMethod"
v-model=
"inputMethod"
...
@@ -73,8 +84,8 @@
...
@@ -73,8 +84,8 @@
</div>
</div>
</div>
</div>
<!-- Refresh Token Input (OpenAI / Antigravity) -->
<!-- Refresh Token Input (OpenAI / Antigravity
/ Mobile RT
) -->
<div
v-if=
"inputMethod === 'refresh_token'"
class=
"space-y-4"
>
<div
v-if=
"inputMethod === 'refresh_token'
|| inputMethod === 'mobile_refresh_token'
"
class=
"space-y-4"
>
<div
<div
class=
"rounded-lg border border-blue-300 bg-white/80 p-4 dark:border-blue-600 dark:bg-gray-800/80"
class=
"rounded-lg border border-blue-300 bg-white/80 p-4 dark:border-blue-600 dark:bg-gray-800/80"
>
>
...
@@ -759,6 +770,7 @@ interface Props {
...
@@ -759,6 +770,7 @@ interface Props {
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)
showMobileRefreshTokenOption
?:
boolean
// Whether to show mobile refresh token option (OpenAI only)
showSessionTokenOption
?:
boolean
// Whether to show session token input option (Sora only)
showSessionTokenOption
?:
boolean
// Whether to show session token input option (Sora only)
showAccessTokenOption
?:
boolean
// Whether to show access token input option (Sora only)
showAccessTokenOption
?:
boolean
// Whether to show access token input option (Sora only)
platform
?:
AccountPlatform
// Platform type for different UI/text
platform
?:
AccountPlatform
// Platform type for different UI/text
...
@@ -776,6 +788,7 @@ const props = withDefaults(defineProps<Props>(), {
...
@@ -776,6 +788,7 @@ const props = withDefaults(defineProps<Props>(), {
methodLabel
:
'
Authorization Method
'
,
methodLabel
:
'
Authorization Method
'
,
showCookieOption
:
true
,
showCookieOption
:
true
,
showRefreshTokenOption
:
false
,
showRefreshTokenOption
:
false
,
showMobileRefreshTokenOption
:
false
,
showSessionTokenOption
:
false
,
showSessionTokenOption
:
false
,
showAccessTokenOption
:
false
,
showAccessTokenOption
:
false
,
platform
:
'
anthropic
'
,
platform
:
'
anthropic
'
,
...
@@ -787,6 +800,7 @@ const emit = defineEmits<{
...
@@ -787,6 +800,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-mobile-refresh-token
'
:
[
refreshToken
:
string
]
'
validate-session-token
'
:
[
sessionToken
:
string
]
'
validate-session-token
'
:
[
sessionToken
:
string
]
'
import-access-token
'
:
[
accessToken
:
string
]
'
import-access-token
'
:
[
accessToken
:
string
]
'
update:inputMethod
'
:
[
method
:
AuthInputMethod
]
'
update:inputMethod
'
:
[
method
:
AuthInputMethod
]
...
@@ -834,7 +848,7 @@ const oauthState = ref('')
...
@@ -834,7 +848,7 @@ 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
||
props
.
showSessionTokenOption
||
props
.
showAccessTokenOption
)
const
showMethodSelection
=
computed
(()
=>
props
.
showCookieOption
||
props
.
showRefreshTokenOption
||
props
.
showMobileRefreshTokenOption
||
props
.
showSessionTokenOption
||
props
.
showAccessTokenOption
)
// Clipboard
// Clipboard
const
{
copied
,
copyToClipboard
}
=
useClipboard
()
const
{
copied
,
copyToClipboard
}
=
useClipboard
()
...
@@ -945,7 +959,11 @@ const handleCookieAuth = () => {
...
@@ -945,7 +959,11 @@ const handleCookieAuth = () => {
const
handleValidateRefreshToken
=
()
=>
{
const
handleValidateRefreshToken
=
()
=>
{
if
(
refreshTokenInput
.
value
.
trim
())
{
if
(
refreshTokenInput
.
value
.
trim
())
{
emit
(
'
validate-refresh-token
'
,
refreshTokenInput
.
value
.
trim
())
if
(
inputMethod
.
value
===
'
mobile_refresh_token
'
)
{
emit
(
'
validate-mobile-refresh-token
'
,
refreshTokenInput
.
value
.
trim
())
}
else
{
emit
(
'
validate-refresh-token
'
,
refreshTokenInput
.
value
.
trim
())
}
}
}
}
}
...
...
frontend/src/composables/useAccountOAuth.ts
View file @
25d961d4
...
@@ -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
'
|
'
session_token
'
|
'
access_token
'
export
type
AuthInputMethod
=
'
manual
'
|
'
cookie
'
|
'
refresh_token
'
|
'
mobile_refresh_token
'
|
'
session_token
'
|
'
access_token
'
export
interface
OAuthState
{
export
interface
OAuthState
{
authUrl
:
string
authUrl
:
string
...
...
frontend/src/composables/useOpenAIOAuth.ts
View file @
25d961d4
...
@@ -13,6 +13,8 @@ export interface OpenAITokenInfo {
...
@@ -13,6 +13,8 @@ export interface OpenAITokenInfo {
scope
?:
string
scope
?:
string
email
?:
string
email
?:
string
name
?:
string
name
?:
string
plan_type
?:
string
privacy_mode
?:
string
// OpenAI specific IDs (extracted from ID Token)
// OpenAI specific IDs (extracted from ID Token)
chatgpt_account_id
?:
string
chatgpt_account_id
?:
string
chatgpt_user_id
?:
string
chatgpt_user_id
?:
string
...
@@ -126,9 +128,11 @@ export function useOpenAIOAuth(options?: UseOpenAIOAuthOptions) {
...
@@ -126,9 +128,11 @@ export function useOpenAIOAuth(options?: UseOpenAIOAuthOptions) {
}
}
// Validate refresh token and get full token info
// Validate refresh token and get full token info
// clientId: 指定 OAuth client_id(用于第三方渠道获取的 RT,如 app_LlGpXReQgckcGGUo2JrYvtJK)
const
validateRefreshToken
=
async
(
const
validateRefreshToken
=
async
(
refreshToken
:
string
,
refreshToken
:
string
,
proxyId
?:
number
|
null
proxyId
?:
number
|
null
,
clientId
?:
string
):
Promise
<
OpenAITokenInfo
|
null
>
=>
{
):
Promise
<
OpenAITokenInfo
|
null
>
=>
{
if
(
!
refreshToken
.
trim
())
{
if
(
!
refreshToken
.
trim
())
{
error
.
value
=
'
Missing refresh token
'
error
.
value
=
'
Missing refresh token
'
...
@@ -143,11 +147,12 @@ export function useOpenAIOAuth(options?: UseOpenAIOAuthOptions) {
...
@@ -143,11 +147,12 @@ export function useOpenAIOAuth(options?: UseOpenAIOAuthOptions) {
const
tokenInfo
=
await
adminAPI
.
accounts
.
refreshOpenAIToken
(
const
tokenInfo
=
await
adminAPI
.
accounts
.
refreshOpenAIToken
(
refreshToken
.
trim
(),
refreshToken
.
trim
(),
proxyId
,
proxyId
,
`
${
endpointPrefix
}
/refresh-token`
`
${
endpointPrefix
}
/refresh-token`
,
clientId
)
)
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
||
err
.
message
||
'
Failed to validate refresh token
'
appStore
.
showError
(
error
.
value
)
appStore
.
showError
(
error
.
value
)
return
null
return
null
}
finally
{
}
finally
{
...
@@ -182,22 +187,23 @@ export function useOpenAIOAuth(options?: UseOpenAIOAuthOptions) {
...
@@ -182,22 +187,23 @@ export function useOpenAIOAuth(options?: UseOpenAIOAuthOptions) {
}
}
}
}
// Build credentials for OpenAI OAuth account
// Build credentials for OpenAI OAuth account
(aligned with backend BuildAccountCredentials)
const
buildCredentials
=
(
tokenInfo
:
OpenAITokenInfo
):
Record
<
string
,
unknown
>
=>
{
const
buildCredentials
=
(
tokenInfo
:
OpenAITokenInfo
):
Record
<
string
,
unknown
>
=>
{
const
creds
:
Record
<
string
,
unknown
>
=
{
const
creds
:
Record
<
string
,
unknown
>
=
{
access_token
:
tokenInfo
.
access_token
,
access_token
:
tokenInfo
.
access_token
,
refresh_token
:
tokenInfo
.
refresh_token
,
expires_at
:
tokenInfo
.
expires_at
token_type
:
tokenInfo
.
token_type
,
expires_in
:
tokenInfo
.
expires_in
,
expires_at
:
tokenInfo
.
expires_at
,
scope
:
tokenInfo
.
scope
}
}
if
(
tokenInfo
.
client_id
)
{
// 仅在返回了新的 refresh_token 时才写入,防止用空值覆盖已有令牌
creds
.
client_id
=
tokenInfo
.
client_id
if
(
tokenInfo
.
refresh_token
)
{
creds
.
refresh_token
=
tokenInfo
.
refresh_token
}
if
(
tokenInfo
.
id_token
)
{
creds
.
id_token
=
tokenInfo
.
id_token
}
if
(
tokenInfo
.
email
)
{
creds
.
email
=
tokenInfo
.
email
}
}
// Include OpenAI specific IDs (required for forwarding)
if
(
tokenInfo
.
chatgpt_account_id
)
{
if
(
tokenInfo
.
chatgpt_account_id
)
{
creds
.
chatgpt_account_id
=
tokenInfo
.
chatgpt_account_id
creds
.
chatgpt_account_id
=
tokenInfo
.
chatgpt_account_id
}
}
...
@@ -207,6 +213,12 @@ export function useOpenAIOAuth(options?: UseOpenAIOAuthOptions) {
...
@@ -207,6 +213,12 @@ export function useOpenAIOAuth(options?: UseOpenAIOAuthOptions) {
if
(
tokenInfo
.
organization_id
)
{
if
(
tokenInfo
.
organization_id
)
{
creds
.
organization_id
=
tokenInfo
.
organization_id
creds
.
organization_id
=
tokenInfo
.
organization_id
}
}
if
(
tokenInfo
.
plan_type
)
{
creds
.
plan_type
=
tokenInfo
.
plan_type
}
if
(
tokenInfo
.
client_id
)
{
creds
.
client_id
=
tokenInfo
.
client_id
}
return
creds
return
creds
}
}
...
@@ -220,6 +232,9 @@ export function useOpenAIOAuth(options?: UseOpenAIOAuthOptions) {
...
@@ -220,6 +232,9 @@ export function useOpenAIOAuth(options?: UseOpenAIOAuthOptions) {
if
(
tokenInfo
.
name
)
{
if
(
tokenInfo
.
name
)
{
extra
.
name
=
tokenInfo
.
name
extra
.
name
=
tokenInfo
.
name
}
}
if
(
tokenInfo
.
privacy_mode
)
{
extra
.
privacy_mode
=
tokenInfo
.
privacy_mode
}
return
Object
.
keys
(
extra
).
length
>
0
?
extra
:
undefined
return
Object
.
keys
(
extra
).
length
>
0
?
extra
:
undefined
}
}
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment