返回 DeepSeek-Reasonix
titlecache.go
根目录 / internal / serve / titlecache.go
1 package serve
2
3 import (
4 "crypto/sha256"
5 "encoding/hex"
6 "encoding/json"
7 "os"
8 "path/filepath"
9 "sync"
10
11 fileencoding "reasonix/internal/fileutil/encoding"
12 )
13
14 // titleCache persists generated session titles to <dir>/.session-titles.json.
15 // Entries are keyed by file name and the first user message: appending turns
16 // changes the transcript mtime without invalidating the title, while replacing
17 // the first turn (for example by rewinding turn zero) produces a cache miss.
18 // Persistence is best-effort: a missing or unreadable cache just regenerates.
19 type titleCache struct {
20 mu sync.Mutex
21 dir string
22 loaded bool
23 entries map[string]titleEntry
24 }
25
26 type titleEntry struct {
27 Title string `json:"title"`
28 Mod int64 `json:"mod"`
29 SourceHash string `json:"source_hash,omitempty"`
30 }
31
32 func newTitleCache(dir string) *titleCache {
33 return &titleCache{dir: dir, entries: map[string]titleEntry{}}
34 }
35
36 func (c *titleCache) load() {
37 if c.loaded {
38 return
39 }
40 c.loaded = true
41 if data, err := fileencoding.ReadFileUTF8(filepath.Join(c.dir, ".session-titles.json")); err == nil {
42 _ = json.Unmarshal(data, &c.entries)
43 }
44 }
45
46 func titleSourceHash(source string) string {
47 sum := sha256.Sum256([]byte(source))
48 return hex.EncodeToString(sum[:])
49 }
50
51 func (c *titleCache) get(name, source string, mod int64) (string, bool) {
52 c.mu.Lock()
53 defer c.mu.Unlock()
54 c.load()
55 e, ok := c.entries[name]
56 if !ok {
57 return "", false
58 }
59 if e.SourceHash == "" {
60 // Legacy entries used only mtime. Accept a still-current entry without
61 // rewriting the cache; the next transcript append regenerates once and
62 // upgrades it to source_hash automatically.
63 if e.Mod == mod {
64 return e.Title, true
65 }
66 return "", false
67 }
68 if e.SourceHash == titleSourceHash(source) {
69 return e.Title, true
70 }
71 return "", false
72 }
73
74 func (c *titleCache) put(name, title, source string, mod int64) {
75 c.mu.Lock()
76 defer c.mu.Unlock()
77 c.load()
78 c.entries[name] = titleEntry{Title: title, Mod: mod, SourceHash: titleSourceHash(source)}
79 if data, err := json.Marshal(c.entries); err == nil {
80 _ = os.WriteFile(filepath.Join(c.dir, ".session-titles.json"), data, 0o644)
81 }
82 }
83
83 lines GO