Ctrl K
ComunidadGuías…

internal/cache/claude_web_search_result_cache.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package cache

import (
	"bytes"
	"container/list"
	"context"
	"crypto/sha256"
	"encoding/hex"
	"strings"
	"sync"
	"time"

	homekv "github.com/router-for-me/CLIProxyAPI/v8/internal/home"
	log "github.com/sirupsen/logrus"
)

const (
	defaultClaudeWebSearchResultTTL   = 24 * time.Hour
	defaultClaudeWebSearchResultLimit = 1024
)

type claudeWebSearchResultEntry struct {
	key       string
	content   []byte
	ttl       time.Duration
	expiresAt time.Time
}

var (
	claudeWebSearchResultMu    sync.Mutex
	claudeWebSearchResultIndex = make(map[string]*list.Element)
	claudeWebSearchResultOrder = list.New()
	claudeWebSearchResultNow   = time.Now
)

type claudeWebSearchResultKVClient interface {
	KVGet(ctx context.Context, key string) ([]byte, bool, error)
	KVSet(ctx context.Context, key string, value []byte, opts homekv.KVSetOptions) (bool, error)
	KVExpire(ctx context.Context, key string, ttl time.Duration) (bool, error)
}

var currentClaudeWebSearchResultKVClient = func() (claudeWebSearchResultKVClient, bool, error) {
	return homekv.CurrentKVClient()
}

func claudeWebSearchResultKey(sessionKey, toolUseID string) (string, bool) {
	sessionKey = strings.TrimSpace(sessionKey)
	toolUseID = strings.TrimSpace(toolUseID)
	if sessionKey == "" || toolUseID == "" {
		return "", false
	}
	sum := sha256.Sum256([]byte(sessionKey + "\x00" + toolUseID))
	return hex.EncodeToString(sum[:]), true
}

func claudeWebSearchResultKVKey(sessionKey, toolUseID string) string {
	key, ok := claudeWebSearchResultKey(sessionKey, toolUseID)
	if !ok {
		return ""
	}
	return "cpa:claude:web-search-result:" + key
}

// CacheClaudeWebSearchResult stores the raw content of one web_search_tool_result
// block for a session. Memory is bounded by the maximum entry count (limit);
// least recently used entries are evicted when the entry limit is exceeded.
func CacheClaudeWebSearchResult(sessionKey, toolUseID string, content []byte, ttl time.Duration, limit int) {
	CacheClaudeWebSearchResultBestEffort(context.Background(), sessionKey, toolUseID, content, ttl, limit)
}

// CacheClaudeWebSearchResultBestEffort stores the raw content of one
// web_search_tool_result block for completed response paths.
func CacheClaudeWebSearchResultBestEffort(ctx context.Context, sessionKey, toolUseID string, content []byte, ttl time.Duration, limit int) bool {
	key, ok := claudeWebSearchResultKey(sessionKey, toolUseID)
	if !ok || len(content) == 0 {
		return false
	}
	if ttl <= 0 {
		ttl = defaultClaudeWebSearchResultTTL
	}
	if ctx == nil {
		ctx = context.Background()
	}
	if client, homeMode, errClient := currentClaudeWebSearchResultKVClient(); homeMode {
		if errClient != nil {
			log.Errorf("home kv best-effort claude web search result set failed prefix=cpa:claude:*: %v", errClient)
			return false
		}
		written, errSet := client.KVSet(ctx, "cpa:claude:web-search-result:"+key, bytes.Clone(content), homekv.KVSetOptions{EX: ttl})
		if errSet != nil {
			log.Errorf("home kv best-effort claude web search result set failed prefix=cpa:claude:*: %v", errSet)
			return false
		}
		return written
	}
	if limit <= 0 {
		limit = defaultClaudeWebSearchResultLimit
	}
	cacheCleanupOnce.Do(startCacheCleanup)

	claudeWebSearchResultMu.Lock()
	defer claudeWebSearchResultMu.Unlock()

	now := claudeWebSearchResultNow()
	if element, exists := claudeWebSearchResultIndex[key]; exists {
		removeClaudeWebSearchResultLocked(element)
	}
	if claudeWebSearchResultOrder.Len() >= limit {
		purgeExpiredClaudeWebSearchResultLocked(now)
	}
	entry := &claudeWebSearchResultEntry{
		key:       key,
		content:   bytes.Clone(content),
		ttl:       ttl,
		expiresAt: now.Add(ttl),
	}
	claudeWebSearchResultIndex[key] = claudeWebSearchResultOrder.PushFront(entry)
	for claudeWebSearchResultOrder.Len() > limit {
		oldest := claudeWebSearchResultOrder.Back()
		if oldest == nil {
			break
		}
		removeClaudeWebSearchResultLocked(oldest)
	}
	return true
}

// GetClaudeWebSearchResult returns a copy of the stored content and extends its
// lifetime. Expired entries are removed and reported as missing.
func GetClaudeWebSearchResult(sessionKey, toolUseID string, ttl ...time.Duration) ([]byte, bool) {
	content, ok, errGet := GetClaudeWebSearchResultRequired(context.Background(), sessionKey, toolUseID, ttl...)
	if errGet != nil {
		return nil, false
	}
	return content, ok
}

// GetClaudeWebSearchResultRequired retrieves a copy of the stored content for
// request-time paths and extends its lifetime.
func GetClaudeWebSearchResultRequired(ctx context.Context, sessionKey, toolUseID string, ttl ...time.Duration) ([]byte, bool, error) {
	key, ok := claudeWebSearchResultKey(sessionKey, toolUseID)
	if !ok {
		return nil, false, nil
	}
	if ctx == nil {
		ctx = context.Background()
	}
	if client, homeMode, errClient := currentClaudeWebSearchResultKVClient(); homeMode {
		if errClient != nil {
			return nil, false, errClient
		}
		kvKey := "cpa:claude:web-search-result:" + key
		raw, found, errGet := client.KVGet(ctx, kvKey)
		if errGet != nil || !found || len(raw) == 0 {
			return nil, false, errGet
		}
		effectiveTTL := defaultClaudeWebSearchResultTTL
		if len(ttl) > 0 && ttl[0] > 0 {
			effectiveTTL = ttl[0]
		}
		if _, errExpire := client.KVExpire(ctx, kvKey, effectiveTTL); errExpire != nil {
			log.Warnf("home kv claude web search result expire failed prefix=cpa:claude:*: %v", errExpire)
		}
		return bytes.Clone(raw), true, nil
	}
	claudeWebSearchResultMu.Lock()
	defer claudeWebSearchResultMu.Unlock()

	element, exists := claudeWebSearchResultIndex[key]
	if !exists {
		return nil, false, nil
	}
	entry := element.Value.(*claudeWebSearchResultEntry)
	now := claudeWebSearchResultNow()
	if !now.Before(entry.expiresAt) {
		removeClaudeWebSearchResultLocked(element)
		return nil, false, nil
	}
	effectiveTTL := entry.ttl
	if len(ttl) > 0 && ttl[0] > 0 {
		effectiveTTL = ttl[0]
		entry.ttl = effectiveTTL
	}
	if effectiveTTL <= 0 {
		effectiveTTL = defaultClaudeWebSearchResultTTL
	}
	entry.expiresAt = now.Add(effectiveTTL)
	claudeWebSearchResultOrder.MoveToFront(element)
	return bytes.Clone(entry.content), true, nil
}

// ClearClaudeWebSearchResultCache removes every stored entry.
func ClearClaudeWebSearchResultCache() {
	claudeWebSearchResultMu.Lock()
	defer claudeWebSearchResultMu.Unlock()
	claudeWebSearchResultIndex = make(map[string]*list.Element)
	claudeWebSearchResultOrder.Init()
}

func purgeExpiredClaudeWebSearchResultCache(now time.Time) {
	claudeWebSearchResultMu.Lock()
	defer claudeWebSearchResultMu.Unlock()
	purgeExpiredClaudeWebSearchResultLocked(now)
}

func purgeExpiredClaudeWebSearchResultLocked(now time.Time) {
	for element := claudeWebSearchResultOrder.Back(); element != nil; {
		prev := element.Prev()
		entry := element.Value.(*claudeWebSearchResultEntry)
		if !now.Before(entry.expiresAt) {
			removeClaudeWebSearchResultLocked(element)
		}
		element = prev
	}
}

func removeClaudeWebSearchResultLocked(element *list.Element) {
	entry := element.Value.(*claudeWebSearchResultEntry)
	delete(claudeWebSearchResultIndex, entry.key)
	claudeWebSearchResultOrder.Remove(element)
}