返回 DeepSeek-Reasonix
submission.go
根目录 / internal / serve / submission.go
1 package serve
2
3 import (
4 "net/http"
5 "reasonix/internal/control"
6 "reasonix/internal/provider"
7 "strings"
8 )
9
10 func (s *Server) submit(w http.ResponseWriter, r *http.Request) {
11 body, trimmed, ok := decodeSubmitRequest(w, r)
12 if !ok {
13 return
14 }
15 // Session rotations must complete while bindMu is held. Controller.Submit
16 // dispatches these verbs asynchronously, which would let a following model,
17 // resume, or extension command cross the rotation generation boundary.
18 switch trimmed {
19 case "/new":
20 s.newSessionFromSubmit(w, r)
21 return
22 case "/clear":
23 s.clearSessionFromSubmit(w, r)
24 return
25 }
26 // Intercept /model <ref> for runtime model switching (the controller's
27 // Submit path only lists models — switching is frontend-specific).
28 if s.submitModelCommand(w, r, trimmed) {
29 return
30 }
31 // Intercept /effort <level> for reasoning effort switching.
32 if strings.HasPrefix(trimmed, "/effort ") {
33 level := strings.TrimSpace(strings.TrimPrefix(trimmed, "/effort"))
34 if level != "" {
35 if err := s.switchEffortExpected(r.Context(), level, r.Header.Get(expectedSessionPathHeader)); err != nil {
36 http.Error(w, err.Error(), runtimeSwitchErrorStatus(err))
37 return
38 }
39 w.WriteHeader(http.StatusNoContent)
40 return
41 }
42 }
43 // Admission and controller replacement share one ownership boundary.
44 s.bindMu.Lock()
45 if !s.admitModelSettingsRunLocked(w, r) {
46 s.bindMu.Unlock()
47 return
48 }
49 ctrl := s.ctl()
50 // Fix false 202 while a turn is active: SubmitHTTPFormat silently drops
51 // concurrent input. Clients must use POST /inbox/items for durable follow-up.
52 identity := control.SubmissionRequest{ID: body.SubmissionID, HTTP: true, Input: body.Input, Format: body.Format, Action: body.Action, RecoveryID: body.RecoveryID}
53 identified, identifiedOK := ctrl.(*control.Controller)
54 if identifiedOK && body.SubmissionID != "" && !isServeManagementCommand(trimmed) {
55 _, found, err := identified.LookupSubmission(identity)
56 if err != nil || found {
57 s.bindMu.Unlock()
58 if err != nil {
59 http.Error(w, err.Error(), http.StatusConflict)
60 } else {
61 w.WriteHeader(http.StatusAccepted)
62 }
63 return
64 }
65 }
66 if ctrl.Running() {
67 s.bindMu.Unlock()
68 http.Error(w, "session is busy; use POST /inbox/items for durable follow-up", http.StatusConflict)
69 return
70 }
71 if body.Action == control.ProtocolRecoveryAction {
72 pending, ok := ctrl.(interface {
73 PendingProtocolRecovery() *provider.ProtocolRecoveryAction
74 })
75 var action *provider.ProtocolRecoveryAction
76 if ok {
77 action = pending.PendingProtocolRecovery()
78 }
79 if action == nil || action.ID != body.RecoveryID {
80 s.bindMu.Unlock()
81 http.Error(w, "protocol recovery is unavailable or stale", http.StatusConflict)
82 return
83 }
84 }
85 if routing, ok := ctrl.(interface{ SetTurnSubmissionID(string) }); ok {
86 routing.SetTurnSubmissionID(body.SubmissionID)
87 }
88 if identifiedOK && !isServeManagementCommand(trimmed) {
89 _, err := identified.SubmitIdentified(identity)
90 if err != nil {
91 s.bindMu.Unlock()
92 http.Error(w, err.Error(), http.StatusConflict)
93 return
94 }
95 s.bindMu.Unlock()
96 w.WriteHeader(http.StatusAccepted)
97 return
98 }
99 submitWithAction(ctrl, body.Input, body.Format, body.Action, body.RecoveryID)
100 if isServeManagementCommand(trimmed) && !ctrl.Running() && !ctrl.RuntimeStatus().PendingPrompt {
101 // Management notices/status are successful non-turn operations.
102 s.bindMu.Unlock()
103 w.WriteHeader(http.StatusNoContent)
104 return
105 }
106 // Legacy clients without receipts still use the synchronous running gate.
107 if !ctrl.Running() && !ctrl.RuntimeStatus().PendingPrompt {
108 s.bindMu.Unlock()
109 http.Error(w, "input was not admitted; session is rotating, closed, or finishing — use POST /inbox/items", http.StatusConflict)
110 return
111 }
112 s.bindMu.Unlock()
113 w.WriteHeader(http.StatusAccepted)
114 }
115
115 lines GO