package agent import ( "crypto/rand" "encoding/hex" "encoding/json" "errors" "os" "path/filepath" "sort" "strings" "sync" "time" "unicode/utf8" ) type SessionStore struct { mu sync.RWMutex path string sessions map[string]*Session order []string } func NewSessionStore(dataDir string) (*SessionStore, error) { if err := os.MkdirAll(dataDir, 0o755); err != nil { return nil, err } path := filepath.Join(dataDir, "sessions.json") store := &SessionStore{ path: path, sessions: make(map[string]*Session), order: make([]string, 0), } _ = store.load() return store, nil } func (s *SessionStore) load() error { data, err := os.ReadFile(s.path) if err != nil { if errors.Is(err, os.ErrNotExist) { return nil } return err } if len(strings.TrimSpace(string(data))) == 0 { return nil } var sessions map[string]*Session if err := json.Unmarshal(data, &sessions); err != nil { return err } for id, session := range sessions { if session == nil || session.ID == "" { continue } s.sessions[id] = session s.order = append(s.order, id) } s.sortOrder() return nil } func (s *SessionStore) saveLocked() error { data, err := json.MarshalIndent(s.sessions, "", " ") if err != nil { return err } tmp := s.path + ".tmp" if err := os.WriteFile(tmp, data, 0o600); err != nil { return err } return os.Rename(tmp, s.path) } func (s *SessionStore) List() []SessionSummary { s.mu.RLock() defer s.mu.RUnlock() return s.listLocked("", 0, 0) } // Search 按关键词过滤会话(标题或任意消息内容,忽略大小写)。 func (s *SessionStore) Search(query string) []SessionSummary { s.mu.RLock() defer s.mu.RUnlock() return s.listLocked(query, 0, 0) } // listLocked 返回会话摘要列表;query 非空时按关键词过滤,limit>0 时截断。 // 必须持读锁调用。 func (s *SessionStore) listLocked(query string, limit, offset int) []SessionSummary { query = strings.ToLower(strings.TrimSpace(query)) out := make([]SessionSummary, 0, len(s.order)) skip := 0 for _, id := range s.order { session, ok := s.sessions[id] if !ok { continue } if query != "" && !sessionMatches(session, query) { continue } if offset > 0 && skip < offset { skip++ continue } if limit > 0 && len(out) >= limit { break } out = append(out, summarize(session)) } return out } func sessionMatches(session *Session, query string) bool { if strings.Contains(strings.ToLower(session.Title), query) { return true } for _, message := range session.Messages { if message.Content != nil && strings.Contains(strings.ToLower(*message.Content), query) { return true } for _, call := range message.ToolCalls { if strings.Contains(strings.ToLower(call.Function.Name), query) || strings.Contains(strings.ToLower(call.Function.Arguments), query) { return true } } } return false } // Messages 分页返回会话消息(按时间正序),offset 从最新一条往前数, // 即 offset=0 返回最新 limit 条;返回 has_more 表示还有更早的消息。 func (s *SessionStore) Messages(id string, limit, offset int) ([]Message, bool, bool) { s.mu.RLock() defer s.mu.RUnlock() session, ok := s.sessions[id] if !ok { return nil, false, false } total := len(session.Messages) if limit <= 0 { limit = 50 } if offset < 0 { offset = 0 } start := total - offset - limit if start < 0 { start = 0 } messages := make([]Message, 0, limit) for i := start; i < total-offset; i++ { messages = append(messages, cloneMessage(session.Messages[i])) } hasMore := start > 0 return messages, true, hasMore } // Clear 清空会话消息(保留会话本身)。 func (s *SessionStore) Clear(id string) bool { s.mu.Lock() defer s.mu.Unlock() session, ok := s.sessions[id] if !ok { return false } session.Messages = make([]Message, 0) session.UpdatedAt = time.Now() _ = s.saveLocked() return true } // TotalMessages 返回会话消息总数(用于分页计算)。 func (s *SessionStore) TotalMessages(id string) (int, bool) { s.mu.RLock() defer s.mu.RUnlock() session, ok := s.sessions[id] if !ok { return 0, false } return len(session.Messages), true } func cloneMessage(message Message) Message { copy := message if message.Content != nil { content := *message.Content copy.Content = &content } copy.ToolCalls = append([]ToolCall(nil), message.ToolCalls...) return copy } func (s *SessionStore) Get(id string) (*Session, bool) { s.mu.RLock() defer s.mu.RUnlock() session, ok := s.sessions[id] return session, ok } func (s *SessionStore) Create() *Session { s.mu.Lock() defer s.mu.Unlock() session := &Session{ ID: newID(), Title: "新对话", CreatedAt: time.Now(), UpdatedAt: time.Now(), Messages: make([]Message, 0), } s.sessions[session.ID] = session s.order = append([]string{session.ID}, s.order...) _ = s.saveLocked() return session } func (s *SessionStore) Delete(id string) bool { s.mu.Lock() defer s.mu.Unlock() if _, ok := s.sessions[id]; !ok { return false } delete(s.sessions, id) for i, item := range s.order { if item == id { s.order = append(s.order[:i], s.order[i+1:]...) break } } _ = s.saveLocked() return true } func (s *SessionStore) Save(session *Session) { session.UpdatedAt = time.Now() s.mu.Lock() // 存储克隆而非原对象:会话对象可能仍在 Agent 循环中被修改, // 直接存指针会导致 saveLocked 序列化时与其他会话的写入产生数据竞争。 s.sessions[session.ID] = cloneSession(session) s.order = append([]string{session.ID}, removeString(s.order, session.ID)...) err := s.saveLocked() s.mu.Unlock() if err != nil { // A failed save should not break an in-memory conversation. return } } func (s *SessionStore) sortOrder() { sort.SliceStable(s.order, func(i, j int) bool { a, aOK := s.sessions[s.order[i]] b, bOK := s.sessions[s.order[j]] if !aOK || !bOK { return aOK && !bOK } return a.UpdatedAt.After(b.UpdatedAt) }) } func summarize(session *Session) SessionSummary { summary := SessionSummary{ ID: session.ID, Title: session.Title, CreatedAt: session.CreatedAt, UpdatedAt: session.UpdatedAt, MessageCount: len(session.Messages), } for i := len(session.Messages) - 1; i >= 0; i-- { message := session.Messages[i] if message.Role != "user" && message.Role != "assistant" { continue } if message.Content != nil { summary.Preview = truncateRunes(*message.Content, 120) break } } return summary } func newID() string { buf := make([]byte, 12) _, _ = rand.Read(buf) return hex.EncodeToString(buf) } // cloneSession 深拷贝会话对象,保证持久化对象在存储后不再被外部修改, // 从而消除 Agent 循环写入与 saveLocked 序列化之间的数据竞争。 func cloneSession(session *Session) *Session { if session == nil { return nil } clone := *session clone.Messages = make([]Message, len(session.Messages)) for i, message := range session.Messages { copy := message if message.Content != nil { content := *message.Content copy.Content = &content } copy.ToolCalls = append([]ToolCall(nil), message.ToolCalls...) clone.Messages[i] = copy } return &clone } func removeString(items []string, target string) []string { result := make([]string, 0, len(items)) for _, item := range items { if item != target { result = append(result, item) } } return result } func truncateRunes(value string, max int) string { runes := []rune(value) if len(runes) <= max { return value } return string(runes[:max]) + "..." } func firstRunes(value string, max int) string { runes := []rune(strings.TrimSpace(value)) if len(runes) <= max { return string(runes) } return string(runes[:max]) } func countRunes(value string) int { return utf8.RuneCountInString(value) }