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
Hide 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:
...
@@ -374,6 +374,9 @@ sora:
# Max retries for upstream requests
# Max retries for upstream requests
# 上游请求最大重试次数
# 上游请求最大重试次数
max_retries
:
3
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)
# 轮询间隔(秒)
# 轮询间隔(秒)
poll_interval_seconds
:
2
poll_interval_seconds
:
2
...
@@ -388,7 +391,11 @@ sora:
...
@@ -388,7 +391,11 @@ sora:
recent_task_limit_max
:
200
recent_task_limit_max
:
200
# Enable debug logs for Sora upstream requests
# Enable debug logs for Sora upstream requests
# 启用 Sora 直连调试日志
# 启用 Sora 直连调试日志
# 调试日志会输出上游请求尝试、重试、响应摘要;Authorization/openai-sentinel-token 等敏感头会自动脱敏
debug
:
false
debug
:
false
# Allow Sora client to fetch token via OpenAI token provider
# 是否允许 Sora 客户端通过 OpenAI token provider 取 token(默认 false,避免误走 OpenAI 刷新链路)
use_openai_token_provider
:
false
# Optional custom headers (key-value)
# Optional custom headers (key-value)
# 额外请求头(键值对)
# 额外请求头(键值对)
headers
:
{}
headers
:
{}
...
@@ -398,6 +405,27 @@ sora:
...
@@ -398,6 +405,27 @@ sora:
# Disable TLS fingerprint for Sora upstream
# Disable TLS fingerprint for Sora upstream
# 关闭 Sora 上游 TLS 指纹伪装
# 关闭 Sora 上游 TLS 指纹伪装
disable_tls_fingerprint
:
false
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
:
# Storage type (local only for now)
# Storage type (local only for now)
# 存储类型(首发仅支持 local)
# 存储类型(首发仅支持 local)
...
@@ -431,6 +459,13 @@ sora:
...
@@ -431,6 +459,13 @@ sora:
# Cron 调度表达式
# Cron 调度表达式
schedule
:
"
0
3
*
*
*"
schedule
:
"
0
3
*
*
*"
# Token refresh behavior
# token 刷新行为控制
token_refresh
:
# Whether OpenAI refresh flow is allowed to sync linked Sora accounts
# 是否允许 OpenAI 刷新流程同步覆盖 linked_openai_account_id 关联的 Sora 账号 token
sync_linked_sora_accounts
:
false
# =============================================================================
# =============================================================================
# API Key Auth Cache Configuration
# API Key Auth Cache Configuration
# API Key 认证缓存配置
# API Key 认证缓存配置
...
...
deploy/docker-compose.yml
View file @
987589ea
...
@@ -173,6 +173,7 @@ services:
...
@@ -173,6 +173,7 @@ services:
-
POSTGRES_USER=${POSTGRES_USER:-sub2api}
-
POSTGRES_USER=${POSTGRES_USER:-sub2api}
-
POSTGRES_PASSWORD=${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}
-
POSTGRES_PASSWORD=${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}
-
POSTGRES_DB=${POSTGRES_DB:-sub2api}
-
POSTGRES_DB=${POSTGRES_DB:-sub2api}
-
PGDATA=/var/lib/postgresql/data
-
TZ=${TZ:-Asia/Shanghai}
-
TZ=${TZ:-Asia/Shanghai}
networks
:
networks
:
-
sub2api-network
-
sub2api-network
...
...
frontend/src/api/admin/accounts.ts
View file @
987589ea
...
@@ -32,6 +32,7 @@ export async function list(
...
@@ -32,6 +32,7 @@ export async function list(
platform
?:
string
platform
?:
string
type
?:
string
type
?:
string
status
?:
string
status
?:
string
group
?:
string
search
?:
string
search
?:
string
},
},
options
?:
{
options
?:
{
...
@@ -271,7 +272,7 @@ export async function generateAuthUrl(
...
@@ -271,7 +272,7 @@ export async function generateAuthUrl(
*/
*/
export
async
function
exchangeCode
(
export
async
function
exchangeCode
(
endpoint
:
string
,
endpoint
:
string
,
exchangeData
:
{
session_id
:
string
;
code
:
string
;
proxy_id
?:
number
}
exchangeData
:
{
session_id
:
string
;
code
:
string
;
state
?:
string
;
proxy_id
?:
number
}
):
Promise
<
Record
<
string
,
unknown
>>
{
):
Promise
<
Record
<
string
,
unknown
>>
{
const
{
data
}
=
await
apiClient
.
post
<
Record
<
string
,
unknown
>>
(
endpoint
,
exchangeData
)
const
{
data
}
=
await
apiClient
.
post
<
Record
<
string
,
unknown
>>
(
endpoint
,
exchangeData
)
return
data
return
data
...
@@ -493,7 +494,8 @@ export async function getAntigravityDefaultModelMapping(): Promise<Record<string
...
@@ -493,7 +494,8 @@ export async function getAntigravityDefaultModelMapping(): Promise<Record<string
*/
*/
export
async
function
refreshOpenAIToken
(
export
async
function
refreshOpenAIToken
(
refreshToken
:
string
,
refreshToken
:
string
,
proxyId
?:
number
|
null
proxyId
?:
number
|
null
,
endpoint
:
string
=
'
/admin/openai/refresh-token
'
):
Promise
<
Record
<
string
,
unknown
>>
{
):
Promise
<
Record
<
string
,
unknown
>>
{
const
payload
:
{
refresh_token
:
string
;
proxy_id
?:
number
}
=
{
const
payload
:
{
refresh_token
:
string
;
proxy_id
?:
number
}
=
{
refresh_token
:
refreshToken
refresh_token
:
refreshToken
...
@@ -501,7 +503,29 @@ export async function refreshOpenAIToken(
...
@@ -501,7 +503,29 @@ export async function refreshOpenAIToken(
if
(
proxyId
)
{
if
(
proxyId
)
{
payload
.
proxy_id
=
proxyId
payload
.
proxy_id
=
proxyId
}
}
const
{
data
}
=
await
apiClient
.
post
<
Record
<
string
,
unknown
>>
(
'
/admin/openai/refresh-token
'
,
payload
)
const
{
data
}
=
await
apiClient
.
post
<
Record
<
string
,
unknown
>>
(
endpoint
,
payload
)
return
data
}
/**
* Validate Sora session token and exchange to access token
* @param sessionToken - Sora session token
* @param proxyId - Optional proxy ID
* @param endpoint - API endpoint path
* @returns Token information including access_token
*/
export
async
function
validateSoraSessionToken
(
sessionToken
:
string
,
proxyId
?:
number
|
null
,
endpoint
:
string
=
'
/admin/sora/st2at
'
):
Promise
<
Record
<
string
,
unknown
>>
{
const
payload
:
{
session_token
:
string
;
proxy_id
?:
number
}
=
{
session_token
:
sessionToken
}
if
(
proxyId
)
{
payload
.
proxy_id
=
proxyId
}
const
{
data
}
=
await
apiClient
.
post
<
Record
<
string
,
unknown
>>
(
endpoint
,
payload
)
return
data
return
data
}
}
...
@@ -527,6 +551,7 @@ export const accountsAPI = {
...
@@ -527,6 +551,7 @@ export const accountsAPI = {
generateAuthUrl
,
generateAuthUrl
,
exchangeCode
,
exchangeCode
,
refreshOpenAIToken
,
refreshOpenAIToken
,
validateSoraSessionToken
,
batchCreate
,
batchCreate
,
batchUpdateCredentials
,
batchUpdateCredentials
,
bulkUpdate
,
bulkUpdate
,
...
...
frontend/src/api/admin/proxies.ts
View file @
987589ea
...
@@ -7,6 +7,7 @@ import { apiClient } from '../client'
...
@@ -7,6 +7,7 @@ import { apiClient } from '../client'
import
type
{
import
type
{
Proxy
,
Proxy
,
ProxyAccountSummary
,
ProxyAccountSummary
,
ProxyQualityCheckResult
,
CreateProxyRequest
,
CreateProxyRequest
,
UpdateProxyRequest
,
UpdateProxyRequest
,
PaginatedResponse
,
PaginatedResponse
,
...
@@ -143,6 +144,16 @@ export async function testProxy(id: number): Promise<{
...
@@ -143,6 +144,16 @@ export async function testProxy(id: number): Promise<{
return
data
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
* Get proxy usage statistics
* @param id - Proxy ID
* @param id - Proxy ID
...
@@ -248,6 +259,7 @@ export const proxiesAPI = {
...
@@ -248,6 +259,7 @@ export const proxiesAPI = {
delete
:
deleteProxy
,
delete
:
deleteProxy
,
toggleStatus
,
toggleStatus
,
testProxy
,
testProxy
,
checkProxyQuality
,
getStats
,
getStats
,
getProxyAccounts
,
getProxyAccounts
,
batchCreate
,
batchCreate
,
...
...
frontend/src/components/account/AccountGroupsCell.vue
View file @
987589ea
...
@@ -41,7 +41,7 @@
...
@@ -41,7 +41,7 @@
>
>
<div
class=
"mb-2 flex items-center justify-between"
>
<div
class=
"mb-2 flex items-center justify-between"
>
<span
class=
"text-xs font-medium text-gray-500 dark:text-gray-400"
>
<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
>
<
/span
>
<
button
<
button
@
click
=
"
showPopover = false
"
@
click
=
"
showPopover = false
"
...
...
frontend/src/components/account/AccountTestModal.vue
View file @
987589ea
...
@@ -41,7 +41,7 @@
...
@@ -41,7 +41,7 @@
</span>
</span>
</div>
</div>
<div
class=
"space-y-1.5"
>
<div
v-if=
"!isSoraAccount"
class=
"space-y-1.5"
>
<label
class=
"text-sm font-medium text-gray-700 dark:text-gray-300"
>
<label
class=
"text-sm font-medium text-gray-700 dark:text-gray-300"
>
{{
t
(
'
admin.accounts.selectTestModel
'
)
}}
{{
t
(
'
admin.accounts.selectTestModel
'
)
}}
</label>
</label>
...
@@ -54,6 +54,12 @@
...
@@ -54,6 +54,12 @@
:placeholder=
"loadingModels ? t('common.loading') + '...' : t('admin.accounts.selectTestModel')"
:placeholder=
"loadingModels ? t('common.loading') + '...' : t('admin.accounts.selectTestModel')"
/>
/>
</div>
</div>
<div
v-else
class=
"rounded-lg border border-blue-200 bg-blue-50 px-3 py-2 text-xs text-blue-700 dark:border-blue-700 dark:bg-blue-900/20 dark:text-blue-300"
>
{{
t
(
'
admin.accounts.soraTestHint
'
)
}}
</div>
<!-- Terminal Output -->
<!-- Terminal Output -->
<div
class=
"group relative"
>
<div
class=
"group relative"
>
...
@@ -135,12 +141,12 @@
...
@@ -135,12 +141,12 @@
<div
class=
"flex items-center gap-3"
>
<div
class=
"flex items-center gap-3"
>
<span
class=
"flex items-center gap-1"
>
<span
class=
"flex items-center gap-1"
>
<Icon
name=
"cpu"
size=
"sm"
:stroke-width=
"2"
/>
<Icon
name=
"cpu"
size=
"sm"
:stroke-width=
"2"
/>
{{
t
(
'
admin.accounts.testModel
'
)
}}
{{
isSoraAccount
?
t
(
'
admin.accounts.soraTestTarget
'
)
:
t
(
'
admin.accounts.testModel
'
)
}}
</span>
</span>
</div>
</div>
<span
class=
"flex items-center gap-1"
>
<span
class=
"flex items-center gap-1"
>
<Icon
name=
"chatBubble"
size=
"sm"
:stroke-width=
"2"
/>
<Icon
name=
"chatBubble"
size=
"sm"
:stroke-width=
"2"
/>
{{
t
(
'
admin.accounts.testPrompt
'
)
}}
{{
isSoraAccount
?
t
(
'
admin.accounts.soraTestMode
'
)
:
t
(
'
admin.accounts.testPrompt
'
)
}}
</span>
</span>
</div>
</div>
</div>
</div>
...
@@ -156,10 +162,10 @@
...
@@ -156,10 +162,10 @@
</button>
</button>
<button
<button
@
click=
"startTest"
@
click=
"startTest"
:disabled=
"status === 'connecting' || !selectedModelId"
:disabled=
"status === 'connecting' ||
(!isSoraAccount &&
!selectedModelId
)
"
:class=
"[
:class=
"[
'flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition-all',
'flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition-all',
status === 'connecting' || !selectedModelId
status === 'connecting' ||
(!isSoraAccount &&
!selectedModelId
)
? 'cursor-not-allowed bg-primary-400 text-white'
? 'cursor-not-allowed bg-primary-400 text-white'
: status === 'success'
: status === 'success'
? 'bg-green-500 text-white hover:bg-green-600'
? 'bg-green-500 text-white hover:bg-green-600'
...
@@ -232,7 +238,7 @@
...
@@ -232,7 +238,7 @@
</template>
</template>
<
script
setup
lang=
"ts"
>
<
script
setup
lang=
"ts"
>
import
{
ref
,
watch
,
nextTick
}
from
'
vue
'
import
{
computed
,
ref
,
watch
,
nextTick
}
from
'
vue
'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
useI18n
}
from
'
vue-i18n
'
import
BaseDialog
from
'
@/components/common/BaseDialog.vue
'
import
BaseDialog
from
'
@/components/common/BaseDialog.vue
'
import
Select
from
'
@/components/common/Select.vue
'
import
Select
from
'
@/components/common/Select.vue
'
...
@@ -267,6 +273,7 @@ const availableModels = ref<ClaudeModel[]>([])
...
@@ -267,6 +273,7 @@ const availableModels = ref<ClaudeModel[]>([])
const
selectedModelId
=
ref
(
''
)
const
selectedModelId
=
ref
(
''
)
const
loadingModels
=
ref
(
false
)
const
loadingModels
=
ref
(
false
)
let
eventSource
:
EventSource
|
null
=
null
let
eventSource
:
EventSource
|
null
=
null
const
isSoraAccount
=
computed
(()
=>
props
.
account
?.
platform
===
'
sora
'
)
// Load available models when modal opens
// Load available models when modal opens
watch
(
watch
(
...
@@ -283,6 +290,12 @@ watch(
...
@@ -283,6 +290,12 @@ watch(
const
loadAvailableModels
=
async
()
=>
{
const
loadAvailableModels
=
async
()
=>
{
if
(
!
props
.
account
)
return
if
(
!
props
.
account
)
return
if
(
props
.
account
.
platform
===
'
sora
'
)
{
availableModels
.
value
=
[]
selectedModelId
.
value
=
''
loadingModels
.
value
=
false
return
}
loadingModels
.
value
=
true
loadingModels
.
value
=
true
selectedModelId
.
value
=
''
// Reset selection before loading
selectedModelId
.
value
=
''
// Reset selection before loading
...
@@ -350,7 +363,7 @@ const scrollToBottom = async () => {
...
@@ -350,7 +363,7 @@ const scrollToBottom = async () => {
}
}
const
startTest
=
async
()
=>
{
const
startTest
=
async
()
=>
{
if
(
!
props
.
account
||
!
selectedModelId
.
value
)
return
if
(
!
props
.
account
||
(
!
isSoraAccount
.
value
&&
!
selectedModelId
.
value
)
)
return
resetState
()
resetState
()
status
.
value
=
'
connecting
'
status
.
value
=
'
connecting
'
...
@@ -371,7 +384,9 @@ const startTest = async () => {
...
@@ -371,7 +384,9 @@ const startTest = async () => {
Authorization
:
`Bearer
${
localStorage
.
getItem
(
'
auth_token
'
)}
`
,
Authorization
:
`Bearer
${
localStorage
.
getItem
(
'
auth_token
'
)}
`
,
'
Content-Type
'
:
'
application/json
'
'
Content-Type
'
:
'
application/json
'
},
},
body
:
JSON
.
stringify
({
model_id
:
selectedModelId
.
value
})
body
:
JSON
.
stringify
(
isSoraAccount
.
value
?
{}
:
{
model_id
:
selectedModelId
.
value
}
)
})
})
if
(
!
response
.
ok
)
{
if
(
!
response
.
ok
)
{
...
@@ -428,7 +443,10 @@ const handleEvent = (event: {
...
@@ -428,7 +443,10 @@ const handleEvent = (event: {
if
(
event
.
model
)
{
if
(
event
.
model
)
{
addLine
(
t
(
'
admin.accounts.usingModel
'
,
{
model
:
event
.
model
}),
'
text-cyan-400
'
)
addLine
(
t
(
'
admin.accounts.usingModel
'
,
{
model
:
event
.
model
}),
'
text-cyan-400
'
)
}
}
addLine
(
t
(
'
admin.accounts.sendingTestMessage
'
),
'
text-gray-400
'
)
addLine
(
isSoraAccount
.
value
?
t
(
'
admin.accounts.soraTestingFlow
'
)
:
t
(
'
admin.accounts.sendingTestMessage
'
),
'
text-gray-400
'
)
addLine
(
''
,
'
text-gray-300
'
)
addLine
(
''
,
'
text-gray-300
'
)
addLine
(
t
(
'
admin.accounts.response
'
),
'
text-yellow-400
'
)
addLine
(
t
(
'
admin.accounts.response
'
),
'
text-yellow-400
'
)
break
break
...
...
frontend/src/components/account/BulkEditAccountModal.vue
View file @
987589ea
...
@@ -710,6 +710,7 @@ const groupIds = ref<number[]>([])
...
@@ -710,6 +710,7 @@ const groupIds = ref<number[]>([])
// All models list (combined Anthropic + OpenAI)
// All models list (combined Anthropic + OpenAI)
const
allModels
=
[
const
allModels
=
[
{
value
:
'
claude-opus-4-6
'
,
label
:
'
Claude Opus 4.6
'
}
,
{
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-opus-4-5-20251101
'
,
label
:
'
Claude Opus 4.5
'
}
,
{
value
:
'
claude-sonnet-4-20250514
'
,
label
:
'
Claude Sonnet 4
'
}
,
{
value
:
'
claude-sonnet-4-20250514
'
,
label
:
'
Claude Sonnet 4
'
}
,
{
value
:
'
claude-sonnet-4-5-20250929
'
,
label
:
'
Claude Sonnet 4.5
'
}
,
{
value
:
'
claude-sonnet-4-5-20250929
'
,
label
:
'
Claude Sonnet 4.5
'
}
,
...
@@ -757,6 +758,13 @@ const presetMappings = [
...
@@ -757,6 +758,13 @@ const presetMappings = [
color
:
color
:
'
bg-purple-100 text-purple-700 hover:bg-purple-200 dark:bg-purple-900/30 dark:text-purple-400
'
'
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
'
,
label
:
'
Opus->Sonnet
'
,
from
:
'
claude-opus-4-5-20251101
'
,
from
:
'
claude-opus-4-5-20251101
'
,
...
...
frontend/src/components/account/CreateAccountModal.vue
View file @
987589ea
...
@@ -109,6 +109,28 @@
...
@@ -109,6 +109,28 @@
</svg>
</svg>
OpenAI
OpenAI
</button>
</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
<button
type=
"button"
type=
"button"
@
click=
"form.platform = 'gemini'"
@
click=
"form.platform = 'gemini'"
...
@@ -150,6 +172,38 @@
...
@@ -150,6 +172,38 @@
</div>
</div>
</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) -->
<!-- Account Type Selection (Anthropic) -->
<div
v-if=
"form.platform === 'anthropic'"
>
<div
v-if=
"form.platform === 'anthropic'"
>
<label
class=
"input-label"
>
{{
t
(
'
admin.accounts.accountType
'
)
}}
</label>
<label
class=
"input-label"
>
{{
t
(
'
admin.accounts.accountType
'
)
}}
</label>
...
@@ -1538,6 +1592,46 @@
...
@@ -1538,6 +1592,46 @@
<
/button
>
<
/button
>
<
/div
>
<
/div
>
<
/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
>
<
div
>
<
div
>
...
@@ -1707,32 +1801,6 @@
...
@@ -1707,32 +1801,6 @@
<!--
Step
2
:
OAuth
Authorization
-->
<!--
Step
2
:
OAuth
Authorization
-->
<
div
v
-
else
class
=
"
space-y-5
"
>
<
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
<
OAuthAuthorizationFlow
ref
=
"
oauthFlowRef
"
ref
=
"
oauthFlowRef
"
:
add
-
method
=
"
form.platform === 'anthropic' ? addMethod : 'oauth'
"
:
add
-
method
=
"
form.platform === 'anthropic' ? addMethod : 'oauth'
"
...
@@ -1741,15 +1809,17 @@
...
@@ -1741,15 +1809,17 @@
:
loading
=
"
currentOAuthLoading
"
:
loading
=
"
currentOAuthLoading
"
:
error
=
"
currentOAuthError
"
:
error
=
"
currentOAuthError
"
:
show
-
help
=
"
form.platform === 'anthropic'
"
:
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'
"
:
allow
-
multiple
=
"
form.platform === 'anthropic'
"
:
show
-
cookie
-
option
=
"
form.platform === 'anthropic'
"
:
show
-
cookie
-
option
=
"
form.platform === 'anthropic'
"
:
show
-
refresh
-
token
-
option
=
"
form.platform === 'openai' || form.platform === 'antigravity'
"
:
show
-
refresh
-
token
-
option
=
"
form.platform === 'openai' || form.platform === 'sora' || form.platform === 'antigravity'
"
:
show
-
session
-
token
-
option
=
"
form.platform === 'sora'
"
:
platform
=
"
form.platform
"
:
platform
=
"
form.platform
"
:
show
-
project
-
id
=
"
geminiOAuthType === 'code_assist'
"
:
show
-
project
-
id
=
"
geminiOAuthType === 'code_assist'
"
@
generate
-
url
=
"
handleGenerateUrl
"
@
generate
-
url
=
"
handleGenerateUrl
"
@
cookie
-
auth
=
"
handleCookieAuth
"
@
cookie
-
auth
=
"
handleCookieAuth
"
@
validate
-
refresh
-
token
=
"
handleValidateRefreshToken
"
@
validate
-
refresh
-
token
=
"
handleValidateRefreshToken
"
@
validate
-
session
-
token
=
"
handleValidateSessionToken
"
/>
/>
<
/div
>
<
/div
>
...
@@ -2108,6 +2178,7 @@ interface OAuthFlowExposed {
...
@@ -2108,6 +2178,7 @@ interface OAuthFlowExposed {
projectId
:
string
projectId
:
string
sessionKey
:
string
sessionKey
:
string
refreshToken
:
string
refreshToken
:
string
sessionToken
:
string
inputMethod
:
AuthInputMethod
inputMethod
:
AuthInputMethod
reset
:
()
=>
void
reset
:
()
=>
void
}
}
...
@@ -2116,7 +2187,7 @@ const { t } = useI18n()
...
@@ -2116,7 +2187,7 @@ const { t } = useI18n()
const
authStore
=
useAuthStore
()
const
authStore
=
useAuthStore
()
const
oauthStepTitle
=
computed
(()
=>
{
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
===
'
gemini
'
)
return
t
(
'
admin.accounts.oauth.gemini.title
'
)
if
(
form
.
platform
===
'
antigravity
'
)
return
t
(
'
admin.accounts.oauth.antigravity.title
'
)
if
(
form
.
platform
===
'
antigravity
'
)
return
t
(
'
admin.accounts.oauth.antigravity.title
'
)
return
t
(
'
admin.accounts.oauth.title
'
)
return
t
(
'
admin.accounts.oauth.title
'
)
...
@@ -2124,13 +2195,13 @@ const oauthStepTitle = computed(() => {
...
@@ -2124,13 +2195,13 @@ const oauthStepTitle = computed(() => {
// Platform-specific hints for API Key type
// Platform-specific hints for API Key type
const
baseUrlHint
=
computed
(()
=>
{
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
'
)
if
(
form
.
platform
===
'
gemini
'
)
return
t
(
'
admin.accounts.gemini.baseUrlHint
'
)
return
t
(
'
admin.accounts.baseUrlHint
'
)
return
t
(
'
admin.accounts.baseUrlHint
'
)
}
)
}
)
const
apiKeyHint
=
computed
(()
=>
{
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
'
)
if
(
form
.
platform
===
'
gemini
'
)
return
t
(
'
admin.accounts.gemini.apiKeyHint
'
)
return
t
(
'
admin.accounts.apiKeyHint
'
)
return
t
(
'
admin.accounts.apiKeyHint
'
)
}
)
}
)
...
@@ -2151,34 +2222,36 @@ const appStore = useAppStore()
...
@@ -2151,34 +2222,36 @@ const appStore = useAppStore()
// OAuth composables
// OAuth composables
const
oauth
=
useAccountOAuth
()
// For Anthropic OAuth
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
geminiOAuth
=
useGeminiOAuth
()
// For Gemini OAuth
const
antigravityOAuth
=
useAntigravityOAuth
()
// For Antigravity OAuth
const
antigravityOAuth
=
useAntigravityOAuth
()
// For Antigravity OAuth
const
activeOpenAIOAuth
=
computed
(()
=>
(
form
.
platform
===
'
sora
'
?
soraOAuth
:
openaiOAuth
))
// Computed: current OAuth state for template binding
// Computed: current OAuth state for template binding
const
currentAuthUrl
=
computed
(()
=>
{
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
===
'
gemini
'
)
return
geminiOAuth
.
authUrl
.
value
if
(
form
.
platform
===
'
antigravity
'
)
return
antigravityOAuth
.
authUrl
.
value
if
(
form
.
platform
===
'
antigravity
'
)
return
antigravityOAuth
.
authUrl
.
value
return
oauth
.
authUrl
.
value
return
oauth
.
authUrl
.
value
}
)
}
)
const
currentSessionId
=
computed
(()
=>
{
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
===
'
gemini
'
)
return
geminiOAuth
.
sessionId
.
value
if
(
form
.
platform
===
'
antigravity
'
)
return
antigravityOAuth
.
sessionId
.
value
if
(
form
.
platform
===
'
antigravity
'
)
return
antigravityOAuth
.
sessionId
.
value
return
oauth
.
sessionId
.
value
return
oauth
.
sessionId
.
value
}
)
}
)
const
currentOAuthLoading
=
computed
(()
=>
{
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
===
'
gemini
'
)
return
geminiOAuth
.
loading
.
value
if
(
form
.
platform
===
'
antigravity
'
)
return
antigravityOAuth
.
loading
.
value
if
(
form
.
platform
===
'
antigravity
'
)
return
antigravityOAuth
.
loading
.
value
return
oauth
.
loading
.
value
return
oauth
.
loading
.
value
}
)
}
)
const
currentOAuthError
=
computed
(()
=>
{
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
===
'
gemini
'
)
return
geminiOAuth
.
error
.
value
if
(
form
.
platform
===
'
antigravity
'
)
return
antigravityOAuth
.
error
.
value
if
(
form
.
platform
===
'
antigravity
'
)
return
antigravityOAuth
.
error
.
value
return
oauth
.
error
.
value
return
oauth
.
error
.
value
...
@@ -2217,7 +2290,6 @@ const interceptWarmupRequests = ref(false)
...
@@ -2217,7 +2290,6 @@ const interceptWarmupRequests = ref(false)
const
autoPauseOnExpired
=
ref
(
true
)
const
autoPauseOnExpired
=
ref
(
true
)
const
openaiPassthroughEnabled
=
ref
(
false
)
const
openaiPassthroughEnabled
=
ref
(
false
)
const
codexCLIOnlyEnabled
=
ref
(
false
)
const
codexCLIOnlyEnabled
=
ref
(
false
)
const
enableSoraOnOpenAIOAuth
=
ref
(
false
)
// OpenAI OAuth 时同时启用 Sora
const
mixedScheduling
=
ref
(
false
)
// For antigravity accounts: enable mixed scheduling
const
mixedScheduling
=
ref
(
false
)
// For antigravity accounts: enable mixed scheduling
const
antigravityAccountType
=
ref
<
'
oauth
'
|
'
upstream
'
>
(
'
oauth
'
)
// For antigravity: oauth or upstream
const
antigravityAccountType
=
ref
<
'
oauth
'
|
'
upstream
'
>
(
'
oauth
'
)
// For antigravity: oauth or upstream
const
upstreamBaseUrl
=
ref
(
''
)
// For upstream type: base URL
const
upstreamBaseUrl
=
ref
(
''
)
// For upstream type: base URL
...
@@ -2250,6 +2322,8 @@ const maxSessions = ref<number | null>(null)
...
@@ -2250,6 +2322,8 @@ const maxSessions = ref<number | null>(null)
const
sessionIdleTimeout
=
ref
<
number
|
null
>
(
null
)
const
sessionIdleTimeout
=
ref
<
number
|
null
>
(
null
)
const
tlsFingerprintEnabled
=
ref
(
false
)
const
tlsFingerprintEnabled
=
ref
(
false
)
const
sessionIdMaskingEnabled
=
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)
// 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
'
)
const
geminiTierGoogleOne
=
ref
<
'
google_one_free
'
|
'
google_ai_pro
'
|
'
google_ai_ultra
'
>
(
'
google_one_free
'
)
...
@@ -2356,8 +2430,8 @@ const expiresAtInput = computed({
...
@@ -2356,8 +2430,8 @@ const expiresAtInput = computed({
const
canExchangeCode
=
computed
(()
=>
{
const
canExchangeCode
=
computed
(()
=>
{
const
authCode
=
oauthFlowRef
.
value
?.
authCode
||
''
const
authCode
=
oauthFlowRef
.
value
?.
authCode
||
''
if
(
form
.
platform
===
'
openai
'
)
{
if
(
form
.
platform
===
'
openai
'
||
form
.
platform
===
'
sora
'
)
{
return
authCode
.
trim
()
&&
o
pen
ai
OAuth
.
sessionId
.
value
&&
!
o
pen
ai
OAuth
.
loading
.
value
return
authCode
.
trim
()
&&
activeO
pen
AI
OAuth
.
value
.
sessionId
.
value
&&
!
activeO
pen
AI
OAuth
.
value
.
loading
.
value
}
}
if
(
form
.
platform
===
'
gemini
'
)
{
if
(
form
.
platform
===
'
gemini
'
)
{
return
authCode
.
trim
()
&&
geminiOAuth
.
sessionId
.
value
&&
!
geminiOAuth
.
loading
.
value
return
authCode
.
trim
()
&&
geminiOAuth
.
sessionId
.
value
&&
!
geminiOAuth
.
loading
.
value
...
@@ -2417,7 +2491,7 @@ watch(
...
@@ -2417,7 +2491,7 @@ watch(
(
newPlatform
)
=>
{
(
newPlatform
)
=>
{
// Reset base URL based on platform
// Reset base URL based on platform
apiKeyBaseUrl
.
value
=
apiKeyBaseUrl
.
value
=
newPlatform
===
'
openai
'
(
newPlatform
===
'
openai
'
||
newPlatform
===
'
sora
'
)
?
'
https://api.openai.com
'
?
'
https://api.openai.com
'
:
newPlatform
===
'
gemini
'
:
newPlatform
===
'
gemini
'
?
'
https://generativelanguage.googleapis.com
'
?
'
https://generativelanguage.googleapis.com
'
...
@@ -2443,6 +2517,11 @@ watch(
...
@@ -2443,6 +2517,11 @@ watch(
if
(
newPlatform
!==
'
anthropic
'
)
{
if
(
newPlatform
!==
'
anthropic
'
)
{
interceptWarmupRequests
.
value
=
false
interceptWarmupRequests
.
value
=
false
}
}
if
(
newPlatform
===
'
sora
'
)
{
accountCategory
.
value
=
'
oauth-based
'
addMethod
.
value
=
'
oauth
'
form
.
type
=
'
oauth
'
}
if
(
newPlatform
!==
'
openai
'
)
{
if
(
newPlatform
!==
'
openai
'
)
{
openaiPassthroughEnabled
.
value
=
false
openaiPassthroughEnabled
.
value
=
false
codexCLIOnlyEnabled
.
value
=
false
codexCLIOnlyEnabled
.
value
=
false
...
@@ -2450,6 +2529,7 @@ watch(
...
@@ -2450,6 +2529,7 @@ watch(
// Reset OAuth states
// Reset OAuth states
oauth
.
resetState
()
oauth
.
resetState
()
openaiOAuth
.
resetState
()
openaiOAuth
.
resetState
()
soraOAuth
.
resetState
()
geminiOAuth
.
resetState
()
geminiOAuth
.
resetState
()
antigravityOAuth
.
resetState
()
antigravityOAuth
.
resetState
()
}
}
...
@@ -2711,7 +2791,6 @@ const resetForm = () => {
...
@@ -2711,7 +2791,6 @@ const resetForm = () => {
autoPauseOnExpired
.
value
=
true
autoPauseOnExpired
.
value
=
true
openaiPassthroughEnabled
.
value
=
false
openaiPassthroughEnabled
.
value
=
false
codexCLIOnlyEnabled
.
value
=
false
codexCLIOnlyEnabled
.
value
=
false
enableSoraOnOpenAIOAuth
.
value
=
false
// Reset quota control state
// Reset quota control state
windowCostEnabled
.
value
=
false
windowCostEnabled
.
value
=
false
windowCostLimit
.
value
=
null
windowCostLimit
.
value
=
null
...
@@ -2721,6 +2800,8 @@ const resetForm = () => {
...
@@ -2721,6 +2800,8 @@ const resetForm = () => {
sessionIdleTimeout
.
value
=
null
sessionIdleTimeout
.
value
=
null
tlsFingerprintEnabled
.
value
=
false
tlsFingerprintEnabled
.
value
=
false
sessionIdMaskingEnabled
.
value
=
false
sessionIdMaskingEnabled
.
value
=
false
cacheTTLOverrideEnabled
.
value
=
false
cacheTTLOverrideTarget
.
value
=
'
5m
'
antigravityAccountType
.
value
=
'
oauth
'
antigravityAccountType
.
value
=
'
oauth
'
upstreamBaseUrl
.
value
=
''
upstreamBaseUrl
.
value
=
''
upstreamApiKey
.
value
=
''
upstreamApiKey
.
value
=
''
...
@@ -2732,6 +2813,7 @@ const resetForm = () => {
...
@@ -2732,6 +2813,7 @@ const resetForm = () => {
geminiTierAIStudio
.
value
=
'
aistudio_free
'
geminiTierAIStudio
.
value
=
'
aistudio_free
'
oauth
.
resetState
()
oauth
.
resetState
()
openaiOAuth
.
resetState
()
openaiOAuth
.
resetState
()
soraOAuth
.
resetState
()
geminiOAuth
.
resetState
()
geminiOAuth
.
resetState
()
antigravityOAuth
.
resetState
()
antigravityOAuth
.
resetState
()
oauthFlowRef
.
value
?.
reset
()
oauthFlowRef
.
value
?.
reset
()
...
@@ -2763,6 +2845,23 @@ const buildOpenAIExtra = (base?: Record<string, unknown>): Record<string, unknow
...
@@ -2763,6 +2845,23 @@ const buildOpenAIExtra = (base?: Record<string, unknown>): Record<string, unknow
return
Object
.
keys
(
extra
).
length
>
0
?
extra
:
undefined
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
// Helper function to create account with mixed channel warning handling
const
doCreateAccount
=
async
(
payload
:
any
)
=>
{
const
doCreateAccount
=
async
(
payload
:
any
)
=>
{
submitting
.
value
=
true
submitting
.
value
=
true
...
@@ -2878,7 +2977,7 @@ const handleSubmit = async () => {
...
@@ -2878,7 +2977,7 @@ const handleSubmit = async () => {
// Determine default base URL based on platform
// Determine default base URL based on platform
const
defaultBaseUrl
=
const
defaultBaseUrl
=
form
.
platform
===
'
openai
'
(
form
.
platform
===
'
openai
'
||
form
.
platform
===
'
sora
'
)
?
'
https://api.openai.com
'
?
'
https://api.openai.com
'
:
form
.
platform
===
'
gemini
'
:
form
.
platform
===
'
gemini
'
?
'
https://generativelanguage.googleapis.com
'
?
'
https://generativelanguage.googleapis.com
'
...
@@ -2930,14 +3029,15 @@ const goBackToBasicInfo = () => {
...
@@ -2930,14 +3029,15 @@ const goBackToBasicInfo = () => {
step
.
value
=
1
step
.
value
=
1
oauth
.
resetState
()
oauth
.
resetState
()
openaiOAuth
.
resetState
()
openaiOAuth
.
resetState
()
soraOAuth
.
resetState
()
geminiOAuth
.
resetState
()
geminiOAuth
.
resetState
()
antigravityOAuth
.
resetState
()
antigravityOAuth
.
resetState
()
oauthFlowRef
.
value
?.
reset
()
oauthFlowRef
.
value
?.
reset
()
}
}
const
handleGenerateUrl
=
async
()
=>
{
const
handleGenerateUrl
=
async
()
=>
{
if
(
form
.
platform
===
'
openai
'
)
{
if
(
form
.
platform
===
'
openai
'
||
form
.
platform
===
'
sora
'
)
{
await
o
pen
ai
OAuth
.
generateAuthUrl
(
form
.
proxy_id
)
await
activeO
pen
AI
OAuth
.
value
.
generateAuthUrl
(
form
.
proxy_id
)
}
else
if
(
form
.
platform
===
'
gemini
'
)
{
}
else
if
(
form
.
platform
===
'
gemini
'
)
{
await
geminiOAuth
.
generateAuthUrl
(
await
geminiOAuth
.
generateAuthUrl
(
form
.
proxy_id
,
form
.
proxy_id
,
...
@@ -2953,13 +3053,19 @@ const handleGenerateUrl = async () => {
...
@@ -2953,13 +3053,19 @@ const handleGenerateUrl = async () => {
}
}
const
handleValidateRefreshToken
=
(
rt
:
string
)
=>
{
const
handleValidateRefreshToken
=
(
rt
:
string
)
=>
{
if
(
form
.
platform
===
'
openai
'
)
{
if
(
form
.
platform
===
'
openai
'
||
form
.
platform
===
'
sora
'
)
{
handleOpenAIValidateRT
(
rt
)
handleOpenAIValidateRT
(
rt
)
}
else
if
(
form
.
platform
===
'
antigravity
'
)
{
}
else
if
(
form
.
platform
===
'
antigravity
'
)
{
handleAntigravityValidateRT
(
rt
)
handleAntigravityValidateRT
(
rt
)
}
}
}
}
const
handleValidateSessionToken
=
(
sessionToken
:
string
)
=>
{
if
(
form
.
platform
===
'
sora
'
)
{
handleSoraValidateST
(
sessionToken
)
}
}
const
formatDateTimeLocal
=
formatDateTimeLocalInput
const
formatDateTimeLocal
=
formatDateTimeLocalInput
const
parseDateTimeLocal
=
parseDateTimeLocalInput
const
parseDateTimeLocal
=
parseDateTimeLocalInput
...
@@ -2995,100 +3101,101 @@ const createAccountAndFinish = async (
...
@@ -2995,100 +3101,101 @@ const createAccountAndFinish = async (
// OpenAI OAuth 授权码兑换
// OpenAI OAuth 授权码兑换
const
handleOpenAIExchange
=
async
(
authCode
:
string
)
=>
{
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
authClient
.
loading
.
value
=
true
o
penaiOAuth
.
error
.
value
=
''
o
authClient
.
error
.
value
=
''
try
{
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
(),
authCode
.
trim
(),
openaiOAuth
.
sessionId
.
value
,
oauthClient
.
sessionId
.
value
,
stateToUse
,
form
.
proxy_id
form
.
proxy_id
)
)
if
(
!
tokenInfo
)
return
if
(
!
tokenInfo
)
return
const
credentials
=
o
penaiOAuth
.
buildCredentials
(
tokenInfo
)
const
credentials
=
o
authClient
.
buildCredentials
(
tokenInfo
)
const
oauthExtra
=
o
penaiOAuth
.
buildExtraInfo
(
tokenInfo
)
as
Record
<
string
,
unknown
>
|
undefined
const
oauthExtra
=
o
authClient
.
buildExtraInfo
(
tokenInfo
)
as
Record
<
string
,
unknown
>
|
undefined
const
extra
=
buildOpenAIExtra
(
oauthExtra
)
const
extra
=
buildOpenAIExtra
(
oauthExtra
)
const
shouldCreateOpenAI
=
form
.
platform
===
'
openai
'
const
shouldCreateSora
=
form
.
platform
===
'
sora
'
// 应用临时不可调度配置
// 应用临时不可调度配置
if
(
!
applyTempUnschedConfig
(
credentials
))
{
if
(
!
applyTempUnschedConfig
(
credentials
))
{
return
return
}
}
// 1. 创建 OpenAI 账号
let
openaiAccountId
:
string
|
number
|
undefined
const
openaiAccount
=
await
adminAPI
.
accounts
.
create
({
name
:
form
.
name
,
if
(
shouldCreateOpenAI
)
{
notes
:
form
.
notes
,
const
openaiAccount
=
await
adminAPI
.
accounts
.
create
({
platform
:
'
openai
'
,
name
:
form
.
name
,
type
:
'
oauth
'
,
notes
:
form
.
notes
,
credentials
,
platform
:
'
openai
'
,
extra
,
type
:
'
oauth
'
,
proxy_id
:
form
.
proxy_id
,
credentials
,
concurrency
:
form
.
concurrency
,
extra
,
priority
:
form
.
priority
,
proxy_id
:
form
.
proxy_id
,
rate_multiplier
:
form
.
rate_multiplier
,
concurrency
:
form
.
concurrency
,
group_ids
:
form
.
group_ids
,
priority
:
form
.
priority
,
expires_at
:
form
.
expires_at
,
rate_multiplier
:
form
.
rate_multiplier
,
auto_pause_on_expired
:
autoPauseOnExpired
.
value
group_ids
:
form
.
group_ids
,
}
)
expires_at
:
form
.
expires_at
,
auto_pause_on_expired
:
autoPauseOnExpired
.
value
appStore
.
showSuccess
(
t
(
'
admin.accounts.accountCreated
'
))
}
)
openaiAccountId
=
openaiAccount
.
id
// 2. 如果启用了 Sora,同时创建 Sora 账号
appStore
.
showSuccess
(
t
(
'
admin.accounts.accountCreated
'
))
if
(
enableSoraOnOpenAIOAuth
.
value
)
{
}
try
{
// Sora 使用相同的 OAuth credentials
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
await
adminAPI
.
accounts
.
create
({
name
:
`${form.name
}
(Sora)`
,
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
}
)
appStore
.
showSuccess
(
t
(
'
admin.accounts.soraAccountCreated
'
))
if
(
shouldCreateSora
)
{
}
catch
(
error
:
any
)
{
const
soraCredentials
=
{
console
.
error
(
'
创建 Sora 账号失败:
'
,
error
)
access_token
:
credentials
.
access_token
,
appStore
.
showWarning
(
t
(
'
admin.accounts.soraAccountFailed
'
))
refresh_token
:
credentials
.
refresh_token
,
expires_at
:
credentials
.
expires_at
}
}
const
soraName
=
shouldCreateOpenAI
?
`${form.name
}
(Sora)`
:
form
.
name
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
}
)
appStore
.
showSuccess
(
t
(
'
admin.accounts.accountCreated
'
))
}
}
emit
(
'
created
'
)
emit
(
'
created
'
)
handleClose
()
handleClose
()
}
catch
(
error
:
any
)
{
}
catch
(
error
:
any
)
{
o
penaiOAuth
.
error
.
value
=
error
.
response
?.
data
?.
detail
||
t
(
'
admin.accounts.oauth.authFailed
'
)
o
authClient
.
error
.
value
=
error
.
response
?.
data
?.
detail
||
t
(
'
admin.accounts.oauth.authFailed
'
)
appStore
.
showError
(
o
penaiOAuth
.
error
.
value
)
appStore
.
showError
(
o
authClient
.
error
.
value
)
}
finally
{
}
finally
{
o
penaiOAuth
.
loading
.
value
=
false
o
authClient
.
loading
.
value
=
false
}
}
}
}
// OpenAI 手动 RT 批量验证和创建
// OpenAI 手动 RT 批量验证和创建
const
handleOpenAIValidateRT
=
async
(
refreshTokenInput
:
string
)
=>
{
const
handleOpenAIValidateRT
=
async
(
refreshTokenInput
:
string
)
=>
{
const
oauthClient
=
activeOpenAIOAuth
.
value
if
(
!
refreshTokenInput
.
trim
())
return
if
(
!
refreshTokenInput
.
trim
())
return
// Parse multiple refresh tokens (one per line)
// Parse multiple refresh tokens (one per line)
...
@@ -3098,45 +3205,164 @@ const handleOpenAIValidateRT = async (refreshTokenInput: string) => {
...
@@ -3098,45 +3205,164 @@ const handleOpenAIValidateRT = async (refreshTokenInput: string) => {
.
filter
((
rt
)
=>
rt
)
.
filter
((
rt
)
=>
rt
)
if
(
refreshTokens
.
length
===
0
)
{
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
return
}
}
o
penaiOAuth
.
loading
.
value
=
true
o
authClient
.
loading
.
value
=
true
o
penaiOAuth
.
error
.
value
=
''
o
authClient
.
error
.
value
=
''
let
successCount
=
0
let
successCount
=
0
let
failedCount
=
0
let
failedCount
=
0
const
errors
:
string
[]
=
[]
const
errors
:
string
[]
=
[]
const
shouldCreateOpenAI
=
form
.
platform
===
'
openai
'
const
shouldCreateSora
=
form
.
platform
===
'
sora
'
try
{
try
{
for
(
let
i
=
0
;
i
<
refreshTokens
.
length
;
i
++
)
{
for
(
let
i
=
0
;
i
<
refreshTokens
.
length
;
i
++
)
{
try
{
try
{
const
tokenInfo
=
await
o
penaiOAuth
.
validateRefreshToken
(
const
tokenInfo
=
await
o
authClient
.
validateRefreshToken
(
refreshTokens
[
i
],
refreshTokens
[
i
],
form
.
proxy_id
form
.
proxy_id
)
)
if
(
!
tokenInfo
)
{
if
(
!
tokenInfo
)
{
failedCount
++
failedCount
++
errors
.
push
(
`#${i + 1
}
: ${o
penaiOAuth
.error.value || 'Validation failed'
}
`
)
errors
.
push
(
`#${i + 1
}
: ${o
authClient
.error.value || 'Validation failed'
}
`
)
o
penaiOAuth
.
error
.
value
=
''
o
authClient
.
error
.
value
=
''
continue
continue
}
}
const
credentials
=
o
penaiOAuth
.
buildCredentials
(
tokenInfo
)
const
credentials
=
o
authClient
.
buildCredentials
(
tokenInfo
)
const
oauthExtra
=
o
penaiOAuth
.
buildExtraInfo
(
tokenInfo
)
as
Record
<
string
,
unknown
>
|
undefined
const
oauthExtra
=
o
authClient
.
buildExtraInfo
(
tokenInfo
)
as
Record
<
string
,
unknown
>
|
undefined
const
extra
=
buildOpenAIExtra
(
oauthExtra
)
const
extra
=
buildOpenAIExtra
(
oauthExtra
)
// Generate account name with index for batch
// Generate account name with index for batch
const
accountName
=
refreshTokens
.
length
>
1
?
`${form.name
}
#${i + 1
}
`
:
form
.
name
const
accountName
=
refreshTokens
.
length
>
1
?
`${form.name
}
#${i + 1
}
`
:
form
.
name
let
openaiAccountId
:
string
|
number
|
undefined
if
(
shouldCreateOpenAI
)
{
const
openaiAccount
=
await
adminAPI
.
accounts
.
create
({
name
:
accountName
,
notes
:
form
.
notes
,
platform
:
'
openai
'
,
type
:
'
oauth
'
,
credentials
,
extra
,
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
}
)
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
++
const
errMsg
=
error
.
response
?.
data
?.
detail
||
error
.
message
||
'
Unknown error
'
errors
.
push
(
`#${i + 1
}
: ${errMsg
}
`
)
}
}
// Show results
if
(
successCount
>
0
&&
failedCount
===
0
)
{
appStore
.
showSuccess
(
refreshTokens
.
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
{
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
({
await
adminAPI
.
accounts
.
create
({
name
:
accountName
,
name
:
accountName
,
notes
:
form
.
notes
,
notes
:
form
.
notes
,
platform
:
'
openai
'
,
platform
:
'
sora
'
,
type
:
'
oauth
'
,
type
:
'
oauth
'
,
credentials
,
credentials
,
extra
,
extra
:
soraExtra
,
proxy_id
:
form
.
proxy_id
,
proxy_id
:
form
.
proxy_id
,
concurrency
:
form
.
concurrency
,
concurrency
:
form
.
concurrency
,
priority
:
form
.
priority
,
priority
:
form
.
priority
,
...
@@ -3153,10 +3379,9 @@ const handleOpenAIValidateRT = async (refreshTokenInput: string) => {
...
@@ -3153,10 +3379,9 @@ const handleOpenAIValidateRT = async (refreshTokenInput: string) => {
}
}
}
}
// Show results
if
(
successCount
>
0
&&
failedCount
===
0
)
{
if
(
successCount
>
0
&&
failedCount
===
0
)
{
appStore
.
showSuccess
(
appStore
.
showSuccess
(
refresh
Tokens
.
length
>
1
session
Tokens
.
length
>
1
?
t
(
'
admin.accounts.oauth.batchSuccess
'
,
{
count
:
successCount
}
)
?
t
(
'
admin.accounts.oauth.batchSuccess
'
,
{
count
:
successCount
}
)
:
t
(
'
admin.accounts.accountCreated
'
)
:
t
(
'
admin.accounts.accountCreated
'
)
)
)
...
@@ -3166,14 +3391,14 @@ const handleOpenAIValidateRT = async (refreshTokenInput: string) => {
...
@@ -3166,14 +3391,14 @@ const handleOpenAIValidateRT = async (refreshTokenInput: string) => {
appStore
.
showWarning
(
appStore
.
showWarning
(
t
(
'
admin.accounts.oauth.batchPartialSuccess
'
,
{
success
:
successCount
,
failed
:
failedCount
}
)
t
(
'
admin.accounts.oauth.batchPartialSuccess
'
,
{
success
:
successCount
,
failed
:
failedCount
}
)
)
)
o
penaiOAuth
.
error
.
value
=
errors
.
join
(
'
\n
'
)
o
authClient
.
error
.
value
=
errors
.
join
(
'
\n
'
)
emit
(
'
created
'
)
emit
(
'
created
'
)
}
else
{
}
else
{
o
penaiOAuth
.
error
.
value
=
errors
.
join
(
'
\n
'
)
o
authClient
.
error
.
value
=
errors
.
join
(
'
\n
'
)
appStore
.
showError
(
t
(
'
admin.accounts.oauth.batchFailed
'
))
appStore
.
showError
(
t
(
'
admin.accounts.oauth.batchFailed
'
))
}
}
}
finally
{
}
finally
{
o
penaiOAuth
.
loading
.
value
=
false
o
authClient
.
loading
.
value
=
false
}
}
}
}
...
@@ -3393,6 +3618,12 @@ const handleAnthropicExchange = async (authCode: string) => {
...
@@ -3393,6 +3618,12 @@ const handleAnthropicExchange = async (authCode: string) => {
extra
.
session_id_masking_enabled
=
true
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
=
{
const
credentials
=
{
...
tokenInfo
,
...
tokenInfo
,
...(
interceptWarmupRequests
.
value
?
{
intercept_warmup_requests
:
true
}
:
{
}
)
...(
interceptWarmupRequests
.
value
?
{
intercept_warmup_requests
:
true
}
:
{
}
)
...
@@ -3412,6 +3643,7 @@ const handleExchangeCode = async () => {
...
@@ -3412,6 +3643,7 @@ const handleExchangeCode = async () => {
switch
(
form
.
platform
)
{
switch
(
form
.
platform
)
{
case
'
openai
'
:
case
'
openai
'
:
case
'
sora
'
:
return
handleOpenAIExchange
(
authCode
)
return
handleOpenAIExchange
(
authCode
)
case
'
gemini
'
:
case
'
gemini
'
:
return
handleGeminiExchange
(
authCode
)
return
handleGeminiExchange
(
authCode
)
...
@@ -3486,6 +3718,12 @@ const handleCookieAuth = async (sessionKey: string) => {
...
@@ -3486,6 +3718,12 @@ const handleCookieAuth = async (sessionKey: string) => {
extra
.
session_id_masking_enabled
=
true
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
const
accountName
=
keys
.
length
>
1
?
`${form.name
}
#${i + 1
}
`
:
form
.
name
// Merge interceptWarmupRequests into credentials
// Merge interceptWarmupRequests into credentials
...
...
frontend/src/components/account/EditAccountModal.vue
View file @
987589ea
...
@@ -975,6 +975,46 @@
...
@@ -975,6 +975,46 @@
<
/button
>
<
/button
>
<
/div
>
<
/div
>
<
/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
>
<
div
class
=
"
border-t border-gray-200 pt-4 dark:border-dark-600
"
>
<
div
class
=
"
border-t border-gray-200 pt-4 dark:border-dark-600
"
>
...
@@ -1177,6 +1217,8 @@ const maxSessions = ref<number | null>(null)
...
@@ -1177,6 +1217,8 @@ const maxSessions = ref<number | null>(null)
const
sessionIdleTimeout
=
ref
<
number
|
null
>
(
null
)
const
sessionIdleTimeout
=
ref
<
number
|
null
>
(
null
)
const
tlsFingerprintEnabled
=
ref
(
false
)
const
tlsFingerprintEnabled
=
ref
(
false
)
const
sessionIdMaskingEnabled
=
ref
(
false
)
const
sessionIdMaskingEnabled
=
ref
(
false
)
const
cacheTTLOverrideEnabled
=
ref
(
false
)
const
cacheTTLOverrideTarget
=
ref
<
string
>
(
'
5m
'
)
// OpenAI 自动透传开关(OAuth/API Key)
// OpenAI 自动透传开关(OAuth/API Key)
const
openaiPassthroughEnabled
=
ref
(
false
)
const
openaiPassthroughEnabled
=
ref
(
false
)
...
@@ -1581,6 +1623,8 @@ function loadQuotaControlSettings(account: Account) {
...
@@ -1581,6 +1623,8 @@ function loadQuotaControlSettings(account: Account) {
sessionIdleTimeout
.
value
=
null
sessionIdleTimeout
.
value
=
null
tlsFingerprintEnabled
.
value
=
false
tlsFingerprintEnabled
.
value
=
false
sessionIdMaskingEnabled
.
value
=
false
sessionIdMaskingEnabled
.
value
=
false
cacheTTLOverrideEnabled
.
value
=
false
cacheTTLOverrideTarget
.
value
=
'
5m
'
// Only applies to Anthropic OAuth/SetupToken accounts
// Only applies to Anthropic OAuth/SetupToken accounts
if
(
account
.
platform
!==
'
anthropic
'
||
(
account
.
type
!==
'
oauth
'
&&
account
.
type
!==
'
setup-token
'
))
{
if
(
account
.
platform
!==
'
anthropic
'
||
(
account
.
type
!==
'
oauth
'
&&
account
.
type
!==
'
setup-token
'
))
{
...
@@ -1609,6 +1653,12 @@ function loadQuotaControlSettings(account: Account) {
...
@@ -1609,6 +1653,12 @@ function loadQuotaControlSettings(account: Account) {
if
(
account
.
session_id_masking_enabled
===
true
)
{
if
(
account
.
session_id_masking_enabled
===
true
)
{
sessionIdMaskingEnabled
.
value
=
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
)
{
function
formatTempUnschedKeywords
(
value
:
unknown
)
{
...
@@ -1820,6 +1870,15 @@ const handleSubmit = async () => {
...
@@ -1820,6 +1870,15 @@ const handleSubmit = async () => {
delete
newExtra
.
session_id_masking_enabled
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
updatePayload
.
extra
=
newExtra
}
}
...
...
frontend/src/components/account/OAuthAuthorizationFlow.vue
View file @
987589ea
...
@@ -48,6 +48,17 @@
...
@@ -48,6 +48,17 @@
t
(
getOAuthKey
(
'
refreshTokenAuth
'
))
t
(
getOAuthKey
(
'
refreshTokenAuth
'
))
}}
</span>
}}
</span>
</label>
</label>
<label
v-if=
"showSessionTokenOption"
class=
"flex cursor-pointer items-center gap-2"
>
<input
v-model=
"inputMethod"
type=
"radio"
value=
"session_token"
class=
"text-blue-600 focus:ring-blue-500"
/>
<span
class=
"text-sm text-blue-900 dark:text-blue-200"
>
{{
t
(
getOAuthKey
(
'
sessionTokenAuth
'
))
}}
</span>
</label>
</div>
</div>
</div>
</div>
...
@@ -135,6 +146,87 @@
...
@@ -135,6 +146,87 @@
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<!--
Session
Token
Input
(
Sora
)
-->
<
div
v
-
if
=
"
inputMethod === 'session_token'
"
class
=
"
space-y-4
"
>
<
div
class
=
"
rounded-lg border border-blue-300 bg-white/80 p-4 dark:border-blue-600 dark:bg-gray-800/80
"
>
<
p
class
=
"
mb-3 text-sm text-blue-700 dark:text-blue-300
"
>
{{
t
(
getOAuthKey
(
'
sessionTokenDesc
'
))
}}
<
/p
>
<
div
class
=
"
mb-4
"
>
<
label
class
=
"
mb-2 flex items-center gap-2 text-sm font-semibold text-gray-700 dark:text-gray-300
"
>
<
Icon
name
=
"
key
"
size
=
"
sm
"
class
=
"
text-blue-500
"
/>
Session
Token
<
span
v
-
if
=
"
parsedSessionTokenCount > 1
"
class
=
"
rounded-full bg-blue-500 px-2 py-0.5 text-xs text-white
"
>
{{
t
(
'
admin.accounts.oauth.keysCount
'
,
{
count
:
parsedSessionTokenCount
}
)
}}
<
/span
>
<
/label
>
<
textarea
v
-
model
=
"
sessionTokenInput
"
rows
=
"
3
"
class
=
"
input w-full resize-y font-mono text-sm
"
:
placeholder
=
"
t(getOAuthKey('sessionTokenPlaceholder'))
"
><
/textarea
>
<
p
v
-
if
=
"
parsedSessionTokenCount > 1
"
class
=
"
mt-1 text-xs text-blue-600 dark:text-blue-400
"
>
{{
t
(
'
admin.accounts.oauth.batchCreateAccounts
'
,
{
count
:
parsedSessionTokenCount
}
)
}}
<
/p
>
<
/div
>
<
div
v
-
if
=
"
error
"
class
=
"
mb-4 rounded-lg border border-red-200 bg-red-50 p-3 dark:border-red-700 dark:bg-red-900/30
"
>
<
p
class
=
"
whitespace-pre-line text-sm text-red-600 dark:text-red-400
"
>
{{
error
}}
<
/p
>
<
/div
>
<
button
type
=
"
button
"
class
=
"
btn btn-primary w-full
"
:
disabled
=
"
loading || !sessionTokenInput.trim()
"
@
click
=
"
handleValidateSessionToken
"
>
<
svg
v
-
if
=
"
loading
"
class
=
"
-ml-1 mr-2 h-4 w-4 animate-spin
"
fill
=
"
none
"
viewBox
=
"
0 0 24 24
"
>
<
circle
class
=
"
opacity-25
"
cx
=
"
12
"
cy
=
"
12
"
r
=
"
10
"
stroke
=
"
currentColor
"
stroke
-
width
=
"
4
"
><
/circle
>
<
path
class
=
"
opacity-75
"
fill
=
"
currentColor
"
d
=
"
M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z
"
><
/path
>
<
/svg
>
<
Icon
v
-
else
name
=
"
sparkles
"
size
=
"
sm
"
class
=
"
mr-2
"
/>
{{
loading
?
t
(
getOAuthKey
(
'
validating
'
))
:
t
(
getOAuthKey
(
'
validateAndCreate
'
))
}}
<
/button
>
<
/div
>
<
/div
>
<!--
Cookie
Auto
-
Auth
Form
-->
<!--
Cookie
Auto
-
Auth
Form
-->
<
div
v
-
if
=
"
inputMethod === 'cookie'
"
class
=
"
space-y-4
"
>
<
div
v
-
if
=
"
inputMethod === 'cookie'
"
class
=
"
space-y-4
"
>
<
div
<
div
...
@@ -521,13 +613,14 @@ interface Props {
...
@@ -521,13 +613,14 @@ interface Props {
error
?:
string
error
?:
string
showHelp
?:
boolean
showHelp
?:
boolean
showProxyWarning
?:
boolean
showProxyWarning
?:
boolean
allowMultiple
?:
boolean
allowMultiple
?:
boolean
methodLabel
?:
string
methodLabel
?:
string
showCookieOption
?:
boolean
// Whether to show cookie auto-auth option
showCookieOption
?:
boolean
// Whether to show cookie auto-auth option
showRefreshTokenOption
?:
boolean
// Whether to show refresh token input option (OpenAI only)
showRefreshTokenOption
?:
boolean
// Whether to show refresh token input option (OpenAI only)
platform
?:
AccountPlatform
// Platform type for different UI/text
showSessionTokenOption
?:
boolean
// Whether to show session token input option (Sora only)
showProjectId
?:
boolean
// New prop to control project ID visibility
platform
?:
AccountPlatform
// Platform type for different UI/text
}
showProjectId
?:
boolean
// New prop to control project ID visibility
}
const
props
=
withDefaults
(
defineProps
<
Props
>
(),
{
const
props
=
withDefaults
(
defineProps
<
Props
>
(),
{
authUrl
:
''
,
authUrl
:
''
,
...
@@ -540,6 +633,7 @@ const props = withDefaults(defineProps<Props>(), {
...
@@ -540,6 +633,7 @@ const props = withDefaults(defineProps<Props>(), {
methodLabel
:
'
Authorization Method
'
,
methodLabel
:
'
Authorization Method
'
,
showCookieOption
:
true
,
showCookieOption
:
true
,
showRefreshTokenOption
:
false
,
showRefreshTokenOption
:
false
,
showSessionTokenOption
:
false
,
platform
:
'
anthropic
'
,
platform
:
'
anthropic
'
,
showProjectId
:
true
showProjectId
:
true
}
)
}
)
...
@@ -549,6 +643,7 @@ const emit = defineEmits<{
...
@@ -549,6 +643,7 @@ const emit = defineEmits<{
'
exchange-code
'
:
[
code
:
string
]
'
exchange-code
'
:
[
code
:
string
]
'
cookie-auth
'
:
[
sessionKey
:
string
]
'
cookie-auth
'
:
[
sessionKey
:
string
]
'
validate-refresh-token
'
:
[
refreshToken
:
string
]
'
validate-refresh-token
'
:
[
refreshToken
:
string
]
'
validate-session-token
'
:
[
sessionToken
:
string
]
'
update:inputMethod
'
:
[
method
:
AuthInputMethod
]
'
update:inputMethod
'
:
[
method
:
AuthInputMethod
]
}
>
()
}
>
()
...
@@ -587,12 +682,13 @@ const inputMethod = ref<AuthInputMethod>(props.showCookieOption ? 'manual' : 'ma
...
@@ -587,12 +682,13 @@ const inputMethod = ref<AuthInputMethod>(props.showCookieOption ? 'manual' : 'ma
const
authCodeInput
=
ref
(
''
)
const
authCodeInput
=
ref
(
''
)
const
sessionKeyInput
=
ref
(
''
)
const
sessionKeyInput
=
ref
(
''
)
const
refreshTokenInput
=
ref
(
''
)
const
refreshTokenInput
=
ref
(
''
)
const
sessionTokenInput
=
ref
(
''
)
const
showHelpDialog
=
ref
(
false
)
const
showHelpDialog
=
ref
(
false
)
const
oauthState
=
ref
(
''
)
const
oauthState
=
ref
(
''
)
const
projectId
=
ref
(
''
)
const
projectId
=
ref
(
''
)
// Computed: show method selection when either cookie or refresh token option is enabled
// Computed: show method selection when either cookie or refresh token option is enabled
const
showMethodSelection
=
computed
(()
=>
props
.
showCookieOption
||
props
.
showRefreshTokenOption
)
const
showMethodSelection
=
computed
(()
=>
props
.
showCookieOption
||
props
.
showRefreshTokenOption
||
props
.
showSessionTokenOption
)
// Clipboard
// Clipboard
const
{
copied
,
copyToClipboard
}
=
useClipboard
()
const
{
copied
,
copyToClipboard
}
=
useClipboard
()
...
@@ -613,6 +709,13 @@ const parsedRefreshTokenCount = computed(() => {
...
@@ -613,6 +709,13 @@ const parsedRefreshTokenCount = computed(() => {
.
filter
((
rt
)
=>
rt
).
length
.
filter
((
rt
)
=>
rt
).
length
}
)
}
)
const
parsedSessionTokenCount
=
computed
(()
=>
{
return
sessionTokenInput
.
value
.
split
(
'
\n
'
)
.
map
((
st
)
=>
st
.
trim
())
.
filter
((
st
)
=>
st
).
length
}
)
// Watchers
// Watchers
watch
(
inputMethod
,
(
newVal
)
=>
{
watch
(
inputMethod
,
(
newVal
)
=>
{
emit
(
'
update:inputMethod
'
,
newVal
)
emit
(
'
update:inputMethod
'
,
newVal
)
...
@@ -631,7 +734,7 @@ watch(authCodeInput, (newVal) => {
...
@@ -631,7 +734,7 @@ watch(authCodeInput, (newVal) => {
const
url
=
new
URL
(
trimmed
)
const
url
=
new
URL
(
trimmed
)
const
code
=
url
.
searchParams
.
get
(
'
code
'
)
const
code
=
url
.
searchParams
.
get
(
'
code
'
)
const
stateParam
=
url
.
searchParams
.
get
(
'
state
'
)
const
stateParam
=
url
.
searchParams
.
get
(
'
state
'
)
if
((
props
.
platform
===
'
gemini
'
||
props
.
platform
===
'
antigravity
'
)
&&
stateParam
)
{
if
((
props
.
platform
===
'
openai
'
||
props
.
platform
===
'
sora
'
||
props
.
platform
===
'
gemini
'
||
props
.
platform
===
'
antigravity
'
)
&&
stateParam
)
{
oauthState
.
value
=
stateParam
oauthState
.
value
=
stateParam
}
}
if
(
code
&&
code
!==
trimmed
)
{
if
(
code
&&
code
!==
trimmed
)
{
...
@@ -642,7 +745,7 @@ watch(authCodeInput, (newVal) => {
...
@@ -642,7 +745,7 @@ watch(authCodeInput, (newVal) => {
// If URL parsing fails, try regex extraction
// If URL parsing fails, try regex extraction
const
match
=
trimmed
.
match
(
/
[
?&
]
code=
([^
&
]
+
)
/
)
const
match
=
trimmed
.
match
(
/
[
?&
]
code=
([^
&
]
+
)
/
)
const
stateMatch
=
trimmed
.
match
(
/
[
?&
]
state=
([^
&
]
+
)
/
)
const
stateMatch
=
trimmed
.
match
(
/
[
?&
]
state=
([^
&
]
+
)
/
)
if
((
props
.
platform
===
'
gemini
'
||
props
.
platform
===
'
antigravity
'
)
&&
stateMatch
&&
stateMatch
[
1
])
{
if
((
props
.
platform
===
'
openai
'
||
props
.
platform
===
'
sora
'
||
props
.
platform
===
'
gemini
'
||
props
.
platform
===
'
antigravity
'
)
&&
stateMatch
&&
stateMatch
[
1
])
{
oauthState
.
value
=
stateMatch
[
1
]
oauthState
.
value
=
stateMatch
[
1
]
}
}
if
(
match
&&
match
[
1
]
&&
match
[
1
]
!==
trimmed
)
{
if
(
match
&&
match
[
1
]
&&
match
[
1
]
!==
trimmed
)
{
...
@@ -680,6 +783,12 @@ const handleValidateRefreshToken = () => {
...
@@ -680,6 +783,12 @@ const handleValidateRefreshToken = () => {
}
}
}
}
const
handleValidateSessionToken
=
()
=>
{
if
(
sessionTokenInput
.
value
.
trim
())
{
emit
(
'
validate-session-token
'
,
sessionTokenInput
.
value
.
trim
())
}
}
// Expose methods and state
// Expose methods and state
defineExpose
({
defineExpose
({
authCode
:
authCodeInput
,
authCode
:
authCodeInput
,
...
@@ -687,6 +796,7 @@ defineExpose({
...
@@ -687,6 +796,7 @@ defineExpose({
projectId
,
projectId
,
sessionKey
:
sessionKeyInput
,
sessionKey
:
sessionKeyInput
,
refreshToken
:
refreshTokenInput
,
refreshToken
:
refreshTokenInput
,
sessionToken
:
sessionTokenInput
,
inputMethod
,
inputMethod
,
reset
:
()
=>
{
reset
:
()
=>
{
authCodeInput
.
value
=
''
authCodeInput
.
value
=
''
...
@@ -694,6 +804,7 @@ defineExpose({
...
@@ -694,6 +804,7 @@ defineExpose({
projectId
.
value
=
''
projectId
.
value
=
''
sessionKeyInput
.
value
=
''
sessionKeyInput
.
value
=
''
refreshTokenInput
.
value
=
''
refreshTokenInput
.
value
=
''
sessionTokenInput
.
value
=
''
inputMethod
.
value
=
'
manual
'
inputMethod
.
value
=
'
manual
'
showHelpDialog
.
value
=
false
showHelpDialog
.
value
=
false
}
}
...
...
frontend/src/components/account/ReAuthAccountModal.vue
View file @
987589ea
...
@@ -14,7 +14,7 @@
...
@@ -14,7 +14,7 @@
<div
<div
:class=
"[
:class=
"[
'flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-br',
'flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-br',
isOpenAI
isOpenAI
Like
? 'from-green-500 to-green-600'
? 'from-green-500 to-green-600'
: isGemini
: isGemini
? 'from-blue-500 to-blue-600'
? 'from-blue-500 to-blue-600'
...
@@ -33,6 +33,8 @@
...
@@ -33,6 +33,8 @@
{{
{{
isOpenAI
isOpenAI
?
t
(
'
admin.accounts.openaiAccount
'
)
?
t
(
'
admin.accounts.openaiAccount
'
)
:
isSora
?
t
(
'
admin.accounts.soraAccount
'
)
:
isGemini
:
isGemini
?
t
(
'
admin.accounts.geminiAccount
'
)
?
t
(
'
admin.accounts.geminiAccount
'
)
:
isAntigravity
:
isAntigravity
...
@@ -128,7 +130,7 @@
...
@@ -128,7 +130,7 @@
:show-cookie-option=
"isAnthropic"
:show-cookie-option=
"isAnthropic"
:allow-multiple=
"false"
:allow-multiple=
"false"
:method-label=
"t('admin.accounts.inputMethod')"
:method-label=
"t('admin.accounts.inputMethod')"
:platform=
"isOpenAI ? 'openai' : isGemini ? 'gemini' : isAntigravity ? 'antigravity' : 'anthropic'"
:platform=
"isOpenAI ? 'openai' :
isSora ? 'sora' :
isGemini ? 'gemini' : isAntigravity ? 'antigravity' : 'anthropic'"
:show-project-id=
"isGemini && geminiOAuthType === 'code_assist'"
:show-project-id=
"isGemini && geminiOAuthType === 'code_assist'"
@
generate-url=
"handleGenerateUrl"
@
generate-url=
"handleGenerateUrl"
@
cookie-auth=
"handleCookieAuth"
@
cookie-auth=
"handleCookieAuth"
...
@@ -224,7 +226,8 @@ const { t } = useI18n()
...
@@ -224,7 +226,8 @@ const { t } = useI18n()
// OAuth composables
// OAuth composables
const
claudeOAuth
=
useAccountOAuth
()
const
claudeOAuth
=
useAccountOAuth
()
const
openaiOAuth
=
useOpenAIOAuth
()
const
openaiOAuth
=
useOpenAIOAuth
({
platform
:
'
openai
'
})
const
soraOAuth
=
useOpenAIOAuth
({
platform
:
'
sora
'
})
const
geminiOAuth
=
useGeminiOAuth
()
const
geminiOAuth
=
useGeminiOAuth
()
const
antigravityOAuth
=
useAntigravityOAuth
()
const
antigravityOAuth
=
useAntigravityOAuth
()
...
@@ -237,31 +240,34 @@ const geminiOAuthType = ref<'code_assist' | 'google_one' | 'ai_studio'>('code_as
...
@@ -237,31 +240,34 @@ const geminiOAuthType = ref<'code_assist' | 'google_one' | 'ai_studio'>('code_as
// Computed - check platform
// Computed - check platform
const
isOpenAI
=
computed
(()
=>
props
.
account
?.
platform
===
'
openai
'
)
const
isOpenAI
=
computed
(()
=>
props
.
account
?.
platform
===
'
openai
'
)
const
isSora
=
computed
(()
=>
props
.
account
?.
platform
===
'
sora
'
)
const
isOpenAILike
=
computed
(()
=>
isOpenAI
.
value
||
isSora
.
value
)
const
isGemini
=
computed
(()
=>
props
.
account
?.
platform
===
'
gemini
'
)
const
isGemini
=
computed
(()
=>
props
.
account
?.
platform
===
'
gemini
'
)
const
isAnthropic
=
computed
(()
=>
props
.
account
?.
platform
===
'
anthropic
'
)
const
isAnthropic
=
computed
(()
=>
props
.
account
?.
platform
===
'
anthropic
'
)
const
isAntigravity
=
computed
(()
=>
props
.
account
?.
platform
===
'
antigravity
'
)
const
isAntigravity
=
computed
(()
=>
props
.
account
?.
platform
===
'
antigravity
'
)
const
activeOpenAIOAuth
=
computed
(()
=>
(
isSora
.
value
?
soraOAuth
:
openaiOAuth
))
// Computed - current OAuth state based on platform
// Computed - current OAuth state based on platform
const
currentAuthUrl
=
computed
(()
=>
{
const
currentAuthUrl
=
computed
(()
=>
{
if
(
isOpenAI
.
value
)
return
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
(
isGemini
.
value
)
return
geminiOAuth
.
authUrl
.
value
if
(
isAntigravity
.
value
)
return
antigravityOAuth
.
authUrl
.
value
if
(
isAntigravity
.
value
)
return
antigravityOAuth
.
authUrl
.
value
return
claudeOAuth
.
authUrl
.
value
return
claudeOAuth
.
authUrl
.
value
})
})
const
currentSessionId
=
computed
(()
=>
{
const
currentSessionId
=
computed
(()
=>
{
if
(
isOpenAI
.
value
)
return
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
(
isGemini
.
value
)
return
geminiOAuth
.
sessionId
.
value
if
(
isAntigravity
.
value
)
return
antigravityOAuth
.
sessionId
.
value
if
(
isAntigravity
.
value
)
return
antigravityOAuth
.
sessionId
.
value
return
claudeOAuth
.
sessionId
.
value
return
claudeOAuth
.
sessionId
.
value
})
})
const
currentLoading
=
computed
(()
=>
{
const
currentLoading
=
computed
(()
=>
{
if
(
isOpenAI
.
value
)
return
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
(
isGemini
.
value
)
return
geminiOAuth
.
loading
.
value
if
(
isAntigravity
.
value
)
return
antigravityOAuth
.
loading
.
value
if
(
isAntigravity
.
value
)
return
antigravityOAuth
.
loading
.
value
return
claudeOAuth
.
loading
.
value
return
claudeOAuth
.
loading
.
value
})
})
const
currentError
=
computed
(()
=>
{
const
currentError
=
computed
(()
=>
{
if
(
isOpenAI
.
value
)
return
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
(
isGemini
.
value
)
return
geminiOAuth
.
error
.
value
if
(
isAntigravity
.
value
)
return
antigravityOAuth
.
error
.
value
if
(
isAntigravity
.
value
)
return
antigravityOAuth
.
error
.
value
return
claudeOAuth
.
error
.
value
return
claudeOAuth
.
error
.
value
...
@@ -269,8 +275,8 @@ const currentError = computed(() => {
...
@@ -269,8 +275,8 @@ const currentError = computed(() => {
// Computed
// Computed
const
isManualInputMethod
=
computed
(()
=>
{
const
isManualInputMethod
=
computed
(()
=>
{
// OpenAI/Gemini/Antigravity always use manual input (no cookie auth option)
// OpenAI/
Sora/
Gemini/Antigravity always use manual input (no cookie auth option)
return
isOpenAI
.
value
||
isGemini
.
value
||
isAntigravity
.
value
||
oauthFlowRef
.
value
?.
inputMethod
===
'
manual
'
return
isOpenAI
Like
.
value
||
isGemini
.
value
||
isAntigravity
.
value
||
oauthFlowRef
.
value
?.
inputMethod
===
'
manual
'
})
})
const
canExchangeCode
=
computed
(()
=>
{
const
canExchangeCode
=
computed
(()
=>
{
...
@@ -313,6 +319,7 @@ const resetState = () => {
...
@@ -313,6 +319,7 @@ const resetState = () => {
geminiOAuthType
.
value
=
'
code_assist
'
geminiOAuthType
.
value
=
'
code_assist
'
claudeOAuth
.
resetState
()
claudeOAuth
.
resetState
()
openaiOAuth
.
resetState
()
openaiOAuth
.
resetState
()
soraOAuth
.
resetState
()
geminiOAuth
.
resetState
()
geminiOAuth
.
resetState
()
antigravityOAuth
.
resetState
()
antigravityOAuth
.
resetState
()
oauthFlowRef
.
value
?.
reset
()
oauthFlowRef
.
value
?.
reset
()
...
@@ -325,8 +332,8 @@ const handleClose = () => {
...
@@ -325,8 +332,8 @@ const handleClose = () => {
const
handleGenerateUrl
=
async
()
=>
{
const
handleGenerateUrl
=
async
()
=>
{
if
(
!
props
.
account
)
return
if
(
!
props
.
account
)
return
if
(
isOpenAI
.
value
)
{
if
(
isOpenAI
Like
.
value
)
{
await
o
pen
ai
OAuth
.
generateAuthUrl
(
props
.
account
.
proxy_id
)
await
activeO
pen
AI
OAuth
.
value
.
generateAuthUrl
(
props
.
account
.
proxy_id
)
}
else
if
(
isGemini
.
value
)
{
}
else
if
(
isGemini
.
value
)
{
const
creds
=
(
props
.
account
.
credentials
||
{})
as
Record
<
string
,
unknown
>
const
creds
=
(
props
.
account
.
credentials
||
{})
as
Record
<
string
,
unknown
>
const
tierId
=
typeof
creds
.
tier_id
===
'
string
'
?
creds
.
tier_id
:
undefined
const
tierId
=
typeof
creds
.
tier_id
===
'
string
'
?
creds
.
tier_id
:
undefined
...
@@ -345,21 +352,29 @@ const handleExchangeCode = async () => {
...
@@ -345,21 +352,29 @@ const handleExchangeCode = async () => {
const
authCode
=
oauthFlowRef
.
value
?.
authCode
||
''
const
authCode
=
oauthFlowRef
.
value
?.
authCode
||
''
if
(
!
authCode
.
trim
())
return
if
(
!
authCode
.
trim
())
return
if
(
isOpenAI
.
value
)
{
if
(
isOpenAI
Like
.
value
)
{
// OpenAI OAuth flow
// OpenAI OAuth flow
const
sessionId
=
openaiOAuth
.
sessionId
.
value
const
oauthClient
=
activeOpenAIOAuth
.
value
const
sessionId
=
oauthClient
.
sessionId
.
value
if
(
!
sessionId
)
return
if
(
!
sessionId
)
return
const
stateToUse
=
(
oauthFlowRef
.
value
?.
oauthState
||
oauthClient
.
oauthState
.
value
||
''
).
trim
()
if
(
!
stateToUse
)
{
oauthClient
.
error
.
value
=
t
(
'
admin.accounts.oauth.authFailed
'
)
appStore
.
showError
(
oauthClient
.
error
.
value
)
return
}
const
tokenInfo
=
await
o
penaiOAuth
.
exchangeAuthCode
(
const
tokenInfo
=
await
o
authClient
.
exchangeAuthCode
(
authCode
.
trim
(),
authCode
.
trim
(),
sessionId
,
sessionId
,
stateToUse
,
props
.
account
.
proxy_id
props
.
account
.
proxy_id
)
)
if
(
!
tokenInfo
)
return
if
(
!
tokenInfo
)
return
// Build credentials and extra info
// Build credentials and extra info
const
credentials
=
o
penaiOAuth
.
buildCredentials
(
tokenInfo
)
const
credentials
=
o
authClient
.
buildCredentials
(
tokenInfo
)
const
extra
=
o
penaiOAuth
.
buildExtraInfo
(
tokenInfo
)
const
extra
=
o
authClient
.
buildExtraInfo
(
tokenInfo
)
try
{
try
{
// Update account with new credentials
// Update account with new credentials
...
@@ -376,8 +391,8 @@ const handleExchangeCode = async () => {
...
@@ -376,8 +391,8 @@ const handleExchangeCode = async () => {
emit
(
'
reauthorized
'
)
emit
(
'
reauthorized
'
)
handleClose
()
handleClose
()
}
catch
(
error
:
any
)
{
}
catch
(
error
:
any
)
{
o
penaiOAuth
.
error
.
value
=
error
.
response
?.
data
?.
detail
||
t
(
'
admin.accounts.oauth.authFailed
'
)
o
authClient
.
error
.
value
=
error
.
response
?.
data
?.
detail
||
t
(
'
admin.accounts.oauth.authFailed
'
)
appStore
.
showError
(
o
penaiOAuth
.
error
.
value
)
appStore
.
showError
(
o
authClient
.
error
.
value
)
}
}
}
else
if
(
isGemini
.
value
)
{
}
else
if
(
isGemini
.
value
)
{
const
sessionId
=
geminiOAuth
.
sessionId
.
value
const
sessionId
=
geminiOAuth
.
sessionId
.
value
...
@@ -490,7 +505,7 @@ const handleExchangeCode = async () => {
...
@@ -490,7 +505,7 @@ const handleExchangeCode = async () => {
}
}
const
handleCookieAuth
=
async
(
sessionKey
:
string
)
=>
{
const
handleCookieAuth
=
async
(
sessionKey
:
string
)
=>
{
if
(
!
props
.
account
||
isOpenAI
.
value
)
return
if
(
!
props
.
account
||
isOpenAI
Like
.
value
)
return
claudeOAuth
.
loading
.
value
=
true
claudeOAuth
.
loading
.
value
=
true
claudeOAuth
.
error
.
value
=
''
claudeOAuth
.
error
.
value
=
''
...
...
frontend/src/components/admin/account/AccountTableFilters.vue
View file @
987589ea
...
@@ -10,16 +10,21 @@
...
@@ -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.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.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.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>
</div>
</
template
>
</
template
>
<
script
setup
lang=
"ts"
>
<
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
'
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
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
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
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
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
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
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
>
</
script
>
frontend/src/components/admin/account/AccountTestModal.vue
View file @
987589ea
...
@@ -41,7 +41,7 @@
...
@@ -41,7 +41,7 @@
</span>
</span>
</div>
</div>
<div
class=
"space-y-1.5"
>
<div
v-if=
"!isSoraAccount"
class=
"space-y-1.5"
>
<label
class=
"text-sm font-medium text-gray-700 dark:text-gray-300"
>
<label
class=
"text-sm font-medium text-gray-700 dark:text-gray-300"
>
{{
t
(
'
admin.accounts.selectTestModel
'
)
}}
{{
t
(
'
admin.accounts.selectTestModel
'
)
}}
</label>
</label>
...
@@ -54,6 +54,12 @@
...
@@ -54,6 +54,12 @@
:placeholder=
"loadingModels ? t('common.loading') + '...' : t('admin.accounts.selectTestModel')"
:placeholder=
"loadingModels ? t('common.loading') + '...' : t('admin.accounts.selectTestModel')"
/>
/>
</div>
</div>
<div
v-else
class=
"rounded-lg border border-blue-200 bg-blue-50 px-3 py-2 text-xs text-blue-700 dark:border-blue-700 dark:bg-blue-900/20 dark:text-blue-300"
>
{{
t
(
'
admin.accounts.soraTestHint
'
)
}}
</div>
<!-- Terminal Output -->
<!-- Terminal Output -->
<div
class=
"group relative"
>
<div
class=
"group relative"
>
...
@@ -114,12 +120,12 @@
...
@@ -114,12 +120,12 @@
<div
class=
"flex items-center gap-3"
>
<div
class=
"flex items-center gap-3"
>
<span
class=
"flex items-center gap-1"
>
<span
class=
"flex items-center gap-1"
>
<Icon
name=
"grid"
size=
"sm"
:stroke-width=
"2"
/>
<Icon
name=
"grid"
size=
"sm"
:stroke-width=
"2"
/>
{{
t
(
'
admin.accounts.testModel
'
)
}}
{{
isSoraAccount
?
t
(
'
admin.accounts.soraTestTarget
'
)
:
t
(
'
admin.accounts.testModel
'
)
}}
</span>
</span>
</div>
</div>
<span
class=
"flex items-center gap-1"
>
<span
class=
"flex items-center gap-1"
>
<Icon
name=
"chat"
size=
"sm"
:stroke-width=
"2"
/>
<Icon
name=
"chat"
size=
"sm"
:stroke-width=
"2"
/>
{{
t
(
'
admin.accounts.testPrompt
'
)
}}
{{
isSoraAccount
?
t
(
'
admin.accounts.soraTestMode
'
)
:
t
(
'
admin.accounts.testPrompt
'
)
}}
</span>
</span>
</div>
</div>
</div>
</div>
...
@@ -135,10 +141,10 @@
...
@@ -135,10 +141,10 @@
</button>
</button>
<button
<button
@
click=
"startTest"
@
click=
"startTest"
:disabled=
"status === 'connecting' || !selectedModelId"
:disabled=
"status === 'connecting' ||
(!isSoraAccount &&
!selectedModelId
)
"
:class=
"[
:class=
"[
'flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition-all',
'flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition-all',
status === 'connecting' || !selectedModelId
status === 'connecting' ||
(!isSoraAccount &&
!selectedModelId
)
? 'cursor-not-allowed bg-primary-400 text-white'
? 'cursor-not-allowed bg-primary-400 text-white'
: status === 'success'
: status === 'success'
? 'bg-green-500 text-white hover:bg-green-600'
? 'bg-green-500 text-white hover:bg-green-600'
...
@@ -172,7 +178,7 @@
...
@@ -172,7 +178,7 @@
</template>
</template>
<
script
setup
lang=
"ts"
>
<
script
setup
lang=
"ts"
>
import
{
ref
,
watch
,
nextTick
}
from
'
vue
'
import
{
computed
,
ref
,
watch
,
nextTick
}
from
'
vue
'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
useI18n
}
from
'
vue-i18n
'
import
BaseDialog
from
'
@/components/common/BaseDialog.vue
'
import
BaseDialog
from
'
@/components/common/BaseDialog.vue
'
import
Select
from
'
@/components/common/Select.vue
'
import
Select
from
'
@/components/common/Select.vue
'
...
@@ -207,6 +213,7 @@ const availableModels = ref<ClaudeModel[]>([])
...
@@ -207,6 +213,7 @@ const availableModels = ref<ClaudeModel[]>([])
const
selectedModelId
=
ref
(
''
)
const
selectedModelId
=
ref
(
''
)
const
loadingModels
=
ref
(
false
)
const
loadingModels
=
ref
(
false
)
let
eventSource
:
EventSource
|
null
=
null
let
eventSource
:
EventSource
|
null
=
null
const
isSoraAccount
=
computed
(()
=>
props
.
account
?.
platform
===
'
sora
'
)
// Load available models when modal opens
// Load available models when modal opens
watch
(
watch
(
...
@@ -223,6 +230,12 @@ watch(
...
@@ -223,6 +230,12 @@ watch(
const
loadAvailableModels
=
async
()
=>
{
const
loadAvailableModels
=
async
()
=>
{
if
(
!
props
.
account
)
return
if
(
!
props
.
account
)
return
if
(
props
.
account
.
platform
===
'
sora
'
)
{
availableModels
.
value
=
[]
selectedModelId
.
value
=
''
loadingModels
.
value
=
false
return
}
loadingModels
.
value
=
true
loadingModels
.
value
=
true
selectedModelId
.
value
=
''
// Reset selection before loading
selectedModelId
.
value
=
''
// Reset selection before loading
...
@@ -290,7 +303,7 @@ const scrollToBottom = async () => {
...
@@ -290,7 +303,7 @@ const scrollToBottom = async () => {
}
}
const
startTest
=
async
()
=>
{
const
startTest
=
async
()
=>
{
if
(
!
props
.
account
||
!
selectedModelId
.
value
)
return
if
(
!
props
.
account
||
(
!
isSoraAccount
.
value
&&
!
selectedModelId
.
value
)
)
return
resetState
()
resetState
()
status
.
value
=
'
connecting
'
status
.
value
=
'
connecting
'
...
@@ -311,7 +324,9 @@ const startTest = async () => {
...
@@ -311,7 +324,9 @@ const startTest = async () => {
Authorization
:
`Bearer
${
localStorage
.
getItem
(
'
auth_token
'
)}
`
,
Authorization
:
`Bearer
${
localStorage
.
getItem
(
'
auth_token
'
)}
`
,
'
Content-Type
'
:
'
application/json
'
'
Content-Type
'
:
'
application/json
'
},
},
body
:
JSON
.
stringify
({
model_id
:
selectedModelId
.
value
})
body
:
JSON
.
stringify
(
isSoraAccount
.
value
?
{}
:
{
model_id
:
selectedModelId
.
value
}
)
})
})
if
(
!
response
.
ok
)
{
if
(
!
response
.
ok
)
{
...
@@ -368,7 +383,10 @@ const handleEvent = (event: {
...
@@ -368,7 +383,10 @@ const handleEvent = (event: {
if
(
event
.
model
)
{
if
(
event
.
model
)
{
addLine
(
t
(
'
admin.accounts.usingModel
'
,
{
model
:
event
.
model
}),
'
text-cyan-400
'
)
addLine
(
t
(
'
admin.accounts.usingModel
'
,
{
model
:
event
.
model
}),
'
text-cyan-400
'
)
}
}
addLine
(
t
(
'
admin.accounts.sendingTestMessage
'
),
'
text-gray-400
'
)
addLine
(
isSoraAccount
.
value
?
t
(
'
admin.accounts.soraTestingFlow
'
)
:
t
(
'
admin.accounts.sendingTestMessage
'
),
'
text-gray-400
'
)
addLine
(
''
,
'
text-gray-300
'
)
addLine
(
''
,
'
text-gray-300
'
)
addLine
(
t
(
'
admin.accounts.response
'
),
'
text-yellow-400
'
)
addLine
(
t
(
'
admin.accounts.response
'
),
'
text-yellow-400
'
)
break
break
...
...
frontend/src/components/admin/account/ReAuthAccountModal.vue
View file @
987589ea
...
@@ -14,7 +14,7 @@
...
@@ -14,7 +14,7 @@
<div
<div
:class=
"[
:class=
"[
'flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-br',
'flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-br',
isOpenAI
isOpenAI
Like
? 'from-green-500 to-green-600'
? 'from-green-500 to-green-600'
: isGemini
: isGemini
? 'from-blue-500 to-blue-600'
? 'from-blue-500 to-blue-600'
...
@@ -33,6 +33,8 @@
...
@@ -33,6 +33,8 @@
{{
{{
isOpenAI
isOpenAI
?
t
(
'
admin.accounts.openaiAccount
'
)
?
t
(
'
admin.accounts.openaiAccount
'
)
:
isSora
?
t
(
'
admin.accounts.soraAccount
'
)
:
isGemini
:
isGemini
?
t
(
'
admin.accounts.geminiAccount
'
)
?
t
(
'
admin.accounts.geminiAccount
'
)
:
isAntigravity
:
isAntigravity
...
@@ -128,7 +130,7 @@
...
@@ -128,7 +130,7 @@
:show-cookie-option=
"isAnthropic"
:show-cookie-option=
"isAnthropic"
:allow-multiple=
"false"
:allow-multiple=
"false"
:method-label=
"t('admin.accounts.inputMethod')"
:method-label=
"t('admin.accounts.inputMethod')"
:platform=
"isOpenAI ? 'openai' : isGemini ? 'gemini' : isAntigravity ? 'antigravity' : 'anthropic'"
:platform=
"isOpenAI ? 'openai' :
isSora ? 'sora' :
isGemini ? 'gemini' : isAntigravity ? 'antigravity' : 'anthropic'"
:show-project-id=
"isGemini && geminiOAuthType === 'code_assist'"
:show-project-id=
"isGemini && geminiOAuthType === 'code_assist'"
@
generate-url=
"handleGenerateUrl"
@
generate-url=
"handleGenerateUrl"
@
cookie-auth=
"handleCookieAuth"
@
cookie-auth=
"handleCookieAuth"
...
@@ -224,7 +226,8 @@ const { t } = useI18n()
...
@@ -224,7 +226,8 @@ const { t } = useI18n()
// OAuth composables
// OAuth composables
const
claudeOAuth
=
useAccountOAuth
()
const
claudeOAuth
=
useAccountOAuth
()
const
openaiOAuth
=
useOpenAIOAuth
()
const
openaiOAuth
=
useOpenAIOAuth
({
platform
:
'
openai
'
})
const
soraOAuth
=
useOpenAIOAuth
({
platform
:
'
sora
'
})
const
geminiOAuth
=
useGeminiOAuth
()
const
geminiOAuth
=
useGeminiOAuth
()
const
antigravityOAuth
=
useAntigravityOAuth
()
const
antigravityOAuth
=
useAntigravityOAuth
()
...
@@ -237,31 +240,34 @@ const geminiOAuthType = ref<'code_assist' | 'google_one' | 'ai_studio'>('code_as
...
@@ -237,31 +240,34 @@ const geminiOAuthType = ref<'code_assist' | 'google_one' | 'ai_studio'>('code_as
// Computed - check platform
// Computed - check platform
const
isOpenAI
=
computed
(()
=>
props
.
account
?.
platform
===
'
openai
'
)
const
isOpenAI
=
computed
(()
=>
props
.
account
?.
platform
===
'
openai
'
)
const
isSora
=
computed
(()
=>
props
.
account
?.
platform
===
'
sora
'
)
const
isOpenAILike
=
computed
(()
=>
isOpenAI
.
value
||
isSora
.
value
)
const
isGemini
=
computed
(()
=>
props
.
account
?.
platform
===
'
gemini
'
)
const
isGemini
=
computed
(()
=>
props
.
account
?.
platform
===
'
gemini
'
)
const
isAnthropic
=
computed
(()
=>
props
.
account
?.
platform
===
'
anthropic
'
)
const
isAnthropic
=
computed
(()
=>
props
.
account
?.
platform
===
'
anthropic
'
)
const
isAntigravity
=
computed
(()
=>
props
.
account
?.
platform
===
'
antigravity
'
)
const
isAntigravity
=
computed
(()
=>
props
.
account
?.
platform
===
'
antigravity
'
)
const
activeOpenAIOAuth
=
computed
(()
=>
(
isSora
.
value
?
soraOAuth
:
openaiOAuth
))
// Computed - current OAuth state based on platform
// Computed - current OAuth state based on platform
const
currentAuthUrl
=
computed
(()
=>
{
const
currentAuthUrl
=
computed
(()
=>
{
if
(
isOpenAI
.
value
)
return
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
(
isGemini
.
value
)
return
geminiOAuth
.
authUrl
.
value
if
(
isAntigravity
.
value
)
return
antigravityOAuth
.
authUrl
.
value
if
(
isAntigravity
.
value
)
return
antigravityOAuth
.
authUrl
.
value
return
claudeOAuth
.
authUrl
.
value
return
claudeOAuth
.
authUrl
.
value
})
})
const
currentSessionId
=
computed
(()
=>
{
const
currentSessionId
=
computed
(()
=>
{
if
(
isOpenAI
.
value
)
return
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
(
isGemini
.
value
)
return
geminiOAuth
.
sessionId
.
value
if
(
isAntigravity
.
value
)
return
antigravityOAuth
.
sessionId
.
value
if
(
isAntigravity
.
value
)
return
antigravityOAuth
.
sessionId
.
value
return
claudeOAuth
.
sessionId
.
value
return
claudeOAuth
.
sessionId
.
value
})
})
const
currentLoading
=
computed
(()
=>
{
const
currentLoading
=
computed
(()
=>
{
if
(
isOpenAI
.
value
)
return
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
(
isGemini
.
value
)
return
geminiOAuth
.
loading
.
value
if
(
isAntigravity
.
value
)
return
antigravityOAuth
.
loading
.
value
if
(
isAntigravity
.
value
)
return
antigravityOAuth
.
loading
.
value
return
claudeOAuth
.
loading
.
value
return
claudeOAuth
.
loading
.
value
})
})
const
currentError
=
computed
(()
=>
{
const
currentError
=
computed
(()
=>
{
if
(
isOpenAI
.
value
)
return
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
(
isGemini
.
value
)
return
geminiOAuth
.
error
.
value
if
(
isAntigravity
.
value
)
return
antigravityOAuth
.
error
.
value
if
(
isAntigravity
.
value
)
return
antigravityOAuth
.
error
.
value
return
claudeOAuth
.
error
.
value
return
claudeOAuth
.
error
.
value
...
@@ -269,8 +275,8 @@ const currentError = computed(() => {
...
@@ -269,8 +275,8 @@ const currentError = computed(() => {
// Computed
// Computed
const
isManualInputMethod
=
computed
(()
=>
{
const
isManualInputMethod
=
computed
(()
=>
{
// OpenAI/Gemini/Antigravity always use manual input (no cookie auth option)
// OpenAI/
Sora/
Gemini/Antigravity always use manual input (no cookie auth option)
return
isOpenAI
.
value
||
isGemini
.
value
||
isAntigravity
.
value
||
oauthFlowRef
.
value
?.
inputMethod
===
'
manual
'
return
isOpenAI
Like
.
value
||
isGemini
.
value
||
isAntigravity
.
value
||
oauthFlowRef
.
value
?.
inputMethod
===
'
manual
'
})
})
const
canExchangeCode
=
computed
(()
=>
{
const
canExchangeCode
=
computed
(()
=>
{
...
@@ -313,6 +319,7 @@ const resetState = () => {
...
@@ -313,6 +319,7 @@ const resetState = () => {
geminiOAuthType
.
value
=
'
code_assist
'
geminiOAuthType
.
value
=
'
code_assist
'
claudeOAuth
.
resetState
()
claudeOAuth
.
resetState
()
openaiOAuth
.
resetState
()
openaiOAuth
.
resetState
()
soraOAuth
.
resetState
()
geminiOAuth
.
resetState
()
geminiOAuth
.
resetState
()
antigravityOAuth
.
resetState
()
antigravityOAuth
.
resetState
()
oauthFlowRef
.
value
?.
reset
()
oauthFlowRef
.
value
?.
reset
()
...
@@ -325,8 +332,8 @@ const handleClose = () => {
...
@@ -325,8 +332,8 @@ const handleClose = () => {
const
handleGenerateUrl
=
async
()
=>
{
const
handleGenerateUrl
=
async
()
=>
{
if
(
!
props
.
account
)
return
if
(
!
props
.
account
)
return
if
(
isOpenAI
.
value
)
{
if
(
isOpenAI
Like
.
value
)
{
await
o
pen
ai
OAuth
.
generateAuthUrl
(
props
.
account
.
proxy_id
)
await
activeO
pen
AI
OAuth
.
value
.
generateAuthUrl
(
props
.
account
.
proxy_id
)
}
else
if
(
isGemini
.
value
)
{
}
else
if
(
isGemini
.
value
)
{
const
creds
=
(
props
.
account
.
credentials
||
{})
as
Record
<
string
,
unknown
>
const
creds
=
(
props
.
account
.
credentials
||
{})
as
Record
<
string
,
unknown
>
const
tierId
=
typeof
creds
.
tier_id
===
'
string
'
?
creds
.
tier_id
:
undefined
const
tierId
=
typeof
creds
.
tier_id
===
'
string
'
?
creds
.
tier_id
:
undefined
...
@@ -345,21 +352,29 @@ const handleExchangeCode = async () => {
...
@@ -345,21 +352,29 @@ const handleExchangeCode = async () => {
const
authCode
=
oauthFlowRef
.
value
?.
authCode
||
''
const
authCode
=
oauthFlowRef
.
value
?.
authCode
||
''
if
(
!
authCode
.
trim
())
return
if
(
!
authCode
.
trim
())
return
if
(
isOpenAI
.
value
)
{
if
(
isOpenAI
Like
.
value
)
{
// OpenAI OAuth flow
// OpenAI OAuth flow
const
sessionId
=
openaiOAuth
.
sessionId
.
value
const
oauthClient
=
activeOpenAIOAuth
.
value
const
sessionId
=
oauthClient
.
sessionId
.
value
if
(
!
sessionId
)
return
if
(
!
sessionId
)
return
const
stateToUse
=
(
oauthFlowRef
.
value
?.
oauthState
||
oauthClient
.
oauthState
.
value
||
''
).
trim
()
if
(
!
stateToUse
)
{
oauthClient
.
error
.
value
=
t
(
'
admin.accounts.oauth.authFailed
'
)
appStore
.
showError
(
oauthClient
.
error
.
value
)
return
}
const
tokenInfo
=
await
o
penaiOAuth
.
exchangeAuthCode
(
const
tokenInfo
=
await
o
authClient
.
exchangeAuthCode
(
authCode
.
trim
(),
authCode
.
trim
(),
sessionId
,
sessionId
,
stateToUse
,
props
.
account
.
proxy_id
props
.
account
.
proxy_id
)
)
if
(
!
tokenInfo
)
return
if
(
!
tokenInfo
)
return
// Build credentials and extra info
// Build credentials and extra info
const
credentials
=
o
penaiOAuth
.
buildCredentials
(
tokenInfo
)
const
credentials
=
o
authClient
.
buildCredentials
(
tokenInfo
)
const
extra
=
o
penaiOAuth
.
buildExtraInfo
(
tokenInfo
)
const
extra
=
o
authClient
.
buildExtraInfo
(
tokenInfo
)
try
{
try
{
// Update account with new credentials
// Update account with new credentials
...
@@ -376,8 +391,8 @@ const handleExchangeCode = async () => {
...
@@ -376,8 +391,8 @@ const handleExchangeCode = async () => {
emit
(
'
reauthorized
'
,
updatedAccount
)
emit
(
'
reauthorized
'
,
updatedAccount
)
handleClose
()
handleClose
()
}
catch
(
error
:
any
)
{
}
catch
(
error
:
any
)
{
o
penaiOAuth
.
error
.
value
=
error
.
response
?.
data
?.
detail
||
t
(
'
admin.accounts.oauth.authFailed
'
)
o
authClient
.
error
.
value
=
error
.
response
?.
data
?.
detail
||
t
(
'
admin.accounts.oauth.authFailed
'
)
appStore
.
showError
(
o
penaiOAuth
.
error
.
value
)
appStore
.
showError
(
o
authClient
.
error
.
value
)
}
}
}
else
if
(
isGemini
.
value
)
{
}
else
if
(
isGemini
.
value
)
{
const
sessionId
=
geminiOAuth
.
sessionId
.
value
const
sessionId
=
geminiOAuth
.
sessionId
.
value
...
@@ -490,7 +505,7 @@ const handleExchangeCode = async () => {
...
@@ -490,7 +505,7 @@ const handleExchangeCode = async () => {
}
}
const
handleCookieAuth
=
async
(
sessionKey
:
string
)
=>
{
const
handleCookieAuth
=
async
(
sessionKey
:
string
)
=>
{
if
(
!
props
.
account
||
isOpenAI
.
value
)
return
if
(
!
props
.
account
||
isOpenAI
Like
.
value
)
return
claudeOAuth
.
loading
.
value
=
true
claudeOAuth
.
loading
.
value
=
true
claudeOAuth
.
error
.
value
=
''
claudeOAuth
.
error
.
value
=
''
...
...
frontend/src/components/admin/usage/UsageTable.vue
View file @
987589ea
...
@@ -70,6 +70,8 @@
...
@@ -70,6 +70,8 @@
<div
v-if=
"row.cache_creation_tokens > 0"
class=
"inline-flex items-center gap-1"
>
<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>
<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
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>
</div>
</div>
</div>
...
@@ -157,9 +159,36 @@
...
@@ -157,9 +159,36 @@
<span
class=
"text-gray-400"
>
{{ t('admin.usage.outputTokens') }}
</span>
<span
class=
"text-gray-400"
>
{{ t('admin.usage.outputTokens') }}
</span>
<span
class=
"font-medium text-white"
>
{{ tokenTooltipData.output_tokens.toLocaleString() }}
</span>
<span
class=
"font-medium text-white"
>
{{ tokenTooltipData.output_tokens.toLocaleString() }}
</span>
</div>
</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"
>
<span
class=
"text-gray-400"
>
{{ t('admin.usage.cacheCreationTokens') }}
</span>
<!-- 有 5m/1h 明细时,展开显示 -->
<span
class=
"font-medium text-white"
>
{{ tokenTooltipData.cache_creation_tokens.toLocaleString() }}
</span>
<
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>
<div
v-if=
"tokenTooltipData && tokenTooltipData.cache_read_tokens > 0"
class=
"flex items-center justify-between gap-4"
>
<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=
"text-gray-400"
>
{{ t('admin.usage.cacheReadTokens') }}
</span>
...
...
frontend/src/components/common/StatCard.vue
View file @
987589ea
...
@@ -6,7 +6,7 @@
...
@@ -6,7 +6,7 @@
<div
class=
"min-w-0 flex-1"
>
<div
class=
"min-w-0 flex-1"
>
<p
class=
"stat-label truncate"
>
{{
title
}}
</p>
<p
class=
"stat-label truncate"
>
{{
title
}}
</p>
<div
class=
"mt-1 flex items-baseline gap-2"
>
<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]"
>
<span
v-if=
"change !== undefined"
:class=
"['stat-trend', trendClass]"
>
<Icon
<Icon
v-if=
"changeType !== 'neutral'"
v-if=
"changeType !== 'neutral'"
...
...
frontend/src/components/layout/AppSidebar.vue
View file @
987589ea
...
@@ -10,7 +10,7 @@
...
@@ -10,7 +10,7 @@
<div
class=
"sidebar-header"
>
<div
class=
"sidebar-header"
>
<!-- Custom Logo or Default Logo -->
<!-- Custom Logo or Default Logo -->
<div
class=
"flex h-9 w-9 items-center justify-center overflow-hidden rounded-xl shadow-glow"
>
<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>
</div>
<transition
name=
"fade"
>
<transition
name=
"fade"
>
<div
v-if=
"!sidebarCollapsed"
class=
"flex flex-col"
>
<div
v-if=
"!sidebarCollapsed"
class=
"flex flex-col"
>
...
@@ -167,6 +167,7 @@ const isDark = ref(document.documentElement.classList.contains('dark'))
...
@@ -167,6 +167,7 @@ const isDark = ref(document.documentElement.classList.contains('dark'))
const
siteName
=
computed
(()
=>
appStore
.
siteName
)
const
siteName
=
computed
(()
=>
appStore
.
siteName
)
const
siteLogo
=
computed
(()
=>
appStore
.
siteLogo
)
const
siteLogo
=
computed
(()
=>
appStore
.
siteLogo
)
const
siteVersion
=
computed
(()
=>
appStore
.
siteVersion
)
const
siteVersion
=
computed
(()
=>
appStore
.
siteVersion
)
const
settingsLoaded
=
computed
(()
=>
appStore
.
publicSettingsLoaded
)
// SVG Icon Components
// SVG Icon Components
const
DashboardIcon
=
{
const
DashboardIcon
=
{
...
...
frontend/src/composables/useAccountOAuth.ts
View file @
987589ea
...
@@ -3,7 +3,7 @@ import { useAppStore } from '@/stores/app'
...
@@ -3,7 +3,7 @@ import { useAppStore } from '@/stores/app'
import
{
adminAPI
}
from
'
@/api/admin
'
import
{
adminAPI
}
from
'
@/api/admin
'
export
type
AddMethod
=
'
oauth
'
|
'
setup-token
'
export
type
AddMethod
=
'
oauth
'
|
'
setup-token
'
export
type
AuthInputMethod
=
'
manual
'
|
'
cookie
'
|
'
refresh_token
'
export
type
AuthInputMethod
=
'
manual
'
|
'
cookie
'
|
'
refresh_token
'
|
'
session_token
'
export
interface
OAuthState
{
export
interface
OAuthState
{
authUrl
:
string
authUrl
:
string
...
...
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