172 lines
3.8 KiB
Go
172 lines
3.8 KiB
Go
package services
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"sproutclaw-web/internal/db"
|
|
"sproutclaw-web/internal/models"
|
|
)
|
|
|
|
// jsonlLine is a minimal parse of one line in a session JSONL file.
|
|
type jsonlLine struct {
|
|
Type string `json:"type"`
|
|
// session header
|
|
ID string `json:"id"`
|
|
Created string `json:"created"`
|
|
// message
|
|
Role string `json:"role"`
|
|
Content any `json:"content"`
|
|
// session_info
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
// BuildSessionList reads all *.jsonl files in sessionsDir and assembles summaries.
|
|
func BuildSessionList(sessionsDir string, database *db.DB) ([]models.SessionSummary, error) {
|
|
entries, err := os.ReadDir(sessionsDir)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
pinned, _ := database.GetPinnedSessions()
|
|
pinnedSet := map[string]struct{}{}
|
|
for _, p := range pinned {
|
|
pinnedSet[p] = struct{}{}
|
|
}
|
|
|
|
var summaries []models.SessionSummary
|
|
for _, e := range entries {
|
|
if e.IsDir() || !strings.HasSuffix(e.Name(), ".jsonl") {
|
|
continue
|
|
}
|
|
path := filepath.Join(sessionsDir, e.Name())
|
|
sum, err := readSessionSummary(path)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
_, sum.Pinned = pinnedSet[path]
|
|
sum.Path = path
|
|
summaries = append(summaries, sum)
|
|
}
|
|
|
|
// Sort: pinned first, then by modified desc
|
|
sort.SliceStable(summaries, func(i, j int) bool {
|
|
pi, pj := summaries[i].Pinned, summaries[j].Pinned
|
|
if pi != pj {
|
|
return pi
|
|
}
|
|
return summaries[i].Modified > summaries[j].Modified
|
|
})
|
|
return summaries, nil
|
|
}
|
|
|
|
func readSessionSummary(path string) (models.SessionSummary, error) {
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return models.SessionSummary{}, err
|
|
}
|
|
defer f.Close()
|
|
|
|
info, _ := f.Stat()
|
|
modified := ""
|
|
if info != nil {
|
|
modified = info.ModTime().UTC().Format(time.RFC3339)
|
|
}
|
|
|
|
var sum models.SessionSummary
|
|
sum.Modified = modified
|
|
|
|
scanner := bufio.NewScanner(f)
|
|
scanner.Buffer(make([]byte, 2*1024*1024), 2*1024*1024)
|
|
msgCount := 0
|
|
for scanner.Scan() {
|
|
var line jsonlLine
|
|
if err := json.Unmarshal(scanner.Bytes(), &line); err != nil {
|
|
continue
|
|
}
|
|
switch line.Type {
|
|
case "session":
|
|
sum.Created = line.Created
|
|
case "session_info":
|
|
if line.Name != "" {
|
|
sum.Name = line.Name
|
|
}
|
|
case "message":
|
|
msgCount++
|
|
if sum.FirstMessage == "" && line.Role == "user" {
|
|
sum.FirstMessage = extractTextContent(line.Content)
|
|
}
|
|
}
|
|
}
|
|
sum.MessageCount = msgCount
|
|
return sum, nil
|
|
}
|
|
|
|
func extractTextContent(content any) string {
|
|
switch v := content.(type) {
|
|
case string:
|
|
return truncate(v, 120)
|
|
case []any:
|
|
for _, item := range v {
|
|
if m, ok := item.(map[string]any); ok {
|
|
if m["type"] == "text" {
|
|
if t, ok := m["text"].(string); ok {
|
|
return truncate(t, 120)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func truncate(s string, n int) string {
|
|
if len([]rune(s)) <= n {
|
|
return s
|
|
}
|
|
runes := []rune(s)
|
|
return string(runes[:n]) + "…"
|
|
}
|
|
|
|
// ReadSessionMessages reads all lines from a JSONL session file.
|
|
func ReadSessionMessages(path string) ([]json.RawMessage, error) {
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer f.Close()
|
|
|
|
var lines []json.RawMessage
|
|
scanner := bufio.NewScanner(f)
|
|
scanner.Buffer(make([]byte, 4*1024*1024), 4*1024*1024)
|
|
for scanner.Scan() {
|
|
raw := append([]byte(nil), scanner.Bytes()...)
|
|
lines = append(lines, raw)
|
|
}
|
|
return lines, nil
|
|
}
|
|
|
|
// AppendSessionName appends a session_info rename record to the JSONL file.
|
|
func AppendSessionName(path, name string) error {
|
|
f, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, 0o644)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
record := map[string]string{"type": "session_info", "name": name}
|
|
b, err := json.Marshal(record)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = f.Write(append(b, '\n'))
|
|
return err
|
|
}
|