Skip to content

Commit d869ac9

Browse files
author
QTom
committed
feat(identity): 指纹缓存 TTL 懒续期机制
- TTL 改为 7 天,配合 24 小时自动续期保持活跃账号永不过期 - 版本升级时采用合并语义,仅更新请求中实际存在的字段 - 添加产品名验证防止浏览器 UA 误判为更新版本
1 parent 3613695 commit d869ac9

2 files changed

Lines changed: 63 additions & 7 deletions

File tree

backend/internal/repository/identity_cache.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import (
1212

1313
const (
1414
fingerprintKeyPrefix = "fingerprint:"
15-
fingerprintTTL = 24 * time.Hour
15+
fingerprintTTL = 7 * 24 * time.Hour // 7天,配合每24小时懒续期可保持活跃账号永不过期
1616
maskedSessionKeyPrefix = "masked_session:"
1717
maskedSessionTTL = 15 * time.Minute
1818
)

backend/internal/service/identity_service.go

Lines changed: 62 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ type Fingerprint struct {
4646
StainlessArch string
4747
StainlessRuntime string
4848
StainlessRuntimeVersion string
49+
UpdatedAt int64 `json:",omitempty"` // Unix timestamp,用于判断是否需要续期TTL
4950
}
5051

5152
// IdentityCache defines cache operations for identity service
@@ -78,14 +79,26 @@ func (s *IdentityService) GetOrCreateFingerprint(ctx context.Context, accountID
7879
// 尝试从缓存获取指纹
7980
cached, err := s.cache.GetFingerprint(ctx, accountID)
8081
if err == nil && cached != nil {
82+
needWrite := false
83+
8184
// 检查客户端的user-agent是否是更新版本
8285
clientUA := headers.Get("User-Agent")
8386
if clientUA != "" && isNewerVersion(clientUA, cached.UserAgent) {
84-
// 更新user-agent
85-
cached.UserAgent = clientUA
86-
// 保存更新后的指纹
87-
_ = s.cache.SetFingerprint(ctx, accountID, cached)
88-
logger.LegacyPrintf("service.identity", "Updated fingerprint user-agent for account %d: %s", accountID, clientUA)
87+
// 版本升级:merge 语义 — 仅更新请求中实际携带的字段,保留缓存值
88+
// 避免缺失的头被硬编码默认值覆盖(如新 CLI 版本 + 旧 SDK 默认值的不一致)
89+
mergeHeadersIntoFingerprint(cached, headers)
90+
needWrite = true
91+
logger.LegacyPrintf("service.identity", "Updated fingerprint for account %d: %s (merge update)", accountID, clientUA)
92+
} else if time.Since(time.Unix(cached.UpdatedAt, 0)) > 24*time.Hour {
93+
// 距上次写入超过24小时,续期TTL
94+
needWrite = true
95+
}
96+
97+
if needWrite {
98+
cached.UpdatedAt = time.Now().Unix()
99+
if err := s.cache.SetFingerprint(ctx, accountID, cached); err != nil {
100+
logger.LegacyPrintf("service.identity", "Warning: failed to refresh fingerprint for account %d: %v", accountID, err)
101+
}
89102
}
90103
return cached, nil
91104
}
@@ -95,8 +108,9 @@ func (s *IdentityService) GetOrCreateFingerprint(ctx context.Context, accountID
95108

96109
// 生成随机ClientID
97110
fp.ClientID = generateClientID()
111+
fp.UpdatedAt = time.Now().Unix()
98112

99-
// 保存到缓存(永不过期
113+
// 保存到缓存(7天TTL,每24小时自动续期
100114
if err := s.cache.SetFingerprint(ctx, accountID, fp); err != nil {
101115
logger.LegacyPrintf("service.identity", "Warning: failed to cache fingerprint for account %d: %v", accountID, err)
102116
}
@@ -127,6 +141,31 @@ func (s *IdentityService) createFingerprintFromHeaders(headers http.Header) *Fin
127141
return fp
128142
}
129143

144+
// mergeHeadersIntoFingerprint 将请求头中实际存在的字段合并到现有指纹中(用于版本升级场景)
145+
// 关键语义:请求中有的字段 → 用新值覆盖;缺失的头 → 保留缓存中的已有值
146+
// 与 createFingerprintFromHeaders 的区别:后者用于首次创建,缺失头回退到 defaultFingerprint;
147+
// 本函数用于升级更新,缺失头保留缓存值,避免将已知的真实值退化为硬编码默认值
148+
func mergeHeadersIntoFingerprint(fp *Fingerprint, headers http.Header) {
149+
// User-Agent:版本升级的触发条件,一定存在
150+
if ua := headers.Get("User-Agent"); ua != "" {
151+
fp.UserAgent = ua
152+
}
153+
// X-Stainless-* 头:仅在请求中实际携带时才更新,否则保留缓存值
154+
mergeHeader(headers, "X-Stainless-Lang", &fp.StainlessLang)
155+
mergeHeader(headers, "X-Stainless-Package-Version", &fp.StainlessPackageVersion)
156+
mergeHeader(headers, "X-Stainless-OS", &fp.StainlessOS)
157+
mergeHeader(headers, "X-Stainless-Arch", &fp.StainlessArch)
158+
mergeHeader(headers, "X-Stainless-Runtime", &fp.StainlessRuntime)
159+
mergeHeader(headers, "X-Stainless-Runtime-Version", &fp.StainlessRuntimeVersion)
160+
}
161+
162+
// mergeHeader 如果请求头中存在该字段则更新目标值,否则保留原值
163+
func mergeHeader(headers http.Header, key string, target *string) {
164+
if v := headers.Get(key); v != "" {
165+
*target = v
166+
}
167+
}
168+
130169
// getHeaderOrDefault 获取header值,如果不存在则返回默认值
131170
func getHeaderOrDefault(headers http.Header, key, defaultValue string) string {
132171
if v := headers.Get(key); v != "" {
@@ -371,8 +410,25 @@ func parseUserAgentVersion(ua string) (major, minor, patch int, ok bool) {
371410
return major, minor, patch, true
372411
}
373412

413+
// extractProduct 提取 User-Agent 中 "/" 前的产品名
414+
// 例如:claude-cli/2.1.22 (external, cli) -> "claude-cli"
415+
func extractProduct(ua string) string {
416+
if idx := strings.Index(ua, "/"); idx > 0 {
417+
return strings.ToLower(ua[:idx])
418+
}
419+
return ""
420+
}
421+
374422
// isNewerVersion 比较版本号,判断newUA是否比cachedUA更新
423+
// 要求产品名一致(防止浏览器 UA 如 Mozilla/5.0 误判为更新版本)
375424
func isNewerVersion(newUA, cachedUA string) bool {
425+
// 校验产品名一致性
426+
newProduct := extractProduct(newUA)
427+
cachedProduct := extractProduct(cachedUA)
428+
if newProduct == "" || cachedProduct == "" || newProduct != cachedProduct {
429+
return false
430+
}
431+
376432
newMajor, newMinor, newPatch, newOk := parseUserAgentVersion(newUA)
377433
cachedMajor, cachedMinor, cachedPatch, cachedOk := parseUserAgentVersion(cachedUA)
378434

0 commit comments

Comments
 (0)