feat(gateway): 添加 Claude Code 客户端最低版本检查功能
- 通过 User-Agent 识别 Claude Code 客户端并提取版本号 - 在网关层验证客户端版本是否满足管理员配置的最低要求 - 在管理后台提供版本要求配置选项(英文/中文双语) - 实现原子缓存 + singleflight 防止并发问题和 thundering herd - 使用 context.WithoutCancel 隔离 DB 查询,避免客户端断连影响缓存 - 双 TTL 策略:60s 正常、5s 错误恢复,保证性能与可用性 - 仅检查 Claude Code 客户端,其他客户端不受影响 - 添加完整单元测试覆盖版本提取、比对、上下文操作
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user