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
61f55674
Commit
61f55674
authored
Jan 10, 2026
by
yangjianbo
Browse files
Merge branch 'main' of
https://github.com/mt21625457/aicodex2api
parents
eeb1282f
7d1fe818
Changes
103
Hide whitespace changes
Inline
Side-by-side
backend/internal/service/gateway_service.go
View file @
61f55674
...
@@ -33,7 +33,7 @@ const (
...
@@ -33,7 +33,7 @@ const (
claudeAPIURL
=
"https://api.anthropic.com/v1/messages?beta=true"
claudeAPIURL
=
"https://api.anthropic.com/v1/messages?beta=true"
claudeAPICountTokensURL
=
"https://api.anthropic.com/v1/messages/count_tokens?beta=true"
claudeAPICountTokensURL
=
"https://api.anthropic.com/v1/messages/count_tokens?beta=true"
stickySessionTTL
=
time
.
Hour
// 粘性会话TTL
stickySessionTTL
=
time
.
Hour
// 粘性会话TTL
defaultMaxLineSize
=
1
0
*
1024
*
1024
defaultMaxLineSize
=
4
0
*
1024
*
1024
claudeCodeSystemPrompt
=
"You are Claude Code, Anthropic's official CLI for Claude."
claudeCodeSystemPrompt
=
"You are Claude Code, Anthropic's official CLI for Claude."
maxCacheControlBlocks
=
4
// Anthropic API 允许的最大 cache_control 块数量
maxCacheControlBlocks
=
4
// Anthropic API 允许的最大 cache_control 块数量
)
)
...
@@ -481,7 +481,7 @@ func (s *GatewayService) SelectAccountWithLoadAwareness(ctx context.Context, gro
...
@@ -481,7 +481,7 @@ func (s *GatewayService) SelectAccountWithLoadAwareness(ctx context.Context, gro
account
,
err
:=
s
.
accountRepo
.
GetByID
(
ctx
,
accountID
)
account
,
err
:=
s
.
accountRepo
.
GetByID
(
ctx
,
accountID
)
if
err
==
nil
&&
s
.
isAccountInGroup
(
account
,
groupID
)
&&
if
err
==
nil
&&
s
.
isAccountInGroup
(
account
,
groupID
)
&&
s
.
isAccountAllowedForPlatform
(
account
,
platform
,
useMixed
)
&&
s
.
isAccountAllowedForPlatform
(
account
,
platform
,
useMixed
)
&&
account
.
IsSchedulable
(
)
&&
account
.
IsSchedulable
ForModel
(
requestedModel
)
&&
(
requestedModel
==
""
||
s
.
isModelSupportedByAccount
(
account
,
requestedModel
))
{
(
requestedModel
==
""
||
s
.
isModelSupportedByAccount
(
account
,
requestedModel
))
{
result
,
err
:=
s
.
tryAcquireAccountSlot
(
ctx
,
accountID
,
account
.
Concurrency
)
result
,
err
:=
s
.
tryAcquireAccountSlot
(
ctx
,
accountID
,
account
.
Concurrency
)
if
err
==
nil
&&
result
.
Acquired
{
if
err
==
nil
&&
result
.
Acquired
{
...
@@ -519,6 +519,9 @@ func (s *GatewayService) SelectAccountWithLoadAwareness(ctx context.Context, gro
...
@@ -519,6 +519,9 @@ func (s *GatewayService) SelectAccountWithLoadAwareness(ctx context.Context, gro
if
!
s
.
isAccountAllowedForPlatform
(
acc
,
platform
,
useMixed
)
{
if
!
s
.
isAccountAllowedForPlatform
(
acc
,
platform
,
useMixed
)
{
continue
continue
}
}
if
!
acc
.
IsSchedulableForModel
(
requestedModel
)
{
continue
}
if
requestedModel
!=
""
&&
!
s
.
isModelSupportedByAccount
(
acc
,
requestedModel
)
{
if
requestedModel
!=
""
&&
!
s
.
isModelSupportedByAccount
(
acc
,
requestedModel
)
{
continue
continue
}
}
...
@@ -812,7 +815,7 @@ func (s *GatewayService) selectAccountForModelWithPlatform(ctx context.Context,
...
@@ -812,7 +815,7 @@ func (s *GatewayService) selectAccountForModelWithPlatform(ctx context.Context,
if
_
,
excluded
:=
excludedIDs
[
accountID
];
!
excluded
{
if
_
,
excluded
:=
excludedIDs
[
accountID
];
!
excluded
{
account
,
err
:=
s
.
accountRepo
.
GetByID
(
ctx
,
accountID
)
account
,
err
:=
s
.
accountRepo
.
GetByID
(
ctx
,
accountID
)
// 检查账号分组归属和平台匹配(确保粘性会话不会跨分组或跨平台)
// 检查账号分组归属和平台匹配(确保粘性会话不会跨分组或跨平台)
if
err
==
nil
&&
s
.
isAccountInGroup
(
account
,
groupID
)
&&
account
.
Platform
==
platform
&&
account
.
IsSchedulable
(
)
&&
(
requestedModel
==
""
||
s
.
isModelSupportedByAccount
(
account
,
requestedModel
))
{
if
err
==
nil
&&
s
.
isAccountInGroup
(
account
,
groupID
)
&&
account
.
Platform
==
platform
&&
account
.
IsSchedulable
ForModel
(
requestedModel
)
&&
(
requestedModel
==
""
||
s
.
isModelSupportedByAccount
(
account
,
requestedModel
))
{
if
err
:=
s
.
cache
.
RefreshSessionTTL
(
ctx
,
derefGroupID
(
groupID
),
sessionHash
,
stickySessionTTL
);
err
!=
nil
{
if
err
:=
s
.
cache
.
RefreshSessionTTL
(
ctx
,
derefGroupID
(
groupID
),
sessionHash
,
stickySessionTTL
);
err
!=
nil
{
log
.
Printf
(
"refresh session ttl failed: session=%s err=%v"
,
sessionHash
,
err
)
log
.
Printf
(
"refresh session ttl failed: session=%s err=%v"
,
sessionHash
,
err
)
}
}
...
@@ -844,6 +847,9 @@ func (s *GatewayService) selectAccountForModelWithPlatform(ctx context.Context,
...
@@ -844,6 +847,9 @@ func (s *GatewayService) selectAccountForModelWithPlatform(ctx context.Context,
if
_
,
excluded
:=
excludedIDs
[
acc
.
ID
];
excluded
{
if
_
,
excluded
:=
excludedIDs
[
acc
.
ID
];
excluded
{
continue
continue
}
}
if
!
acc
.
IsSchedulableForModel
(
requestedModel
)
{
continue
}
if
requestedModel
!=
""
&&
!
s
.
isModelSupportedByAccount
(
acc
,
requestedModel
)
{
if
requestedModel
!=
""
&&
!
s
.
isModelSupportedByAccount
(
acc
,
requestedModel
)
{
continue
continue
}
}
...
@@ -901,7 +907,7 @@ func (s *GatewayService) selectAccountWithMixedScheduling(ctx context.Context, g
...
@@ -901,7 +907,7 @@ func (s *GatewayService) selectAccountWithMixedScheduling(ctx context.Context, g
if
_
,
excluded
:=
excludedIDs
[
accountID
];
!
excluded
{
if
_
,
excluded
:=
excludedIDs
[
accountID
];
!
excluded
{
account
,
err
:=
s
.
accountRepo
.
GetByID
(
ctx
,
accountID
)
account
,
err
:=
s
.
accountRepo
.
GetByID
(
ctx
,
accountID
)
// 检查账号分组归属和有效性:原生平台直接匹配,antigravity 需要启用混合调度
// 检查账号分组归属和有效性:原生平台直接匹配,antigravity 需要启用混合调度
if
err
==
nil
&&
s
.
isAccountInGroup
(
account
,
groupID
)
&&
account
.
IsSchedulable
(
)
&&
(
requestedModel
==
""
||
s
.
isModelSupportedByAccount
(
account
,
requestedModel
))
{
if
err
==
nil
&&
s
.
isAccountInGroup
(
account
,
groupID
)
&&
account
.
IsSchedulable
ForModel
(
requestedModel
)
&&
(
requestedModel
==
""
||
s
.
isModelSupportedByAccount
(
account
,
requestedModel
))
{
if
account
.
Platform
==
nativePlatform
||
(
account
.
Platform
==
PlatformAntigravity
&&
account
.
IsMixedSchedulingEnabled
())
{
if
account
.
Platform
==
nativePlatform
||
(
account
.
Platform
==
PlatformAntigravity
&&
account
.
IsMixedSchedulingEnabled
())
{
if
err
:=
s
.
cache
.
RefreshSessionTTL
(
ctx
,
derefGroupID
(
groupID
),
sessionHash
,
stickySessionTTL
);
err
!=
nil
{
if
err
:=
s
.
cache
.
RefreshSessionTTL
(
ctx
,
derefGroupID
(
groupID
),
sessionHash
,
stickySessionTTL
);
err
!=
nil
{
log
.
Printf
(
"refresh session ttl failed: session=%s err=%v"
,
sessionHash
,
err
)
log
.
Printf
(
"refresh session ttl failed: session=%s err=%v"
,
sessionHash
,
err
)
...
@@ -936,6 +942,9 @@ func (s *GatewayService) selectAccountWithMixedScheduling(ctx context.Context, g
...
@@ -936,6 +942,9 @@ func (s *GatewayService) selectAccountWithMixedScheduling(ctx context.Context, g
if
acc
.
Platform
==
PlatformAntigravity
&&
!
acc
.
IsMixedSchedulingEnabled
()
{
if
acc
.
Platform
==
PlatformAntigravity
&&
!
acc
.
IsMixedSchedulingEnabled
()
{
continue
continue
}
}
if
!
acc
.
IsSchedulableForModel
(
requestedModel
)
{
continue
}
if
requestedModel
!=
""
&&
!
s
.
isModelSupportedByAccount
(
acc
,
requestedModel
)
{
if
requestedModel
!=
""
&&
!
s
.
isModelSupportedByAccount
(
acc
,
requestedModel
)
{
continue
continue
}
}
...
@@ -2247,6 +2256,7 @@ type RecordUsageInput struct {
...
@@ -2247,6 +2256,7 @@ type RecordUsageInput struct {
Account
*
Account
Account
*
Account
Subscription
*
UserSubscription
// 可选:订阅信息
Subscription
*
UserSubscription
// 可选:订阅信息
UserAgent
string
// 请求的 User-Agent
UserAgent
string
// 请求的 User-Agent
IPAddress
string
// 请求的客户端 IP 地址
}
}
// RecordUsage 记录使用量并扣费(或更新订阅用量)
// RecordUsage 记录使用量并扣费(或更新订阅用量)
...
@@ -2337,6 +2347,11 @@ func (s *GatewayService) RecordUsage(ctx context.Context, input *RecordUsageInpu
...
@@ -2337,6 +2347,11 @@ func (s *GatewayService) RecordUsage(ctx context.Context, input *RecordUsageInpu
usageLog
.
UserAgent
=
&
input
.
UserAgent
usageLog
.
UserAgent
=
&
input
.
UserAgent
}
}
// 添加 IPAddress
if
input
.
IPAddress
!=
""
{
usageLog
.
IPAddress
=
&
input
.
IPAddress
}
// 添加分组和订阅关联
// 添加分组和订阅关联
if
apiKey
.
GroupID
!=
nil
{
if
apiKey
.
GroupID
!=
nil
{
usageLog
.
GroupID
=
apiKey
.
GroupID
usageLog
.
GroupID
=
apiKey
.
GroupID
...
...
backend/internal/service/gemini_messages_compat_service.go
View file @
61f55674
...
@@ -114,7 +114,7 @@ func (s *GeminiMessagesCompatService) SelectAccountForModelWithExclusions(ctx co
...
@@ -114,7 +114,7 @@ func (s *GeminiMessagesCompatService) SelectAccountForModelWithExclusions(ctx co
if
_
,
excluded
:=
excludedIDs
[
accountID
];
!
excluded
{
if
_
,
excluded
:=
excludedIDs
[
accountID
];
!
excluded
{
account
,
err
:=
s
.
accountRepo
.
GetByID
(
ctx
,
accountID
)
account
,
err
:=
s
.
accountRepo
.
GetByID
(
ctx
,
accountID
)
// 检查账号是否有效:原生平台直接匹配,antigravity 需要启用混合调度
// 检查账号是否有效:原生平台直接匹配,antigravity 需要启用混合调度
if
err
==
nil
&&
account
.
IsSchedulable
(
)
&&
(
requestedModel
==
""
||
s
.
isModelSupportedByAccount
(
account
,
requestedModel
))
{
if
err
==
nil
&&
account
.
IsSchedulable
ForModel
(
requestedModel
)
&&
(
requestedModel
==
""
||
s
.
isModelSupportedByAccount
(
account
,
requestedModel
))
{
valid
:=
false
valid
:=
false
if
account
.
Platform
==
platform
{
if
account
.
Platform
==
platform
{
valid
=
true
valid
=
true
...
@@ -172,6 +172,9 @@ func (s *GeminiMessagesCompatService) SelectAccountForModelWithExclusions(ctx co
...
@@ -172,6 +172,9 @@ func (s *GeminiMessagesCompatService) SelectAccountForModelWithExclusions(ctx co
if
useMixedScheduling
&&
acc
.
Platform
==
PlatformAntigravity
&&
!
acc
.
IsMixedSchedulingEnabled
()
{
if
useMixedScheduling
&&
acc
.
Platform
==
PlatformAntigravity
&&
!
acc
.
IsMixedSchedulingEnabled
()
{
continue
continue
}
}
if
!
acc
.
IsSchedulableForModel
(
requestedModel
)
{
continue
}
if
requestedModel
!=
""
&&
!
s
.
isModelSupportedByAccount
(
acc
,
requestedModel
)
{
if
requestedModel
!=
""
&&
!
s
.
isModelSupportedByAccount
(
acc
,
requestedModel
)
{
continue
continue
}
}
...
...
backend/internal/service/gemini_multiplatform_test.go
View file @
61f55674
...
@@ -121,6 +121,9 @@ func (m *mockAccountRepoForGemini) ListSchedulableByGroupIDAndPlatforms(ctx cont
...
@@ -121,6 +121,9 @@ func (m *mockAccountRepoForGemini) ListSchedulableByGroupIDAndPlatforms(ctx cont
func
(
m
*
mockAccountRepoForGemini
)
SetRateLimited
(
ctx
context
.
Context
,
id
int64
,
resetAt
time
.
Time
)
error
{
func
(
m
*
mockAccountRepoForGemini
)
SetRateLimited
(
ctx
context
.
Context
,
id
int64
,
resetAt
time
.
Time
)
error
{
return
nil
return
nil
}
}
func
(
m
*
mockAccountRepoForGemini
)
SetAntigravityQuotaScopeLimit
(
ctx
context
.
Context
,
id
int64
,
scope
AntigravityQuotaScope
,
resetAt
time
.
Time
)
error
{
return
nil
}
func
(
m
*
mockAccountRepoForGemini
)
SetOverloaded
(
ctx
context
.
Context
,
id
int64
,
until
time
.
Time
)
error
{
func
(
m
*
mockAccountRepoForGemini
)
SetOverloaded
(
ctx
context
.
Context
,
id
int64
,
until
time
.
Time
)
error
{
return
nil
return
nil
}
}
...
@@ -131,6 +134,9 @@ func (m *mockAccountRepoForGemini) ClearTempUnschedulable(ctx context.Context, i
...
@@ -131,6 +134,9 @@ func (m *mockAccountRepoForGemini) ClearTempUnschedulable(ctx context.Context, i
return
nil
return
nil
}
}
func
(
m
*
mockAccountRepoForGemini
)
ClearRateLimit
(
ctx
context
.
Context
,
id
int64
)
error
{
return
nil
}
func
(
m
*
mockAccountRepoForGemini
)
ClearRateLimit
(
ctx
context
.
Context
,
id
int64
)
error
{
return
nil
}
func
(
m
*
mockAccountRepoForGemini
)
ClearAntigravityQuotaScopes
(
ctx
context
.
Context
,
id
int64
)
error
{
return
nil
}
func
(
m
*
mockAccountRepoForGemini
)
UpdateSessionWindow
(
ctx
context
.
Context
,
id
int64
,
start
,
end
*
time
.
Time
,
status
string
)
error
{
func
(
m
*
mockAccountRepoForGemini
)
UpdateSessionWindow
(
ctx
context
.
Context
,
id
int64
,
start
,
end
*
time
.
Time
,
status
string
)
error
{
return
nil
return
nil
}
}
...
@@ -166,7 +172,7 @@ func (m *mockGroupRepoForGemini) DeleteCascade(ctx context.Context, id int64) ([
...
@@ -166,7 +172,7 @@ func (m *mockGroupRepoForGemini) DeleteCascade(ctx context.Context, id int64) ([
func
(
m
*
mockGroupRepoForGemini
)
List
(
ctx
context
.
Context
,
params
pagination
.
PaginationParams
)
([]
Group
,
*
pagination
.
PaginationResult
,
error
)
{
func
(
m
*
mockGroupRepoForGemini
)
List
(
ctx
context
.
Context
,
params
pagination
.
PaginationParams
)
([]
Group
,
*
pagination
.
PaginationResult
,
error
)
{
return
nil
,
nil
,
nil
return
nil
,
nil
,
nil
}
}
func
(
m
*
mockGroupRepoForGemini
)
ListWithFilters
(
ctx
context
.
Context
,
params
pagination
.
PaginationParams
,
platform
,
status
string
,
isExclusive
*
bool
)
([]
Group
,
*
pagination
.
PaginationResult
,
error
)
{
func
(
m
*
mockGroupRepoForGemini
)
ListWithFilters
(
ctx
context
.
Context
,
params
pagination
.
PaginationParams
,
platform
,
status
,
search
string
,
isExclusive
*
bool
)
([]
Group
,
*
pagination
.
PaginationResult
,
error
)
{
return
nil
,
nil
,
nil
return
nil
,
nil
,
nil
}
}
func
(
m
*
mockGroupRepoForGemini
)
ListActive
(
ctx
context
.
Context
)
([]
Group
,
error
)
{
return
nil
,
nil
}
func
(
m
*
mockGroupRepoForGemini
)
ListActive
(
ctx
context
.
Context
)
([]
Group
,
error
)
{
return
nil
,
nil
}
...
...
backend/internal/service/group_service.go
View file @
61f55674
...
@@ -21,7 +21,7 @@ type GroupRepository interface {
...
@@ -21,7 +21,7 @@ type GroupRepository interface {
DeleteCascade
(
ctx
context
.
Context
,
id
int64
)
([]
int64
,
error
)
DeleteCascade
(
ctx
context
.
Context
,
id
int64
)
([]
int64
,
error
)
List
(
ctx
context
.
Context
,
params
pagination
.
PaginationParams
)
([]
Group
,
*
pagination
.
PaginationResult
,
error
)
List
(
ctx
context
.
Context
,
params
pagination
.
PaginationParams
)
([]
Group
,
*
pagination
.
PaginationResult
,
error
)
ListWithFilters
(
ctx
context
.
Context
,
params
pagination
.
PaginationParams
,
platform
,
status
string
,
isExclusive
*
bool
)
([]
Group
,
*
pagination
.
PaginationResult
,
error
)
ListWithFilters
(
ctx
context
.
Context
,
params
pagination
.
PaginationParams
,
platform
,
status
,
search
string
,
isExclusive
*
bool
)
([]
Group
,
*
pagination
.
PaginationResult
,
error
)
ListActive
(
ctx
context
.
Context
)
([]
Group
,
error
)
ListActive
(
ctx
context
.
Context
)
([]
Group
,
error
)
ListActiveByPlatform
(
ctx
context
.
Context
,
platform
string
)
([]
Group
,
error
)
ListActiveByPlatform
(
ctx
context
.
Context
,
platform
string
)
([]
Group
,
error
)
...
...
backend/internal/service/openai_gateway_service.go
View file @
61f55674
...
@@ -1197,6 +1197,7 @@ type OpenAIRecordUsageInput struct {
...
@@ -1197,6 +1197,7 @@ type OpenAIRecordUsageInput struct {
Account
*
Account
Account
*
Account
Subscription
*
UserSubscription
Subscription
*
UserSubscription
UserAgent
string
// 请求的 User-Agent
UserAgent
string
// 请求的 User-Agent
IPAddress
string
// 请求的客户端 IP 地址
}
}
// RecordUsage records usage and deducts balance
// RecordUsage records usage and deducts balance
...
@@ -1271,6 +1272,11 @@ func (s *OpenAIGatewayService) RecordUsage(ctx context.Context, input *OpenAIRec
...
@@ -1271,6 +1272,11 @@ func (s *OpenAIGatewayService) RecordUsage(ctx context.Context, input *OpenAIRec
usageLog
.
UserAgent
=
&
input
.
UserAgent
usageLog
.
UserAgent
=
&
input
.
UserAgent
}
}
// 添加 IPAddress
if
input
.
IPAddress
!=
""
{
usageLog
.
IPAddress
=
&
input
.
IPAddress
}
if
apiKey
.
GroupID
!=
nil
{
if
apiKey
.
GroupID
!=
nil
{
usageLog
.
GroupID
=
apiKey
.
GroupID
usageLog
.
GroupID
=
apiKey
.
GroupID
}
}
...
...
backend/internal/service/ratelimit_service.go
View file @
61f55674
...
@@ -345,7 +345,7 @@ func (s *RateLimitService) UpdateSessionWindow(ctx context.Context, account *Acc
...
@@ -345,7 +345,7 @@ func (s *RateLimitService) UpdateSessionWindow(ctx context.Context, account *Acc
// 如果状态为allowed且之前有限流,说明窗口已重置,清除限流状态
// 如果状态为allowed且之前有限流,说明窗口已重置,清除限流状态
if
status
==
"allowed"
&&
account
.
IsRateLimited
()
{
if
status
==
"allowed"
&&
account
.
IsRateLimited
()
{
if
err
:=
s
.
accountRepo
.
ClearRateLimit
(
ctx
,
account
.
ID
);
err
!=
nil
{
if
err
:=
s
.
ClearRateLimit
(
ctx
,
account
.
ID
);
err
!=
nil
{
log
.
Printf
(
"ClearRateLimit failed for account %d: %v"
,
account
.
ID
,
err
)
log
.
Printf
(
"ClearRateLimit failed for account %d: %v"
,
account
.
ID
,
err
)
}
}
}
}
...
@@ -353,7 +353,10 @@ func (s *RateLimitService) UpdateSessionWindow(ctx context.Context, account *Acc
...
@@ -353,7 +353,10 @@ func (s *RateLimitService) UpdateSessionWindow(ctx context.Context, account *Acc
// ClearRateLimit 清除账号的限流状态
// ClearRateLimit 清除账号的限流状态
func
(
s
*
RateLimitService
)
ClearRateLimit
(
ctx
context
.
Context
,
accountID
int64
)
error
{
func
(
s
*
RateLimitService
)
ClearRateLimit
(
ctx
context
.
Context
,
accountID
int64
)
error
{
return
s
.
accountRepo
.
ClearRateLimit
(
ctx
,
accountID
)
if
err
:=
s
.
accountRepo
.
ClearRateLimit
(
ctx
,
accountID
);
err
!=
nil
{
return
err
}
return
s
.
accountRepo
.
ClearAntigravityQuotaScopes
(
ctx
,
accountID
)
}
}
func
(
s
*
RateLimitService
)
ClearTempUnschedulable
(
ctx
context
.
Context
,
accountID
int64
)
error
{
func
(
s
*
RateLimitService
)
ClearTempUnschedulable
(
ctx
context
.
Context
,
accountID
int64
)
error
{
...
...
backend/internal/service/setting_service.go
View file @
61f55674
...
@@ -7,6 +7,7 @@ import (
...
@@ -7,6 +7,7 @@ import (
"errors"
"errors"
"fmt"
"fmt"
"strconv"
"strconv"
"strings"
"github.com/Wei-Shaw/sub2api/internal/config"
"github.com/Wei-Shaw/sub2api/internal/config"
infraerrors
"github.com/Wei-Shaw/sub2api/internal/pkg/errors"
infraerrors
"github.com/Wei-Shaw/sub2api/internal/pkg/errors"
...
@@ -64,6 +65,7 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings
...
@@ -64,6 +65,7 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings
SettingKeyAPIBaseURL
,
SettingKeyAPIBaseURL
,
SettingKeyContactInfo
,
SettingKeyContactInfo
,
SettingKeyDocURL
,
SettingKeyDocURL
,
SettingKeyLinuxDoConnectEnabled
,
}
}
settings
,
err
:=
s
.
settingRepo
.
GetMultiple
(
ctx
,
keys
)
settings
,
err
:=
s
.
settingRepo
.
GetMultiple
(
ctx
,
keys
)
...
@@ -71,6 +73,13 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings
...
@@ -71,6 +73,13 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings
return
nil
,
fmt
.
Errorf
(
"get public settings: %w"
,
err
)
return
nil
,
fmt
.
Errorf
(
"get public settings: %w"
,
err
)
}
}
linuxDoEnabled
:=
false
if
raw
,
ok
:=
settings
[
SettingKeyLinuxDoConnectEnabled
];
ok
{
linuxDoEnabled
=
raw
==
"true"
}
else
{
linuxDoEnabled
=
s
.
cfg
!=
nil
&&
s
.
cfg
.
LinuxDo
.
Enabled
}
return
&
PublicSettings
{
return
&
PublicSettings
{
RegistrationEnabled
:
settings
[
SettingKeyRegistrationEnabled
]
==
"true"
,
RegistrationEnabled
:
settings
[
SettingKeyRegistrationEnabled
]
==
"true"
,
EmailVerifyEnabled
:
settings
[
SettingKeyEmailVerifyEnabled
]
==
"true"
,
EmailVerifyEnabled
:
settings
[
SettingKeyEmailVerifyEnabled
]
==
"true"
,
...
@@ -82,6 +91,7 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings
...
@@ -82,6 +91,7 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings
APIBaseURL
:
settings
[
SettingKeyAPIBaseURL
],
APIBaseURL
:
settings
[
SettingKeyAPIBaseURL
],
ContactInfo
:
settings
[
SettingKeyContactInfo
],
ContactInfo
:
settings
[
SettingKeyContactInfo
],
DocURL
:
settings
[
SettingKeyDocURL
],
DocURL
:
settings
[
SettingKeyDocURL
],
LinuxDoOAuthEnabled
:
linuxDoEnabled
,
},
nil
},
nil
}
}
...
@@ -111,6 +121,14 @@ func (s *SettingService) UpdateSettings(ctx context.Context, settings *SystemSet
...
@@ -111,6 +121,14 @@ func (s *SettingService) UpdateSettings(ctx context.Context, settings *SystemSet
updates
[
SettingKeyTurnstileSecretKey
]
=
settings
.
TurnstileSecretKey
updates
[
SettingKeyTurnstileSecretKey
]
=
settings
.
TurnstileSecretKey
}
}
// LinuxDo Connect OAuth 登录(终端用户 SSO)
updates
[
SettingKeyLinuxDoConnectEnabled
]
=
strconv
.
FormatBool
(
settings
.
LinuxDoConnectEnabled
)
updates
[
SettingKeyLinuxDoConnectClientID
]
=
settings
.
LinuxDoConnectClientID
updates
[
SettingKeyLinuxDoConnectRedirectURL
]
=
settings
.
LinuxDoConnectRedirectURL
if
settings
.
LinuxDoConnectClientSecret
!=
""
{
updates
[
SettingKeyLinuxDoConnectClientSecret
]
=
settings
.
LinuxDoConnectClientSecret
}
// OEM设置
// OEM设置
updates
[
SettingKeySiteName
]
=
settings
.
SiteName
updates
[
SettingKeySiteName
]
=
settings
.
SiteName
updates
[
SettingKeySiteLogo
]
=
settings
.
SiteLogo
updates
[
SettingKeySiteLogo
]
=
settings
.
SiteLogo
...
@@ -271,6 +289,38 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin
...
@@ -271,6 +289,38 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin
result
.
SMTPPassword
=
settings
[
SettingKeySMTPPassword
]
result
.
SMTPPassword
=
settings
[
SettingKeySMTPPassword
]
result
.
TurnstileSecretKey
=
settings
[
SettingKeyTurnstileSecretKey
]
result
.
TurnstileSecretKey
=
settings
[
SettingKeyTurnstileSecretKey
]
// LinuxDo Connect 设置:
// - 兼容 config.yaml/env(避免老部署因为未迁移到数据库设置而被意外关闭)
// - 支持在后台“系统设置”中覆盖并持久化(存储于 DB)
linuxDoBase
:=
config
.
LinuxDoConnectConfig
{}
if
s
.
cfg
!=
nil
{
linuxDoBase
=
s
.
cfg
.
LinuxDo
}
if
raw
,
ok
:=
settings
[
SettingKeyLinuxDoConnectEnabled
];
ok
{
result
.
LinuxDoConnectEnabled
=
raw
==
"true"
}
else
{
result
.
LinuxDoConnectEnabled
=
linuxDoBase
.
Enabled
}
if
v
,
ok
:=
settings
[
SettingKeyLinuxDoConnectClientID
];
ok
&&
strings
.
TrimSpace
(
v
)
!=
""
{
result
.
LinuxDoConnectClientID
=
strings
.
TrimSpace
(
v
)
}
else
{
result
.
LinuxDoConnectClientID
=
linuxDoBase
.
ClientID
}
if
v
,
ok
:=
settings
[
SettingKeyLinuxDoConnectRedirectURL
];
ok
&&
strings
.
TrimSpace
(
v
)
!=
""
{
result
.
LinuxDoConnectRedirectURL
=
strings
.
TrimSpace
(
v
)
}
else
{
result
.
LinuxDoConnectRedirectURL
=
linuxDoBase
.
RedirectURL
}
result
.
LinuxDoConnectClientSecret
=
strings
.
TrimSpace
(
settings
[
SettingKeyLinuxDoConnectClientSecret
])
if
result
.
LinuxDoConnectClientSecret
==
""
{
result
.
LinuxDoConnectClientSecret
=
strings
.
TrimSpace
(
linuxDoBase
.
ClientSecret
)
}
result
.
LinuxDoConnectClientSecretConfigured
=
result
.
LinuxDoConnectClientSecret
!=
""
// Model fallback settings
// Model fallback settings
result
.
EnableModelFallback
=
settings
[
SettingKeyEnableModelFallback
]
==
"true"
result
.
EnableModelFallback
=
settings
[
SettingKeyEnableModelFallback
]
==
"true"
result
.
FallbackModelAnthropic
=
s
.
getStringOrDefault
(
settings
,
SettingKeyFallbackModelAnthropic
,
"claude-3-5-sonnet-20241022"
)
result
.
FallbackModelAnthropic
=
s
.
getStringOrDefault
(
settings
,
SettingKeyFallbackModelAnthropic
,
"claude-3-5-sonnet-20241022"
)
...
@@ -289,6 +339,99 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin
...
@@ -289,6 +339,99 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin
return
result
return
result
}
}
// GetLinuxDoConnectOAuthConfig 返回用于登录的“最终生效” LinuxDo Connect 配置。
//
// 优先级:
// - 若对应系统设置键存在,则覆盖 config.yaml/env 的值
// - 否则回退到 config.yaml/env 的值
func
(
s
*
SettingService
)
GetLinuxDoConnectOAuthConfig
(
ctx
context
.
Context
)
(
config
.
LinuxDoConnectConfig
,
error
)
{
if
s
==
nil
||
s
.
cfg
==
nil
{
return
config
.
LinuxDoConnectConfig
{},
infraerrors
.
ServiceUnavailable
(
"CONFIG_NOT_READY"
,
"config not loaded"
)
}
effective
:=
s
.
cfg
.
LinuxDo
keys
:=
[]
string
{
SettingKeyLinuxDoConnectEnabled
,
SettingKeyLinuxDoConnectClientID
,
SettingKeyLinuxDoConnectClientSecret
,
SettingKeyLinuxDoConnectRedirectURL
,
}
settings
,
err
:=
s
.
settingRepo
.
GetMultiple
(
ctx
,
keys
)
if
err
!=
nil
{
return
config
.
LinuxDoConnectConfig
{},
fmt
.
Errorf
(
"get linuxdo connect settings: %w"
,
err
)
}
if
raw
,
ok
:=
settings
[
SettingKeyLinuxDoConnectEnabled
];
ok
{
effective
.
Enabled
=
raw
==
"true"
}
if
v
,
ok
:=
settings
[
SettingKeyLinuxDoConnectClientID
];
ok
&&
strings
.
TrimSpace
(
v
)
!=
""
{
effective
.
ClientID
=
strings
.
TrimSpace
(
v
)
}
if
v
,
ok
:=
settings
[
SettingKeyLinuxDoConnectClientSecret
];
ok
&&
strings
.
TrimSpace
(
v
)
!=
""
{
effective
.
ClientSecret
=
strings
.
TrimSpace
(
v
)
}
if
v
,
ok
:=
settings
[
SettingKeyLinuxDoConnectRedirectURL
];
ok
&&
strings
.
TrimSpace
(
v
)
!=
""
{
effective
.
RedirectURL
=
strings
.
TrimSpace
(
v
)
}
if
!
effective
.
Enabled
{
return
config
.
LinuxDoConnectConfig
{},
infraerrors
.
NotFound
(
"OAUTH_DISABLED"
,
"oauth login is disabled"
)
}
// 基础健壮性校验(避免把用户重定向到一个必然失败或不安全的 OAuth 流程里)。
if
strings
.
TrimSpace
(
effective
.
ClientID
)
==
""
{
return
config
.
LinuxDoConnectConfig
{},
infraerrors
.
InternalServer
(
"OAUTH_CONFIG_INVALID"
,
"oauth client id not configured"
)
}
if
strings
.
TrimSpace
(
effective
.
AuthorizeURL
)
==
""
{
return
config
.
LinuxDoConnectConfig
{},
infraerrors
.
InternalServer
(
"OAUTH_CONFIG_INVALID"
,
"oauth authorize url not configured"
)
}
if
strings
.
TrimSpace
(
effective
.
TokenURL
)
==
""
{
return
config
.
LinuxDoConnectConfig
{},
infraerrors
.
InternalServer
(
"OAUTH_CONFIG_INVALID"
,
"oauth token url not configured"
)
}
if
strings
.
TrimSpace
(
effective
.
UserInfoURL
)
==
""
{
return
config
.
LinuxDoConnectConfig
{},
infraerrors
.
InternalServer
(
"OAUTH_CONFIG_INVALID"
,
"oauth userinfo url not configured"
)
}
if
strings
.
TrimSpace
(
effective
.
RedirectURL
)
==
""
{
return
config
.
LinuxDoConnectConfig
{},
infraerrors
.
InternalServer
(
"OAUTH_CONFIG_INVALID"
,
"oauth redirect url not configured"
)
}
if
strings
.
TrimSpace
(
effective
.
FrontendRedirectURL
)
==
""
{
return
config
.
LinuxDoConnectConfig
{},
infraerrors
.
InternalServer
(
"OAUTH_CONFIG_INVALID"
,
"oauth frontend redirect url not configured"
)
}
if
err
:=
config
.
ValidateAbsoluteHTTPURL
(
effective
.
AuthorizeURL
);
err
!=
nil
{
return
config
.
LinuxDoConnectConfig
{},
infraerrors
.
InternalServer
(
"OAUTH_CONFIG_INVALID"
,
"oauth authorize url invalid"
)
}
if
err
:=
config
.
ValidateAbsoluteHTTPURL
(
effective
.
TokenURL
);
err
!=
nil
{
return
config
.
LinuxDoConnectConfig
{},
infraerrors
.
InternalServer
(
"OAUTH_CONFIG_INVALID"
,
"oauth token url invalid"
)
}
if
err
:=
config
.
ValidateAbsoluteHTTPURL
(
effective
.
UserInfoURL
);
err
!=
nil
{
return
config
.
LinuxDoConnectConfig
{},
infraerrors
.
InternalServer
(
"OAUTH_CONFIG_INVALID"
,
"oauth userinfo url invalid"
)
}
if
err
:=
config
.
ValidateAbsoluteHTTPURL
(
effective
.
RedirectURL
);
err
!=
nil
{
return
config
.
LinuxDoConnectConfig
{},
infraerrors
.
InternalServer
(
"OAUTH_CONFIG_INVALID"
,
"oauth redirect url invalid"
)
}
if
err
:=
config
.
ValidateFrontendRedirectURL
(
effective
.
FrontendRedirectURL
);
err
!=
nil
{
return
config
.
LinuxDoConnectConfig
{},
infraerrors
.
InternalServer
(
"OAUTH_CONFIG_INVALID"
,
"oauth frontend redirect url invalid"
)
}
method
:=
strings
.
ToLower
(
strings
.
TrimSpace
(
effective
.
TokenAuthMethod
))
switch
method
{
case
""
,
"client_secret_post"
,
"client_secret_basic"
:
if
strings
.
TrimSpace
(
effective
.
ClientSecret
)
==
""
{
return
config
.
LinuxDoConnectConfig
{},
infraerrors
.
InternalServer
(
"OAUTH_CONFIG_INVALID"
,
"oauth client secret not configured"
)
}
case
"none"
:
if
!
effective
.
UsePKCE
{
return
config
.
LinuxDoConnectConfig
{},
infraerrors
.
InternalServer
(
"OAUTH_CONFIG_INVALID"
,
"oauth pkce must be enabled when token_auth_method=none"
)
}
default
:
return
config
.
LinuxDoConnectConfig
{},
infraerrors
.
InternalServer
(
"OAUTH_CONFIG_INVALID"
,
"oauth token_auth_method invalid"
)
}
return
effective
,
nil
}
// getStringOrDefault 获取字符串值或默认值
// getStringOrDefault 获取字符串值或默认值
func
(
s
*
SettingService
)
getStringOrDefault
(
settings
map
[
string
]
string
,
key
,
defaultValue
string
)
string
{
func
(
s
*
SettingService
)
getStringOrDefault
(
settings
map
[
string
]
string
,
key
,
defaultValue
string
)
string
{
if
value
,
ok
:=
settings
[
key
];
ok
&&
value
!=
""
{
if
value
,
ok
:=
settings
[
key
];
ok
&&
value
!=
""
{
...
...
backend/internal/service/settings_view.go
View file @
61f55674
...
@@ -18,6 +18,13 @@ type SystemSettings struct {
...
@@ -18,6 +18,13 @@ type SystemSettings struct {
TurnstileSecretKey
string
TurnstileSecretKey
string
TurnstileSecretKeyConfigured
bool
TurnstileSecretKeyConfigured
bool
// LinuxDo Connect OAuth 登录(终端用户 SSO)
LinuxDoConnectEnabled
bool
LinuxDoConnectClientID
string
LinuxDoConnectClientSecret
string
LinuxDoConnectClientSecretConfigured
bool
LinuxDoConnectRedirectURL
string
SiteName
string
SiteName
string
SiteLogo
string
SiteLogo
string
SiteSubtitle
string
SiteSubtitle
string
...
@@ -51,5 +58,6 @@ type PublicSettings struct {
...
@@ -51,5 +58,6 @@ type PublicSettings struct {
APIBaseURL
string
APIBaseURL
string
ContactInfo
string
ContactInfo
string
DocURL
string
DocURL
string
LinuxDoOAuthEnabled
bool
Version
string
Version
string
}
}
backend/internal/service/usage_log.go
View file @
61f55674
...
@@ -39,6 +39,7 @@ type UsageLog struct {
...
@@ -39,6 +39,7 @@ type UsageLog struct {
DurationMs
*
int
DurationMs
*
int
FirstTokenMs
*
int
FirstTokenMs
*
int
UserAgent
*
string
UserAgent
*
string
IPAddress
*
string
// 图片生成字段
// 图片生成字段
ImageCount
int
ImageCount
int
...
...
backend/migrations/031_add_ip_address.sql
0 → 100644
View file @
61f55674
-- Add IP address field to usage_logs table for request tracking (admin-only visibility)
ALTER
TABLE
usage_logs
ADD
COLUMN
IF
NOT
EXISTS
ip_address
VARCHAR
(
45
);
-- Create index for IP address queries
CREATE
INDEX
IF
NOT
EXISTS
idx_usage_logs_ip_address
ON
usage_logs
(
ip_address
);
backend/migrations/032_add_api_key_ip_restriction.sql
0 → 100644
View file @
61f55674
-- Add IP restriction fields to api_keys table
-- ip_whitelist: JSON array of allowed IPs/CIDRs (if set, only these IPs can use the key)
-- ip_blacklist: JSON array of blocked IPs/CIDRs (these IPs are always blocked)
ALTER
TABLE
api_keys
ADD
COLUMN
IF
NOT
EXISTS
ip_whitelist
JSONB
DEFAULT
NULL
;
ALTER
TABLE
api_keys
ADD
COLUMN
IF
NOT
EXISTS
ip_blacklist
JSONB
DEFAULT
NULL
;
COMMENT
ON
COLUMN
api_keys
.
ip_whitelist
IS
'JSON array of allowed IPs/CIDRs, e.g. ["192.168.1.100", "10.0.0.0/8"]'
;
COMMENT
ON
COLUMN
api_keys
.
ip_blacklist
IS
'JSON array of blocked IPs/CIDRs, e.g. ["1.2.3.4", "5.6.0.0/16"]'
;
backend/repository.test
0 → 100755
View file @
61f55674
File added
config.yaml
View file @
61f55674
...
@@ -154,9 +154,9 @@ gateway:
...
@@ -154,9 +154,9 @@ gateway:
# Stream keepalive interval (seconds), 0=disable
# Stream keepalive interval (seconds), 0=disable
# 流式 keepalive 间隔(秒),0=禁用
# 流式 keepalive 间隔(秒),0=禁用
stream_keepalive_interval
:
10
stream_keepalive_interval
:
10
# SSE max line size in bytes (default:
1
0MB)
# SSE max line size in bytes (default:
4
0MB)
# SSE 单行最大字节数(默认
1
0MB)
# SSE 单行最大字节数(默认
4
0MB)
max_line_size
:
1048576
0
max_line_size
:
4194304
0
# Log upstream error response body summary (safe/truncated; does not log request content)
# Log upstream error response body summary (safe/truncated; does not log request content)
# 记录上游错误响应体摘要(安全/截断;不记录请求内容)
# 记录上游错误响应体摘要(安全/截断;不记录请求内容)
log_upstream_error_body
:
false
log_upstream_error_body
:
false
...
...
deploy/config.example.yaml
View file @
61f55674
...
@@ -154,9 +154,9 @@ gateway:
...
@@ -154,9 +154,9 @@ gateway:
# Stream keepalive interval (seconds), 0=disable
# Stream keepalive interval (seconds), 0=disable
# 流式 keepalive 间隔(秒),0=禁用
# 流式 keepalive 间隔(秒),0=禁用
stream_keepalive_interval
:
10
stream_keepalive_interval
:
10
# SSE max line size in bytes (default:
1
0MB)
# SSE max line size in bytes (default:
4
0MB)
# SSE 单行最大字节数(默认
1
0MB)
# SSE 单行最大字节数(默认
4
0MB)
max_line_size
:
1048576
0
max_line_size
:
4194304
0
# Log upstream error response body summary (safe/truncated; does not log request content)
# Log upstream error response body summary (safe/truncated; does not log request content)
# 记录上游错误响应体摘要(安全/截断;不记录请求内容)
# 记录上游错误响应体摘要(安全/截断;不记录请求内容)
log_upstream_error_body
:
false
log_upstream_error_body
:
false
...
@@ -234,6 +234,31 @@ jwt:
...
@@ -234,6 +234,31 @@ jwt:
# 令牌过期时间(小时,最大 24)
# 令牌过期时间(小时,最大 24)
expire_hour
:
24
expire_hour
:
24
# =============================================================================
# LinuxDo Connect OAuth Login (SSO)
# LinuxDo Connect OAuth 登录(用于 Sub2API 用户登录)
# =============================================================================
linuxdo_connect
:
enabled
:
false
client_id
:
"
"
client_secret
:
"
"
authorize_url
:
"
https://connect.linux.do/oauth2/authorize"
token_url
:
"
https://connect.linux.do/oauth2/token"
userinfo_url
:
"
https://connect.linux.do/api/user"
scopes
:
"
user"
# 示例: "https://your-domain.com/api/v1/auth/oauth/linuxdo/callback"
redirect_url
:
"
"
# 安全提示:
# - 建议使用同源相对路径(以 / 开头),避免把 token 重定向到意外的第三方域名
# - 该地址不应包含 #fragment(本实现使用 URL fragment 传递 access_token)
frontend_redirect_url
:
"
/auth/linuxdo/callback"
token_auth_method
:
"
client_secret_post"
# client_secret_post | client_secret_basic | none
# 注意:当 token_auth_method=none(public client)时,必须启用 PKCE
use_pkce
:
false
userinfo_email_path
:
"
"
userinfo_id_path
:
"
"
userinfo_username_path
:
"
"
# =============================================================================
# =============================================================================
# Default Settings
# Default Settings
# 默认设置
# 默认设置
...
...
deploy/docker-compose.standalone.yml
0 → 100644
View file @
61f55674
# =============================================================================
# Sub2API Docker Compose - Standalone Configuration
# =============================================================================
# This configuration runs only the Sub2API application.
# PostgreSQL and Redis must be provided externally.
#
# Usage:
# 1. Copy .env.example to .env and configure database/redis connection
# 2. docker-compose -f docker-compose.standalone.yml up -d
# 3. Access: http://localhost:8080
# =============================================================================
services
:
sub2api
:
image
:
weishaw/sub2api:latest
container_name
:
sub2api
restart
:
unless-stopped
ulimits
:
nofile
:
soft
:
100000
hard
:
100000
ports
:
-
"
${BIND_HOST:-0.0.0.0}:${SERVER_PORT:-8080}:8080"
volumes
:
-
sub2api_data:/app/data
extra_hosts
:
-
"
host.docker.internal:host-gateway"
environment
:
# =======================================================================
# Auto Setup
# =======================================================================
-
AUTO_SETUP=true
# =======================================================================
# Server Configuration
# =======================================================================
-
SERVER_HOST=0.0.0.0
-
SERVER_PORT=8080
-
SERVER_MODE=${SERVER_MODE:-release}
-
RUN_MODE=${RUN_MODE:-standard}
# =======================================================================
# Database Configuration (PostgreSQL) - Required
# =======================================================================
-
DATABASE_HOST=${DATABASE_HOST:?DATABASE_HOST is required}
-
DATABASE_PORT=${DATABASE_PORT:-5432}
-
DATABASE_USER=${DATABASE_USER:-sub2api}
-
DATABASE_PASSWORD=${DATABASE_PASSWORD:?DATABASE_PASSWORD is required}
-
DATABASE_DBNAME=${DATABASE_DBNAME:-sub2api}
-
DATABASE_SSLMODE=${DATABASE_SSLMODE:-disable}
# =======================================================================
# Redis Configuration - Required
# =======================================================================
-
REDIS_HOST=${REDIS_HOST:?REDIS_HOST is required}
-
REDIS_PORT=${REDIS_PORT:-6379}
-
REDIS_PASSWORD=${REDIS_PASSWORD:-}
-
REDIS_DB=${REDIS_DB:-0}
# =======================================================================
# Admin Account (auto-created on first run)
# =======================================================================
-
ADMIN_EMAIL=${ADMIN_EMAIL:-admin@sub2api.local}
-
ADMIN_PASSWORD=${ADMIN_PASSWORD:-}
# =======================================================================
# JWT Configuration
# =======================================================================
-
JWT_SECRET=${JWT_SECRET:-}
-
JWT_EXPIRE_HOUR=${JWT_EXPIRE_HOUR:-24}
# =======================================================================
# Timezone Configuration
# =======================================================================
-
TZ=${TZ:-Asia/Shanghai}
# =======================================================================
# Gemini OAuth Configuration (optional)
# =======================================================================
-
GEMINI_OAUTH_CLIENT_ID=${GEMINI_OAUTH_CLIENT_ID:-}
-
GEMINI_OAUTH_CLIENT_SECRET=${GEMINI_OAUTH_CLIENT_SECRET:-}
-
GEMINI_OAUTH_SCOPES=${GEMINI_OAUTH_SCOPES:-}
-
GEMINI_QUOTA_POLICY=${GEMINI_QUOTA_POLICY:-}
healthcheck
:
test
:
[
"
CMD"
,
"
curl"
,
"
-f"
,
"
http://localhost:8080/health"
]
interval
:
30s
timeout
:
10s
retries
:
3
start_period
:
30s
volumes
:
sub2api_data
:
driver
:
local
deploy/docker-compose.yml
View file @
61f55674
...
@@ -173,11 +173,12 @@ services:
...
@@ -173,11 +173,12 @@ services:
volumes
:
volumes
:
-
redis_data:/data
-
redis_data:/data
command
:
>
command
:
>
redis-server
sh -c '
--save 60 1
redis-server
--appendonly yes
--save 60 1
--appendfsync everysec
--appendonly yes
${REDIS_PASSWORD:+--requirepass ${REDIS_PASSWORD}}
--appendfsync everysec
${REDIS_PASSWORD:+--requirepass "$REDIS_PASSWORD"}'
environment
:
environment
:
-
TZ=${TZ:-Asia/Shanghai}
-
TZ=${TZ:-Asia/Shanghai}
# REDISCLI_AUTH is used by redis-cli for authentication (safer than -a flag)
# REDISCLI_AUTH is used by redis-cli for authentication (safer than -a flag)
...
...
frontend/src/api/admin/groups.ts
View file @
61f55674
...
@@ -16,7 +16,7 @@ import type {
...
@@ -16,7 +16,7 @@ import type {
* List all groups with pagination
* List all groups with pagination
* @param page - Page number (default: 1)
* @param page - Page number (default: 1)
* @param pageSize - Items per page (default: 20)
* @param pageSize - Items per page (default: 20)
* @param filters - Optional filters (platform, status, is_exclusive)
* @param filters - Optional filters (platform, status, is_exclusive
, search
)
* @returns Paginated list of groups
* @returns Paginated list of groups
*/
*/
export
async
function
list
(
export
async
function
list
(
...
@@ -26,6 +26,7 @@ export async function list(
...
@@ -26,6 +26,7 @@ export async function list(
platform
?:
GroupPlatform
platform
?:
GroupPlatform
status
?:
'
active
'
|
'
inactive
'
status
?:
'
active
'
|
'
inactive
'
is_exclusive
?:
boolean
is_exclusive
?:
boolean
search
?:
string
},
},
options
?:
{
options
?:
{
signal
?:
AbortSignal
signal
?:
AbortSignal
...
...
frontend/src/api/admin/settings.ts
View file @
61f55674
...
@@ -34,6 +34,11 @@ export interface SystemSettings {
...
@@ -34,6 +34,11 @@ export interface SystemSettings {
turnstile_enabled
:
boolean
turnstile_enabled
:
boolean
turnstile_site_key
:
string
turnstile_site_key
:
string
turnstile_secret_key_configured
:
boolean
turnstile_secret_key_configured
:
boolean
// LinuxDo Connect OAuth 登录(终端用户 SSO)
linuxdo_connect_enabled
:
boolean
linuxdo_connect_client_id
:
string
linuxdo_connect_client_secret_configured
:
boolean
linuxdo_connect_redirect_url
:
string
// Identity patch configuration (Claude -> Gemini)
// Identity patch configuration (Claude -> Gemini)
enable_identity_patch
:
boolean
enable_identity_patch
:
boolean
identity_patch_prompt
:
string
identity_patch_prompt
:
string
...
@@ -60,6 +65,10 @@ export interface UpdateSettingsRequest {
...
@@ -60,6 +65,10 @@ export interface UpdateSettingsRequest {
turnstile_enabled
?:
boolean
turnstile_enabled
?:
boolean
turnstile_site_key
?:
string
turnstile_site_key
?:
string
turnstile_secret_key
?:
string
turnstile_secret_key
?:
string
linuxdo_connect_enabled
?:
boolean
linuxdo_connect_client_id
?:
string
linuxdo_connect_client_secret
?:
string
linuxdo_connect_redirect_url
?:
string
enable_identity_patch
?:
boolean
enable_identity_patch
?:
boolean
identity_patch_prompt
?:
string
identity_patch_prompt
?:
string
}
}
...
...
frontend/src/api/admin/usage.ts
View file @
61f55674
...
@@ -64,7 +64,6 @@ export async function getStats(params: {
...
@@ -64,7 +64,6 @@ export async function getStats(params: {
group_id
?:
number
group_id
?:
number
model
?:
string
model
?:
string
stream
?:
boolean
stream
?:
boolean
billing_type
?:
number
period
?:
string
period
?:
string
start_date
?:
string
start_date
?:
string
end_date
?:
string
end_date
?:
string
...
...
frontend/src/api/keys.ts
View file @
61f55674
...
@@ -42,12 +42,16 @@ export async function getById(id: number): Promise<ApiKey> {
...
@@ -42,12 +42,16 @@ export async function getById(id: number): Promise<ApiKey> {
* @param name - Key name
* @param name - Key name
* @param groupId - Optional group ID
* @param groupId - Optional group ID
* @param customKey - Optional custom key value
* @param customKey - Optional custom key value
* @param ipWhitelist - Optional IP whitelist
* @param ipBlacklist - Optional IP blacklist
* @returns Created API key
* @returns Created API key
*/
*/
export
async
function
create
(
export
async
function
create
(
name
:
string
,
name
:
string
,
groupId
?:
number
|
null
,
groupId
?:
number
|
null
,
customKey
?:
string
customKey
?:
string
,
ipWhitelist
?:
string
[],
ipBlacklist
?:
string
[]
):
Promise
<
ApiKey
>
{
):
Promise
<
ApiKey
>
{
const
payload
:
CreateApiKeyRequest
=
{
name
}
const
payload
:
CreateApiKeyRequest
=
{
name
}
if
(
groupId
!==
undefined
)
{
if
(
groupId
!==
undefined
)
{
...
@@ -56,6 +60,12 @@ export async function create(
...
@@ -56,6 +60,12 @@ export async function create(
if
(
customKey
)
{
if
(
customKey
)
{
payload
.
custom_key
=
customKey
payload
.
custom_key
=
customKey
}
}
if
(
ipWhitelist
&&
ipWhitelist
.
length
>
0
)
{
payload
.
ip_whitelist
=
ipWhitelist
}
if
(
ipBlacklist
&&
ipBlacklist
.
length
>
0
)
{
payload
.
ip_blacklist
=
ipBlacklist
}
const
{
data
}
=
await
apiClient
.
post
<
ApiKey
>
(
'
/keys
'
,
payload
)
const
{
data
}
=
await
apiClient
.
post
<
ApiKey
>
(
'
/keys
'
,
payload
)
return
data
return
data
...
...
Prev
1
2
3
4
5
6
Next
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment