Files
LoveLosita 26c350f378 Version: 0.4.4.dev.260307
feat: 🚀 增强会话管理与缓存机制

* 会话 ID 空值兜底,若 `conversation_id` 为空时自动生成 UUID
* 在响应头写入 `X-Conversation-ID`,供前端使用,保持同一会话状态

perf:  会话状态缓存优化

* 当缓存未命中但 DB 已确认/创建会话后,调用 `SetConversationStatus` 回写 Redis
* 缓存写回失败时记录日志,不中断聊天主流程,确保业务流畅性

fix: 🐛 修复历史消息顺序问题与编译错误

* 修复历史消息顺序问题,保证返回的 N 条历史消息按时间正序喂给模型

  * 通过反转 `created_at desc` 查询结果的切片,确保模型输入顺序正确
* 修复 `fmt.Errorf` 参数不匹配问题,修正编译错误
* 整理 `agent-cache.go` 为标准 UTF-8 编码,避免 Go 编译报错 `invalid UTF-8 encoding`

feat: 🛠️ 独立构建 MCP 服务器

* 使用 `Codex` 构建独立于后端的 MCP 服务器,简化与 Codex 的协作
* 通过该服务器方便 Codex 直接测试和查看 Redis 与 MySQL 中的数据
2026-03-07 15:25:40 +08:00

190 lines
4.5 KiB
Go

package store
import (
"context"
"fmt"
"strings"
"time"
cfgpkg "github.com/LoveLosita/smartflow/infra/smartflow-mcp-server/internal/config"
"github.com/go-redis/redis/v8"
)
type RedisClient struct {
client *redis.Client
}
type RedisGetResult struct {
Exists bool `json:"exists"`
Key string `json:"key"`
Type string `json:"type"`
Value any `json:"value,omitempty"`
Truncated bool `json:"truncated"`
DurationMs int64 `json:"durationMs"`
}
type RedisScanResult struct {
Pattern string `json:"pattern"`
Keys []string `json:"keys"`
Returned int `json:"returned"`
NextCursor uint64 `json:"nextCursor"`
Truncated bool `json:"truncated"`
DurationMs int64 `json:"durationMs"`
}
func NewRedisClient(ctx context.Context, cfg cfgpkg.RedisConfig) (*RedisClient, error) {
client := redis.NewClient(&redis.Options{
Addr: cfg.Addr,
Password: cfg.Password,
DB: cfg.DB,
})
pingCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
if err := client.Ping(pingCtx).Err(); err != nil {
_ = client.Close()
return nil, fmt.Errorf("ping redis: %w", err)
}
return &RedisClient{client: client}, nil
}
func (c *RedisClient) Close() error {
if c == nil || c.client == nil {
return nil
}
return c.client.Close()
}
func (c *RedisClient) GetWithType(ctx context.Context, key string, maxItems int, maxStringBytes int) (RedisGetResult, error) {
start := time.Now()
t, err := c.client.Type(ctx, key).Result()
if err != nil {
return RedisGetResult{}, err
}
if t == "none" {
return RedisGetResult{
Exists: false,
Key: key,
Type: "none",
Truncated: false,
DurationMs: time.Since(start).Milliseconds(),
}, nil
}
result := RedisGetResult{Exists: true, Key: key, Type: t}
switch t {
case "string":
v, err := c.client.Get(ctx, key).Result()
if err != nil {
return RedisGetResult{}, err
}
if len(v) > maxStringBytes {
result.Value = v[:maxStringBytes]
result.Truncated = true
} else {
result.Value = v
}
case "list":
vals, err := c.client.LRange(ctx, key, 0, int64(maxItems-1)).Result()
if err != nil {
return RedisGetResult{}, err
}
result.Value = vals
if length, _ := c.client.LLen(ctx, key).Result(); length > int64(maxItems) {
result.Truncated = true
}
case "set":
vals, err := c.client.SMembers(ctx, key).Result()
if err != nil {
return RedisGetResult{}, err
}
if len(vals) > maxItems {
result.Value = vals[:maxItems]
result.Truncated = true
} else {
result.Value = vals
}
case "zset":
vals, err := c.client.ZRangeWithScores(ctx, key, 0, int64(maxItems-1)).Result()
if err != nil {
return RedisGetResult{}, err
}
resultRows := make([]map[string]any, 0, len(vals))
for _, item := range vals {
resultRows = append(resultRows, map[string]any{"member": item.Member, "score": item.Score})
}
result.Value = resultRows
if length, _ := c.client.ZCard(ctx, key).Result(); length > int64(maxItems) {
result.Truncated = true
}
case "hash":
vals, err := c.client.HGetAll(ctx, key).Result()
if err != nil {
return RedisGetResult{}, err
}
if len(vals) <= maxItems {
result.Value = vals
} else {
trimmed := make(map[string]string, maxItems)
count := 0
for k, v := range vals {
trimmed[k] = v
count++
if count >= maxItems {
break
}
}
result.Value = trimmed
result.Truncated = true
}
default:
raw, err := c.client.Dump(ctx, key).Result()
if err != nil {
return RedisGetResult{}, err
}
result.Value = strings.ToUpper(fmt.Sprintf("UNSUPPORTED_TYPE_%s_DUMP_SIZE_%d", t, len(raw)))
}
result.DurationMs = time.Since(start).Milliseconds()
return result, nil
}
func (c *RedisClient) ScanKeys(ctx context.Context, pattern string, count int64, maxKeys int) (RedisScanResult, error) {
start := time.Now()
if pattern == "" {
pattern = "*"
}
if count <= 0 {
count = 20
}
keys := make([]string, 0, maxKeys)
var cursor uint64
truncated := false
for {
batch, nextCursor, err := c.client.Scan(ctx, cursor, pattern, count).Result()
if err != nil {
return RedisScanResult{}, err
}
for _, key := range batch {
if len(keys) >= maxKeys {
truncated = true
break
}
keys = append(keys, key)
}
cursor = nextCursor
if truncated || cursor == 0 {
break
}
}
return RedisScanResult{
Pattern: pattern,
Keys: keys,
Returned: len(keys),
NextCursor: cursor,
Truncated: truncated,
DurationMs: time.Since(start).Milliseconds(),
}, nil
}