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
03c75787
Unverified
Commit
03c75787
authored
Jan 19, 2026
by
Wesley Liddick
Committed by
GitHub
Jan 19, 2026
Browse files
Merge pull request #325 from slovx2/main
fix(antigravity): 修复Antigravity 频繁429的问题,以及一系列优化,配置增强
parents
de6797c5
c115c9e0
Changes
28
Hide whitespace changes
Inline
Side-by-side
backend/internal/service/antigravity_token_refresher.go
View file @
03c75787
...
@@ -61,5 +61,10 @@ func (r *AntigravityTokenRefresher) Refresh(ctx context.Context, account *Accoun
...
@@ -61,5 +61,10 @@ func (r *AntigravityTokenRefresher) Refresh(ctx context.Context, account *Accoun
}
}
}
}
// 如果 project_id 获取失败,返回 credentials 但同时返回错误让账户被标记
if
tokenInfo
.
ProjectIDMissing
{
return
newCredentials
,
fmt
.
Errorf
(
"missing_project_id: 账户缺少project id,可能无法使用Antigravity"
)
}
return
newCredentials
,
nil
return
newCredentials
,
nil
}
}
backend/internal/service/gateway_multiplatform_test.go
View file @
03c75787
...
@@ -105,6 +105,9 @@ func (m *mockAccountRepoForPlatform) BatchUpdateLastUsed(ctx context.Context, up
...
@@ -105,6 +105,9 @@ func (m *mockAccountRepoForPlatform) BatchUpdateLastUsed(ctx context.Context, up
func
(
m
*
mockAccountRepoForPlatform
)
SetError
(
ctx
context
.
Context
,
id
int64
,
errorMsg
string
)
error
{
func
(
m
*
mockAccountRepoForPlatform
)
SetError
(
ctx
context
.
Context
,
id
int64
,
errorMsg
string
)
error
{
return
nil
return
nil
}
}
func
(
m
*
mockAccountRepoForPlatform
)
ClearError
(
ctx
context
.
Context
,
id
int64
)
error
{
return
nil
}
func
(
m
*
mockAccountRepoForPlatform
)
SetSchedulable
(
ctx
context
.
Context
,
id
int64
,
schedulable
bool
)
error
{
func
(
m
*
mockAccountRepoForPlatform
)
SetSchedulable
(
ctx
context
.
Context
,
id
int64
,
schedulable
bool
)
error
{
return
nil
return
nil
}
}
...
...
backend/internal/service/gateway_service.go
View file @
03c75787
...
@@ -11,6 +11,7 @@ import (
...
@@ -11,6 +11,7 @@ import (
"fmt"
"fmt"
"io"
"io"
"log"
"log"
mathrand
"math/rand"
"net/http"
"net/http"
"os"
"os"
"regexp"
"regexp"
...
@@ -918,7 +919,7 @@ func (s *GatewayService) SelectAccountWithLoadAwareness(ctx context.Context, gro
...
@@ -918,7 +919,7 @@ func (s *GatewayService) SelectAccountWithLoadAwareness(ctx context.Context, gro
}
}
// ============ Layer 3: 兜底排队 ============
// ============ Layer 3: 兜底排队 ============
sort
AccountsByPriorityAndLastUsed
(
candidates
,
preferOAuth
)
s
.
sort
CandidatesForFallback
(
candidates
,
preferOAuth
,
cfg
.
FallbackSelectionMode
)
for
_
,
acc
:=
range
candidates
{
for
_
,
acc
:=
range
candidates
{
// 会话数量限制检查(等待计划也需要占用会话配额)
// 会话数量限制检查(等待计划也需要占用会话配额)
if
!
s
.
checkAndRegisterSession
(
ctx
,
acc
,
sessionHash
)
{
if
!
s
.
checkAndRegisterSession
(
ctx
,
acc
,
sessionHash
)
{
...
@@ -1318,6 +1319,56 @@ func sortAccountsByPriorityAndLastUsed(accounts []*Account, preferOAuth bool) {
...
@@ -1318,6 +1319,56 @@ func sortAccountsByPriorityAndLastUsed(accounts []*Account, preferOAuth bool) {
})
})
}
}
// sortCandidatesForFallback 根据配置选择排序策略
// mode: "last_used"(按最后使用时间) 或 "random"(随机)
func
(
s
*
GatewayService
)
sortCandidatesForFallback
(
accounts
[]
*
Account
,
preferOAuth
bool
,
mode
string
)
{
if
mode
==
"random"
{
// 先按优先级排序,然后在同优先级内随机打乱
sortAccountsByPriorityOnly
(
accounts
,
preferOAuth
)
shuffleWithinPriority
(
accounts
)
}
else
{
// 默认按最后使用时间排序
sortAccountsByPriorityAndLastUsed
(
accounts
,
preferOAuth
)
}
}
// sortAccountsByPriorityOnly 仅按优先级排序
func
sortAccountsByPriorityOnly
(
accounts
[]
*
Account
,
preferOAuth
bool
)
{
sort
.
SliceStable
(
accounts
,
func
(
i
,
j
int
)
bool
{
a
,
b
:=
accounts
[
i
],
accounts
[
j
]
if
a
.
Priority
!=
b
.
Priority
{
return
a
.
Priority
<
b
.
Priority
}
if
preferOAuth
&&
a
.
Type
!=
b
.
Type
{
return
a
.
Type
==
AccountTypeOAuth
}
return
false
})
}
// shuffleWithinPriority 在同优先级内随机打乱顺序
func
shuffleWithinPriority
(
accounts
[]
*
Account
)
{
if
len
(
accounts
)
<=
1
{
return
}
r
:=
mathrand
.
New
(
mathrand
.
NewSource
(
time
.
Now
()
.
UnixNano
()))
start
:=
0
for
start
<
len
(
accounts
)
{
priority
:=
accounts
[
start
]
.
Priority
end
:=
start
+
1
for
end
<
len
(
accounts
)
&&
accounts
[
end
]
.
Priority
==
priority
{
end
++
}
// 对 [start, end) 范围内的账户随机打乱
if
end
-
start
>
1
{
r
.
Shuffle
(
end
-
start
,
func
(
i
,
j
int
)
{
accounts
[
start
+
i
],
accounts
[
start
+
j
]
=
accounts
[
start
+
j
],
accounts
[
start
+
i
]
})
}
start
=
end
}
}
// selectAccountForModelWithPlatform 选择单平台账户(完全隔离)
// selectAccountForModelWithPlatform 选择单平台账户(完全隔离)
func
(
s
*
GatewayService
)
selectAccountForModelWithPlatform
(
ctx
context
.
Context
,
groupID
*
int64
,
sessionHash
string
,
requestedModel
string
,
excludedIDs
map
[
int64
]
struct
{},
platform
string
)
(
*
Account
,
error
)
{
func
(
s
*
GatewayService
)
selectAccountForModelWithPlatform
(
ctx
context
.
Context
,
groupID
*
int64
,
sessionHash
string
,
requestedModel
string
,
excludedIDs
map
[
int64
]
struct
{},
platform
string
)
(
*
Account
,
error
)
{
preferOAuth
:=
platform
==
PlatformGemini
preferOAuth
:=
platform
==
PlatformGemini
...
...
backend/internal/service/gemini_multiplatform_test.go
View file @
03c75787
...
@@ -88,6 +88,9 @@ func (m *mockAccountRepoForGemini) BatchUpdateLastUsed(ctx context.Context, upda
...
@@ -88,6 +88,9 @@ func (m *mockAccountRepoForGemini) BatchUpdateLastUsed(ctx context.Context, upda
func
(
m
*
mockAccountRepoForGemini
)
SetError
(
ctx
context
.
Context
,
id
int64
,
errorMsg
string
)
error
{
func
(
m
*
mockAccountRepoForGemini
)
SetError
(
ctx
context
.
Context
,
id
int64
,
errorMsg
string
)
error
{
return
nil
return
nil
}
}
func
(
m
*
mockAccountRepoForGemini
)
ClearError
(
ctx
context
.
Context
,
id
int64
)
error
{
return
nil
}
func
(
m
*
mockAccountRepoForGemini
)
SetSchedulable
(
ctx
context
.
Context
,
id
int64
,
schedulable
bool
)
error
{
func
(
m
*
mockAccountRepoForGemini
)
SetSchedulable
(
ctx
context
.
Context
,
id
int64
,
schedulable
bool
)
error
{
return
nil
return
nil
}
}
...
...
backend/internal/service/token_refresh_service.go
View file @
03c75787
...
@@ -166,11 +166,25 @@ func (s *TokenRefreshService) refreshWithRetry(ctx context.Context, account *Acc
...
@@ -166,11 +166,25 @@ func (s *TokenRefreshService) refreshWithRetry(ctx context.Context, account *Acc
for
attempt
:=
1
;
attempt
<=
s
.
cfg
.
MaxRetries
;
attempt
++
{
for
attempt
:=
1
;
attempt
<=
s
.
cfg
.
MaxRetries
;
attempt
++
{
newCredentials
,
err
:=
refresher
.
Refresh
(
ctx
,
account
)
newCredentials
,
err
:=
refresher
.
Refresh
(
ctx
,
account
)
if
err
==
nil
{
// 刷新成功,更新账号credentials
// 如果有新凭证,先更新(即使有错误也要保存 token)
if
newCredentials
!=
nil
{
account
.
Credentials
=
newCredentials
account
.
Credentials
=
newCredentials
if
err
:=
s
.
accountRepo
.
Update
(
ctx
,
account
);
err
!=
nil
{
if
saveErr
:=
s
.
accountRepo
.
Update
(
ctx
,
account
);
saveErr
!=
nil
{
return
fmt
.
Errorf
(
"failed to save credentials: %w"
,
err
)
return
fmt
.
Errorf
(
"failed to save credentials: %w"
,
saveErr
)
}
}
if
err
==
nil
{
// Antigravity 账户:如果之前是因为缺少 project_id 而标记为 error,现在成功获取到了,清除错误状态
if
account
.
Platform
==
PlatformAntigravity
&&
account
.
Status
==
StatusError
&&
strings
.
Contains
(
account
.
ErrorMessage
,
"missing_project_id:"
)
{
if
clearErr
:=
s
.
accountRepo
.
ClearError
(
ctx
,
account
.
ID
);
clearErr
!=
nil
{
log
.
Printf
(
"[TokenRefresh] Failed to clear error status for account %d: %v"
,
account
.
ID
,
clearErr
)
}
else
{
log
.
Printf
(
"[TokenRefresh] Account %d: cleared missing_project_id error"
,
account
.
ID
)
}
}
}
// 对所有 OAuth 账号调用缓存失效(InvalidateToken 内部根据平台判断是否需要处理)
// 对所有 OAuth 账号调用缓存失效(InvalidateToken 内部根据平台判断是否需要处理)
if
s
.
cacheInvalidator
!=
nil
&&
account
.
Type
==
AccountTypeOAuth
{
if
s
.
cacheInvalidator
!=
nil
&&
account
.
Type
==
AccountTypeOAuth
{
...
@@ -230,6 +244,7 @@ func isNonRetryableRefreshError(err error) bool {
...
@@ -230,6 +244,7 @@ func isNonRetryableRefreshError(err error) bool {
"invalid_client"
,
// 客户端配置错误
"invalid_client"
,
// 客户端配置错误
"unauthorized_client"
,
// 客户端未授权
"unauthorized_client"
,
// 客户端未授权
"access_denied"
,
// 访问被拒绝
"access_denied"
,
// 访问被拒绝
"missing_project_id"
,
// 缺少 project_id
}
}
for
_
,
needle
:=
range
nonRetryable
{
for
_
,
needle
:=
range
nonRetryable
{
if
strings
.
Contains
(
msg
,
needle
)
{
if
strings
.
Contains
(
msg
,
needle
)
{
...
...
frontend/src/components/layout/AppHeader.vue
View file @
03c75787
...
@@ -21,8 +21,20 @@
...
@@ -21,8 +21,20 @@
</div>
</div>
</div>
</div>
<!-- Right: Language + Subscriptions + Balance + User Dropdown -->
<!-- Right:
Docs +
Language + Subscriptions + Balance + User Dropdown -->
<div
class=
"flex items-center gap-3"
>
<div
class=
"flex items-center gap-3"
>
<!-- Docs Link -->
<a
v-if=
"docUrl"
:href=
"docUrl"
target=
"_blank"
rel=
"noopener noreferrer"
class=
"flex items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-sm font-medium text-gray-600 transition-colors hover:bg-gray-100 hover:text-gray-900 dark:text-dark-400 dark:hover:bg-dark-800 dark:hover:text-white"
>
<Icon
name=
"book"
size=
"sm"
/>
<span
class=
"hidden sm:inline"
>
{{
t
(
'
nav.docs
'
)
}}
</span>
</a>
<!-- Language Switcher -->
<!-- Language Switcher -->
<LocaleSwitcher
/>
<LocaleSwitcher
/>
...
@@ -211,6 +223,7 @@ const user = computed(() => authStore.user)
...
@@ -211,6 +223,7 @@ const user = computed(() => authStore.user)
const
dropdownOpen
=
ref
(
false
)
const
dropdownOpen
=
ref
(
false
)
const
dropdownRef
=
ref
<
HTMLElement
|
null
>
(
null
)
const
dropdownRef
=
ref
<
HTMLElement
|
null
>
(
null
)
const
contactInfo
=
computed
(()
=>
appStore
.
contactInfo
)
const
contactInfo
=
computed
(()
=>
appStore
.
contactInfo
)
const
docUrl
=
computed
(()
=>
appStore
.
docUrl
)
// 只在标准模式的管理员下显示新手引导按钮
// 只在标准模式的管理员下显示新手引导按钮
const
showOnboardingButton
=
computed
(()
=>
{
const
showOnboardingButton
=
computed
(()
=>
{
...
...
frontend/src/i18n/locales/en.ts
View file @
03c75787
...
@@ -196,7 +196,8 @@ export default {
...
@@ -196,7 +196,8 @@ export default {
expand
:
'
Expand
'
,
expand
:
'
Expand
'
,
logout
:
'
Logout
'
,
logout
:
'
Logout
'
,
github
:
'
GitHub
'
,
github
:
'
GitHub
'
,
mySubscriptions
:
'
My Subscriptions
'
mySubscriptions
:
'
My Subscriptions
'
,
docs
:
'
Docs
'
},
},
// Auth
// Auth
...
...
frontend/src/i18n/locales/zh.ts
View file @
03c75787
...
@@ -193,7 +193,8 @@ export default {
...
@@ -193,7 +193,8 @@ export default {
expand
:
'
展开
'
,
expand
:
'
展开
'
,
logout
:
'
退出登录
'
,
logout
:
'
退出登录
'
,
github
:
'
GitHub
'
,
github
:
'
GitHub
'
,
mySubscriptions
:
'
我的订阅
'
mySubscriptions
:
'
我的订阅
'
,
docs
:
'
文档
'
},
},
// Auth
// Auth
...
...
Prev
1
2
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