返回 DeepSeek-Reasonix
submit_request.go
根目录 / internal / serve / submit_request.go
1 package serve
2
3 import (
4 "encoding/json"
5 "net/http"
6 "strings"
7
8 "reasonix/internal/control"
9 )
10
11 // submit runs raw user input as a turn (slash commands and @-references
12 // resolved by the controller). Returns 202 — output arrives on the event stream.
13 // An optional "format":"json_object" asks the model for structured JSON output
14 // on this turn (text.format on the wire).
15 type submitRequest struct {
16 ModelApplication *control.ModelApplicationChoice `json:"modelApplication,omitempty"`
17 SubmissionID string `json:"submissionId"`
18 Input string `json:"input"`
19 Format string `json:"format"`
20 Action string `json:"action"`
21 RecoveryID string `json:"recoveryId"`
22 }
23
24 func decodeSubmitRequest(w http.ResponseWriter, r *http.Request) (submitRequest, string, bool) {
25 var body submitRequest
26 if err := json.NewDecoder(r.Body).Decode(&body); err != nil || (body.Input == "" && body.Action != control.ProtocolRecoveryAction) {
27 http.Error(w, "missing input", http.StatusBadRequest)
28 return submitRequest{}, "", false
29 }
30 if len(body.SubmissionID) > 256 {
31 http.Error(w, "submissionId is too long", http.StatusBadRequest)
32 return submitRequest{}, "", false
33 }
34 body.Format = strings.TrimSpace(body.Format)
35 body.Action = strings.TrimSpace(body.Action)
36 if body.Action == control.ProtocolRecoveryAction && strings.TrimSpace(body.RecoveryID) == "" {
37 http.Error(w, "missing recoveryId", http.StatusBadRequest)
38 return submitRequest{}, "", false
39 }
40 switch body.Format {
41 case "", "json_object":
42 // Supported: empty = default text output, json_object = structured.
43 default:
44 http.Error(w, `unsupported format (supported: "json_object")`, http.StatusBadRequest)
45 return submitRequest{}, "", false
46 }
47 if err := validateSubmitAction(body.Format, body.Action); err != nil {
48 http.Error(w, err.Error(), http.StatusBadRequest)
49 return submitRequest{}, "", false
50 }
51 trimmed := strings.TrimSpace(body.Input)
52 // Typed recovery guidance is never dispatched as a management command.
53 if body.Action != "" {
54 trimmed = ""
55 }
56 if strings.HasPrefix(trimmed, "!") {
57 http.Error(w, "shell commands are unavailable over HTTP", http.StatusForbidden)
58 return submitRequest{}, "", false
59 }
60 if namesMigrationSource(trimmed) {
61 http.Error(w, "a migration source directory is unavailable over HTTP", http.StatusForbidden)
62 return submitRequest{}, "", false
63 }
64 return body, trimmed, true
65 }
66
67 // namesMigrationSource reports a /migrate that picks its own source: a host
68 // directory to read and import, which only a local frontend may choose.
69 func namesMigrationSource(input string) bool {
70 fields := strings.Fields(input)
71 return len(fields) > 1 && (fields[0] == "/migrate" || fields[0] == "/migration")
72 }
73
73 lines GO