refactor: ein Binary — talk startet den Daemon selbst, talkd und Degraded Mode entfallen
Zwei Binaries erzeugten den halben Komplexitätsballast: Degraded Mode mit Lock-Datei und Stale-Lock-Race, „läuft talkd?", Autostart-Konfiguration je OS, doppelte Chain-Konstruktion in cmd/talk und cmd/talkd — und der Degraded Mode kannte weder Rate-Limit noch Ruhezeiten. Jetzt: - internal/speak: der eine Sprech-Kern (Config + Sink + Kette → Ansage); Daemon und Direkt-Pfad benutzen ihn, es gibt keine zweite Zusammensetzung der Kette mehr. - internal/daemon: Run (der bisherige talkd-Kern), Spawn (detached per Setsid bzw. DETACHED_PROCESS, stdout/stderr → daemon.log), PID-Datei daemon.pid, Shutdown-Trigger. - `talk daemon run|start|stop`: Vordergrund (systemd/launchd), expliziter Hintergrund-Start, Stop per POST /shutdown — stop wartet, bis HTTP weg und die PID-Datei gelöscht ist (Prozess wirklich beendet). - Autostart: say/wrap starten den Daemon beim ersten Sprechbefehl selbst (daemon.autostart, Default an; Env TALK_AUTOSTART). Verliert ein paralleler Aufruf das Port-Rennen, erkennt er das am Kind-Ende und nutzt den Daemon des anderen — kein Timeout-Warten. - Direkt-Pfad statt Degraded Mode: ohne erreichbaren oder startbaren Daemon spricht talk direkt (Mute respektiert, kein Lock, Hinweis + Grund + Log-Pfad auf stderr). Steuerbefehle starten den Daemon bewusst nicht; `talk status` zeigt „läuft nicht" (Exit 1). - cmd/talkd und cmd/talk/degraded.go entfernt; README-Daemon-Abschnitt neu. Tests: Harness baut nur noch talk und startet `talk daemon run`; TALK_AUTOSTART=false als Grundausstattung aller Tests (kein Test hinterlässt Daemons), autostart_test.go schaltet es bewusst an: Autostart beim ersten say, Idempotenz, paralleler Start in EINE Queue, expliziter start/stop, Port fremd belegt → Direkt-Pfad ohne Timeout. Cross-Compile aller sechs Targets mit CGO_ENABLED=0 verifiziert. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
b86632006e
commit
bc26a73aeb
29 files changed
+1145
-584
No files matched your search
@@ -6,9 +6,10 @@
|
||||
|
||||
**talk** macht Skripte und KI-Agenten hörbar: Ein Aufruf — `talk "Build fertig"` —
|
||||
spricht eine kurze Nachricht über eine TTS-Provider-Kette
|
||||
(ElevenLabs → OpenAI → Edge-TTS → OS-Stimme), serialisiert durch den Daemon
|
||||
`talkd`. Cross-platform (Windows/macOS/Linux, amd64/arm64), statische Binaries,
|
||||
keine Runtime-Abhängigkeiten.
|
||||
(ElevenLabs → OpenAI → Edge-TTS → OS-Stimme), serialisiert durch einen
|
||||
Hintergrund-Daemon, den talk beim ersten Aufruf selbst startet. Ein
|
||||
statisches Binary, cross-platform (Windows/macOS/Linux, amd64/arm64), keine
|
||||
Runtime-Abhängigkeiten.
|
||||
|
||||
## Befehle
|
||||
|
||||
@@ -20,6 +21,7 @@ keine Runtime-Abhängigkeiten.
|
||||
| `talk mute` / `talk unmute` | Ansagen dauerhaft stummschalten / Stummschaltung aufheben |
|
||||
| `talk status` | Daemon-Status (Queue, Mute, Provider, Version) |
|
||||
| `talk cancel` | laufende und geplante Ansagen abbrechen |
|
||||
| `talk daemon run\|start\|stop` | Daemon im Vordergrund betreiben / im Hintergrund starten / stoppen (normalerweise nicht nötig, siehe unten) |
|
||||
| `talk voices` | verfügbare Stimmen anzeigen |
|
||||
| `talk test` | Test-Ansage ausgeben |
|
||||
| `talk doctor` | Selbstdiagnose des Setups |
|
||||
@@ -29,17 +31,37 @@ Say-Flags (gelten auch für `talk "Text"`): `--wait`, `--now`, `--priority`,
|
||||
`--voice`, `--lang`, `--provider`.
|
||||
|
||||
**Hinweis:** In diesem Stand sind implementiert: `say` (und `talk "Text"`)
|
||||
mit der kompletten Provider-Kette und Daemon-Anbindung (non-blocking über
|
||||
`talkd`, siehe unten), `mute`, `unmute`, `status`, `cancel` sowie `wrap`.
|
||||
Die übrigen Befehle sind Stubs und melden `not implemented yet` (Exit-Code
|
||||
≠ 0).
|
||||
mit der kompletten Provider-Kette und Daemon-Anbindung (non-blocking,
|
||||
Autostart, siehe unten), `mute`, `unmute`, `status`, `cancel`, `daemon`
|
||||
sowie `wrap`. Die übrigen Befehle sind Stubs und melden `not implemented
|
||||
yet` (Exit-Code ≠ 0).
|
||||
|
||||
## Daemon talkd (Task 04–06)
|
||||
## Daemon
|
||||
|
||||
`talkd` besitzt die Ansage-Queue und spricht seriell ab — genau eine Ansage
|
||||
gleichzeitig, Reihenfolge `now` > `high` > `normal` > `low` (FIFO je
|
||||
Level). Der HTTP-Server lauscht nur auf `127.0.0.1`; Port: Default `7800`,
|
||||
config `[server] port`, Env `TALK_PORT` (schlagen den Default).
|
||||
Es gibt genau ein Binary. Der Daemon ist `talk daemon run` — derselbe Code,
|
||||
im Hintergrund. Er besitzt die Ansage-Queue und spricht seriell ab — genau
|
||||
eine Ansage gleichzeitig, Reihenfolge `now` > `high` > `normal` > `low`
|
||||
(FIFO je Level). Der HTTP-Server lauscht nur auf `127.0.0.1`; Port: Default
|
||||
`7800`, config `[server] port`, Env `TALK_PORT` (schlagen den Default).
|
||||
|
||||
**Autostart:** Läuft kein Daemon, startet `talk say` (und `talk wrap`) ihn
|
||||
beim ersten Sprechbefehl selbst — losgelöst vom Terminal, mit Protokoll in
|
||||
`daemon.log` und Prozess-ID in `daemon.pid` im Config-Dir; stderr meldet
|
||||
einmalig „Daemon gestartet". Der Daemon läuft dann weiter (Leerlauf kostet
|
||||
nichts Nennenswertes), bis `talk daemon stop`, Abmelden oder Neustart.
|
||||
Parallele erste Aufrufe sind harmlos: Verliert einer das Port-Rennen,
|
||||
benutzt er den Daemon des anderen. Wer den Daemon selbst verwaltet
|
||||
(systemd, launchd, Task Scheduler mit `talk daemon run`), schaltet den
|
||||
Autostart ab:
|
||||
|
||||
```toml
|
||||
[daemon]
|
||||
autostart = false # Env TALK_AUTOSTART=false
|
||||
```
|
||||
|
||||
Steuerbefehle (`mute`, `unmute`, `status`, `cancel`) starten den Daemon
|
||||
bewusst nicht: `talk status` sagt dann „läuft nicht" (Exit 1), `talk mute`
|
||||
schreibt die Mute-Datei direkt.
|
||||
|
||||
| Endpunkt | Wirkung |
|
||||
| --- | --- |
|
||||
@@ -48,8 +70,9 @@ config `[server] port`, Env `TALK_PORT` (schlagen den Default).
|
||||
| `GET /status` | `{"version", "queue", "queue_ids", "current", "muted", "provider", "rate_limit": {"interval", "allow_high_burst", "next_slot"}, "quiet_hours": {"window", "active", "mode", "until"}, "mcp": true, "cors": [origins]}` |
|
||||
| `POST /cancel` | bricht die laufende Ansage ab + leert die Queue → `200 {"cancelled":n}` |
|
||||
| `POST /mute` / `POST /unmute` | persistenter Mute-State → `200 {"muted":bool}` |
|
||||
| `POST /shutdown` | fährt den Daemon herunter (`talk daemon stop`) → `200 {"ok":true}` |
|
||||
| `POST /mcp` | MCP (Streamable HTTP): JSON-RPC `initialize`, `notifications/initialized`, `ping`, `tools/list`, `tools/call` — ein Tool `speak` (siehe unten) |
|
||||
| `GET /mcp` | `405` — talkd bietet keine server-initiierten Streams an |
|
||||
| `GET /mcp` | `405` — der Daemon bietet keine server-initiierten Streams an |
|
||||
|
||||
- **CLI-Verhalten:** `talk say` (und `talk "Text"`) erkennt den Daemon via
|
||||
`GET /status`. Läuft er: enqueue + sofortige Rückkehr (non-blocking,
|
||||
@@ -61,10 +84,11 @@ config `[server] port`, Env `TALK_PORT` (schlagen den Default).
|
||||
`false`) und überlebt Daemon-Restarts. `talk mute` ohne laufenden Daemon
|
||||
schreibt die Datei direkt — der Daemon liest sie bei jeder Ansage neu,
|
||||
der Zustand greift also auch offline.
|
||||
- **Degraded Mode:** Antwortet kein Daemon, spielt `talk` lokal und
|
||||
sequenziell weiter — serialisiert über die Lock-Datei `talk.lock` im
|
||||
Config-Dir (Kollision = warten; verwaisste Locks gelten nach 60 s als
|
||||
verfallen). Mute wird auch hier respektiert. Nichts bricht still.
|
||||
- **Direkt-Pfad:** Ist kein Daemon erreichbar und keiner startbar
|
||||
(Autostart aus, Port fremd belegt, Start gescheitert), spricht `talk`
|
||||
direkt im eigenen Prozess — blockierend, ohne Warteschlange (parallele
|
||||
Aufrufe können überlappen), mit Hinweis und Grund auf stderr. Mute wird
|
||||
auch hier respektiert. Nichts bricht still.
|
||||
|
||||
### Rate-Limit + Ruhezeiten (Task 05)
|
||||
|
||||
@@ -129,7 +153,7 @@ allowed_origins = ["https://chat.az-gruppe.com"] # Env TALK_ALLOWED_ORIGINS
|
||||
- **Die Tür ist serverseitig zu, nicht nur browserseitig.** CORS allein
|
||||
verhindert nur das *Lesen* der Antwort; einen POST mit `text/plain`-Body
|
||||
oder einen Formular-POST schickt jede Webseite ohne Preflight ab. Deshalb
|
||||
lehnt talkd jede Anfrage mit fremdem `Origin`-Header mit `403` ab, die
|
||||
lehnt der Daemon jede Anfrage mit fremdem `Origin`-Header mit `403` ab, die
|
||||
nicht POST /speak von einer gelisteten Origin ist — also ungelistete
|
||||
Origins auf /speak, jeden Browser-Zugriff auf `/status`, `/mute`,
|
||||
`/unmute`, `/cancel` und `/mcp`, auch von gelisteten Origins. Nur der
|
||||
@@ -261,8 +285,8 @@ talk wrap --wait -- make test
|
||||
Befehle (Binary nicht gefunden) zählen als 127 (Shell-Konvention),
|
||||
Signaltode portabel als 1. Benutzungsfehler von talk selbst (kein
|
||||
Befehl, ungültige Flags) scheitern vor dem Start des Befehls.
|
||||
- **Ansageweg:** derselbe wie say — Daemon-Pfad (enqueue, non-blocking;
|
||||
`--wait` respektiert), ohne Daemon Degraded Mode über die Lock-Datei.
|
||||
- **Ansageweg:** derselbe wie say — Daemon (enqueue, non-blocking;
|
||||
`--wait` respektiert; Autostart), ohne Daemon der Direkt-Pfad.
|
||||
wrap erbt die say-Flags (`--wait`, `--now`, `--priority`, `--voice`,
|
||||
`--lang`, `--provider`) und reicht sie an die Ansage durch.
|
||||
|
||||
|
||||
Reference in new issue
Block a user