feat(voice): M47 r1 探活地址单向回退到 voice-tts.json 的 baseUrl(组合根)

塔 2026-10-26 裁决:主人若把合成端口从 9880 改掉,不该还要记得两处一起改。
回退只做一向,且只发生在组合根(包内不互相读配置):

1. voice-tts-launch.json 显式写了 probeUrl ⇒ 以它为准,**永不**被别的键压过;
2. 没写 ⇒ 用 voice-tts.json 的 baseUrl(同一个服务);
3. 两者都空(或 voice-tts.json 坏掉)⇒ 默认 http://127.0.0.1:9880。

启动 Info 行同时打印 probe_url 与 probe_url_source(voice-tts-launch.json /
voice-tts.json / default),一眼看出用的是哪个源。反向(拿这里地址去改合成面)
一律不做。VoiceTTSTuning 坏掉时只回落默认地址并 WARN,**不**因此关掉自启。

新增三条用例(均已变异验证打红):
- ProbeURLInheritedFromVoiceTTS:真让探活打到自定义端口(hits>0),摘掉回退即红
- ProbeURLFallsBackToDefault:两者都空 ⇒ 9880 + source=default
- ExplicitProbeURLWins:显式压过 baseUrl,且 baseUrl 一次都不被探
This commit is contained in:
AskaEth committed 2026-10-26 18:50:52 +08:00
1 parent 6906d274fc
commit faf18afb91
2 files changed
+163 -1

No files matched your search

+59 -1
View File
@@ -11,12 +11,16 @@ package main
// 落不到后端的启动路径上。
// 3. **失败只 WARN**:脚本不存在、起进程失败、探活超时都只是带 source 的日志,
// 本函数**没有 error 返回**,绝不阻塞或改坏后端启动。
// 4. **探活地址单向回退**(塔 2026-10-26 裁决,见 resolveProbeURL):launch 文件
// 显式写了 `probeUrl` 就以它为准;没写则用 `voice-tts.json` 的 `baseUrl`;两者
// 都空才是默认 9880。反向(拿这里的地址去改合成面)一律不做。
//
// 零新 `.env` 键(`.env` 已冻结):开关与脚本路径都走面板文件家族
// (`runtimeconfig`,与 `voice-tts.json` 同一套读取口径)。
import (
"context"
"strings"
"time"
"git.yeij.top/AskaEth/Cyrene_v2/internal/config/runtimeconfig"
@@ -46,10 +50,14 @@ func (a *app) startVoiceTTSLaunch(ctx context.Context) *ttslaunch.Launcher {
"path", runtimeconfig.VoiceTTSLaunchConfigRelPath, "hint", `set {"enabled": true}`)
return nil
}
launcher := ttslaunch.New(voiceTTSLaunchConfig(tuning), ttslaunch.WithLogger(log))
resolved := a.resolveProbeURL(tuning.ProbeURL)
launcher := ttslaunch.New(withProbeURL(voiceTTSLaunchConfig(tuning), resolved), ttslaunch.WithLogger(log))
cfg := launcher.Config()
log.Info("emotionTTS auto-launch enabled",
"probe_url", cfg.ProbeURL,
// 探活地址可能来自三处,日志必须**同时**给出地址与来源:主人改端口后
// 一眼能看出实际问的是哪个地址、以及它是显式写的还是兜底来的。
"probe_url_source", resolved.probeSource,
"script", cfg.ScriptPath,
"probe_timeout_ms", cfg.ProbeTimeout.Milliseconds(),
"cooldown_sec", int(cfg.Cooldown/time.Second),
@@ -59,6 +67,56 @@ func (a *app) startVoiceTTSLaunch(ctx context.Context) *ttslaunch.Launcher {
return launcher
}
// probeResolution 是"探活地址 + 它的来源"(来源只用于日志,绝不参与判定)。
type probeResolution struct {
url string
probeSource string
}
// 探活地址的三个来源标签(写进启动 Info 行,供主人一眼核对)。
const (
probeSourceLaunchConfig = "voice-tts-launch.json"
probeSourceVoiceTTS = "voice-tts.json"
probeSourceDefault = "default"
)
// resolveProbeURL 决定这次到底探哪个地址 —— **单向**回退,且只在组合根发生:
//
// 1. `voice-tts-launch.json` 显式写了 `probeUrl` ⇒ 用它(**永不**被别的键压过);
// 2. 没写 ⇒ 兜底用 `voice-tts.json` 的 `baseUrl`(同一个服务,主人换端口只该改
// 一处;这正是"两处一起改"的 footgun 的收口);
// 3. 两者都没写(或 voice-tts.json 坏了)⇒ 空串,由 `ttslaunch.Config.Normalized`
// 补默认 `http://127.0.0.1:9880`。
//
// 🔴 只做这一向:反向(拿 launch 的地址去改 `voice-tts.json` 的 `baseUrl`,或用它
// 顶替合成地址)一律不做 —— 合成面与"起没起"是两个问题,前者不能被后者的配置影响。
func (a *app) resolveProbeURL(explicit string) probeResolution {
if url := strings.TrimSpace(explicit); url != "" {
return probeResolution{url: url, probeSource: probeSourceLaunchConfig}
}
log := logger.WithSource(a.log, voiceTTSLaunchSource)
tuning, err := runtimeconfig.LoadVoiceTTSTuning(runtimeconfig.DefaultVoiceTTSPath(resolveDataDir(a.cfg)))
if err != nil {
// 坏文件 ⇒ 这里只回落默认地址(**不**因此关掉自启:那是 launch 文件自己的
// 事),并且必须说清楚"我没读到 baseUrl"。
log.Warn("voice tts config unreadable, probe url falls back to default",
"path", runtimeconfig.VoiceTTSConfigRelPath, "err", err)
return probeResolution{probeSource: probeSourceDefault}
}
if base := strings.TrimSpace(tuning.BaseURL); base != "" {
log.Debug("probe url inherited from voice tts config",
"path", runtimeconfig.VoiceTTSConfigRelPath, "base_url", base)
return probeResolution{url: base, probeSource: probeSourceVoiceTTS}
}
return probeResolution{probeSource: probeSourceDefault}
}
// withProbeURL 把解析出的探活地址填进 launcher 配置(空串留给 Normalized 补默认)。
func withProbeURL(cfg ttslaunch.Config, r probeResolution) ttslaunch.Config {
cfg.ProbeURL = r.url
return cfg
}
// voiceTTSLaunchConfig 把面板形状映射成 launcher 的配置。
//
// 它**不**在这里裁决缺省值(脚本路径/探活地址/超时/冷却的默认值全在