feat(api): /speak meldet Gesetz-Verwürfe, /status zeigt Gesetz-State
POST /speak antwortet bei Verwurf dokumentiert mit 202 {id:"",
discarded:"rate_limit"|"quiet_hours"} (+ toast:true im Toast-Modus)
— analog zum Mute-Muster, kein Fehler. GET /status ergänzt additiv
rate_limit {interval, allow_high_burst, next_slot} und quiet_hours
{window, active, mode, until} (RFC3339 bzw. null). talkd verwired die
Gesetze aus der Config und loggt sie beim Start. Die CLI meldet
'discarded: <grund>' plain (wie muted) und zeigt in talk status die
Zeilen rate-limit/ruhezeit mit Slot bzw. Fenster-Ende.
This commit is contained in:
1 parent
5385a4b87d
commit
b56c8abfae
6 files changed
+194
-27
No files matched your search
+6
-4
@@ -1,6 +1,8 @@
|
||||
// Package api definiert die HTTP-Oberfläche des Daemons auf 127.0.0.1:
|
||||
// POST /speak (enqueue, 202 {id}; bei Mute 202 {muted:true}), GET /status,
|
||||
// POST /cancel, POST /mute, POST /unmute (Task 04). CORS für /speak und
|
||||
// der MCP-Endpoint (Streamable HTTP) folgen in Task 06. Der Client-Teil
|
||||
// ist die CLI-Seite derselben Vertragsschnittstelle.
|
||||
// POST /speak (enqueue, 202 {id}; bei Mute 202 {muted:true}; bei Gesetz-
|
||||
// Verwurf 202 {discarded:"rate_limit"|"quiet_hours"} — Task 05),
|
||||
// GET /status (inkl. Rate-Limit- und Ruhezeit-State), POST /cancel,
|
||||
// POST /mute, POST /unmute. CORS für /speak und der MCP-Endpoint
|
||||
// (Streamable HTTP) folgen in Task 06. Der Client-Teil ist die CLI-Seite
|
||||
// derselben Vertragsschnittstelle.
|
||||
package api
|
||||
+41
-4
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.m3ta.dev/m3tam3re/talk/internal/queue"
|
||||
)
|
||||
@@ -68,7 +69,9 @@ type handler struct {
|
||||
|
||||
// speak behandelt POST /speak: validieren, bei Mute verwerfen (202,
|
||||
// dokumentiertes Verhalten — bewusst kein Fehler, damit Aufrufer keine
|
||||
// Sonderbehandlung brauchen), sonst enqueue → 202 {id}.
|
||||
// Sonderbehandlung brauchen), sonst enqueue → 202 {id}. Das Gesetz
|
||||
// (Rate-Limit-Budget, Ruhezeit) kann beim Eintritt ebenfalls verwerfen —
|
||||
// die Antwort nennt dann den Grund ({"discarded":"…"}).
|
||||
func (h *handler) speak(w http.ResponseWriter, r *http.Request) {
|
||||
var req SpeakRequest
|
||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, maxBodyBytes)).Decode(&req); err != nil {
|
||||
@@ -100,7 +103,7 @@ func (h *handler) speak(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
id, err := h.queue.Enqueue(queue.Item{
|
||||
adm, err := h.queue.Enqueue(queue.Item{
|
||||
Text: req.Text,
|
||||
Lang: req.Lang,
|
||||
Provider: req.Provider,
|
||||
@@ -112,11 +115,19 @@ func (h *handler) speak(w http.ResponseWriter, r *http.Request) {
|
||||
writeError(w, http.StatusServiceUnavailable, "enqueue: "+err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusAccepted, SpeakResponse{ID: id})
|
||||
if adm.Discarded != "" {
|
||||
// Gesetz-Verwurf (Rate-Limit-Budget / Ruhezeit): dokumentiertes
|
||||
// Verhalten wie bei Mute — 202 mit Grund statt Fehler, damit
|
||||
// Aufrufer keine Sonderbehandlung brauchen.
|
||||
writeJSON(w, http.StatusAccepted, SpeakResponse{Discarded: adm.Discarded, Toast: adm.Toast})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusAccepted, SpeakResponse{ID: adm.ID})
|
||||
}
|
||||
|
||||
// status behandelt GET /status: Queue-Länge (+ wartende IDs), aktuelles
|
||||
// Item, Mute-State, aktiver Provider, Version.
|
||||
// Item, Mute-State, aktiver Provider, Version — und seit Task 05 den
|
||||
// Gesetz-State (Rate-Limit mit nächstem Slot, Ruhezeit-Fenster).
|
||||
func (h *handler) status(w http.ResponseWriter, r *http.Request) {
|
||||
pending := h.queue.Pending()
|
||||
ids := make([]string, len(pending))
|
||||
@@ -140,9 +151,35 @@ func (h *handler) status(w http.ResponseWriter, r *http.Request) {
|
||||
if h.provider != nil {
|
||||
st.Provider = h.provider()
|
||||
}
|
||||
st.RateLimit, st.QuietHours = lawState(h.queue.LawState())
|
||||
writeJSON(w, http.StatusOK, st)
|
||||
}
|
||||
|
||||
// lawState überführt die Gesetz-Sicht der Queue in die JSON-Form von
|
||||
// /status: Zeitstempel nur, wenn sie in der Zukunft liegen (null = Slot
|
||||
// frei / Ruhezeit inaktiv).
|
||||
func lawState(l queue.LawState) (*RateLimitState, *QuietHoursState) {
|
||||
now := time.Now()
|
||||
rl := &RateLimitState{
|
||||
Interval: l.RateLimit.String(),
|
||||
AllowHighBurst: l.AllowHighBurst,
|
||||
}
|
||||
if l.NextSlot.After(now) {
|
||||
slot := l.NextSlot.Format(time.RFC3339)
|
||||
rl.NextSlot = &slot
|
||||
}
|
||||
qh := &QuietHoursState{
|
||||
Window: l.Quiet.String(),
|
||||
Active: l.QuietActive,
|
||||
Mode: l.QuietMode,
|
||||
}
|
||||
if !l.QuietUntil.IsZero() {
|
||||
until := l.QuietUntil.Format(time.RFC3339)
|
||||
qh.Until = &until
|
||||
}
|
||||
return rl, qh
|
||||
}
|
||||
|
||||
// cancel behandelt POST /cancel: laufende Ansage abbrechen + Queue leeren.
|
||||
func (h *handler) cancel(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, CancelResponse{Cancelled: h.queue.Cancel()})
|
||||
|
||||
+51
-8
@@ -27,10 +27,20 @@ type SpeakRequest struct {
|
||||
|
||||
// SpeakResponse ist die Antwort von POST /speak: 202 {"id":"…"} — bei
|
||||
// aktivem Mute 202 {"id":"","muted":true} (Ansage verworfen; dokumentiertes
|
||||
// Verhalten, kein Fehler).
|
||||
// Verhalten, kein Fehler). Seit Task 05 meldet verworfenen Spam ebenso
|
||||
// dokumentiert: 202 {"id":"","discarded":"rate_limit"|"quiet_hours"} —
|
||||
// plus "toast":true, wenn die Ruhezeit-Ansage durch eine Benachrichtigung
|
||||
// ersetzt wurde (quiet_mode = "toast").
|
||||
type SpeakResponse struct {
|
||||
ID string `json:"id"`
|
||||
Muted bool `json:"muted,omitempty"`
|
||||
|
||||
// Discarded ist der Verwurfs-Grund ("rate_limit" | "quiet_hours");
|
||||
// leer, wenn die Ansage aufgenommen wurde.
|
||||
Discarded string `json:"discarded,omitempty"`
|
||||
|
||||
// Toast meldet die Ersetzung durch eine Benachrichtigung.
|
||||
Toast bool `json:"toast,omitempty"`
|
||||
}
|
||||
|
||||
// CurrentItem beschreibt das laufende Item in GET /status.
|
||||
@@ -44,14 +54,47 @@ type CurrentItem struct {
|
||||
// StatusResponse ist die Antwort von GET /status: Queue-Länge, aktuelles
|
||||
// Item (null im Leerlauf), Mute-State, aktiver Provider, Daemon-Version.
|
||||
// QueueIDs ist die Ergänzung der wartenden IDs in Abarbeitungs-Reihenfolge
|
||||
// — die CLI braucht sie für ihr --wait-Polling.
|
||||
// — die CLI braucht sie für ihr --wait-Polling. Task 05 ergänzt additiv
|
||||
// den Gesetz-State (rate_limit, quiet_hours).
|
||||
type StatusResponse struct {
|
||||
Version string `json:"version"`
|
||||
Queue int `json:"queue"`
|
||||
QueueIDs []string `json:"queue_ids,omitempty"`
|
||||
Current *CurrentItem `json:"current"`
|
||||
Muted bool `json:"muted"`
|
||||
Provider string `json:"provider"`
|
||||
Version string `json:"version"`
|
||||
Queue int `json:"queue"`
|
||||
QueueIDs []string `json:"queue_ids,omitempty"`
|
||||
Current *CurrentItem `json:"current"`
|
||||
Muted bool `json:"muted"`
|
||||
Provider string `json:"provider"`
|
||||
RateLimit *RateLimitState `json:"rate_limit,omitempty"`
|
||||
QuietHours *QuietHoursState `json:"quiet_hours,omitempty"`
|
||||
}
|
||||
|
||||
// RateLimitState ist der Rate-Limit-State in GET /status (Task 05).
|
||||
type RateLimitState struct {
|
||||
// Interval ist der konfigurierte Mindestabstand als Dauer-String
|
||||
// ("30s"; "0s" = unbegrenzt).
|
||||
Interval string `json:"interval"`
|
||||
|
||||
// AllowHighBurst meldet, ob priority=high verstoßen darf.
|
||||
AllowHighBurst bool `json:"allow_high_burst"`
|
||||
|
||||
// NextSlot ist der nächste freie Ansage-Slot als RFC3339-Zeitstempel;
|
||||
// null, wenn sofort gespielt werden dürfte (oder unbegrenzt).
|
||||
NextSlot *string `json:"next_slot"`
|
||||
}
|
||||
|
||||
// QuietHoursState ist der Ruhezeit-State in GET /status (Task 05).
|
||||
type QuietHoursState struct {
|
||||
// Window ist das konfigurierte Fenster ("18:00-08:00"; "" = deaktiviert).
|
||||
Window string `json:"window"`
|
||||
|
||||
// Active meldet, ob die lokale Zeit gerade im Fenster liegt.
|
||||
Active bool `json:"active"`
|
||||
|
||||
// Mode ist der konfigurierte Ruhezeit-Modus ("discard" | "toast").
|
||||
Mode string `json:"mode"`
|
||||
|
||||
// Until ist das Fenster-Ende als RFC3339-Zeitstempel, wenn aktiv;
|
||||
// sonst null.
|
||||
Until *string `json:"until"`
|
||||
}
|
||||
|
||||
// PendingOrCurrent meldet, ob die Ansage mit der ID noch wartet oder
|
||||
|
||||
Reference in new issue
Block a user