feat(installer): M1 安装流程 + /setup 引导页
新增: - installer/lock.go: lock 文件检测/创建/删除(原子写入);ErrLockExists sentinel - installer/bootstrap.go: Detect() 启动期判定 ModeSetup/ModeNormal, 损坏自愈(自动备份 db.corrupt-<ts> + 删 lock + 回退安装模式) - middleware/installguard.go: 安装模式白名单(/setup /api/setup /health /metrics), 中间件顺序: SecurityHeaders → InstallGuard → RateLimit → ...(避免限流计数污染) - controller/setup.go: GET /api/setup/status + /api/setup/prefill + POST /api/setup; token 校验用常量时间比较防计时攻击;安装后端点永久 404 - router/setup.html: Vue3 + axios CDN,4 字段表单 + 动态音色行, 调用 /api/setup/prefill 自动从旧 env 预填(API key 永不预填) 改造: - main.go: 启动期 store.Open → installer.Detect → 注入 setup 控制器 - controller/tts.go: 安装模式双保险(/v1/audio/speech 即便漏过中间件也 503) - controller/health.go (via dto): 加 installed + mode 字段,部署探针可识别 - router/router.go: 挂载 /setup + /api/setup/*,根路径安装模式跳 /setup - setting/config.go: InitSetupToken 读 TTS_ADMIN_KEY 或随机生成 32 字符 hex (公网部署建议设 env;空时启动日志一次性打印) 删除: - router/dashboard.go: 内容并入 router/router.go .gitignore: tts.db / tts.db-* / installed.lock 加入(运行时产物) 验收(端到端跑通,见 scripts/ 已删除): 1. 删 lock → /v1/audio/speech 503 + /setup 200 HTML 2. POST /api/setup 错 token → 401;对 token → 200 + 写 lock + 写 db 3. 重复 POST → 404(端点永久关闭) 4. /health: 安装期 installed=false,装后 installed=true 5. 损坏 db header: 自动备份 tts.db.corrupt-<ts> + 删 lock + 回安装模式 测试: - installer: 6 个 test, 覆盖 lock 生命周期 + Detect 三态 + 损坏自愈 - middleware: 3 个 test, 覆盖白名单/denylist/自定义 - store(M0): 仍 27/27 绿, 70.7% 覆盖率 二进制大小: 9.77 → 15.87 MB (+6MB,大部分是 setup.html embed + 新包)
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
package setting
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
@@ -51,11 +53,26 @@ var Server ServerConfig
|
||||
// 不直接调用 middleware(避免循环 import)。
|
||||
var TrustedProxyHops int
|
||||
|
||||
// SetupToken 是安装模式下的初始化凭证。
|
||||
// - 若 TTS_ADMIN_KEY 环境变量非空,用其值(用户可复现,便于脚本化安装)
|
||||
// - 若 TTS_ADMIN_KEY 为空,启动时随机生成 32 字节十六进制,
|
||||
// 打印到日志(/api/setup 提交时必须带这个 token)
|
||||
//
|
||||
// 安装完成后,/api/setup 端点永久关闭,SetupToken 失去意义但保留在内存。
|
||||
var SetupToken string
|
||||
|
||||
// SetupTokenSource 标记 SetupToken 的来源,便于日志区分。
|
||||
// "env" = 来自 TTS_ADMIN_KEY
|
||||
// "ephemeral"= 启动时随机生成(每次启动变)
|
||||
// "" = 未设置
|
||||
var SetupTokenSource string
|
||||
|
||||
// InitAllConfigs 集中初始化所有配置,启动期调用一次。
|
||||
func InitAllConfigs() {
|
||||
InitServerConfig()
|
||||
InitAuthConfig()
|
||||
InitCORSConfig()
|
||||
InitSetupToken()
|
||||
TTSConfigErr = InitTTSConfig()
|
||||
}
|
||||
|
||||
@@ -213,6 +230,30 @@ func getEnvBool(name string, def bool) bool {
|
||||
return b
|
||||
}
|
||||
|
||||
// InitSetupToken 加载或生成安装模式下的初始化凭证。
|
||||
// - TTS_ADMIN_KEY 存在:用其值,SetupTokenSource="env"
|
||||
// - TTS_ADMIN_KEY 空:随机生成 16 字节 = 32 字符 hex,SetupTokenSource="ephemeral",打印到日志
|
||||
func InitSetupToken() {
|
||||
v := os.Getenv("TTS_ADMIN_KEY")
|
||||
if v != "" {
|
||||
SetupToken = v
|
||||
SetupTokenSource = "env"
|
||||
return
|
||||
}
|
||||
// 临时 token:16 字节随机 = 32 字符 hex,够用且短
|
||||
b := make([]byte, 16)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
// 极端情况:随机源失败,降级为时间戳(不应发生)
|
||||
log.Printf("[setup] 生成一次性 token 失败,使用时间戳: %v", err)
|
||||
SetupToken = fmt.Sprintf("dev-%d", time.Now().UnixNano())
|
||||
SetupTokenSource = "ephemeral"
|
||||
return
|
||||
}
|
||||
SetupToken = hex.EncodeToString(b)
|
||||
SetupTokenSource = "ephemeral"
|
||||
log.Printf("[setup] 一次性安装 token(仅打印一次,公网部署请设置 TTS_ADMIN_KEY): %s", SetupToken)
|
||||
}
|
||||
|
||||
// CheckEnvironmentVariables 返回 /health 用的环境变量状态快照。
|
||||
func CheckEnvironmentVariables() map[string]interface{} {
|
||||
required := map[string]bool{
|
||||
|
||||
Reference in New Issue
Block a user