返回 DeepSeek-Reasonix
project_topic_group_filter.go
根目录 / desktop / project_topic_group_filter.go
1 package main
2
3 import (
4 "crypto/sha256"
5 "encoding/json"
6 "fmt"
7 "slices"
8 "strings"
9 )
10
11 func resolveProjectTopicGroupFilter(req *ProjectTopicPageRequest) error {
12 requestedFilter := strings.TrimSpace(strings.ToLower(req.GroupFilter))
13 filter := requestedFilter
14 if filter == "" {
15 filter = "all"
16 }
17 if filter != "all" && filter != "ungrouped" && filter != "group" {
18 return fmt.Errorf("invalid project topic group filter %q", req.GroupFilter)
19 }
20 if filter == "all" {
21 req.GroupFilter = "all"
22 // An omitted filter keeps the legacy unbound cursor contract. Every
23 // caller that opts into the new sidebar fields receives a cursor bound
24 // to the complete list identity, including project, query and sort.
25 if requestedFilter != "" || req.ExcludePinned {
26 req.groupCursorBind = projectTopicCursorBinding(*req, filter, "", 0)
27 }
28 return nil
29 }
30
31 f := loadProjectsFile()
32 groups := f.GlobalGroups
33 revision := f.GlobalGroupsRevision
34 if strings.TrimSpace(req.Scope) == "project" {
35 index := projectIndexByRoot(f.Projects, req.WorkspaceRoot)
36 if index >= 0 {
37 groups = f.Projects[index].Groups
38 revision = f.Projects[index].GroupsRevision
39 } else {
40 groups = nil
41 }
42 }
43 groups = normalizeGroups(groups)
44 req.GroupFilter = filter
45 req.groupAll = append([]desktopGroup(nil), groups...)
46
47 if filter == "group" {
48 groupID := strings.TrimSpace(req.GroupID)
49 if groupID == "" {
50 return fmt.Errorf("project topic group id is required")
51 }
52 for _, group := range groups {
53 if group.ID != groupID {
54 continue
55 }
56 selected := group
57 req.groupSelected = &selected
58 if !groupHasSessionRules(group) {
59 req.groupIncludeJSON, req.groupInclude = topicIDFilter(group.TopicIDs)
60 }
61 req.groupCursorBind = projectTopicCursorBinding(*req, filter, groupID, revision)
62 return nil
63 }
64 return fmt.Errorf("project topic group %q no longer exists", groupID)
65 }
66
67 allGrouped := make([]string, 0)
68 for _, group := range groups {
69 allGrouped = append(allGrouped, group.TopicIDs...)
70 }
71 if !groupsHaveSessionRules(groups) {
72 req.groupExcludeJSON, req.groupExclude = topicIDFilter(allGrouped)
73 }
74 req.groupCursorBind = projectTopicCursorBinding(*req, filter, "", revision)
75 return nil
76 }
77
78 func projectTopicCursorBinding(req ProjectTopicPageRequest, filter, groupID string, membershipRevision uint64) string {
79 payload, _ := json.Marshal([]any{
80 "project-topics-v1",
81 strings.TrimSpace(strings.ToLower(req.Scope)),
82 strings.TrimSpace(req.WorkspaceRoot),
83 strings.TrimSpace(strings.ToLower(req.Query)),
84 strings.TrimSpace(strings.ToLower(req.TimeFilter)),
85 strings.TrimSpace(strings.ToLower(req.SortMode)),
86 filter,
87 strings.TrimSpace(groupID),
88 membershipRevision,
89 req.ExcludePinned,
90 req.pinnedOnly,
91 })
92 digest := sha256.Sum256(payload)
93 return fmt.Sprintf("%x", digest[:])
94 }
95
96 func topicIDFilter(ids []string) (string, map[string]struct{}) {
97 set := make(map[string]struct{}, len(ids))
98 ordered := make([]string, 0, len(ids))
99 for _, id := range ids {
100 id = strings.TrimSpace(id)
101 if id == "" {
102 continue
103 }
104 if _, exists := set[id]; exists {
105 continue
106 }
107 set[id] = struct{}{}
108 ordered = append(ordered, id)
109 }
110 encoded, _ := json.Marshal(ordered)
111 return string(encoded), set
112 }
113
114 func projectTopicRequestAllows(req ProjectTopicPageRequest, topicID string, pinned bool) bool {
115 if req.pinnedOnly && !pinned {
116 return false
117 }
118 if req.ExcludePinned && pinned {
119 return false
120 }
121 if req.groupInclude != nil {
122 _, ok := req.groupInclude[topicID]
123 return ok
124 }
125 if req.groupExclude != nil {
126 _, excluded := req.groupExclude[topicID]
127 return !excluded
128 }
129 return true
130 }
131
132 func groupHasSessionRules(group desktopGroup) bool {
133 return len(group.SessionKeys) > 0 || len(group.ExcludedSessionKeys) > 0
134 }
135
136 func groupsHaveSessionRules(groups []desktopGroup) bool {
137 return slices.ContainsFunc(groups, groupHasSessionRules)
138 }
139
140 func projectNodeSessionKey(node ProjectNode) string {
141 if node.Session != nil && strings.TrimSpace(node.Session.SessionID) != "" {
142 hostID := strings.TrimSpace(node.Session.HostID)
143 if hostID == "" {
144 hostID = localDesktopHostID
145 }
146 return "ref\x00" + hostID + "\x00" + strings.TrimSpace(node.Session.SessionID)
147 }
148 if node.Source != nil && node.Source.SourceKey != "" {
149 host := node.Source.HostID
150 if host == "" {
151 host = localDesktopHostID
152 }
153 return "source\x00" + host + "\x00" + node.Source.SourceKey
154 }
155 if path := strings.TrimSpace(node.SessionPath); path != "" {
156 return "path\x00" + path
157 }
158 return "topic\x00" + firstNonEmpty(strings.TrimSpace(node.TopicID), strings.TrimSpace(node.Key))
159 }
160
161 func desktopGroupContainsNode(group desktopGroup, node ProjectNode) bool {
162 key := projectNodeSessionKey(node)
163 for _, excluded := range group.ExcludedSessionKeys {
164 if strings.TrimSpace(excluded) == key {
165 return false
166 }
167 }
168 for _, explicit := range group.SessionKeys {
169 if strings.TrimSpace(explicit) == key {
170 return true
171 }
172 }
173 for _, topicID := range group.TopicIDs {
174 if strings.TrimSpace(topicID) != "" && strings.TrimSpace(topicID) == strings.TrimSpace(node.TopicID) {
175 return true
176 }
177 }
178 return false
179 }
180
181 func projectNodeRequestAllows(req ProjectTopicPageRequest, node ProjectNode) bool {
182 if req.pinnedOnly && !node.Pinned {
183 return false
184 }
185 if req.ExcludePinned && node.Pinned {
186 return false
187 }
188 switch req.GroupFilter {
189 case "group":
190 return req.groupSelected != nil && desktopGroupContainsNode(*req.groupSelected, node)
191 case "ungrouped":
192 for _, group := range req.groupAll {
193 if desktopGroupContainsNode(group, node) {
194 return false
195 }
196 }
197 return true
198 default:
199 return true
200 }
201 }
202
202 lines GO