sun
|
90faf083fd
|
docs(m4): 重写 README,聚焦 WebUI 安装 + 老用户迁移 + 资源 ID 章节
README 旧版 (~535 行) 仍以 BYTEDANCE_TTS_* env 为主线, 与 v0.2.0 实际架构脱节。
M3 起业务配置已全部入 DB, env 只剩 4 个引导变量。
新版 README (重写, ~300 行) 调整:
结构重排:
- 快速开始: 编译 → 启动 → 浏览器 /setup 引导(默认 4 步)→ 调用
- 环境变量: 4 个引导 env + DB 业务配置对照表
- WebUI 使用: /setup 4 步说明 + /admin 5 个 tab + 客户端可见错误码表
- API 端点: /v1/audio/speech + /health + /metrics + /admin + /setup + /dashboard
- 老用户迁移: 11 个 BYTEDANCE_TTS_* 旧 env 逐项映射到 /admin 设置
- 资源 ID 章节: 重要! 强调 v3 API 只允许 2 个 ID, volc.megatts.* 是 1.0 不是 2.0
- 观测 / Metrics: 14 个指标, 告警示例 (tts_config_load_failures_total)
- 公网部署: 鉴权清单 + /metrics /dashboard 反代保护
- 部署: Docker / Systemd
- 架构: 13 个包
- FAQ: 9 条
- 技术支持 checklist
内容更新:
- 删 BYTEDANCE_TTS_* 详细配置章节(老的 11 个变量, 全搬 DB 了)
- 加 "v0.2.0 老用户迁移" 完整映射表
- 资源 ID 章节纠正 1.0 vs 2.0 区别
- /health body 改用 v0.2.0 真实 schema (含 error 字段)
- 错误码表加 400/403/503 客户端可见错误
- 模型名更正: 复刻 2.0 子模型 = seed-tts-2.0-standard (和资源 ID 不同)
- 强调 4 个 bootstrap env + DB 业务配置分工
未提交 (待 M4 全部完成)
|
2026-08-30 23:32:10 +08:00 |
|
sun
|
621f9f847e
|
docs(m4): 收敛文档/dock/changelog,env 减到 4 个引导变量
M3 后所有业务配置(api_key/resource_id/speaker/format/sample_rate/...)已搬入
数据库,env 只剩 4 个启动引导参数(setup 前 / DB 还不存在时必需):
1. TTS_ADMIN_KEY 安装 token(/api/setup 鉴权)
2. TTS_DB_PATH DB 路径(可选)
3. PORT 监听端口(可选)
4. OPENAI_TTS_API_KEY OpenAI 端鉴权(可选)
之前 .env.example 仍列出 11 个 BYTEDANCE_TTS_* env,误导用户把配置填 env 里
(实际被 DB 覆盖,改了不生效)。
改:
- .env.example: 只列 4 个 bootstrap env,其他写"通过 WebUI 设置"
- docker-compose.yml: env 块只透传 4 个,加 named volume 持久化 tts.db
(无 volume = 容器重启丢所有配置)
- Dockerfile: 准备 /data 目录,appuser 可写
- CHANGELOG.md: 新文件,记 v0.2.0 全部 M0-M3 + 这次修复
不动:
- README.md(M4 第 5 步独立 commit,文件大)
- 集成测试(M4 第 6 步)
e2e 验证:
- go build ./... 通过
- 旧 .env 文件如果存在仍然兼容(env var 作为 DB 缺失时的 fallback)
未 push (待 M4 全部完成)
|
2026-08-30 23:23:53 +08:00 |
|
sun
|
9e38566952
|
fix(startup): normal 模式配置损坏 → fail-fast, 加 health error 字段 + metric
Bug 报告 (用户排查后提出):
- main.go:61-68 在 normal 模式下 LoadRuntimeConfig 失败只打 log.Printf
- TTSConfigErr 被设, /v1/audio/speech 返 503
- 但服务进程继续跑, 监听端口
- 运维看到"服务在跑", 排查成本高
- /health 虽然 503 + body 有 config_error:true, 但普通用户不会看 body
- 没 metric 计数, 监控告警配不出来
修法 (P0+P1+P2 一起):
P0 - main.go: mode 区分
- setup mode + 失败 = WARN (没装正常)
- normal mode + 失败 = log.Fatalf (进程退出, K8s 拉起, 触发告警)
P1 - dto/health.go + controller/tts.go:
- ConfigStatusResponse 加 Error 字段 (omitempty)
- 错误时填 setting.TTSConfigErr.Error()
- /health body 运维一眼看出原因
P2 - metrics/metrics.go:
- 加 tts_config_load_failures_total counter, labeled by mode
- 不管 fail-fast 还是 warn 都计数, 但 mode 区分
- Prometheus 告警: rate(tts_config_load_failures_total{mode="normal"}[5m]) > 0
e2e 验证 (本机, 3 场景):
- 空 DB / setup 模式 → /health 200 + body error 字段 + 进程不退出 ✓
- 装好 OK / normal 模式 → /health 200, body 无 error 字段 ✓
- 装完 + default_resource_id='' (损坏) / normal 模式 →
log 出现 [FATAL] "service cannot start in normal mode..."
进程退出, K8s 拉起会循环触发, 直到 DB 修好 ✓
副作用:
- fail-fast 后, 自愈回退逻辑不变 (installer.Detect 仍把损坏 db
备份 + 转 setup mode), 但用户已正常装过的 db 不会被自愈删除
- 不影响启动日志, LogStartupSummary() 仍照常打印
- 不影响 setup 模式, 该 WARN 还是 WARN
未 push (待用户)
|
2026-08-30 22:43:07 +08:00 |
|
sun
|
a655f8ae0e
|
fix(tts): voice.enabled=0 真生效,返回 403 voice_disabled
Bug: admin UI 切换"启用/禁用"toggle → DB 字段更新 ✓,
但 controller/tts.go 路由时不查 enabled, 关掉的 voice 仍能正常调通。
admin 关闭形同虚设。
修法: 在 voice 查库命中后, 立即检查 v.Enabled:
- enabled=1 → 走原流程
- enabled=0 → 立即 403, 错误码 voice_disabled
Body 格式(对齐 OpenAI error 规范):
{
"error": {
"code": "voice_disabled",
"message": "voice 'chun' is disabled",
"type": "invalid_request_error"
}
}
为什么 403 (vs 400 / 410):
- 400: 客户端发错 — 不准, voice 存在, 只是被关
- 410: 资源 gone — 不准, 不是永久弃用, 可能再开
- 403: 资源存在但无权用 — 准确, admin 关 = 拒绝调用
(OpenAI 其他端点对禁用资源用 403, 这是惯例)
e2e (本机):
- TEST 1: voice=chun, enabled=1 → 500 (volcano 假 key 401, 路由通了)
- TEST 2: voice=chun, enabled=0 → 403 voice_disabled ✓
- TEST 3: voice=ghost, 不存在 → 400 unknown_voice (原有行为不变)
副作用:
- 没有任何 admin 路径失败 (admin 改 enabled 字段直接生效, 无需 reload)
- metrics 没加新 label (后续可加 voice_disabled 计数, 留给 M4)
未 push (待用户)
|
2026-08-30 19:25:05 +08:00 |
|
sun
|
3f18f85925
|
fix(ui): setup 默认 resource_id 改 seed-icl-2.0
与 admin 同一类问题 (见 admin 修复 commit):
项目是豆包声音复刻大模型 2.0, 资源 ID 必为 seed-icl-2.0。
setup.html 错的默认值有 5 处:
1. form.default_resource_id placeholder: volc.megatts.default
2. v.resource_id placeholder: volc.megatts.icl
3. v.model placeholder: seed-tts-2.0-standard
4. form 默认 (default_resource_id): volc.megatts.default
5. form 默认 voices[0] + addVoice push: volc.megatts.icl / seed-tts-2.0-standard
修法同 admin: resource_id 改 seed-icl-2.0, model 保持 seed-tts-2.0-standard
(用户后澄清模型名独立)。
附带修:
- step 2 hint 文案从 "volc.megatts.icl 用于复刻音色" 改为
"本项目为豆包声音复刻大模型 2.0, 固定使用 seed-icl-2.0"
(避免误导用户填错资源 ID)
未 push (待用户 push)
|
2026-08-30 15:57:00 +08:00 |
|
sun
|
155017a27b
|
fix(ui): admin 默认 resource_id 改 seed-icl-2.0
背景: 项目是豆包声音复刻大模型 2.0, v3 API 只允许 2 个
资源 ID: seed-tts-2.0 (普通 TTS) + seed-icl-2.0 (复刻 2.0)。
本项目用复刻 2.0, 资源 ID 必为 seed-icl-2.0。
Bug 报告: admin (admin.html) 表单有 5 处默认值都是错的:
1. settingsForm.default_resource_id placeholder: volc.megatts.default
2. form.resource_id placeholder: volc.megatts.icl
3. form.model placeholder: seed-tts-2.0-standard (1.0 模型, 项目用不了)
4. form resource_id 默认: volc.megatts.icl
5. form model 默认: seed-tts-2.0-standard
+ openAdd 重置
+ settingsForm.model placeholder: seed-tts-2.0-standard
volc.megatts.* 是 1.0 API 的资源/服务品类编码, 不是 v3 API 2.0 的合法
资源 ID, 装完 admin 一看就是错的, 用户不知道要改成什么。
修法:
- 所有 resource_id 默认值 + placeholder 改成 seed-icl-2.0
- model 默认值保持 seed-tts-2.0-standard (用户确认模型名)
(用户后澄清: 模型在 v3 API 2.0 里只此一个名字, 不跟 resource 同步)
- 不硬编码 — 用户仍可改
受影响的 7 处:
- admin.html: settingsForm.default_resource_id placeholder
- admin.html: settingsForm.model placeholder
- admin.html: form.resource_id placeholder
- admin.html: form.model placeholder
- admin.html: form 默认 (resource_id)
- admin.html: form 默认 (model) + openAdd reset
- 全部 7 处 detail 见 diff
未 push (待用户 push)
|
2026-08-30 15:57:00 +08:00 |
|
sun
|
ad4727c507
|
fix(tts): voice 命中 log 跟合成 log 一致打码
Bug 报告: 同一次请求, controller/tts.go:193 输出明文 speaker
[tts] voice=chun 命中 (speaker=S_G8tEKnaJ1 ...)
但 adapter/volcano/synthesis.go:148 输出打码
TTS 合成成功 - 音色=S_G8****naJ1
不一致 — 同一进程同一请求, 一处裸奔一处打码, 脱裤子放屁。
修法: 全部用 telemetry.MaskSpeaker (前4+****+后4)。
不动 telemetry.SpeakerLabel (sha1[:8] 不可逆哈希, /metrics 用, 保留)。
注意: server log 仍能看到带前缀/后缀的 speaker (e.g. S_G8****naJ1),
但 *完整* speaker 不可见, 防 log 泄漏给非 root 用户。
如果 log 路径对外可读, 仍需考虑更严的方案 (e.g. 整体不 log speaker),
但自托管单用户场景下, 这个折中 OK。
未 push (待用户 push)
|
2026-08-30 15:56:59 +08:00 |
|
sun
|
65247b095a
|
ux(setup): voice.resource_id 留空时自动用 settings.default_resource_id
问题: step 2 有 "默认资源 ID" 字段,但 step 3 的每个 voice 行
也有 "资源 ID" 字段,要求用户填 2 次。用户经常只在 step 2 填,
留空 step 3 的 resource_id,导致 voice 行拿到 form placeholder
"volc.megatts.icl" 或空串。结果 settings 和 voice 不一致,
运行时火山 500(mismatch / not granted)。
修法 (后端兜底,无需前端改动):
1. 装时校验放宽: resource_id 留空是允许的(原来是必填)
2. 装时遍历 voices, 如果 voice.resource_id == "" 就用
settings.default_resource_id 兜底, 并记 log
用户收益: step 3 voice 行的 resource_id 字段可以不动,
step 2 填一次就行,始终保持一致。
可选前端优化 (待前端做): step 2 的 "默认资源 ID" 改变时,
自动同步所有 voice 行的 resource_id (placeholder 提示已自动填充)。
e2e 验证 (本机):
- 提交 install body 中 voice[0].resource_id=""
- 启动 log: '[setup] voice "chun" resource_id 留空,自动用 default_resource_id="seed-icl-2.0"'
- dumpdb 看 voice chun.resource_id = seed-icl-2.0 (从 settings 兜底) ✓
二进制: ~18MB → ~18MB (代码量几乎不变)
未 push (待用户确认)
|
2026-08-30 14:07:03 +08:00 |
|
sun
|
8a4187720c
|
fix(tts): settings.default_resource_id 不再被 voice 行覆盖
Bug 001 (后端):
之前 f5563e6 修 55000000 时,LoadRuntimeConfig 拿 default_speaker 查
voice 表后,把 voice.Speaker/ResourceID/Model 全部覆盖了 settings 里
的对应值。本意是修 speaker,副作用是 default_resource_id 永远被
voice.resource_id 覆盖 → 用户在 setup 设的 default_resource_id
(seed-icl-2.0) 没机会生效,被 voice 行的 (volc.megatts.icl) 替换。
修法 (option B: settings 优先 + voice 兜底):
- speaker: 从 voice 表查 (default_speaker 是 voice 名,要解析成真 ID)
- resource_id: 保持 settings 里的(用户偏好)
- model: voice 行的优先,settings 里的次之(向后兼容, 不动)
字段语义重新明确:
- settings.default_resource_id: 全局默认 resource(用户必设,运行时用)
- voices[].resource_id: 仅在 voice= 显式传时使用(per-voice 覆盖)
- 用户保持两个一致即可
附带修复: controller/tts.go 的注释误导(说 voice 为空用
setting.TTSOptions.Speaker 即 default_speaker),实际是 LoadRuntimeConfig
已解析为真 speaker ID。
测试:
- 已有 store/installer/middleware/router/telemetry 测试全绿
- e2e (本机): install settings.default_resource_id=seed-icl-2.0
voice chun.resource_id=volc.megatts.icl (故意不一致)
→ 启动 log: 'resource=seed-icl-2.0' (settings 优先 ✓)
→ 火山 55000000/45000030 → 401 Invalid X-Api-Key (说明参数对,key 假)
→ 换真 key 即能 work
二进制大小: ~18MB → ~18MB (代码量不变)
未 push (待用户确认)
|
2026-08-30 13:09:40 +08:00 |
|
sun
|
92cf8fab8a
|
fix(ui): dashboard 加 loadSettings, CORS banner 终于不假阳性了
Bug FE-002:
- /admin dashboard 顶部黄条"跨域 CORS 未配置"永远显示
- 原因: reloadAll() 没调 loadSettings, dashboard 上 settings.value 一直是 {}
- banner 判断 `!settings.cors_configured` → undefined → falsy → 触发
- 只有手动进 settings tab 触发 loadSettings() 后才消失
- 刷新页面又出现(被用户当成"CORS 没生效")
修法: 一行。reloadAll() 加上 loadSettings(), mount 时一并加载,
dashboard 上 settings 就有真值了, banner 判断正确。
diff:
- const reloadAll = () => { loadOverview(); loadVoices(); };
+ const reloadAll = () => { loadOverview(); loadVoices(); loadSettings(); };
副作用: 无。 loadSettings 自带 try/catch, 即使 /api/settings 失败
也不影响 overview/voices 加载。 watch(tab) 仍会再调一次
loadSettings (idempotent, 覆盖 fresh 值)。
验证 (本机 + 你的报告复现):
- 强刷 admin → mount 时 3 个 API 并行加载
- settings.value 填好, cors_configured = true
- banner `v-if="settings && !settings.cors_configured && ..."` 评估为 false → 不显示
未 push(等你确认)
二进制: ~18MB → ~18MB (HTML 嵌入尺寸不变)
|
2026-08-30 13:04:26 +08:00 |
|
sun
|
aa3f09752e
|
tools: 加 cmd/dumpdb ops 工具
dumpdb 是 read-only 的 SQLite 探查工具:
go run ./cmd/dumpdb /path/to/tts.db
输出:
- 所有表
- schema_version
- settings(敏感字段打码: api_key/auth_key/speaker_id 等)
- voices(完整行,无打码 — 这是 ops 用)
- journal_mode/page_size 等 pragmas
用法: 服务器 db 排查时,直接下 tts.db 下来用这个查,
不用 sqlite3 CLI,也不用写 SQL。
无依赖(用已有的 modernc.org/sqlite)。
未 push(收尾 commit,跟之前一起发)
|
2026-08-30 11:45:08 +08:00 |
|
sun
|
f5563e6f31
|
fix(tts): default_speaker 查 voice 表,改后立即生效
Bug 链:
1. 【核心 bug】setting/config.go LoadRuntimeConfig 把
"default_speaker" 当成"火山 speaker ID"用 — 但 DB 里存的是
voice **名字**(如 "chun"),不是 speaker ID (如 "S_G8tEKnaJ1")。
结果: TTSOptions.Speaker = "chun", TTSOptions.ResourceID 仍来自
default_resource_id 配置。调 /v1/audio/speech 不传 voice 时,
把 "chun" 当 speaker ID 发给火山 → 55000000 resource ID mismatched。
2. 【次生 bug】装完 install 没 reload TTS 配置。
启动期 LoadRuntimeConfig 失败(无 lock 无 settings), TTSConfigErr 被设;
装完 mode 变 normal 但 TTSConfigErr 没清, /v1/audio/speech 仍 503/500
(要重启才生效)。
修法:
- setting/config.go: 拿到 default_speaker 后调 GetVoiceForTTS(name)
查 voice 表, 用 voice.Speaker / ResourceID / Model 覆盖。
(新增 store.Store interface 方法 GetVoiceForTTS,4 元组返回值
避免循环 import;store 包加 GetVoiceForTTS 实现,内部包 VoiceGetByName)
- controller/setup.go: 装完 install 调 LoadRuntimeConfig 一次,
让 TTSConfigErr 立即清,TTSOptions 立即可用。
测试:
- 已有 store/installer/middleware/router 测试全绿
- e2e (本机): install voice "chun" + speaker "S_G8tEKnaJ1" →
启动 log 显示 "speaker=S_G8tEKnaJ1" (不是 "chun") ✓
→ /v1/audio/speech 不传 voice → 火山收到正确 speaker,
错误从 55000000 变成 401 Invalid X-Api-Key (假 key,换真 key 就 work)
二进制大小: ~18MB → ~18MB (代码量不变)
未 push (等用户验证)
|
2026-08-30 01:53:30 +08:00 |
|
sun
|
d22abecf12
|
fix(ui): store.Voice 加 JSON tag,修「音色是空的」+ UI 整体换皮
本次 3 个改动:
1. **【BUG 修复】store.Voice 加 JSON tag** — 用户报「进入系统音色是空的」。
实际:DB 里有数据,API 也返了。Bug 在 store.Voice 结构体没加 json tag,
Go 序列化为大写("Name"/"Speaker"/"ResourceID"/"Enabled"),而所有
admin.html 用 v.name/v.speaker/v.resource_id/v.enabled 拿,全是 undefined。
表渲染了但每格空。修复后 API 返小写,前端能正常读。
写 store/voices_test.go 加 TestVoice_JSONTags 防退化。
2. **【UI 换皮】Codex 改造 admin.html + setup.html**
- admin.html: 31KB → 58.8KB (1075 行)
加: 侧边栏 + 品牌 + 4 步导航、toast 通知系统、grid 卡片、
搜索/筛选/状态分页、stat-card 带 pill、modal + 过渡动画、
按钮大小/颜色变体、响应式字体缩放
- setup.html: 13KB → 23KB (428 行)
加: 4 步向导 + 进度条 (步骤 1: 上游凭证 / 2: 默认路由 /
3: 音色列表 / 4: 确认)、语音行 hover、确认页打码展示
- 功能完全保留: API 路径/字段/hash 路由/401 拦截都未改
3. **【小修】setup.html 装完跳转回退用 /admin 不是 /dashboard**
Codex 改的版本回退写成 /dashboard,是回归。已改回 /admin。
测试:
- 已有 store/installer/middleware/router/telemetry 测试全绿
- 新增 store/voices_test.go: TestVoice_JSONTags + TestVoice_UnmarshalTags
E2E 验证(本机):
- install 2 voices → DB 写入 ✓
- GET /api/voices 返小写 id/name/speaker/resource_id/enabled 等 ✓
- PATCH /api/voices/qian/toggle → 200 + lowercase 回包 ✓
- POST /api/voices → 201 + lowercase 回包 ✓
- GET /api/voices 再次列 3 条(原 2 + 新 1)✓
未 push (等用户测试「音色终于显示出来」)
二进制大小: ~18MB → ~18.5MB (admin.html +27KB embed)
|
2026-08-30 00:51:23 +08:00 |
|
sun
|
7660db14d8
|
fix(cors): CORS 全 DB 化,install 模式完全跳过,WebUI 可配
完整方案: 与 auth_key 同思路,让 CORS 也走 DB,彻底干掉 ALLOWED_ORIGINS env。
设计:
- install 模式: CORS 中间件直接 next(无 CORS 头也无 Vary),让装时永远能成功
(用户首次装不可能提前知道自己的访问域名)
- normal 模式: 走 CORS 检查,但有 2 道防线:
1) 同源豁免 (isSameOrigin) — Origin 匹配 Host 直接放行
2) 跨域白名单 — 读 setting.CORS (DB > env)
- DB 字段:
- cors_allow_all (bool): 允许所有(*)
- cors_origins (string): 换行分隔白名单,后端 trim+lower+去末尾 /
- WebUI: /admin 设置 tab 加 CORS 段(allow_all checkbox + origins textarea + 保存)
- 顶部 banner: cors_configured=false 时显示黄条提示,引导去设置
新增:
- middleware/cors.go: 顶部 installer.GetMode() == ModeSetup 时直接 next
- setting/config.go: SplitOriginsForCORS 导出 helper(供 controller 复用)
- controller/settings.go: SettingsCORSRequest + SettingsCORSHandler
- 校验每个 origin 必须 http:// 或 https:// 开头
- 写完立即刷新 setting.CORS.AllowAll + setting.CORS.Origins(下个跨域请求生效)
- 拒绝空 body
- router/router.go: 挂 /api/settings/cors (PUT, RequireAdmin)
- router/admin.html: 设置 tab 加 CORS card + 顶部 banner (31.19KB, 略超 30KB 预算可接受)
改造:
- setting/config.go LoadRuntimeConfig: 顺便读 cors_allow_all + cors_origins,
DB > env 兜底
- controller/settings.go SettingsResponse: 加 cors_allow_all + cors_origins + cors_configured
e2e 跑通(本机)10 场景:
- install 任意 Origin → 200 (install mode 跳 CORS)
- normal 同源 → 200
- normal 跨域无 allowlist → 403
- GET settings cors_configured=false
- PUT /api/settings/cors → 200
- GET settings cors_configured=true
- 跨域命中白名单 → 200
- 跨域未命中白名单 → 403
- allow_all=true → 任意跨域 200
- 重启后 CORS 仍从 DB 加载
未 push(等用户测试 install 是否顺利,以及 CORS banner 是否合适)
|
2026-08-30 00:08:14 +08:00 |
|
sun
|
1bed8d8ed0
|
fix(cors): 同源请求跳过 CORS 校验,避免浏览器自家人拦自家人
修 bug: 用户从浏览器访问 /setup 提交表单时,浏览器对同源 POST
也会设 Origin 头(防 fetch 滥用)。原 CORS 中间件只看 Origin 是否
在 ALLOWED_ORIGINS 白名单,未配白名单时同源 POST 会被 403 拒,
表现为"安装失败: status code 403"。
此前用 curl 测试时没 Origin 头(CORS 跳过),所以本地测不出来。
浏览器必带。
修法: 在 CORS 校验前加同源豁免 — 比较 Origin 与 r.Host(含
X-Forwarded-Host / X-Forwarded-Proto 反向代理场景),一致就放行,
不进入 CORS 检查、不设 Vary 头。
测试 (被 .gitignore 排除, 本地保留):
- middleware/cors_test.go: 新增
- TestIsSameOrigin: 10 场景
- 空 origin / 同 host+port / 同 host https / 不同 port / 不同 host
- 大小写不敏感 / XFH 代理 / XFH 不匹配 / origin 带 path / 无 scheme
- TestSplitOrigin: 5 场景
- https://example.com:8080 / http://localhost / 带 path
- not-a-url / ://broken
e2e 验证 (本机跑通):
- install 无 Origin (curl): 200
- install 同源 Origin: 200 ✓ 修复
- install 跨域 Origin: 403 (仍正确拒绝)
- install 不同端口 Origin: 403 (仍正确拒绝)
未 push (等用户确认)
|
2026-08-30 00:00:21 +08:00 |
|
sun
|
a4ea53bfa0
|
fix(auth): OpenAI 端 key 也走 DB,setup 是单一配置入口
修 M2/M3 遗漏:OPENAI_TTS_API_KEY (admin + 合成鉴权) 此前只能从 env
读,setup 前端没收集。装完用户还要回去设 env,体验断,违反
'setup 是单一配置源' 承诺。
改动:
- setting/config.go: LoadRuntimeConfig 顺便把 auth_key 灌到
setting.Auth.APIKeys。优先级 DB > env(env 仅作 fallback,首次启
动无 DB 时仍可用,向后兼容)。
- controller/setup.go: validateSetupSettings 加 auth_key 为必填,
写 DB 后立即 setting.Auth.APIKeys = [auth_key](本进程内立刻生效)。
- controller/settings.go: GET /api/settings 返 auth_key (打码) +
auth_key_set;新增 SettingsAuthKeyHandler (PUT /api/settings/auth-key),
改完单独刷新 setting.Auth.APIKeys(避免被自踢,只下次请求生效)。
- router/router.go: 挂 /api/settings/auth-key 路由。
- router/setup.html: 加 OpenAI 鉴权 Key 字段(密码框,带提示);
表单 default + submit 都加 auth_key。
- router/admin.html: 设置 tab 拆两张卡 — 鉴权 Key(独立保存按钮,
改完会自踢当前会话)+ 火山 TTS 凭证;vue setup 加 authKeyInput +
saveAuthKey 函数。
- main.go: 把 LoadRuntimeConfig(st) 提到 LogStartupSummary 之前,
让启动日志反映真实运行时状态(DB 已加载的 key 数量)而不是
过时的 env 状态。
未 push(等用户当前 M3 测完一起或单独)
|
2026-08-29 23:18:51 +08:00 |
|
sun
|
9db038dd05
|
feat(settings): M3 全局设置 + voice 动态路由 + env 收敛
M3 完成:env 业务配置全进 DB,WebUI 可改,改完立即生效(无需重启)。
新增:
- controller/settings.go: 3 个 handler
- GET /api/settings: 读所有设置,api_key 永远打码
- PUT /api/settings: 部分更新(白名单字段);default_speaker 改时校验
音色必须在 voices 表里(避免引用不存在);改完调 LoadRuntimeConfig 即时生效
- PUT /api/settings/api-key: 独立端点,只改 api_key
改造:
- setting/config.go: 用 LoadRuntimeConfig(store) 替代 InitTTSConfig
- 不再读 BYTEDANCE_TTS_* env,完全从 store.Settings 拿
- 必填项缺失返 error,设 TTSConfigErr(/v1/audio/speech 返 503)
- 定义 Store 接口(避免 setting → store 循环 import)
- 字段映射: BYTEDANCE_TTS_API_KEY → api_key, BYTEDANCE_TTS_RESOURCE_ID
→ default_resource_id, ... 全部一一对应
- controller/tts.go: voice 路由
- voice 为空 → 用 setting.TTSOptions.Speaker (即 store.default_speaker)
- voice 非空 → 查 voices 表,替换 opts.Speaker / ResourceID / Model
- 命中但 enabled=0 → 仍可用(显式 voice 覆盖)
- 未命中 → 400 unknown_voice
- voice 路由日志: "[tts] voice=xxx 命中 (speaker=... resource=... model=...)"
- router/admin.html: 加 #settings tab (24.78KB, 仍 <30KB 预算)
- API key 区:打码显示 + 独立更新按钮
- 路由/格式区:default_resource_id / default_speaker (下拉源 /api/voices) /
default_format / sample_rate / model
- "保存" 按钮 PUT /api/settings + 立即 reload (settingsOk 提示)
- router/router.go: 挂 /api/settings[/{api-key}] 三路由
- main.go: Detect 之后调 LoadRuntimeConfig,失败也允许进入
(env 阶段无 db,LogStartupSummary 仍显示 BYTEDANCE_TTS_* 必填项用于诊断)
测试(都被 .gitignore 排除,本地保留):
- 已有 store/installer/middleware/router/telemetry 测试保持全绿
- e2e 验证(本机跑通 9 场景):
- install 写入 3 voices + 7 settings
- 重启后 main.go log: "TTS 运行时配置已加载(api_key=***, speaker=default-voice, resource=volc.megatts.icl, format=wav)" — 确认走 DB 不读 env
- GET /api/settings 返所有字段
- voice=clone-voice-1 命中 S_clone_1 + seed-tts-2.0-expressive
- voice=clone-voice-2 命中 S_clone_2 + volc.megatts.icl-2
- voice=unknown 返 400 unknown_voice
- PUT /api/settings 改 default_speaker 即时生效
- PUT /api/settings/api-key 改 key 即时生效
- PUT /api/settings default_speaker=ghost 返 400 default_speaker_missing
注意: e2e 里的 TTS 500 是因为我用假 key 调上游,路由逻辑本身正确。
换真火山 API key 即可 work。
二进制大小: ~17MB → ~18MB (+1MB)
未 push(等用户测试 M1+M2+speaker fix 全部稳定后,跟 51bd7ae+70865a3 一起或单独 push)
|
2026-08-29 22:40:53 +08:00 |
|
sun
|
70865a3cf4
|
feat(admin): M2 WebUI 管理后台(鉴权+仪表盘+音色 CRUD)
新增 /admin 管理后台,Vue3 + axios 单文件 SPA。
新增:
- middleware/admin_auth.go: RequireAdmin 中间件
复用 OPENAI_TTS_API_KEY(Bearer),密钥为空等同无鉴权;
常量时间比较防计时攻击(用 common.SecureEqualString);
401 + WWW-Authenticate 头 + JSON 错误体。
- controller/admin.go: 5 个 admin handler
- GET /api/admin/overview: 模式/db路径/lock/版本/运行时间/音色数/内存
- GET /api/admin/metrics: 鉴权版 Prometheus 文本(无鉴权版 /metrics 仍给 Prometheus)
- GET /api/voices: 列表(含 disabled)
- POST /api/voices: 新增,409 处理重复,400 处理字段不合法
- DELETE /api/voices/{name}: 删除,409 处理被 default_speaker 引用
- PATCH /api/voices/{name}/toggle: 启停
- router/admin.html: 单文件 SPA(18KB)
登录页(单 input) + 仪表盘(metrics cards) + 音色 tab(表格+新增+启停+删除);
hash 路由切 tab;axios 拦截器自动加 Bearer,401 回登录页;
sessionStorage 存 key(关浏览器失效)。
- common/constants.go: SecureEqualString 提取到公共包
controller/setup.go + middleware/admin_auth.go 共用,避免重复实现。
改造:
- router/router.go: 挂 /admin(/admin HTML 公开,鉴权由前端 JS 拦截)
+ /api/admin/{overview,metrics} + /api/voices{,/{name},/{name}/toggle}
全部 API 端点套 RequireAdmin;正常模式 / → 302 /admin(M2 优先于 /dashboard)。
- main.go: 启动期调 SetAdminStore + SetMetricsTextWriter(避免 controller → metrics cycle)
- metrics/metrics.go: AdapterRecorder 用 SpeakerLabel 替代明文 speaker
(这是第 4 处 speaker 泄漏,顺带补上;前面 3 处在 51bd7ae 已修)。
- controller/setup.go: 用 common.SecureEqualString 替自己的实现(去重)。
测试(都被 .gitignore 排除,本地保留):
- router/router_test.go: 更新 4 个 NotFound 测试路径(/admin 现是注册路由,不再 404)
- telemetry/labels_test.go: 已有 SpeakerLabel/MaskSpeaker 测试(M2 复用)
e2e 验证(本机跑通):
- /admin 200 HTML(无鉴权,前端 JS 拦截)
- /api/admin/overview 无 Bearer 401
- /api/admin/overview 错 Bearer 401
- /api/admin/overview 对 Bearer 200 + JSON
- /api/voices GET 200 + 2 voices
- /api/voices POST 201 + new voice
- /api/voices/{name} DELETE 200
- /api/voices/{name}/toggle PATCH 200 + updated voice
- 正常模式 / 302 /admin
- 启动日志 BYTEDANCE_TTS_SPEAKER 仍打码 S_G8****naJ1(前次 fix 生效)
二进制大小: 16MB → ~17MB(admin.html +5 个 handler + 中间件)
未 push(用户要求与 speaker fix 51bd7ae 一起发,等服务器测完 M1 后一起验证)
|
2026-08-29 21:53:19 +08:00 |
|
sun
|
51bd7ae37a
|
fix(privacy): speaker ID 不再裸奔(3 处)
speaker 是火山复刻音色 ID,用户付费资产,不能裸奔。
本次修复 3 处泄漏:
1. controller/tts.go: /metrics label 用 sha1(speaker)[:8] 替代
- /metrics 无鉴权,Prometheus 可枚举,原值直接暴露是最大的泄漏面
- 哈希替代保留 per-voice 聚合能力,反推不出原值
- Admin UI 想看原名通过 /api/voices 拿 name 字段
2. setting/config.go: 启动日志 LogStartupSummary 用 MaskSpeaker
- API Key 早用 maskAPIKey,speaker 漏了
- 现在 S_G8tEKnaJ1 形式
3. adapter/volcano/synthesis.go: 合成 debug + 成功日志用 MaskSpeaker
- 之前 resource_id=xxx speaker=原始ID 直接打
- 现在打码
新增 telemetry/labels.go:
- SpeakerLabel(s) -> sha1(s)[:8] (8 字符十六进制,稳定、不可逆、同输入同输出)
- MaskSpeaker(s) -> 前 4 + **** + 后 4 (空串返中文未设置,长度 <=4 全打码)
- 空 speaker label -> 'unknown' (Prometheus 禁止空 label)
单测 telemetry/labels_test.go 覆盖:
- SpeakerLabel: 同输入同输出 / 不同输入不同输出 / 空串 unknown / 长度 8 / 全是 hex
- MaskSpeaker: 各种长度边界 + 真实火山 ID 形式
e2e 验证(本机):
- 启动日志看到打码形式,无明文
- 自动化 grep 原始 ID 在 server log: 0 匹配
- go test ./... 全绿
未 push(用户要求先修后等 M2 一起发)
|
2026-08-29 21:42:57 +08:00 |
|
sun
|
dbaee35400
|
fix(install): M1 后续 · 2 个 install 模式入口 bug
Bug 1: 安装模式下浏览器访问 / 看到 503 JSON
- 根因: middleware/installguard.go 默认白名单没包含 /;
浏览器访问 / 时 InstallGuard 先于 router 的 redirect 拦掉,返 503 JSON。
- 修复: InstallGuard 非白名单分支加 Accept 内容协商,
text/html → 302 Location: /setup(浏览器友好),
其它(API 客户端 / curl / 空 Accept)→ 维持 503 JSON(API 行为不变)。
- 额外: mux 路由未匹配时不走 r.Use() 中间件链,
手动设 r.NotFoundHandler 复用同一份 Accept 协商逻辑,
覆盖 /admin /api/voices 等任意未注册路径。
Bug 2: 装完访问 /setup 仍能进入(看到安装表单)
- 根因: router.go 的 /setup handler 没做 mode 校验。
- 修复: handler 顶部加 if ModeNormal → 302 Location: /admin
(M2 之后才有 /admin;目前会 404 也比继续显示表单好)。
测试(都被 .gitignore 排除,本地保留):
- middleware/installguard_test.go: 加 2 个 case(浏览器 redirect / API JSON)+ acceptsHTML 单元
- router/router_test.go(新): /setup 装后 redirect + NotFoundHandler 4 个 case
- 全套 go test ./... 全绿
验证(本机 e2e 跑通):
- setup 模式: 浏览器 /, /admin, /api/voices → 302 /setup
- setup 模式: API/curl / → 503 JSON(行为不变)
- normal 模式: /setup → 302 /admin(Bug 2)
- normal 模式: /admin → 404(M2 才有,行为不变)
- normal 模式: /api/setup/status → 200 {installed:true,mode:normal}
二进制大小: 15.87 → ~16MB(几乎不变)
|
2026-08-29 21:16:08 +08:00 |
|
sun
|
7984c1880a
|
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 + 新包)
|
2026-08-29 20:44:33 +08:00 |
|
sun
|
865fdd81e8
|
feat(store): M0 SQLite 存储层 + modernc.org/sqlite
- 引入 modernc.org/sqlite(纯 Go 无 CGO,保持单二进制分发)
- store/db.go: Open/Close/IntegrityCheck + WAL/foreign_keys/synchronous PRAGMA
- store/settings.go: key-value CRUD,Get/Set/Delete/GetAll/SetBatch,带 int/bool/duration 类型 helper
- store/voices.go: 完整 CRUD + ErrDuplicate/ErrNotFound/ErrInUse,name 白名单 [a-zA-Z0-9_-]{1,64}
- store/migrate.go: schema 版本号占位(migrate 调度留待 v2)
- 27 个单测覆盖:CRUD/Pragma/Idempotent/Integrity/Unique/DefaultSpeaker 保护
- 测试覆盖率 70.7%,测试二进制 +0.87MB(预期 +30MB,实际大幅低于预期)
- 不改 setting/config.go、main.go、router、controller(按 M0 计划保持改动局部)
验证: go vet ./... 干净; go test ./... store 包全绿
|
2026-08-29 20:29:28 +08:00 |
|
sun
|
dc3248d11c
|
chore: 转移前备份 — cleanup + project book
- 删 VULNERABILITY_REPORT.md(已审完,合并到 commit 历史)
- 删 middleware/ratelimit_test.go(单测延后)
- .gitignore 加 *_test.go 规则(防误提交)
- README XFF 验证段措辞更新
- 新增 PROJECT_BOOK.md(v0.1 待评审)
基线备份:目录迁移到非云盘路径前的快照。
|
2026-08-29 18:40:36 +08:00 |
|
sun
|
8be40ae6dc
|
chore: embed health.html 到 binary,单文件分发
把 health.html 从外部依赖改为 //go:embed 内嵌,
实现真正的"单 exe 分发"。
- 新建 router/dashboard.go,用 //go:embed 嵌入 health.html
- router/router.go:/dashboard handler 改为 w.Write(dashboardHTML)
- setting/config.go:删 CheckStaticFiles(无需文件检查)
- main.go:删 setting.CheckStaticFiles() 调用
- Dockerfile:删 COPY health.html 步骤(已嵌入)
- .gitignore:加 release/(build 产物不入 git)
- 物理移动 health.html 到 router/(embed 限制:同包或子包)
效果:
- 之前: release 包 = tts-api.exe + health.html (2 个文件)
- 之后: release 包 = tts-api.exe (1 个文件,~7 MB)
- 用户部署无需额外文件,/dashboard 路由直接工作
|
2026-08-27 11:44:44 +08:00 |
|
sun
|
cdc9a7c94b
|
ci: 添加 GitHub Actions Docker 发布 workflow
为 Gitea→GitHub 镜像 + GitHub 镜像仓库场景准备 Docker 镜像
自动构建和发布流程。
触发条件:
- push tag v* (release 时打 tag 自动触发)
- workflow_dispatch (手动触发,可在 GitHub UI 测试)
构建:
- 多平台:linux/amd64 + linux/arm64
- 通过 QEMU + Buildx 跨架构构建
- 推送到 ghcr.io/<github-user>/volcano-engine-tts-ui
- 双标签:语义版本 + 短 commit hash
- OCI labels 包含 version 和 revision
与之前 version 包配合:
workflow 用 git describe --tags --always --dirty 算 VERSION
(commit hash 类似),通过 --build-arg 传给 Dockerfile,
Dockerfile 用 -ldflags 注入到 version.Version / version.Commit
两个变量。最终 /health 端点返回构建时注入的真实版本信息。
前置条件(用户需自行完成):
1. GitHub 建镜像仓库
2. Gitea 配置 Push Mirror 到 GitHub(开启 Sync Tags)
3. 仓库设为 Public(否则 GHCR 镜像默认 private)
|
2026-08-27 10:43:19 +08:00 |
|
sun
|
c00c46e7a1
|
chore: 注入构建时版本信息到 /health 端点
现状:controller/tts.go:232 硬编码 Version: "2.0.0 (v3 API)",
不会随代码变化而更新,/health 无法反映实际跑的代码。
修复:四文件改动,实现构建时 ldflags 注入。
1. 新建 version/version.go,声明两个包级变量:
Version (默认 "dev")
Commit (默认 "dev")
2. dto/health.go:HealthResponse 加 Commit 字段(JSON 输出多一字段)
3. controller/tts.go:HealthResponse 用 version.Version / version.Commit
替代硬编码字面量
4. Dockerfile:加 ARG VERSION=dev ARG COMMIT=dev,
go build 时通过 -ldflags 注入到 version 包的两个变量
效果:
- 本地 go build (不传 ldflags) → version=dev commit=dev
- 开发 build (git describe) → version=v0.2.0-5-g4abcd5 commit=g4abcd5
- release build (打 tag 后) → version=v0.2.1 commit=<对应 hash>
测试环境(develop 分支,无 tag)显示距离上次 release 几个 commit +
具体 hash;生产环境(main + tag)显示干净 semver。
|
2026-08-27 10:18:34 +08:00 |
|
sun
|
3b3aa3b708
|
chore: 清理 DEBT-2 死代码(7 文件,约 30 行)
VUL-003 修复期间意外发现项目遗留一批死代码,本次一并清掉:
- middleware/ratelimit_middleware.go(37 行,物理删除)
文件内 RateLimit / ConcurrencyLimit 函数从 977e9cc 创建后
从未被引用,370a217 commit 用 ratelimit_instrumented.go
(带 metrics 埋点 + 路径过滤)取代了它。占用包体,清。
- middleware/auth.go:InitAPIKeys(6 行)
注释说"已在 setting.InitAuthConfig 中完成",无 op。
- middleware/cors.go:InitCORSConfig(6 行)
同上,setting.InitCORSConfig 已做实际工作。
- dto/tts.go:ByteDanceTTSConfig 类型(7 行)
完整的配置走 setting.TTSOptions + adapter/volcano.Options,
此类型从未被任何代码实例化。
- setting/config.go: var _ = dto.ByteDanceTTSConfig{} 占位(3 行)
配合上方类型删除,移除 dto import。
- controller/tts.go:resolveClientFormat
合并 if reqFmt == "" 与 default 分支(都返回
setting.TTSOptions.Format),2 行简化。
- common/constants.go: MaxResponseTimes / MaxErrors
定义后从未被任何文件引用。
- middleware/ratelimit_instrumented.go 顶部注释
移除对"原 ratelimit_middleware.go"的悬空引用,
改为描述本文件相对路由使用实现的两个增强点。
影响:
- 包体减少约 30 行
- 降低新人接手时的代码理解成本
- 零功能变更,24 个现有测试用例全过
|
2026-08-27 00:57:49 +08:00 |
|
sun
|
695b3ecf25
|
docs: README 修正 speed 与 sample_rate 描述 (VUL-008 / VUL-009)
VUL-008 (信息): speed 超范围静默截断无提示
修复:「API 使用说明」speed 描述从"0.25 ~ 4.0"改为
"客户端接受范围 0.25 ~ 4.0;火山实际生效范围 0.5 ~ 2.0
(speech_rate [-50, 100]),超出范围会被静默截断,客户端无感反馈"。
选 TODO 方案 1(诚实修正文档),不改代码。
VUL-009 (信息): WAV 采样率依赖配置而非上游实际值
修复:BYTEDANCE_TTS_SAMPLE_RATE 描述加风险提示
"此值直接写入 WAV 头,需与上游实际 PCM 采样率一致,
否则音频变速变调"。
上游 v3 协议不返回 sample_rate 字段,代码侧无法自动取真实值,
仅文档强化。
|
2026-08-26 16:16:56 +08:00 |
|
sun
|
7343d5aa5c
|
VUL-006 (低): 监控端点 (/metrics /health /dashboard) 无鉴权
判定:不引入新鉴权机制(会破坏 Prometheus 抓取),文档引导。
修复:「观测 / Metrics」section 末尾新增「公网部署:监控端点
无鉴权」段,含三端点风险表 + nginx 反代 basic auth 配置示例。
VUL-007 (低): 未设置 OPENAI_TTS_API_KEY 时鉴权完全关闭
判定:不改代码(无 API Key 即不鉴权是 README 明示的设计),
文档强化。
修复:三处加强:
1. 环境变量表 OPENAI_TTS_API_KEY 行加 🔴 公网必设 标记
2. 「API 使用说明」section 顶部 callout 警示
3. 「部署」section 末尾新增「公网部署安全清单」段,统一
覆盖鉴权关闭与监控端点保护,形成完整安全姿态
|
2026-08-26 16:06:17 +08:00 |
|
sun
|
171503d775
|
fix: VUL-002 修复 transport 错误埋点缺失
VUL-002 (低): transport 层错误不进入 UpstreamErrors 指标
原代码用 if errCode != 0 判断是否记录错误,但合成链路中
多种错误场景的 errCode 本身是 0(火山 v3 业务码非 0 时才
会传入),导致这些场景在 /metrics 上完全不可见:
- transport_error: client.PostStream 失败(DNS / 连接 / TLS)
- request_error : buildRequest 序列化失败
- wrap_error : WAV 头拼装失败
- stream_error (code=0): 读流错误
修复:把判断改为 if status != "ok",status 是上游调用全链
路的权威错误指示器,任何非 ok 状态都计为错误。
codeLabel(0) → "transport" 已有定义,修复后该分类真正生效,
火山接口不可达等网络故障首次在监控上可见。
向后兼容:已统计的 http_XXX 错误(status="http_xxx" != "ok")
行为不变,新增 transport / request / wrap / stream 错误进入指标。
|
2026-08-26 15:35:24 +08:00 |
|
sun
|
91b0c8acee
|
fix: VUL-005 修复日志注入(RequestURI 与上游错误体转义)
VUL-005 (低): 攻击者可在 HTTP 请求 URL 或上游错误响应中
注入 \n / \r 字符,伪造日志行干扰排障。无代码执行风险。
修复位置:
- middleware/logger.go: 访问日志中的 r.RequestURI 是未经
解析的原始请求行,客户端可控。转义 \n / \r 为字面字符
- adapter/volcano/synthesis.go: 上游非 200 响应体 (rawBody)
可能是攻击者控制的恶意内容,转义后再嵌入错误消息
|
2026-08-26 11:52:28 +08:00 |
|
sun
|
a238e5c2a4
|
chore: 忽略本地 TODO.md,避免误提交
TODO.md 是本地待办清单,通过任务看板追踪更合适,不入 git 仓库。
|
2026-08-26 00:37:15 +08:00 |
|
sun
|
272565f736
|
fix: VUL-003 完整修复,启发式/精确双模式 XFF 解析
VUL-003 (中): X-Forwarded-For 信任链可被伪造 IP 绕过限流
本服务定位为公网入口,即使单人使用,公网暴露意味着攻击面
与公开服务等同,不能"够用就行"。
采用渐进式披露设计,平衡易用性与功能性:
1. 启发式模式(默认, 不设环境变量 或 TRUSTED_PROXY_HOPS=0)
- 从 XFF 链尾扫描,跳过私有 IP,返回第一个公网 IP
- 适合 90% 部署(单跳/多跳/直出),无需了解精确跳数
- 限制:多跳 CDN 场景下,限流粒度为"按 CDN 边缘 IP"
- 直出部署:整个 XFF 分支不会执行
2. 精确模式(TRUSTED_PROXY_HOPS=N, N>0)
- 从 XFF 链尾倒数第 N+1 个位置取值
- 精准到真实 client,需按实际反代跳数正确配置
- N=1:单跳反代;N=2:CDN+反代;以此类推
3. 两种模式都从链尾扫描
- XFF 首值是客户端可控的,信任首值等于信任攻击者
- 链尾由受控的反代添加,天然免疫伪造绕过
4. 默认值从 1 改为 0(行为变化)
- 旧默认:精确模式 N=1,取 XFF 末值
- 新默认:启发式模式,跳过链尾私有 IP
- 对单跳场景行为相同
- 对多跳/链尾含私有 IP 场景新版更准确(返回真实公网 IP)
5. 配套
- middleware/ratelimit_test.go:24 个表驱动测试用例,
覆盖直出/单跳/多跳/伪造/畸形/精确 N 边界,全部通过
- setting/config.go:LogStartupSummary 显示当前 XFF 模式
- .env.example:重写说明,标注默认行为 + 何时需配
- README.md:新增"反代拓扑与 X-Forwarded-For 解析"章节
(何时需要/两种模式/行为对比/为什么从链尾/启动日志验证)
6. 已知边界:TRUSTED_PROXY_HOPS=00 等被 Atoi 解析为 0 的
输入归入启发式模式,日志不会出现"精确模式 0 跳"矛盾输出。
|
2026-08-26 00:36:22 +08:00 |
|
sun
|
ed3d7c6b61
|
本次代码审查(全 12 个包,约 2400 行)的交付物:
- 9 项漏洞(高 1 / 中 2 / 低 4 / 信息 2)
- 已核查无风险项 8 条
- 工程债务记录(零测试、死代码、云盘占用)
- 安全加固建议(Docker 密钥传递、TLS、依赖固定)
供后续按优先级处理备查。
"
|
2026-08-25 23:10:34 +08:00 |
|
sun
|
72d0d6a3a9
|
VUL-001 (中): aac/flac 响应 Content-Type 与真实数据不一致
controller.tts.go:contentTypeFor 对 aac/flac 返回 audio/aac/flac,
但 adapter/volcano/synthesis.go 在上游降级时仅修改 opts.Format,
finalFormat 仍保留 clientFormat,导致响应头与字节流不符。
修复:finalFormat 改为反映真实输出格式(非 wav 时取 opts.Format),
客户端按 AAC/FLAC 解码 MP3 流的失败场景消除。
VUL-004 (高): .env 凭据泄露
README 引导用户 cp .env.example .env 填密钥,但 .gitignore
未忽略 .env,任何 git add . 都会把含 BYTEDANCE_TTS_API_KEY
的文件提交进 git 历史,不可逆。
修复:
- .gitignore 新增 Secrets section,拦截 .env 与 .env.* 变体,
保留 .env.example 作为模板追踪
- .dockerignore 升级为同名规则模式,覆盖未来 .env.local /
.env.production 等变体,保证 git 与 docker 两通道一致
详见 VULNERABILITY_REPORT.md
"
|
2026-08-25 23:09:47 +08:00 |
|
sun
|
78c72004bf
|
chore: 删除 ratelimit_middleware.go.tmp 临时文件
与 ratelimit_middleware.go 内容完全重复(SHA256 一致,1116 字节),
且无任何代码引用 .tmp 路径,属于误提交的开发期残留文件。
|
2026-08-23 13:15:15 +08:00 |
|
sun
|
370a21760c
|
fix: 限流和并发中间件跳过非 /v1/ 路由,避免监控路径污染计数
|
2026-08-16 18:50:33 +08:00 |
|
sun
|
f971de6821
|
chore: 删除无用的预检测试脚本 _preflight.js
该脚本仅用于本地测试,不属于项目核心代码,清理冗余文件减少项目杂乱度
|
2026-08-16 18:43:56 +08:00 |
|
sun
|
0f7102a41d
|
fix: 补上 dashboard 缺失的 formatBytes 函数定义
|
2026-08-16 18:41:33 +08:00 |
|
sun
|
ec2b78533d
|
test: add preflight check test script for cors
新增了用于测试CORS预检请求的_node脚本,包含OPTIONS预检请求和实际POST请求的测试逻辑,用于验证接口的跨域配置是否正确。
|
2026-08-16 18:35:34 +08:00 |
|
sun
|
36e0d87cec
|
ui: 替换 unpkg 为 bootcdn 国内 CDN
|
2026-08-16 18:32:58 +08:00 |
|
sun
|
c4e278d544
|
ui: 重写监控面板,解析Prometheus指标分组展示,移除原始文本iframe
|
2026-08-16 18:13:04 +08:00 |
|
sun
|
5519697f87
|
fix: merge case sentence and empty to handle audio frames with empty event field
|
2026-08-16 18:03:21 +08:00 |
|
sun
|
78bb7d87ab
|
refactor: remove fallback audio collection in case empty after namespace fix
|
2026-08-16 17:56:49 +08:00 |
|
sun
|
d516dddf1d
|
fix: namespace 修正为 UnidirectionalTTS 匹配 HTTP 单向端点
|
2026-08-16 17:54:43 +08:00 |
|
sun
|
677f606935
|
fix: 修复上游省略event字段导致音频帧被丢弃的bug
|
2026-08-16 17:50:02 +08:00 |
|
sun
|
55244e9a45
|
fix: 修复 response.go switch 语句大括号结构错误
|
2026-08-16 17:32:00 +08:00 |
|
sun
|
483db85259
|
fix: 兼容上游 sentence 字段为对象/字符串两种形态,加 debug 打印无 event 元数据行
|
2026-08-16 17:27:20 +08:00 |
|
sun
|
b4b8ec33ed
|
docs: 重写README,补充BYTEDANCE_TTS_DEBUG调试日志独立小节
|
2026-08-16 14:41:07 +08:00 |
|
sun
|
cdd143f7e2
|
docs: 移除复刻1.0相关内容,仅保留2.0
|
2026-08-15 19:17:42 +08:00 |
|