返回 DeepSeek-Reasonix
browser_diagnostic_export.go
根目录 / desktop / browser_diagnostic_export.go
1 package main
2
3 import (
4 "bytes"
5 "context"
6 "crypto/sha256"
7 "encoding/hex"
8 "encoding/json"
9 "errors"
10 "io"
11 "os"
12 "path/filepath"
13 "time"
14
15 "reasonix/desktop/internal/browserops"
16 "reasonix/internal/config"
17 "reasonix/internal/extension/rpcwire"
18 )
19
20 func browserDiagnosticScope(hostID, sessionID string) string {
21 if hostID == "" || sessionID == "" {
22 return ""
23 }
24 data, _ := json.Marshal([]string{hostID, sessionID})
25 hash := sha256.Sum256(data)
26 return hex.EncodeToString(hash[:])
27 }
28
29 func (a *App) browserDiagnosticExport(job *sessionExportJob) map[string]any {
30 out := map[string]any{
31 "schemaVersion": 1, "capturedAt": time.Now().UTC(), "source": "desktop",
32 "toolEvidence": "Browser tool calls, results and errors are in the session commits; their prefix cutoff can precede this runtime capture.",
33 "coverage": "Host/page events are a bounded in-memory window, lost on shell restart. This is not a complete browsing history. Remote sessions include only the browser hosted by this desktop; remote CDP/browser logs are not collected. Legacy unattributed records are omitted.",
34 }
35 if job.browserScope == "" {
36 out["unavailable"] = "No fixed browser diagnostic identity for this session."
37 return out
38 }
39 a.browserExecMu.Lock()
40 ledger := a.browserOps
41 a.browserExecMu.Unlock()
42 var operations browserops.DiagnosticSnapshot
43 var err error
44 if ledger != nil {
45 operations = ledger.Diagnostics(job.browserScope)
46 } else {
47 operations, err = browserops.ReadDiagnostics(filepath.Join(config.MemoryUserDir(), "browser", "operations-v1.json"), job.browserScope)
48 }
49 if err != nil {
50 out["operations"] = map[string]any{"available": false, "reason": "Operation ledger is absent, unreadable, unsupported or exceeds the read budget."}
51 } else {
52 out["operations"] = operations
53 }
54 var host hostRequester
55 if a.hostShell != nil && a.hostShell.server != nil {
56 host = a.hostShell.server
57 }
58 out["host"] = captureBrowserHostDiagnostics(job.ctx, host, job.browserScope)
59 return out
60 }
61
62 func captureBrowserHostDiagnostics(ctx context.Context, host hostRequester, scope string) any {
63 unavailable := func(kind, reason string) any {
64 return map[string]any{"available": false, "kind": kind, "reason": reason}
65 }
66 if host == nil || scope == "" {
67 return unavailable("host_unavailable", "Browser diagnostic host or fixed session identity is unavailable.")
68 }
69 ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
70 defer cancel()
71 var raw json.RawMessage
72 if err := host.Request(ctx, "host/browser.exportDiagnostics", map[string]string{"scope": scope}, &raw); err != nil {
73 if ctx.Err() != nil {
74 return unavailable("capture_interrupted", "Browser diagnostic capture was cancelled or exceeded its three-second deadline.")
75 }
76 var response *rpcwire.ResponseError
77 if errors.As(err, &response) && response.Code == -32601 {
78 return unavailable("capability_unsupported", "This shell does not support browser diagnostic export.")
79 }
80 return unavailable("host_request_failed", "Browser diagnostic host failed or disconnected; no browser action was replayed.")
81 }
82 if len(raw) > 512<<10 || !json.Valid(raw) {
83 return unavailable("invalid_response", "Invalid or oversized host diagnostic response.")
84 }
85 var header struct {
86 Scope string `json:"scope"`
87 Available bool `json:"available"`
88 }
89 if json.Unmarshal(raw, &header) != nil || header.Scope != scope || !header.Available {
90 return unavailable("identity_unverified", "Host diagnostic response is unavailable or its session identity does not match.")
91 }
92 return raw
93 }
94
95 // The remote endpoint intentionally accepts only its original observation
96 // fields. Append desktop evidence locally, preserving its streaming document
97 // and compatibility with older remote services (and their 64 KiB POST limit).
98 func appendBrowserDiagnosticSection(dst io.Writer, section []byte) error {
99 f, ok := dst.(*os.File)
100 if !ok {
101 return errors.New("diagnostic destination is not seekable")
102 }
103 end, err := f.Seek(0, io.SeekCurrent)
104 if err != nil {
105 return err
106 }
107 // Diagnostic producers emit a JSON object with bounded trailing whitespace.
108 start := max(int64(0), end-4096)
109 tail := make([]byte, end-start)
110 if _, err = f.ReadAt(tail, start); err != nil {
111 return err
112 }
113 i := len(tail) - 1
114 for i >= 0 && (tail[i] == ' ' || tail[i] == '\n' || tail[i] == '\r' || tail[i] == '\t') {
115 i--
116 }
117 if i < 0 || tail[i] != '}' {
118 return errors.New("remote diagnostics did not end in a JSON object")
119 }
120 first := make([]byte, min(end, 4096))
121 if _, err = f.ReadAt(first, 0); err != nil {
122 return err
123 }
124 first = bytes.TrimSpace(first)
125 if len(first) == 0 || first[0] != '{' {
126 return errors.New("remote diagnostics is not a JSON object")
127 }
128 prefix := ",\n\"browserDiagnostics\":"
129 if start == 0 && bytes.Equal(bytes.TrimSpace(tail[:i]), []byte("{")) {
130 prefix = "\n\"browserDiagnostics\":"
131 }
132 if _, err = f.Seek(start+int64(i), io.SeekStart); err != nil {
133 return err
134 }
135 if _, err = f.Write(append(append([]byte(prefix), section...), []byte("\n}\n")...)); err != nil {
136 return err
137 }
138 position, err := f.Seek(0, io.SeekCurrent)
139 if err != nil {
140 return err
141 }
142 return f.Truncate(position)
143 }
144
144 lines GO