返回 DeepSeek-Reasonix
httpexec.go
根目录 / internal / browser / httpexec.go
1 package browser
2
3 import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "errors"
8 "fmt"
9 "io"
10 "net/http"
11 "strings"
12 "sync"
13 "time"
14 )
15
16 const httpHealthTTL = 30 * time.Second
17
18 // httpExecutor is the JSON-over-HTTP client half of the contract. It never
19 // retries: a write whose reply was lost is reported as ErrUnknownOutcome, and
20 // the tools tell the model not to try again.
21 type httpExecutor struct {
22 endpoint string
23 token string
24 client *http.Client
25 now func() time.Time
26
27 mu sync.Mutex
28 healthyAt time.Time
29 }
30
31 // NewHTTPExecutor returns an Executor that forwards every call to the
32 // broker at endpoint with a bearer token. A nil client uses
33 // http.DefaultClient; the caller decides timeouts through ctx.
34 func NewHTTPExecutor(endpoint, token string, client *http.Client) Executor {
35 if client == nil {
36 client = http.DefaultClient
37 }
38 return &httpExecutor{
39 endpoint: strings.TrimRight(strings.TrimSpace(endpoint), "/"),
40 token: strings.TrimSpace(token),
41 client: client,
42 now: time.Now,
43 }
44 }
45
46 // Available reports whether a health probe succeeded within the last 30 s,
47 // probing again when the cache is cold or expired.
48 func (e *httpExecutor) Available(ctx context.Context) bool {
49 e.mu.Lock()
50 fresh := !e.healthyAt.IsZero() && e.now().Sub(e.healthyAt) < httpHealthTTL
51 e.mu.Unlock()
52 if fresh {
53 return true
54 }
55 req, err := e.newRequest(ctx, http.MethodGet, e.endpoint+httpHealthRoute, nil)
56 if err != nil {
57 return false
58 }
59 resp, err := e.client.Do(req)
60 if err != nil {
61 return false
62 }
63 _, _ = io.Copy(io.Discard, io.LimitReader(resp.Body, 4<<10))
64 _ = resp.Body.Close()
65 if resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusOK {
66 return false
67 }
68 e.mu.Lock()
69 e.healthyAt = e.now()
70 e.mu.Unlock()
71 return true
72 }
73
74 func (e *httpExecutor) newRequest(ctx context.Context, method, url string, body []byte) (*http.Request, error) {
75 req, err := http.NewRequestWithContext(ctx, method, url, bytes.NewReader(body))
76 if err != nil {
77 return nil, err
78 }
79 req.Header.Set("Authorization", "Bearer "+e.token)
80 if body != nil {
81 req.Header.Set("Content-Type", "application/json")
82 }
83 if id := SessionFromContext(ctx); id != "" {
84 req.Header.Set(SessionHeader, id)
85 }
86 return req, nil
87 }
88
89 // call posts in as JSON to /v1/browser/<method> and decodes the reply into
90 // out. Transport failures (no HTTP reply at all) come back as errTransport
91 // so Act can turn them into ErrUnknownOutcome.
92 func (e *httpExecutor) call(ctx context.Context, method string, in, out any) error {
93 body, err := json.Marshal(in)
94 if err != nil {
95 return fmt.Errorf("browser broker: encode %s: %w", method, err)
96 }
97 req, err := e.newRequest(ctx, http.MethodPost, e.endpoint+httpRoutePrefix+method, body)
98 if err != nil {
99 return fmt.Errorf("browser broker: %s: %w", method, err)
100 }
101 resp, err := e.client.Do(req)
102 if err != nil {
103 return &transportError{method: method, err: err}
104 }
105 defer resp.Body.Close()
106 data, err := io.ReadAll(io.LimitReader(resp.Body, httpMaxResponseBytes+1))
107 if err != nil {
108 return &transportError{method: method, err: err}
109 }
110 if len(data) > httpMaxResponseBytes {
111 return fmt.Errorf("browser broker: %s: reply exceeds %d bytes", method, httpMaxResponseBytes)
112 }
113 if resp.StatusCode == http.StatusConflict {
114 return decodeWireError(method, data)
115 }
116 if resp.StatusCode != http.StatusOK {
117 return fmt.Errorf("browser broker: %s: status %d: %s", method, resp.StatusCode, wireMessage(data))
118 }
119 if out == nil {
120 return nil
121 }
122 if err := json.Unmarshal(data, out); err != nil {
123 return fmt.Errorf("browser broker: decode %s reply: %w", method, err)
124 }
125 return nil
126 }
127
128 type transportError struct {
129 method string
130 err error
131 }
132
133 func (t *transportError) Error() string { return "browser broker: " + t.method + ": " + t.err.Error() }
134 func (t *transportError) Unwrap() error { return t.err }
135
136 // wireMessage prefers the handler's message over a raw body dump.
137 func wireMessage(data []byte) string {
138 var we wireError
139 if err := json.Unmarshal(data, &we); err == nil && we.Message != "" {
140 return we.Message
141 }
142 return strings.TrimSpace(string(data))
143 }
144
145 func decodeWireError(method string, data []byte) error {
146 var we wireError
147 if err := json.Unmarshal(data, &we); err != nil || we.Error == "" {
148 return fmt.Errorf("browser broker: %s: status 409: %s", method, strings.TrimSpace(string(data)))
149 }
150 sentinel, ok := wireErrorCodes[we.Error]
151 if !ok {
152 return fmt.Errorf("browser broker: %s: %s: %s", method, we.Error, we.Message)
153 }
154 detail := strings.TrimPrefix(strings.TrimPrefix(we.Message, sentinel.Error()), ": ")
155 if detail == "" {
156 return sentinel
157 }
158 return fmt.Errorf("%w: %s", sentinel, detail)
159 }
160
161 func (e *httpExecutor) Tabs(ctx context.Context) ([]Tab, error) {
162 var out wireTabs
163 if err := e.call(ctx, "tabs", struct{}{}, &out); err != nil {
164 return nil, err
165 }
166 tabs := make([]Tab, 0, len(out.Tabs))
167 for _, t := range out.Tabs {
168 tabs = append(tabs, t.tab())
169 }
170 return tabs, nil
171 }
172
173 func (e *httpExecutor) Open(ctx context.Context, req OpenRequest) (Tab, error) {
174 var out wireTab
175 if err := e.write(ctx, "open", wireOpenRequest(req), &out); err != nil {
176 return Tab{}, err
177 }
178 return out.tab(), nil
179 }
180
181 func (e *httpExecutor) Navigate(ctx context.Context, req NavigateRequest) (Tab, error) {
182 var out wireTab
183 if err := e.write(ctx, "navigate", wireNavigateRequest(req), &out); err != nil {
184 return Tab{}, err
185 }
186 return out.tab(), nil
187 }
188
189 func (e *httpExecutor) Snapshot(ctx context.Context, req SnapshotRequest) (Snapshot, error) {
190 var out wireSnapshot
191 if err := e.call(ctx, "snapshot", wireSnapshotRequest(req), &out); err != nil {
192 return Snapshot{}, err
193 }
194 return Snapshot(out), nil
195 }
196
197 func (e *httpExecutor) Screenshot(ctx context.Context, req ScreenshotRequest) (Screenshot, error) {
198 var out wireScreenshot
199 if err := e.call(ctx, "screenshot", wireScreenshotRequest(req), &out); err != nil {
200 return Screenshot{}, err
201 }
202 return Screenshot(out), nil
203 }
204
205 // Act sends one reserved write. A reply that never arrived leaves the
206 // action's fate unknown, which is exactly ErrUnknownOutcome.
207 func (e *httpExecutor) Act(ctx context.Context, req ActRequest) (ActResult, error) {
208 var out wireActResult
209 if err := e.write(ctx, "act", toWireAct(req), &out); err != nil {
210 if errors.Is(err, ErrUnknownOutcome) {
211 return ActResult{Outcome: OutcomeUnknown}, err
212 }
213 return ActResult{}, err
214 }
215 res := ActResult(out)
216 if res.Outcome == "" {
217 res.Outcome = OutcomeNotExecuted
218 if res.Executed {
219 res.Outcome = OutcomeExecuted
220 }
221 }
222 return res, nil
223 }
224
225 func (e *httpExecutor) Downloads(ctx context.Context, req DownloadsRequest) ([]Download, error) {
226 var out wireDownloads
227 in := wireDownloadsRequest{TabID: req.TabID, WaitForMs: req.WaitFor.Milliseconds()}
228 if err := e.call(ctx, "downloads", in, &out); err != nil {
229 return nil, err
230 }
231 downloads := make([]Download, 0, len(out.Downloads))
232 for _, d := range out.Downloads {
233 downloads = append(downloads, Download(d))
234 }
235 return downloads, nil
236 }
237
238 func (e *httpExecutor) Close(ctx context.Context, req CloseRequest) error {
239 return e.write(ctx, "close", wireCloseRequest(req), nil)
240 }
241
242 // Once a write is handed to HTTP, only explicit refusal codes prove it did
243 // not run. Truncated/invalid replies and HTTP failures also leave it unknown.
244 func (e *httpExecutor) write(ctx context.Context, method string, in, out any) error {
245 err := e.call(ctx, method, in, out)
246 if err == nil || errors.Is(err, ErrStaleReference) || errors.Is(err, ErrTakenOver) || errors.Is(err, ErrNoGrant) || errors.Is(err, ErrUnknownOutcome) {
247 return err
248 }
249 return fmt.Errorf("%w: %s", ErrUnknownOutcome, err.Error())
250 }
251
251 lines GO