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
2d22623b
Unverified
Commit
2d22623b
authored
Dec 31, 2025
by
程序猿MT
Committed by
GitHub
Dec 31, 2025
Browse files
Merge branch 'Wei-Shaw:main' into main
parents
59269dc1
81213f23
Changes
10
Hide whitespace changes
Inline
Side-by-side
backend/internal/repository/setting_repo_integration_test.go
View file @
2d22623b
...
@@ -124,10 +124,10 @@ func (s *SettingRepoSuite) TestSetMultiple_WithEmptyValues() {
...
@@ -124,10 +124,10 @@ func (s *SettingRepoSuite) TestSetMultiple_WithEmptyValues() {
settings
:=
map
[
string
]
string
{
settings
:=
map
[
string
]
string
{
"site_name"
:
"AICodex2API"
,
"site_name"
:
"AICodex2API"
,
"site_subtitle"
:
"Subscription to API"
,
"site_subtitle"
:
"Subscription to API"
,
"site_logo"
:
""
,
// 用户未上传Logo
"site_logo"
:
""
,
// 用户未上传Logo
"api_base_url"
:
""
,
// 用户未设置API地址
"api_base_url"
:
""
,
// 用户未设置API地址
"contact_info"
:
""
,
// 用户未设置联系方式
"contact_info"
:
""
,
// 用户未设置联系方式
"doc_url"
:
""
,
// 用户未设置文档链接
"doc_url"
:
""
,
// 用户未设置文档链接
}
}
s
.
Require
()
.
NoError
(
s
.
repo
.
SetMultiple
(
s
.
ctx
,
settings
),
"SetMultiple with empty values should succeed"
)
s
.
Require
()
.
NoError
(
s
.
repo
.
SetMultiple
(
s
.
ctx
,
settings
),
"SetMultiple with empty values should succeed"
)
...
...
backend/internal/service/account.go
View file @
2d22623b
...
@@ -110,6 +110,28 @@ func (a *Account) GetCredential(key string) string {
...
@@ -110,6 +110,28 @@ func (a *Account) GetCredential(key string) string {
}
}
}
}
// GetCredentialAsTime 解析凭证中的时间戳字段,支持多种格式
// 兼容以下格式:
// - RFC3339 字符串: "2025-01-01T00:00:00Z"
// - Unix 时间戳字符串: "1735689600"
// - Unix 时间戳数字: 1735689600 (float64/int64/json.Number)
func
(
a
*
Account
)
GetCredentialAsTime
(
key
string
)
*
time
.
Time
{
s
:=
a
.
GetCredential
(
key
)
if
s
==
""
{
return
nil
}
// 尝试 RFC3339 格式
if
t
,
err
:=
time
.
Parse
(
time
.
RFC3339
,
s
);
err
==
nil
{
return
&
t
}
// 尝试 Unix 时间戳(纯数字字符串)
if
ts
,
err
:=
strconv
.
ParseInt
(
s
,
10
,
64
);
err
==
nil
{
t
:=
time
.
Unix
(
ts
,
0
)
return
&
t
}
return
nil
}
func
(
a
*
Account
)
GetModelMapping
()
map
[
string
]
string
{
func
(
a
*
Account
)
GetModelMapping
()
map
[
string
]
string
{
if
a
.
Credentials
==
nil
{
if
a
.
Credentials
==
nil
{
return
nil
return
nil
...
@@ -324,19 +346,7 @@ func (a *Account) GetOpenAITokenExpiresAt() *time.Time {
...
@@ -324,19 +346,7 @@ func (a *Account) GetOpenAITokenExpiresAt() *time.Time {
if
!
a
.
IsOpenAIOAuth
()
{
if
!
a
.
IsOpenAIOAuth
()
{
return
nil
return
nil
}
}
expiresAtStr
:=
a
.
GetCredential
(
"expires_at"
)
return
a
.
GetCredentialAsTime
(
"expires_at"
)
if
expiresAtStr
==
""
{
return
nil
}
t
,
err
:=
time
.
Parse
(
time
.
RFC3339
,
expiresAtStr
)
if
err
!=
nil
{
if
v
,
ok
:=
a
.
Credentials
[
"expires_at"
]
.
(
float64
);
ok
{
tt
:=
time
.
Unix
(
int64
(
v
),
0
)
return
&
tt
}
return
nil
}
return
&
t
}
}
func
(
a
*
Account
)
IsOpenAITokenExpired
()
bool
{
func
(
a
*
Account
)
IsOpenAITokenExpired
()
bool
{
...
...
backend/internal/service/account_test_service.go
View file @
2d22623b
...
@@ -12,7 +12,6 @@ import (
...
@@ -12,7 +12,6 @@ import (
"log"
"log"
"net/http"
"net/http"
"regexp"
"regexp"
"strconv"
"strings"
"strings"
"time"
"time"
...
@@ -187,9 +186,8 @@ func (s *AccountTestService) testClaudeAccountConnection(c *gin.Context, account
...
@@ -187,9 +186,8 @@ func (s *AccountTestService) testClaudeAccountConnection(c *gin.Context, account
// Check if token needs refresh
// Check if token needs refresh
needRefresh
:=
false
needRefresh
:=
false
if
expiresAtStr
:=
account
.
GetCredential
(
"expires_at"
);
expiresAtStr
!=
""
{
if
expiresAt
:=
account
.
GetCredentialAsTime
(
"expires_at"
);
expiresAt
!=
nil
{
expiresAt
,
err
:=
strconv
.
ParseInt
(
expiresAtStr
,
10
,
64
)
if
time
.
Now
()
.
Add
(
5
*
time
.
Minute
)
.
After
(
*
expiresAt
)
{
if
err
==
nil
&&
time
.
Now
()
.
Unix
()
+
300
>
expiresAt
{
needRefresh
=
true
needRefresh
=
true
}
}
}
}
...
...
backend/internal/service/antigravity_quota_refresher.go
View file @
2d22623b
...
@@ -191,7 +191,7 @@ func (r *AntigravityQuotaRefresher) refreshAccountQuota(ctx context.Context, acc
...
@@ -191,7 +191,7 @@ func (r *AntigravityQuotaRefresher) refreshAccountQuota(ctx context.Context, acc
// isTokenExpired 检查 token 是否过期
// isTokenExpired 检查 token 是否过期
func
(
r
*
AntigravityQuotaRefresher
)
isTokenExpired
(
account
*
Account
)
bool
{
func
(
r
*
AntigravityQuotaRefresher
)
isTokenExpired
(
account
*
Account
)
bool
{
expiresAt
:=
parseAntigravityExpiresAt
(
account
)
expiresAt
:=
account
.
GetCredentialAsTime
(
"expires_at"
)
if
expiresAt
==
nil
{
if
expiresAt
==
nil
{
return
false
return
false
}
}
...
...
backend/internal/service/antigravity_token_provider.go
View file @
2d22623b
...
@@ -55,7 +55,7 @@ func (p *AntigravityTokenProvider) GetAccessToken(ctx context.Context, account *
...
@@ -55,7 +55,7 @@ func (p *AntigravityTokenProvider) GetAccessToken(ctx context.Context, account *
}
}
// 2. 如果即将过期则刷新
// 2. 如果即将过期则刷新
expiresAt
:=
parseAntigravityExpiresAt
(
account
)
expiresAt
:=
account
.
GetCredentialAsTime
(
"expires_at"
)
needsRefresh
:=
expiresAt
==
nil
||
time
.
Until
(
*
expiresAt
)
<=
antigravityTokenRefreshSkew
needsRefresh
:=
expiresAt
==
nil
||
time
.
Until
(
*
expiresAt
)
<=
antigravityTokenRefreshSkew
if
needsRefresh
&&
p
.
tokenCache
!=
nil
{
if
needsRefresh
&&
p
.
tokenCache
!=
nil
{
locked
,
err
:=
p
.
tokenCache
.
AcquireRefreshLock
(
ctx
,
cacheKey
,
30
*
time
.
Second
)
locked
,
err
:=
p
.
tokenCache
.
AcquireRefreshLock
(
ctx
,
cacheKey
,
30
*
time
.
Second
)
...
@@ -72,7 +72,7 @@ func (p *AntigravityTokenProvider) GetAccessToken(ctx context.Context, account *
...
@@ -72,7 +72,7 @@ func (p *AntigravityTokenProvider) GetAccessToken(ctx context.Context, account *
if
err
==
nil
&&
fresh
!=
nil
{
if
err
==
nil
&&
fresh
!=
nil
{
account
=
fresh
account
=
fresh
}
}
expiresAt
=
parseAntigravityExpiresAt
(
account
)
expiresAt
=
account
.
GetCredentialAsTime
(
"expires_at"
)
if
expiresAt
==
nil
||
time
.
Until
(
*
expiresAt
)
<=
antigravityTokenRefreshSkew
{
if
expiresAt
==
nil
||
time
.
Until
(
*
expiresAt
)
<=
antigravityTokenRefreshSkew
{
if
p
.
antigravityOAuthService
==
nil
{
if
p
.
antigravityOAuthService
==
nil
{
return
""
,
errors
.
New
(
"antigravity oauth service not configured"
)
return
""
,
errors
.
New
(
"antigravity oauth service not configured"
)
...
@@ -91,7 +91,7 @@ func (p *AntigravityTokenProvider) GetAccessToken(ctx context.Context, account *
...
@@ -91,7 +91,7 @@ func (p *AntigravityTokenProvider) GetAccessToken(ctx context.Context, account *
if
updateErr
:=
p
.
accountRepo
.
Update
(
ctx
,
account
);
updateErr
!=
nil
{
if
updateErr
:=
p
.
accountRepo
.
Update
(
ctx
,
account
);
updateErr
!=
nil
{
log
.
Printf
(
"[AntigravityTokenProvider] Failed to update account credentials: %v"
,
updateErr
)
log
.
Printf
(
"[AntigravityTokenProvider] Failed to update account credentials: %v"
,
updateErr
)
}
}
expiresAt
=
parseAntigravityExpiresAt
(
account
)
expiresAt
=
account
.
GetCredentialAsTime
(
"expires_at"
)
}
}
}
}
}
}
...
@@ -128,18 +128,3 @@ func antigravityTokenCacheKey(account *Account) string {
...
@@ -128,18 +128,3 @@ func antigravityTokenCacheKey(account *Account) string {
}
}
return
"ag:account:"
+
strconv
.
FormatInt
(
account
.
ID
,
10
)
return
"ag:account:"
+
strconv
.
FormatInt
(
account
.
ID
,
10
)
}
}
func
parseAntigravityExpiresAt
(
account
*
Account
)
*
time
.
Time
{
raw
:=
strings
.
TrimSpace
(
account
.
GetCredential
(
"expires_at"
))
if
raw
==
""
{
return
nil
}
if
unixSec
,
err
:=
strconv
.
ParseInt
(
raw
,
10
,
64
);
err
==
nil
&&
unixSec
>
0
{
t
:=
time
.
Unix
(
unixSec
,
0
)
return
&
t
}
if
t
,
err
:=
time
.
Parse
(
time
.
RFC3339
,
raw
);
err
==
nil
{
return
&
t
}
return
nil
}
backend/internal/service/antigravity_token_refresher.go
View file @
2d22623b
...
@@ -2,7 +2,6 @@ package service
...
@@ -2,7 +2,6 @@ package service
import
(
import
(
"context"
"context"
"strconv"
"time"
"time"
)
)
...
@@ -34,16 +33,11 @@ func (r *AntigravityTokenRefresher) NeedsRefresh(account *Account, _ time.Durati
...
@@ -34,16 +33,11 @@ func (r *AntigravityTokenRefresher) NeedsRefresh(account *Account, _ time.Durati
if
!
r
.
CanRefresh
(
account
)
{
if
!
r
.
CanRefresh
(
account
)
{
return
false
return
false
}
}
expiresAt
Str
:=
account
.
GetCredential
(
"expires_at"
)
expiresAt
:=
account
.
GetCredential
AsTime
(
"expires_at"
)
if
expiresAt
Str
==
""
{
if
expiresAt
==
nil
{
return
false
return
false
}
}
expiresAt
,
err
:=
strconv
.
ParseInt
(
expiresAtStr
,
10
,
64
)
return
time
.
Until
(
*
expiresAt
)
<
antigravityRefreshWindow
if
err
!=
nil
{
return
false
}
expiryTime
:=
time
.
Unix
(
expiresAt
,
0
)
return
time
.
Until
(
expiryTime
)
<
antigravityRefreshWindow
}
}
// Refresh 执行 token 刷新
// Refresh 执行 token 刷新
...
...
backend/internal/service/gemini_token_provider.go
View file @
2d22623b
...
@@ -50,7 +50,7 @@ func (p *GeminiTokenProvider) GetAccessToken(ctx context.Context, account *Accou
...
@@ -50,7 +50,7 @@ func (p *GeminiTokenProvider) GetAccessToken(ctx context.Context, account *Accou
}
}
// 2) Refresh if needed (pre-expiry skew).
// 2) Refresh if needed (pre-expiry skew).
expiresAt
:=
parseExpiresAt
(
account
)
expiresAt
:=
account
.
GetCredentialAsTime
(
"expires_at"
)
needsRefresh
:=
expiresAt
==
nil
||
time
.
Until
(
*
expiresAt
)
<=
geminiTokenRefreshSkew
needsRefresh
:=
expiresAt
==
nil
||
time
.
Until
(
*
expiresAt
)
<=
geminiTokenRefreshSkew
if
needsRefresh
&&
p
.
tokenCache
!=
nil
{
if
needsRefresh
&&
p
.
tokenCache
!=
nil
{
locked
,
err
:=
p
.
tokenCache
.
AcquireRefreshLock
(
ctx
,
cacheKey
,
30
*
time
.
Second
)
locked
,
err
:=
p
.
tokenCache
.
AcquireRefreshLock
(
ctx
,
cacheKey
,
30
*
time
.
Second
)
...
@@ -66,7 +66,7 @@ func (p *GeminiTokenProvider) GetAccessToken(ctx context.Context, account *Accou
...
@@ -66,7 +66,7 @@ func (p *GeminiTokenProvider) GetAccessToken(ctx context.Context, account *Accou
if
err
==
nil
&&
fresh
!=
nil
{
if
err
==
nil
&&
fresh
!=
nil
{
account
=
fresh
account
=
fresh
}
}
expiresAt
=
parseExpiresAt
(
account
)
expiresAt
=
account
.
GetCredentialAsTime
(
"expires_at"
)
if
expiresAt
==
nil
||
time
.
Until
(
*
expiresAt
)
<=
geminiTokenRefreshSkew
{
if
expiresAt
==
nil
||
time
.
Until
(
*
expiresAt
)
<=
geminiTokenRefreshSkew
{
if
p
.
geminiOAuthService
==
nil
{
if
p
.
geminiOAuthService
==
nil
{
return
""
,
errors
.
New
(
"gemini oauth service not configured"
)
return
""
,
errors
.
New
(
"gemini oauth service not configured"
)
...
@@ -83,7 +83,7 @@ func (p *GeminiTokenProvider) GetAccessToken(ctx context.Context, account *Accou
...
@@ -83,7 +83,7 @@ func (p *GeminiTokenProvider) GetAccessToken(ctx context.Context, account *Accou
}
}
account
.
Credentials
=
newCredentials
account
.
Credentials
=
newCredentials
_
=
p
.
accountRepo
.
Update
(
ctx
,
account
)
_
=
p
.
accountRepo
.
Update
(
ctx
,
account
)
expiresAt
=
parseExpiresAt
(
account
)
expiresAt
=
account
.
GetCredentialAsTime
(
"expires_at"
)
}
}
}
}
}
}
...
@@ -154,18 +154,3 @@ func geminiTokenCacheKey(account *Account) string {
...
@@ -154,18 +154,3 @@ func geminiTokenCacheKey(account *Account) string {
}
}
return
"account:"
+
strconv
.
FormatInt
(
account
.
ID
,
10
)
return
"account:"
+
strconv
.
FormatInt
(
account
.
ID
,
10
)
}
}
func
parseExpiresAt
(
account
*
Account
)
*
time
.
Time
{
raw
:=
strings
.
TrimSpace
(
account
.
GetCredential
(
"expires_at"
))
if
raw
==
""
{
return
nil
}
if
unixSec
,
err
:=
strconv
.
ParseInt
(
raw
,
10
,
64
);
err
==
nil
&&
unixSec
>
0
{
t
:=
time
.
Unix
(
unixSec
,
0
)
return
&
t
}
if
t
,
err
:=
time
.
Parse
(
time
.
RFC3339
,
raw
);
err
==
nil
{
return
&
t
}
return
nil
}
backend/internal/service/gemini_token_refresher.go
View file @
2d22623b
...
@@ -2,7 +2,6 @@ package service
...
@@ -2,7 +2,6 @@ package service
import
(
import
(
"context"
"context"
"strconv"
"time"
"time"
)
)
...
@@ -22,16 +21,11 @@ func (r *GeminiTokenRefresher) NeedsRefresh(account *Account, refreshWindow time
...
@@ -22,16 +21,11 @@ func (r *GeminiTokenRefresher) NeedsRefresh(account *Account, refreshWindow time
if
!
r
.
CanRefresh
(
account
)
{
if
!
r
.
CanRefresh
(
account
)
{
return
false
return
false
}
}
expiresAt
Str
:=
account
.
GetCredential
(
"expires_at"
)
expiresAt
:=
account
.
GetCredential
AsTime
(
"expires_at"
)
if
expiresAt
Str
==
""
{
if
expiresAt
==
nil
{
return
false
return
false
}
}
expiresAt
,
err
:=
strconv
.
ParseInt
(
expiresAtStr
,
10
,
64
)
return
time
.
Until
(
*
expiresAt
)
<
refreshWindow
if
err
!=
nil
{
return
false
}
expiryTime
:=
time
.
Unix
(
expiresAt
,
0
)
return
time
.
Until
(
expiryTime
)
<
refreshWindow
}
}
func
(
r
*
GeminiTokenRefresher
)
Refresh
(
ctx
context
.
Context
,
account
*
Account
)
(
map
[
string
]
any
,
error
)
{
func
(
r
*
GeminiTokenRefresher
)
Refresh
(
ctx
context
.
Context
,
account
*
Account
)
(
map
[
string
]
any
,
error
)
{
...
...
backend/internal/service/token_refresher.go
View file @
2d22623b
...
@@ -43,17 +43,11 @@ func (r *ClaudeTokenRefresher) CanRefresh(account *Account) bool {
...
@@ -43,17 +43,11 @@ func (r *ClaudeTokenRefresher) CanRefresh(account *Account) bool {
// NeedsRefresh 检查token是否需要刷新
// NeedsRefresh 检查token是否需要刷新
// 基于 expires_at 字段判断是否在刷新窗口内
// 基于 expires_at 字段判断是否在刷新窗口内
func
(
r
*
ClaudeTokenRefresher
)
NeedsRefresh
(
account
*
Account
,
refreshWindow
time
.
Duration
)
bool
{
func
(
r
*
ClaudeTokenRefresher
)
NeedsRefresh
(
account
*
Account
,
refreshWindow
time
.
Duration
)
bool
{
s
:=
account
.
GetCredential
(
"expires_at"
)
expiresAt
:=
account
.
GetCredentialAsTime
(
"expires_at"
)
if
s
==
""
{
if
expiresAt
==
nil
{
return
false
}
expiresAt
,
err
:=
strconv
.
ParseInt
(
s
,
10
,
64
)
if
err
!=
nil
{
return
false
return
false
}
}
return
time
.
Until
(
*
expiresAt
)
<
refreshWindow
return
time
.
Until
(
time
.
Unix
(
expiresAt
,
0
))
<
refreshWindow
}
}
// Refresh 执行token刷新
// Refresh 执行token刷新
...
...
backend/internal/service/token_refresher_test.go
View file @
2d22623b
...
@@ -33,6 +33,13 @@ func TestClaudeTokenRefresher_NeedsRefresh(t *testing.T) {
...
@@ -33,6 +33,13 @@ func TestClaudeTokenRefresher_NeedsRefresh(t *testing.T) {
},
},
wantRefresh
:
true
,
wantRefresh
:
true
,
},
},
{
name
:
"expires_at as RFC3339 - expired"
,
credentials
:
map
[
string
]
any
{
"expires_at"
:
"1970-01-01T00:00:00Z"
,
// RFC3339 格式,已过期
},
wantRefresh
:
true
,
},
{
{
name
:
"expires_at as string - far future"
,
name
:
"expires_at as string - far future"
,
credentials
:
map
[
string
]
any
{
credentials
:
map
[
string
]
any
{
...
@@ -47,6 +54,13 @@ func TestClaudeTokenRefresher_NeedsRefresh(t *testing.T) {
...
@@ -47,6 +54,13 @@ func TestClaudeTokenRefresher_NeedsRefresh(t *testing.T) {
},
},
wantRefresh
:
false
,
wantRefresh
:
false
,
},
},
{
name
:
"expires_at as RFC3339 - far future"
,
credentials
:
map
[
string
]
any
{
"expires_at"
:
"2099-12-31T23:59:59Z"
,
// RFC3339 格式,远未来
},
wantRefresh
:
false
,
},
{
{
name
:
"expires_at missing"
,
name
:
"expires_at missing"
,
credentials
:
map
[
string
]
any
{},
credentials
:
map
[
string
]
any
{},
...
...
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