返回 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 })
91 digest := sha256.Sum256(payload)
92 return fmt.Sprintf("%x", digest[:])
93 }
94
95 func topicIDFilter(ids []string) (string, map[string]struct{}) {
96 set := make(map[string]struct{}, len(ids))
97 ordered := make([]string, 0, len(ids))
98 for _, id := range ids {
99 id = strings.TrimSpace(id)
100 if id == "" {
101 continue
102 }
103 if _, exists := set[id]; exists {
104 continue
105 }
106 set[id] = struct{}{}
107 ordered = append(ordered, id)
108 }
109 encoded, _ := json.Marshal(ordered)
110 return string(encoded), set
111 }
112
113 func projectTopicRequestAllows(req ProjectTopicPageRequest, topicID string, pinned bool) bool {
114 if req.ExcludePinned && pinned {
115 return false
116 }
117 if req.groupInclude != nil {
118 _, ok := req.groupInclude[topicID]
119 return ok
120 }
121 if req.groupExclude != nil {
122 _, excluded := req.groupExclude[topicID]
123 return !excluded
124 }
125 return true
126 }
127
128 func groupHasSessionRules(group desktopGroup) bool {
129 return len(group.SessionKeys) > 0 || len(group.ExcludedSessionKeys) > 0
130 }
131
132 func groupsHaveSessionRules(groups []desktopGroup) bool {
133 return slices.ContainsFunc(groups, groupHasSessionRules)
134 }
135
136 func projectNodeSessionKey(node ProjectNode) string {
137 if node.Session != nil && strings.TrimSpace(node.Session.SessionID) != "" {
138 hostID := strings.TrimSpace(node.Session.HostID)
139 if hostID == "" {
140 hostID = localDesktopHostID
141 }
142 return "ref\x00" + hostID + "\x00" + strings.TrimSpace(node.Session.SessionID)
143 }
144 if node.Source != nil && node.Source.SourceKey != "" {
145 host := node.Source.HostID
146 if host == "" {
147 host = localDesktopHostID
148 }
149 return "source\x00" + host + "\x00" + node.Source.SourceKey
150 }
151 if path := strings.TrimSpace(node.SessionPath); path != "" {
152 return "path\x00" + path
153 }
154 return "topic\x00" + firstNonEmpty(strings.TrimSpace(node.TopicID), strings.TrimSpace(node.Key))
155 }
156
157 func desktopGroupContainsNode(group desktopGroup, node ProjectNode) bool {
158 key := projectNodeSessionKey(node)
159 for _, excluded := range group.ExcludedSessionKeys {
160 if strings.TrimSpace(excluded) == key {
161 return false
162 }
163 }
164 for _, explicit := range group.SessionKeys {
165 if strings.TrimSpace(explicit) == key {
166 return true
167 }
168 }
169 for _, topicID := range group.TopicIDs {
170 if strings.TrimSpace(topicID) != "" && strings.TrimSpace(topicID) == strings.TrimSpace(node.TopicID) {
171 return true
172 }
173 }
174 return false
175 }
176
177 func projectNodeRequestAllows(req ProjectTopicPageRequest, node ProjectNode) bool {
178 if req.ExcludePinned && node.Pinned {
179 return false
180 }
181 switch req.GroupFilter {
182 case "group":
183 return req.groupSelected != nil && desktopGroupContainsNode(*req.groupSelected, node)
184 case "ungrouped":
185 for _, group := range req.groupAll {
186 if desktopGroupContainsNode(group, node) {
187 return false
188 }
189 }
190 return true
191 default:
192 return true
193 }
194 }
195
195 lines GO