返回 DeepSeek-Reasonix
inbox.go
根目录 / internal / serve / inbox.go
1 package serve
2
3 import (
4 "encoding/json"
5 "errors"
6 "net/http"
7 "strings"
8
9 "reasonix/internal/control"
10 "reasonix/internal/sessioninbox"
11 )
12
13 func (s *Server) registerInboxRoutes(mux *http.ServeMux) {
14 mux.HandleFunc("GET /inbox", s.inboxList)
15 mux.HandleFunc("GET /inbox/receipt", s.inboxReceipt)
16 mux.HandleFunc("POST /inbox/items", s.foregroundMutation(s.inboxEnqueue))
17 mux.HandleFunc("GET /inbox/items/{id}", s.inboxGet)
18 mux.HandleFunc("PATCH /inbox/items/{id}", s.foregroundMutation(s.inboxUpdate))
19 mux.HandleFunc("DELETE /inbox/items/{id}", s.foregroundMutation(s.inboxDelete))
20 mux.HandleFunc("POST /inbox/move", s.foregroundMutation(s.inboxMove))
21 mux.HandleFunc("POST /inbox/pause", s.foregroundMutation(s.inboxPause))
22 mux.HandleFunc("POST /inbox/resume", s.foregroundMutation(s.inboxResume))
23 mux.HandleFunc("POST /inbox/items/{id}/retry", s.foregroundMutation(s.inboxRetry))
24 mux.HandleFunc("POST /inbox/items/{id}/refresh", s.foregroundMutation(s.inboxRefresh))
25 }
26
27 func (s *Server) inboxAPI() control.SessionAPI {
28 return s.ctl()
29 }
30
31 func writeInboxError(w http.ResponseWriter, err error) {
32 switch {
33 case errors.Is(err, sessioninbox.ErrItemTooLarge):
34 http.Error(w, err.Error(), http.StatusRequestEntityTooLarge) // 413
35 case errors.Is(err, sessioninbox.ErrCapacityItems), errors.Is(err, sessioninbox.ErrCapacityBytes),
36 errors.Is(err, sessioninbox.ErrInvalidState), errors.Is(err, sessioninbox.ErrPaused),
37 errors.Is(err, sessioninbox.ErrNotFound), errors.Is(err, sessioninbox.ErrIdempotencyConflict):
38 http.Error(w, err.Error(), http.StatusConflict) // 409
39 case errors.Is(err, sessioninbox.ErrEmpty):
40 http.Error(w, err.Error(), http.StatusBadRequest)
41 default:
42 http.Error(w, err.Error(), http.StatusInternalServerError)
43 }
44 }
45
46 func (s *Server) inboxList(w http.ResponseWriter, r *http.Request) {
47 s.bindMu.Lock()
48 defer s.bindMu.Unlock()
49 if !s.validateInboxReadSessionLocked(w, r) {
50 return
51 }
52 snap := s.inboxAPI().InboxSnapshot()
53 w.Header().Set("Content-Type", "application/json")
54 _ = json.NewEncoder(w).Encode(snap)
55 }
56
57 // validateInboxReadSessionLocked keeps legacy unscoped reads compatible while
58 // fencing modern Desktop reads against a concurrent foreground replacement.
59 func (s *Server) validateInboxReadSessionLocked(w http.ResponseWriter, r *http.Request) bool {
60 if !s.validateExpectedSessionLocked(w, r) {
61 return false
62 }
63 if err := s.expectedSessionPathErrorLocked(r.URL.Query().Get("session")); err != nil {
64 http.Error(w, err.Error(), http.StatusConflict)
65 return false
66 }
67 return true
68 }
69
70 func (s *Server) inboxEnqueue(w http.ResponseWriter, r *http.Request) {
71 var body struct {
72 Input string `json:"input"`
73 Display string `json:"display"`
74 Invocations []control.InvocationRequest `json:"invocations"`
75 Intent string `json:"intent"`
76 IdempotencyKey string `json:"idempotencyKey"`
77 }
78 if err := json.NewDecoder(r.Body).Decode(&body); err != nil || strings.TrimSpace(body.Input) == "" {
79 http.Error(w, "missing input", http.StatusBadRequest)
80 return
81 }
82 intent := sessioninbox.IntentFollowup
83 if strings.EqualFold(body.Intent, "steer") {
84 intent = sessioninbox.IntentSteer
85 }
86 api := s.inboxAPI()
87 if ensurer, ok := any(api).(interface{ EnsureSessionPath() }); ok {
88 ensurer.EnsureSessionPath()
89 }
90 req := control.InboxRequest{
91 Intent: intent,
92 Display: body.Display,
93 Raw: body.Input,
94 Submit: body.Input,
95 Source: "http",
96 Idempotency: body.IdempotencyKey,
97 Invocations: body.Invocations,
98 }
99 if req.Display == "" {
100 req.Display = body.Input
101 }
102 var rec sessioninbox.InboxReceipt
103 var err error
104 if intent == sessioninbox.IntentSteer {
105 rec, err = api.TryEnqueueAndSteer(req)
106 } else {
107 rec, err = api.TryEnqueueFollowup(req)
108 }
109 if err != nil {
110 writeInboxError(w, err)
111 return
112 }
113 w.Header().Set("Content-Type", "application/json")
114 w.WriteHeader(http.StatusAccepted)
115 _ = json.NewEncoder(w).Encode(rec)
116 }
117
118 func (s *Server) inboxReceipt(w http.ResponseWriter, r *http.Request) {
119 s.bindMu.Lock()
120 defer s.bindMu.Unlock()
121 if !s.validateInboxReadSessionLocked(w, r) {
122 return
123 }
124 ctrl := s.ctl()
125 reader, ok := ctrl.(interface {
126 LookupInboxReceipt(string) (sessioninbox.InboxReceipt, bool, error)
127 })
128 if !ok {
129 http.NotFound(w, r)
130 return
131 }
132 receipt, found, err := reader.LookupInboxReceipt(r.URL.Query().Get("key"))
133 if err != nil {
134 writeInboxError(w, err)
135 return
136 }
137 if !found {
138 http.NotFound(w, r)
139 return
140 }
141 writeJSON(w, receipt)
142 }
143
144 func (s *Server) inboxGet(w http.ResponseWriter, r *http.Request) {
145 id := r.PathValue("id")
146 meta, env, err := s.inboxAPI().ReadInboxItem(id)
147 if err != nil {
148 writeInboxError(w, err)
149 return
150 }
151 w.Header().Set("Content-Type", "application/json")
152 _ = json.NewEncoder(w).Encode(map[string]any{"meta": meta, "envelope": env})
153 }
154
155 func (s *Server) inboxUpdate(w http.ResponseWriter, r *http.Request) {
156 id := r.PathValue("id")
157 var body struct {
158 Input string `json:"input"`
159 }
160 if err := json.NewDecoder(r.Body).Decode(&body); err != nil || strings.TrimSpace(body.Input) == "" {
161 http.Error(w, "missing input", http.StatusBadRequest)
162 return
163 }
164 meta, err := s.inboxAPI().UpdateInboxItem(id, body.Input, body.Input, body.Input)
165 if err != nil {
166 writeInboxError(w, err)
167 return
168 }
169 w.Header().Set("Content-Type", "application/json")
170 _ = json.NewEncoder(w).Encode(meta)
171 }
172
173 func (s *Server) inboxDelete(w http.ResponseWriter, r *http.Request) {
174 id := r.PathValue("id")
175 if err := s.inboxAPI().DeleteInboxItem(id); err != nil {
176 writeInboxError(w, err)
177 return
178 }
179 w.WriteHeader(http.StatusNoContent)
180 }
181
182 func (s *Server) inboxMove(w http.ResponseWriter, r *http.Request) {
183 var body struct {
184 ID string `json:"id"`
185 ToIndex int `json:"toIndex"`
186 }
187 if err := json.NewDecoder(r.Body).Decode(&body); err != nil || body.ID == "" {
188 http.Error(w, "missing id", http.StatusBadRequest)
189 return
190 }
191 if err := s.inboxAPI().MoveInboxItem(body.ID, body.ToIndex); err != nil {
192 writeInboxError(w, err)
193 return
194 }
195 w.WriteHeader(http.StatusNoContent)
196 }
197
198 func (s *Server) inboxPause(w http.ResponseWriter, r *http.Request) {
199 _ = r
200 if err := s.inboxAPI().SetInboxPaused(true); err != nil {
201 writeInboxError(w, err)
202 return
203 }
204 w.WriteHeader(http.StatusNoContent)
205 }
206
207 func (s *Server) inboxResume(w http.ResponseWriter, r *http.Request) {
208 _ = r
209 if err := s.inboxAPI().SetInboxPaused(false); err != nil {
210 writeInboxError(w, err)
211 return
212 }
213 w.WriteHeader(http.StatusNoContent)
214 }
215
216 func (s *Server) inboxRetry(w http.ResponseWriter, r *http.Request) {
217 id := r.PathValue("id")
218 if err := s.inboxAPI().RetryInboxItem(id); err != nil {
219 writeInboxError(w, err)
220 return
221 }
222 w.WriteHeader(http.StatusNoContent)
223 }
224
225 func (s *Server) inboxRefresh(w http.ResponseWriter, r *http.Request) {
226 id := r.PathValue("id")
227 if err := s.inboxAPI().RefreshInboxReferences(id); err != nil {
228 writeInboxError(w, err)
229 return
230 }
231 w.WriteHeader(http.StatusNoContent)
232 }
233
233 lines GO