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
0fffba54
Unverified
Commit
0fffba54
authored
Mar 01, 2026
by
Wesley Liddick
Committed by
GitHub
Mar 01, 2026
Browse files
Merge pull request #692 from DaydreamCoding/feat/CC_UA
feat(gateway): 添加 Claude Code 客户端最低版本检查功能
parents
f7fa71bc
b2141a96
Changes
16
Show whitespace changes
Inline
Side-by-side
backend/cmd/server/wire_gen.go
View file @
0fffba54
...
...
@@ -196,7 +196,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
adminAPIKeyHandler
:=
admin
.
NewAdminAPIKeyHandler
(
adminService
)
adminHandlers
:=
handler
.
ProvideAdminHandlers
(
dashboardHandler
,
adminUserHandler
,
groupHandler
,
accountHandler
,
adminAnnouncementHandler
,
dataManagementHandler
,
oAuthHandler
,
openAIOAuthHandler
,
geminiOAuthHandler
,
antigravityOAuthHandler
,
proxyHandler
,
adminRedeemHandler
,
promoHandler
,
settingHandler
,
opsHandler
,
systemHandler
,
adminSubscriptionHandler
,
adminUsageHandler
,
userAttributeHandler
,
errorPassthroughHandler
,
adminAPIKeyHandler
)
usageRecordWorkerPool
:=
service
.
NewUsageRecordWorkerPool
(
configConfig
)
gatewayHandler
:=
handler
.
NewGatewayHandler
(
gatewayService
,
geminiMessagesCompatService
,
antigravityGatewayService
,
userService
,
concurrencyService
,
billingCacheService
,
usageService
,
apiKeyService
,
usageRecordWorkerPool
,
errorPassthroughService
,
configConfig
)
gatewayHandler
:=
handler
.
NewGatewayHandler
(
gatewayService
,
geminiMessagesCompatService
,
antigravityGatewayService
,
userService
,
concurrencyService
,
billingCacheService
,
usageService
,
apiKeyService
,
usageRecordWorkerPool
,
errorPassthroughService
,
configConfig
,
settingService
)
openAIGatewayHandler
:=
handler
.
NewOpenAIGatewayHandler
(
openAIGatewayService
,
concurrencyService
,
billingCacheService
,
apiKeyService
,
usageRecordWorkerPool
,
errorPassthroughService
,
configConfig
)
soraSDKClient
:=
service
.
ProvideSoraSDKClient
(
configConfig
,
httpUpstream
,
openAITokenProvider
,
accountRepository
,
soraAccountRepository
)
soraMediaStorage
:=
service
.
ProvideSoraMediaStorage
(
configConfig
)
...
...
backend/internal/handler/admin/setting_handler.go
View file @
0fffba54
...
...
@@ -3,6 +3,8 @@ package admin
import
(
"fmt"
"log"
"net/http"
"regexp"
"strings"
"time"
...
...
@@ -15,6 +17,9 @@ import (
"github.com/gin-gonic/gin"
)
// semverPattern 预编译 semver 格式校验正则
var
semverPattern
=
regexp
.
MustCompile
(
`^\d+\.\d+\.\d+$`
)
// SettingHandler 系统设置处理器
type
SettingHandler
struct
{
settingService
*
service
.
SettingService
...
...
@@ -93,6 +98,7 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
OpsRealtimeMonitoringEnabled
:
settings
.
OpsRealtimeMonitoringEnabled
,
OpsQueryModeDefault
:
settings
.
OpsQueryModeDefault
,
OpsMetricsIntervalSeconds
:
settings
.
OpsMetricsIntervalSeconds
,
MinClaudeCodeVersion
:
settings
.
MinClaudeCodeVersion
,
})
}
...
...
@@ -159,6 +165,8 @@ type UpdateSettingsRequest struct {
OpsRealtimeMonitoringEnabled
*
bool
`json:"ops_realtime_monitoring_enabled"`
OpsQueryModeDefault
*
string
`json:"ops_query_mode_default"`
OpsMetricsIntervalSeconds
*
int
`json:"ops_metrics_interval_seconds"`
MinClaudeCodeVersion
string
`json:"min_claude_code_version"`
}
// UpdateSettings 更新系统设置
...
...
@@ -293,6 +301,14 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
req
.
OpsMetricsIntervalSeconds
=
&
v
}
// 验证最低版本号格式(空字符串=禁用,或合法 semver)
if
req
.
MinClaudeCodeVersion
!=
""
{
if
!
semverPattern
.
MatchString
(
req
.
MinClaudeCodeVersion
)
{
response
.
Error
(
c
,
http
.
StatusBadRequest
,
"min_claude_code_version must be empty or a valid semver (e.g. 2.1.63)"
)
return
}
}
settings
:=
&
service
.
SystemSettings
{
RegistrationEnabled
:
req
.
RegistrationEnabled
,
EmailVerifyEnabled
:
req
.
EmailVerifyEnabled
,
...
...
@@ -334,6 +350,7 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
FallbackModelAntigravity
:
req
.
FallbackModelAntigravity
,
EnableIdentityPatch
:
req
.
EnableIdentityPatch
,
IdentityPatchPrompt
:
req
.
IdentityPatchPrompt
,
MinClaudeCodeVersion
:
req
.
MinClaudeCodeVersion
,
OpsMonitoringEnabled
:
func
()
bool
{
if
req
.
OpsMonitoringEnabled
!=
nil
{
return
*
req
.
OpsMonitoringEnabled
...
...
@@ -420,6 +437,7 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
OpsRealtimeMonitoringEnabled
:
updatedSettings
.
OpsRealtimeMonitoringEnabled
,
OpsQueryModeDefault
:
updatedSettings
.
OpsQueryModeDefault
,
OpsMetricsIntervalSeconds
:
updatedSettings
.
OpsMetricsIntervalSeconds
,
MinClaudeCodeVersion
:
updatedSettings
.
MinClaudeCodeVersion
,
})
}
...
...
@@ -562,6 +580,9 @@ func diffSettings(before *service.SystemSettings, after *service.SystemSettings,
if
before
.
OpsMetricsIntervalSeconds
!=
after
.
OpsMetricsIntervalSeconds
{
changed
=
append
(
changed
,
"ops_metrics_interval_seconds"
)
}
if
before
.
MinClaudeCodeVersion
!=
after
.
MinClaudeCodeVersion
{
changed
=
append
(
changed
,
"min_claude_code_version"
)
}
return
changed
}
...
...
backend/internal/handler/dto/settings.go
View file @
0fffba54
...
...
@@ -58,6 +58,8 @@ type SystemSettings struct {
OpsRealtimeMonitoringEnabled
bool
`json:"ops_realtime_monitoring_enabled"`
OpsQueryModeDefault
string
`json:"ops_query_mode_default"`
OpsMetricsIntervalSeconds
int
`json:"ops_metrics_interval_seconds"`
MinClaudeCodeVersion
string
`json:"min_claude_code_version"`
}
type
PublicSettings
struct
{
...
...
backend/internal/handler/gateway_handler.go
View file @
0fffba54
...
...
@@ -48,6 +48,7 @@ type GatewayHandler struct {
maxAccountSwitches
int
maxAccountSwitchesGemini
int
cfg
*
config
.
Config
settingService
*
service
.
SettingService
}
// NewGatewayHandler creates a new GatewayHandler
...
...
@@ -63,6 +64,7 @@ func NewGatewayHandler(
usageRecordWorkerPool
*
service
.
UsageRecordWorkerPool
,
errorPassthroughService
*
service
.
ErrorPassthroughService
,
cfg
*
config
.
Config
,
settingService
*
service
.
SettingService
,
)
*
GatewayHandler
{
pingInterval
:=
time
.
Duration
(
0
)
maxAccountSwitches
:=
10
...
...
@@ -90,6 +92,7 @@ func NewGatewayHandler(
maxAccountSwitches
:
maxAccountSwitches
,
maxAccountSwitchesGemini
:
maxAccountSwitchesGemini
,
cfg
:
cfg
,
settingService
:
settingService
,
}
}
...
...
@@ -155,6 +158,11 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
SetClaudeCodeClientContext
(
c
,
body
,
parsedReq
)
isClaudeCodeClient
:=
service
.
IsClaudeCodeClient
(
c
.
Request
.
Context
())
// 版本检查:仅对 Claude Code 客户端,拒绝低于最低版本的请求
if
!
h
.
checkClaudeCodeVersion
(
c
)
{
return
}
// 在请求上下文中记录 thinking 状态,供 Antigravity 最终模型 key 推导/模型维度限流使用
c
.
Request
=
c
.
Request
.
WithContext
(
service
.
WithThinkingEnabled
(
c
.
Request
.
Context
(),
parsedReq
.
ThinkingEnabled
,
h
.
metadataBridgeEnabled
()))
...
...
@@ -1003,6 +1011,41 @@ func (h *GatewayHandler) ensureForwardErrorResponse(c *gin.Context, streamStarte
return
true
}
// checkClaudeCodeVersion 检查 Claude Code 客户端版本是否满足最低要求
// 仅对已识别的 Claude Code 客户端执行,count_tokens 路径除外
func
(
h
*
GatewayHandler
)
checkClaudeCodeVersion
(
c
*
gin
.
Context
)
bool
{
ctx
:=
c
.
Request
.
Context
()
if
!
service
.
IsClaudeCodeClient
(
ctx
)
{
return
true
}
// 排除 count_tokens 子路径
if
strings
.
HasSuffix
(
c
.
Request
.
URL
.
Path
,
"/count_tokens"
)
{
return
true
}
minVersion
:=
h
.
settingService
.
GetMinClaudeCodeVersion
(
ctx
)
if
minVersion
==
""
{
return
true
// 未设置,不检查
}
clientVersion
:=
service
.
GetClaudeCodeVersion
(
ctx
)
if
clientVersion
==
""
{
h
.
errorResponse
(
c
,
http
.
StatusBadRequest
,
"invalid_request_error"
,
"Unable to determine Claude Code version. Please update Claude Code: npm update -g @anthropic-ai/claude-code"
)
return
false
}
if
service
.
CompareVersions
(
clientVersion
,
minVersion
)
<
0
{
h
.
errorResponse
(
c
,
http
.
StatusBadRequest
,
"invalid_request_error"
,
fmt
.
Sprintf
(
"Your Claude Code version (%s) is below the minimum required version (%s). Please update: npm update -g @anthropic-ai/claude-code"
,
clientVersion
,
minVersion
))
return
false
}
return
true
}
// errorResponse 返回Claude API格式的错误响应
func
(
h
*
GatewayHandler
)
errorResponse
(
c
*
gin
.
Context
,
status
int
,
errType
,
message
string
)
{
c
.
JSON
(
status
,
gin
.
H
{
...
...
backend/internal/handler/gateway_helper.go
View file @
0fffba54
...
...
@@ -29,8 +29,10 @@ func SetClaudeCodeClientContext(c *gin.Context, body []byte, parsedReq *service.
if
parsedReq
!=
nil
{
c
.
Set
(
claudeCodeParsedRequestContextKey
,
parsedReq
)
}
ua
:=
c
.
GetHeader
(
"User-Agent"
)
// Fast path:非 Claude CLI UA 直接判定 false,避免热路径二次 JSON 反序列化。
if
!
claudeCodeValidator
.
ValidateUserAgent
(
c
.
GetHeader
(
"User-Agent"
)
)
{
if
!
claudeCodeValidator
.
ValidateUserAgent
(
ua
)
{
ctx
:=
service
.
SetClaudeCodeClient
(
c
.
Request
.
Context
(),
false
)
c
.
Request
=
c
.
Request
.
WithContext
(
ctx
)
return
...
...
@@ -54,6 +56,14 @@ func SetClaudeCodeClientContext(c *gin.Context, body []byte, parsedReq *service.
// 更新 request context
ctx
:=
service
.
SetClaudeCodeClient
(
c
.
Request
.
Context
(),
isClaudeCode
)
// 仅在确认为 Claude Code 客户端时提取版本号写入 context
if
isClaudeCode
{
if
version
:=
claudeCodeValidator
.
ExtractVersion
(
ua
);
version
!=
""
{
ctx
=
service
.
SetClaudeCodeVersion
(
ctx
,
version
)
}
}
c
.
Request
=
c
.
Request
.
WithContext
(
ctx
)
}
...
...
backend/internal/pkg/ctxkey/ctxkey.go
View file @
0fffba54
...
...
@@ -52,4 +52,7 @@ const (
// PrefetchedStickyGroupID 标识上游预取 sticky session 时所使用的分组 ID。
// Service 层仅在分组匹配时复用 PrefetchedStickyAccountID,避免分组切换重试误用旧 sticky。
PrefetchedStickyGroupID
Key
=
"ctx_prefetched_sticky_group_id"
// ClaudeCodeVersion stores the extracted Claude Code version from User-Agent (e.g. "2.1.22")
ClaudeCodeVersion
Key
=
"ctx_claude_code_version"
)
backend/internal/server/api_contract_test.go
View file @
0fffba54
...
...
@@ -511,7 +511,8 @@ func TestAPIContracts(t *testing.T) {
"home_content": "",
"hide_ccs_import_button": false,
"purchase_subscription_enabled": false,
"purchase_subscription_url": ""
"purchase_subscription_url": "",
"min_claude_code_version": ""
}
}`
,
},
...
...
backend/internal/service/claude_code_validator.go
View file @
0fffba54
...
...
@@ -4,6 +4,7 @@ import (
"context"
"net/http"
"regexp"
"strconv"
"strings"
"github.com/Wei-Shaw/sub2api/internal/pkg/ctxkey"
...
...
@@ -17,6 +18,9 @@ var (
// User-Agent 匹配: claude-cli/x.x.x (仅支持官方 CLI,大小写不敏感)
claudeCodeUAPattern
=
regexp
.
MustCompile
(
`(?i)^claude-cli/\d+\.\d+\.\d+`
)
// 带捕获组的版本提取正则
claudeCodeUAVersionPattern
=
regexp
.
MustCompile
(
`(?i)^claude-cli/(\d+\.\d+\.\d+)`
)
// metadata.user_id 格式: user_{64位hex}_account__session_{uuid}
userIDPattern
=
regexp
.
MustCompile
(
`^user_[a-fA-F0-9]{64}_account__session_[\w-]+$`
)
...
...
@@ -270,3 +274,55 @@ func IsClaudeCodeClient(ctx context.Context) bool {
func
SetClaudeCodeClient
(
ctx
context
.
Context
,
isClaudeCode
bool
)
context
.
Context
{
return
context
.
WithValue
(
ctx
,
ctxkey
.
IsClaudeCodeClient
,
isClaudeCode
)
}
// ExtractVersion 从 User-Agent 中提取 Claude Code 版本号
// 返回 "2.1.22" 形式的版本号,如果不匹配返回空字符串
func
(
v
*
ClaudeCodeValidator
)
ExtractVersion
(
ua
string
)
string
{
matches
:=
claudeCodeUAVersionPattern
.
FindStringSubmatch
(
ua
)
if
len
(
matches
)
>=
2
{
return
matches
[
1
]
}
return
""
}
// SetClaudeCodeVersion 将 Claude Code 版本号设置到 context 中
func
SetClaudeCodeVersion
(
ctx
context
.
Context
,
version
string
)
context
.
Context
{
return
context
.
WithValue
(
ctx
,
ctxkey
.
ClaudeCodeVersion
,
version
)
}
// GetClaudeCodeVersion 从 context 中获取 Claude Code 版本号
func
GetClaudeCodeVersion
(
ctx
context
.
Context
)
string
{
if
v
,
ok
:=
ctx
.
Value
(
ctxkey
.
ClaudeCodeVersion
)
.
(
string
);
ok
{
return
v
}
return
""
}
// CompareVersions 比较两个 semver 版本号
// 返回: -1 (a < b), 0 (a == b), 1 (a > b)
func
CompareVersions
(
a
,
b
string
)
int
{
aParts
:=
parseSemver
(
a
)
bParts
:=
parseSemver
(
b
)
for
i
:=
0
;
i
<
3
;
i
++
{
if
aParts
[
i
]
<
bParts
[
i
]
{
return
-
1
}
if
aParts
[
i
]
>
bParts
[
i
]
{
return
1
}
}
return
0
}
// parseSemver 解析 semver 版本号为 [major, minor, patch]
func
parseSemver
(
v
string
)
[
3
]
int
{
v
=
strings
.
TrimPrefix
(
v
,
"v"
)
parts
:=
strings
.
Split
(
v
,
"."
)
result
:=
[
3
]
int
{
0
,
0
,
0
}
for
i
:=
0
;
i
<
len
(
parts
)
&&
i
<
3
;
i
++
{
if
parsed
,
err
:=
strconv
.
Atoi
(
parts
[
i
]);
err
==
nil
{
result
[
i
]
=
parsed
}
}
return
result
}
backend/internal/service/claude_code_validator_test.go
View file @
0fffba54
...
...
@@ -56,3 +56,51 @@ func TestClaudeCodeValidator_NonMessagesPathUAOnly(t *testing.T) {
ok
:=
validator
.
Validate
(
req
,
nil
)
require
.
True
(
t
,
ok
)
}
func
TestExtractVersion
(
t
*
testing
.
T
)
{
v
:=
NewClaudeCodeValidator
()
tests
:=
[]
struct
{
ua
string
want
string
}{
{
"claude-cli/2.1.22 (darwin; arm64)"
,
"2.1.22"
},
{
"claude-cli/1.0.0"
,
"1.0.0"
},
{
"Claude-CLI/3.10.5 (linux; x86_64)"
,
"3.10.5"
},
// 大小写不敏感
{
"curl/8.0.0"
,
""
},
// 非 Claude CLI
{
""
,
""
},
// 空字符串
{
"claude-cli/"
,
""
},
// 无版本号
{
"claude-cli/2.1.22-beta"
,
"2.1.22"
},
// 带后缀仍提取主版本号
}
for
_
,
tt
:=
range
tests
{
got
:=
v
.
ExtractVersion
(
tt
.
ua
)
require
.
Equal
(
t
,
tt
.
want
,
got
,
"ExtractVersion(%q)"
,
tt
.
ua
)
}
}
func
TestCompareVersions
(
t
*
testing
.
T
)
{
tests
:=
[]
struct
{
a
,
b
string
want
int
}{
{
"2.1.0"
,
"2.1.0"
,
0
},
// 相等
{
"2.1.1"
,
"2.1.0"
,
1
},
// patch 更大
{
"2.0.0"
,
"2.1.0"
,
-
1
},
// minor 更小
{
"3.0.0"
,
"2.99.99"
,
1
},
// major 更大
{
"1.0.0"
,
"2.0.0"
,
-
1
},
// major 更小
{
"0.0.1"
,
"0.0.0"
,
1
},
// patch 差异
{
""
,
"1.0.0"
,
-
1
},
// 空字符串 vs 正常版本
{
"v2.1.0"
,
"2.1.0"
,
0
},
// v 前缀处理
}
for
_
,
tt
:=
range
tests
{
got
:=
CompareVersions
(
tt
.
a
,
tt
.
b
)
require
.
Equal
(
t
,
tt
.
want
,
got
,
"CompareVersions(%q, %q)"
,
tt
.
a
,
tt
.
b
)
}
}
func
TestSetGetClaudeCodeVersion
(
t
*
testing
.
T
)
{
ctx
:=
context
.
Background
()
require
.
Equal
(
t
,
""
,
GetClaudeCodeVersion
(
ctx
),
"empty context should return empty string"
)
ctx
=
SetClaudeCodeVersion
(
ctx
,
"2.1.63"
)
require
.
Equal
(
t
,
"2.1.63"
,
GetClaudeCodeVersion
(
ctx
))
}
backend/internal/service/domain_constants.go
View file @
0fffba54
...
...
@@ -192,6 +192,13 @@ const (
// =========================
SettingKeySoraDefaultStorageQuotaBytes
=
"sora_default_storage_quota_bytes"
// 新用户默认 Sora 存储配额(字节)
// =========================
// Claude Code Version Check
// =========================
// SettingKeyMinClaudeCodeVersion 最低 Claude Code 版本号要求 (semver, 如 "2.1.0",空值=不检查)
SettingKeyMinClaudeCodeVersion
=
"min_claude_code_version"
)
// AdminAPIKeyPrefix is the prefix for admin API keys (distinct from user "sk-" keys).
...
...
backend/internal/service/setting_service.go
View file @
0fffba54
...
...
@@ -7,12 +7,15 @@ import (
"encoding/json"
"errors"
"fmt"
"log/slog"
"strconv"
"strings"
"sync/atomic"
"time"
"github.com/Wei-Shaw/sub2api/internal/config"
infraerrors
"github.com/Wei-Shaw/sub2api/internal/pkg/errors"
"golang.org/x/sync/singleflight"
)
var
(
...
...
@@ -32,6 +35,27 @@ type SettingRepository interface {
Delete
(
ctx
context
.
Context
,
key
string
)
error
}
// cachedMinVersion 缓存最低 Claude Code 版本号(进程内缓存,60s TTL)
type
cachedMinVersion
struct
{
value
string
// 空字符串 = 不检查
expiresAt
int64
// unix nano
}
// minVersionCache 最低版本号进程内缓存
var
minVersionCache
atomic
.
Value
// *cachedMinVersion
// minVersionSF 防止缓存过期时 thundering herd
var
minVersionSF
singleflight
.
Group
// minVersionCacheTTL 缓存有效期
const
minVersionCacheTTL
=
60
*
time
.
Second
// minVersionErrorTTL DB 错误时的短缓存,快速重试
const
minVersionErrorTTL
=
5
*
time
.
Second
// minVersionDBTimeout singleflight 内 DB 查询超时,独立于请求 context
const
minVersionDBTimeout
=
5
*
time
.
Second
// SettingService 系统设置服务
type
SettingService
struct
{
settingRepo
SettingRepository
...
...
@@ -270,10 +294,21 @@ func (s *SettingService) UpdateSettings(ctx context.Context, settings *SystemSet
updates
[
SettingKeyOpsMetricsIntervalSeconds
]
=
strconv
.
Itoa
(
settings
.
OpsMetricsIntervalSeconds
)
}
// Claude Code version check
updates
[
SettingKeyMinClaudeCodeVersion
]
=
settings
.
MinClaudeCodeVersion
err
:=
s
.
settingRepo
.
SetMultiple
(
ctx
,
updates
)
if
err
==
nil
&&
s
.
onUpdate
!=
nil
{
if
err
==
nil
{
// 先使 inflight singleflight 失效,再刷新缓存,缩小旧值覆盖新值的竞态窗口
minVersionSF
.
Forget
(
"min_version"
)
minVersionCache
.
Store
(
&
cachedMinVersion
{
value
:
settings
.
MinClaudeCodeVersion
,
expiresAt
:
time
.
Now
()
.
Add
(
minVersionCacheTTL
)
.
UnixNano
(),
})
if
s
.
onUpdate
!=
nil
{
s
.
onUpdate
()
// Invalidate cache after settings update
}
}
return
err
}
...
...
@@ -417,6 +452,9 @@ func (s *SettingService) InitializeDefaultSettings(ctx context.Context) error {
SettingKeyOpsRealtimeMonitoringEnabled
:
"true"
,
SettingKeyOpsQueryModeDefault
:
"auto"
,
SettingKeyOpsMetricsIntervalSeconds
:
"60"
,
// Claude Code version check (default: empty = disabled)
SettingKeyMinClaudeCodeVersion
:
""
,
}
return
s
.
settingRepo
.
SetMultiple
(
ctx
,
defaults
)
...
...
@@ -542,6 +580,9 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin
}
}
// Claude Code version check
result
.
MinClaudeCodeVersion
=
settings
[
SettingKeyMinClaudeCodeVersion
]
return
result
}
...
...
@@ -839,6 +880,49 @@ func (s *SettingService) GetStreamTimeoutSettings(ctx context.Context) (*StreamT
return
&
settings
,
nil
}
// GetMinClaudeCodeVersion 获取最低 Claude Code 版本号要求
// 使用进程内 atomic.Value 缓存,60 秒 TTL,热路径零锁开销
// singleflight 防止缓存过期时 thundering herd
// 返回空字符串表示不做版本检查
func
(
s
*
SettingService
)
GetMinClaudeCodeVersion
(
ctx
context
.
Context
)
string
{
if
cached
,
ok
:=
minVersionCache
.
Load
()
.
(
*
cachedMinVersion
);
ok
{
if
time
.
Now
()
.
UnixNano
()
<
cached
.
expiresAt
{
return
cached
.
value
}
}
// singleflight: 同一时刻只有一个 goroutine 查询 DB,其余复用结果
result
,
_
,
_
:=
minVersionSF
.
Do
(
"min_version"
,
func
()
(
any
,
error
)
{
// 二次检查,避免排队的 goroutine 重复查询
if
cached
,
ok
:=
minVersionCache
.
Load
()
.
(
*
cachedMinVersion
);
ok
{
if
time
.
Now
()
.
UnixNano
()
<
cached
.
expiresAt
{
return
cached
.
value
,
nil
}
}
// 使用独立 context:断开请求取消链,避免客户端断连导致空值被长期缓存
dbCtx
,
cancel
:=
context
.
WithTimeout
(
context
.
WithoutCancel
(
ctx
),
minVersionDBTimeout
)
defer
cancel
()
value
,
err
:=
s
.
settingRepo
.
GetValue
(
dbCtx
,
SettingKeyMinClaudeCodeVersion
)
if
err
!=
nil
{
// fail-open: DB 错误时不阻塞请求,但记录日志并使用短 TTL 快速重试
slog
.
Warn
(
"failed to get min claude code version setting, skipping version check"
,
"error"
,
err
)
minVersionCache
.
Store
(
&
cachedMinVersion
{
value
:
""
,
expiresAt
:
time
.
Now
()
.
Add
(
minVersionErrorTTL
)
.
UnixNano
(),
})
return
""
,
nil
}
minVersionCache
.
Store
(
&
cachedMinVersion
{
value
:
value
,
expiresAt
:
time
.
Now
()
.
Add
(
minVersionCacheTTL
)
.
UnixNano
(),
})
return
value
,
nil
})
if
s
,
ok
:=
result
.
(
string
);
ok
{
return
s
}
return
""
}
// SetStreamTimeoutSettings 设置流超时处理配置
func
(
s
*
SettingService
)
SetStreamTimeoutSettings
(
ctx
context
.
Context
,
settings
*
StreamTimeoutSettings
)
error
{
if
settings
==
nil
{
...
...
backend/internal/service/settings_view.go
View file @
0fffba54
...
...
@@ -60,6 +60,9 @@ type SystemSettings struct {
OpsRealtimeMonitoringEnabled
bool
OpsQueryModeDefault
string
OpsMetricsIntervalSeconds
int
// Claude Code version check
MinClaudeCodeVersion
string
}
type
PublicSettings
struct
{
...
...
frontend/src/api/admin/settings.ts
View file @
0fffba54
...
...
@@ -67,6 +67,9 @@ export interface SystemSettings {
ops_realtime_monitoring_enabled
:
boolean
ops_query_mode_default
:
'
auto
'
|
'
raw
'
|
'
preagg
'
|
string
ops_metrics_interval_seconds
:
number
// Claude Code version check
min_claude_code_version
:
string
}
export
interface
UpdateSettingsRequest
{
...
...
@@ -114,6 +117,7 @@ export interface UpdateSettingsRequest {
ops_realtime_monitoring_enabled
?:
boolean
ops_query_mode_default
?:
'
auto
'
|
'
raw
'
|
'
preagg
'
|
string
ops_metrics_interval_seconds
?:
number
min_claude_code_version
?:
string
}
/**
...
...
frontend/src/i18n/locales/en.ts
View file @
0fffba54
...
...
@@ -3548,6 +3548,14 @@ export default {
defaultConcurrency
:
'
Default Concurrency
'
,
defaultConcurrencyHint
:
'
Maximum concurrent requests for new users
'
},
claudeCode
:
{
title
:
'
Claude Code Settings
'
,
description
:
'
Control Claude Code client access requirements
'
,
minVersion
:
'
Minimum Version
'
,
minVersionPlaceholder
:
'
e.g. 2.1.63
'
,
minVersionHint
:
'
Reject Claude Code clients below this version (semver format). Leave empty to disable version check.
'
},
site
:
{
title
:
'
Site Settings
'
,
description
:
'
Customize site branding
'
,
...
...
frontend/src/i18n/locales/zh.ts
View file @
0fffba54
...
...
@@ -3718,6 +3718,13 @@ export default {
defaultConcurrency
:
'
默认并发数
'
,
defaultConcurrencyHint
:
'
新用户的最大并发请求数
'
},
claudeCode
:
{
title
:
'
Claude Code 设置
'
,
description
:
'
控制 Claude Code 客户端访问要求
'
,
minVersion
:
'
最低版本号
'
,
minVersionPlaceholder
:
'
例如 2.1.63
'
,
minVersionHint
:
'
拒绝低于此版本的 Claude Code 客户端请求(semver 格式)。留空则不检查版本。
'
},
site
:
{
title
:
'
站点设置
'
,
description
:
'
自定义站点品牌
'
,
...
...
frontend/src/views/admin/SettingsView.vue
View file @
0fffba54
...
...
@@ -616,6 +616,35 @@
</div>
</div>
<!-- Claude Code Settings -->
<div
class=
"card"
>
<div
class=
"border-b border-gray-100 px-6 py-4 dark:border-dark-700"
>
<h2
class=
"text-lg font-semibold text-gray-900 dark:text-white"
>
{{ t('admin.settings.claudeCode.title') }}
</h2>
<p
class=
"mt-1 text-sm text-gray-500 dark:text-gray-400"
>
{{ t('admin.settings.claudeCode.description') }}
</p>
</div>
<div
class=
"p-6"
>
<div>
<label
class=
"mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{{ t('admin.settings.claudeCode.minVersion') }}
</label>
<input
v-model=
"form.min_claude_code_version"
type=
"text"
class=
"input max-w-xs font-mono text-sm"
:placeholder=
"t('admin.settings.claudeCode.minVersionPlaceholder')"
pattern=
"\d+\.\d+\.\d+"
/>
<p
class=
"mt-1.5 text-xs text-gray-500 dark:text-gray-400"
>
{{ t('admin.settings.claudeCode.minVersionHint') }}
</p>
</div>
</div>
</div>
<!-- Site Settings -->
<div
class=
"card"
>
<div
class=
"border-b border-gray-100 px-6 py-4 dark:border-dark-700"
>
...
...
@@ -1203,7 +1232,9 @@ const form = reactive<SettingsForm>({
ops_monitoring_enabled
:
true
,
ops_realtime_monitoring_enabled
:
true
,
ops_query_mode_default
:
'
auto
'
,
ops_metrics_interval_seconds
:
60
ops_metrics_interval_seconds
:
60
,
// Claude Code version check
min_claude_code_version
:
''
})
// LinuxDo OAuth redirect URL suggestion
...
...
@@ -1320,7 +1351,8 @@ async function saveSettings() {
fallback_model_gemini
:
form
.
fallback_model_gemini
,
fallback_model_antigravity
:
form
.
fallback_model_antigravity
,
enable_identity_patch
:
form
.
enable_identity_patch
,
identity_patch_prompt
:
form
.
identity_patch_prompt
identity_patch_prompt
:
form
.
identity_patch_prompt
,
min_claude_code_version
:
form
.
min_claude_code_version
}
const
updated
=
await
adminAPI
.
settings
.
updateSettings
(
payload
)
Object
.
assign
(
form
,
updated
)
...
...
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