返回 DeepSeek-Reasonix
goal_diagnostics_export.go
根目录 / desktop / goal_diagnostics_export.go
1 package main
2
3 import (
4 "bytes"
5 "errors"
6 "fmt"
7 "io"
8 "net/http"
9 "os"
10 "path/filepath"
11 "strings"
12
13 "reasonix/desktop/internal/hostrpc"
14 "reasonix/internal/control"
15 "reasonix/internal/fileutil"
16 "reasonix/internal/servecontract"
17 )
18
19 // ExportGoalDiagnostics lets the user save the authoritative event stream,
20 // including tool calls/results and the exact goal/runtime identity, without
21 // relying on the frontend's paginated transcript.
22 func (a *App) ExportGoalDiagnostics() (string, error) {
23 tab, api := a.activeTabAndCtrl()
24 if tab == nil {
25 return "", errors.New("goal diagnostics are unavailable for this session")
26 }
27 // New peers and local canonical sessions share the explicit export owner.
28 if !a.isRemoteTab(tab.ID) || a.remoteSessionExportSupported(tab.ID) {
29 handle, err := a.BeginSessionExportForTarget(SessionSelector{}, tab.ID, "diagnostic", tab.TopicTitle, "")
30 if err != nil || handle.ExportID == "" {
31 return "", err
32 }
33 defer func() { _ = a.CancelSessionExport(handle.ExportID) }()
34 result, err := a.FinishSessionExport(handle.ExportID)
35 if err != nil {
36 return "", err
37 }
38 if len(result.Paths) == 0 {
39 return "", nil
40 }
41 return result.Paths[0], nil
42 }
43 path, err := a.nativeHost().SaveFileDialog(a.ctx, nativeDialogOptions{
44 Title: "Export goal diagnostics",
45 DefaultDirectory: dialogDefaultDirectory(tab.WorkspaceRoot),
46 DefaultFilename: safeExportFilename(tab.TopicTitle + "-goal-diagnostics.json"),
47 CanCreateDirectories: true,
48 Filters: exportFileFilters("application/json", ".json"),
49 })
50 if err != nil || path == "" {
51 return "", err
52 }
53 if filepath.Ext(path) == "" {
54 path += ".json"
55 }
56 ctrl, local := api.(*control.Controller)
57 if local && ctrl != nil {
58 err = writeGoalDiagnosticsFile(path, func(dst io.Writer) error {
59 return ctrl.WriteGoalDiagnostics(a.ctx, dst, control.GoalDiagnosticMetadata{
60 ApplicationVersion: version,
61 BuildCommit: buildCommit(),
62 ProtocolVersion: hostrpc.ProtocolVersion,
63 Capabilities: []string{"session-history-v1", "session-identity-v1", servecontract.GoalLifecycleV2},
64 })
65 })
66 } else if a.isRemoteTab(tab.ID) {
67 err = writeGoalDiagnosticsFile(path, func(dst io.Writer) error {
68 return a.writeRemoteGoalDiagnostics(tab.ID, dst)
69 })
70 } else {
71 err = errors.New("goal diagnostics are unavailable for this session")
72 }
73 if err != nil {
74 return "", err
75 }
76 return path, nil
77 }
78
79 func (a *App) exportRemoteGoalDiagnostics(tabID string) ([]byte, error) {
80 var output bytes.Buffer
81 if err := a.writeRemoteGoalDiagnostics(tabID, &output); err != nil {
82 return nil, err
83 }
84 return output.Bytes(), nil
85 }
86
87 func (a *App) writeRemoteGoalDiagnostics(tabID string, dst io.Writer) error {
88 if err := a.requireRemoteGoalLifecycle(tabID); err != nil {
89 return err
90 }
91 client, base, expectedPath, err := a.remoteTabCommandTarget(tabID)
92 if err != nil {
93 return err
94 }
95 resp, err := serveDoForSession(a.ctx, client, http.MethodGet, serveURL(base, "/goal-diagnostics"), nil, expectedPath)
96 if err != nil {
97 return err
98 }
99 defer resp.Body.Close()
100 if resp.StatusCode < 200 || resp.StatusCode >= 300 {
101 data, _ := io.ReadAll(io.LimitReader(resp.Body, 16<<10))
102 return fmt.Errorf("export remote goal diagnostics: status %d: %s", resp.StatusCode, string(data))
103 }
104 _, err = io.Copy(dst, resp.Body)
105 if err != nil {
106 return fmt.Errorf("read remote goal diagnostics: %w", err)
107 }
108 return nil
109 }
110
111 func writeGoalDiagnosticsFile(path string, write func(io.Writer) error) error {
112 if strings.TrimSpace(path) == "" {
113 return nil
114 }
115 tmp, err := os.CreateTemp(filepath.Dir(path), ".reasonix-goal-diagnostics-*")
116 if err != nil {
117 return exportOperationError("save goal diagnostics", path, err)
118 }
119 tmpPath := tmp.Name()
120 keep := false
121 defer func() {
122 _ = tmp.Close()
123 if !keep {
124 _ = os.Remove(tmpPath)
125 }
126 }()
127 if err := tmp.Chmod(0o644); err != nil {
128 return exportOperationError("save goal diagnostics", path, err)
129 }
130 if err := write(tmp); err != nil {
131 return exportOperationError("save goal diagnostics", path, err)
132 }
133 if err := tmp.Sync(); err != nil {
134 return exportOperationError("save goal diagnostics", path, err)
135 }
136 if err := tmp.Close(); err != nil {
137 return exportOperationError("save goal diagnostics", path, err)
138 }
139 if err := fileutil.ReplaceFile(tmpPath, path); err != nil {
140 return exportOperationError("save goal diagnostics", path, err)
141 }
142 keep = true
143 return nil
144 }
145
146 func (a *App) remoteSessionExportSupported(tabID string) bool {
147 a.remoteTabMu.Lock()
148 defer a.remoteTabMu.Unlock()
149 tab := a.remoteTabs[tabID]
150 return tab != nil && tab.capabilities[servecontract.SessionExportV1]
151 }
152
152 lines GO