feat: add Anthropic sticky session digest chain matching via Trie

The previous fallback (step 3) in GenerateSessionHash hashed system +
all messages together, producing a different hash each round as the
conversation grew ([a] -> [a,b] -> [a,b,c]). This made fallback sticky
sessions ineffective for multi-turn conversations.

Implement per-message Trie digest chain matching (reusing Gemini's Trie
infrastructure) so that the previous round's chain is always a prefix
of the current round's chain, enabling reliable session affinity.
This commit is contained in:
erio
2026-02-07 17:35:05 +08:00
parent e1a68497d6
commit 50a783ff01
8 changed files with 604 additions and 22 deletions

View File

@@ -22,6 +22,7 @@ import (
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
// GatewayHandler handles API gateway requests
@@ -212,6 +213,53 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
if sessionKey != "" {
sessionBoundAccountID, _ = h.gatewayService.GetCachedSessionAccountID(c.Request.Context(), apiKey.GroupID, sessionKey)
}
// === Anthropic 内容摘要会话 Fallback 逻辑 ===
// 当原有会话标识无效时sessionBoundAccountID == 0尝试基于内容摘要链匹配
var anthropicDigestChain string
var anthropicPrefixHash string
var anthropicSessionUUID string
useAnthropicDigestFallback := sessionBoundAccountID == 0 && platform != service.PlatformGemini
if useAnthropicDigestFallback {
anthropicDigestChain = service.BuildAnthropicDigestChain(parsedReq)
if anthropicDigestChain != "" {
userAgent := c.GetHeader("User-Agent")
clientIP := ip.GetClientIP(c)
anthropicPrefixHash = service.GenerateGeminiPrefixHash(
subject.UserID,
apiKey.ID,
clientIP,
userAgent,
platform,
reqModel,
)
foundUUID, foundAccountID, found := h.gatewayService.FindAnthropicSession(
c.Request.Context(),
derefGroupID(apiKey.GroupID),
anthropicPrefixHash,
anthropicDigestChain,
)
if found {
sessionBoundAccountID = foundAccountID
anthropicSessionUUID = foundUUID
log.Printf("[Anthropic] Digest fallback matched: uuid=%s, accountID=%d, chain=%s",
foundUUID[:8], foundAccountID, truncateDigestChain(anthropicDigestChain))
if sessionKey == "" {
sessionKey = service.GenerateAnthropicDigestSessionKey(anthropicPrefixHash, foundUUID)
}
_ = h.gatewayService.BindStickySession(c.Request.Context(), apiKey.GroupID, sessionKey, foundAccountID)
} else {
anthropicSessionUUID = uuid.New().String()
if sessionKey == "" {
sessionKey = service.GenerateAnthropicDigestSessionKey(anthropicPrefixHash, anthropicSessionUUID)
}
}
}
}
// 判断是否真的绑定了粘性会话:有 sessionKey 且已经绑定到某个账号
hasBoundSession := sessionKey != "" && sessionBoundAccountID > 0
@@ -540,6 +588,20 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
userAgent := c.GetHeader("User-Agent")
clientIP := ip.GetClientIP(c)
// 保存 Anthropic 内容摘要会话(用于 Fallback 匹配)
if useAnthropicDigestFallback && anthropicDigestChain != "" && anthropicPrefixHash != "" {
if err := h.gatewayService.SaveAnthropicSession(
c.Request.Context(),
derefGroupID(apiKey.GroupID),
anthropicPrefixHash,
anthropicDigestChain,
anthropicSessionUUID,
account.ID,
); err != nil {
log.Printf("[Anthropic] Failed to save digest session: %v", err)
}
}
// 异步记录使用量subscription已在函数开头获取
go func(result *service.ForwardResult, usedAccount *service.Account, ua, clientIP string, fcb bool) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)