feat(api): CORS-Tür für POST /speak — OPTIONS-Preflight und ACAO aus Config
OPTIONS /speak beantwortet Preflights mit 204 + Allow-Headers/Methods/Max-Age, wenn die Origin in server.allowed_origins steht; ungeliste Origins und leere Liste bekommen keine CORS-Header (Browser blockt). Nur /speak, kein anderer Endpunkt. /status ergänzt additiv cors:[origins] (immer vorhanden, [] wenn zu).
This commit is contained in:
1 parent
cef6e6b9f2
commit
156c20dbb3
5 files changed
+164
-25
No files matched your search
+10
-3
@@ -5,7 +5,9 @@
|
|||||||
// Default 1 Ansage / 30 s; low über Budget wird verworfen, normal parkt,
|
// Default 1 Ansage / 30 s; low über Budget wird verworfen, normal parkt,
|
||||||
// high darf mit queue.allow_high_burst verstoßen) plus Ruhezeiten
|
// high darf mit queue.allow_high_burst verstoßen) plus Ruhezeiten
|
||||||
// (quiet_hours/quiet_mode; now:true bricht bewusst aus beiden aus).
|
// (quiet_hours/quiet_mode; now:true bricht bewusst aus beiden aus).
|
||||||
// CORS/MCP folgen in Task 06.
|
// Task 06 ergänzt die CORS-Tür für POST /speak (server.allowed_origins /
|
||||||
|
// TALK_ALLOWED_ORIGINS; leer = kein CORS) und den MCP-Endpoint POST /mcp
|
||||||
|
// (Streamable HTTP, ein speak-Tool).
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -120,6 +122,7 @@ func run() error {
|
|||||||
Mute: mute,
|
Mute: mute,
|
||||||
Version: versionString(),
|
Version: versionString(),
|
||||||
KnownProviders: provider.Names(),
|
KnownProviders: provider.Names(),
|
||||||
|
AllowedOrigins: cfg.Server.AllowedOrigins,
|
||||||
Provider: func() string {
|
Provider: func() string {
|
||||||
if cur, ok := q.Current(); ok {
|
if cur, ok := q.Current(); ok {
|
||||||
return resolveProviderName(baseChain, cur)
|
return resolveProviderName(baseChain, cur)
|
||||||
@@ -139,9 +142,13 @@ func run() error {
|
|||||||
srv := &http.Server{Handler: handler, ReadHeaderTimeout: 5 * time.Second}
|
srv := &http.Server{Handler: handler, ReadHeaderTimeout: 5 * time.Second}
|
||||||
errCh := make(chan error, 1)
|
errCh := make(chan error, 1)
|
||||||
go func() { errCh <- srv.Serve(ln) }()
|
go func() { errCh <- srv.Serve(ln) }()
|
||||||
log.Printf("listening on http://%s (version %s, audio sink %s, mute=%v, rate-limit %s, high-burst=%v, ruhezeit=%q modus %s)",
|
corsLog := "zu (kein CORS)"
|
||||||
|
if len(cfg.Server.AllowedOrigins) > 0 {
|
||||||
|
corsLog = fmt.Sprintf("%v", cfg.Server.AllowedOrigins)
|
||||||
|
}
|
||||||
|
log.Printf("listening on http://%s (version %s, audio sink %s, mute=%v, rate-limit %s, high-burst=%v, ruhezeit=%q modus %s, cors=%s, mcp=/mcp)",
|
||||||
addr, versionString(), sink.Name(), mute.Muted(),
|
addr, versionString(), sink.Name(), mute.Muted(),
|
||||||
cfg.Queue.RateLimit, cfg.Queue.AllowHighBurst, cfg.QuietHours, cfg.QuietMode)
|
cfg.Queue.RateLimit, cfg.Queue.AllowHighBurst, cfg.QuietHours, cfg.QuietMode, corsLog)
|
||||||
|
|
||||||
sig := make(chan os.Signal, 1)
|
sig := make(chan os.Signal, 1)
|
||||||
signal.Notify(sig, os.Interrupt, syscall.SIGTERM)
|
signal.Notify(sig, os.Interrupt, syscall.SIGTERM)
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CORS-Tür für POST /speak (Task 06) — und NUR für diesen Endpunkt: Ein
|
||||||
|
// Browser (z. B. eine spätere LibreChat-Integration) darf genau die Ansage-
|
||||||
|
// Schnittstelle cross-origin rufen, keine Steuer-Endpunkte. Die Erlaubnis
|
||||||
|
// kommt ausschließlich aus der Config-Liste server.allowed_origins
|
||||||
|
// (Env TALK_ALLOWED_ORIGINS): exakte Origins wie
|
||||||
|
// "https://chat.az-gruppe.com"; der Eintrag "*" erlaubt jede Origin
|
||||||
|
// ausdrücklich. Bewusster Default: LEER — dann sendet der Daemon gar keine
|
||||||
|
// CORS-Header und der Browser blockt (Tür zu, bis sie konfiguriert wird).
|
||||||
|
//
|
||||||
|
// Ungelistete Origins bekommen bewusst trotzdem 204/202 ohne CORS-Header:
|
||||||
|
// HTTP-seitig ist die Anfrage sauber, die Blockade macht der Browser (so
|
||||||
|
// verrät der Daemon nichts über seine Erlaubnisliste).
|
||||||
|
|
||||||
|
// CORS-Antwort-Header (Namen zentral, damit Preflight und echter POST
|
||||||
|
// dieselben setzen).
|
||||||
|
const (
|
||||||
|
headerAllowOrigin = "Access-Control-Allow-Origin"
|
||||||
|
headerAllowMethods = "Access-Control-Allow-Methods"
|
||||||
|
headerAllowHeaders = "Access-Control-Allow-Headers"
|
||||||
|
headerMaxAge = "Access-Control-Max-Age"
|
||||||
|
headerVary = "Vary"
|
||||||
|
)
|
||||||
|
|
||||||
|
// preflightAge ist die Preflight-Cache-Dauer: 10 Minuten reichen für eine
|
||||||
|
// Chat-Session und halten den OPTIONS-Chatter klein.
|
||||||
|
const preflightAge = "600"
|
||||||
|
|
||||||
|
// corsACAOLiefert den Wert für Access-Control-Allow-Origin: bei Wildcard-
|
||||||
|
// Konfiguration das literale "*", sonst die exakt gematchte Origin (die
|
||||||
|
// identisch mit dem Config-Eintrag ist). Leer heißt: nicht erlaubt.
|
||||||
|
func (h *handler) corsACAO(r *http.Request) string {
|
||||||
|
origin := r.Header.Get("Origin")
|
||||||
|
if origin == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
for _, allowed := range h.allowedOrigins {
|
||||||
|
if allowed == "*" {
|
||||||
|
return "*"
|
||||||
|
}
|
||||||
|
if allowed == origin {
|
||||||
|
return allowed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// setSpeakCors setzt die CORS-Header auf echten POST-/speak-Antworten,
|
||||||
|
// wenn die Anfrage-Origin erlaubt ist. Vary: Origin steht immer (auch bei
|
||||||
|
// geschlossener Tür), damit Caches die Antwort nie fälschlich origin-
|
||||||
|
// übergreifend wiederverwenden.
|
||||||
|
func (h *handler) setSpeakCors(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Add(headerVary, "Origin")
|
||||||
|
if acao := h.corsACAO(r); acao != "" {
|
||||||
|
w.Header().Set(headerAllowOrigin, acao)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// speakPreflight behandelt OPTIONS /speak (CORS-Preflight): 204 mit den
|
||||||
|
// Freigabe-Headern bei gelisteter Origin, 204 ohne jegliche CORS-Header
|
||||||
|
// sonst (ungelistet, fehlend oder Tür komplett zu) — der Browser verwirft
|
||||||
|
// den eigentlichen Request, während der Daemon für Nicht-Browser-Clients
|
||||||
|
// erreichbar bleibt.
|
||||||
|
func (h *handler) speakPreflight(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Add(headerVary, "Origin")
|
||||||
|
if acao := h.corsACAO(r); acao != "" {
|
||||||
|
w.Header().Set(headerAllowOrigin, acao)
|
||||||
|
w.Header().Set(headerAllowMethods, "POST, OPTIONS")
|
||||||
|
w.Header().Set(headerAllowHeaders, "Content-Type")
|
||||||
|
w.Header().Set(headerMaxAge, preflightAge)
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
+7
-4
@@ -1,8 +1,11 @@
|
|||||||
// Package api definiert die HTTP-Oberfläche des Daemons auf 127.0.0.1:
|
// Package api definiert die HTTP-Oberfläche des Daemons auf 127.0.0.1:
|
||||||
// POST /speak (enqueue, 202 {id}; bei Mute 202 {muted:true}; bei Gesetz-
|
// POST /speak (enqueue, 202 {id}; bei Mute 202 {muted:true}; bei Gesetz-
|
||||||
// Verwurf 202 {discarded:"rate_limit"|"quiet_hours"} — Task 05),
|
// Verwurf 202 {discarded:"rate_limit"|"quiet_hours"} — Task 05),
|
||||||
// GET /status (inkl. Rate-Limit- und Ruhezeit-State), POST /cancel,
|
// GET /status (inkl. Gesetz-State, mcp/cors — Task 06), POST /cancel,
|
||||||
// POST /mute, POST /unmute. CORS für /speak und der MCP-Endpoint
|
// POST /mute, POST /unmute. Task 06 ergänzt die CORS-Tür für POST /speak
|
||||||
// (Streamable HTTP) folgen in Task 06. Der Client-Teil ist die CLI-Seite
|
// (OPTIONS-Preflight + ACAO aus server.allowed_origins, siehe cors.go)
|
||||||
// derselben Vertragsschnittstelle.
|
// und den MCP-Endpoint POST /mcp als schlankes eigenes JSON-RPC-Subset
|
||||||
|
// (initialize, notifications/initialized, tools/list, tools/call mit dem
|
||||||
|
// speak-Tool — siehe mcp.go). Der Client-Teil ist die CLI-Seite derselben
|
||||||
|
// Vertragsschnittstelle.
|
||||||
package api
|
package api
|
||||||
+59
-17
@@ -24,7 +24,7 @@ type HandlerConfig struct {
|
|||||||
// Mute ist der persistente Mute-State (Datei mute im Config-Dir).
|
// Mute ist der persistente Mute-State (Datei mute im Config-Dir).
|
||||||
Mute queue.MuteStore
|
Mute queue.MuteStore
|
||||||
|
|
||||||
// Version ist die Daemon-Version für GET /status.
|
// Version ist die Daemon-Version für GET /status und serverInfo.
|
||||||
Version string
|
Version string
|
||||||
|
|
||||||
// KnownProviders sind die gültigen Provider-Namen (Validierung des
|
// KnownProviders sind die gültigen Provider-Namen (Validierung des
|
||||||
@@ -34,6 +34,10 @@ type HandlerConfig struct {
|
|||||||
// Provider löst den aktiven Provider für GET /status auf (Daemon-seitig:
|
// Provider löst den aktiven Provider für GET /status auf (Daemon-seitig:
|
||||||
// Pin des laufenden Items bzw. erste verfügbare Ketten-Stufe). Optional.
|
// Pin des laufenden Items bzw. erste verfügbare Ketten-Stufe). Optional.
|
||||||
Provider func() string
|
Provider func() string
|
||||||
|
|
||||||
|
// AllowedOrigins ist die CORS-Erlaubnisliste für POST /speak (leer =
|
||||||
|
// keine CORS-Header, der Browser blockt; siehe cors.go).
|
||||||
|
AllowedOrigins []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHandler liefert die HTTP-Routen des Daemons. Methoden-Bindung per
|
// NewHandler liefert die HTTP-Routen des Daemons. Methoden-Bindung per
|
||||||
@@ -45,16 +49,20 @@ func NewHandler(cfg HandlerConfig) http.Handler {
|
|||||||
version: cfg.Version,
|
version: cfg.Version,
|
||||||
known: make(map[string]bool, len(cfg.KnownProviders)),
|
known: make(map[string]bool, len(cfg.KnownProviders)),
|
||||||
provider: cfg.Provider,
|
provider: cfg.Provider,
|
||||||
|
allowedOrigins: cfg.AllowedOrigins,
|
||||||
}
|
}
|
||||||
for _, name := range cfg.KnownProviders {
|
for _, name := range cfg.KnownProviders {
|
||||||
h.known[name] = true
|
h.known[name] = true
|
||||||
}
|
}
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.HandleFunc("POST /speak", h.speak)
|
mux.HandleFunc("POST /speak", h.speak)
|
||||||
|
mux.HandleFunc("OPTIONS /speak", h.speakPreflight) // CORS-Preflight (Task 06)
|
||||||
mux.HandleFunc("GET /status", h.status)
|
mux.HandleFunc("GET /status", h.status)
|
||||||
mux.HandleFunc("POST /cancel", h.cancel)
|
mux.HandleFunc("POST /cancel", h.cancel)
|
||||||
mux.HandleFunc("POST /mute", h.muteEndpoint(true))
|
mux.HandleFunc("POST /mute", h.muteEndpoint(true))
|
||||||
mux.HandleFunc("POST /unmute", h.muteEndpoint(false))
|
mux.HandleFunc("POST /unmute", h.muteEndpoint(false))
|
||||||
|
mux.HandleFunc("POST /mcp", h.serveMCP) // MCP Streamable HTTP (Task 06)
|
||||||
|
mux.HandleFunc("GET /mcp", h.mcpGet) // 405: keine server-initiierten Streams
|
||||||
return mux
|
return mux
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,42 +73,67 @@ type handler struct {
|
|||||||
version string
|
version string
|
||||||
known map[string]bool
|
known map[string]bool
|
||||||
provider func() string
|
provider func() string
|
||||||
|
allowedOrigins []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// speak behandelt POST /speak: validieren, bei Mute verwerfen (202,
|
// speak behandelt POST /speak: validieren, bei Mute verwerfen (202,
|
||||||
// dokumentiertes Verhalten — bewusst kein Fehler, damit Aufrufer keine
|
// dokumentiertes Verhalten — bewusst kein Fehler, damit Aufrufer keine
|
||||||
// Sonderbehandlung brauchen), sonst enqueue → 202 {id}. Das Gesetz
|
// Sonderbehandlung brauchen), sonst enqueue → 202 {id}. Das Gesetz
|
||||||
// (Rate-Limit-Budget, Ruhezeit) kann beim Eintritt ebenfalls verwerfen —
|
// (Rate-Limit-Budget, Ruhezeit) kann beim Eintritt ebenfalls verwerfen —
|
||||||
// die Antwort nennt dann den Grund ({"discarded":"…"}).
|
// die Antwort nennt dann den Grund ({"discarded":"…"}). CORS-Header nur
|
||||||
|
// hier und nur bei gelisteter Origin (siehe cors.go).
|
||||||
func (h *handler) speak(w http.ResponseWriter, r *http.Request) {
|
func (h *handler) speak(w http.ResponseWriter, r *http.Request) {
|
||||||
|
h.setSpeakCors(w, r)
|
||||||
|
|
||||||
var req SpeakRequest
|
var req SpeakRequest
|
||||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, maxBodyBytes)).Decode(&req); err != nil {
|
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, maxBodyBytes)).Decode(&req); err != nil {
|
||||||
writeError(w, http.StatusBadRequest, "ungültiger JSON-Body: "+err.Error())
|
writeError(w, http.StatusBadRequest, "ungültiger JSON-Body: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(req.Text) == "" {
|
adm := h.admitSpeak(req)
|
||||||
writeError(w, http.StatusBadRequest, "text fehlt oder ist leer")
|
if adm.errMsg != "" {
|
||||||
|
writeError(w, adm.status, adm.errMsg)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
writeJSON(w, adm.status, adm.resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// speakAdmission ist das Ergebnis des gemeinsamen Aufnahme-Pfads von
|
||||||
|
// POST /speak und dem MCP-Tool speak: HTTP-Status-Äquivalent plus Body —
|
||||||
|
// 202 mit {id}/{muted}/{discarded} als Aufnahme-Ergebnis, 400/503 mit
|
||||||
|
// errMsg als Fehler (MCP übersetzt beides, siehe mcp.go).
|
||||||
|
type speakAdmission struct {
|
||||||
|
status int
|
||||||
|
resp SpeakResponse
|
||||||
|
errMsg string
|
||||||
|
}
|
||||||
|
|
||||||
|
// admitSpeak ist der eine Aufnahme-Pfad hinter POST /speak und dem
|
||||||
|
// MCP-Tool speak (Task 06): Validierung, Mute-Check, Enqueue inklusive
|
||||||
|
// Gesetz (Rate-Limit-Budget, Ruhezeit) — identische Semantik für beide
|
||||||
|
// Türen, kein Contract-Drift.
|
||||||
|
func (h *handler) admitSpeak(req SpeakRequest) speakAdmission {
|
||||||
|
if strings.TrimSpace(req.Text) == "" {
|
||||||
|
return speakAdmission{status: http.StatusBadRequest, errMsg: "text fehlt oder ist leer"}
|
||||||
|
}
|
||||||
if req.Priority == "" {
|
if req.Priority == "" {
|
||||||
req.Priority = string(queue.PriorityNormal)
|
req.Priority = string(queue.PriorityNormal)
|
||||||
}
|
}
|
||||||
if !queue.Priority(req.Priority).Valid() {
|
if !queue.Priority(req.Priority).Valid() {
|
||||||
writeError(w, http.StatusBadRequest, fmt.Sprintf("priority: ungültiger Wert %q (erlaubt: low, normal, high)", req.Priority))
|
return speakAdmission{status: http.StatusBadRequest,
|
||||||
return
|
errMsg: fmt.Sprintf("priority: ungültiger Wert %q (erlaubt: low, normal, high)", req.Priority)}
|
||||||
}
|
}
|
||||||
if req.Provider != "" && !h.known[req.Provider] {
|
if req.Provider != "" && !h.known[req.Provider] {
|
||||||
writeError(w, http.StatusBadRequest, fmt.Sprintf("provider: unbekannter Wert %q", req.Provider))
|
return speakAdmission{status: http.StatusBadRequest,
|
||||||
return
|
errMsg: fmt.Sprintf("provider: unbekannter Wert %q", req.Provider)}
|
||||||
}
|
}
|
||||||
if req.Lang != "" && req.Lang != "de" && req.Lang != "en" {
|
if req.Lang != "" && req.Lang != "de" && req.Lang != "en" {
|
||||||
writeError(w, http.StatusBadRequest, fmt.Sprintf("lang: ungültiger Wert %q (erlaubt: de, en)", req.Lang))
|
return speakAdmission{status: http.StatusBadRequest,
|
||||||
return
|
errMsg: fmt.Sprintf("lang: ungültiger Wert %q (erlaubt: de, en)", req.Lang)}
|
||||||
}
|
}
|
||||||
|
|
||||||
if h.mute.Muted() {
|
if h.mute.Muted() {
|
||||||
writeJSON(w, http.StatusAccepted, SpeakResponse{Muted: true})
|
return speakAdmission{status: http.StatusAccepted, resp: SpeakResponse{Muted: true}}
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
adm, err := h.queue.Enqueue(queue.Item{
|
adm, err := h.queue.Enqueue(queue.Item{
|
||||||
@@ -112,17 +145,16 @@ func (h *handler) speak(w http.ResponseWriter, r *http.Request) {
|
|||||||
Now: req.Now,
|
Now: req.Now,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeError(w, http.StatusServiceUnavailable, "enqueue: "+err.Error())
|
return speakAdmission{status: http.StatusServiceUnavailable, errMsg: "enqueue: " + err.Error()}
|
||||||
return
|
|
||||||
}
|
}
|
||||||
if adm.Discarded != "" {
|
if adm.Discarded != "" {
|
||||||
// Gesetz-Verwurf (Rate-Limit-Budget / Ruhezeit): dokumentiertes
|
// Gesetz-Verwurf (Rate-Limit-Budget / Ruhezeit): dokumentiertes
|
||||||
// Verhalten wie bei Mute — 202 mit Grund statt Fehler, damit
|
// Verhalten wie bei Mute — 202 mit Grund statt Fehler, damit
|
||||||
// Aufrufer keine Sonderbehandlung brauchen.
|
// Aufrufer keine Sonderbehandlung brauchen.
|
||||||
writeJSON(w, http.StatusAccepted, SpeakResponse{Discarded: adm.Discarded, Toast: adm.Toast})
|
return speakAdmission{status: http.StatusAccepted,
|
||||||
return
|
resp: SpeakResponse{Discarded: adm.Discarded, Toast: adm.Toast}}
|
||||||
}
|
}
|
||||||
writeJSON(w, http.StatusAccepted, SpeakResponse{ID: adm.ID})
|
return speakAdmission{status: http.StatusAccepted, resp: SpeakResponse{ID: adm.ID}}
|
||||||
}
|
}
|
||||||
|
|
||||||
// status behandelt GET /status: Queue-Länge (+ wartende IDs), aktuelles
|
// status behandelt GET /status: Queue-Länge (+ wartende IDs), aktuelles
|
||||||
@@ -139,6 +171,8 @@ func (h *handler) status(w http.ResponseWriter, r *http.Request) {
|
|||||||
Queue: len(pending),
|
Queue: len(pending),
|
||||||
QueueIDs: ids,
|
QueueIDs: ids,
|
||||||
Muted: h.mute.Muted(),
|
Muted: h.mute.Muted(),
|
||||||
|
MCP: true,
|
||||||
|
Cors: corsList(h.allowedOrigins),
|
||||||
}
|
}
|
||||||
if cur, ok := h.queue.Current(); ok {
|
if cur, ok := h.queue.Current(); ok {
|
||||||
st.Current = &CurrentItem{
|
st.Current = &CurrentItem{
|
||||||
@@ -155,6 +189,14 @@ func (h *handler) status(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeJSON(w, http.StatusOK, st)
|
writeJSON(w, http.StatusOK, st)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// corsList kopiert die Erlaubnisliste als JSON-[] statt null: Der Status
|
||||||
|
// soll "cors": [] zeigen, wenn die Tür zu ist.
|
||||||
|
func corsList(origins []string) []string {
|
||||||
|
out := make([]string, len(origins))
|
||||||
|
copy(out, origins)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// lawState überführt die Gesetz-Sicht der Queue in die JSON-Form von
|
// lawState überführt die Gesetz-Sicht der Queue in die JSON-Form von
|
||||||
// /status: Zeitstempel nur, wenn sie in der Zukunft liegen (null = Slot
|
// /status: Zeitstempel nur, wenn sie in der Zukunft liegen (null = Slot
|
||||||
// frei / Ruhezeit inaktiv).
|
// frei / Ruhezeit inaktiv).
|
||||||
|
|||||||
+10
-1
@@ -55,7 +55,8 @@ type CurrentItem struct {
|
|||||||
// Item (null im Leerlauf), Mute-State, aktiver Provider, Daemon-Version.
|
// Item (null im Leerlauf), Mute-State, aktiver Provider, Daemon-Version.
|
||||||
// QueueIDs ist die Ergänzung der wartenden IDs in Abarbeitungs-Reihenfolge
|
// QueueIDs ist die Ergänzung der wartenden IDs in Abarbeitungs-Reihenfolge
|
||||||
// — die CLI braucht sie für ihr --wait-Polling. Task 05 ergänzt additiv
|
// — die CLI braucht sie für ihr --wait-Polling. Task 05 ergänzt additiv
|
||||||
// den Gesetz-State (rate_limit, quiet_hours).
|
// den Gesetz-State (rate_limit, quiet_hours); Task 06 additiv mcp/cors
|
||||||
|
// (Compat-Felder: MCP-Endpoint vorhanden, konfigurierte CORS-Origins).
|
||||||
type StatusResponse struct {
|
type StatusResponse struct {
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
Queue int `json:"queue"`
|
Queue int `json:"queue"`
|
||||||
@@ -65,6 +66,14 @@ type StatusResponse struct {
|
|||||||
Provider string `json:"provider"`
|
Provider string `json:"provider"`
|
||||||
RateLimit *RateLimitState `json:"rate_limit,omitempty"`
|
RateLimit *RateLimitState `json:"rate_limit,omitempty"`
|
||||||
QuietHours *QuietHoursState `json:"quiet_hours,omitempty"`
|
QuietHours *QuietHoursState `json:"quiet_hours,omitempty"`
|
||||||
|
|
||||||
|
// MCP meldet, dass der Daemon den MCP-Endpoint anbietet (immer true;
|
||||||
|
// Feld existiert für additive Compat-Sicht von Clients/Browsern).
|
||||||
|
MCP bool `json:"mcp"`
|
||||||
|
|
||||||
|
// Cors ist die konfigurierte CORS-Erlaubnisliste von POST /speak
|
||||||
|
// (leeres [] = Tür zu). Immer vorhanden (auch leer), nicht omitempty.
|
||||||
|
Cors []string `json:"cors"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// RateLimitState ist der Rate-Limit-State in GET /status (Task 05).
|
// RateLimitState ist der Rate-Limit-State in GET /status (Task 05).
|
||||||
|
|||||||
Reference in new issue
Block a user