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
c7e18bd5
Unverified
Commit
c7e18bd5
authored
Feb 25, 2026
by
Wesley Liddick
Committed by
GitHub
Feb 25, 2026
Browse files
Merge pull request #627 from touwaeriol/pr/bugfixes-and-enhancements
feat: 反重力(Antigravity)增强、Failover 重构及新模型支持
parents
516f8f28
8365a832
Changes
41
Hide whitespace changes
Inline
Side-by-side
backend/internal/service/antigravity_model_mapping_test.go
View file @
c7e18bd5
...
@@ -76,6 +76,12 @@ func TestAntigravityGatewayService_GetMappedModel(t *testing.T) {
...
@@ -76,6 +76,12 @@ func TestAntigravityGatewayService_GetMappedModel(t *testing.T) {
},
},
// 3. 默认映射中的透传(映射到自己)
// 3. 默认映射中的透传(映射到自己)
{
name
:
"默认映射透传 - claude-sonnet-4-6"
,
requestedModel
:
"claude-sonnet-4-6"
,
accountMapping
:
nil
,
expected
:
"claude-sonnet-4-6"
,
},
{
{
name
:
"默认映射透传 - claude-sonnet-4-5"
,
name
:
"默认映射透传 - claude-sonnet-4-5"
,
requestedModel
:
"claude-sonnet-4-5"
,
requestedModel
:
"claude-sonnet-4-5"
,
...
...
backend/internal/service/antigravity_rate_limit_test.go
View file @
c7e18bd5
...
@@ -197,6 +197,22 @@ func TestHandleUpstreamError_429_NonModelRateLimit(t *testing.T) {
...
@@ -197,6 +197,22 @@ func TestHandleUpstreamError_429_NonModelRateLimit(t *testing.T) {
require
.
Equal
(
t
,
"claude-sonnet-4-5"
,
repo
.
modelRateLimitCalls
[
0
]
.
modelKey
)
require
.
Equal
(
t
,
"claude-sonnet-4-5"
,
repo
.
modelRateLimitCalls
[
0
]
.
modelKey
)
}
}
// TestHandleUpstreamError_429_NonModelRateLimit_UsesMappedModelKey 测试 429 非模型限流场景
// 验证:requestedModel 会被映射到 Antigravity 最终模型(例如 claude-opus-4-6 -> claude-opus-4-6-thinking)
func
TestHandleUpstreamError_429_NonModelRateLimit_UsesMappedModelKey
(
t
*
testing
.
T
)
{
repo
:=
&
stubAntigravityAccountRepo
{}
svc
:=
&
AntigravityGatewayService
{
accountRepo
:
repo
}
account
:=
&
Account
{
ID
:
20
,
Name
:
"acc-20"
,
Platform
:
PlatformAntigravity
}
body
:=
buildGeminiRateLimitBody
(
"5s"
)
result
:=
svc
.
handleUpstreamError
(
context
.
Background
(),
"[test]"
,
account
,
http
.
StatusTooManyRequests
,
http
.
Header
{},
body
,
"claude-opus-4-6"
,
0
,
""
,
false
)
require
.
Nil
(
t
,
result
)
require
.
Len
(
t
,
repo
.
modelRateLimitCalls
,
1
)
require
.
Equal
(
t
,
"claude-opus-4-6-thinking"
,
repo
.
modelRateLimitCalls
[
0
]
.
modelKey
)
}
// TestHandleUpstreamError_503_ModelCapacityExhausted 测试 503 模型容量不足场景
// TestHandleUpstreamError_503_ModelCapacityExhausted 测试 503 模型容量不足场景
// MODEL_CAPACITY_EXHAUSTED 时应等待重试,不切换账号
// MODEL_CAPACITY_EXHAUSTED 时应等待重试,不切换账号
func
TestHandleUpstreamError_503_ModelCapacityExhausted
(
t
*
testing
.
T
)
{
func
TestHandleUpstreamError_503_ModelCapacityExhausted
(
t
*
testing
.
T
)
{
...
...
backend/internal/service/billing_service.go
View file @
c7e18bd5
...
@@ -133,6 +133,18 @@ func (s *BillingService) initFallbackPricing() {
...
@@ -133,6 +133,18 @@ func (s *BillingService) initFallbackPricing() {
CacheReadPricePerToken
:
0.03e-6
,
// $0.03 per MTok
CacheReadPricePerToken
:
0.03e-6
,
// $0.03 per MTok
SupportsCacheBreakdown
:
false
,
SupportsCacheBreakdown
:
false
,
}
}
// Claude 4.6 Opus (与4.5同价)
s
.
fallbackPrices
[
"claude-opus-4.6"
]
=
s
.
fallbackPrices
[
"claude-opus-4.5"
]
// Gemini 3.1 Pro
s
.
fallbackPrices
[
"gemini-3.1-pro"
]
=
&
ModelPricing
{
InputPricePerToken
:
2e-6
,
// $2 per MTok
OutputPricePerToken
:
12e-6
,
// $12 per MTok
CacheCreationPricePerToken
:
2e-6
,
// $2 per MTok
CacheReadPricePerToken
:
0.2e-6
,
// $0.20 per MTok
SupportsCacheBreakdown
:
false
,
}
}
}
// getFallbackPricing 根据模型系列获取回退价格
// getFallbackPricing 根据模型系列获取回退价格
...
@@ -141,6 +153,9 @@ func (s *BillingService) getFallbackPricing(model string) *ModelPricing {
...
@@ -141,6 +153,9 @@ func (s *BillingService) getFallbackPricing(model string) *ModelPricing {
// 按模型系列匹配
// 按模型系列匹配
if
strings
.
Contains
(
modelLower
,
"opus"
)
{
if
strings
.
Contains
(
modelLower
,
"opus"
)
{
if
strings
.
Contains
(
modelLower
,
"4.6"
)
||
strings
.
Contains
(
modelLower
,
"4-6"
)
{
return
s
.
fallbackPrices
[
"claude-opus-4.6"
]
}
if
strings
.
Contains
(
modelLower
,
"4.5"
)
||
strings
.
Contains
(
modelLower
,
"4-5"
)
{
if
strings
.
Contains
(
modelLower
,
"4.5"
)
||
strings
.
Contains
(
modelLower
,
"4-5"
)
{
return
s
.
fallbackPrices
[
"claude-opus-4.5"
]
return
s
.
fallbackPrices
[
"claude-opus-4.5"
]
}
}
...
@@ -158,6 +173,9 @@ func (s *BillingService) getFallbackPricing(model string) *ModelPricing {
...
@@ -158,6 +173,9 @@ func (s *BillingService) getFallbackPricing(model string) *ModelPricing {
}
}
return
s
.
fallbackPrices
[
"claude-3-haiku"
]
return
s
.
fallbackPrices
[
"claude-3-haiku"
]
}
}
if
strings
.
Contains
(
modelLower
,
"gemini-3.1-pro"
)
||
strings
.
Contains
(
modelLower
,
"gemini-3-1-pro"
)
{
return
s
.
fallbackPrices
[
"gemini-3.1-pro"
]
}
// 默认使用Sonnet价格
// 默认使用Sonnet价格
return
s
.
fallbackPrices
[
"claude-sonnet-4"
]
return
s
.
fallbackPrices
[
"claude-sonnet-4"
]
...
...
backend/internal/service/gateway_multiplatform_test.go
View file @
c7e18bd5
...
@@ -895,6 +895,55 @@ func TestGatewayService_SelectAccountForModelWithPlatform_GeminiPreferOAuth(t *t
...
@@ -895,6 +895,55 @@ func TestGatewayService_SelectAccountForModelWithPlatform_GeminiPreferOAuth(t *t
require
.
Equal
(
t
,
int64
(
2
),
acc
.
ID
)
require
.
Equal
(
t
,
int64
(
2
),
acc
.
ID
)
}
}
func
TestGatewayService_SelectAccountForModelWithPlatform_GeminiAPIKeyModelMappingFilter
(
t
*
testing
.
T
)
{
ctx
:=
context
.
Background
()
repo
:=
&
mockAccountRepoForPlatform
{
accounts
:
[]
Account
{
{
ID
:
1
,
Platform
:
PlatformGemini
,
Type
:
AccountTypeAPIKey
,
Priority
:
1
,
Status
:
StatusActive
,
Schedulable
:
true
,
Credentials
:
map
[
string
]
any
{
"model_mapping"
:
map
[
string
]
any
{
"gemini-2.5-pro"
:
"gemini-2.5-pro"
}},
},
{
ID
:
2
,
Platform
:
PlatformGemini
,
Type
:
AccountTypeAPIKey
,
Priority
:
2
,
Status
:
StatusActive
,
Schedulable
:
true
,
Credentials
:
map
[
string
]
any
{
"model_mapping"
:
map
[
string
]
any
{
"gemini-2.5-flash"
:
"gemini-2.5-flash"
}},
},
},
accountsByID
:
map
[
int64
]
*
Account
{},
}
for
i
:=
range
repo
.
accounts
{
repo
.
accountsByID
[
repo
.
accounts
[
i
]
.
ID
]
=
&
repo
.
accounts
[
i
]
}
cache
:=
&
mockGatewayCacheForPlatform
{}
svc
:=
&
GatewayService
{
accountRepo
:
repo
,
cache
:
cache
,
cfg
:
testConfig
(),
}
acc
,
err
:=
svc
.
selectAccountForModelWithPlatform
(
ctx
,
nil
,
""
,
"gemini-2.5-flash"
,
nil
,
PlatformGemini
)
require
.
NoError
(
t
,
err
)
require
.
NotNil
(
t
,
acc
)
require
.
Equal
(
t
,
int64
(
2
),
acc
.
ID
,
"应过滤不支持请求模型的 APIKey 账号"
)
acc
,
err
=
svc
.
selectAccountForModelWithPlatform
(
ctx
,
nil
,
""
,
"gemini-3-pro-preview"
,
nil
,
PlatformGemini
)
require
.
Error
(
t
,
err
)
require
.
Nil
(
t
,
acc
)
require
.
Contains
(
t
,
err
.
Error
(),
"supporting model"
)
}
func
TestGatewayService_SelectAccountForModelWithPlatform_StickyInGroup
(
t
*
testing
.
T
)
{
func
TestGatewayService_SelectAccountForModelWithPlatform_StickyInGroup
(
t
*
testing
.
T
)
{
ctx
:=
context
.
Background
()
ctx
:=
context
.
Background
()
groupID
:=
int64
(
50
)
groupID
:=
int64
(
50
)
...
@@ -1070,6 +1119,36 @@ func TestGatewayService_isModelSupportedByAccount(t *testing.T) {
...
@@ -1070,6 +1119,36 @@ func TestGatewayService_isModelSupportedByAccount(t *testing.T) {
model
:
"claude-3-5-sonnet-20241022"
,
model
:
"claude-3-5-sonnet-20241022"
,
expected
:
true
,
expected
:
true
,
},
},
{
name
:
"Gemini平台-无映射配置-支持所有模型"
,
account
:
&
Account
{
Platform
:
PlatformGemini
,
Type
:
AccountTypeAPIKey
},
model
:
"gemini-2.5-flash"
,
expected
:
true
,
},
{
name
:
"Gemini平台-有映射配置-只支持配置的模型"
,
account
:
&
Account
{
Platform
:
PlatformGemini
,
Type
:
AccountTypeAPIKey
,
Credentials
:
map
[
string
]
any
{
"model_mapping"
:
map
[
string
]
any
{
"gemini-2.5-pro"
:
"gemini-2.5-pro"
},
},
},
model
:
"gemini-2.5-flash"
,
expected
:
false
,
},
{
name
:
"Gemini平台-有映射配置-支持配置的模型"
,
account
:
&
Account
{
Platform
:
PlatformGemini
,
Type
:
AccountTypeAPIKey
,
Credentials
:
map
[
string
]
any
{
"model_mapping"
:
map
[
string
]
any
{
"gemini-2.5-pro"
:
"gemini-2.5-pro"
},
},
},
model
:
"gemini-2.5-pro"
,
expected
:
true
,
},
}
}
for
_
,
tt
:=
range
tests
{
for
_
,
tt
:=
range
tests
{
...
...
backend/internal/service/gateway_service.go
View file @
c7e18bd5
...
@@ -2825,10 +2825,6 @@ func (s *GatewayService) isModelSupportedByAccount(account *Account, requestedMo
...
@@ -2825,10 +2825,6 @@ func (s *GatewayService) isModelSupportedByAccount(account *Account, requestedMo
if
account
.
Platform
==
PlatformAnthropic
&&
account
.
Type
!=
AccountTypeAPIKey
{
if
account
.
Platform
==
PlatformAnthropic
&&
account
.
Type
!=
AccountTypeAPIKey
{
requestedModel
=
claude
.
NormalizeModelID
(
requestedModel
)
requestedModel
=
claude
.
NormalizeModelID
(
requestedModel
)
}
}
// Gemini API Key 账户直接透传,由上游判断模型是否支持
if
account
.
Platform
==
PlatformGemini
&&
account
.
Type
==
AccountTypeAPIKey
{
return
true
}
// 其他平台使用账户的模型支持检查
// 其他平台使用账户的模型支持检查
return
account
.
IsModelSupported
(
requestedModel
)
return
account
.
IsModelSupported
(
requestedModel
)
}
}
...
...
backend/internal/service/model_rate_limit_test.go
View file @
c7e18bd5
...
@@ -107,12 +107,12 @@ func TestIsModelRateLimited(t *testing.T) {
...
@@ -107,12 +107,12 @@ func TestIsModelRateLimited(t *testing.T) {
expected
:
true
,
expected
:
true
,
},
},
{
{
name
:
"antigravity platform - gemini-3-pro-preview mapped to gemini-3
.1
-pro-high"
,
name
:
"antigravity platform - gemini-3-pro-preview mapped to gemini-3-pro-high"
,
account
:
&
Account
{
account
:
&
Account
{
Platform
:
PlatformAntigravity
,
Platform
:
PlatformAntigravity
,
Extra
:
map
[
string
]
any
{
Extra
:
map
[
string
]
any
{
modelRateLimitsKey
:
map
[
string
]
any
{
modelRateLimitsKey
:
map
[
string
]
any
{
"gemini-3
.1
-pro-high"
:
map
[
string
]
any
{
"gemini-3-pro-high"
:
map
[
string
]
any
{
"rate_limit_reset_at"
:
future
,
"rate_limit_reset_at"
:
future
,
},
},
},
},
...
...
backend/migrations/058_add_sonnet46_to_model_mapping.sql
0 → 100644
View file @
c7e18bd5
-- Add claude-sonnet-4-6 to model_mapping for all Antigravity accounts
--
-- Background:
-- Antigravity now supports claude-sonnet-4-6
--
-- Strategy:
-- Directly overwrite the entire model_mapping with updated mappings
-- This ensures consistency with DefaultAntigravityModelMapping in constants.go
UPDATE
accounts
SET
credentials
=
jsonb_set
(
credentials
,
'{model_mapping}'
,
'{
"claude-opus-4-6-thinking": "claude-opus-4-6-thinking",
"claude-opus-4-6": "claude-opus-4-6-thinking",
"claude-opus-4-5-thinking": "claude-opus-4-6-thinking",
"claude-opus-4-5-20251101": "claude-opus-4-6-thinking",
"claude-sonnet-4-6": "claude-sonnet-4-6",
"claude-sonnet-4-5": "claude-sonnet-4-5",
"claude-sonnet-4-5-thinking": "claude-sonnet-4-5-thinking",
"claude-sonnet-4-5-20250929": "claude-sonnet-4-5",
"claude-haiku-4-5": "claude-sonnet-4-5",
"claude-haiku-4-5-20251001": "claude-sonnet-4-5",
"gemini-2.5-flash": "gemini-2.5-flash",
"gemini-2.5-flash-lite": "gemini-2.5-flash-lite",
"gemini-2.5-flash-thinking": "gemini-2.5-flash-thinking",
"gemini-2.5-pro": "gemini-2.5-pro",
"gemini-3-flash": "gemini-3-flash",
"gemini-3-pro-high": "gemini-3-pro-high",
"gemini-3-pro-low": "gemini-3-pro-low",
"gemini-3-pro-image": "gemini-3-pro-image",
"gemini-3-flash-preview": "gemini-3-flash",
"gemini-3-pro-preview": "gemini-3-pro-high",
"gemini-3-pro-image-preview": "gemini-3-pro-image",
"gpt-oss-120b-medium": "gpt-oss-120b-medium",
"tab_flash_lite_preview": "tab_flash_lite_preview"
}'
::
jsonb
)
WHERE
platform
=
'antigravity'
AND
deleted_at
IS
NULL
AND
credentials
->
'model_mapping'
IS
NOT
NULL
;
backend/migrations/059_add_gemini31_pro_to_model_mapping.sql
0 → 100644
View file @
c7e18bd5
-- Add gemini-3.1-pro-high, gemini-3.1-pro-low, gemini-3.1-pro-preview to model_mapping
--
-- Background:
-- Antigravity now supports gemini-3.1-pro-high and gemini-3.1-pro-low
--
-- Strategy:
-- Directly overwrite the entire model_mapping with updated mappings
-- This ensures consistency with DefaultAntigravityModelMapping in constants.go
UPDATE
accounts
SET
credentials
=
jsonb_set
(
credentials
,
'{model_mapping}'
,
'{
"claude-opus-4-6-thinking": "claude-opus-4-6-thinking",
"claude-opus-4-6": "claude-opus-4-6-thinking",
"claude-opus-4-5-thinking": "claude-opus-4-6-thinking",
"claude-opus-4-5-20251101": "claude-opus-4-6-thinking",
"claude-sonnet-4-6": "claude-sonnet-4-6",
"claude-sonnet-4-5": "claude-sonnet-4-5",
"claude-sonnet-4-5-thinking": "claude-sonnet-4-5-thinking",
"claude-sonnet-4-5-20250929": "claude-sonnet-4-5",
"claude-haiku-4-5": "claude-sonnet-4-5",
"claude-haiku-4-5-20251001": "claude-sonnet-4-5",
"gemini-2.5-flash": "gemini-2.5-flash",
"gemini-2.5-flash-lite": "gemini-2.5-flash-lite",
"gemini-2.5-flash-thinking": "gemini-2.5-flash-thinking",
"gemini-2.5-pro": "gemini-2.5-pro",
"gemini-3-flash": "gemini-3-flash",
"gemini-3-pro-high": "gemini-3-pro-high",
"gemini-3-pro-low": "gemini-3-pro-low",
"gemini-3-pro-image": "gemini-3-pro-image",
"gemini-3-flash-preview": "gemini-3-flash",
"gemini-3-pro-preview": "gemini-3-pro-high",
"gemini-3-pro-image-preview": "gemini-3-pro-image",
"gemini-3.1-pro-high": "gemini-3.1-pro-high",
"gemini-3.1-pro-low": "gemini-3.1-pro-low",
"gemini-3.1-pro-preview": "gemini-3.1-pro-high",
"gpt-oss-120b-medium": "gpt-oss-120b-medium",
"tab_flash_lite_preview": "tab_flash_lite_preview"
}'
::
jsonb
)
WHERE
platform
=
'antigravity'
AND
deleted_at
IS
NULL
AND
credentials
->
'model_mapping'
IS
NOT
NULL
;
frontend/src/api/admin/accounts.ts
View file @
c7e18bd5
...
@@ -15,7 +15,9 @@ import type {
...
@@ -15,7 +15,9 @@ import type {
AccountUsageStatsResponse
,
AccountUsageStatsResponse
,
TempUnschedulableStatus
,
TempUnschedulableStatus
,
AdminDataPayload
,
AdminDataPayload
,
AdminDataImportResult
AdminDataImportResult
,
CheckMixedChannelRequest
,
CheckMixedChannelResponse
}
from
'
@/types
'
}
from
'
@/types
'
/**
/**
...
@@ -133,6 +135,16 @@ export async function update(id: number, updates: UpdateAccountRequest): Promise
...
@@ -133,6 +135,16 @@ export async function update(id: number, updates: UpdateAccountRequest): Promise
return
data
return
data
}
}
/**
* Check mixed-channel risk for account-group binding.
*/
export
async
function
checkMixedChannelRisk
(
payload
:
CheckMixedChannelRequest
):
Promise
<
CheckMixedChannelResponse
>
{
const
{
data
}
=
await
apiClient
.
post
<
CheckMixedChannelResponse
>
(
'
/admin/accounts/check-mixed-channel
'
,
payload
)
return
data
}
/**
/**
* Delete account
* Delete account
* @param id - Account ID
* @param id - Account ID
...
@@ -535,6 +547,7 @@ export const accountsAPI = {
...
@@ -535,6 +547,7 @@ export const accountsAPI = {
getById
,
getById
,
create
,
create
,
update
,
update
,
checkMixedChannelRisk
,
delete
:
deleteAccount
,
delete
:
deleteAccount
,
toggleStatus
,
toggleStatus
,
testAccount
,
testAccount
,
...
...
frontend/src/components/account/AccountStatusIndicator.vue
View file @
c7e18bd5
...
@@ -77,13 +77,23 @@
...
@@ -77,13 +77,23 @@
</div>
</div>
<!-- Model Rate Limit Indicators (Antigravity OAuth Smart Retry) -->
<!-- Model Rate Limit Indicators (Antigravity OAuth Smart Retry) -->
<
template
v-if=
"activeModelRateLimits.length > 0"
>
<div
<div
v-for=
"item in activeModelRateLimits"
:key=
"item.model"
class=
"group relative"
>
v-if=
"activeModelRateLimits.length > 0"
:class=
"[
activeModelRateLimits.length <= 4
? 'flex flex-col gap-1'
: activeModelRateLimits.length <= 8
? 'columns-2 gap-x-2'
: 'columns-3 gap-x-2'
]"
>
<div
v-for=
"item in activeModelRateLimits"
:key=
"item.model"
class=
"group relative mb-1 break-inside-avoid"
>
<span
<span
class=
"inline-flex items-center gap-1 rounded bg-purple-100 px-1.5 py-0.5 text-xs font-medium text-purple-700 dark:bg-purple-900/30 dark:text-purple-400"
class=
"inline-flex items-center gap-1 rounded bg-purple-100 px-1.5 py-0.5 text-xs font-medium text-purple-700 dark:bg-purple-900/30 dark:text-purple-400"
>
>
<Icon
name=
"exclamationTriangle"
size=
"xs"
:stroke-width=
"2"
/>
<Icon
name=
"exclamationTriangle"
size=
"xs"
:stroke-width=
"2"
/>
{{ formatScopeName(item.model) }}
{{ formatScopeName(item.model) }}
<span
class=
"text-[10px] opacity-70"
>
{{ formatModelResetTime(item.reset_at) }}
</span>
</span>
</span>
<!-- Tooltip -->
<!-- Tooltip -->
<div
<div
...
@@ -95,7 +105,7 @@
...
@@ -95,7 +105,7 @@
></div>
></div>
</div>
</div>
</div>
</div>
<
/
template
>
</
div
>
<!-- Overload Indicator (529) -->
<!-- Overload Indicator (529) -->
<div
v-if=
"isOverloaded"
class=
"group relative"
>
<div
v-if=
"isOverloaded"
class=
"group relative"
>
...
@@ -154,17 +164,50 @@ const activeModelRateLimits = computed(() => {
...
@@ -154,17 +164,50 @@ const activeModelRateLimits = computed(() => {
})
})
const
formatScopeName
=
(
scope
:
string
):
string
=>
{
const
formatScopeName
=
(
scope
:
string
):
string
=>
{
const
names
:
Record
<
string
,
string
>
=
{
const
aliases
:
Record
<
string
,
string
>
=
{
// Claude 系列
'
claude-opus-4-6-thinking
'
:
'
COpus46
'
,
'
claude-sonnet-4-6
'
:
'
CSon46
'
,
'
claude-sonnet-4-5
'
:
'
CSon45
'
,
'
claude-sonnet-4-5-thinking
'
:
'
CSon45T
'
,
// Gemini 2.5 系列
'
gemini-2.5-flash
'
:
'
G25F
'
,
'
gemini-2.5-flash-lite
'
:
'
G25FL
'
,
'
gemini-2.5-flash-thinking
'
:
'
G25FT
'
,
'
gemini-2.5-pro
'
:
'
G25P
'
,
// Gemini 3 系列
'
gemini-3-flash
'
:
'
G3F
'
,
'
gemini-3.1-pro-high
'
:
'
G3PH
'
,
'
gemini-3.1-pro-low
'
:
'
G3PL
'
,
'
gemini-3-pro-image
'
:
'
G3PI
'
,
// 其他
'
gpt-oss-120b-medium
'
:
'
GPT120
'
,
'
tab_flash_lite_preview
'
:
'
TabFL
'
,
// 旧版 scope 别名(兼容)
claude
:
'
Claude
'
,
claude
:
'
Claude
'
,
claude_sonnet
:
'
C
laude Sonnet
'
,
claude_sonnet
:
'
C
Son
'
,
claude_opus
:
'
C
laude
Opus
'
,
claude_opus
:
'
COpus
'
,
claude_haiku
:
'
C
laude
Haiku
'
,
claude_haiku
:
'
CHaiku
'
,
gemini_text
:
'
Gemini
'
,
gemini_text
:
'
Gemini
'
,
gemini_image
:
'
Im
age
'
,
gemini_image
:
'
G
Im
g
'
,
gemini_flash
:
'
G
emini
Flash
'
,
gemini_flash
:
'
GFlash
'
,
gemini_pro
:
'
G
emini
Pro
'
gemini_pro
:
'
GPro
'
,
}
}
return
names
[
scope
]
||
scope
return
aliases
[
scope
]
||
scope
}
const
formatModelResetTime
=
(
resetAt
:
string
):
string
=>
{
const
date
=
new
Date
(
resetAt
)
const
now
=
new
Date
()
const
diffMs
=
date
.
getTime
()
-
now
.
getTime
()
if
(
diffMs
<=
0
)
return
''
const
totalSecs
=
Math
.
floor
(
diffMs
/
1000
)
const
h
=
Math
.
floor
(
totalSecs
/
3600
)
const
m
=
Math
.
floor
((
totalSecs
%
3600
)
/
60
)
const
s
=
totalSecs
%
60
if
(
h
>
0
)
return
`
${
h
}
h
${
m
}
m`
if
(
m
>
0
)
return
`
${
m
}
m
${
s
}
s`
return
`
${
s
}
s`
}
}
// Computed: is overloaded (529)
// Computed: is overloaded (529)
...
...
frontend/src/components/account/AccountUsageCell.vue
View file @
c7e18bd5
...
@@ -172,12 +172,12 @@
...
@@ -172,12 +172,12 @@
color=
"purple"
color=
"purple"
/>
/>
<!-- Claude
4.5
-->
<!-- Claude -->
<UsageProgressBar
<UsageProgressBar
v-if=
"antigravityClaude
45
UsageFromAPI !== null"
v-if=
"antigravityClaudeUsageFromAPI !== null"
:label=
"t('admin.accounts.usageWindow.claude
45
')"
:label=
"t('admin.accounts.usageWindow.claude')"
:utilization=
"antigravityClaude
45
UsageFromAPI.utilization"
:utilization=
"antigravityClaudeUsageFromAPI.utilization"
:resets-at=
"antigravityClaude
45
UsageFromAPI.resetTime"
:resets-at=
"antigravityClaudeUsageFromAPI.resetTime"
color=
"amber"
color=
"amber"
/>
/>
</div>
</div>
...
@@ -400,9 +400,12 @@ const antigravity3FlashUsageFromAPI = computed(() => getAntigravityUsageFromAPI(
...
@@ -400,9 +400,12 @@ const antigravity3FlashUsageFromAPI = computed(() => getAntigravityUsageFromAPI(
// Gemini 3 Image from API
// Gemini 3 Image from API
const
antigravity3ImageUsageFromAPI
=
computed
(()
=>
getAntigravityUsageFromAPI
([
'
gemini-3-pro-image
'
]))
const
antigravity3ImageUsageFromAPI
=
computed
(()
=>
getAntigravityUsageFromAPI
([
'
gemini-3-pro-image
'
]))
// Claude 4.5 from API
// Claude from API (all Claude model variants)
const
antigravityClaude45UsageFromAPI
=
computed
(()
=>
const
antigravityClaudeUsageFromAPI
=
computed
(()
=>
getAntigravityUsageFromAPI
([
'
claude-sonnet-4-5
'
,
'
claude-opus-4-5-thinking
'
])
getAntigravityUsageFromAPI
([
'
claude-sonnet-4-5
'
,
'
claude-opus-4-5-thinking
'
,
'
claude-sonnet-4-6
'
,
'
claude-opus-4-6-thinking
'
,
])
)
)
// Antigravity 账户类型(从 load_code_assist 响应中提取)
// Antigravity 账户类型(从 load_code_assist 响应中提取)
...
...
frontend/src/components/account/BulkEditAccountModal.vue
View file @
c7e18bd5
...
@@ -209,7 +209,7 @@
...
@@ -209,7 +209,7 @@
<
div
v
-
if
=
"
modelMappings.length > 0
"
class
=
"
mb-3 space-y-2
"
>
<
div
v
-
if
=
"
modelMappings.length > 0
"
class
=
"
mb-3 space-y-2
"
>
<
div
<
div
v
-
for
=
"
(mapping, index) in modelMappings
"
v
-
for
=
"
(mapping, index) in modelMappings
"
:
key
=
"
getModelMappingKey(mapping)
"
:
key
=
"
index
"
class
=
"
flex items-center gap-2
"
class
=
"
flex items-center gap-2
"
>
>
<
input
<
input
...
@@ -654,7 +654,7 @@ import Select from '@/components/common/Select.vue'
...
@@ -654,7 +654,7 @@ import Select from '@/components/common/Select.vue'
import
ProxySelector
from
'
@/components/common/ProxySelector.vue
'
import
ProxySelector
from
'
@/components/common/ProxySelector.vue
'
import
GroupSelector
from
'
@/components/common/GroupSelector.vue
'
import
GroupSelector
from
'
@/components/common/GroupSelector.vue
'
import
Icon
from
'
@/components/icons/Icon.vue
'
import
Icon
from
'
@/components/icons/Icon.vue
'
import
{
createStableObjectKeyResolver
}
from
'
@/utils/stableObjectKey
'
import
{
buildModelMappingObject
as
buildModelMappingPayload
}
from
'
@/composables/useModelWhitelist
'
interface
Props
{
interface
Props
{
show
:
boolean
show
:
boolean
...
@@ -696,7 +696,6 @@ const baseUrl = ref('')
...
@@ -696,7 +696,6 @@ const baseUrl = ref('')
const
modelRestrictionMode
=
ref
<
'
whitelist
'
|
'
mapping
'
>
(
'
whitelist
'
)
const
modelRestrictionMode
=
ref
<
'
whitelist
'
|
'
mapping
'
>
(
'
whitelist
'
)
const
allowedModels
=
ref
<
string
[]
>
([])
const
allowedModels
=
ref
<
string
[]
>
([])
const
modelMappings
=
ref
<
ModelMapping
[]
>
([])
const
modelMappings
=
ref
<
ModelMapping
[]
>
([])
const
getModelMappingKey
=
createStableObjectKeyResolver
<
ModelMapping
>
(
'
bulk-model-mapping
'
)
const
selectedErrorCodes
=
ref
<
number
[]
>
([])
const
selectedErrorCodes
=
ref
<
number
[]
>
([])
const
customErrorCodeInput
=
ref
<
number
|
null
>
(
null
)
const
customErrorCodeInput
=
ref
<
number
|
null
>
(
null
)
const
interceptWarmupRequests
=
ref
(
false
)
const
interceptWarmupRequests
=
ref
(
false
)
...
@@ -707,7 +706,7 @@ const rateMultiplier = ref(1)
...
@@ -707,7 +706,7 @@ const rateMultiplier = ref(1)
const
status
=
ref
<
'
active
'
|
'
inactive
'
>
(
'
active
'
)
const
status
=
ref
<
'
active
'
|
'
inactive
'
>
(
'
active
'
)
const
groupIds
=
ref
<
number
[]
>
([])
const
groupIds
=
ref
<
number
[]
>
([])
// All models list (combined Anthropic + OpenAI)
// All models list (combined Anthropic + OpenAI
+ Gemini
)
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-sonnet-4-6
'
,
label
:
'
Claude Sonnet 4.6
'
}
,
...
@@ -719,17 +718,21 @@ const allModels = [
...
@@ -719,17 +718,21 @@ const allModels = [
{
value
:
'
claude-3-opus-20240229
'
,
label
:
'
Claude 3 Opus
'
}
,
{
value
:
'
claude-3-opus-20240229
'
,
label
:
'
Claude 3 Opus
'
}
,
{
value
:
'
claude-3-5-sonnet-20241022
'
,
label
:
'
Claude 3.5 Sonnet
'
}
,
{
value
:
'
claude-3-5-sonnet-20241022
'
,
label
:
'
Claude 3.5 Sonnet
'
}
,
{
value
:
'
claude-3-haiku-20240307
'
,
label
:
'
Claude 3 Haiku
'
}
,
{
value
:
'
claude-3-haiku-20240307
'
,
label
:
'
Claude 3 Haiku
'
}
,
{
value
:
'
gpt-5.3-codex-spark
'
,
label
:
'
GPT-5.3 Codex Spark
'
}
,
{
value
:
'
gpt-5.2-2025-12-11
'
,
label
:
'
GPT-5.2
'
}
,
{
value
:
'
gpt-5.2-2025-12-11
'
,
label
:
'
GPT-5.2
'
}
,
{
value
:
'
gpt-5.2-codex
'
,
label
:
'
GPT-5.2 Codex
'
}
,
{
value
:
'
gpt-5.2-codex
'
,
label
:
'
GPT-5.2 Codex
'
}
,
{
value
:
'
gpt-5.1-codex-max
'
,
label
:
'
GPT-5.1 Codex Max
'
}
,
{
value
:
'
gpt-5.1-codex-max
'
,
label
:
'
GPT-5.1 Codex Max
'
}
,
{
value
:
'
gpt-5.1-codex
'
,
label
:
'
GPT-5.1 Codex
'
}
,
{
value
:
'
gpt-5.1-codex
'
,
label
:
'
GPT-5.1 Codex
'
}
,
{
value
:
'
gpt-5.1-2025-11-13
'
,
label
:
'
GPT-5.1
'
}
,
{
value
:
'
gpt-5.1-2025-11-13
'
,
label
:
'
GPT-5.1
'
}
,
{
value
:
'
gpt-5.1-codex-mini
'
,
label
:
'
GPT-5.1 Codex Mini
'
}
,
{
value
:
'
gpt-5.1-codex-mini
'
,
label
:
'
GPT-5.1 Codex Mini
'
}
,
{
value
:
'
gpt-5-2025-08-07
'
,
label
:
'
GPT-5
'
}
{
value
:
'
gpt-5-2025-08-07
'
,
label
:
'
GPT-5
'
}
,
{
value
:
'
gemini-2.0-flash
'
,
label
:
'
Gemini 2.0 Flash
'
}
,
{
value
:
'
gemini-2.5-flash
'
,
label
:
'
Gemini 2.5 Flash
'
}
,
{
value
:
'
gemini-2.5-pro
'
,
label
:
'
Gemini 2.5 Pro
'
}
,
{
value
:
'
gemini-3-flash-preview
'
,
label
:
'
Gemini 3 Flash Preview
'
}
,
{
value
:
'
gemini-3-pro-preview
'
,
label
:
'
Gemini 3 Pro Preview
'
}
]
]
// Preset mappings (combined Anthropic + OpenAI)
// Preset mappings (combined Anthropic + OpenAI
+ Gemini
)
const
presetMappings
=
[
const
presetMappings
=
[
{
{
label
:
'
Sonnet 4
'
,
label
:
'
Sonnet 4
'
,
...
@@ -796,12 +799,6 @@ const presetMappings = [
...
@@ -796,12 +799,6 @@ const presetMappings = [
to
:
'
claude-sonnet-4-5-20250929
'
,
to
:
'
claude-sonnet-4-5-20250929
'
,
color
:
'
bg-amber-100 text-amber-700 hover:bg-amber-200 dark:bg-amber-900/30 dark:text-amber-400
'
color
:
'
bg-amber-100 text-amber-700 hover:bg-amber-200 dark:bg-amber-900/30 dark:text-amber-400
'
}
,
}
,
{
label
:
'
GPT-5.3 Codex Spark
'
,
from
:
'
gpt-5.3-codex-spark
'
,
to
:
'
gpt-5.3-codex-spark
'
,
color
:
'
bg-teal-100 text-teal-700 hover:bg-teal-200 dark:bg-teal-900/30 dark:text-teal-400
'
}
,
{
{
label
:
'
GPT-5.2
'
,
label
:
'
GPT-5.2
'
,
from
:
'
gpt-5.2-2025-12-11
'
,
from
:
'
gpt-5.2-2025-12-11
'
,
...
@@ -938,23 +935,11 @@ const removeErrorCode = (code: number) => {
...
@@ -938,23 +935,11 @@ const removeErrorCode = (code: number) => {
}
}
const
buildModelMappingObject
=
():
Record
<
string
,
string
>
|
null
=>
{
const
buildModelMappingObject
=
():
Record
<
string
,
string
>
|
null
=>
{
const
mapping
:
Record
<
string
,
string
>
=
{
}
return
buildModelMappingPayload
(
modelRestrictionMode
.
value
,
if
(
modelRestrictionMode
.
value
===
'
whitelist
'
)
{
allowedModels
.
value
,
for
(
const
model
of
allowedModels
.
value
)
{
modelMappings
.
value
mapping
[
model
]
=
model
)
}
}
else
{
for
(
const
m
of
modelMappings
.
value
)
{
const
from
=
m
.
from
.
trim
()
const
to
=
m
.
to
.
trim
()
if
(
from
&&
to
)
{
mapping
[
from
]
=
to
}
}
}
return
Object
.
keys
(
mapping
).
length
>
0
?
mapping
:
null
}
}
const
buildUpdatePayload
=
():
Record
<
string
,
unknown
>
|
null
=>
{
const
buildUpdatePayload
=
():
Record
<
string
,
unknown
>
|
null
=>
{
...
...
frontend/src/components/account/CreateAccountModal.vue
View file @
c7e18bd5
...
@@ -916,8 +916,8 @@
...
@@ -916,8 +916,8 @@
<p
class=
"input-hint"
>
{{
t
(
'
admin.accounts.gemini.tier.aiStudioHint
'
)
}}
</p>
<p
class=
"input-hint"
>
{{
t
(
'
admin.accounts.gemini.tier.aiStudioHint
'
)
}}
</p>
</div>
</div>
<!-- Model Restriction Section (
不适用于 Gemini,
Antigravity 已在上层条件排除) -->
<!-- Model Restriction Section (Antigravity 已在上层条件排除) -->
<div
v-if=
"form.platform !== 'gemini'"
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"
>
<label
class=
"input-label"
>
{{
t
(
'
admin.accounts.modelRestriction
'
)
}}
</label>
<label
class=
"input-label"
>
{{
t
(
'
admin.accounts.modelRestriction
'
)
}}
</label>
<div
<div
...
@@ -1200,34 +1200,6 @@
...
@@ -1200,34 +1200,6 @@
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<!--
Gemini
模型说明
-->
<
div
v
-
if
=
"
form.platform === 'gemini'
"
class
=
"
border-t border-gray-200 pt-4 dark:border-dark-600
"
>
<
div
class
=
"
rounded-lg bg-blue-50 p-4 dark:bg-blue-900/20
"
>
<
div
class
=
"
flex items-start gap-3
"
>
<
svg
class
=
"
h-5 w-5 flex-shrink-0 text-blue-600 dark:text-blue-400
"
fill
=
"
none
"
viewBox
=
"
0 0 24 24
"
stroke
=
"
currentColor
"
>
<
path
stroke
-
linecap
=
"
round
"
stroke
-
linejoin
=
"
round
"
stroke
-
width
=
"
2
"
d
=
"
M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z
"
/>
<
/svg
>
<
div
>
<
p
class
=
"
text-sm font-medium text-blue-800 dark:text-blue-300
"
>
{{
t
(
'
admin.accounts.gemini.modelPassthrough
'
)
}}
<
/p
>
<
p
class
=
"
mt-1 text-xs text-blue-700 dark:text-blue-400
"
>
{{
t
(
'
admin.accounts.gemini.modelPassthroughDesc
'
)
}}
<
/p
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<!--
Temp
Unschedulable
Rules
-->
<!--
Temp
Unschedulable
Rules
-->
...
@@ -1378,9 +1350,9 @@
...
@@ -1378,9 +1350,9 @@
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<!--
Intercept
Warmup
Requests
(
Anthropic
onl
y
)
-->
<!--
Intercept
Warmup
Requests
(
Anthropic
/
Antigravit
y
)
-->
<
div
<
div
v
-
if
=
"
form.platform === 'anthropic'
"
v
-
if
=
"
form.platform === 'anthropic'
|| form.platform === 'antigravity'
"
class
=
"
border-t border-gray-200 pt-4 dark:border-dark-600
"
class
=
"
border-t border-gray-200 pt-4 dark:border-dark-600
"
>
>
<
div
class
=
"
flex items-center justify-between
"
>
<
div
class
=
"
flex items-center justify-between
"
>
...
@@ -2157,7 +2129,7 @@
...
@@ -2157,7 +2129,7 @@
<
ConfirmDialog
<
ConfirmDialog
:
show
=
"
showMixedChannelWarning
"
:
show
=
"
showMixedChannelWarning
"
:
title
=
"
t('admin.accounts.mixedChannelWarningTitle')
"
:
title
=
"
t('admin.accounts.mixedChannelWarningTitle')
"
:
message
=
"
mixedChannelWarning
Details ? t('admin.accounts.mixedChannelWarning', mixedChannelWarningDetails) : ''
"
:
message
=
"
mixedChannelWarning
MessageText
"
:
confirm
-
text
=
"
t('common.confirm')
"
:
confirm
-
text
=
"
t('common.confirm')
"
:
cancel
-
text
=
"
t('common.cancel')
"
:
cancel
-
text
=
"
t('common.cancel')
"
:
danger
=
"
true
"
:
danger
=
"
true
"
...
@@ -2189,13 +2161,21 @@ import {
...
@@ -2189,13 +2161,21 @@ import {
import
{
useOpenAIOAuth
}
from
'
@/composables/useOpenAIOAuth
'
import
{
useOpenAIOAuth
}
from
'
@/composables/useOpenAIOAuth
'
import
{
useGeminiOAuth
}
from
'
@/composables/useGeminiOAuth
'
import
{
useGeminiOAuth
}
from
'
@/composables/useGeminiOAuth
'
import
{
useAntigravityOAuth
}
from
'
@/composables/useAntigravityOAuth
'
import
{
useAntigravityOAuth
}
from
'
@/composables/useAntigravityOAuth
'
import
type
{
Proxy
,
AdminGroup
,
AccountPlatform
,
AccountType
}
from
'
@/types
'
import
type
{
Proxy
,
AdminGroup
,
AccountPlatform
,
AccountType
,
CheckMixedChannelResponse
,
CreateAccountRequest
}
from
'
@/types
'
import
BaseDialog
from
'
@/components/common/BaseDialog.vue
'
import
BaseDialog
from
'
@/components/common/BaseDialog.vue
'
import
ConfirmDialog
from
'
@/components/common/ConfirmDialog.vue
'
import
ConfirmDialog
from
'
@/components/common/ConfirmDialog.vue
'
import
Icon
from
'
@/components/icons/Icon.vue
'
import
Icon
from
'
@/components/icons/Icon.vue
'
import
ProxySelector
from
'
@/components/common/ProxySelector.vue
'
import
ProxySelector
from
'
@/components/common/ProxySelector.vue
'
import
GroupSelector
from
'
@/components/common/GroupSelector.vue
'
import
GroupSelector
from
'
@/components/common/GroupSelector.vue
'
import
ModelWhitelistSelector
from
'
@/components/account/ModelWhitelistSelector.vue
'
import
ModelWhitelistSelector
from
'
@/components/account/ModelWhitelistSelector.vue
'
import
{
applyInterceptWarmup
}
from
'
@/components/account/credentialsBuilder
'
import
{
formatDateTimeLocalInput
,
parseDateTimeLocalInput
}
from
'
@/utils/format
'
import
{
formatDateTimeLocalInput
,
parseDateTimeLocalInput
}
from
'
@/utils/format
'
import
{
createStableObjectKeyResolver
}
from
'
@/utils/stableObjectKey
'
import
{
createStableObjectKeyResolver
}
from
'
@/utils/stableObjectKey
'
import
OAuthAuthorizationFlow
from
'
./OAuthAuthorizationFlow.vue
'
import
OAuthAuthorizationFlow
from
'
./OAuthAuthorizationFlow.vue
'
...
@@ -2337,10 +2317,13 @@ const getTempUnschedRuleKey = createStableObjectKeyResolver<TempUnschedRuleForm>
...
@@ -2337,10 +2317,13 @@ const getTempUnschedRuleKey = createStableObjectKeyResolver<TempUnschedRuleForm>
const
geminiOAuthType
=
ref
<
'
code_assist
'
|
'
google_one
'
|
'
ai_studio
'
>
(
'
google_one
'
)
const
geminiOAuthType
=
ref
<
'
code_assist
'
|
'
google_one
'
|
'
ai_studio
'
>
(
'
google_one
'
)
const
geminiAIStudioOAuthEnabled
=
ref
(
false
)
const
geminiAIStudioOAuthEnabled
=
ref
(
false
)
// Mixed channel warning dialog state
const
showMixedChannelWarning
=
ref
(
false
)
const
showMixedChannelWarning
=
ref
(
false
)
const
mixedChannelWarningDetails
=
ref
<
{
groupName
:
string
;
currentPlatform
:
string
;
otherPlatform
:
string
}
|
null
>
(
null
)
const
mixedChannelWarningDetails
=
ref
<
{
groupName
:
string
;
currentPlatform
:
string
;
otherPlatform
:
string
}
|
null
>
(
const
pendingCreatePayload
=
ref
<
any
>
(
null
)
null
)
const
mixedChannelWarningRawMessage
=
ref
(
''
)
const
mixedChannelWarningAction
=
ref
<
(()
=>
Promise
<
void
>
)
|
null
>
(
null
)
const
antigravityMixedChannelConfirmed
=
ref
(
false
)
const
showAdvancedOAuth
=
ref
(
false
)
const
showAdvancedOAuth
=
ref
(
false
)
const
showGeminiHelpDialog
=
ref
(
false
)
const
showGeminiHelpDialog
=
ref
(
false
)
...
@@ -2378,6 +2361,13 @@ const isOpenAIModelRestrictionDisabled = computed(() =>
...
@@ -2378,6 +2361,13 @@ const isOpenAIModelRestrictionDisabled = computed(() =>
form
.
platform
===
'
openai
'
&&
openaiPassthroughEnabled
.
value
form
.
platform
===
'
openai
'
&&
openaiPassthroughEnabled
.
value
)
)
const
mixedChannelWarningMessageText
=
computed
(()
=>
{
if
(
mixedChannelWarningDetails
.
value
)
{
return
t
(
'
admin.accounts.mixedChannelWarning
'
,
mixedChannelWarningDetails
.
value
)
}
return
mixedChannelWarningRawMessage
.
value
}
)
const
geminiQuotaDocs
=
{
const
geminiQuotaDocs
=
{
codeAssist
:
'
https://developers.google.com/gemini-code-assist/resources/quotas
'
,
codeAssist
:
'
https://developers.google.com/gemini-code-assist/resources/quotas
'
,
aiStudio
:
'
https://ai.google.dev/pricing
'
,
aiStudio
:
'
https://ai.google.dev/pricing
'
,
...
@@ -2544,8 +2534,8 @@ watch(
...
@@ -2544,8 +2534,8 @@ watch(
antigravityModelMappings
.
value
=
[]
antigravityModelMappings
.
value
=
[]
antigravityModelRestrictionMode
.
value
=
'
mapping
'
antigravityModelRestrictionMode
.
value
=
'
mapping
'
}
}
// Reset Anthropic-specific settings when switching to other platforms
// Reset Anthropic
/Antigravity
-specific settings when switching to other platforms
if
(
newPlatform
!==
'
anthropic
'
)
{
if
(
newPlatform
!==
'
anthropic
'
&&
newPlatform
!==
'
antigravity
'
)
{
interceptWarmupRequests
.
value
=
false
interceptWarmupRequests
.
value
=
false
}
}
if
(
newPlatform
===
'
sora
'
)
{
if
(
newPlatform
===
'
sora
'
)
{
...
@@ -2794,6 +2784,105 @@ const splitTempUnschedKeywords = (value: string) => {
...
@@ -2794,6 +2784,105 @@ const splitTempUnschedKeywords = (value: string) => {
.
filter
((
item
)
=>
item
.
length
>
0
)
.
filter
((
item
)
=>
item
.
length
>
0
)
}
}
const
needsMixedChannelCheck
=
(
platform
:
AccountPlatform
)
=>
platform
===
'
antigravity
'
||
platform
===
'
anthropic
'
const
buildMixedChannelDetails
=
(
resp
?:
CheckMixedChannelResponse
)
=>
{
const
details
=
resp
?.
details
if
(
!
details
)
{
return
null
}
return
{
groupName
:
details
.
group_name
||
'
Unknown
'
,
currentPlatform
:
details
.
current_platform
||
'
Unknown
'
,
otherPlatform
:
details
.
other_platform
||
'
Unknown
'
}
}
const
clearMixedChannelDialog
=
()
=>
{
showMixedChannelWarning
.
value
=
false
mixedChannelWarningDetails
.
value
=
null
mixedChannelWarningRawMessage
.
value
=
''
mixedChannelWarningAction
.
value
=
null
}
const
openMixedChannelDialog
=
(
opts
:
{
response
?:
CheckMixedChannelResponse
message
?:
string
onConfirm
:
()
=>
Promise
<
void
>
}
)
=>
{
mixedChannelWarningDetails
.
value
=
buildMixedChannelDetails
(
opts
.
response
)
mixedChannelWarningRawMessage
.
value
=
opts
.
message
||
opts
.
response
?.
message
||
t
(
'
admin.accounts.failedToCreate
'
)
mixedChannelWarningAction
.
value
=
opts
.
onConfirm
showMixedChannelWarning
.
value
=
true
}
const
withAntigravityConfirmFlag
=
(
payload
:
CreateAccountRequest
):
CreateAccountRequest
=>
{
if
(
needsMixedChannelCheck
(
payload
.
platform
)
&&
antigravityMixedChannelConfirmed
.
value
)
{
return
{
...
payload
,
confirm_mixed_channel_risk
:
true
}
}
const
cloned
=
{
...
payload
}
delete
cloned
.
confirm_mixed_channel_risk
return
cloned
}
const
ensureAntigravityMixedChannelConfirmed
=
async
(
onConfirm
:
()
=>
Promise
<
void
>
):
Promise
<
boolean
>
=>
{
if
(
!
needsMixedChannelCheck
(
form
.
platform
))
{
return
true
}
if
(
antigravityMixedChannelConfirmed
.
value
)
{
return
true
}
try
{
const
result
=
await
adminAPI
.
accounts
.
checkMixedChannelRisk
({
platform
:
form
.
platform
,
group_ids
:
form
.
group_ids
}
)
if
(
!
result
.
has_risk
)
{
return
true
}
openMixedChannelDialog
({
response
:
result
,
onConfirm
:
async
()
=>
{
antigravityMixedChannelConfirmed
.
value
=
true
await
onConfirm
()
}
}
)
return
false
}
catch
(
error
:
any
)
{
appStore
.
showError
(
error
.
response
?.
data
?.
message
||
error
.
response
?.
data
?.
detail
||
t
(
'
admin.accounts.failedToCreate
'
))
return
false
}
}
const
submitCreateAccount
=
async
(
payload
:
CreateAccountRequest
)
=>
{
submitting
.
value
=
true
try
{
await
adminAPI
.
accounts
.
create
(
withAntigravityConfirmFlag
(
payload
))
appStore
.
showSuccess
(
t
(
'
admin.accounts.accountCreated
'
))
emit
(
'
created
'
)
handleClose
()
}
catch
(
error
:
any
)
{
if
(
error
.
response
?.
status
===
409
&&
error
.
response
?.
data
?.
error
===
'
mixed_channel_warning
'
&&
needsMixedChannelCheck
(
form
.
platform
))
{
openMixedChannelDialog
({
message
:
error
.
response
?.
data
?.
message
,
onConfirm
:
async
()
=>
{
antigravityMixedChannelConfirmed
.
value
=
true
await
submitCreateAccount
(
payload
)
}
}
)
return
}
appStore
.
showError
(
error
.
response
?.
data
?.
message
||
error
.
response
?.
data
?.
detail
||
t
(
'
admin.accounts.failedToCreate
'
))
}
finally
{
submitting
.
value
=
false
}
}
// Methods
// Methods
const
resetForm
=
()
=>
{
const
resetForm
=
()
=>
{
step
.
value
=
1
step
.
value
=
1
...
@@ -2855,9 +2944,13 @@ const resetForm = () => {
...
@@ -2855,9 +2944,13 @@ const resetForm = () => {
geminiOAuth
.
resetState
()
geminiOAuth
.
resetState
()
antigravityOAuth
.
resetState
()
antigravityOAuth
.
resetState
()
oauthFlowRef
.
value
?.
reset
()
oauthFlowRef
.
value
?.
reset
()
antigravityMixedChannelConfirmed
.
value
=
false
clearMixedChannelDialog
()
}
}
const
handleClose
=
()
=>
{
const
handleClose
=
()
=>
{
antigravityMixedChannelConfirmed
.
value
=
false
clearMixedChannelDialog
()
emit
(
'
close
'
)
emit
(
'
close
'
)
}
}
...
@@ -2916,56 +3009,34 @@ const buildSoraExtra = (
...
@@ -2916,56 +3009,34 @@ const buildSoraExtra = (
}
}
// 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
:
CreateAccountRequest
)
=>
{
submitting
.
value
=
true
const
canContinue
=
await
ensureAntigravityMixedChannelConfirmed
(
async
()
=>
{
try
{
await
submitCreateAccount
(
payload
)
await
adminAPI
.
accounts
.
create
(
payload
)
}
)
appStore
.
showSuccess
(
t
(
'
admin.accounts.accountCreated
'
))
if
(
!
canContinue
)
{
emit
(
'
created
'
)
return
handleClose
()
}
catch
(
error
:
any
)
{
// Handle 409 mixed_channel_warning - show confirmation dialog
if
(
error
.
response
?.
status
===
409
&&
error
.
response
?.
data
?.
error
===
'
mixed_channel_warning
'
)
{
const
details
=
error
.
response
.
data
.
details
||
{
}
mixedChannelWarningDetails
.
value
=
{
groupName
:
details
.
group_name
||
'
Unknown
'
,
currentPlatform
:
details
.
current_platform
||
'
Unknown
'
,
otherPlatform
:
details
.
other_platform
||
'
Unknown
'
}
pendingCreatePayload
.
value
=
payload
showMixedChannelWarning
.
value
=
true
}
else
{
appStore
.
showError
(
error
.
response
?.
data
?.
detail
||
t
(
'
admin.accounts.failedToCreate
'
))
}
}
finally
{
submitting
.
value
=
false
}
}
await
submitCreateAccount
(
payload
)
}
}
// Handle mixed channel warning confirmation
// Handle mixed channel warning confirmation
const
handleMixedChannelConfirm
=
async
()
=>
{
const
handleMixedChannelConfirm
=
async
()
=>
{
showMixedChannelWarning
.
value
=
false
const
action
=
mixedChannelWarningAction
.
value
if
(
pendingCreatePayload
.
value
)
{
if
(
!
action
)
{
pendingCreatePayload
.
value
.
confirm_mixed_channel_risk
=
true
clearMixedChannelDialog
()
submitting
.
value
=
true
return
try
{
}
await
adminAPI
.
accounts
.
create
(
pendingCreatePayload
.
value
)
clearMixedChannelDialog
()
appStore
.
showSuccess
(
t
(
'
admin.accounts.accountCreated
'
))
submitting
.
value
=
true
emit
(
'
created
'
)
try
{
handleClose
()
await
action
()
}
catch
(
error
:
any
)
{
}
finally
{
appStore
.
showError
(
error
.
response
?.
data
?.
detail
||
t
(
'
admin.accounts.failedToCreate
'
))
submitting
.
value
=
false
}
finally
{
submitting
.
value
=
false
pendingCreatePayload
.
value
=
null
}
}
}
}
}
const
handleMixedChannelCancel
=
()
=>
{
const
handleMixedChannelCancel
=
()
=>
{
showMixedChannelWarning
.
value
=
false
clearMixedChannelDialog
()
pendingCreatePayload
.
value
=
null
mixedChannelWarningDetails
.
value
=
null
}
}
const
handleSubmit
=
async
()
=>
{
const
handleSubmit
=
async
()
=>
{
...
@@ -2975,6 +3046,12 @@ const handleSubmit = async () => {
...
@@ -2975,6 +3046,12 @@ const handleSubmit = async () => {
appStore
.
showError
(
t
(
'
admin.accounts.pleaseEnterAccountName
'
))
appStore
.
showError
(
t
(
'
admin.accounts.pleaseEnterAccountName
'
))
return
return
}
}
const
canContinue
=
await
ensureAntigravityMixedChannelConfirmed
(
async
()
=>
{
step
.
value
=
2
}
)
if
(
!
canContinue
)
{
return
}
step
.
value
=
2
step
.
value
=
2
return
return
}
}
...
@@ -3010,15 +3087,10 @@ const handleSubmit = async () => {
...
@@ -3010,15 +3087,10 @@ const handleSubmit = async () => {
credentials
.
model_mapping
=
antigravityModelMapping
credentials
.
model_mapping
=
antigravityModelMapping
}
}
submitting
.
value
=
true
applyInterceptWarmup
(
credentials
,
interceptWarmupRequests
.
value
,
'
create
'
)
try
{
const
extra
=
mixedScheduling
.
value
?
{
mixed_scheduling
:
true
}
:
undefined
const
extra
=
mixedScheduling
.
value
?
{
mixed_scheduling
:
true
}
:
undefined
await
createAccountAndFinish
(
form
.
platform
,
'
apikey
'
,
credentials
,
extra
)
await
createAccountAndFinish
(
form
.
platform
,
'
apikey
'
,
credentials
,
extra
)
}
catch
(
error
:
any
)
{
appStore
.
showError
(
error
.
response
?.
data
?.
detail
||
t
(
'
admin.accounts.failedToCreate
'
))
}
finally
{
submitting
.
value
=
false
}
return
return
}
}
...
@@ -3059,10 +3131,7 @@ const handleSubmit = async () => {
...
@@ -3059,10 +3131,7 @@ const handleSubmit = async () => {
credentials
.
custom_error_codes
=
[...
selectedErrorCodes
.
value
]
credentials
.
custom_error_codes
=
[...
selectedErrorCodes
.
value
]
}
}
// Add intercept warmup requests setting
applyInterceptWarmup
(
credentials
,
interceptWarmupRequests
.
value
,
'
create
'
)
if
(
interceptWarmupRequests
.
value
)
{
credentials
.
intercept_warmup_requests
=
true
}
if
(
!
applyTempUnschedConfig
(
credentials
))
{
if
(
!
applyTempUnschedConfig
(
credentials
))
{
return
return
}
}
...
@@ -3132,7 +3201,7 @@ const createAccountAndFinish = async (
...
@@ -3132,7 +3201,7 @@ const createAccountAndFinish = async (
if
(
!
applyTempUnschedConfig
(
credentials
))
{
if
(
!
applyTempUnschedConfig
(
credentials
))
{
return
return
}
}
await
adminAPI
.
accounts
.
create
({
await
doCreateAccount
({
name
:
form
.
name
,
name
:
form
.
name
,
notes
:
form
.
notes
,
notes
:
form
.
notes
,
platform
,
platform
,
...
@@ -3147,9 +3216,6 @@ const createAccountAndFinish = async (
...
@@ -3147,9 +3216,6 @@ const createAccountAndFinish = async (
expires_at
:
form
.
expires_at
,
expires_at
:
form
.
expires_at
,
auto_pause_on_expired
:
autoPauseOnExpired
.
value
auto_pause_on_expired
:
autoPauseOnExpired
.
value
}
)
}
)
appStore
.
showSuccess
(
t
(
'
admin.accounts.accountCreated
'
))
emit
(
'
created
'
)
handleClose
()
}
}
// OpenAI OAuth 授权码兑换
// OpenAI OAuth 授权码兑换
...
@@ -3497,7 +3563,7 @@ const handleAntigravityValidateRT = async (refreshTokenInput: string) => {
...
@@ -3497,7 +3563,7 @@ const handleAntigravityValidateRT = async (refreshTokenInput: string) => {
const
accountName
=
refreshTokens
.
length
>
1
?
`${form.name
}
#${i + 1
}
`
:
form
.
name
const
accountName
=
refreshTokens
.
length
>
1
?
`${form.name
}
#${i + 1
}
`
:
form
.
name
// Note: Antigravity doesn't have buildExtraInfo, so we pass empty extra or rely on credentials
// Note: Antigravity doesn't have buildExtraInfo, so we pass empty extra or rely on credentials
await
adminAPI
.
accounts
.
create
({
const
createPayload
=
withAntigravityConfirmFlag
({
name
:
accountName
,
name
:
accountName
,
notes
:
form
.
notes
,
notes
:
form
.
notes
,
platform
:
'
antigravity
'
,
platform
:
'
antigravity
'
,
...
@@ -3512,6 +3578,7 @@ const handleAntigravityValidateRT = async (refreshTokenInput: string) => {
...
@@ -3512,6 +3578,7 @@ const handleAntigravityValidateRT = async (refreshTokenInput: string) => {
expires_at
:
form
.
expires_at
,
expires_at
:
form
.
expires_at
,
auto_pause_on_expired
:
autoPauseOnExpired
.
value
auto_pause_on_expired
:
autoPauseOnExpired
.
value
}
)
}
)
await
adminAPI
.
accounts
.
create
(
createPayload
)
successCount
++
successCount
++
}
catch
(
error
:
any
)
{
}
catch
(
error
:
any
)
{
failedCount
++
failedCount
++
...
@@ -3606,6 +3673,7 @@ const handleAntigravityExchange = async (authCode: string) => {
...
@@ -3606,6 +3673,7 @@ const handleAntigravityExchange = async (authCode: string) => {
if
(
!
tokenInfo
)
return
if
(
!
tokenInfo
)
return
const
credentials
=
antigravityOAuth
.
buildCredentials
(
tokenInfo
)
const
credentials
=
antigravityOAuth
.
buildCredentials
(
tokenInfo
)
applyInterceptWarmup
(
credentials
,
interceptWarmupRequests
.
value
,
'
create
'
)
// Antigravity 只使用映射模式
// Antigravity 只使用映射模式
const
antigravityModelMapping
=
buildModelMappingObject
(
const
antigravityModelMapping
=
buildModelMappingObject
(
'
mapping
'
,
'
mapping
'
,
...
@@ -3677,10 +3745,8 @@ const handleAnthropicExchange = async (authCode: string) => {
...
@@ -3677,10 +3745,8 @@ const handleAnthropicExchange = async (authCode: string) => {
extra
.
cache_ttl_override_target
=
cacheTTLOverrideTarget
.
value
extra
.
cache_ttl_override_target
=
cacheTTLOverrideTarget
.
value
}
}
const
credentials
=
{
const
credentials
:
Record
<
string
,
unknown
>
=
{
...
tokenInfo
}
...
tokenInfo
,
applyInterceptWarmup
(
credentials
,
interceptWarmupRequests
.
value
,
'
create
'
)
...(
interceptWarmupRequests
.
value
?
{
intercept_warmup_requests
:
true
}
:
{
}
)
}
await
createAccountAndFinish
(
form
.
platform
,
addMethod
.
value
as
AccountType
,
credentials
,
extra
)
await
createAccountAndFinish
(
form
.
platform
,
addMethod
.
value
as
AccountType
,
credentials
,
extra
)
}
catch
(
error
:
any
)
{
}
catch
(
error
:
any
)
{
oauth
.
error
.
value
=
error
.
response
?.
data
?.
detail
||
t
(
'
admin.accounts.oauth.authFailed
'
)
oauth
.
error
.
value
=
error
.
response
?.
data
?.
detail
||
t
(
'
admin.accounts.oauth.authFailed
'
)
...
@@ -3779,11 +3845,8 @@ const handleCookieAuth = async (sessionKey: string) => {
...
@@ -3779,11 +3845,8 @@ const handleCookieAuth = async (sessionKey: string) => {
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
const
credentials
:
Record
<
string
,
unknown
>
=
{
...
tokenInfo
}
const
credentials
:
Record
<
string
,
unknown
>
=
{
applyInterceptWarmup
(
credentials
,
interceptWarmupRequests
.
value
,
'
create
'
)
...
tokenInfo
,
...(
interceptWarmupRequests
.
value
?
{
intercept_warmup_requests
:
true
}
:
{
}
)
}
if
(
tempUnschedEnabled
.
value
)
{
if
(
tempUnschedEnabled
.
value
)
{
credentials
.
temp_unschedulable_enabled
=
true
credentials
.
temp_unschedulable_enabled
=
true
credentials
.
temp_unschedulable_rules
=
tempUnschedPayload
credentials
.
temp_unschedulable_rules
=
tempUnschedPayload
...
...
frontend/src/components/account/EditAccountModal.vue
View file @
c7e18bd5
...
@@ -65,8 +65,8 @@
...
@@ -65,8 +65,8 @@
<p
class=
"input-hint"
>
{{
t
(
'
admin.accounts.leaveEmptyToKeep
'
)
}}
</p>
<p
class=
"input-hint"
>
{{
t
(
'
admin.accounts.leaveEmptyToKeep
'
)
}}
</p>
</div>
</div>
<!-- Model Restriction Section (不适用于
Gemini 和
Antigravity) -->
<!-- Model Restriction Section (不适用于 Antigravity) -->
<div
v-if=
"account.platform !==
'gemini' && account.platform !==
'antigravity'"
class=
"border-t border-gray-200 pt-4 dark:border-dark-600"
>
<div
v-if=
"account.platform !== 'antigravity'"
class=
"border-t border-gray-200 pt-4 dark:border-dark-600"
>
<label
class=
"input-label"
>
{{
t
(
'
admin.accounts.modelRestriction
'
)
}}
</label>
<label
class=
"input-label"
>
{{
t
(
'
admin.accounts.modelRestriction
'
)
}}
</label>
<div
<div
...
@@ -349,34 +349,6 @@
...
@@ -349,34 +349,6 @@
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<!--
Gemini
模型说明
-->
<
div
v
-
if
=
"
account.platform === 'gemini'
"
class
=
"
border-t border-gray-200 pt-4 dark:border-dark-600
"
>
<
div
class
=
"
rounded-lg bg-blue-50 p-4 dark:bg-blue-900/20
"
>
<
div
class
=
"
flex items-start gap-3
"
>
<
svg
class
=
"
h-5 w-5 flex-shrink-0 text-blue-600 dark:text-blue-400
"
fill
=
"
none
"
viewBox
=
"
0 0 24 24
"
stroke
=
"
currentColor
"
>
<
path
stroke
-
linecap
=
"
round
"
stroke
-
linejoin
=
"
round
"
stroke
-
width
=
"
2
"
d
=
"
M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z
"
/>
<
/svg
>
<
div
>
<
p
class
=
"
text-sm font-medium text-blue-800 dark:text-blue-300
"
>
{{
t
(
'
admin.accounts.gemini.modelPassthrough
'
)
}}
<
/p
>
<
p
class
=
"
mt-1 text-xs text-blue-700 dark:text-blue-400
"
>
{{
t
(
'
admin.accounts.gemini.modelPassthroughDesc
'
)
}}
<
/p
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<!--
Upstream
fields
(
only
for
upstream
type
)
-->
<!--
Upstream
fields
(
only
for
upstream
type
)
-->
...
@@ -641,9 +613,9 @@
...
@@ -641,9 +613,9 @@
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<!--
Intercept
Warmup
Requests
(
Anthropic
onl
y
)
-->
<!--
Intercept
Warmup
Requests
(
Anthropic
/
Antigravit
y
)
-->
<
div
<
div
v
-
if
=
"
account?.platform === 'anthropic'
"
v
-
if
=
"
account?.platform === 'anthropic'
|| account?.platform === 'antigravity'
"
class
=
"
border-t border-gray-200 pt-4 dark:border-dark-600
"
class
=
"
border-t border-gray-200 pt-4 dark:border-dark-600
"
>
>
<
div
class
=
"
flex items-center justify-between
"
>
<
div
class
=
"
flex items-center justify-between
"
>
...
@@ -1139,7 +1111,7 @@
...
@@ -1139,7 +1111,7 @@
<
ConfirmDialog
<
ConfirmDialog
:
show
=
"
showMixedChannelWarning
"
:
show
=
"
showMixedChannelWarning
"
:
title
=
"
t('admin.accounts.mixedChannelWarningTitle')
"
:
title
=
"
t('admin.accounts.mixedChannelWarningTitle')
"
:
message
=
"
mixedChannelWarning
Details ? t('admin.accounts.mixedChannelWarning', mixedChannelWarningDetails) : ''
"
:
message
=
"
mixedChannelWarning
MessageText
"
:
confirm
-
text
=
"
t('common.confirm')
"
:
confirm
-
text
=
"
t('common.confirm')
"
:
cancel
-
text
=
"
t('common.cancel')
"
:
cancel
-
text
=
"
t('common.cancel')
"
:
danger
=
"
true
"
:
danger
=
"
true
"
...
@@ -1154,7 +1126,7 @@ import { useI18n } from 'vue-i18n'
...
@@ -1154,7 +1126,7 @@ import { useI18n } from 'vue-i18n'
import
{
useAppStore
}
from
'
@/stores/app
'
import
{
useAppStore
}
from
'
@/stores/app
'
import
{
useAuthStore
}
from
'
@/stores/auth
'
import
{
useAuthStore
}
from
'
@/stores/auth
'
import
{
adminAPI
}
from
'
@/api/admin
'
import
{
adminAPI
}
from
'
@/api/admin
'
import
type
{
Account
,
Proxy
,
AdminGroup
}
from
'
@/types
'
import
type
{
Account
,
Proxy
,
AdminGroup
,
CheckMixedChannelResponse
}
from
'
@/types
'
import
BaseDialog
from
'
@/components/common/BaseDialog.vue
'
import
BaseDialog
from
'
@/components/common/BaseDialog.vue
'
import
ConfirmDialog
from
'
@/components/common/ConfirmDialog.vue
'
import
ConfirmDialog
from
'
@/components/common/ConfirmDialog.vue
'
import
Select
from
'
@/components/common/Select.vue
'
import
Select
from
'
@/components/common/Select.vue
'
...
@@ -1162,6 +1134,7 @@ import Icon from '@/components/icons/Icon.vue'
...
@@ -1162,6 +1134,7 @@ import Icon from '@/components/icons/Icon.vue'
import
ProxySelector
from
'
@/components/common/ProxySelector.vue
'
import
ProxySelector
from
'
@/components/common/ProxySelector.vue
'
import
GroupSelector
from
'
@/components/common/GroupSelector.vue
'
import
GroupSelector
from
'
@/components/common/GroupSelector.vue
'
import
ModelWhitelistSelector
from
'
@/components/account/ModelWhitelistSelector.vue
'
import
ModelWhitelistSelector
from
'
@/components/account/ModelWhitelistSelector.vue
'
import
{
applyInterceptWarmup
}
from
'
@/components/account/credentialsBuilder
'
import
{
formatDateTimeLocalInput
,
parseDateTimeLocalInput
}
from
'
@/utils/format
'
import
{
formatDateTimeLocalInput
,
parseDateTimeLocalInput
}
from
'
@/utils/format
'
import
{
createStableObjectKeyResolver
}
from
'
@/utils/stableObjectKey
'
import
{
createStableObjectKeyResolver
}
from
'
@/utils/stableObjectKey
'
import
{
import
{
...
@@ -1233,10 +1206,13 @@ const getModelMappingKey = createStableObjectKeyResolver<ModelMapping>('edit-mod
...
@@ -1233,10 +1206,13 @@ const getModelMappingKey = createStableObjectKeyResolver<ModelMapping>('edit-mod
const
getAntigravityModelMappingKey
=
createStableObjectKeyResolver
<
ModelMapping
>
(
'
edit-antigravity-model-mapping
'
)
const
getAntigravityModelMappingKey
=
createStableObjectKeyResolver
<
ModelMapping
>
(
'
edit-antigravity-model-mapping
'
)
const
getTempUnschedRuleKey
=
createStableObjectKeyResolver
<
TempUnschedRuleForm
>
(
'
edit-temp-unsched-rule
'
)
const
getTempUnschedRuleKey
=
createStableObjectKeyResolver
<
TempUnschedRuleForm
>
(
'
edit-temp-unsched-rule
'
)
// Mixed channel warning dialog state
const
showMixedChannelWarning
=
ref
(
false
)
const
showMixedChannelWarning
=
ref
(
false
)
const
mixedChannelWarningDetails
=
ref
<
{
groupName
:
string
;
currentPlatform
:
string
;
otherPlatform
:
string
}
|
null
>
(
null
)
const
mixedChannelWarningDetails
=
ref
<
{
groupName
:
string
;
currentPlatform
:
string
;
otherPlatform
:
string
}
|
null
>
(
const
pendingUpdatePayload
=
ref
<
Record
<
string
,
unknown
>
|
null
>
(
null
)
null
)
const
mixedChannelWarningRawMessage
=
ref
(
''
)
const
mixedChannelWarningAction
=
ref
<
(()
=>
Promise
<
void
>
)
|
null
>
(
null
)
const
antigravityMixedChannelConfirmed
=
ref
(
false
)
// Quota control state (Anthropic OAuth/SetupToken only)
// Quota control state (Anthropic OAuth/SetupToken only)
const
windowCostEnabled
=
ref
(
false
)
const
windowCostEnabled
=
ref
(
false
)
...
@@ -1297,6 +1273,13 @@ const defaultBaseUrl = computed(() => {
...
@@ -1297,6 +1273,13 @@ const defaultBaseUrl = computed(() => {
return
'
https://api.anthropic.com
'
return
'
https://api.anthropic.com
'
}
)
}
)
const
mixedChannelWarningMessageText
=
computed
(()
=>
{
if
(
mixedChannelWarningDetails
.
value
)
{
return
t
(
'
admin.accounts.mixedChannelWarning
'
,
mixedChannelWarningDetails
.
value
)
}
return
mixedChannelWarningRawMessage
.
value
}
)
const
form
=
reactive
({
const
form
=
reactive
({
name
:
''
,
name
:
''
,
notes
:
''
,
notes
:
''
,
...
@@ -1326,6 +1309,11 @@ watch(
...
@@ -1326,6 +1309,11 @@ watch(
()
=>
props
.
account
,
()
=>
props
.
account
,
(
newAccount
)
=>
{
(
newAccount
)
=>
{
if
(
newAccount
)
{
if
(
newAccount
)
{
antigravityMixedChannelConfirmed
.
value
=
false
showMixedChannelWarning
.
value
=
false
mixedChannelWarningDetails
.
value
=
null
mixedChannelWarningRawMessage
.
value
=
''
mixedChannelWarningAction
.
value
=
null
form
.
name
=
newAccount
.
name
form
.
name
=
newAccount
.
name
form
.
notes
=
newAccount
.
notes
||
''
form
.
notes
=
newAccount
.
notes
||
''
form
.
proxy_id
=
newAccount
.
proxy_id
form
.
proxy_id
=
newAccount
.
proxy_id
...
@@ -1725,18 +1713,123 @@ function toPositiveNumber(value: unknown) {
...
@@ -1725,18 +1713,123 @@ function toPositiveNumber(value: unknown) {
return
Math
.
trunc
(
num
)
return
Math
.
trunc
(
num
)
}
}
const
needsMixedChannelCheck
=
()
=>
props
.
account
?.
platform
===
'
antigravity
'
||
props
.
account
?.
platform
===
'
anthropic
'
const
buildMixedChannelDetails
=
(
resp
?:
CheckMixedChannelResponse
)
=>
{
const
details
=
resp
?.
details
if
(
!
details
)
{
return
null
}
return
{
groupName
:
details
.
group_name
||
'
Unknown
'
,
currentPlatform
:
details
.
current_platform
||
'
Unknown
'
,
otherPlatform
:
details
.
other_platform
||
'
Unknown
'
}
}
const
clearMixedChannelDialog
=
()
=>
{
showMixedChannelWarning
.
value
=
false
mixedChannelWarningDetails
.
value
=
null
mixedChannelWarningRawMessage
.
value
=
''
mixedChannelWarningAction
.
value
=
null
}
const
openMixedChannelDialog
=
(
opts
:
{
response
?:
CheckMixedChannelResponse
message
?:
string
onConfirm
:
()
=>
Promise
<
void
>
}
)
=>
{
mixedChannelWarningDetails
.
value
=
buildMixedChannelDetails
(
opts
.
response
)
mixedChannelWarningRawMessage
.
value
=
opts
.
message
||
opts
.
response
?.
message
||
t
(
'
admin.accounts.failedToUpdate
'
)
mixedChannelWarningAction
.
value
=
opts
.
onConfirm
showMixedChannelWarning
.
value
=
true
}
const
withAntigravityConfirmFlag
=
(
payload
:
Record
<
string
,
unknown
>
)
=>
{
if
(
needsMixedChannelCheck
()
&&
antigravityMixedChannelConfirmed
.
value
)
{
return
{
...
payload
,
confirm_mixed_channel_risk
:
true
}
}
const
cloned
=
{
...
payload
}
delete
cloned
.
confirm_mixed_channel_risk
return
cloned
}
const
ensureAntigravityMixedChannelConfirmed
=
async
(
onConfirm
:
()
=>
Promise
<
void
>
):
Promise
<
boolean
>
=>
{
if
(
!
needsMixedChannelCheck
())
{
return
true
}
if
(
antigravityMixedChannelConfirmed
.
value
)
{
return
true
}
if
(
!
props
.
account
)
{
return
false
}
try
{
const
result
=
await
adminAPI
.
accounts
.
checkMixedChannelRisk
({
platform
:
props
.
account
.
platform
,
group_ids
:
form
.
group_ids
,
account_id
:
props
.
account
.
id
}
)
if
(
!
result
.
has_risk
)
{
return
true
}
openMixedChannelDialog
({
response
:
result
,
onConfirm
:
async
()
=>
{
antigravityMixedChannelConfirmed
.
value
=
true
await
onConfirm
()
}
}
)
return
false
}
catch
(
error
:
any
)
{
appStore
.
showError
(
error
.
response
?.
data
?.
message
||
error
.
response
?.
data
?.
detail
||
t
(
'
admin.accounts.failedToUpdate
'
))
return
false
}
}
const
formatDateTimeLocal
=
formatDateTimeLocalInput
const
formatDateTimeLocal
=
formatDateTimeLocalInput
const
parseDateTimeLocal
=
parseDateTimeLocalInput
const
parseDateTimeLocal
=
parseDateTimeLocalInput
// Methods
// Methods
const
handleClose
=
()
=>
{
const
handleClose
=
()
=>
{
antigravityMixedChannelConfirmed
.
value
=
false
clearMixedChannelDialog
()
emit
(
'
close
'
)
emit
(
'
close
'
)
}
}
const
submitUpdateAccount
=
async
(
accountID
:
number
,
updatePayload
:
Record
<
string
,
unknown
>
)
=>
{
submitting
.
value
=
true
try
{
const
updatedAccount
=
await
adminAPI
.
accounts
.
update
(
accountID
,
withAntigravityConfirmFlag
(
updatePayload
))
appStore
.
showSuccess
(
t
(
'
admin.accounts.accountUpdated
'
))
emit
(
'
updated
'
,
updatedAccount
)
handleClose
()
}
catch
(
error
:
any
)
{
if
(
error
.
response
?.
status
===
409
&&
error
.
response
?.
data
?.
error
===
'
mixed_channel_warning
'
&&
needsMixedChannelCheck
())
{
openMixedChannelDialog
({
message
:
error
.
response
?.
data
?.
message
,
onConfirm
:
async
()
=>
{
antigravityMixedChannelConfirmed
.
value
=
true
await
submitUpdateAccount
(
accountID
,
updatePayload
)
}
}
)
return
}
appStore
.
showError
(
error
.
response
?.
data
?.
message
||
error
.
response
?.
data
?.
detail
||
t
(
'
admin.accounts.failedToUpdate
'
))
}
finally
{
submitting
.
value
=
false
}
}
const
handleSubmit
=
async
()
=>
{
const
handleSubmit
=
async
()
=>
{
if
(
!
props
.
account
)
return
if
(
!
props
.
account
)
return
const
accountID
=
props
.
account
.
id
submitting
.
value
=
true
const
updatePayload
:
Record
<
string
,
unknown
>
=
{
...
form
}
const
updatePayload
:
Record
<
string
,
unknown
>
=
{
...
form
}
try
{
try
{
// 后端期望 proxy_id: 0 表示清除代理,而不是 null
// 后端期望 proxy_id: 0 表示清除代理,而不是 null
...
@@ -1768,7 +1861,6 @@ const handleSubmit = async () => {
...
@@ -1768,7 +1861,6 @@ const handleSubmit = async () => {
newCredentials
.
api_key
=
currentCredentials
.
api_key
newCredentials
.
api_key
=
currentCredentials
.
api_key
}
else
{
}
else
{
appStore
.
showError
(
t
(
'
admin.accounts.apiKeyIsRequired
'
))
appStore
.
showError
(
t
(
'
admin.accounts.apiKeyIsRequired
'
))
submitting
.
value
=
false
return
return
}
}
...
@@ -1789,11 +1881,8 @@ const handleSubmit = async () => {
...
@@ -1789,11 +1881,8 @@ const handleSubmit = async () => {
}
}
// Add intercept warmup requests setting
// Add intercept warmup requests setting
if
(
interceptWarmupRequests
.
value
)
{
applyInterceptWarmup
(
newCredentials
,
interceptWarmupRequests
.
value
,
'
edit
'
)
newCredentials
.
intercept_warmup_requests
=
true
}
if
(
!
applyTempUnschedConfig
(
newCredentials
))
{
if
(
!
applyTempUnschedConfig
(
newCredentials
))
{
submitting
.
value
=
false
return
return
}
}
...
@@ -1808,8 +1897,10 @@ const handleSubmit = async () => {
...
@@ -1808,8 +1897,10 @@ const handleSubmit = async () => {
newCredentials
.
api_key
=
editApiKey
.
value
.
trim
()
newCredentials
.
api_key
=
editApiKey
.
value
.
trim
()
}
}
// Add intercept warmup requests setting
applyInterceptWarmup
(
newCredentials
,
interceptWarmupRequests
.
value
,
'
edit
'
)
if
(
!
applyTempUnschedConfig
(
newCredentials
))
{
if
(
!
applyTempUnschedConfig
(
newCredentials
))
{
submitting
.
value
=
false
return
return
}
}
...
@@ -1819,13 +1910,8 @@ const handleSubmit = async () => {
...
@@ -1819,13 +1910,8 @@ const handleSubmit = async () => {
const
currentCredentials
=
(
props
.
account
.
credentials
as
Record
<
string
,
unknown
>
)
||
{
}
const
currentCredentials
=
(
props
.
account
.
credentials
as
Record
<
string
,
unknown
>
)
||
{
}
const
newCredentials
:
Record
<
string
,
unknown
>
=
{
...
currentCredentials
}
const
newCredentials
:
Record
<
string
,
unknown
>
=
{
...
currentCredentials
}
if
(
interceptWarmupRequests
.
value
)
{
applyInterceptWarmup
(
newCredentials
,
interceptWarmupRequests
.
value
,
'
edit
'
)
newCredentials
.
intercept_warmup_requests
=
true
}
else
{
delete
newCredentials
.
intercept_warmup_requests
}
if
(
!
applyTempUnschedConfig
(
newCredentials
))
{
if
(
!
applyTempUnschedConfig
(
newCredentials
))
{
submitting
.
value
=
false
return
return
}
}
...
@@ -1955,52 +2041,36 @@ const handleSubmit = async () => {
...
@@ -1955,52 +2041,36 @@ const handleSubmit = async () => {
updatePayload
.
extra
=
newExtra
updatePayload
.
extra
=
newExtra
}
}
const
updatedAccount
=
await
adminAPI
.
accounts
.
update
(
props
.
account
.
id
,
updatePayload
)
const
canContinue
=
await
ensureAntigravityMixedChannelConfirmed
(
async
()
=>
{
appStore
.
showSuccess
(
t
(
'
admin.accounts.accountUpdated
'
))
await
submitUpdateAccount
(
accountID
,
updatePayload
)
emit
(
'
updated
'
,
updatedAccount
)
}
)
handleClose
()
if
(
!
canContinue
)
{
}
catch
(
error
:
any
)
{
return
// Handle 409 mixed_channel_warning - show confirmation dialog
if
(
error
.
response
?.
status
===
409
&&
error
.
response
?.
data
?.
error
===
'
mixed_channel_warning
'
)
{
const
details
=
error
.
response
.
data
.
details
||
{
}
mixedChannelWarningDetails
.
value
=
{
groupName
:
details
.
group_name
||
'
Unknown
'
,
currentPlatform
:
details
.
current_platform
||
'
Unknown
'
,
otherPlatform
:
details
.
other_platform
||
'
Unknown
'
}
pendingUpdatePayload
.
value
=
updatePayload
showMixedChannelWarning
.
value
=
true
}
else
{
appStore
.
showError
(
error
.
response
?.
data
?.
message
||
error
.
response
?.
data
?.
detail
||
t
(
'
admin.accounts.failedToUpdate
'
))
}
}
}
finally
{
submitting
.
value
=
false
await
submitUpdateAccount
(
accountID
,
updatePayload
)
}
catch
(
error
:
any
)
{
appStore
.
showError
(
error
.
response
?.
data
?.
message
||
error
.
response
?.
data
?.
detail
||
t
(
'
admin.accounts.failedToUpdate
'
))
}
}
}
}
// Handle mixed channel warning confirmation
// Handle mixed channel warning confirmation
const
handleMixedChannelConfirm
=
async
()
=>
{
const
handleMixedChannelConfirm
=
async
()
=>
{
showMixedChannelWarning
.
value
=
false
const
action
=
mixedChannelWarningAction
.
value
if
(
pendingUpdatePayload
.
value
&&
props
.
account
)
{
if
(
!
action
)
{
pendingUpdatePayload
.
value
.
confirm_mixed_channel_risk
=
true
clearMixedChannelDialog
()
submitting
.
value
=
true
return
try
{
}
const
updatedAccount
=
await
adminAPI
.
accounts
.
update
(
props
.
account
.
id
,
pendingUpdatePayload
.
value
)
clearMixedChannelDialog
()
appStore
.
showSuccess
(
t
(
'
admin.accounts.accountUpdated
'
))
submitting
.
value
=
true
emit
(
'
updated
'
,
updatedAccount
)
try
{
handleClose
()
await
action
()
}
catch
(
error
:
any
)
{
}
finally
{
appStore
.
showError
(
error
.
response
?.
data
?.
message
||
error
.
response
?.
data
?.
detail
||
t
(
'
admin.accounts.failedToUpdate
'
))
submitting
.
value
=
false
}
finally
{
submitting
.
value
=
false
pendingUpdatePayload
.
value
=
null
}
}
}
}
}
const
handleMixedChannelCancel
=
()
=>
{
const
handleMixedChannelCancel
=
()
=>
{
showMixedChannelWarning
.
value
=
false
clearMixedChannelDialog
()
pendingUpdatePayload
.
value
=
null
mixedChannelWarningDetails
.
value
=
null
}
}
<
/script
>
<
/script
>
frontend/src/components/account/__tests__/credentialsBuilder.spec.ts
0 → 100644
View file @
c7e18bd5
import
{
describe
,
it
,
expect
}
from
'
vitest
'
import
{
applyInterceptWarmup
}
from
'
../credentialsBuilder
'
describe
(
'
applyInterceptWarmup
'
,
()
=>
{
it
(
'
create + enabled=true: should set intercept_warmup_requests to true
'
,
()
=>
{
const
creds
:
Record
<
string
,
unknown
>
=
{
access_token
:
'
tok
'
}
applyInterceptWarmup
(
creds
,
true
,
'
create
'
)
expect
(
creds
.
intercept_warmup_requests
).
toBe
(
true
)
})
it
(
'
create + enabled=false: should not add the field
'
,
()
=>
{
const
creds
:
Record
<
string
,
unknown
>
=
{
access_token
:
'
tok
'
}
applyInterceptWarmup
(
creds
,
false
,
'
create
'
)
expect
(
'
intercept_warmup_requests
'
in
creds
).
toBe
(
false
)
})
it
(
'
edit + enabled=true: should set intercept_warmup_requests to true
'
,
()
=>
{
const
creds
:
Record
<
string
,
unknown
>
=
{
api_key
:
'
sk
'
}
applyInterceptWarmup
(
creds
,
true
,
'
edit
'
)
expect
(
creds
.
intercept_warmup_requests
).
toBe
(
true
)
})
it
(
'
edit + enabled=false + field exists: should delete the field
'
,
()
=>
{
const
creds
:
Record
<
string
,
unknown
>
=
{
api_key
:
'
sk
'
,
intercept_warmup_requests
:
true
}
applyInterceptWarmup
(
creds
,
false
,
'
edit
'
)
expect
(
'
intercept_warmup_requests
'
in
creds
).
toBe
(
false
)
})
it
(
'
edit + enabled=false + field absent: should not throw
'
,
()
=>
{
const
creds
:
Record
<
string
,
unknown
>
=
{
api_key
:
'
sk
'
}
applyInterceptWarmup
(
creds
,
false
,
'
edit
'
)
expect
(
'
intercept_warmup_requests
'
in
creds
).
toBe
(
false
)
})
it
(
'
should not affect other fields
'
,
()
=>
{
const
creds
:
Record
<
string
,
unknown
>
=
{
api_key
:
'
sk
'
,
base_url
:
'
url
'
,
intercept_warmup_requests
:
true
}
applyInterceptWarmup
(
creds
,
false
,
'
edit
'
)
expect
(
creds
.
api_key
).
toBe
(
'
sk
'
)
expect
(
creds
.
base_url
).
toBe
(
'
url
'
)
expect
(
'
intercept_warmup_requests
'
in
creds
).
toBe
(
false
)
})
})
frontend/src/components/account/credentialsBuilder.ts
0 → 100644
View file @
c7e18bd5
export
function
applyInterceptWarmup
(
credentials
:
Record
<
string
,
unknown
>
,
enabled
:
boolean
,
mode
:
'
create
'
|
'
edit
'
):
void
{
if
(
enabled
)
{
credentials
.
intercept_warmup_requests
=
true
}
else
if
(
mode
===
'
edit
'
)
{
delete
credentials
.
intercept_warmup_requests
}
}
frontend/src/composables/useModelWhitelist.ts
View file @
c7e18bd5
...
@@ -76,6 +76,7 @@ const antigravityModels = [
...
@@ -76,6 +76,7 @@ const antigravityModels = [
// Claude 4.5+ 系列
// Claude 4.5+ 系列
'
claude-opus-4-6
'
,
'
claude-opus-4-6
'
,
'
claude-opus-4-5-thinking
'
,
'
claude-opus-4-5-thinking
'
,
'
claude-sonnet-4-6
'
,
'
claude-sonnet-4-5
'
,
'
claude-sonnet-4-5
'
,
'
claude-sonnet-4-5-thinking
'
,
'
claude-sonnet-4-5-thinking
'
,
// Gemini 2.5 系列
// Gemini 2.5 系列
...
@@ -88,6 +89,9 @@ const antigravityModels = [
...
@@ -88,6 +89,9 @@ const antigravityModels = [
'
gemini-3-pro-high
'
,
'
gemini-3-pro-high
'
,
'
gemini-3-pro-low
'
,
'
gemini-3-pro-low
'
,
'
gemini-3-pro-image
'
,
'
gemini-3-pro-image
'
,
// Gemini 3.1 系列
'
gemini-3.1-pro-high
'
,
'
gemini-3.1-pro-low
'
,
// 其他
// 其他
'
gpt-oss-120b-medium
'
,
'
gpt-oss-120b-medium
'
,
'
tab_flash_lite_preview
'
'
tab_flash_lite_preview
'
...
@@ -301,6 +305,7 @@ const antigravityPresetMappings = [
...
@@ -301,6 +305,7 @@ const antigravityPresetMappings = [
{
label
:
'
3-Flash透传
'
,
from
:
'
gemini-3-flash
'
,
to
:
'
gemini-3-flash
'
,
color
:
'
bg-lime-100 text-lime-700 hover:bg-lime-200 dark:bg-lime-900/30 dark:text-lime-400
'
},
{
label
:
'
3-Flash透传
'
,
from
:
'
gemini-3-flash
'
,
to
:
'
gemini-3-flash
'
,
color
:
'
bg-lime-100 text-lime-700 hover:bg-lime-200 dark:bg-lime-900/30 dark:text-lime-400
'
},
{
label
:
'
2.5-Flash-Lite透传
'
,
from
:
'
gemini-2.5-flash-lite
'
,
to
:
'
gemini-2.5-flash-lite
'
,
color
:
'
bg-green-100 text-green-700 hover:bg-green-200 dark:bg-green-900/30 dark:text-green-400
'
},
{
label
:
'
2.5-Flash-Lite透传
'
,
from
:
'
gemini-2.5-flash-lite
'
,
to
:
'
gemini-2.5-flash-lite
'
,
color
:
'
bg-green-100 text-green-700 hover:bg-green-200 dark:bg-green-900/30 dark:text-green-400
'
},
// 精确映射
// 精确映射
{
label
:
'
Sonnet 4.6
'
,
from
:
'
claude-sonnet-4-6
'
,
to
:
'
claude-sonnet-4-6
'
,
color
:
'
bg-cyan-100 text-cyan-700 hover:bg-cyan-200 dark:bg-cyan-900/30 dark:text-cyan-400
'
},
{
label
:
'
Sonnet 4.5
'
,
from
:
'
claude-sonnet-4-5
'
,
to
:
'
claude-sonnet-4-5
'
,
color
:
'
bg-cyan-100 text-cyan-700 hover:bg-cyan-200 dark:bg-cyan-900/30 dark:text-cyan-400
'
},
{
label
:
'
Sonnet 4.5
'
,
from
:
'
claude-sonnet-4-5
'
,
to
:
'
claude-sonnet-4-5
'
,
color
:
'
bg-cyan-100 text-cyan-700 hover:bg-cyan-200 dark:bg-cyan-900/30 dark:text-cyan-400
'
},
{
label
:
'
Opus 4.6-thinking
'
,
from
:
'
claude-opus-4-6-thinking
'
,
to
:
'
claude-opus-4-6-thinking
'
,
color
:
'
bg-pink-100 text-pink-700 hover:bg-pink-200 dark:bg-pink-900/30 dark:text-pink-400
'
}
{
label
:
'
Opus 4.6-thinking
'
,
from
:
'
claude-opus-4-6-thinking
'
,
to
:
'
claude-opus-4-6-thinking
'
,
color
:
'
bg-pink-100 text-pink-700 hover:bg-pink-200 dark:bg-pink-900/30 dark:text-pink-400
'
}
]
]
...
...
frontend/src/i18n/locales/en.ts
View file @
c7e18bd5
...
@@ -2047,7 +2047,7 @@ export default {
...
@@ -2047,7 +2047,7 @@ export default {
gemini3Pro
:
'
G3P
'
,
gemini3Pro
:
'
G3P
'
,
gemini3Flash
:
'
G3F
'
,
gemini3Flash
:
'
G3F
'
,
gemini3Image
:
'
G3I
'
,
gemini3Image
:
'
G3I
'
,
claude
45
:
'
C
4.5
'
claude
:
'
C
laude
'
},
},
tier
:
{
tier
:
{
free
:
'
Free
'
,
free
:
'
Free
'
,
...
...
frontend/src/i18n/locales/zh.ts
View file @
c7e18bd5
...
@@ -1583,7 +1583,7 @@ export default {
...
@@ -1583,7 +1583,7 @@ export default {
gemini3Pro
:
'
G3P
'
,
gemini3Pro
:
'
G3P
'
,
gemini3Flash
:
'
G3F
'
,
gemini3Flash
:
'
G3F
'
,
gemini3Image
:
'
G3I
'
,
gemini3Image
:
'
G3I
'
,
claude
45
:
'
C
4.5
'
claude
:
'
C
laude
'
},
},
tier
:
{
tier
:
{
free
:
'
Free
'
,
free
:
'
Free
'
,
...
...
frontend/src/types/index.ts
View file @
c7e18bd5
...
@@ -581,6 +581,7 @@ export interface GeminiCredentials {
...
@@ -581,6 +581,7 @@ export interface GeminiCredentials {
token_type
?:
string
token_type
?:
string
scope
?:
string
scope
?:
string
expires_at
?:
string
expires_at
?:
string
model_mapping
?:
Record
<
string
,
string
>
}
}
export
interface
TempUnschedulableRule
{
export
interface
TempUnschedulableRule
{
...
@@ -766,6 +767,26 @@ export interface UpdateAccountRequest {
...
@@ -766,6 +767,26 @@ export interface UpdateAccountRequest {
confirm_mixed_channel_risk
?:
boolean
confirm_mixed_channel_risk
?:
boolean
}
}
export
interface
CheckMixedChannelRequest
{
platform
:
AccountPlatform
group_ids
:
number
[]
account_id
?:
number
}
export
interface
MixedChannelWarningDetails
{
group_id
:
number
group_name
:
string
current_platform
:
string
other_platform
:
string
}
export
interface
CheckMixedChannelResponse
{
has_risk
:
boolean
error
?:
string
message
?:
string
details
?:
MixedChannelWarningDetails
}
export
interface
CreateProxyRequest
{
export
interface
CreateProxyRequest
{
name
:
string
name
:
string
protocol
:
ProxyProtocol
protocol
:
ProxyProtocol
...
...
Prev
1
2
3
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