Merge pull request #705 from DaydreamCoding/feat/fingerprint-ttl-lazy-renewal
feat(identity): 指纹缓存 TTL 懒续期机制
This commit is contained in:
@@ -12,7 +12,7 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
fingerprintKeyPrefix = "fingerprint:"
|
fingerprintKeyPrefix = "fingerprint:"
|
||||||
fingerprintTTL = 24 * time.Hour
|
fingerprintTTL = 7 * 24 * time.Hour // 7天,配合每24小时懒续期可保持活跃账号永不过期
|
||||||
maskedSessionKeyPrefix = "masked_session:"
|
maskedSessionKeyPrefix = "masked_session:"
|
||||||
maskedSessionTTL = 15 * time.Minute
|
maskedSessionTTL = 15 * time.Minute
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ type Fingerprint struct {
|
|||||||
StainlessArch string
|
StainlessArch string
|
||||||
StainlessRuntime string
|
StainlessRuntime string
|
||||||
StainlessRuntimeVersion string
|
StainlessRuntimeVersion string
|
||||||
|
UpdatedAt int64 `json:",omitempty"` // Unix timestamp,用于判断是否需要续期TTL
|
||||||
}
|
}
|
||||||
|
|
||||||
// IdentityCache defines cache operations for identity service
|
// 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)
|
cached, err := s.cache.GetFingerprint(ctx, accountID)
|
||||||
if err == nil && cached != nil {
|
if err == nil && cached != nil {
|
||||||
|
needWrite := false
|
||||||
|
|
||||||
// 检查客户端的user-agent是否是更新版本
|
// 检查客户端的user-agent是否是更新版本
|
||||||
clientUA := headers.Get("User-Agent")
|
clientUA := headers.Get("User-Agent")
|
||||||
if clientUA != "" && isNewerVersion(clientUA, cached.UserAgent) {
|
if clientUA != "" && isNewerVersion(clientUA, cached.UserAgent) {
|
||||||
// 更新user-agent
|
// 版本升级:merge 语义 — 仅更新请求中实际携带的字段,保留缓存值
|
||||||
cached.UserAgent = clientUA
|
// 避免缺失的头被硬编码默认值覆盖(如新 CLI 版本 + 旧 SDK 默认值的不一致)
|
||||||
// 保存更新后的指纹
|
mergeHeadersIntoFingerprint(cached, headers)
|
||||||
_ = s.cache.SetFingerprint(ctx, accountID, cached)
|
needWrite = true
|
||||||
logger.LegacyPrintf("service.identity", "Updated fingerprint user-agent for account %d: %s", accountID, clientUA)
|
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
|
return cached, nil
|
||||||
}
|
}
|
||||||
@@ -95,8 +108,9 @@ func (s *IdentityService) GetOrCreateFingerprint(ctx context.Context, accountID
|
|||||||
|
|
||||||
// 生成随机ClientID
|
// 生成随机ClientID
|
||||||
fp.ClientID = generateClientID()
|
fp.ClientID = generateClientID()
|
||||||
|
fp.UpdatedAt = time.Now().Unix()
|
||||||
|
|
||||||
// 保存到缓存(永不过期)
|
// 保存到缓存(7天TTL,每24小时自动续期)
|
||||||
if err := s.cache.SetFingerprint(ctx, accountID, fp); err != nil {
|
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)
|
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
|
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值,如果不存在则返回默认值
|
// getHeaderOrDefault 获取header值,如果不存在则返回默认值
|
||||||
func getHeaderOrDefault(headers http.Header, key, defaultValue string) string {
|
func getHeaderOrDefault(headers http.Header, key, defaultValue string) string {
|
||||||
if v := headers.Get(key); v != "" {
|
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
|
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更新
|
// isNewerVersion 比较版本号,判断newUA是否比cachedUA更新
|
||||||
|
// 要求产品名一致(防止浏览器 UA 如 Mozilla/5.0 误判为更新版本)
|
||||||
func isNewerVersion(newUA, cachedUA string) bool {
|
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)
|
newMajor, newMinor, newPatch, newOk := parseUserAgentVersion(newUA)
|
||||||
cachedMajor, cachedMinor, cachedPatch, cachedOk := parseUserAgentVersion(cachedUA)
|
cachedMajor, cachedMinor, cachedPatch, cachedOk := parseUserAgentVersion(cachedUA)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user