// Package history 是会话短期多轮历史的存储后端(Redis CacheDB)。 // 与长期偏好记忆(Postgres)分开:历史是易失的、按会话滚动保留最近 N 轮。 package history import ( "context" "encoding/json" "log" "time" "github.com/redis/go-redis/v9" ) // maxTurns 是单会话保留的最近消息条数(user/assistant 各算一条)。 const maxTurns = 20 // Turn 是一条历史消息。 type Turn struct { Role string `json:"role"` // user / assistant Content string `json:"content"` } // Store 封装会话历史读写。rdb 为 nil 表示降级(无 Redis 时历史为空,不阻断)。 type Store struct{ rdb *redis.Client } // Open 连接 Redis。失败不 fatal:返回降级实例。 func Open(addr string) *Store { rdb := redis.NewClient(&redis.Options{Addr: addr}) ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() if err := rdb.Ping(ctx).Err(); err != nil { log.Printf("[history] redis 不可用,会话历史降级(为空): %v", err) _ = rdb.Close() return &Store{} } log.Println("[history] redis connected") return &Store{rdb: rdb} } func key(session string) string { return "sundynix:history:" + session } // Get 返回某会话最近的历史(按时间正序)。 func (s *Store) Get(ctx context.Context, session string) ([]Turn, error) { if s.rdb == nil || session == "" { return nil, nil } // 列表头插尾老:取全部后反转为正序。 vals, err := s.rdb.LRange(ctx, key(session), 0, maxTurns-1).Result() if err != nil { return nil, err } turns := make([]Turn, 0, len(vals)) for i := len(vals) - 1; i >= 0; i-- { // 反转 → 正序 var t Turn if json.Unmarshal([]byte(vals[i]), &t) == nil { turns = append(turns, t) } } return turns, nil } // Append 追加一条消息并裁剪到最近 maxTurns 条,刷新 TTL。 func (s *Store) Append(ctx context.Context, session, role, content string) error { if s.rdb == nil || session == "" { return nil } data, err := json.Marshal(Turn{Role: role, Content: content}) if err != nil { return err } pipe := s.rdb.TxPipeline() pipe.LPush(ctx, key(session), data) pipe.LTrim(ctx, key(session), 0, maxTurns-1) pipe.Expire(ctx, key(session), 24*time.Hour) _, err = pipe.Exec(ctx) return err } // Close 释放连接。 func (s *Store) Close() { if s.rdb != nil { _ = s.rdb.Close() } }