返回 DeepSeek-Reasonix
session_markdown_export.go
根目录 / desktop / session_markdown_export.go
1 package main
2
3 import (
4 "bufio"
5 "context"
6 "fmt"
7 "io"
8 "os"
9 "path/filepath"
10 "reasonix/internal/sessionexport"
11 "strings"
12
13 "reasonix/internal/fileutil"
14 )
15
16 // SaveSessionMarkdownForTab writes the complete host-side transcript without
17 // transferring or joining it in the renderer. The destination is published
18 // only after the buffered stream has been flushed and synced.
19 func (a *App) SaveSessionMarkdownForTab(tabID, path, title string) error {
20 if strings.TrimSpace(path) == "" {
21 return nil
22 }
23 query, ref, err := a.canonicalSessionQuery(tabID)
24 if err != nil {
25 return err
26 }
27 snapshot, err := query.CaptureExportSnapshot(context.Background(), ref)
28 if err != nil {
29 return err
30 }
31 snapshot.Title = title
32 dir, err := os.MkdirTemp("", "reasonix-markdown-")
33 if err != nil {
34 return err
35 }
36 defer os.RemoveAll(dir)
37 if _, err = sessionexport.Build(context.Background(), query, snapshot, dir, nil); err != nil {
38 return err
39 }
40 return writeStreamingExport(path, func(dst io.Writer) error {
41 file, err := os.Open(filepath.Join(dir, "markdown"))
42 if err != nil {
43 return err
44 }
45 defer file.Close()
46 _, err = io.Copy(dst, file)
47 return err
48 })
49 }
50
51 func writeSessionMarkdown(dst io.Writer, title string, messages []HistoryMessage) error {
52 writer := bufio.NewWriterSize(dst, 64<<10)
53 heading := strings.TrimSpace(strings.ReplaceAll(strings.ReplaceAll(title, "\r", " "), "\n", " "))
54 if heading == "" {
55 heading = "Reasonix session"
56 }
57 if _, err := fmt.Fprintf(writer, "# %s\n\n", heading); err != nil {
58 return err
59 }
60 for _, message := range messages {
61 if err := writeSessionMarkdownMessage(writer, message); err != nil {
62 return err
63 }
64 }
65 return writer.Flush()
66 }
67
68 func writeSessionMarkdownMessage(dst io.Writer, message HistoryMessage) error {
69 writeSection := func(heading, body string) error {
70 body = strings.TrimSpace(body)
71 if body == "" {
72 return nil
73 }
74 _, err := fmt.Fprintf(dst, "%s\n\n%s\n\n", heading, body)
75 return err
76 }
77 switch message.Role {
78 case "user":
79 return writeSection("## User", message.Content)
80 case "assistant":
81 if err := writeSection("### Reasoning", message.Reasoning); err != nil {
82 return err
83 }
84 if err := writeSection("## Assistant", message.Content); err != nil {
85 return err
86 }
87 for _, call := range message.ToolCalls {
88 if _, err := fmt.Fprintf(dst, "### Tool: %s\n\n", call.Name); err != nil {
89 return err
90 }
91 if err := writeMarkdownFence(dst, "Args", call.Arguments); err != nil {
92 return err
93 }
94 }
95 case "tool":
96 if err := writeMarkdownFence(dst, "Output", message.Content); err != nil {
97 return err
98 }
99 return writeMarkdownFence(dst, "Error", message.ToolResultError)
100 case "notice":
101 return writeSection("### Notice", strings.TrimSpace(message.Content+"\n"+message.Detail))
102 case "phase":
103 return writeSection("### Phase", message.Content)
104 }
105 return nil
106 }
107
108 func writeMarkdownFence(dst io.Writer, label, body string) error {
109 if body == "" {
110 return nil
111 }
112 fence := sessionexport.Fence(body)
113 _, err := fmt.Fprintf(dst, "%s\n%s\n%s\n%s\n\n", label, fence, body, fence)
114 return err
115 }
116
117 func writeStreamingExport(path string, write func(io.Writer) error) error {
118 tmp, err := os.CreateTemp(filepath.Dir(path), ".reasonix-session-export-*")
119 if err != nil {
120 return exportOperationError("save session export", path, err)
121 }
122 tmpPath := tmp.Name()
123 keep := false
124 defer func() {
125 _ = tmp.Close()
126 if !keep {
127 _ = os.Remove(tmpPath)
128 }
129 }()
130 if err := tmp.Chmod(0o644); err != nil {
131 return exportOperationError("save session export", path, err)
132 }
133 if err := write(tmp); err != nil {
134 return exportOperationError("save session export", path, err)
135 }
136 if err := tmp.Sync(); err != nil {
137 return exportOperationError("save session export", path, err)
138 }
139 if err := tmp.Close(); err != nil {
140 return exportOperationError("save session export", path, err)
141 }
142 if err := fileutil.ReplaceFile(tmpPath, path); err != nil {
143 return exportOperationError("save session export", path, err)
144 }
145 keep = true
146 return nil
147 }
148
148 lines GO