sub2api/backend/internal/pkg/windsurf/conversation_pool.go
win 21325afb33
Some checks failed
CI / test (push) Failing after 10s
CI / frontend (push) Failing after 8s
CI / golangci-lint (push) Failing after 5s
Security Scan / backend-security (push) Failing after 5s
Security Scan / frontend-security (push) Failing after 4s
feat(windsurf): 补全ops日志记录与endpoint派生,对齐其他平台
- windsurf_gateway_service: 添加上游延迟/TTFT/错误上下文记录
- endpoint: DeriveUpstreamEndpoint 添加 PlatformWindsurf 分支
- ops_error_logger: guessPlatformFromPath 添加 /windsurf/ 识别
2026-04-23 20:46:27 +08:00

186 lines
3.9 KiB
Go

package windsurf
import (
"crypto/sha256"
"encoding/json"
"fmt"
"sync"
"time"
)
const (
poolTTL = 30 * time.Minute
poolMax = 500
)
type ConversationEntry struct {
CascadeID string
SessionID string
LSPort int
APIKey string
CreatedAt time.Time
LastAccess time.Time
}
type ConversationPool struct {
mu sync.Mutex
pool map[string]*ConversationEntry
stats poolStats
}
type poolStats struct {
Hits int `json:"hits"`
Misses int `json:"misses"`
Stores int `json:"stores"`
Evictions int `json:"evictions"`
Expired int `json:"expired"`
}
func NewConversationPool() *ConversationPool {
cp := &ConversationPool{
pool: make(map[string]*ConversationEntry),
}
go cp.pruneLoop()
return cp
}
func (cp *ConversationPool) Checkout(fingerprint string) *ConversationEntry {
if fingerprint == "" {
cp.mu.Lock()
cp.stats.Misses++
cp.mu.Unlock()
return nil
}
cp.mu.Lock()
defer cp.mu.Unlock()
entry, ok := cp.pool[fingerprint]
if !ok {
cp.stats.Misses++
return nil
}
delete(cp.pool, fingerprint)
if time.Since(entry.LastAccess) > poolTTL {
cp.stats.Expired++
cp.stats.Misses++
return nil
}
cp.stats.Hits++
return entry
}
func (cp *ConversationPool) Checkin(fingerprint string, entry *ConversationEntry) {
if fingerprint == "" || entry == nil {
return
}
now := time.Now()
cp.mu.Lock()
defer cp.mu.Unlock()
if entry.CreatedAt.IsZero() {
entry.CreatedAt = now
}
entry.LastAccess = now
cp.pool[fingerprint] = entry
cp.stats.Stores++
cp.pruneLocked(now)
}
func (cp *ConversationPool) InvalidateFor(apiKey string, lsPort int) int {
cp.mu.Lock()
defer cp.mu.Unlock()
dropped := 0
for fp, e := range cp.pool {
if (apiKey != "" && e.APIKey == apiKey) || (lsPort > 0 && e.LSPort == lsPort) {
delete(cp.pool, fp)
dropped++
}
}
return dropped
}
func (cp *ConversationPool) pruneLocked(now time.Time) {
for fp, e := range cp.pool {
if now.Sub(e.LastAccess) > poolTTL {
delete(cp.pool, fp)
cp.stats.Expired++
}
}
if len(cp.pool) <= poolMax {
return
}
// LRU eviction: find oldest entries
type fpTime struct {
fp string
t time.Time
}
entries := make([]fpTime, 0, len(cp.pool))
for fp, e := range cp.pool {
entries = append(entries, fpTime{fp, e.LastAccess})
}
// Simple sort by time
for i := 0; i < len(entries)-1; i++ {
for j := i + 1; j < len(entries); j++ {
if entries[j].t.Before(entries[i].t) {
entries[i], entries[j] = entries[j], entries[i]
}
}
}
toDrop := len(entries) - poolMax
for i := 0; i < toDrop; i++ {
delete(cp.pool, entries[i].fp)
cp.stats.Evictions++
}
}
func (cp *ConversationPool) pruneLoop() {
ticker := time.NewTicker(5 * time.Minute)
defer ticker.Stop()
for range ticker.C {
cp.mu.Lock()
cp.pruneLocked(time.Now())
cp.mu.Unlock()
}
}
// FingerprintBefore computes the fingerprint for resuming a conversation.
// Hash only user/tool turns (excluding the last one) for lookup.
func FingerprintBefore(messages []ChatMessage, modelKey string) string {
turns := stableTurns(messages)
if len(turns) < 2 {
return ""
}
return hashFingerprint(modelKey, turns[:len(turns)-1])
}
// FingerprintAfter computes the fingerprint after a successful turn.
func FingerprintAfter(messages []ChatMessage, modelKey string) string {
turns := stableTurns(messages)
if len(turns) == 0 {
return ""
}
return hashFingerprint(modelKey, turns)
}
func stableTurns(messages []ChatMessage) []ChatMessage {
var turns []ChatMessage
for _, m := range messages {
if m.Role == "user" || m.Role == "tool" {
turns = append(turns, m)
}
}
return turns
}
func hashFingerprint(modelKey string, turns []ChatMessage) string {
type canonical struct {
Role string `json:"role"`
Content string `json:"content"`
}
cans := make([]canonical, len(turns))
for i, t := range turns {
cans[i] = canonical{Role: t.Role, Content: t.Content}
}
data, _ := json.Marshal(cans)
h := sha256.Sum256([]byte(fmt.Sprintf("%s\x00\x00%s", modelKey, data)))
return fmt.Sprintf("%x", h)
}