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) {
oAuthService
:=
service
.
NewOAuthService
(
proxyRepository
,
claudeOAuthClient
)
openAIOAuthClient
:=
repository
.
NewOpenAIOAuthClient
()
openAIOAuthService
:=
service
.
NewOpenAIOAuthService
(
proxyRepository
,
openAIOAuthClient
)
openAIOAuthService
.
SetPrivacyClientFactory
(
privacyClientFactory
)
geminiOAuthClient
:=
repository
.
NewGeminiOAuthClient
(
configConfig
)
geminiCliCodeAssistClient
:=
repository
.
NewGeminiCliCodeAssistClient
()
driveClient
:=
repository
.
NewGeminiDriveClient
()
...
...
backend/internal/pkg/openai/oauth.go
View file @
25d961d4
...
...
@@ -270,6 +270,7 @@ type OpenAIAuthClaims struct {
ChatGPTUserID
string
`json:"chatgpt_user_id"`
ChatGPTPlanType
string
`json:"chatgpt_plan_type"`
UserID
string
`json:"user_id"`
POID
string
`json:"poid"`
// organization ID in access_token JWT
Organizations
[]
OrganizationClaim
`json:"organizations"`
}
...
...
backend/internal/service/openai_oauth_service.go
View file @
25d961d4
...
...
@@ -29,9 +29,10 @@ type soraSessionChunk struct {
// OpenAIOAuthService handles OpenAI OAuth authentication flows
type
OpenAIOAuthService
struct
{
sessionStore
*
openai
.
SessionStore
proxyRepo
ProxyRepository
oauthClient
OpenAIOAuthClient
sessionStore
*
openai
.
SessionStore
proxyRepo
ProxyRepository
oauthClient
OpenAIOAuthClient
privacyClientFactory
PrivacyClientFactory
// 用于调用 chatgpt.com/backend-api(ImpersonateChrome)
}
// NewOpenAIOAuthService creates a new OpenAI OAuth service
...
...
@@ -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
type
OpenAIAuthURLResult
struct
{
AuthURL
string
`json:"auth_url"`
...
...
@@ -131,6 +138,7 @@ type OpenAITokenInfo struct {
ChatGPTUserID
string
`json:"chatgpt_user_id,omitempty"`
OrganizationID
string
`json:"organization_id,omitempty"`
PlanType
string
`json:"plan_type,omitempty"`
PrivacyMode
string
`json:"privacy_mode,omitempty"`
}
// ExchangeCode exchanges authorization code for tokens
...
...
@@ -251,6 +259,30 @@ func (s *OpenAIOAuthService) RefreshTokenWithClientID(ctx context.Context, refre
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
}
...
...
backend/internal/service/openai_privacy_service.go
View file @
25d961d4
...
...
@@ -69,6 +69,139 @@ func disableOpenAITraining(ctx context.Context, clientFactory PrivacyClientFacto
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
{
if
len
(
s
)
<=
n
{
return
s
...
...
frontend/src/api/admin/accounts.ts
View file @
25d961d4
...
...
@@ -550,14 +550,18 @@ export async function getAntigravityDefaultModelMapping(): Promise<Record<string
export
async
function
refreshOpenAIToken
(
refreshToken
:
string
,
proxyId
?:
number
|
null
,
endpoint
:
string
=
'
/admin/openai/refresh-token
'
endpoint
:
string
=
'
/admin/openai/refresh-token
'
,
clientId
?:
string
):
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
}
if
(
proxyId
)
{
payload
.
proxy_id
=
proxyId
}
if
(
clientId
)
{
payload
.
client_id
=
clientId
}
const
{
data
}
=
await
apiClient
.
post
<
Record
<
string
,
unknown
>>
(
endpoint
,
payload
)
return
data
}
...
...
frontend/src/components/account/CreateAccountModal.vue
View file @
25d961d4
...
...
@@ -2504,6 +2504,7 @@
:
allow
-
multiple
=
"
form.platform === 'anthropic'
"
:
show
-
cookie
-
option
=
"
form.platform === 'anthropic'
"
:
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
-
access
-
token
-
option
=
"
form.platform === 'sora'
"
:
platform
=
"
form.platform
"
...
...
@@ -2511,6 +2512,7 @@
@
generate
-
url
=
"
handleGenerateUrl
"
@
cookie
-
auth
=
"
handleCookieAuth
"
@
validate
-
refresh
-
token
=
"
handleValidateRefreshToken
"
@
validate
-
mobile
-
refresh
-
token
=
"
handleOpenAIValidateMobileRT
"
@
validate
-
session
-
token
=
"
handleValidateSessionToken
"
@
import
-
access
-
token
=
"
handleImportAccessToken
"
/>
...
...
@@ -4360,11 +4362,14 @@ const handleOpenAIExchange = async (authCode: string) => {
}
// 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
if
(
!
refreshTokenInput
.
trim
())
return
// Parse multiple refresh tokens (one per line)
const
refreshTokens
=
refreshTokenInput
.
split
(
'
\n
'
)
.
map
((
rt
)
=>
rt
.
trim
())
...
...
@@ -4389,7 +4394,8 @@ const handleOpenAIValidateRT = async (refreshTokenInput: string) => {
try
{
const
tokenInfo
=
await
oauthClient
.
validateRefreshToken
(
refreshTokens
[
i
],
form
.
proxy_id
form
.
proxy_id
,
clientId
)
if
(
!
tokenInfo
)
{
failedCount
++
...
...
@@ -4399,6 +4405,9 @@ const handleOpenAIValidateRT = async (refreshTokenInput: string) => {
}
const
credentials
=
oauthClient
.
buildCredentials
(
tokenInfo
)
if
(
clientId
)
{
credentials
.
client_id
=
clientId
}
const
oauthExtra
=
oauthClient
.
buildExtraInfo
(
tokenInfo
)
as
Record
<
string
,
unknown
>
|
undefined
const
extra
=
buildOpenAIExtra
(
oauthExtra
)
...
...
@@ -4410,8 +4419,9 @@ const handleOpenAIValidateRT = async (refreshTokenInput: string) => {
}
}
// Generate account name with index for batch
const
accountName
=
refreshTokens
.
length
>
1
?
`${form.name
}
#${i + 1
}
`
:
form
.
name
// Generate account name; fallback to email if name is empty (ent schema requires NotEmpty)
const
baseName
=
form
.
name
||
tokenInfo
.
email
||
'
OpenAI OAuth Account
'
const
accountName
=
refreshTokens
.
length
>
1
?
`${baseName
}
#${i + 1
}
`
:
baseName
let
openaiAccountId
:
string
|
number
|
undefined
...
...
@@ -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 批量验证和创建
const
handleSoraValidateST
=
async
(
sessionTokenInput
:
string
)
=>
{
const
oauthClient
=
activeOpenAIOAuth
.
value
...
...
frontend/src/components/account/OAuthAuthorizationFlow.vue
View file @
25d961d4
...
...
@@ -48,6 +48,17 @@
t
(
getOAuthKey
(
'
refreshTokenAuth
'
))
}}
</span>
</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"
>
<input
v-model=
"inputMethod"
...
...
@@ -73,8 +84,8 @@
</div>
</div>
<!-- Refresh Token Input (OpenAI / Antigravity) -->
<div
v-if=
"inputMethod === 'refresh_token'"
class=
"space-y-4"
>
<!-- Refresh Token Input (OpenAI / Antigravity
/ Mobile RT
) -->
<div
v-if=
"inputMethod === 'refresh_token'
|| inputMethod === 'mobile_refresh_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"
>
...
...
@@ -759,6 +770,7 @@ interface Props {
methodLabel
?:
string
showCookieOption
?:
boolean
// Whether to show cookie auto-auth option
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)
showAccessTokenOption
?:
boolean
// Whether to show access token input option (Sora only)
platform
?:
AccountPlatform
// Platform type for different UI/text
...
...
@@ -776,6 +788,7 @@ const props = withDefaults(defineProps<Props>(), {
methodLabel
:
'
Authorization Method
'
,
showCookieOption
:
true
,
showRefreshTokenOption
:
false
,
showMobileRefreshTokenOption
:
false
,
showSessionTokenOption
:
false
,
showAccessTokenOption
:
false
,
platform
:
'
anthropic
'
,
...
...
@@ -787,6 +800,7 @@ const emit = defineEmits<{
'
exchange-code
'
:
[
code
:
string
]
'
cookie-auth
'
:
[
sessionKey
:
string
]
'
validate-refresh-token
'
:
[
refreshToken
:
string
]
'
validate-mobile-refresh-token
'
:
[
refreshToken
:
string
]
'
validate-session-token
'
:
[
sessionToken
:
string
]
'
import-access-token
'
:
[
accessToken
:
string
]
'
update:inputMethod
'
:
[
method
:
AuthInputMethod
]
...
...
@@ -834,7 +848,7 @@ const oauthState = ref('')
const
projectId
=
ref
(
''
)
// 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
const
{
copied
,
copyToClipboard
}
=
useClipboard
()
...
...
@@ -945,7 +959,11 @@ const handleCookieAuth = () => {
const
handleValidateRefreshToken
=
()
=>
{
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'
import
{
adminAPI
}
from
'
@/api/admin
'
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
{
authUrl
:
string
...
...
frontend/src/composables/useOpenAIOAuth.ts
View file @
25d961d4
...
...
@@ -13,6 +13,8 @@ export interface OpenAITokenInfo {
scope
?:
string
email
?:
string
name
?:
string
plan_type
?:
string
privacy_mode
?:
string
// OpenAI specific IDs (extracted from ID Token)
chatgpt_account_id
?:
string
chatgpt_user_id
?:
string
...
...
@@ -126,9 +128,11 @@ export function useOpenAIOAuth(options?: UseOpenAIOAuthOptions) {
}
// Validate refresh token and get full token info
// clientId: 指定 OAuth client_id(用于第三方渠道获取的 RT,如 app_LlGpXReQgckcGGUo2JrYvtJK)
const
validateRefreshToken
=
async
(
refreshToken
:
string
,
proxyId
?:
number
|
null
proxyId
?:
number
|
null
,
clientId
?:
string
):
Promise
<
OpenAITokenInfo
|
null
>
=>
{
if
(
!
refreshToken
.
trim
())
{
error
.
value
=
'
Missing refresh token
'
...
...
@@ -143,11 +147,12 @@ export function useOpenAIOAuth(options?: UseOpenAIOAuthOptions) {
const
tokenInfo
=
await
adminAPI
.
accounts
.
refreshOpenAIToken
(
refreshToken
.
trim
(),
proxyId
,
`
${
endpointPrefix
}
/refresh-token`
`
${
endpointPrefix
}
/refresh-token`
,
clientId
)
return
tokenInfo
as
OpenAITokenInfo
}
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
)
return
null
}
finally
{
...
...
@@ -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
creds
:
Record
<
string
,
unknown
>
=
{
access_token
:
tokenInfo
.
access_token
,
refresh_token
:
tokenInfo
.
refresh_token
,
token_type
:
tokenInfo
.
token_type
,
expires_in
:
tokenInfo
.
expires_in
,
expires_at
:
tokenInfo
.
expires_at
,
scope
:
tokenInfo
.
scope
expires_at
:
tokenInfo
.
expires_at
}
if
(
tokenInfo
.
client_id
)
{
creds
.
client_id
=
tokenInfo
.
client_id
// 仅在返回了新的 refresh_token 时才写入,防止用空值覆盖已有令牌
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
)
{
creds
.
chatgpt_account_id
=
tokenInfo
.
chatgpt_account_id
}
...
...
@@ -207,6 +213,12 @@ export function useOpenAIOAuth(options?: UseOpenAIOAuthOptions) {
if
(
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
}
...
...
@@ -220,6 +232,9 @@ export function useOpenAIOAuth(options?: UseOpenAIOAuthOptions) {
if
(
tokenInfo
.
name
)
{
extra
.
name
=
tokenInfo
.
name
}
if
(
tokenInfo
.
privacy_mode
)
{
extra
.
privacy_mode
=
tokenInfo
.
privacy_mode
}
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