refactor: switch the whole project to English
talk is going to be published. Code comments, CLI output, error messages,
daemon log lines, MCP descriptions and results, test names and assertions,
README, spec, skill, build scripts and flake are now English. Only the
spoken announcement texts stay bilingual: the wrap result and the test
sentence are German or English depending on the detected language.
Language fallback is now configurable: `lang = "en" | "de"` (env
TALK_LANG, default en) is used when detection cannot tell (numbers, single
words, ties) and for talk's own announcements. langdetect.Detect takes the
fallback as an argument instead of hard-coding German. `talk setup` asks for
it; `talk doctor` and `talk status` show it.
Vocabulary pinned across packages so the process-boundary tests observe
stable strings: chain log lines ("chain: tier X skipped (not available)",
"falling back to the next tier", "spoken via X"), queue lines ("discarded:
quiet hours ... active", "replaced by toast", "max_age"), CLI notes
("speaking directly", "daemon started at", "no daemon", "nothing to cancel"),
status block with aligned English labels, MCP messages, origin guard 403.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
4d30bd95ad
commit
85fa64cfe6
95 files changed
+4707
-4854
No files matched your search
+105
-108
@@ -9,69 +9,68 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// DefaultOrder ist die bindende Default-Fallback-Reihenfolge der Kette
|
||||
// (Spec: ElevenLabs → OpenAI TTS → Edge-TTS → OS built-in).
|
||||
// DefaultOrder is the binding default fallback order of the chain
|
||||
// (spec: ElevenLabs → OpenAI TTS → Edge TTS → OS built-in).
|
||||
var DefaultOrder = []string{"elevenlabs", "openai", "edge", "osbuiltin"}
|
||||
|
||||
// NullPlayerName ist der dokumentierte Name des verwerfenden Sinks
|
||||
// (Spec: audio.sink = "oto" | "null"). Die Kette braucht ihn für die
|
||||
// Entscheidung, ob eine Direktansage (DirectSpeaker) verworfen werden
|
||||
// muss — der null-Sink darf niemals Ton erzeugen.
|
||||
// NullPlayerName is the documented name of the discarding sink
|
||||
// (spec: audio.sink = "oto" | "null"). The chain needs it to decide
|
||||
// whether a direct announcement (DirectSpeaker) must be discarded; the
|
||||
// null sink must never produce sound.
|
||||
const NullPlayerName = "null"
|
||||
|
||||
// Player spielt Audio-Bytes ab, die die Kette geliefert hat. Der Vertrag
|
||||
// ist absichtlich strukturell identisch zu audio.Sink (Play + Name): CLI
|
||||
// und Daemon (Task 04) übergeben das Ergebnis von audio.NewSink(name, log)
|
||||
// unverändert — dadurch bleibt die Kette ohne Import des audio-Pakets
|
||||
// testbar und wiederverwendbar.
|
||||
// Player plays audio bytes the chain has produced. The contract is
|
||||
// deliberately structurally identical to audio.Sink (Play + Name): CLI and
|
||||
// daemon pass the result of audio.NewSink(name, log) unchanged, which keeps
|
||||
// the chain testable and reusable without importing the audio package.
|
||||
type Player interface {
|
||||
Play(ctx context.Context, data []byte) error
|
||||
Name() string
|
||||
}
|
||||
|
||||
// ChainConfig ist alles, was die Kette zum Bau braucht. CLI (Task 03) und
|
||||
// Daemon (Task 04) füllen sie aus Config + Flags; die Kette selbst liest
|
||||
// keine Dateien und keine Prozess-Env — Getenv ist die injizierbare Seam
|
||||
// (Tests und Daemon kommen so ohne echte Keys aus).
|
||||
// ChainConfig is everything the chain needs for construction. CLI and
|
||||
// daemon fill it from config + flags; the chain itself reads no files and
|
||||
// no process environment. Getenv is the injectable seam (tests and daemon
|
||||
// thus work without real keys).
|
||||
type ChainConfig struct {
|
||||
// Pin reduziert die Kette auf genau einen Provider (CLI-Flag --provider
|
||||
// schlägt config `provider = "…"`). Leer = DefaultOrder.
|
||||
// Pin reduces the chain to exactly one provider (CLI flag --provider
|
||||
// beats config `provider = "…"`). Empty = DefaultOrder.
|
||||
Pin string
|
||||
|
||||
// Voice ist eine explizite Voice-ID (CLI-Flag --voice); sie wird über
|
||||
// VoicePinner an alle Stufen gepinnt und schlägt Mapping + Defaults.
|
||||
// Voice is an explicit voice ID (CLI flag --voice); it is pinned on all
|
||||
// tiers via VoicePinner and beats mapping + defaults.
|
||||
Voice string
|
||||
|
||||
// Voices ist das voice.<provider>.<lang>-Mapping aus der config.toml.
|
||||
// Voices is the voice.<provider>.<lang> mapping from config.toml.
|
||||
Voices map[string]map[string]string
|
||||
|
||||
// Endpoints sind die Datei-Endpunkte [providers.<name>].endpoint;
|
||||
// TALK_<NAME>_ENDPOINT (via Getenv) schlägt sie, sie schlägt die
|
||||
// eingebauten Defaults (siehe endpointFrom).
|
||||
// Endpoints are the file endpoints [providers.<name>].endpoint;
|
||||
// TALK_<NAME>_ENDPOINT (via Getenv) beats them, they beat the built-in
|
||||
// defaults (see endpointFrom).
|
||||
Endpoints map[string]string
|
||||
|
||||
// Model ist [providers.openai].model (leer = "tts-1").
|
||||
// Model is [providers.openai].model (empty = "tts-1").
|
||||
Model string
|
||||
|
||||
// Getenv liefert Umgebungsvariablen (Keys, Endpunkt-Overrides). Nil
|
||||
// wird beim Bau durch eine Funktion ersetzt, die immer "" liefert.
|
||||
// Getenv returns environment variables (keys, endpoint overrides). Nil
|
||||
// is replaced at construction by a function that always returns "".
|
||||
Getenv func(string) string
|
||||
|
||||
// Player gibt erfolgreiche Synthesen aus; nil = die Kette liefert nur
|
||||
// Bytes (Synthesize) und Speak meldet einen klaren Fehler.
|
||||
// Player plays successful syntheses; nil = the chain only returns bytes
|
||||
// (Synthesize) and Speak reports a clear error.
|
||||
Player Player
|
||||
|
||||
// Log empfängt die Fallback-Protokoll-Zeilen (CLI: stderr). Fallbacks
|
||||
// sind nie still: Jede übersprungene oder fehlgeschlagene Stufe wird
|
||||
// hier gemeldet — auch wenn am Ende noch eine Stufe Erfolg hat.
|
||||
// Log receives the fallback log lines (CLI: stderr). Fallbacks are never
|
||||
// silent: every skipped or failed tier is reported here, even when a
|
||||
// later tier succeeds.
|
||||
Log io.Writer
|
||||
}
|
||||
|
||||
// endpointFrom löst einen Provider-Endpunkt auf:
|
||||
// TALK_<NAME>_ENDPOINT (Env) > [providers.<name>].endpoint (Datei) >
|
||||
// eingebauter Default. Env-Overrides in der Provider-Schicht zu lesen hält
|
||||
// das config-Paket frei von Provider-Namen und erlaubt Prozessgrenzen-
|
||||
// Tests ohne Config-Datei.
|
||||
// endpointFrom resolves a provider endpoint:
|
||||
// TALK_<NAME>_ENDPOINT (env) > [providers.<name>].endpoint (file) >
|
||||
// built-in default. Reading env overrides in the provider layer keeps the
|
||||
// config package free of provider names and allows process-boundary tests
|
||||
// without a config file.
|
||||
func endpointFrom(cfg ChainConfig, name, envName, def string) string {
|
||||
if cfg.Getenv != nil {
|
||||
if v := cfg.Getenv(envName); v != "" {
|
||||
@@ -84,25 +83,25 @@ func endpointFrom(cfg ChainConfig, name, envName, def string) string {
|
||||
return def
|
||||
}
|
||||
|
||||
// Chain ist die geordnete Fallback-Kette über Provider. Sie implementiert
|
||||
// selbst Provider (Synthesize liefert die Bytes der ersten erfolgreichen
|
||||
// Stufe) UND DirectSpeaker (letzter Stufen-Ausweg für Plattformen ohne
|
||||
// Byte-fähiges TTS) — damit ist sie für CLI und Daemon (Task 04)
|
||||
// austauschbar gegen jeden einzelnen Provider.
|
||||
// Chain is the ordered fallback chain over providers. It implements
|
||||
// Provider itself (Synthesize returns the bytes of the first successful
|
||||
// tier) AND DirectSpeaker (last-resort tier for platforms without
|
||||
// byte-capable TTS), so CLI and daemon can swap it for any single provider.
|
||||
//
|
||||
// Speak ist die komplette Ansage-Orchestrierung: Bytes-Synthese mit
|
||||
// Fallback, Wiedergabe über den Player, und als letzte Stufe die
|
||||
// Direktansage (osbuiltin über spd-say). Bei einem null-Player wird die
|
||||
// Direktansage tonlos verworfen (CI/Server) statt Ton zu erzeugen.
|
||||
// Speak is the complete announcement orchestration: byte synthesis with
|
||||
// fallback, playback via the Player, and as the last tier the direct
|
||||
// announcement (osbuiltin via spd-say). With a null player the direct
|
||||
// announcement is discarded silently (CI/server) instead of producing
|
||||
// sound.
|
||||
type Chain struct {
|
||||
cfg ChainConfig
|
||||
tiers []Provider
|
||||
}
|
||||
|
||||
// NewChain baut die Kette: gepinnt (Pin) oder in DefaultOrder. Unbekannte
|
||||
// Namen sind ein klarer Fehler, der die verfügbaren Namen nennt. Das
|
||||
// --voice-Pin wird auf alle Stufen angewandt, die VoicePinner implementieren
|
||||
// (Stufen ohne Voice-Begriff bekommen einen protokollierten Hinweis).
|
||||
// NewChain builds the chain: pinned (Pin) or in DefaultOrder. Unknown names
|
||||
// are a clear error that lists the available names. The --voice pin is
|
||||
// applied to all tiers implementing VoicePinner (tiers without a notion of
|
||||
// voice get a logged note).
|
||||
func NewChain(cfg ChainConfig) (*Chain, error) {
|
||||
if cfg.Getenv == nil {
|
||||
cfg.Getenv = func(string) string { return "" }
|
||||
@@ -121,7 +120,7 @@ func NewChain(cfg ChainConfig) (*Chain, error) {
|
||||
if vp, ok := p.(VoicePinner); ok {
|
||||
vp.PinVoice(cfg.Voice)
|
||||
} else {
|
||||
chainLogf(cfg, "kette: --voice für Stufe %s ignoriert (kennt keine Voice-IDs)\n", name)
|
||||
chainLogf(cfg, "chain: --voice ignored for tier %s (it has no voice IDs)\n", name)
|
||||
}
|
||||
}
|
||||
tiers = append(tiers, p)
|
||||
@@ -129,13 +128,13 @@ func NewChain(cfg ChainConfig) (*Chain, error) {
|
||||
return &Chain{cfg: cfg, tiers: tiers}, nil
|
||||
}
|
||||
|
||||
// Name implementiert Provider.
|
||||
// Name implements Provider.
|
||||
func (*Chain) Name() string { return "chain" }
|
||||
|
||||
// Tiers liefert die Stufen in Ketten-Reihenfolge (Diagnose/doctor).
|
||||
// Tiers returns the tiers in chain order (diagnosis/doctor).
|
||||
func (c *Chain) Tiers() []Provider { return c.tiers }
|
||||
|
||||
// Available implementiert Provider: true, sobald eine Stunde verfügbar ist.
|
||||
// Available implements Provider: true as soon as one tier is available.
|
||||
func (c *Chain) Available(ctx context.Context) bool {
|
||||
for _, p := range c.tiers {
|
||||
if p.Available(ctx) {
|
||||
@@ -145,55 +144,54 @@ func (c *Chain) Available(ctx context.Context) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// walkResult ist das interne Ergebnis des Ketten-Durchlaufs.
|
||||
// walkResult is the internal result of a chain walk.
|
||||
type walkResult struct {
|
||||
data []byte // Bytes der ersten erfolgreichen Stufe (nil = keine)
|
||||
used Provider // die erfolgreiche Stufe
|
||||
direct DirectSpeaker // erste Stufe, die nur direkt sprechen kann
|
||||
data []byte // bytes of the first successful tier (nil = none)
|
||||
used Provider // the successful tier
|
||||
direct DirectSpeaker // first tier that can only speak directly
|
||||
directName string
|
||||
failed []string // "name: fehler" je fehlgeschlagener Stufe
|
||||
skipped []string // Namen nicht verfügbarer Stufen
|
||||
failed []string // "name: error" per failed tier
|
||||
skipped []string // names of unavailable tiers
|
||||
}
|
||||
|
||||
// walk läuft die Kette in Ordnung ab: nicht verfügbare Stufen werden
|
||||
// übersprungen (protokolliert), fehlgeschlagene protokolliert und
|
||||
// überwunden. Eine Stufe mit ErrNoByteOutput, die DirectSpeaker
|
||||
// implementiert, wird als letzter Ausweg gemerkt (der Durchlauf geht
|
||||
// weiter — eine spätere Stufe könnte noch Bytes liefern).
|
||||
// walk runs the chain in order: unavailable tiers are skipped (logged),
|
||||
// failed ones are logged and passed over. A tier returning ErrNoByteOutput
|
||||
// that implements DirectSpeaker is remembered as the last resort (the walk
|
||||
// continues; a later tier might still deliver bytes).
|
||||
func (c *Chain) walk(ctx context.Context, text, lang string) walkResult {
|
||||
var r walkResult
|
||||
for _, p := range c.tiers {
|
||||
if !p.Available(ctx) {
|
||||
r.skipped = append(r.skipped, p.Name())
|
||||
chainLogf(c.cfg, "kette: Stufe %s übersprungen (nicht verfügbar)\n", p.Name())
|
||||
chainLogf(c.cfg, "chain: tier %s skipped (not available)\n", p.Name())
|
||||
continue
|
||||
}
|
||||
data, err := p.Synthesize(ctx, text, lang)
|
||||
switch {
|
||||
case err == nil:
|
||||
r.data, r.used = data, p
|
||||
chainLogf(c.cfg, "kette: Ansage über %s\n", p.Name())
|
||||
chainLogf(c.cfg, "chain: spoken via %s\n", p.Name())
|
||||
return r
|
||||
case errors.Is(err, ErrNoByteOutput) && r.direct == nil:
|
||||
if ds, ok := p.(DirectSpeaker); ok {
|
||||
r.direct, r.directName = ds, p.Name()
|
||||
chainLogf(c.cfg, "kette: Stufe %s liefert keine Bytes, kann aber direkt sprechen (als letzte Stufe gemerkt)\n", p.Name())
|
||||
chainLogf(c.cfg, "chain: tier %s yields no bytes but can speak directly (kept as last resort)\n", p.Name())
|
||||
continue
|
||||
}
|
||||
r.failed = append(r.failed, p.Name()+": "+err.Error())
|
||||
chainLogf(c.cfg, "kette: Stufe %s fehlgeschlagen: %v — Fallback auf nächste Stufe\n", p.Name(), err)
|
||||
chainLogf(c.cfg, "chain: tier %s failed: %v; falling back to the next tier\n", p.Name(), err)
|
||||
default:
|
||||
r.failed = append(r.failed, p.Name()+": "+err.Error())
|
||||
chainLogf(c.cfg, "kette: Stufe %s fehlgeschlagen: %v — Fallback auf nächste Stufe\n", p.Name(), err)
|
||||
chainLogf(c.cfg, "chain: tier %s failed: %v; falling back to the next tier\n", p.Name(), err)
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// Synthesize implementiert Provider: Bytes der ersten erfolgreichen Stufe.
|
||||
// Kann nur eine Direkt-Stufe sprechen, ist der Fehler ErrNoByteOutput (die
|
||||
// Kette erfüllt damit den DirectSpeaker-Vertrag am Aufrufer). Scheitert
|
||||
// alles, nennt der Fehler die letzte Stufen-Fehler — nie still.
|
||||
// Synthesize implements Provider: bytes of the first successful tier. If
|
||||
// only a direct tier can speak, the error is ErrNoByteOutput (the chain thus
|
||||
// honours the DirectSpeaker contract towards the caller). If everything
|
||||
// fails, the error names the tier errors; never silent.
|
||||
func (c *Chain) Synthesize(ctx context.Context, text, lang string) ([]byte, error) {
|
||||
r := c.walk(ctx, text, lang)
|
||||
if r.data != nil {
|
||||
@@ -201,50 +199,49 @@ func (c *Chain) Synthesize(ctx context.Context, text, lang string) ([]byte, erro
|
||||
}
|
||||
if r.direct != nil {
|
||||
if len(r.failed) > 0 {
|
||||
return nil, fmt.Errorf("%w (Stufe %s spricht nur direkt; daneben fehlgeschlagen: %s)",
|
||||
return nil, fmt.Errorf("%w (tier %s speaks directly only; also failed: %s)",
|
||||
ErrNoByteOutput, r.directName, strings.Join(r.failed, "; "))
|
||||
}
|
||||
return nil, fmt.Errorf("%w (Stufe %s spricht nur direkt)", ErrNoByteOutput, r.directName)
|
||||
return nil, fmt.Errorf("%w (tier %s speaks directly only)", ErrNoByteOutput, r.directName)
|
||||
}
|
||||
return nil, r.noTierErr()
|
||||
}
|
||||
|
||||
// SpeakDirect implementiert DirectSpeaker: Direktansage über die Stufe,
|
||||
// die nur direkt sprechen kann (typischerweise osbuiltin/spd-say auf Linux
|
||||
// ohne espeak). Gibt es eine Byte-fähige Stufe, ist SpeakDirect ein klarer
|
||||
// Fehler (der Aufrufer soll Synthesize + Player nutzen).
|
||||
// SpeakDirect implements DirectSpeaker: direct announcement via the tier
|
||||
// that can only speak directly (typically osbuiltin/spd-say on Linux
|
||||
// without espeak). If a byte-capable tier exists, SpeakDirect is a clear
|
||||
// error (the caller should use Synthesize + Player).
|
||||
func (c *Chain) SpeakDirect(ctx context.Context, text, lang string) error {
|
||||
r := c.walk(ctx, text, lang)
|
||||
if r.direct != nil {
|
||||
return r.direct.SpeakDirect(ctx, text, lang)
|
||||
}
|
||||
if r.data != nil {
|
||||
return fmt.Errorf("kette: Stufe %s liefert Bytes — über Synthesize und den Player abspielen, nicht direkt", r.used.Name())
|
||||
return fmt.Errorf("chain: tier %s yields bytes; play them via Synthesize and the player, not directly", r.used.Name())
|
||||
}
|
||||
return fmt.Errorf("kette: keine Stufe kann direkt sprechen (nicht verfügbar: %s; fehlgeschlagen: %s)",
|
||||
joinOr(r.skipped, "keine"), joinOr(r.failed, "keine"))
|
||||
return fmt.Errorf("chain: no tier can speak directly (not available: %s; failed: %s)",
|
||||
joinOr(r.skipped, "none"), joinOr(r.failed, "none"))
|
||||
}
|
||||
|
||||
// Speak ist die komplette Ansage in EINEM Ketten-Durchlauf: Synthese mit
|
||||
// Fallback → Wiedergabe über den Player; fällt die Kette auf die Direkt-
|
||||
// Stufe zurück, entscheidet der Player-Name über Ton (alles außer „null")
|
||||
// oder tonloses Verwerfen (null: CI/Server bleiben fehler- und tonfrei,
|
||||
// der Verwurf wird protokolliert). Der Durchlauf findet bewusst genau
|
||||
// einmal statt: fehlgeschlagene Cloud-Stufen werden nicht ein zweites Mal
|
||||
// angefragt (keine doppelten POSTs auf bezahlten APIs, keine doppelten
|
||||
// Protokoll-Zeilen), und Bytes aus diesem Durchlauf werden abgespielt,
|
||||
// statt sie wegzuwerfen und neu zu laufen. Nur ein Fehler aller Stufen
|
||||
// oder eine fehlgeschlagene Wiedergabe sind Fehler.
|
||||
// Speak is the complete announcement in ONE chain walk: synthesis with
|
||||
// fallback → playback via the Player; if the chain falls back to the direct
|
||||
// tier, the player name decides between sound (anything but "null") and
|
||||
// silent discarding (null: CI/server stay error- and sound-free, the
|
||||
// discard is logged). The walk deliberately happens exactly once: failed
|
||||
// cloud tiers are not requested a second time (no duplicate POSTs to paid
|
||||
// APIs, no duplicate log lines), and bytes from this walk are played
|
||||
// instead of being thrown away and re-run. Only a failure of all tiers or a
|
||||
// failed playback are errors.
|
||||
func (c *Chain) Speak(ctx context.Context, text, lang string) error {
|
||||
_, err := c.SpeakOutcome(ctx, text, lang)
|
||||
return err
|
||||
}
|
||||
|
||||
// Outcome beschreibt eine gelungene Ansage: welche Stufe gesprochen hat,
|
||||
// wie lange Synthese (Ketten-Durchlauf bis zu den Bytes) und Wiedergabe
|
||||
// gedauert haben, und ob es eine Direktansage war (keine Bytes, Provider
|
||||
// spielt selbst — dann ist Synthesis die Gesamtzeit und Playback 0).
|
||||
// Discarded meldet die tonlose Verwerfung einer Direktansage am null-Sink.
|
||||
// Outcome describes a successful announcement: which tier spoke, how long
|
||||
// synthesis (chain walk up to the bytes) and playback took, and whether it
|
||||
// was a direct announcement (no bytes, the provider plays itself; then
|
||||
// Synthesis is the total time and Playback is 0). Discarded reports the
|
||||
// silent discarding of a direct announcement on the null sink.
|
||||
type Outcome struct {
|
||||
Provider string
|
||||
Direct bool
|
||||
@@ -254,7 +251,7 @@ type Outcome struct {
|
||||
Playback time.Duration
|
||||
}
|
||||
|
||||
// SpeakOutcome ist Speak mit Bericht (Diagnose: `talk test`, Daemon-Log).
|
||||
// SpeakOutcome is Speak with a report (diagnosis: `talk test`, daemon log).
|
||||
func (c *Chain) SpeakOutcome(ctx context.Context, text, lang string) (Outcome, error) {
|
||||
start := time.Now()
|
||||
r := c.walk(ctx, text, lang)
|
||||
@@ -262,16 +259,16 @@ func (c *Chain) SpeakOutcome(ctx context.Context, text, lang string) (Outcome, e
|
||||
switch {
|
||||
case r.data != nil:
|
||||
if c.cfg.Player == nil {
|
||||
return Outcome{}, fmt.Errorf("kette: kein Player konfiguriert — Bytes können nicht abgespielt werden")
|
||||
return Outcome{}, fmt.Errorf("chain: no player configured; bytes cannot be played")
|
||||
}
|
||||
playStart := time.Now()
|
||||
if err := c.cfg.Player.Play(ctx, r.data); err != nil {
|
||||
return Outcome{}, fmt.Errorf("kette: Wiedergabe über %s fehlgeschlagen: %w", c.cfg.Player.Name(), err)
|
||||
return Outcome{}, fmt.Errorf("chain: playback via %s failed: %w", c.cfg.Player.Name(), err)
|
||||
}
|
||||
return Outcome{Provider: r.used.Name(), Bytes: len(r.data), Synthesis: synth, Playback: time.Since(playStart)}, nil
|
||||
case r.direct != nil:
|
||||
if c.cfg.Player != nil && c.cfg.Player.Name() == NullPlayerName {
|
||||
chainLogf(c.cfg, "kette: Direktansage verworfen (audio.sink=null)\n")
|
||||
chainLogf(c.cfg, "chain: direct speech discarded (audio.sink=null)\n")
|
||||
return Outcome{Provider: r.directName, Direct: true, Discarded: true, Synthesis: synth}, nil
|
||||
}
|
||||
if err := r.direct.SpeakDirect(ctx, text, lang); err != nil {
|
||||
@@ -283,22 +280,22 @@ func (c *Chain) SpeakOutcome(ctx context.Context, text, lang string) (Outcome, e
|
||||
}
|
||||
}
|
||||
|
||||
// noTierErr bündelt den Ende-ohne-Ergebnis-Fehler eines Durchlaufs: keine
|
||||
// Stufe lieferte Bytes oder Direktansage — übersprungene (nicht verfügbar)
|
||||
// und fehlgeschlagene Stufen werden genannt, nie still.
|
||||
// noTierErr bundles the no-result error of a walk: no tier delivered bytes
|
||||
// or a direct announcement; skipped (unavailable) and failed tiers are
|
||||
// named, never silent.
|
||||
func (r walkResult) noTierErr() error {
|
||||
return fmt.Errorf("kette: keine Stufe konnte sprechen (nicht verfügbar: %s; fehlgeschlagen: %s)",
|
||||
joinOr(r.skipped, "keine"), joinOr(r.failed, "keine"))
|
||||
return fmt.Errorf("chain: no tier could speak (not available: %s; failed: %s)",
|
||||
joinOr(r.skipped, "none"), joinOr(r.failed, "none"))
|
||||
}
|
||||
|
||||
// chainLogf schreibt eine Protokoll-Zeile, wenn ein Log gesetzt ist.
|
||||
// chainLogf writes a log line if a log is set.
|
||||
func chainLogf(cfg ChainConfig, format string, args ...any) {
|
||||
if cfg.Log != nil {
|
||||
fmt.Fprintf(cfg.Log, format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// joinOr verbindet Einträge mit sep; ist die Liste leer, kommt fallback.
|
||||
// joinOr joins items with "; "; if the list is empty, fallback is returned.
|
||||
func joinOr(items []string, fallback string) string {
|
||||
if len(items) == 0 {
|
||||
return fallback
|
||||
|
||||
Reference in new issue
Block a user