返回 DeepSeek-Reasonix
project_tree_organization_store.go
根目录 / desktop / project_tree_organization_store.go
1 package main
2
3 import (
4 "encoding/json"
5 "fmt"
6 "os"
7 "path/filepath"
8 "strings"
9 "sync"
10
11 "reasonix/internal/fileutil"
12 )
13
14 const (
15 desktopProjectOrganizationFile = "desktop-project-tree-organization.json"
16 desktopProjectOrganizationVersion = 2
17 )
18
19 var desktopProjectOrganizationFileMu sync.Mutex
20
21 // Project-tree organization lives in a sidecar older Reasonix builds do not
22 // know about and therefore cannot rewrite. desktop-projects.json retains the
23 // inline fields for one-release interoperability, while this file is the
24 // durable source when a user temporarily downgrades and the old build saves.
25 type desktopProjectOrganization struct {
26 Root string `json:"root,omitempty"`
27 ManualTopicOrder bool `json:"manualTopicOrder,omitempty"`
28 TopicOrder []string `json:"topicOrder,omitempty"`
29 ManualSessionOrder bool `json:"manualSessionOrder,omitempty"`
30 SessionOrder []string `json:"sessionOrder,omitempty"`
31 Groups []desktopGroup `json:"groups"`
32 GroupsRevision uint64 `json:"groupsRevision,omitempty"`
33 }
34
35 type desktopProjectOrganizationFileData struct {
36 Version int `json:"version"`
37 Global desktopProjectOrganization `json:"global"`
38 Projects []desktopProjectOrganization `json:"projects"`
39 }
40
41 func projectsFileHasOrganization(f desktopProjectFile) bool {
42 if f.GlobalManualTopicOrder || len(f.GlobalGroups) > 0 || f.GlobalGroupsRevision > 0 {
43 return true
44 }
45 for _, project := range f.Projects {
46 if project.ManualTopicOrder || len(project.Groups) > 0 || project.GroupsRevision > 0 {
47 return true
48 }
49 }
50 return false
51 }
52
53 func organizationFromProjectsFile(f desktopProjectFile) desktopProjectOrganizationFileData {
54 out := desktopProjectOrganizationFileData{
55 Version: desktopProjectOrganizationVersion,
56 Global: desktopProjectOrganization{
57 ManualTopicOrder: f.GlobalManualTopicOrder,
58 TopicOrder: append([]string(nil), f.GlobalTopics...),
59 ManualSessionOrder: f.GlobalManualSessionOrder,
60 SessionOrder: append([]string(nil), f.GlobalSessionOrder...),
61 Groups: normalizeGroups(f.GlobalGroups),
62 GroupsRevision: f.GlobalGroupsRevision,
63 },
64 Projects: make([]desktopProjectOrganization, 0, len(f.Projects)),
65 }
66 for _, project := range f.Projects {
67 out.Projects = append(out.Projects, desktopProjectOrganization{
68 Root: project.Root,
69 ManualTopicOrder: project.ManualTopicOrder,
70 TopicOrder: append([]string(nil), project.Topics...),
71 ManualSessionOrder: project.ManualSessionOrder,
72 SessionOrder: append([]string(nil), project.SessionOrder...),
73 Groups: normalizeGroups(project.Groups),
74 GroupsRevision: project.GroupsRevision,
75 })
76 }
77 return out
78 }
79
80 func applyPersistedTopicOrder(current, persisted []string) []string {
81 available := make(map[string]bool, len(current))
82 for _, topicID := range current {
83 available[topicID] = true
84 }
85 seen := make(map[string]bool, len(current))
86 out := make([]string, 0, len(current))
87 for _, topicID := range persisted {
88 topicID = strings.TrimSpace(topicID)
89 if topicID == "" || !available[topicID] || seen[topicID] {
90 continue
91 }
92 seen[topicID] = true
93 out = append(out, topicID)
94 }
95 for _, topicID := range current {
96 if !seen[topicID] {
97 seen[topicID] = true
98 out = append(out, topicID)
99 }
100 }
101 return out
102 }
103
104 func groupsWithoutDeletedTopics(groups []desktopGroup, deleted map[string]bool) []desktopGroup {
105 out := normalizeGroups(groups)
106 for index := range out {
107 kept := out[index].TopicIDs[:0]
108 for _, topicID := range out[index].TopicIDs {
109 if !deleted[topicID] {
110 kept = append(kept, topicID)
111 }
112 }
113 out[index].TopicIDs = kept
114 }
115 return out
116 }
117
118 func applyProjectOrganization(f desktopProjectFile, organization desktopProjectOrganizationFileData) desktopProjectFile {
119 deleted := make(map[string]bool, len(f.DeletedTopics))
120 for _, topicID := range f.DeletedTopics {
121 deleted[topicID] = true
122 }
123 f.GlobalManualTopicOrder = organization.Global.ManualTopicOrder
124 f.GlobalManualSessionOrder = organization.Global.ManualSessionOrder
125 f.GlobalSessionOrder = append([]string(nil), organization.Global.SessionOrder...)
126 if f.GlobalManualTopicOrder {
127 f.GlobalTopics = applyPersistedTopicOrder(f.GlobalTopics, organization.Global.TopicOrder)
128 }
129 f.GlobalGroups = groupsWithoutDeletedTopics(organization.Global.Groups, deleted)
130 f.GlobalGroupsRevision = organization.Global.GroupsRevision
131 for _, persisted := range organization.Projects {
132 index := projectIndexByRoot(f.Projects, persisted.Root)
133 if index < 0 {
134 continue
135 }
136 project := &f.Projects[index]
137 project.ManualTopicOrder = persisted.ManualTopicOrder
138 project.ManualSessionOrder = persisted.ManualSessionOrder
139 project.SessionOrder = append([]string(nil), persisted.SessionOrder...)
140 if project.ManualTopicOrder {
141 project.Topics = applyPersistedTopicOrder(project.Topics, persisted.TopicOrder)
142 }
143 project.Groups = groupsWithoutDeletedTopics(persisted.Groups, deleted)
144 project.GroupsRevision = persisted.GroupsRevision
145 }
146 return normalizeProjectsFile(f)
147 }
148
149 func loadProjectOrganizationFile() (desktopProjectOrganizationFileData, bool) {
150 path := filepath.Join(desktopConfigDir(), desktopProjectOrganizationFile)
151 b, err := readFileUTF8(path)
152 if err != nil {
153 return desktopProjectOrganizationFileData{}, false
154 }
155 var organization desktopProjectOrganizationFileData
156 if json.Unmarshal(b, &organization) != nil || organization.Version < 1 || organization.Version > desktopProjectOrganizationVersion {
157 return desktopProjectOrganizationFileData{}, false
158 }
159 if organization.Global.Groups == nil {
160 organization.Global.Groups = []desktopGroup{}
161 }
162 return organization, true
163 }
164
165 func saveProjectOrganizationFile(f desktopProjectFile) error {
166 desktopProjectOrganizationFileMu.Lock()
167 defer desktopProjectOrganizationFileMu.Unlock()
168
169 dir := desktopConfigDir()
170 if err := os.MkdirAll(dir, 0o755); err != nil {
171 return err
172 }
173 path := filepath.Join(dir, desktopProjectOrganizationFile)
174 if existing, err := readFileUTF8(path); err == nil {
175 var header struct {
176 Version int `json:"version"`
177 }
178 if json.Unmarshal(existing, &header) == nil && header.Version > desktopProjectOrganizationVersion {
179 return fmt.Errorf("project organization version %d is newer than supported version %d", header.Version, desktopProjectOrganizationVersion)
180 }
181 }
182 b, err := json.MarshalIndent(organizationFromProjectsFile(f), "", " ")
183 if err != nil {
184 return err
185 }
186 tmp := path + ".tmp"
187 if err := os.WriteFile(tmp, b, 0o644); err != nil {
188 return err
189 }
190 return fileutil.ReplaceFile(tmp, path)
191 }
192
192 lines GO