@@ -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值,如果不存在则返回默认值
131170func 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 误判为更新版本)
375424func 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