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
987589ea
Commit
987589ea
authored
Feb 21, 2026
by
yangjianbo
Browse files
Merge branch 'test' into release
parents
372e04f6
03f69dd3
Changes
109
Show whitespace changes
Inline
Side-by-side
backend/migrations/054_drop_legacy_cache_columns.sql
0 → 100644
View file @
987589ea
-- Drop legacy cache token columns that lack the underscore separator.
-- These were created by GORM's automatic snake_case conversion:
-- CacheCreation5mTokens → cache_creation5m_tokens (incorrect)
-- CacheCreation1hTokens → cache_creation1h_tokens (incorrect)
--
-- The canonical columns are:
-- cache_creation_5m_tokens (defined in 001_init.sql)
-- cache_creation_1h_tokens (defined in 001_init.sql)
--
-- Migration 009 already copied data from legacy → canonical columns.
-- But upgraded instances may still have post-009 writes in legacy columns.
-- Backfill once more before dropping to prevent data loss.
DO
$$
BEGIN
IF
EXISTS
(
SELECT
1
FROM
information_schema
.
columns
WHERE
table_schema
=
'public'
AND
table_name
=
'usage_logs'
AND
column_name
=
'cache_creation5m_tokens'
)
THEN
UPDATE
usage_logs
SET
cache_creation_5m_tokens
=
cache_creation5m_tokens
WHERE
cache_creation_5m_tokens
=
0
AND
cache_creation5m_tokens
<>
0
;
END
IF
;
IF
EXISTS
(
SELECT
1
FROM
information_schema
.
columns
WHERE
table_schema
=
'public'
AND
table_name
=
'usage_logs'
AND
column_name
=
'cache_creation1h_tokens'
)
THEN
UPDATE
usage_logs
SET
cache_creation_1h_tokens
=
cache_creation1h_tokens
WHERE
cache_creation_1h_tokens
=
0
AND
cache_creation1h_tokens
<>
0
;
END
IF
;
END
$$
;
ALTER
TABLE
usage_logs
DROP
COLUMN
IF
EXISTS
cache_creation5m_tokens
;
ALTER
TABLE
usage_logs
DROP
COLUMN
IF
EXISTS
cache_creation1h_tokens
;
backend/migrations/055_add_cache_ttl_overridden.sql
0 → 100644
View file @
987589ea
-- Add cache_ttl_overridden flag to usage_logs for tracking cache TTL override per account.
ALTER
TABLE
usage_logs
ADD
COLUMN
IF
NOT
EXISTS
cache_ttl_overridden
BOOLEAN
NOT
NULL
DEFAULT
FALSE
;
deploy/config.example.yaml
View file @
987589ea
...
...
@@ -374,6 +374,9 @@ sora:
# Max retries for upstream requests
# 上游请求最大重试次数
max_retries
:
3
# Account+proxy cooldown window after Cloudflare challenge (seconds, 0 to disable)
# Cloudflare challenge 后按账号+代理冷却窗口(秒,0 表示关闭)
cloudflare_challenge_cooldown_seconds
:
900
# Poll interval (seconds)
# 轮询间隔(秒)
poll_interval_seconds
:
2
...
...
@@ -388,7 +391,11 @@ sora:
recent_task_limit_max
:
200
# Enable debug logs for Sora upstream requests
# 启用 Sora 直连调试日志
# 调试日志会输出上游请求尝试、重试、响应摘要;Authorization/openai-sentinel-token 等敏感头会自动脱敏
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)
# 额外请求头(键值对)
headers
:
{}
...
...
@@ -398,6 +405,27 @@ sora:
# Disable TLS fingerprint for Sora upstream
# 关闭 Sora 上游 TLS 指纹伪装
disable_tls_fingerprint
:
false
# curl_cffi sidecar for Sora only (required)
# 仅 Sora 链路使用的 curl_cffi sidecar(必需)
curl_cffi_sidecar
:
# Sora 强制通过 sidecar 请求,必须启用
# Sora is forced to use sidecar only; keep enabled=true
enabled
:
true
# Sidecar base URL (default endpoint: /request)
# sidecar 基础地址(默认请求端点:/request)
base_url
:
"
http://sora-curl-cffi-sidecar:8080"
# curl_cffi impersonate profile, e.g. chrome131/chrome124/safari18_0
# curl_cffi 指纹伪装 profile,例如 chrome131/chrome124/safari18_0
impersonate
:
"
chrome131"
# Sidecar request timeout (seconds)
# sidecar 请求超时(秒)
timeout_seconds
:
60
# Reuse session key per account+proxy to let sidecar persist cookies/session
# 按账号+代理复用 session key,让 sidecar 持久化 cookies/session
session_reuse_enabled
:
true
# Session TTL in sidecar (seconds)
# sidecar 会话 TTL(秒)
session_ttl_seconds
:
3600
storage
:
# Storage type (local only for now)
# 存储类型(首发仅支持 local)
...
...
@@ -431,6 +459,13 @@ sora:
# Cron 调度表达式
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 认证缓存配置
...
...
deploy/docker-compose.yml
View file @
987589ea
...
...
@@ -173,6 +173,7 @@ services:
-
POSTGRES_USER=${POSTGRES_USER:-sub2api}
-
POSTGRES_PASSWORD=${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}
-
POSTGRES_DB=${POSTGRES_DB:-sub2api}
-
PGDATA=/var/lib/postgresql/data
-
TZ=${TZ:-Asia/Shanghai}
networks
:
-
sub2api-network
...
...
frontend/src/api/admin/accounts.ts
View file @
987589ea
...
...
@@ -32,6 +32,7 @@ export async function list(
platform
?:
string
type
?:
string
status
?:
string
group
?:
string
search
?:
string
},
options
?:
{
...
...
@@ -271,7 +272,7 @@ export async function generateAuthUrl(
*/
export
async
function
exchangeCode
(
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
>>
{
const
{
data
}
=
await
apiClient
.
post
<
Record
<
string
,
unknown
>>
(
endpoint
,
exchangeData
)
return
data
...
...
@@ -493,7 +494,8 @@ export async function getAntigravityDefaultModelMapping(): Promise<Record<string
*/
export
async
function
refreshOpenAIToken
(
refreshToken
:
string
,
proxyId
?:
number
|
null
proxyId
?:
number
|
null
,
endpoint
:
string
=
'
/admin/openai/refresh-token
'
):
Promise
<
Record
<
string
,
unknown
>>
{
const
payload
:
{
refresh_token
:
string
;
proxy_id
?:
number
}
=
{
refresh_token
:
refreshToken
...
...
@@ -501,7 +503,29 @@ export async function refreshOpenAIToken(
if
(
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
}
...
...
@@ -527,6 +551,7 @@ export const accountsAPI = {
generateAuthUrl
,
exchangeCode
,
refreshOpenAIToken
,
validateSoraSessionToken
,
batchCreate
,
batchUpdateCredentials
,
bulkUpdate
,
...
...
frontend/src/api/admin/proxies.ts
View file @
987589ea
...
...
@@ -7,6 +7,7 @@ import { apiClient } from '../client'
import
type
{
Proxy
,
ProxyAccountSummary
,
ProxyQualityCheckResult
,
CreateProxyRequest
,
UpdateProxyRequest
,
PaginatedResponse
,
...
...
@@ -143,6 +144,16 @@ export async function testProxy(id: number): Promise<{
return
data
}
/**
* Check proxy quality across common AI targets
* @param id - Proxy ID
* @returns Quality check result
*/
export
async
function
checkProxyQuality
(
id
:
number
):
Promise
<
ProxyQualityCheckResult
>
{
const
{
data
}
=
await
apiClient
.
post
<
ProxyQualityCheckResult
>
(
`/admin/proxies/
${
id
}
/quality-check`
)
return
data
}
/**
* Get proxy usage statistics
* @param id - Proxy ID
...
...
@@ -248,6 +259,7 @@ export const proxiesAPI = {
delete
:
deleteProxy
,
toggleStatus
,
testProxy
,
checkProxyQuality
,
getStats
,
getProxyAccounts
,
batchCreate
,
...
...
frontend/src/components/account/AccountGroupsCell.vue
View file @
987589ea
...
...
@@ -41,7 +41,7 @@
>
<div
class=
"mb-2 flex items-center justify-between"
>
<span
class=
"text-xs font-medium text-gray-500 dark:text-gray-400"
>
{{
t
(
'
admin.accounts.
allGroups
'
,
{
count
:
groups
.
length
}
)
}}
{{
t
(
'
admin.accounts.
groupCountTotal
'
,
{
count
:
groups
.
length
}
)
}}
<
/span
>
<
button
@
click
=
"
showPopover = false
"
...
...
frontend/src/components/account/AccountTestModal.vue
View file @
987589ea
...
...
@@ -41,7 +41,7 @@
</span>
</div>
<div
class=
"space-y-1.5"
>
<div
v-if=
"!isSoraAccount"
class=
"space-y-1.5"
>
<label
class=
"text-sm font-medium text-gray-700 dark:text-gray-300"
>
{{
t
(
'
admin.accounts.selectTestModel
'
)
}}
</label>
...
...
@@ -54,6 +54,12 @@
:placeholder=
"loadingModels ? t('common.loading') + '...' : t('admin.accounts.selectTestModel')"
/>
</div>
<div
v-else
class=
"rounded-lg border border-blue-200 bg-blue-50 px-3 py-2 text-xs text-blue-700 dark:border-blue-700 dark:bg-blue-900/20 dark:text-blue-300"
>
{{
t
(
'
admin.accounts.soraTestHint
'
)
}}
</div>
<!-- Terminal Output -->
<div
class=
"group relative"
>
...
...
@@ -135,12 +141,12 @@
<div
class=
"flex items-center gap-3"
>
<span
class=
"flex items-center gap-1"
>
<Icon
name=
"cpu"
size=
"sm"
:stroke-width=
"2"
/>
{{
t
(
'
admin.accounts.testModel
'
)
}}
{{
isSoraAccount
?
t
(
'
admin.accounts.soraTestTarget
'
)
:
t
(
'
admin.accounts.testModel
'
)
}}
</span>
</div>
<span
class=
"flex items-center gap-1"
>
<Icon
name=
"chatBubble"
size=
"sm"
:stroke-width=
"2"
/>
{{
t
(
'
admin.accounts.testPrompt
'
)
}}
{{
isSoraAccount
?
t
(
'
admin.accounts.soraTestMode
'
)
:
t
(
'
admin.accounts.testPrompt
'
)
}}
</span>
</div>
</div>
...
...
@@ -156,10 +162,10 @@
</button>
<button
@
click=
"startTest"
:disabled=
"status === 'connecting' || !selectedModelId"
:disabled=
"status === 'connecting' ||
(!isSoraAccount &&
!selectedModelId
)
"
:class=
"[
'flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition-all',
status === 'connecting' || !selectedModelId
status === 'connecting' ||
(!isSoraAccount &&
!selectedModelId
)
? 'cursor-not-allowed bg-primary-400 text-white'
: status === 'success'
? 'bg-green-500 text-white hover:bg-green-600'
...
...
@@ -232,7 +238,7 @@
</template>
<
script
setup
lang=
"ts"
>
import
{
ref
,
watch
,
nextTick
}
from
'
vue
'
import
{
computed
,
ref
,
watch
,
nextTick
}
from
'
vue
'
import
{
useI18n
}
from
'
vue-i18n
'
import
BaseDialog
from
'
@/components/common/BaseDialog.vue
'
import
Select
from
'
@/components/common/Select.vue
'
...
...
@@ -267,6 +273,7 @@ const availableModels = ref<ClaudeModel[]>([])
const
selectedModelId
=
ref
(
''
)
const
loadingModels
=
ref
(
false
)
let
eventSource
:
EventSource
|
null
=
null
const
isSoraAccount
=
computed
(()
=>
props
.
account
?.
platform
===
'
sora
'
)
// Load available models when modal opens
watch
(
...
...
@@ -283,6 +290,12 @@ watch(
const
loadAvailableModels
=
async
()
=>
{
if
(
!
props
.
account
)
return
if
(
props
.
account
.
platform
===
'
sora
'
)
{
availableModels
.
value
=
[]
selectedModelId
.
value
=
''
loadingModels
.
value
=
false
return
}
loadingModels
.
value
=
true
selectedModelId
.
value
=
''
// Reset selection before loading
...
...
@@ -350,7 +363,7 @@ const scrollToBottom = async () => {
}
const
startTest
=
async
()
=>
{
if
(
!
props
.
account
||
!
selectedModelId
.
value
)
return
if
(
!
props
.
account
||
(
!
isSoraAccount
.
value
&&
!
selectedModelId
.
value
)
)
return
resetState
()
status
.
value
=
'
connecting
'
...
...
@@ -371,7 +384,9 @@ const startTest = async () => {
Authorization
:
`Bearer
${
localStorage
.
getItem
(
'
auth_token
'
)}
`
,
'
Content-Type
'
:
'
application/json
'
},
body
:
JSON
.
stringify
({
model_id
:
selectedModelId
.
value
})
body
:
JSON
.
stringify
(
isSoraAccount
.
value
?
{}
:
{
model_id
:
selectedModelId
.
value
}
)
})
if
(
!
response
.
ok
)
{
...
...
@@ -428,7 +443,10 @@ const handleEvent = (event: {
if
(
event
.
model
)
{
addLine
(
t
(
'
admin.accounts.usingModel
'
,
{
model
:
event
.
model
}),
'
text-cyan-400
'
)
}
addLine
(
t
(
'
admin.accounts.sendingTestMessage
'
),
'
text-gray-400
'
)
addLine
(
isSoraAccount
.
value
?
t
(
'
admin.accounts.soraTestingFlow
'
)
:
t
(
'
admin.accounts.sendingTestMessage
'
),
'
text-gray-400
'
)
addLine
(
''
,
'
text-gray-300
'
)
addLine
(
t
(
'
admin.accounts.response
'
),
'
text-yellow-400
'
)
break
...
...
frontend/src/components/account/BulkEditAccountModal.vue
View file @
987589ea
...
...
@@ -710,6 +710,7 @@ const groupIds = ref<number[]>([])
// All models list (combined Anthropic + OpenAI)
const
allModels
=
[
{
value
:
'
claude-opus-4-6
'
,
label
:
'
Claude Opus 4.6
'
}
,
{
value
:
'
claude-sonnet-4-6
'
,
label
:
'
Claude Sonnet 4.6
'
}
,
{
value
:
'
claude-opus-4-5-20251101
'
,
label
:
'
Claude Opus 4.5
'
}
,
{
value
:
'
claude-sonnet-4-20250514
'
,
label
:
'
Claude Sonnet 4
'
}
,
{
value
:
'
claude-sonnet-4-5-20250929
'
,
label
:
'
Claude Sonnet 4.5
'
}
,
...
...
@@ -757,6 +758,13 @@ const presetMappings = [
color
:
'
bg-purple-100 text-purple-700 hover:bg-purple-200 dark:bg-purple-900/30 dark:text-purple-400
'
}
,
{
label
:
'
Sonnet 4.6
'
,
from
:
'
claude-sonnet-4-6
'
,
to
:
'
claude-sonnet-4-6
'
,
color
:
'
bg-purple-100 text-purple-700 hover:bg-purple-200 dark:bg-purple-900/30 dark:text-purple-400
'
}
,
{
label
:
'
Opus->Sonnet
'
,
from
:
'
claude-opus-4-5-20251101
'
,
...
...
frontend/src/components/account/CreateAccountModal.vue
View file @
987589ea
...
...
@@ -109,6 +109,28 @@
</svg>
OpenAI
</button>
<button
type=
"button"
@
click=
"form.platform = 'sora'"
:class=
"[
'flex flex-1 items-center justify-center gap-2 rounded-md px-4 py-2.5 text-sm font-medium transition-all',
form.platform === 'sora'
? 'bg-white text-rose-600 shadow-sm dark:bg-dark-600 dark:text-rose-400'
: 'text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200'
]"
>
<svg
class=
"h-4 w-4"
fill=
"none"
viewBox=
"0 0 24 24"
stroke=
"currentColor"
stroke-width=
"2"
>
<path
stroke-linecap=
"round"
stroke-linejoin=
"round"
d=
"M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"
/>
<path
stroke-linecap=
"round"
stroke-linejoin=
"round"
d=
"M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
Sora
</button>
<button
type=
"button"
@
click=
"form.platform = 'gemini'"
...
...
@@ -150,6 +172,38 @@
</div>
</div>
<!-- Account Type Selection (Sora) -->
<div
v-if=
"form.platform === 'sora'"
>
<label
class=
"input-label"
>
{{
t
(
'
admin.accounts.accountType
'
)
}}
</label>
<div
class=
"mt-2 grid grid-cols-1 gap-3"
data-tour=
"account-form-type"
>
<button
type=
"button"
@
click=
"accountCategory = 'oauth-based'"
:class=
"[
'flex items-center gap-3 rounded-lg border-2 p-3 text-left transition-all',
accountCategory === 'oauth-based'
? 'border-rose-500 bg-rose-50 dark:bg-rose-900/20'
: 'border-gray-200 hover:border-rose-300 dark:border-dark-600 dark:hover:border-rose-700'
]"
>
<div
:class=
"[
'flex h-8 w-8 shrink-0 items-center justify-center rounded-lg',
accountCategory === 'oauth-based'
? 'bg-rose-500 text-white'
: 'bg-gray-100 text-gray-500 dark:bg-dark-600 dark:text-gray-400'
]"
>
<Icon
name=
"key"
size=
"sm"
/>
</div>
<div>
<span
class=
"block text-sm font-medium text-gray-900 dark:text-white"
>
OAuth
</span>
<span
class=
"text-xs text-gray-500 dark:text-gray-400"
>
{{
t
(
'
admin.accounts.types.chatgptOauth
'
)
}}
</span>
</div>
</button>
</div>
</div>
<!-- Account Type Selection (Anthropic) -->
<div
v-if=
"form.platform === 'anthropic'"
>
<label
class=
"input-label"
>
{{
t
(
'
admin.accounts.accountType
'
)
}}
</label>
...
...
@@ -1538,6 +1592,46 @@
<
/button
>
<
/div
>
<
/div
>
<!--
Cache
TTL
Override
-->
<
div
class
=
"
rounded-lg border border-gray-200 p-4 dark:border-dark-600
"
>
<
div
class
=
"
flex items-center justify-between
"
>
<
div
>
<
label
class
=
"
input-label mb-0
"
>
{{
t
(
'
admin.accounts.quotaControl.cacheTTLOverride.label
'
)
}}
<
/label
>
<
p
class
=
"
mt-1 text-xs text-gray-500 dark:text-gray-400
"
>
{{
t
(
'
admin.accounts.quotaControl.cacheTTLOverride.hint
'
)
}}
<
/p
>
<
/div
>
<
button
type
=
"
button
"
@
click
=
"
cacheTTLOverrideEnabled = !cacheTTLOverrideEnabled
"
:
class
=
"
[
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2',
cacheTTLOverrideEnabled ? 'bg-primary-600' : 'bg-gray-200 dark:bg-dark-600'
]
"
>
<
span
:
class
=
"
[
'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out',
cacheTTLOverrideEnabled ? 'translate-x-5' : 'translate-x-0'
]
"
/>
<
/button
>
<
/div
>
<
div
v
-
if
=
"
cacheTTLOverrideEnabled
"
class
=
"
mt-3
"
>
<
label
class
=
"
input-label text-xs
"
>
{{
t
(
'
admin.accounts.quotaControl.cacheTTLOverride.target
'
)
}}
<
/label
>
<
select
v
-
model
=
"
cacheTTLOverrideTarget
"
class
=
"
mt-1 block w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm shadow-sm focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500 dark:border-dark-500 dark:bg-dark-700 dark:text-white
"
>
<
option
value
=
"
5m
"
>
5
m
<
/option
>
<
option
value
=
"
1h
"
>
1
h
<
/option
>
<
/select
>
<
p
class
=
"
mt-1 text-xs text-gray-500 dark:text-gray-400
"
>
{{
t
(
'
admin.accounts.quotaControl.cacheTTLOverride.targetHint
'
)
}}
<
/p
>
<
/div
>
<
/div
>
<
/div
>
<
div
>
...
...
@@ -1707,32 +1801,6 @@
<!--
Step
2
:
OAuth
Authorization
-->
<
div
v
-
else
class
=
"
space-y-5
"
>
<!--
同时启用
Sora
开关
(
仅
OpenAI
OAuth
)
-->
<
div
v
-
if
=
"
form.platform === 'openai' && accountCategory === 'oauth-based'
"
class
=
"
mb-4
"
>
<
label
class
=
"
flex items-center justify-between rounded-lg border border-gray-200 p-3 dark:border-dark-600
"
>
<
div
class
=
"
flex items-center gap-3
"
>
<
div
class
=
"
flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-rose-100 text-rose-600 dark:bg-rose-900/30 dark:text-rose-400
"
>
<
svg
class
=
"
h-4 w-4
"
fill
=
"
none
"
viewBox
=
"
0 0 24 24
"
stroke
=
"
currentColor
"
stroke
-
width
=
"
2
"
>
<
path
stroke
-
linecap
=
"
round
"
stroke
-
linejoin
=
"
round
"
d
=
"
M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z
"
/>
<
path
stroke
-
linecap
=
"
round
"
stroke
-
linejoin
=
"
round
"
d
=
"
M21 12a9 9 0 11-18 0 9 9 0 0118 0z
"
/>
<
/svg
>
<
/div
>
<
div
>
<
span
class
=
"
block text-sm font-medium text-gray-900 dark:text-white
"
>
{{
t
(
'
admin.accounts.openai.enableSora
'
)
}}
<
/span
>
<
span
class
=
"
text-xs text-gray-500 dark:text-gray-400
"
>
{{
t
(
'
admin.accounts.openai.enableSoraHint
'
)
}}
<
/span
>
<
/div
>
<
/div
>
<
label
:
class
=
"
['switch', { 'switch-active': enableSoraOnOpenAIOAuth
}
]
"
>
<
input
type
=
"
checkbox
"
v
-
model
=
"
enableSoraOnOpenAIOAuth
"
class
=
"
sr-only
"
/>
<
span
class
=
"
switch-thumb
"
><
/span
>
<
/label
>
<
/label
>
<
/div
>
<
OAuthAuthorizationFlow
ref
=
"
oauthFlowRef
"
:
add
-
method
=
"
form.platform === 'anthropic' ? addMethod : 'oauth'
"
...
...
@@ -1741,15 +1809,17 @@
:
loading
=
"
currentOAuthLoading
"
:
error
=
"
currentOAuthError
"
:
show
-
help
=
"
form.platform === 'anthropic'
"
:
show
-
proxy
-
warning
=
"
form.platform !== 'openai' && !!form.proxy_id
"
:
show
-
proxy
-
warning
=
"
form.platform !== 'openai' &&
form.platform !== 'sora' &&
!!form.proxy_id
"
:
allow
-
multiple
=
"
form.platform === 'anthropic'
"
:
show
-
cookie
-
option
=
"
form.platform === 'anthropic'
"
:
show
-
refresh
-
token
-
option
=
"
form.platform === 'openai' || form.platform === 'antigravity'
"
:
show
-
refresh
-
token
-
option
=
"
form.platform === 'openai' || form.platform === 'sora' || form.platform === 'antigravity'
"
:
show
-
session
-
token
-
option
=
"
form.platform === 'sora'
"
:
platform
=
"
form.platform
"
:
show
-
project
-
id
=
"
geminiOAuthType === 'code_assist'
"
@
generate
-
url
=
"
handleGenerateUrl
"
@
cookie
-
auth
=
"
handleCookieAuth
"
@
validate
-
refresh
-
token
=
"
handleValidateRefreshToken
"
@
validate
-
session
-
token
=
"
handleValidateSessionToken
"
/>
<
/div
>
...
...
@@ -2108,6 +2178,7 @@ interface OAuthFlowExposed {
projectId
:
string
sessionKey
:
string
refreshToken
:
string
sessionToken
:
string
inputMethod
:
AuthInputMethod
reset
:
()
=>
void
}
...
...
@@ -2116,7 +2187,7 @@ const { t } = useI18n()
const
authStore
=
useAuthStore
()
const
oauthStepTitle
=
computed
(()
=>
{
if
(
form
.
platform
===
'
openai
'
)
return
t
(
'
admin.accounts.oauth.openai.title
'
)
if
(
form
.
platform
===
'
openai
'
||
form
.
platform
===
'
sora
'
)
return
t
(
'
admin.accounts.oauth.openai.title
'
)
if
(
form
.
platform
===
'
gemini
'
)
return
t
(
'
admin.accounts.oauth.gemini.title
'
)
if
(
form
.
platform
===
'
antigravity
'
)
return
t
(
'
admin.accounts.oauth.antigravity.title
'
)
return
t
(
'
admin.accounts.oauth.title
'
)
...
...
@@ -2124,13 +2195,13 @@ const oauthStepTitle = computed(() => {
// Platform-specific hints for API Key type
const
baseUrlHint
=
computed
(()
=>
{
if
(
form
.
platform
===
'
openai
'
)
return
t
(
'
admin.accounts.openai.baseUrlHint
'
)
if
(
form
.
platform
===
'
openai
'
||
form
.
platform
===
'
sora
'
)
return
t
(
'
admin.accounts.openai.baseUrlHint
'
)
if
(
form
.
platform
===
'
gemini
'
)
return
t
(
'
admin.accounts.gemini.baseUrlHint
'
)
return
t
(
'
admin.accounts.baseUrlHint
'
)
}
)
const
apiKeyHint
=
computed
(()
=>
{
if
(
form
.
platform
===
'
openai
'
)
return
t
(
'
admin.accounts.openai.apiKeyHint
'
)
if
(
form
.
platform
===
'
openai
'
||
form
.
platform
===
'
sora
'
)
return
t
(
'
admin.accounts.openai.apiKeyHint
'
)
if
(
form
.
platform
===
'
gemini
'
)
return
t
(
'
admin.accounts.gemini.apiKeyHint
'
)
return
t
(
'
admin.accounts.apiKeyHint
'
)
}
)
...
...
@@ -2151,34 +2222,36 @@ const appStore = useAppStore()
// OAuth composables
const
oauth
=
useAccountOAuth
()
// For Anthropic OAuth
const
openaiOAuth
=
useOpenAIOAuth
()
// For OpenAI OAuth
const
openaiOAuth
=
useOpenAIOAuth
({
platform
:
'
openai
'
}
)
// For OpenAI OAuth
const
soraOAuth
=
useOpenAIOAuth
({
platform
:
'
sora
'
}
)
// For Sora OAuth
const
geminiOAuth
=
useGeminiOAuth
()
// For Gemini OAuth
const
antigravityOAuth
=
useAntigravityOAuth
()
// For Antigravity OAuth
const
activeOpenAIOAuth
=
computed
(()
=>
(
form
.
platform
===
'
sora
'
?
soraOAuth
:
openaiOAuth
))
// Computed: current OAuth state for template binding
const
currentAuthUrl
=
computed
(()
=>
{
if
(
form
.
platform
===
'
openai
'
)
return
o
pen
ai
OAuth
.
authUrl
.
value
if
(
form
.
platform
===
'
openai
'
||
form
.
platform
===
'
sora
'
)
return
activeO
pen
AI
OAuth
.
value
.
authUrl
.
value
if
(
form
.
platform
===
'
gemini
'
)
return
geminiOAuth
.
authUrl
.
value
if
(
form
.
platform
===
'
antigravity
'
)
return
antigravityOAuth
.
authUrl
.
value
return
oauth
.
authUrl
.
value
}
)
const
currentSessionId
=
computed
(()
=>
{
if
(
form
.
platform
===
'
openai
'
)
return
o
pen
ai
OAuth
.
sessionId
.
value
if
(
form
.
platform
===
'
openai
'
||
form
.
platform
===
'
sora
'
)
return
activeO
pen
AI
OAuth
.
value
.
sessionId
.
value
if
(
form
.
platform
===
'
gemini
'
)
return
geminiOAuth
.
sessionId
.
value
if
(
form
.
platform
===
'
antigravity
'
)
return
antigravityOAuth
.
sessionId
.
value
return
oauth
.
sessionId
.
value
}
)
const
currentOAuthLoading
=
computed
(()
=>
{
if
(
form
.
platform
===
'
openai
'
)
return
o
pen
ai
OAuth
.
loading
.
value
if
(
form
.
platform
===
'
openai
'
||
form
.
platform
===
'
sora
'
)
return
activeO
pen
AI
OAuth
.
value
.
loading
.
value
if
(
form
.
platform
===
'
gemini
'
)
return
geminiOAuth
.
loading
.
value
if
(
form
.
platform
===
'
antigravity
'
)
return
antigravityOAuth
.
loading
.
value
return
oauth
.
loading
.
value
}
)
const
currentOAuthError
=
computed
(()
=>
{
if
(
form
.
platform
===
'
openai
'
)
return
o
pen
ai
OAuth
.
error
.
value
if
(
form
.
platform
===
'
openai
'
||
form
.
platform
===
'
sora
'
)
return
activeO
pen
AI
OAuth
.
value
.
error
.
value
if
(
form
.
platform
===
'
gemini
'
)
return
geminiOAuth
.
error
.
value
if
(
form
.
platform
===
'
antigravity
'
)
return
antigravityOAuth
.
error
.
value
return
oauth
.
error
.
value
...
...
@@ -2217,7 +2290,6 @@ const interceptWarmupRequests = ref(false)
const
autoPauseOnExpired
=
ref
(
true
)
const
openaiPassthroughEnabled
=
ref
(
false
)
const
codexCLIOnlyEnabled
=
ref
(
false
)
const
enableSoraOnOpenAIOAuth
=
ref
(
false
)
// OpenAI OAuth 时同时启用 Sora
const
mixedScheduling
=
ref
(
false
)
// For antigravity accounts: enable mixed scheduling
const
antigravityAccountType
=
ref
<
'
oauth
'
|
'
upstream
'
>
(
'
oauth
'
)
// For antigravity: oauth or upstream
const
upstreamBaseUrl
=
ref
(
''
)
// For upstream type: base URL
...
...
@@ -2250,6 +2322,8 @@ const maxSessions = ref<number | null>(null)
const
sessionIdleTimeout
=
ref
<
number
|
null
>
(
null
)
const
tlsFingerprintEnabled
=
ref
(
false
)
const
sessionIdMaskingEnabled
=
ref
(
false
)
const
cacheTTLOverrideEnabled
=
ref
(
false
)
const
cacheTTLOverrideTarget
=
ref
<
string
>
(
'
5m
'
)
// Gemini tier selection (used as fallback when auto-detection is unavailable/fails)
const
geminiTierGoogleOne
=
ref
<
'
google_one_free
'
|
'
google_ai_pro
'
|
'
google_ai_ultra
'
>
(
'
google_one_free
'
)
...
...
@@ -2356,8 +2430,8 @@ const expiresAtInput = computed({
const
canExchangeCode
=
computed
(()
=>
{
const
authCode
=
oauthFlowRef
.
value
?.
authCode
||
''
if
(
form
.
platform
===
'
openai
'
)
{
return
authCode
.
trim
()
&&
o
pen
ai
OAuth
.
sessionId
.
value
&&
!
o
pen
ai
OAuth
.
loading
.
value
if
(
form
.
platform
===
'
openai
'
||
form
.
platform
===
'
sora
'
)
{
return
authCode
.
trim
()
&&
activeO
pen
AI
OAuth
.
value
.
sessionId
.
value
&&
!
activeO
pen
AI
OAuth
.
value
.
loading
.
value
}
if
(
form
.
platform
===
'
gemini
'
)
{
return
authCode
.
trim
()
&&
geminiOAuth
.
sessionId
.
value
&&
!
geminiOAuth
.
loading
.
value
...
...
@@ -2417,7 +2491,7 @@ watch(
(
newPlatform
)
=>
{
// Reset base URL based on platform
apiKeyBaseUrl
.
value
=
newPlatform
===
'
openai
'
(
newPlatform
===
'
openai
'
||
newPlatform
===
'
sora
'
)
?
'
https://api.openai.com
'
:
newPlatform
===
'
gemini
'
?
'
https://generativelanguage.googleapis.com
'
...
...
@@ -2443,6 +2517,11 @@ watch(
if
(
newPlatform
!==
'
anthropic
'
)
{
interceptWarmupRequests
.
value
=
false
}
if
(
newPlatform
===
'
sora
'
)
{
accountCategory
.
value
=
'
oauth-based
'
addMethod
.
value
=
'
oauth
'
form
.
type
=
'
oauth
'
}
if
(
newPlatform
!==
'
openai
'
)
{
openaiPassthroughEnabled
.
value
=
false
codexCLIOnlyEnabled
.
value
=
false
...
...
@@ -2450,6 +2529,7 @@ watch(
// Reset OAuth states
oauth
.
resetState
()
openaiOAuth
.
resetState
()
soraOAuth
.
resetState
()
geminiOAuth
.
resetState
()
antigravityOAuth
.
resetState
()
}
...
...
@@ -2711,7 +2791,6 @@ const resetForm = () => {
autoPauseOnExpired
.
value
=
true
openaiPassthroughEnabled
.
value
=
false
codexCLIOnlyEnabled
.
value
=
false
enableSoraOnOpenAIOAuth
.
value
=
false
// Reset quota control state
windowCostEnabled
.
value
=
false
windowCostLimit
.
value
=
null
...
...
@@ -2721,6 +2800,8 @@ const resetForm = () => {
sessionIdleTimeout
.
value
=
null
tlsFingerprintEnabled
.
value
=
false
sessionIdMaskingEnabled
.
value
=
false
cacheTTLOverrideEnabled
.
value
=
false
cacheTTLOverrideTarget
.
value
=
'
5m
'
antigravityAccountType
.
value
=
'
oauth
'
upstreamBaseUrl
.
value
=
''
upstreamApiKey
.
value
=
''
...
...
@@ -2732,6 +2813,7 @@ const resetForm = () => {
geminiTierAIStudio
.
value
=
'
aistudio_free
'
oauth
.
resetState
()
openaiOAuth
.
resetState
()
soraOAuth
.
resetState
()
geminiOAuth
.
resetState
()
antigravityOAuth
.
resetState
()
oauthFlowRef
.
value
?.
reset
()
...
...
@@ -2763,6 +2845,23 @@ const buildOpenAIExtra = (base?: Record<string, unknown>): Record<string, unknow
return
Object
.
keys
(
extra
).
length
>
0
?
extra
:
undefined
}
const
buildSoraExtra
=
(
base
?:
Record
<
string
,
unknown
>
,
linkedOpenAIAccountId
?:
string
|
number
):
Record
<
string
,
unknown
>
|
undefined
=>
{
const
extra
:
Record
<
string
,
unknown
>
=
{
...(
base
||
{
}
)
}
if
(
linkedOpenAIAccountId
!==
undefined
&&
linkedOpenAIAccountId
!==
null
)
{
const
id
=
String
(
linkedOpenAIAccountId
).
trim
()
if
(
id
)
{
extra
.
linked_openai_account_id
=
id
}
}
delete
extra
.
openai_passthrough
delete
extra
.
openai_oauth_passthrough
delete
extra
.
codex_cli_only
return
Object
.
keys
(
extra
).
length
>
0
?
extra
:
undefined
}
// Helper function to create account with mixed channel warning handling
const
doCreateAccount
=
async
(
payload
:
any
)
=>
{
submitting
.
value
=
true
...
...
@@ -2878,7 +2977,7 @@ const handleSubmit = async () => {
// Determine default base URL based on platform
const
defaultBaseUrl
=
form
.
platform
===
'
openai
'
(
form
.
platform
===
'
openai
'
||
form
.
platform
===
'
sora
'
)
?
'
https://api.openai.com
'
:
form
.
platform
===
'
gemini
'
?
'
https://generativelanguage.googleapis.com
'
...
...
@@ -2930,14 +3029,15 @@ const goBackToBasicInfo = () => {
step
.
value
=
1
oauth
.
resetState
()
openaiOAuth
.
resetState
()
soraOAuth
.
resetState
()
geminiOAuth
.
resetState
()
antigravityOAuth
.
resetState
()
oauthFlowRef
.
value
?.
reset
()
}
const
handleGenerateUrl
=
async
()
=>
{
if
(
form
.
platform
===
'
openai
'
)
{
await
o
pen
ai
OAuth
.
generateAuthUrl
(
form
.
proxy_id
)
if
(
form
.
platform
===
'
openai
'
||
form
.
platform
===
'
sora
'
)
{
await
activeO
pen
AI
OAuth
.
value
.
generateAuthUrl
(
form
.
proxy_id
)
}
else
if
(
form
.
platform
===
'
gemini
'
)
{
await
geminiOAuth
.
generateAuthUrl
(
form
.
proxy_id
,
...
...
@@ -2953,13 +3053,19 @@ const handleGenerateUrl = async () => {
}
const
handleValidateRefreshToken
=
(
rt
:
string
)
=>
{
if
(
form
.
platform
===
'
openai
'
)
{
if
(
form
.
platform
===
'
openai
'
||
form
.
platform
===
'
sora
'
)
{
handleOpenAIValidateRT
(
rt
)
}
else
if
(
form
.
platform
===
'
antigravity
'
)
{
handleAntigravityValidateRT
(
rt
)
}
}
const
handleValidateSessionToken
=
(
sessionToken
:
string
)
=>
{
if
(
form
.
platform
===
'
sora
'
)
{
handleSoraValidateST
(
sessionToken
)
}
}
const
formatDateTimeLocal
=
formatDateTimeLocalInput
const
parseDateTimeLocal
=
parseDateTimeLocalInput
...
...
@@ -2995,29 +3101,42 @@ const createAccountAndFinish = async (
// OpenAI OAuth 授权码兑换
const
handleOpenAIExchange
=
async
(
authCode
:
string
)
=>
{
if
(
!
authCode
.
trim
()
||
!
openaiOAuth
.
sessionId
.
value
)
return
const
oauthClient
=
activeOpenAIOAuth
.
value
if
(
!
authCode
.
trim
()
||
!
oauthClient
.
sessionId
.
value
)
return
o
penaiOAuth
.
loading
.
value
=
true
o
penaiOAuth
.
error
.
value
=
''
o
authClient
.
loading
.
value
=
true
o
authClient
.
error
.
value
=
''
try
{
const
tokenInfo
=
await
openaiOAuth
.
exchangeAuthCode
(
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
oauthClient
.
exchangeAuthCode
(
authCode
.
trim
(),
openaiOAuth
.
sessionId
.
value
,
oauthClient
.
sessionId
.
value
,
stateToUse
,
form
.
proxy_id
)
if
(
!
tokenInfo
)
return
const
credentials
=
o
penaiOAuth
.
buildCredentials
(
tokenInfo
)
const
oauthExtra
=
o
penaiOAuth
.
buildExtraInfo
(
tokenInfo
)
as
Record
<
string
,
unknown
>
|
undefined
const
credentials
=
o
authClient
.
buildCredentials
(
tokenInfo
)
const
oauthExtra
=
o
authClient
.
buildExtraInfo
(
tokenInfo
)
as
Record
<
string
,
unknown
>
|
undefined
const
extra
=
buildOpenAIExtra
(
oauthExtra
)
const
shouldCreateOpenAI
=
form
.
platform
===
'
openai
'
const
shouldCreateSora
=
form
.
platform
===
'
sora
'
// 应用临时不可调度配置
if
(
!
applyTempUnschedConfig
(
credentials
))
{
return
}
// 1. 创建 OpenAI 账号
let
openaiAccountId
:
string
|
number
|
undefined
if
(
shouldCreateOpenAI
)
{
const
openaiAccount
=
await
adminAPI
.
accounts
.
create
({
name
:
form
.
name
,
notes
:
form
.
notes
,
...
...
@@ -3033,29 +3152,21 @@ const handleOpenAIExchange = async (authCode: string) => {
expires_at
:
form
.
expires_at
,
auto_pause_on_expired
:
autoPauseOnExpired
.
value
}
)
openaiAccountId
=
openaiAccount
.
id
appStore
.
showSuccess
(
t
(
'
admin.accounts.accountCreated
'
))
}
// 2. 如果启用了 Sora,同时创建 Sora 账号
if
(
enableSoraOnOpenAIOAuth
.
value
)
{
try
{
// Sora 使用相同的 OAuth credentials
if
(
shouldCreateSora
)
{
const
soraCredentials
=
{
access_token
:
credentials
.
access_token
,
refresh_token
:
credentials
.
refresh_token
,
expires_at
:
credentials
.
expires_at
}
// 建立关联关系
const
soraExtra
:
Record
<
string
,
unknown
>
=
{
...(
extra
||
{
}
),
linked_openai_account_id
:
String
(
openaiAccount
.
id
)
}
delete
soraExtra
.
openai_passthrough
delete
soraExtra
.
openai_oauth_passthrough
const
soraName
=
shouldCreateOpenAI
?
`${form.name
}
(Sora)`
:
form
.
name
const
soraExtra
=
buildSoraExtra
(
shouldCreateOpenAI
?
extra
:
oauthExtra
,
openaiAccountId
)
await
adminAPI
.
accounts
.
create
({
name
:
`${form.name
}
(Sora)`
,
name
:
soraName
,
notes
:
form
.
notes
,
platform
:
'
sora
'
,
type
:
'
oauth
'
,
...
...
@@ -3069,26 +3180,22 @@ const handleOpenAIExchange = async (authCode: string) => {
expires_at
:
form
.
expires_at
,
auto_pause_on_expired
:
autoPauseOnExpired
.
value
}
)
appStore
.
showSuccess
(
t
(
'
admin.accounts.soraAccountCreated
'
))
}
catch
(
error
:
any
)
{
console
.
error
(
'
创建 Sora 账号失败:
'
,
error
)
appStore
.
showWarning
(
t
(
'
admin.accounts.soraAccountFailed
'
))
}
appStore
.
showSuccess
(
t
(
'
admin.accounts.accountCreated
'
))
}
emit
(
'
created
'
)
handleClose
()
}
catch
(
error
:
any
)
{
o
penaiOAuth
.
error
.
value
=
error
.
response
?.
data
?.
detail
||
t
(
'
admin.accounts.oauth.authFailed
'
)
appStore
.
showError
(
o
penaiOAuth
.
error
.
value
)
o
authClient
.
error
.
value
=
error
.
response
?.
data
?.
detail
||
t
(
'
admin.accounts.oauth.authFailed
'
)
appStore
.
showError
(
o
authClient
.
error
.
value
)
}
finally
{
o
penaiOAuth
.
loading
.
value
=
false
o
authClient
.
loading
.
value
=
false
}
}
// OpenAI 手动 RT 批量验证和创建
const
handleOpenAIValidateRT
=
async
(
refreshTokenInput
:
string
)
=>
{
const
oauthClient
=
activeOpenAIOAuth
.
value
if
(
!
refreshTokenInput
.
trim
())
return
// Parse multiple refresh tokens (one per line)
...
...
@@ -3098,39 +3205,44 @@ const handleOpenAIValidateRT = async (refreshTokenInput: string) => {
.
filter
((
rt
)
=>
rt
)
if
(
refreshTokens
.
length
===
0
)
{
o
penaiOAuth
.
error
.
value
=
t
(
'
admin.accounts.oauth.openai.pleaseEnterRefreshToken
'
)
o
authClient
.
error
.
value
=
t
(
'
admin.accounts.oauth.openai.pleaseEnterRefreshToken
'
)
return
}
o
penaiOAuth
.
loading
.
value
=
true
o
penaiOAuth
.
error
.
value
=
''
o
authClient
.
loading
.
value
=
true
o
authClient
.
error
.
value
=
''
let
successCount
=
0
let
failedCount
=
0
const
errors
:
string
[]
=
[]
const
shouldCreateOpenAI
=
form
.
platform
===
'
openai
'
const
shouldCreateSora
=
form
.
platform
===
'
sora
'
try
{
for
(
let
i
=
0
;
i
<
refreshTokens
.
length
;
i
++
)
{
try
{
const
tokenInfo
=
await
o
penaiOAuth
.
validateRefreshToken
(
const
tokenInfo
=
await
o
authClient
.
validateRefreshToken
(
refreshTokens
[
i
],
form
.
proxy_id
)
if
(
!
tokenInfo
)
{
failedCount
++
errors
.
push
(
`#${i + 1
}
: ${o
penaiOAuth
.error.value || 'Validation failed'
}
`
)
o
penaiOAuth
.
error
.
value
=
''
errors
.
push
(
`#${i + 1
}
: ${o
authClient
.error.value || 'Validation failed'
}
`
)
o
authClient
.
error
.
value
=
''
continue
}
const
credentials
=
o
penaiOAuth
.
buildCredentials
(
tokenInfo
)
const
oauthExtra
=
o
penaiOAuth
.
buildExtraInfo
(
tokenInfo
)
as
Record
<
string
,
unknown
>
|
undefined
const
credentials
=
o
authClient
.
buildCredentials
(
tokenInfo
)
const
oauthExtra
=
o
authClient
.
buildExtraInfo
(
tokenInfo
)
as
Record
<
string
,
unknown
>
|
undefined
const
extra
=
buildOpenAIExtra
(
oauthExtra
)
// Generate account name with index for batch
const
accountName
=
refreshTokens
.
length
>
1
?
`${form.name
}
#${i + 1
}
`
:
form
.
name
await
adminAPI
.
accounts
.
create
({
let
openaiAccountId
:
string
|
number
|
undefined
if
(
shouldCreateOpenAI
)
{
const
openaiAccount
=
await
adminAPI
.
accounts
.
create
({
name
:
accountName
,
notes
:
form
.
notes
,
platform
:
'
openai
'
,
...
...
@@ -3145,6 +3257,34 @@ const handleOpenAIValidateRT = async (refreshTokenInput: string) => {
expires_at
:
form
.
expires_at
,
auto_pause_on_expired
:
autoPauseOnExpired
.
value
}
)
openaiAccountId
=
openaiAccount
.
id
}
if
(
shouldCreateSora
)
{
const
soraCredentials
=
{
access_token
:
credentials
.
access_token
,
refresh_token
:
credentials
.
refresh_token
,
expires_at
:
credentials
.
expires_at
}
const
soraName
=
shouldCreateOpenAI
?
`${accountName
}
(Sora)`
:
accountName
const
soraExtra
=
buildSoraExtra
(
shouldCreateOpenAI
?
extra
:
oauthExtra
,
openaiAccountId
)
await
adminAPI
.
accounts
.
create
({
name
:
soraName
,
notes
:
form
.
notes
,
platform
:
'
sora
'
,
type
:
'
oauth
'
,
credentials
:
soraCredentials
,
extra
:
soraExtra
,
proxy_id
:
form
.
proxy_id
,
concurrency
:
form
.
concurrency
,
priority
:
form
.
priority
,
rate_multiplier
:
form
.
rate_multiplier
,
group_ids
:
form
.
group_ids
,
expires_at
:
form
.
expires_at
,
auto_pause_on_expired
:
autoPauseOnExpired
.
value
}
)
}
successCount
++
}
catch
(
error
:
any
)
{
failedCount
++
...
...
@@ -3166,14 +3306,99 @@ const handleOpenAIValidateRT = async (refreshTokenInput: string) => {
appStore
.
showWarning
(
t
(
'
admin.accounts.oauth.batchPartialSuccess
'
,
{
success
:
successCount
,
failed
:
failedCount
}
)
)
openaiOAuth
.
error
.
value
=
errors
.
join
(
'
\n
'
)
oauthClient
.
error
.
value
=
errors
.
join
(
'
\n
'
)
emit
(
'
created
'
)
}
else
{
oauthClient
.
error
.
value
=
errors
.
join
(
'
\n
'
)
appStore
.
showError
(
t
(
'
admin.accounts.oauth.batchFailed
'
))
}
}
finally
{
oauthClient
.
loading
.
value
=
false
}
}
// Sora 手动 ST 批量验证和创建
const
handleSoraValidateST
=
async
(
sessionTokenInput
:
string
)
=>
{
const
oauthClient
=
activeOpenAIOAuth
.
value
if
(
!
sessionTokenInput
.
trim
())
return
const
sessionTokens
=
sessionTokenInput
.
split
(
'
\n
'
)
.
map
((
st
)
=>
st
.
trim
())
.
filter
((
st
)
=>
st
)
if
(
sessionTokens
.
length
===
0
)
{
oauthClient
.
error
.
value
=
t
(
'
admin.accounts.oauth.openai.pleaseEnterSessionToken
'
)
return
}
oauthClient
.
loading
.
value
=
true
oauthClient
.
error
.
value
=
''
let
successCount
=
0
let
failedCount
=
0
const
errors
:
string
[]
=
[]
try
{
for
(
let
i
=
0
;
i
<
sessionTokens
.
length
;
i
++
)
{
try
{
const
tokenInfo
=
await
oauthClient
.
validateSessionToken
(
sessionTokens
[
i
],
form
.
proxy_id
)
if
(
!
tokenInfo
)
{
failedCount
++
errors
.
push
(
`#${i + 1
}
: ${oauthClient.error.value || 'Validation failed'
}
`
)
oauthClient
.
error
.
value
=
''
continue
}
const
credentials
=
oauthClient
.
buildCredentials
(
tokenInfo
)
credentials
.
session_token
=
sessionTokens
[
i
]
const
oauthExtra
=
oauthClient
.
buildExtraInfo
(
tokenInfo
)
as
Record
<
string
,
unknown
>
|
undefined
const
soraExtra
=
buildSoraExtra
(
oauthExtra
)
const
accountName
=
sessionTokens
.
length
>
1
?
`${form.name
}
#${i + 1
}
`
:
form
.
name
await
adminAPI
.
accounts
.
create
({
name
:
accountName
,
notes
:
form
.
notes
,
platform
:
'
sora
'
,
type
:
'
oauth
'
,
credentials
,
extra
:
soraExtra
,
proxy_id
:
form
.
proxy_id
,
concurrency
:
form
.
concurrency
,
priority
:
form
.
priority
,
rate_multiplier
:
form
.
rate_multiplier
,
group_ids
:
form
.
group_ids
,
expires_at
:
form
.
expires_at
,
auto_pause_on_expired
:
autoPauseOnExpired
.
value
}
)
successCount
++
}
catch
(
error
:
any
)
{
failedCount
++
const
errMsg
=
error
.
response
?.
data
?.
detail
||
error
.
message
||
'
Unknown error
'
errors
.
push
(
`#${i + 1
}
: ${errMsg
}
`
)
}
}
if
(
successCount
>
0
&&
failedCount
===
0
)
{
appStore
.
showSuccess
(
sessionTokens
.
length
>
1
?
t
(
'
admin.accounts.oauth.batchSuccess
'
,
{
count
:
successCount
}
)
:
t
(
'
admin.accounts.accountCreated
'
)
)
emit
(
'
created
'
)
handleClose
()
}
else
if
(
successCount
>
0
&&
failedCount
>
0
)
{
appStore
.
showWarning
(
t
(
'
admin.accounts.oauth.batchPartialSuccess
'
,
{
success
:
successCount
,
failed
:
failedCount
}
)
)
oauthClient
.
error
.
value
=
errors
.
join
(
'
\n
'
)
emit
(
'
created
'
)
}
else
{
o
penaiOAuth
.
error
.
value
=
errors
.
join
(
'
\n
'
)
o
authClient
.
error
.
value
=
errors
.
join
(
'
\n
'
)
appStore
.
showError
(
t
(
'
admin.accounts.oauth.batchFailed
'
))
}
}
finally
{
o
penaiOAuth
.
loading
.
value
=
false
o
authClient
.
loading
.
value
=
false
}
}
...
...
@@ -3393,6 +3618,12 @@ const handleAnthropicExchange = async (authCode: string) => {
extra
.
session_id_masking_enabled
=
true
}
// Add cache TTL override settings
if
(
cacheTTLOverrideEnabled
.
value
)
{
extra
.
cache_ttl_override_enabled
=
true
extra
.
cache_ttl_override_target
=
cacheTTLOverrideTarget
.
value
}
const
credentials
=
{
...
tokenInfo
,
...(
interceptWarmupRequests
.
value
?
{
intercept_warmup_requests
:
true
}
:
{
}
)
...
...
@@ -3412,6 +3643,7 @@ const handleExchangeCode = async () => {
switch
(
form
.
platform
)
{
case
'
openai
'
:
case
'
sora
'
:
return
handleOpenAIExchange
(
authCode
)
case
'
gemini
'
:
return
handleGeminiExchange
(
authCode
)
...
...
@@ -3486,6 +3718,12 @@ const handleCookieAuth = async (sessionKey: string) => {
extra
.
session_id_masking_enabled
=
true
}
// Add cache TTL override settings
if
(
cacheTTLOverrideEnabled
.
value
)
{
extra
.
cache_ttl_override_enabled
=
true
extra
.
cache_ttl_override_target
=
cacheTTLOverrideTarget
.
value
}
const
accountName
=
keys
.
length
>
1
?
`${form.name
}
#${i + 1
}
`
:
form
.
name
// Merge interceptWarmupRequests into credentials
...
...
frontend/src/components/account/EditAccountModal.vue
View file @
987589ea
...
...
@@ -975,6 +975,46 @@
<
/button
>
<
/div
>
<
/div
>
<!--
Cache
TTL
Override
-->
<
div
class
=
"
rounded-lg border border-gray-200 p-4 dark:border-dark-600
"
>
<
div
class
=
"
flex items-center justify-between
"
>
<
div
>
<
label
class
=
"
input-label mb-0
"
>
{{
t
(
'
admin.accounts.quotaControl.cacheTTLOverride.label
'
)
}}
<
/label
>
<
p
class
=
"
mt-1 text-xs text-gray-500 dark:text-gray-400
"
>
{{
t
(
'
admin.accounts.quotaControl.cacheTTLOverride.hint
'
)
}}
<
/p
>
<
/div
>
<
button
type
=
"
button
"
@
click
=
"
cacheTTLOverrideEnabled = !cacheTTLOverrideEnabled
"
:
class
=
"
[
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2',
cacheTTLOverrideEnabled ? 'bg-primary-600' : 'bg-gray-200 dark:bg-dark-600'
]
"
>
<
span
:
class
=
"
[
'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out',
cacheTTLOverrideEnabled ? 'translate-x-5' : 'translate-x-0'
]
"
/>
<
/button
>
<
/div
>
<
div
v
-
if
=
"
cacheTTLOverrideEnabled
"
class
=
"
mt-3
"
>
<
label
class
=
"
input-label text-xs
"
>
{{
t
(
'
admin.accounts.quotaControl.cacheTTLOverride.target
'
)
}}
<
/label
>
<
select
v
-
model
=
"
cacheTTLOverrideTarget
"
class
=
"
mt-1 block w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm shadow-sm focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500 dark:border-dark-500 dark:bg-dark-700 dark:text-white
"
>
<
option
value
=
"
5m
"
>
5
m
<
/option
>
<
option
value
=
"
1h
"
>
1
h
<
/option
>
<
/select
>
<
p
class
=
"
mt-1 text-xs text-gray-500 dark:text-gray-400
"
>
{{
t
(
'
admin.accounts.quotaControl.cacheTTLOverride.targetHint
'
)
}}
<
/p
>
<
/div
>
<
/div
>
<
/div
>
<
div
class
=
"
border-t border-gray-200 pt-4 dark:border-dark-600
"
>
...
...
@@ -1177,6 +1217,8 @@ const maxSessions = ref<number | null>(null)
const
sessionIdleTimeout
=
ref
<
number
|
null
>
(
null
)
const
tlsFingerprintEnabled
=
ref
(
false
)
const
sessionIdMaskingEnabled
=
ref
(
false
)
const
cacheTTLOverrideEnabled
=
ref
(
false
)
const
cacheTTLOverrideTarget
=
ref
<
string
>
(
'
5m
'
)
// OpenAI 自动透传开关(OAuth/API Key)
const
openaiPassthroughEnabled
=
ref
(
false
)
...
...
@@ -1581,6 +1623,8 @@ function loadQuotaControlSettings(account: Account) {
sessionIdleTimeout
.
value
=
null
tlsFingerprintEnabled
.
value
=
false
sessionIdMaskingEnabled
.
value
=
false
cacheTTLOverrideEnabled
.
value
=
false
cacheTTLOverrideTarget
.
value
=
'
5m
'
// Only applies to Anthropic OAuth/SetupToken accounts
if
(
account
.
platform
!==
'
anthropic
'
||
(
account
.
type
!==
'
oauth
'
&&
account
.
type
!==
'
setup-token
'
))
{
...
...
@@ -1609,6 +1653,12 @@ function loadQuotaControlSettings(account: Account) {
if
(
account
.
session_id_masking_enabled
===
true
)
{
sessionIdMaskingEnabled
.
value
=
true
}
// Load cache TTL override setting
if
(
account
.
cache_ttl_override_enabled
===
true
)
{
cacheTTLOverrideEnabled
.
value
=
true
cacheTTLOverrideTarget
.
value
=
account
.
cache_ttl_override_target
||
'
5m
'
}
}
function
formatTempUnschedKeywords
(
value
:
unknown
)
{
...
...
@@ -1820,6 +1870,15 @@ const handleSubmit = async () => {
delete
newExtra
.
session_id_masking_enabled
}
// Cache TTL override setting
if
(
cacheTTLOverrideEnabled
.
value
)
{
newExtra
.
cache_ttl_override_enabled
=
true
newExtra
.
cache_ttl_override_target
=
cacheTTLOverrideTarget
.
value
}
else
{
delete
newExtra
.
cache_ttl_override_enabled
delete
newExtra
.
cache_ttl_override_target
}
updatePayload
.
extra
=
newExtra
}
...
...
frontend/src/components/account/OAuthAuthorizationFlow.vue
View file @
987589ea
...
...
@@ -48,6 +48,17 @@
t
(
getOAuthKey
(
'
refreshTokenAuth
'
))
}}
</span>
</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>
...
...
@@ -135,6 +146,87 @@
<
/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
-->
<
div
v
-
if
=
"
inputMethod === 'cookie'
"
class
=
"
space-y-4
"
>
<
div
...
...
@@ -525,9 +617,10 @@ interface Props {
methodLabel
?:
string
showCookieOption
?:
boolean
// Whether to show cookie auto-auth option
showRefreshTokenOption
?:
boolean
// Whether to show refresh token input option (OpenAI only)
showSessionTokenOption
?:
boolean
// Whether to show session token input option (Sora only)
platform
?:
AccountPlatform
// Platform type for different UI/text
showProjectId
?:
boolean
// New prop to control project ID visibility
}
}
const
props
=
withDefaults
(
defineProps
<
Props
>
(),
{
authUrl
:
''
,
...
...
@@ -540,6 +633,7 @@ const props = withDefaults(defineProps<Props>(), {
methodLabel
:
'
Authorization Method
'
,
showCookieOption
:
true
,
showRefreshTokenOption
:
false
,
showSessionTokenOption
:
false
,
platform
:
'
anthropic
'
,
showProjectId
:
true
}
)
...
...
@@ -549,6 +643,7 @@ const emit = defineEmits<{
'
exchange-code
'
:
[
code
:
string
]
'
cookie-auth
'
:
[
sessionKey
:
string
]
'
validate-refresh-token
'
:
[
refreshToken
:
string
]
'
validate-session-token
'
:
[
sessionToken
:
string
]
'
update:inputMethod
'
:
[
method
:
AuthInputMethod
]
}
>
()
...
...
@@ -587,12 +682,13 @@ const inputMethod = ref<AuthInputMethod>(props.showCookieOption ? 'manual' : 'ma
const
authCodeInput
=
ref
(
''
)
const
sessionKeyInput
=
ref
(
''
)
const
refreshTokenInput
=
ref
(
''
)
const
sessionTokenInput
=
ref
(
''
)
const
showHelpDialog
=
ref
(
false
)
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
)
const
showMethodSelection
=
computed
(()
=>
props
.
showCookieOption
||
props
.
showRefreshTokenOption
||
props
.
showSessionTokenOption
)
// Clipboard
const
{
copied
,
copyToClipboard
}
=
useClipboard
()
...
...
@@ -613,6 +709,13 @@ const parsedRefreshTokenCount = computed(() => {
.
filter
((
rt
)
=>
rt
).
length
}
)
const
parsedSessionTokenCount
=
computed
(()
=>
{
return
sessionTokenInput
.
value
.
split
(
'
\n
'
)
.
map
((
st
)
=>
st
.
trim
())
.
filter
((
st
)
=>
st
).
length
}
)
// Watchers
watch
(
inputMethod
,
(
newVal
)
=>
{
emit
(
'
update:inputMethod
'
,
newVal
)
...
...
@@ -631,7 +734,7 @@ watch(authCodeInput, (newVal) => {
const
url
=
new
URL
(
trimmed
)
const
code
=
url
.
searchParams
.
get
(
'
code
'
)
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
}
if
(
code
&&
code
!==
trimmed
)
{
...
...
@@ -642,7 +745,7 @@ watch(authCodeInput, (newVal) => {
// If URL parsing fails, try regex extraction
const
match
=
trimmed
.
match
(
/
[
?&
]
code=
([^
&
]
+
)
/
)
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
]
}
if
(
match
&&
match
[
1
]
&&
match
[
1
]
!==
trimmed
)
{
...
...
@@ -680,6 +783,12 @@ const handleValidateRefreshToken = () => {
}
}
const
handleValidateSessionToken
=
()
=>
{
if
(
sessionTokenInput
.
value
.
trim
())
{
emit
(
'
validate-session-token
'
,
sessionTokenInput
.
value
.
trim
())
}
}
// Expose methods and state
defineExpose
({
authCode
:
authCodeInput
,
...
...
@@ -687,6 +796,7 @@ defineExpose({
projectId
,
sessionKey
:
sessionKeyInput
,
refreshToken
:
refreshTokenInput
,
sessionToken
:
sessionTokenInput
,
inputMethod
,
reset
:
()
=>
{
authCodeInput
.
value
=
''
...
...
@@ -694,6 +804,7 @@ defineExpose({
projectId
.
value
=
''
sessionKeyInput
.
value
=
''
refreshTokenInput
.
value
=
''
sessionTokenInput
.
value
=
''
inputMethod
.
value
=
'
manual
'
showHelpDialog
.
value
=
false
}
...
...
frontend/src/components/account/ReAuthAccountModal.vue
View file @
987589ea
...
...
@@ -14,7 +14,7 @@
<div
:class=
"[
'flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-br',
isOpenAI
isOpenAI
Like
? 'from-green-500 to-green-600'
: isGemini
? 'from-blue-500 to-blue-600'
...
...
@@ -33,6 +33,8 @@
{{
isOpenAI
?
t
(
'
admin.accounts.openaiAccount
'
)
:
isSora
?
t
(
'
admin.accounts.soraAccount
'
)
:
isGemini
?
t
(
'
admin.accounts.geminiAccount
'
)
:
isAntigravity
...
...
@@ -128,7 +130,7 @@
:show-cookie-option=
"isAnthropic"
:allow-multiple=
"false"
: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'"
@
generate-url=
"handleGenerateUrl"
@
cookie-auth=
"handleCookieAuth"
...
...
@@ -224,7 +226,8 @@ const { t } = useI18n()
// OAuth composables
const
claudeOAuth
=
useAccountOAuth
()
const
openaiOAuth
=
useOpenAIOAuth
()
const
openaiOAuth
=
useOpenAIOAuth
({
platform
:
'
openai
'
})
const
soraOAuth
=
useOpenAIOAuth
({
platform
:
'
sora
'
})
const
geminiOAuth
=
useGeminiOAuth
()
const
antigravityOAuth
=
useAntigravityOAuth
()
...
...
@@ -237,31 +240,34 @@ const geminiOAuthType = ref<'code_assist' | 'google_one' | 'ai_studio'>('code_as
// Computed - check platform
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
isAnthropic
=
computed
(()
=>
props
.
account
?.
platform
===
'
anthropic
'
)
const
isAntigravity
=
computed
(()
=>
props
.
account
?.
platform
===
'
antigravity
'
)
const
activeOpenAIOAuth
=
computed
(()
=>
(
isSora
.
value
?
soraOAuth
:
openaiOAuth
))
// Computed - current OAuth state based on platform
const
currentAuthUrl
=
computed
(()
=>
{
if
(
isOpenAI
.
value
)
return
o
pen
ai
OAuth
.
authUrl
.
value
if
(
isOpenAI
Like
.
value
)
return
activeO
pen
AI
OAuth
.
value
.
authUrl
.
value
if
(
isGemini
.
value
)
return
geminiOAuth
.
authUrl
.
value
if
(
isAntigravity
.
value
)
return
antigravityOAuth
.
authUrl
.
value
return
claudeOAuth
.
authUrl
.
value
})
const
currentSessionId
=
computed
(()
=>
{
if
(
isOpenAI
.
value
)
return
o
pen
ai
OAuth
.
sessionId
.
value
if
(
isOpenAI
Like
.
value
)
return
activeO
pen
AI
OAuth
.
value
.
sessionId
.
value
if
(
isGemini
.
value
)
return
geminiOAuth
.
sessionId
.
value
if
(
isAntigravity
.
value
)
return
antigravityOAuth
.
sessionId
.
value
return
claudeOAuth
.
sessionId
.
value
})
const
currentLoading
=
computed
(()
=>
{
if
(
isOpenAI
.
value
)
return
o
pen
ai
OAuth
.
loading
.
value
if
(
isOpenAI
Like
.
value
)
return
activeO
pen
AI
OAuth
.
value
.
loading
.
value
if
(
isGemini
.
value
)
return
geminiOAuth
.
loading
.
value
if
(
isAntigravity
.
value
)
return
antigravityOAuth
.
loading
.
value
return
claudeOAuth
.
loading
.
value
})
const
currentError
=
computed
(()
=>
{
if
(
isOpenAI
.
value
)
return
o
pen
ai
OAuth
.
error
.
value
if
(
isOpenAI
Like
.
value
)
return
activeO
pen
AI
OAuth
.
value
.
error
.
value
if
(
isGemini
.
value
)
return
geminiOAuth
.
error
.
value
if
(
isAntigravity
.
value
)
return
antigravityOAuth
.
error
.
value
return
claudeOAuth
.
error
.
value
...
...
@@ -269,8 +275,8 @@ const currentError = computed(() => {
// Computed
const
isManualInputMethod
=
computed
(()
=>
{
// OpenAI/Gemini/Antigravity always use manual input (no cookie auth option)
return
isOpenAI
.
value
||
isGemini
.
value
||
isAntigravity
.
value
||
oauthFlowRef
.
value
?.
inputMethod
===
'
manual
'
// OpenAI/
Sora/
Gemini/Antigravity always use manual input (no cookie auth option)
return
isOpenAI
Like
.
value
||
isGemini
.
value
||
isAntigravity
.
value
||
oauthFlowRef
.
value
?.
inputMethod
===
'
manual
'
})
const
canExchangeCode
=
computed
(()
=>
{
...
...
@@ -313,6 +319,7 @@ const resetState = () => {
geminiOAuthType
.
value
=
'
code_assist
'
claudeOAuth
.
resetState
()
openaiOAuth
.
resetState
()
soraOAuth
.
resetState
()
geminiOAuth
.
resetState
()
antigravityOAuth
.
resetState
()
oauthFlowRef
.
value
?.
reset
()
...
...
@@ -325,8 +332,8 @@ const handleClose = () => {
const
handleGenerateUrl
=
async
()
=>
{
if
(
!
props
.
account
)
return
if
(
isOpenAI
.
value
)
{
await
o
pen
ai
OAuth
.
generateAuthUrl
(
props
.
account
.
proxy_id
)
if
(
isOpenAI
Like
.
value
)
{
await
activeO
pen
AI
OAuth
.
value
.
generateAuthUrl
(
props
.
account
.
proxy_id
)
}
else
if
(
isGemini
.
value
)
{
const
creds
=
(
props
.
account
.
credentials
||
{})
as
Record
<
string
,
unknown
>
const
tierId
=
typeof
creds
.
tier_id
===
'
string
'
?
creds
.
tier_id
:
undefined
...
...
@@ -345,21 +352,29 @@ const handleExchangeCode = async () => {
const
authCode
=
oauthFlowRef
.
value
?.
authCode
||
''
if
(
!
authCode
.
trim
())
return
if
(
isOpenAI
.
value
)
{
if
(
isOpenAI
Like
.
value
)
{
// OpenAI OAuth flow
const
sessionId
=
openaiOAuth
.
sessionId
.
value
const
oauthClient
=
activeOpenAIOAuth
.
value
const
sessionId
=
oauthClient
.
sessionId
.
value
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
o
penaiOAuth
.
exchangeAuthCode
(
const
tokenInfo
=
await
o
authClient
.
exchangeAuthCode
(
authCode
.
trim
(),
sessionId
,
stateToUse
,
props
.
account
.
proxy_id
)
if
(
!
tokenInfo
)
return
// Build credentials and extra info
const
credentials
=
o
penaiOAuth
.
buildCredentials
(
tokenInfo
)
const
extra
=
o
penaiOAuth
.
buildExtraInfo
(
tokenInfo
)
const
credentials
=
o
authClient
.
buildCredentials
(
tokenInfo
)
const
extra
=
o
authClient
.
buildExtraInfo
(
tokenInfo
)
try
{
// Update account with new credentials
...
...
@@ -376,8 +391,8 @@ const handleExchangeCode = async () => {
emit
(
'
reauthorized
'
)
handleClose
()
}
catch
(
error
:
any
)
{
o
penaiOAuth
.
error
.
value
=
error
.
response
?.
data
?.
detail
||
t
(
'
admin.accounts.oauth.authFailed
'
)
appStore
.
showError
(
o
penaiOAuth
.
error
.
value
)
o
authClient
.
error
.
value
=
error
.
response
?.
data
?.
detail
||
t
(
'
admin.accounts.oauth.authFailed
'
)
appStore
.
showError
(
o
authClient
.
error
.
value
)
}
}
else
if
(
isGemini
.
value
)
{
const
sessionId
=
geminiOAuth
.
sessionId
.
value
...
...
@@ -490,7 +505,7 @@ const handleExchangeCode = async () => {
}
const
handleCookieAuth
=
async
(
sessionKey
:
string
)
=>
{
if
(
!
props
.
account
||
isOpenAI
.
value
)
return
if
(
!
props
.
account
||
isOpenAI
Like
.
value
)
return
claudeOAuth
.
loading
.
value
=
true
claudeOAuth
.
error
.
value
=
''
...
...
frontend/src/components/admin/account/AccountTableFilters.vue
View file @
987589ea
...
...
@@ -10,16 +10,21 @@
<Select
:model-value=
"filters.platform"
class=
"w-40"
:options=
"pOpts"
@
update:model-value=
"updatePlatform"
@
change=
"$emit('change')"
/>
<Select
:model-value=
"filters.type"
class=
"w-40"
:options=
"tOpts"
@
update:model-value=
"updateType"
@
change=
"$emit('change')"
/>
<Select
:model-value=
"filters.status"
class=
"w-40"
:options=
"sOpts"
@
update:model-value=
"updateStatus"
@
change=
"$emit('change')"
/>
<Select
:model-value=
"filters.group"
class=
"w-40"
:options=
"gOpts"
@
update:model-value=
"updateGroup"
@
change=
"$emit('change')"
/>
</div>
</
template
>
<
script
setup
lang=
"ts"
>
import
{
computed
}
from
'
vue
'
;
import
{
useI18n
}
from
'
vue-i18n
'
;
import
Select
from
'
@/components/common/Select.vue
'
;
import
SearchInput
from
'
@/components/common/SearchInput.vue
'
const
props
=
defineProps
([
'
searchQuery
'
,
'
filters
'
]);
const
emit
=
defineEmits
([
'
update:searchQuery
'
,
'
update:filters
'
,
'
change
'
]);
const
{
t
}
=
useI18n
()
import
type
{
AdminGroup
}
from
'
@/types
'
const
props
=
defineProps
<
{
searchQuery
:
string
;
filters
:
Record
<
string
,
any
>
;
groups
?:
AdminGroup
[]
}
>
()
const
emit
=
defineEmits
([
'
update:searchQuery
'
,
'
update:filters
'
,
'
change
'
]);
const
{
t
}
=
useI18n
()
const
updatePlatform
=
(
value
:
string
|
number
|
boolean
|
null
)
=>
{
emit
(
'
update:filters
'
,
{
...
props
.
filters
,
platform
:
value
})
}
const
updateType
=
(
value
:
string
|
number
|
boolean
|
null
)
=>
{
emit
(
'
update:filters
'
,
{
...
props
.
filters
,
type
:
value
})
}
const
updateStatus
=
(
value
:
string
|
number
|
boolean
|
null
)
=>
{
emit
(
'
update:filters
'
,
{
...
props
.
filters
,
status
:
value
})
}
const
updateGroup
=
(
value
:
string
|
number
|
boolean
|
null
)
=>
{
emit
(
'
update:filters
'
,
{
...
props
.
filters
,
group
:
value
})
}
const
pOpts
=
computed
(()
=>
[{
value
:
''
,
label
:
t
(
'
admin.accounts.allPlatforms
'
)
},
{
value
:
'
anthropic
'
,
label
:
'
Anthropic
'
},
{
value
:
'
openai
'
,
label
:
'
OpenAI
'
},
{
value
:
'
gemini
'
,
label
:
'
Gemini
'
},
{
value
:
'
antigravity
'
,
label
:
'
Antigravity
'
},
{
value
:
'
sora
'
,
label
:
'
Sora
'
}])
const
tOpts
=
computed
(()
=>
[{
value
:
''
,
label
:
t
(
'
admin.accounts.allTypes
'
)
},
{
value
:
'
oauth
'
,
label
:
t
(
'
admin.accounts.oauthType
'
)
},
{
value
:
'
setup-token
'
,
label
:
t
(
'
admin.accounts.setupToken
'
)
},
{
value
:
'
apikey
'
,
label
:
t
(
'
admin.accounts.apiKey
'
)
}])
const
sOpts
=
computed
(()
=>
[{
value
:
''
,
label
:
t
(
'
admin.accounts.allStatus
'
)
},
{
value
:
'
active
'
,
label
:
t
(
'
admin.accounts.status.active
'
)
},
{
value
:
'
inactive
'
,
label
:
t
(
'
admin.accounts.status.inactive
'
)
},
{
value
:
'
error
'
,
label
:
t
(
'
admin.accounts.status.error
'
)
},
{
value
:
'
rate_limited
'
,
label
:
t
(
'
admin.accounts.status.rateLimited
'
)
}])
const
gOpts
=
computed
(()
=>
[{
value
:
''
,
label
:
t
(
'
admin.accounts.allGroups
'
)
},
...(
props
.
groups
||
[]).
map
(
g
=>
({
value
:
String
(
g
.
id
),
label
:
g
.
name
}))])
</
script
>
frontend/src/components/admin/account/AccountTestModal.vue
View file @
987589ea
...
...
@@ -41,7 +41,7 @@
</span>
</div>
<div
class=
"space-y-1.5"
>
<div
v-if=
"!isSoraAccount"
class=
"space-y-1.5"
>
<label
class=
"text-sm font-medium text-gray-700 dark:text-gray-300"
>
{{
t
(
'
admin.accounts.selectTestModel
'
)
}}
</label>
...
...
@@ -54,6 +54,12 @@
:placeholder=
"loadingModels ? t('common.loading') + '...' : t('admin.accounts.selectTestModel')"
/>
</div>
<div
v-else
class=
"rounded-lg border border-blue-200 bg-blue-50 px-3 py-2 text-xs text-blue-700 dark:border-blue-700 dark:bg-blue-900/20 dark:text-blue-300"
>
{{
t
(
'
admin.accounts.soraTestHint
'
)
}}
</div>
<!-- Terminal Output -->
<div
class=
"group relative"
>
...
...
@@ -114,12 +120,12 @@
<div
class=
"flex items-center gap-3"
>
<span
class=
"flex items-center gap-1"
>
<Icon
name=
"grid"
size=
"sm"
:stroke-width=
"2"
/>
{{
t
(
'
admin.accounts.testModel
'
)
}}
{{
isSoraAccount
?
t
(
'
admin.accounts.soraTestTarget
'
)
:
t
(
'
admin.accounts.testModel
'
)
}}
</span>
</div>
<span
class=
"flex items-center gap-1"
>
<Icon
name=
"chat"
size=
"sm"
:stroke-width=
"2"
/>
{{
t
(
'
admin.accounts.testPrompt
'
)
}}
{{
isSoraAccount
?
t
(
'
admin.accounts.soraTestMode
'
)
:
t
(
'
admin.accounts.testPrompt
'
)
}}
</span>
</div>
</div>
...
...
@@ -135,10 +141,10 @@
</button>
<button
@
click=
"startTest"
:disabled=
"status === 'connecting' || !selectedModelId"
:disabled=
"status === 'connecting' ||
(!isSoraAccount &&
!selectedModelId
)
"
:class=
"[
'flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition-all',
status === 'connecting' || !selectedModelId
status === 'connecting' ||
(!isSoraAccount &&
!selectedModelId
)
? 'cursor-not-allowed bg-primary-400 text-white'
: status === 'success'
? 'bg-green-500 text-white hover:bg-green-600'
...
...
@@ -172,7 +178,7 @@
</template>
<
script
setup
lang=
"ts"
>
import
{
ref
,
watch
,
nextTick
}
from
'
vue
'
import
{
computed
,
ref
,
watch
,
nextTick
}
from
'
vue
'
import
{
useI18n
}
from
'
vue-i18n
'
import
BaseDialog
from
'
@/components/common/BaseDialog.vue
'
import
Select
from
'
@/components/common/Select.vue
'
...
...
@@ -207,6 +213,7 @@ const availableModels = ref<ClaudeModel[]>([])
const
selectedModelId
=
ref
(
''
)
const
loadingModels
=
ref
(
false
)
let
eventSource
:
EventSource
|
null
=
null
const
isSoraAccount
=
computed
(()
=>
props
.
account
?.
platform
===
'
sora
'
)
// Load available models when modal opens
watch
(
...
...
@@ -223,6 +230,12 @@ watch(
const
loadAvailableModels
=
async
()
=>
{
if
(
!
props
.
account
)
return
if
(
props
.
account
.
platform
===
'
sora
'
)
{
availableModels
.
value
=
[]
selectedModelId
.
value
=
''
loadingModels
.
value
=
false
return
}
loadingModels
.
value
=
true
selectedModelId
.
value
=
''
// Reset selection before loading
...
...
@@ -290,7 +303,7 @@ const scrollToBottom = async () => {
}
const
startTest
=
async
()
=>
{
if
(
!
props
.
account
||
!
selectedModelId
.
value
)
return
if
(
!
props
.
account
||
(
!
isSoraAccount
.
value
&&
!
selectedModelId
.
value
)
)
return
resetState
()
status
.
value
=
'
connecting
'
...
...
@@ -311,7 +324,9 @@ const startTest = async () => {
Authorization
:
`Bearer
${
localStorage
.
getItem
(
'
auth_token
'
)}
`
,
'
Content-Type
'
:
'
application/json
'
},
body
:
JSON
.
stringify
({
model_id
:
selectedModelId
.
value
})
body
:
JSON
.
stringify
(
isSoraAccount
.
value
?
{}
:
{
model_id
:
selectedModelId
.
value
}
)
})
if
(
!
response
.
ok
)
{
...
...
@@ -368,7 +383,10 @@ const handleEvent = (event: {
if
(
event
.
model
)
{
addLine
(
t
(
'
admin.accounts.usingModel
'
,
{
model
:
event
.
model
}),
'
text-cyan-400
'
)
}
addLine
(
t
(
'
admin.accounts.sendingTestMessage
'
),
'
text-gray-400
'
)
addLine
(
isSoraAccount
.
value
?
t
(
'
admin.accounts.soraTestingFlow
'
)
:
t
(
'
admin.accounts.sendingTestMessage
'
),
'
text-gray-400
'
)
addLine
(
''
,
'
text-gray-300
'
)
addLine
(
t
(
'
admin.accounts.response
'
),
'
text-yellow-400
'
)
break
...
...
frontend/src/components/admin/account/ReAuthAccountModal.vue
View file @
987589ea
...
...
@@ -14,7 +14,7 @@
<div
:class=
"[
'flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-br',
isOpenAI
isOpenAI
Like
? 'from-green-500 to-green-600'
: isGemini
? 'from-blue-500 to-blue-600'
...
...
@@ -33,6 +33,8 @@
{{
isOpenAI
?
t
(
'
admin.accounts.openaiAccount
'
)
:
isSora
?
t
(
'
admin.accounts.soraAccount
'
)
:
isGemini
?
t
(
'
admin.accounts.geminiAccount
'
)
:
isAntigravity
...
...
@@ -128,7 +130,7 @@
:show-cookie-option=
"isAnthropic"
:allow-multiple=
"false"
: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'"
@
generate-url=
"handleGenerateUrl"
@
cookie-auth=
"handleCookieAuth"
...
...
@@ -224,7 +226,8 @@ const { t } = useI18n()
// OAuth composables
const
claudeOAuth
=
useAccountOAuth
()
const
openaiOAuth
=
useOpenAIOAuth
()
const
openaiOAuth
=
useOpenAIOAuth
({
platform
:
'
openai
'
})
const
soraOAuth
=
useOpenAIOAuth
({
platform
:
'
sora
'
})
const
geminiOAuth
=
useGeminiOAuth
()
const
antigravityOAuth
=
useAntigravityOAuth
()
...
...
@@ -237,31 +240,34 @@ const geminiOAuthType = ref<'code_assist' | 'google_one' | 'ai_studio'>('code_as
// Computed - check platform
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
isAnthropic
=
computed
(()
=>
props
.
account
?.
platform
===
'
anthropic
'
)
const
isAntigravity
=
computed
(()
=>
props
.
account
?.
platform
===
'
antigravity
'
)
const
activeOpenAIOAuth
=
computed
(()
=>
(
isSora
.
value
?
soraOAuth
:
openaiOAuth
))
// Computed - current OAuth state based on platform
const
currentAuthUrl
=
computed
(()
=>
{
if
(
isOpenAI
.
value
)
return
o
pen
ai
OAuth
.
authUrl
.
value
if
(
isOpenAI
Like
.
value
)
return
activeO
pen
AI
OAuth
.
value
.
authUrl
.
value
if
(
isGemini
.
value
)
return
geminiOAuth
.
authUrl
.
value
if
(
isAntigravity
.
value
)
return
antigravityOAuth
.
authUrl
.
value
return
claudeOAuth
.
authUrl
.
value
})
const
currentSessionId
=
computed
(()
=>
{
if
(
isOpenAI
.
value
)
return
o
pen
ai
OAuth
.
sessionId
.
value
if
(
isOpenAI
Like
.
value
)
return
activeO
pen
AI
OAuth
.
value
.
sessionId
.
value
if
(
isGemini
.
value
)
return
geminiOAuth
.
sessionId
.
value
if
(
isAntigravity
.
value
)
return
antigravityOAuth
.
sessionId
.
value
return
claudeOAuth
.
sessionId
.
value
})
const
currentLoading
=
computed
(()
=>
{
if
(
isOpenAI
.
value
)
return
o
pen
ai
OAuth
.
loading
.
value
if
(
isOpenAI
Like
.
value
)
return
activeO
pen
AI
OAuth
.
value
.
loading
.
value
if
(
isGemini
.
value
)
return
geminiOAuth
.
loading
.
value
if
(
isAntigravity
.
value
)
return
antigravityOAuth
.
loading
.
value
return
claudeOAuth
.
loading
.
value
})
const
currentError
=
computed
(()
=>
{
if
(
isOpenAI
.
value
)
return
o
pen
ai
OAuth
.
error
.
value
if
(
isOpenAI
Like
.
value
)
return
activeO
pen
AI
OAuth
.
value
.
error
.
value
if
(
isGemini
.
value
)
return
geminiOAuth
.
error
.
value
if
(
isAntigravity
.
value
)
return
antigravityOAuth
.
error
.
value
return
claudeOAuth
.
error
.
value
...
...
@@ -269,8 +275,8 @@ const currentError = computed(() => {
// Computed
const
isManualInputMethod
=
computed
(()
=>
{
// OpenAI/Gemini/Antigravity always use manual input (no cookie auth option)
return
isOpenAI
.
value
||
isGemini
.
value
||
isAntigravity
.
value
||
oauthFlowRef
.
value
?.
inputMethod
===
'
manual
'
// OpenAI/
Sora/
Gemini/Antigravity always use manual input (no cookie auth option)
return
isOpenAI
Like
.
value
||
isGemini
.
value
||
isAntigravity
.
value
||
oauthFlowRef
.
value
?.
inputMethod
===
'
manual
'
})
const
canExchangeCode
=
computed
(()
=>
{
...
...
@@ -313,6 +319,7 @@ const resetState = () => {
geminiOAuthType
.
value
=
'
code_assist
'
claudeOAuth
.
resetState
()
openaiOAuth
.
resetState
()
soraOAuth
.
resetState
()
geminiOAuth
.
resetState
()
antigravityOAuth
.
resetState
()
oauthFlowRef
.
value
?.
reset
()
...
...
@@ -325,8 +332,8 @@ const handleClose = () => {
const
handleGenerateUrl
=
async
()
=>
{
if
(
!
props
.
account
)
return
if
(
isOpenAI
.
value
)
{
await
o
pen
ai
OAuth
.
generateAuthUrl
(
props
.
account
.
proxy_id
)
if
(
isOpenAI
Like
.
value
)
{
await
activeO
pen
AI
OAuth
.
value
.
generateAuthUrl
(
props
.
account
.
proxy_id
)
}
else
if
(
isGemini
.
value
)
{
const
creds
=
(
props
.
account
.
credentials
||
{})
as
Record
<
string
,
unknown
>
const
tierId
=
typeof
creds
.
tier_id
===
'
string
'
?
creds
.
tier_id
:
undefined
...
...
@@ -345,21 +352,29 @@ const handleExchangeCode = async () => {
const
authCode
=
oauthFlowRef
.
value
?.
authCode
||
''
if
(
!
authCode
.
trim
())
return
if
(
isOpenAI
.
value
)
{
if
(
isOpenAI
Like
.
value
)
{
// OpenAI OAuth flow
const
sessionId
=
openaiOAuth
.
sessionId
.
value
const
oauthClient
=
activeOpenAIOAuth
.
value
const
sessionId
=
oauthClient
.
sessionId
.
value
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
o
penaiOAuth
.
exchangeAuthCode
(
const
tokenInfo
=
await
o
authClient
.
exchangeAuthCode
(
authCode
.
trim
(),
sessionId
,
stateToUse
,
props
.
account
.
proxy_id
)
if
(
!
tokenInfo
)
return
// Build credentials and extra info
const
credentials
=
o
penaiOAuth
.
buildCredentials
(
tokenInfo
)
const
extra
=
o
penaiOAuth
.
buildExtraInfo
(
tokenInfo
)
const
credentials
=
o
authClient
.
buildCredentials
(
tokenInfo
)
const
extra
=
o
authClient
.
buildExtraInfo
(
tokenInfo
)
try
{
// Update account with new credentials
...
...
@@ -376,8 +391,8 @@ const handleExchangeCode = async () => {
emit
(
'
reauthorized
'
,
updatedAccount
)
handleClose
()
}
catch
(
error
:
any
)
{
o
penaiOAuth
.
error
.
value
=
error
.
response
?.
data
?.
detail
||
t
(
'
admin.accounts.oauth.authFailed
'
)
appStore
.
showError
(
o
penaiOAuth
.
error
.
value
)
o
authClient
.
error
.
value
=
error
.
response
?.
data
?.
detail
||
t
(
'
admin.accounts.oauth.authFailed
'
)
appStore
.
showError
(
o
authClient
.
error
.
value
)
}
}
else
if
(
isGemini
.
value
)
{
const
sessionId
=
geminiOAuth
.
sessionId
.
value
...
...
@@ -490,7 +505,7 @@ const handleExchangeCode = async () => {
}
const
handleCookieAuth
=
async
(
sessionKey
:
string
)
=>
{
if
(
!
props
.
account
||
isOpenAI
.
value
)
return
if
(
!
props
.
account
||
isOpenAI
Like
.
value
)
return
claudeOAuth
.
loading
.
value
=
true
claudeOAuth
.
error
.
value
=
''
...
...
frontend/src/components/admin/usage/UsageTable.vue
View file @
987589ea
...
...
@@ -70,6 +70,8 @@
<div
v-if=
"row.cache_creation_tokens > 0"
class=
"inline-flex items-center gap-1"
>
<svg
class=
"h-3.5 w-3.5 text-amber-500"
fill=
"none"
stroke=
"currentColor"
viewBox=
"0 0 24 24"
><path
stroke-linecap=
"round"
stroke-linejoin=
"round"
stroke-width=
"2"
d=
"M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
/></svg>
<span
class=
"font-medium text-amber-600 dark:text-amber-400"
>
{{
formatCacheTokens
(
row
.
cache_creation_tokens
)
}}
</span>
<span
v-if=
"row.cache_creation_1h_tokens > 0"
class=
"inline-flex items-center rounded px-1 py-px text-[10px] font-medium leading-tight bg-orange-100 text-orange-600 ring-1 ring-inset ring-orange-200 dark:bg-orange-500/20 dark:text-orange-400 dark:ring-orange-500/30"
>
1h
</span>
<span
v-if=
"row.cache_ttl_overridden"
:title=
"t('usage.cacheTtlOverriddenHint')"
class=
"inline-flex items-center rounded px-1 py-px text-[10px] font-medium leading-tight bg-rose-100 text-rose-600 ring-1 ring-inset ring-rose-200 dark:bg-rose-500/20 dark:text-rose-400 dark:ring-rose-500/30 cursor-help"
>
R
</span>
</div>
</div>
</div>
...
...
@@ -157,10 +159,37 @@
<span
class=
"text-gray-400"
>
{{ t('admin.usage.outputTokens') }}
</span>
<span
class=
"font-medium text-white"
>
{{ tokenTooltipData.output_tokens.toLocaleString() }}
</span>
</div>
<div
v-if=
"tokenTooltipData && tokenTooltipData.cache_creation_tokens > 0"
class=
"flex items-center justify-between gap-4"
>
<div
v-if=
"tokenTooltipData && tokenTooltipData.cache_creation_tokens > 0"
>
<!-- 有 5m/1h 明细时,展开显示 -->
<
template
v-if=
"tokenTooltipData.cache_creation_5m_tokens > 0 || tokenTooltipData.cache_creation_1h_tokens > 0"
>
<div
v-if=
"tokenTooltipData.cache_creation_5m_tokens > 0"
class=
"flex items-center justify-between gap-4"
>
<span
class=
"text-gray-400 flex items-center gap-1.5"
>
{{
t
(
'
admin.usage.cacheCreation5mTokens
'
)
}}
<span
class=
"inline-flex items-center rounded px-1 py-px text-[10px] font-medium leading-tight bg-amber-500/20 text-amber-400 ring-1 ring-inset ring-amber-500/30"
>
5m
</span>
</span>
<span
class=
"font-medium text-white"
>
{{
tokenTooltipData
.
cache_creation_5m_tokens
.
toLocaleString
()
}}
</span>
</div>
<div
v-if=
"tokenTooltipData.cache_creation_1h_tokens > 0"
class=
"flex items-center justify-between gap-4"
>
<span
class=
"text-gray-400 flex items-center gap-1.5"
>
{{
t
(
'
admin.usage.cacheCreation1hTokens
'
)
}}
<span
class=
"inline-flex items-center rounded px-1 py-px text-[10px] font-medium leading-tight bg-orange-500/20 text-orange-400 ring-1 ring-inset ring-orange-500/30"
>
1h
</span>
</span>
<span
class=
"font-medium text-white"
>
{{
tokenTooltipData
.
cache_creation_1h_tokens
.
toLocaleString
()
}}
</span>
</div>
</
template
>
<!-- 无明细时,只显示聚合值 -->
<div
v-else
class=
"flex items-center justify-between gap-4"
>
<span
class=
"text-gray-400"
>
{{ t('admin.usage.cacheCreationTokens') }}
</span>
<span
class=
"font-medium text-white"
>
{{ tokenTooltipData.cache_creation_tokens.toLocaleString() }}
</span>
</div>
</div>
<div
v-if=
"tokenTooltipData && tokenTooltipData.cache_ttl_overridden"
class=
"flex items-center justify-between gap-4"
>
<span
class=
"text-gray-400 flex items-center gap-1.5"
>
{{ t('usage.cacheTtlOverriddenLabel') }}
<span
class=
"inline-flex items-center rounded px-1 py-px text-[10px] font-medium leading-tight bg-rose-500/20 text-rose-400 ring-1 ring-inset ring-rose-500/30"
>
R-{{ tokenTooltipData.cache_creation_1h_tokens > 0 ? '5m' : '1H' }}
</span>
</span>
<span
class=
"font-medium text-rose-400"
>
{{ tokenTooltipData.cache_creation_1h_tokens > 0 ? t('usage.cacheTtlOverridden1h') : t('usage.cacheTtlOverridden5m') }}
</span>
</div>
<div
v-if=
"tokenTooltipData && tokenTooltipData.cache_read_tokens > 0"
class=
"flex items-center justify-between gap-4"
>
<span
class=
"text-gray-400"
>
{{ t('admin.usage.cacheReadTokens') }}
</span>
<span
class=
"font-medium text-white"
>
{{ tokenTooltipData.cache_read_tokens.toLocaleString() }}
</span>
...
...
frontend/src/components/common/StatCard.vue
View file @
987589ea
...
...
@@ -6,7 +6,7 @@
<div
class=
"min-w-0 flex-1"
>
<p
class=
"stat-label truncate"
>
{{
title
}}
</p>
<div
class=
"mt-1 flex items-baseline gap-2"
>
<p
class=
"stat-value"
>
{{
formattedValue
}}
</p>
<p
class=
"stat-value"
:title=
"String(formattedValue)"
>
{{
formattedValue
}}
</p>
<span
v-if=
"change !== undefined"
:class=
"['stat-trend', trendClass]"
>
<Icon
v-if=
"changeType !== 'neutral'"
...
...
frontend/src/components/layout/AppSidebar.vue
View file @
987589ea
...
...
@@ -10,7 +10,7 @@
<div
class=
"sidebar-header"
>
<!-- Custom Logo or Default Logo -->
<div
class=
"flex h-9 w-9 items-center justify-center overflow-hidden rounded-xl shadow-glow"
>
<img
:src=
"siteLogo || '/logo.png'"
alt=
"Logo"
class=
"h-full w-full object-contain"
/>
<img
v-if=
"settingsLoaded"
:src=
"siteLogo || '/logo.png'"
alt=
"Logo"
class=
"h-full w-full object-contain"
/>
</div>
<transition
name=
"fade"
>
<div
v-if=
"!sidebarCollapsed"
class=
"flex flex-col"
>
...
...
@@ -167,6 +167,7 @@ const isDark = ref(document.documentElement.classList.contains('dark'))
const
siteName
=
computed
(()
=>
appStore
.
siteName
)
const
siteLogo
=
computed
(()
=>
appStore
.
siteLogo
)
const
siteVersion
=
computed
(()
=>
appStore
.
siteVersion
)
const
settingsLoaded
=
computed
(()
=>
appStore
.
publicSettingsLoaded
)
// SVG Icon Components
const
DashboardIcon
=
{
...
...
frontend/src/composables/useAccountOAuth.ts
View file @
987589ea
...
...
@@ -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
'
export
type
AuthInputMethod
=
'
manual
'
|
'
cookie
'
|
'
refresh_token
'
|
'
session_token
'
export
interface
OAuthState
{
authUrl
:
string
...
...
Prev
1
2
3
4
5
6
Next
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