返回 DeepSeek-Reasonix
remote_fork_targets.go
根目录 / desktop / remote_fork_targets.go
1 package main
2
3 import (
4 "encoding/json"
5 "errors"
6 "fmt"
7 "io"
8 "net/http"
9 "strings"
10 )
11
12 func (a *App) forkTargetsCapable(tabID string) bool {
13 a.remoteTabMu.Lock()
14 defer a.remoteTabMu.Unlock()
15 return remoteForkTargetsSupported(a.remoteTabs[tabID])
16 }
17
18 func remoteForkTargetsSupported(tab *remoteTab) bool {
19 return tab != nil && tab.capabilities[serveCapabilitySessionForkTargetsV1]
20 }
21
22 const remoteForkUnsupported = "this remote Reasonix Serve does not support " + serveCapabilitySessionForkTargetsV1 + "; upgrade it to fork a remote turn into an independent session"
23
24 func (a *App) ForkTargetsRemoteTab(tabID string) (ForkTargetSetView, error) {
25 empty := ForkTargetSetView{Targets: []ForkTargetView{}}
26 if !a.forkTargetsCapable(tabID) {
27 return empty, nil
28 }
29 a.remoteTabMu.Lock()
30 tab := a.remoteTabs[tabID]
31 if tab == nil || tab.client == nil || tab.state != "ready" || tab.routing.currentPath == "" {
32 a.remoteTabMu.Unlock()
33 return empty, fmt.Errorf("remote tab %q is unavailable", tabID)
34 }
35 client, base, expectedPath, generation := tab.client, tab.base, tab.routing.currentPath, tab.gen
36 a.remoteTabMu.Unlock()
37 ctx, cancel := commandContext(a)
38 defer cancel()
39 resp, err := serveDoForSession(ctx, client, http.MethodGet, serveURL(base, "/fork-targets"), nil, expectedPath)
40 if err != nil {
41 return empty, err
42 }
43 defer resp.Body.Close()
44 body, _ := io.ReadAll(io.LimitReader(resp.Body, serveSnapshotMaxBytes+1))
45 if resp.StatusCode < 200 || resp.StatusCode >= 300 {
46 return empty, &serveHTTPStatusError{url: serveURL(base, "/fork-targets"), statusCode: resp.StatusCode, message: strings.TrimSpace(string(body))}
47 }
48 var wire struct {
49 Source struct {
50 HostID string `json:"hostId"`
51 SessionID string `json:"sessionId"`
52 } `json:"source"`
53 Targets []struct {
54 TurnID string `json:"turnId"`
55 BoundarySequence uint64 `json:"boundarySequence"`
56 TurnNumber int `json:"turnNumber"`
57 Status string `json:"status"`
58 MessageID string `json:"messageId"`
59 Available bool `json:"available"`
60 Reason string `json:"reason"`
61 } `json:"targets"`
62 Verifiable bool `json:"verifiable"`
63 }
64 if err := json.Unmarshal(body, &wire); err != nil {
65 return empty, fmt.Errorf("decode remote fork targets: %w", err)
66 }
67 a.remoteTabMu.Lock()
68 current := a.remoteTabs[tabID]
69 stale := current != tab || current.client != client || current.base != base || current.gen != generation || current.routing.currentPath != expectedPath
70 a.remoteTabMu.Unlock()
71 expectedID, hasID := strings.CutPrefix(expectedPath, remoteSessionIDRoutePrefix)
72 if stale || !hasID || strings.TrimSpace(wire.Source.SessionID) == "" || wire.Source.SessionID != expectedID {
73 return empty, fmt.Errorf("stale_source")
74 }
75 view := ForkTargetSetView{SourceHostID: wire.Source.HostID, SourceSessionID: wire.Source.SessionID,
76 SessionGeneration: generation, Targets: make([]ForkTargetView, 0, len(wire.Targets)), Verifiable: wire.Verifiable}
77 for _, target := range wire.Targets {
78 view.Targets = append(view.Targets, ForkTargetView{SourceHostID: wire.Source.HostID,
79 SourceSessionID: wire.Source.SessionID, SessionGeneration: generation, TurnID: target.TurnID,
80 BoundarySequence: target.BoundarySequence, TurnNumber: target.TurnNumber, Status: target.Status,
81 MessageID: target.MessageID, Available: target.Available, Reason: target.Reason})
82 }
83 return view, nil
84 }
85
86 func (a *App) CreateForkRemoteTab(tabID string, anchor ForkAnchorView) (ForkCreationView, error) {
87 if strings.TrimSpace(anchor.TurnID) == "" {
88 return ForkCreationView{}, fmt.Errorf("forking a remote turn requires a turn id")
89 }
90 if !a.forkTargetsCapable(tabID) {
91 return ForkCreationView{Code: "fork_unavailable", Reason: "unsupported", Error: remoteForkUnsupported}, nil
92 }
93 a.remoteTabMu.Lock()
94 tab := a.remoteTabs[tabID]
95 currentID := ""
96 if tab != nil {
97 currentID, _ = strings.CutPrefix(tab.routing.currentPath, remoteSessionIDRoutePrefix)
98 }
99 stale := tab == nil || tab.gen != anchor.SessionGeneration || currentID != strings.TrimSpace(anchor.SourceSessionID)
100 a.remoteTabMu.Unlock()
101 if stale {
102 return ForkCreationView{Code: "fork_unavailable", Reason: "stale_source", Error: "fork source session changed"}, nil
103 }
104 operation, err := a.beginForkOperation(forkOperation{Surface: "remote", TabID: tabID,
105 SourceHostID: anchor.SourceHostID, SourceSessionID: anchor.SourceSessionID,
106 TurnID: strings.TrimSpace(anchor.TurnID), BoundarySequence: anchor.BoundarySequence})
107 if err != nil {
108 return ForkCreationView{}, err
109 }
110 if operation.State == "completed" && operation.ChildSessionID != "" {
111 return ForkCreationView{SessionID: operation.ChildSessionID, OperationID: operation.OperationID, Opened: true}, nil
112 }
113 var view ForkCreationView
114 err = a.remoteTabPostJSON(tabID, "/fork-session", map[string]any{
115 "sourceSessionId": operation.SourceSessionID, "turnId": operation.TurnID,
116 "boundarySequence": operation.BoundarySequence, "name": "", "operationId": operation.OperationID,
117 }, &view)
118 if err != nil {
119 var statusErr *serveHTTPStatusError
120 if errors.As(err, &statusErr) {
121 var refusal struct{ Code, Reason, Message string }
122 if json.Unmarshal([]byte(statusErr.message), &refusal) == nil && refusal.Code != "" {
123 if statusErr.statusCode >= 400 && statusErr.statusCode < 500 {
124 _ = a.discardForkOperation(operation.OperationID)
125 }
126 return ForkCreationView{Code: refusal.Code, Reason: refusal.Reason, Error: refusal.Message}, nil
127 }
128 if statusErr.statusCode >= 400 && statusErr.statusCode < 500 {
129 _ = a.discardForkOperation(operation.OperationID)
130 }
131 return ForkCreationView{Error: statusErr.message}, nil
132 }
133 return ForkCreationView{}, err
134 }
135 view.OperationID = operation.OperationID
136 if strings.TrimSpace(view.SessionID) == "" {
137 return ForkCreationView{}, fmt.Errorf("remote fork response has no child session id")
138 }
139 if err := a.completeForkOperation(operation.OperationID, view.SessionID); err != nil {
140 return ForkCreationView{}, err
141 }
142 view.Opened = true
143 return view, nil
144 }
145
145 lines GO