返回 DeepSeek-Reasonix
session_target_history_legacy.go
根目录 / desktop / session_target_history_legacy.go
1 package main
2
3 import (
4 "encoding/base64"
5 "encoding/json"
6 "path/filepath"
7 "strings"
8
9 "reasonix/internal/history"
10 "reasonix/internal/historycatalog"
11 "reasonix/internal/provider"
12 "reasonix/internal/retrieval"
13 )
14
15 type targetHistorySliceCursor struct {
16 V int `json:"v"`
17 Target string `json:"target"`
18 Cursor string `json:"cursor"`
19 }
20
21 func decodeTargetHistorySliceCursor(raw, target string) (string, error) {
22 raw = strings.TrimSpace(raw)
23 if raw == "" {
24 return "", nil
25 }
26 data, err := base64.RawURLEncoding.DecodeString(raw)
27 if err != nil {
28 return "", newSessionOperationError("stale_cursor", "The session content changed. Reload it and try again.")
29 }
30 var cursor targetHistorySliceCursor
31 if err := json.Unmarshal(data, &cursor); err != nil || cursor.V != 1 || cursor.Target != target {
32 return "", newSessionOperationError("stale_cursor", "The session content changed. Reload it and try again.")
33 }
34 return cursor.Cursor, nil
35 }
36
37 func encodeTargetHistorySliceCursor(target, cursor string) string {
38 if strings.TrimSpace(cursor) == "" {
39 return ""
40 }
41 data, err := json.Marshal(targetHistorySliceCursor{V: 1, Target: target, Cursor: cursor})
42 if err != nil {
43 return ""
44 }
45 return base64.RawURLEncoding.EncodeToString(data)
46 }
47
48 // HistorySliceForTarget reads a bounded history page for either a canonical or
49 // legacy local session without selecting it or creating a controller.
50 func (a *App) HistorySliceForTarget(selector SessionSelector, req HistorySliceRequest) (HistorySlice, error) {
51 target, err := a.resolveSessionTargetWithArchived(selector, true)
52 if err != nil {
53 return emptyHistorySlice(), err
54 }
55 targetKey := target.key()
56 req.Cursor, err = decodeTargetHistorySliceCursor(req.Cursor, targetKey)
57 if err != nil {
58 return emptyHistorySlice(), err
59 }
60 req = normalizeHistorySliceRequest(req)
61 if target.SessionRef.SessionID != "" {
62 page, err := a.canonicalHistorySlice(a.desktopSessionService("").Query(), target.SessionRef, "", "", req)
63 page.NextCursor = encodeTargetHistorySliceCursor(targetKey, page.NextCursor)
64 return page, err
65 }
66 dir, path, err := a.sessionDirForPath(target.SessionPath)
67 if err != nil {
68 return emptyHistorySlice(), newSessionOperationError(sessionOperationTargetNotFound, "The session no longer exists.")
69 }
70 page, err := a.coldHistorySlice(dir, path, req)
71 if err != nil {
72 if strings.Contains(strings.ToLower(err.Error()), "cursor") {
73 return emptyHistorySlice(), newSessionOperationError("stale_cursor", "The session content changed. Reload it and try again.")
74 }
75 return emptyHistorySlice(), err
76 }
77 page.NextCursor = encodeTargetHistorySliceCursor(targetKey, page.NextCursor)
78 return page, nil
79 }
80
81 type targetHistorySearchCursor struct {
82 V int `json:"v"`
83 Revision uint64 `json:"revision"`
84 Path string `json:"path"`
85 Query string `json:"query"`
86 Rank float64 `json:"rank"`
87 Message int `json:"message"`
88 Part int `json:"part"`
89 RowID int64 `json:"rowId"`
90 }
91
92 func encodeTargetHistorySearchCursor(cursor targetHistorySearchCursor) string {
93 data, err := json.Marshal(cursor)
94 if err != nil {
95 return ""
96 }
97 return base64.RawURLEncoding.EncodeToString(data)
98 }
99
100 func decodeTargetHistorySearchCursor(raw, path, query string, revision uint64) (*historycatalog.SearchCursor, error) {
101 raw = strings.TrimSpace(raw)
102 if raw == "" {
103 return nil, nil
104 }
105 data, err := base64.RawURLEncoding.DecodeString(raw)
106 if err != nil {
107 return nil, newSessionOperationError("stale_cursor", "The session content changed. Reload it and try again.")
108 }
109 var cursor targetHistorySearchCursor
110 if err := json.Unmarshal(data, &cursor); err != nil ||
111 cursor.V != 1 || cursor.Revision != revision ||
112 sessionRuntimeKey(cursor.Path) != sessionRuntimeKey(path) ||
113 cursor.Query != strings.TrimSpace(query) {
114 return nil, newSessionOperationError("stale_cursor", "The session content changed. Reload it and try again.")
115 }
116 return &historycatalog.SearchCursor{
117 Rank: cursor.Rank, SessionPath: path, MessageIndex: cursor.Message,
118 PartIndex: cursor.Part, RowID: cursor.RowID,
119 }, nil
120 }
121
122 // SearchHistoryContentForTarget searches the disposable legacy history index
123 // for one exact session. The cursor is bound to target, query, and index
124 // revision, so changing tabs or replaying a cursor against a sibling cannot
125 // change the routed object.
126 func (a *App) SearchHistoryContentForTarget(selector SessionSelector, query, cursor string, limit int) (HistorySearchPage, error) {
127 target, err := a.resolveSessionTargetWithArchived(selector, true)
128 if err != nil {
129 return HistorySearchPage{Items: []HistorySearchHit{}}, err
130 }
131 if target.SessionRef.SessionID != "" {
132 return HistorySearchPage{Items: []HistorySearchHit{}}, newSessionOperationError("unsupported", "Use canonical session search for this session.")
133 }
134 _, path, err := a.sessionDirForPath(target.SessionPath)
135 if err != nil {
136 return HistorySearchPage{Items: []HistorySearchHit{}}, newSessionOperationError(sessionOperationTargetNotFound, "The session no longer exists.")
137 }
138 status := a.GetHistoryIndexStatus()
139 out := HistorySearchPage{Items: []HistorySearchHit{}, Status: status, Revision: status.Revision,
140 Partial: status.State != "ready" || status.Pending > 0 || (status.Total > 0 && status.Indexed < status.Total)}
141 catalog := history.SharedCatalog()
142 query = strings.TrimSpace(query)
143 if catalog == nil || query == "" {
144 return out, nil
145 }
146 after, err := decodeTargetHistorySearchCursor(cursor, path, query, status.Revision)
147 if err != nil {
148 return out, err
149 }
150 if limit <= 0 {
151 limit = 50
152 }
153 if limit > 200 {
154 limit = 200
155 }
156 result, err := catalog.Search(a.bootContext(), historycatalog.SearchRequest{
157 Query: query, SessionPath: path,
158 Kinds: []string{"user_text", "assistant_text", "tool_input", "tool_error"},
159 Limit: limit + 1, After: after,
160 })
161 if err != nil {
162 return out, err
163 }
164 queryTerms, _ := retrieval.QueryTerms(query)
165 _, overlays := a.catalogRuntimeOverlays()
166 loaded := map[string][]provider.Message{}
167 recoveryChecked, recoveryCovered := map[string]bool{}, map[string]bool{}
168 for _, candidate := range result.Items {
169 hit, ok := a.historyHitFromCandidate(
170 HistorySearchRequest{Query: query}, candidate, "", overlays, queryTerms,
171 loaded, recoveryChecked, recoveryCovered, catalog,
172 )
173 if ok {
174 out.Items = append(out.Items, hit)
175 }
176 }
177 if len(result.Items) > limit {
178 last := result.Items[limit-1]
179 out.Items = out.Items[:min(len(out.Items), limit)]
180 out.NextCursor = encodeTargetHistorySearchCursor(targetHistorySearchCursor{
181 V: 1, Revision: status.Revision, Path: filepath.Clean(path), Query: query,
182 Rank: last.Rank, Message: last.MessageIndex, Part: last.PartIndex, RowID: last.RowID,
183 })
184 }
185 return out, nil
186 }
187
187 lines GO