返回 DeepSeek-Reasonix
task_catalog.go
根目录 / desktop / task_catalog.go
1 package main
2
3 import (
4 "fmt"
5 "strings"
6
7 "reasonix/internal/control"
8 "reasonix/internal/taskcatalog"
9 "reasonix/internal/taskmonitor"
10 )
11
12 type TaskPageRequest struct {
13 Scope string `json:"scope"`
14 TabID string `json:"tabId"`
15 ProjectKey string `json:"projectKey"`
16 States []string `json:"states"`
17 Query string `json:"query"`
18 Cursor string `json:"cursor"`
19 Limit int `json:"limit"`
20 }
21
22 type TaskCatalogItem = taskcatalog.Item
23 type TaskCatalogStatus = taskcatalog.Status
24
25 type TaskPage struct {
26 Items []TaskCatalogItem `json:"items"`
27 NextCursor string `json:"nextCursor"`
28 Revision uint64 `json:"revision"`
29 Partial bool `json:"partial"`
30 StaleCursor bool `json:"staleCursor"`
31 Status TaskCatalogStatus `json:"status"`
32 }
33
34 type TaskEventPageRequest struct {
35 ProjectKey string `json:"projectKey"`
36 TaskID string `json:"taskId"`
37 After int `json:"after"`
38 Limit int `json:"limit"`
39 }
40
41 type TaskEventPage = taskcatalog.EventPage
42
43 type TaskActionRequest struct {
44 ProjectKey string `json:"projectKey"`
45 TaskID string `json:"taskId"`
46 ExpectedVersion uint64 `json:"expectedVersion"`
47 Reason string `json:"reason"`
48 IdempotencyKey string `json:"idempotencyKey"`
49 }
50
51 type TaskOpenRequest struct {
52 ProjectKey string `json:"projectKey"`
53 TaskID string `json:"taskId"`
54 }
55
56 func (a *App) GetTaskCatalogStatus() TaskCatalogStatus {
57 if catalog := taskcatalog.Shared(); catalog != nil {
58 return catalog.Status()
59 }
60 return TaskCatalogStatus{State: "opening", Mode: "memory", Pending: 1}
61 }
62
63 func (a *App) taskProjectKeys(req TaskPageRequest) ([]string, string, error) {
64 switch strings.TrimSpace(req.Scope) {
65 case "session":
66 target, err := a.taskMonitorTargetForTab(req.TabID)
67 if err != nil {
68 return nil, "", err
69 }
70 if target.sessionID == "" {
71 return nil, "", fmt.Errorf("session is not ready")
72 }
73 key := taskcatalog.RegisterSharedProject(target.projectDir, workspaceName(target.projectDir))
74 return []string{key}, target.sessionID, nil
75 case "project", "":
76 key := strings.TrimSpace(req.ProjectKey)
77 if key == "" {
78 root := a.projectDir()
79 key = taskcatalog.RegisterSharedProject(root, workspaceName(root))
80 } else if !a.allowedTaskProjectKey(key) {
81 return nil, "", fmt.Errorf("unknown project key")
82 }
83 return []string{key}, "", nil
84 case "all":
85 projects := loadProjectsFile()
86 keys := []string{taskcatalog.RegisterSharedProject(globalWorkspaceRoot(), projects.GlobalTitle)}
87 for _, project := range projects.Projects {
88 keys = append(keys, taskcatalog.RegisterSharedProject(project.Root, projectDisplayName(project)))
89 }
90 return keys, "", nil
91 default:
92 return nil, "", fmt.Errorf("unknown task scope %q", req.Scope)
93 }
94 }
95
96 func (a *App) allowedTaskProjectRoots() []string {
97 roots := []string{globalWorkspaceRoot(), a.projectDir()}
98 for _, project := range loadProjectsFile().Projects {
99 roots = append(roots, project.Root)
100 }
101 return roots
102 }
103
104 func (a *App) allowedTaskProjectKey(key string) bool {
105 _, ok := a.resolveTaskProject(key)
106 return ok
107 }
108
109 // resolveTaskProject maps a project key to an allowlisted workspace root without
110 // consulting the SQLite task catalog. Control actions use FileStore as authority.
111 func (a *App) resolveTaskProject(key string) (taskcatalog.Project, bool) {
112 key = strings.TrimSpace(key)
113 if key == "" {
114 return taskcatalog.Project{}, false
115 }
116 projects := loadProjectsFile()
117 labels := map[string]string{
118 globalWorkspaceRoot(): projects.GlobalTitle,
119 a.projectDir(): workspaceName(a.projectDir()),
120 }
121 for _, project := range projects.Projects {
122 labels[project.Root] = projectDisplayName(project)
123 }
124 for _, root := range a.allowedTaskProjectRoots() {
125 if taskcatalog.ProjectKey(root) != key {
126 continue
127 }
128 label := labels[root]
129 if strings.TrimSpace(label) == "" {
130 label = workspaceName(root)
131 }
132 return taskcatalog.Project{Key: key, Root: root, Label: label}, true
133 }
134 return taskcatalog.Project{}, false
135 }
136
137 func (a *App) ListTaskPage(req TaskPageRequest) TaskPage {
138 status := a.GetTaskCatalogStatus()
139 out := TaskPage{Items: []TaskCatalogItem{}, Status: status, Partial: true}
140 catalog := taskcatalog.Shared()
141 if catalog == nil {
142 return out
143 }
144 keys, sessionID, err := a.taskProjectKeys(req)
145 if err != nil {
146 out.Status.LastError = err.Error()
147 return out
148 }
149 for _, key := range keys {
150 if _, ok, lookupErr := catalog.Project(a.bootContext(), key); lookupErr != nil || !ok {
151 out.Status.LastError = "unknown project key"
152 return out
153 }
154 }
155 page, err := catalog.ListPage(a.bootContext(), taskcatalog.PageRequest{ProjectKeys: keys, SessionID: sessionID,
156 States: req.States, Query: req.Query, Cursor: req.Cursor, Limit: req.Limit})
157 if err != nil {
158 out.Status.LastError = err.Error()
159 return out
160 }
161 a.overlayTaskCatalogRuntime(page.Items)
162 return TaskPage{Items: page.Items, NextCursor: page.NextCursor, Revision: page.Revision,
163 Partial: page.Partial, StaleCursor: page.StaleCursor, Status: page.Status}
164 }
165
166 func (a *App) overlayTaskCatalogRuntime(items []TaskCatalogItem) {
167 type controllerRef struct {
168 projectKey string
169 sessionID string
170 ctrl control.SessionAPI
171 }
172 a.mu.RLock()
173 tabs := a.runtimeTabsLocked()
174 controllers := make([]controllerRef, 0, len(tabs))
175 for _, tab := range tabs {
176 if tab == nil || tab.Ctrl == nil {
177 continue
178 }
179 root := strings.TrimSpace(tab.WorkspaceRoot)
180 if root == "" {
181 root = globalWorkspaceRoot()
182 }
183 controllers = append(controllers, controllerRef{projectKey: taskcatalog.ProjectKey(root), ctrl: tab.Ctrl})
184 }
185 a.mu.RUnlock()
186 for i := range controllers {
187 controllers[i].sessionID = controllerTaskSessionID(controllers[i].ctrl)
188 }
189
190 type runtimeValue struct{ status string }
191 running := map[string]runtimeValue{}
192 for _, controller := range controllers {
193 for _, job := range controller.ctrl.Jobs() {
194 key := controller.projectKey + "\x00" + controller.sessionID + "\x00" + job.ID
195 running[key] = runtimeValue{status: job.Status}
196 }
197 }
198 for i := range items {
199 key := items[i].ProjectKey + "\x00" + items[i].Task.SessionID + "\x00" + items[i].Task.JobID
200 value, ok := running[key]
201 if !ok {
202 continue
203 }
204 items[i].Task.RuntimeState = taskmonitor.RuntimeStateAlive
205 switch value.status {
206 case "running":
207 items[i].Task.State = taskmonitor.TaskStateRunning
208 case "done":
209 items[i].Task.RuntimeState = taskmonitor.RuntimeStateExited
210 items[i].Task.State = taskmonitor.TaskStateSucceeded
211 case "failed":
212 items[i].Task.RuntimeState = taskmonitor.RuntimeStateExited
213 items[i].Task.State = taskmonitor.TaskStateFailed
214 case "killed", "interrupted":
215 items[i].Task.RuntimeState = taskmonitor.RuntimeStateExited
216 items[i].Task.State = taskmonitor.TaskStateCancelled
217 }
218 }
219 }
220
221 func (a *App) ListTaskEventPage(req TaskEventPageRequest) TaskEventPage {
222 out := TaskEventPage{Items: []taskmonitor.TaskEvent{}, NextSequence: req.After}
223 if !a.allowedTaskProjectKey(req.ProjectKey) {
224 out.Partial = true
225 return out
226 }
227 catalog := taskcatalog.Shared()
228 if catalog == nil {
229 out.Partial = true
230 return out
231 }
232 page, err := catalog.ListEventPage(a.bootContext(), req.ProjectKey, req.TaskID, req.After, req.Limit)
233 if err != nil {
234 out.Partial = true
235 return out
236 }
237 return page
238 }
239
240 func (a *App) taskActionProject(key string) (taskcatalog.Project, error) {
241 project, ok := a.resolveTaskProject(key)
242 if !ok {
243 return taskcatalog.Project{}, fmt.Errorf("unknown project key")
244 }
245 // Catalog may supply a display label only. Never override the allowlisted
246 // root — a stale or damaged SQLite projection must not redirect FileStore.
247 if catalog := taskcatalog.Shared(); catalog != nil {
248 if row, found, err := catalog.Project(a.bootContext(), project.Key); err == nil && found {
249 if strings.TrimSpace(row.Label) != "" {
250 project.Label = row.Label
251 }
252 }
253 }
254 return project, nil
255 }
256
257 func (a *App) StopTaskByKey(req TaskActionRequest) (taskmonitor.ControlResult, error) {
258 project, err := a.taskActionProject(req.ProjectKey)
259 if err != nil {
260 return taskmonitor.ControlResult{}, err
261 }
262 return a.taskControl().StopTaskWithKiller(a.bootContext(), project.Root, req.TaskID, req.ExpectedVersion, req.Reason, req.IdempotencyKey,
263 desktopTaskJobKiller{app: a, projectDir: project.Root})
264 }
265
266 func (a *App) CancelTaskByKey(req TaskActionRequest) (taskmonitor.ControlResult, error) {
267 project, err := a.taskActionProject(req.ProjectKey)
268 if err != nil {
269 return taskmonitor.ControlResult{}, err
270 }
271 return a.taskControl().CancelTaskWithKiller(a.bootContext(), project.Root, req.TaskID, req.ExpectedVersion, req.Reason, req.IdempotencyKey,
272 desktopTaskJobKiller{app: a, projectDir: project.Root})
273 }
274
275 func (a *App) RequeueTaskByKey(req TaskActionRequest) (taskmonitor.ControlResult, error) {
276 project, err := a.taskActionProject(req.ProjectKey)
277 if err != nil {
278 return taskmonitor.ControlResult{}, err
279 }
280 return a.taskControl().RequeueTask(a.bootContext(), project.Root, req.TaskID, req.ExpectedVersion, req.IdempotencyKey)
281 }
282
283 func (a *App) OpenTaskSessionByKey(req TaskOpenRequest) (taskmonitor.ControlResult, error) {
284 project, err := a.taskActionProject(req.ProjectKey)
285 if err != nil {
286 return taskmonitor.ControlResult{}, err
287 }
288 return a.taskControl().OpenTaskSession(a.bootContext(), project.Root, req.TaskID)
289 }
290
291 func (a *App) RebuildTaskCatalog() error {
292 if a == nil || a.shuttingDown.Load() {
293 return fmt.Errorf("application is shutting down")
294 }
295 projects := loadProjectsFile()
296 items := []taskcatalog.Project{{Root: globalWorkspaceRoot(), Label: projects.GlobalTitle}}
297 for _, project := range projects.Projects {
298 items = append(items, taskcatalog.Project{Root: project.Root, Label: projectDisplayName(project)})
299 }
300 return taskcatalog.RebuildSharedCatalog(a.bootContext(), items)
301 }
302
302 lines GO