Files
talk/internal/api/types.go
T
m3ta-chironandClaude Fable 5.1 97650ad0ec feat(queue): Gesetze max_text und max_age — gekürzte Langtexte, verfallene Stau-Ansagen
Zwei Löcher im Anti-Spam-Gesetz, die ein Prompt trivial nutzt: Ein Agent
kann 64 KB Text einreichen (ElevenLabs stellt die Rechnung, die Ansage
dauert Minuten), und geparkte normal-Ansagen spielen unbegrenzt spät —
„Build fertig" zweieinhalb Minuten nach dem Build ist keine Nachricht.

- queue.max_text (Default 400 Zeichen, Env TALK_MAX_TEXT): längere Texte
  werden beim Eintritt an einer Wortgrenze gekürzt und so gesprochen; die
  /speak-Antwort meldet truncated, die CLI warnt auf stderr, das MCP-
  Ergebnis sagt es im Text. Runen, nicht Bytes.
- queue.max_age (Default 2m, Env TALK_MAX_AGE): Items, die beim
  Fälligwerden länger gewartet haben, werden verworfen (discarded
  "expired", Protokoll nennt Wartezeit und Grenze). now:true ist
  ausgenommen. Rang: now > veraltet > Ruhezeit > Rate-Limit.
- /status trägt "limits": {max_text, max_age}; `talk status` zeigt sie.
- Negative Werte sind Config-Fehler; 0 = unbegrenzt.

Tests: truncateText-Tabelle (Wortgrenze, Satzzeichen, Runen), Enqueue-
Kürzung, decide-Vorrang inkl. now-Ausnahme, geparktes Item verfällt;
Config-Schichten; Prozessgrenze: Provider bekommt die gekürzte Fassung,
dritte von drei Stau-Ansagen wird nie gesprochen.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-24 09:54:40 +02:00

154 lines
5.4 KiB
Go

package api
// Request/Response-Typen des Daemon-HTTP-Vertrags (Task-04-Brief, bindende
// Felder). Die CLI und spätere Konsumenten (MCP, Task 06) benutzen dieselben
// Typen — eine Quelle, kein Contract-Drift zwischen Daemon und Client.
// SpeakRequest ist der Body von POST /speak.
type SpeakRequest struct {
// Text ist der Ansage-Text (Pflicht).
Text string `json:"text"`
// Priority: low | normal | high (Default normal, wenn leer).
Priority string `json:"priority,omitempty"`
// Provider pinnt die Provider-Kette (--provider).
Provider string `json:"provider,omitempty"`
// Voice pinnt eine Voice-ID (--voice).
Voice string `json:"voice,omitempty"`
// Lang pinnt die Sprache de | en (leer = automatische Erkennung).
Lang string `json:"lang,omitempty"`
// Now lässt die Ansage vor allen wartenden springen (--now).
Now bool `json:"now,omitempty"`
}
// SpeakResponse ist die Antwort von POST /speak: 202 {"id":"…"} — bei
// aktivem Mute 202 {"id":"","muted":true} (Ansage verworfen; dokumentiertes
// 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"`
// Truncated meldet, dass der Text auf queue.max_text gekürzt und so
// aufgenommen wurde (Gesetz gegen vorgelesene Langantworten).
Truncated bool `json:"truncated,omitempty"`
}
// CurrentItem beschreibt das laufende Item in GET /status.
type CurrentItem struct {
ID string `json:"id"`
Text string `json:"text"`
Priority string `json:"priority"`
Now bool `json:"now"`
}
// 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. Task 05 ergänzt additiv
// den Gesetz-State (rate_limit, quiet_hours); Task 06 additiv mcp/cors
// (Compat-Felder: MCP-Endpoint vorhanden, konfigurierte CORS-Origins).
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"`
RateLimit *RateLimitState `json:"rate_limit,omitempty"`
QuietHours *QuietHoursState `json:"quiet_hours,omitempty"`
Limits *LimitsState `json:"limits,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).
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"`
}
// LimitsState sind die Längen- und Alters-Gesetze in GET /status.
type LimitsState struct {
// MaxText ist die Obergrenze der Ansage-Länge in Zeichen (0 = unbegrenzt).
MaxText int `json:"max_text"`
// MaxAge ist die längste Wartezeit in der Queue als Dauer-String
// ("2m0s"; "0s" = unbegrenzt).
MaxAge string `json:"max_age"`
}
// 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
// gerade läuft — Grundlage des --wait-Pollings der CLI. Ist die Ansage
// weder wartend noch laufend, ist sie fertig (oder abgebrochen) — in
// beiden Fällen hat der Aufrufer nichts mehr zu warten.
func (s StatusResponse) PendingOrCurrent(id string) bool {
if s.Current != nil && s.Current.ID == id {
return true
}
for _, qid := range s.QueueIDs {
if qid == id {
return true
}
}
return false
}
// CancelResponse ist die Antwort von POST /cancel.
type CancelResponse struct {
Cancelled int `json:"cancelled"`
}
// MuteResponse ist die Antwort von POST /mute und POST /unmute.
type MuteResponse struct {
Muted bool `json:"muted"`
}
// ShutdownResponse ist die Antwort von POST /shutdown.
type ShutdownResponse struct {
OK bool `json:"ok"`
}