Merge pull request #705 from DaydreamCoding/feat/fingerprint-ttl-lazy-renewal

feat(identity): 指纹缓存 TTL 懒续期机制
This commit is contained in:
Wesley Liddick
2026-03-02 08:33:23 +08:00
committed by GitHub
2 changed files with 63 additions and 7 deletions

View File

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

View File

@@ -46,6 +46,7 @@ type Fingerprint struct {
StainlessArch string
StainlessRuntime string
StainlessRuntimeVersion string
UpdatedAt int64 `json:",omitempty"` // Unix timestamp用于判断是否需要续期TTL
}
// IdentityCache defines cache operations for identity service
@@ -78,14 +79,26 @@ func (s *IdentityService) GetOrCreateFingerprint(ctx context.Context, accountID
// 尝试从缓存获取指纹
cached, err := s.cache.GetFingerprint(ctx, accountID)
if err == nil && cached != nil {
needWrite := false
// 检查客户端的user-agent是否是更新版本
clientUA := headers.Get("User-Agent")
if clientUA != "" && isNewerVersion(clientUA, cached.UserAgent) {
// 更新user-agent
cached.UserAgent = clientUA
// 保存更新后的指纹
_ = s.cache.SetFingerprint(ctx, accountID, cached)
logger.LegacyPrintf("service.identity", "Updated fingerprint user-agent for account %d: %s", accountID, clientUA)
// 版本升级merge 语义 — 仅更新请求中实际携带的字段,保留缓存值
// 避免缺失的头被硬编码默认值覆盖(如新 CLI 版本 + 旧 SDK 默认值的不一致)
mergeHeadersIntoFingerprint(cached, headers)
needWrite = true
logger.LegacyPrintf("service.identity", "Updated fingerprint for account %d: %s (merge update)", accountID, clientUA)
} else if time.Since(time.Unix(cached.UpdatedAt, 0)) > 24*time.Hour {
// 距上次写入超过24小时续期TTL
needWrite = true
}
if needWrite {
cached.UpdatedAt = time.Now().Unix()
if err := s.cache.SetFingerprint(ctx, accountID, cached); err != nil {
logger.LegacyPrintf("service.identity", "Warning: failed to refresh fingerprint for account %d: %v", accountID, err)
}
}
return cached, nil
}
@@ -95,8 +108,9 @@ func (s *IdentityService) GetOrCreateFingerprint(ctx context.Context, accountID
// 生成随机ClientID
fp.ClientID = generateClientID()
fp.UpdatedAt = time.Now().Unix()
// 保存到缓存(永不过期)
// 保存到缓存(7天TTL每24小时自动续期)
if err := s.cache.SetFingerprint(ctx, accountID, fp); err != nil {
logger.LegacyPrintf("service.identity", "Warning: failed to cache fingerprint for account %d: %v", accountID, err)
}
@@ -127,6 +141,31 @@ func (s *IdentityService) createFingerprintFromHeaders(headers http.Header) *Fin
return fp
}
// mergeHeadersIntoFingerprint 将请求头中实际存在的字段合并到现有指纹中(用于版本升级场景)
// 关键语义:请求中有的字段 → 用新值覆盖;缺失的头 → 保留缓存中的已有值
// 与 createFingerprintFromHeaders 的区别:后者用于首次创建,缺失头回退到 defaultFingerprint
// 本函数用于升级更新,缺失头保留缓存值,避免将已知的真实值退化为硬编码默认值
func mergeHeadersIntoFingerprint(fp *Fingerprint, headers http.Header) {
// User-Agent版本升级的触发条件一定存在
if ua := headers.Get("User-Agent"); ua != "" {
fp.UserAgent = ua
}
// X-Stainless-* 头:仅在请求中实际携带时才更新,否则保留缓存值
mergeHeader(headers, "X-Stainless-Lang", &fp.StainlessLang)
mergeHeader(headers, "X-Stainless-Package-Version", &fp.StainlessPackageVersion)
mergeHeader(headers, "X-Stainless-OS", &fp.StainlessOS)
mergeHeader(headers, "X-Stainless-Arch", &fp.StainlessArch)
mergeHeader(headers, "X-Stainless-Runtime", &fp.StainlessRuntime)
mergeHeader(headers, "X-Stainless-Runtime-Version", &fp.StainlessRuntimeVersion)
}
// mergeHeader 如果请求头中存在该字段则更新目标值,否则保留原值
func mergeHeader(headers http.Header, key string, target *string) {
if v := headers.Get(key); v != "" {
*target = v
}
}
// getHeaderOrDefault 获取header值如果不存在则返回默认值
func getHeaderOrDefault(headers http.Header, key, defaultValue string) string {
if v := headers.Get(key); v != "" {
@@ -371,8 +410,25 @@ func parseUserAgentVersion(ua string) (major, minor, patch int, ok bool) {
return major, minor, patch, true
}
// extractProduct 提取 User-Agent 中 "/" 前的产品名
// 例如claude-cli/2.1.22 (external, cli) -> "claude-cli"
func extractProduct(ua string) string {
if idx := strings.Index(ua, "/"); idx > 0 {
return strings.ToLower(ua[:idx])
}
return ""
}
// isNewerVersion 比较版本号判断newUA是否比cachedUA更新
// 要求产品名一致(防止浏览器 UA 如 Mozilla/5.0 误判为更新版本)
func isNewerVersion(newUA, cachedUA string) bool {
// 校验产品名一致性
newProduct := extractProduct(newUA)
cachedProduct := extractProduct(cachedUA)
if newProduct == "" || cachedProduct == "" || newProduct != cachedProduct {
return false
}
newMajor, newMinor, newPatch, newOk := parseUserAgentVersion(newUA)
cachedMajor, cachedMinor, cachedPatch, cachedOk := parseUserAgentVersion(cachedUA)