docs: v0.2.4 阶段三 文档、示例配置与发布说明

文档与配置同步 v0.3.0 的破坏性变更:

README.md:
- 特性列表更新:管理入口 /dashboard、凭证分离、默认无匿名监控端点
- 环境变量表补充 METRICS_ALLOW_CIDR,并说明未配任何管理凭证时服务拒绝启动
- 端点文档重写:/healthz(匿名且不含字段)、/health(需鉴权)、
  /metrics(默认 404,两种使用方式)、管理页面与安装页对照表
  (含"页面需鉴权为何又能直接打开"的内容协商说明)
- 公网部署安全清单重写:按端点列出真实暴露面,并提醒探针必须指向 /healthz
- 故障排查、架构表、迁移表中的 /admin 引用统一改为 /dashboard

.env.example:新增 METRICS_ALLOW_CIDR 与 admin_key 说明(含 Docker 网段警告)。

两处部署陷阱修复(阶段一给 /health 加鉴权引发的连带问题):
- docker-compose.yml 的 healthcheck 原指向 /health,加鉴权后会一律 401,
  导致容器被判定为 unhealthy → 改为 /healthz
- Dockerfile 的 HEALTHCHECK 同样指向 /health → 改为 /healthz
  (这两处若不改,升级后容器健康状态会持续异常,且现象与鉴权改动看起来无关,排查成本高)

新增 docs/RELEASE_NOTES_v0.3.0.md:可直接用于创建 Release,
含 Breaking Change 清单、新特性、验证方式与实测结果、升级步骤。

docs/IMPLEMENT_v0.3.0.md:
- 按合并后的阶段编号重写实施进度表与阶段标题
  (原「阶段 1 收紧鉴权」与「阶段 2 收口端点」合并为「阶段 1」,后续顺延),
  并说明合并原因:两者只做前者是"有风险无收益"
- 记录本轮实际验证结果

验证:go build / go vet / go test ./... -count=1 全绿。
This commit is contained in:
sun
2026-10-04 01:17:43 +08:00
parent 5113941f44
commit 94b3557c7e
6 changed files with 719 additions and 41 deletions
+21 -1
View File
@@ -29,5 +29,25 @@ OPENAI_TTS_API_KEY=
# 装完后可改(也支持环境变量作为 DB 缺失时的 fallback)
# ==========================================
# X-Forwarded-For 解析跳数(可选, 0=启发式默认, N>0=精确 N 跳反代)
# 装好后推荐到 /admin 改 trusted_proxy_hops
# 装好后推荐到 /dashboard 改 trusted_proxy_hops
# TRUSTED_PROXY_HOPS=2
# ==========================================
# v0.3.0 新增
# ==========================================
# 根路径 /metrics 的内网 CIDR 白名单(可选, 逗号分隔, 也接受裸 IP)
# - 不配置:根路径 /metrics 完全不注册(访问 404)。
# Prometheus 请改用鉴权版 /api/admin/metrics 并带 Bearer 凭证。
# - 配置后:根路径 /metrics 仅对白名单来源开放,非白名单返回 404。
# ⚠️ Docker 部署必须填「容器内网网段」,不要填 127.0.0.1/32 ——
# 那是容器自身回环,宿主机/其他容器里的 Prometheus 进不来。
# 例:
# METRICS_ALLOW_CIDR=10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
# 管理后台专用凭证(可选)
# - 不配置:管理接口回退使用 DB 里的 auth_key(即 OPENAI_TTS_API_KEY 那套),行为与旧版一致。
# - 配置后:管理后台/管理接口只认这个 key,业务调用方拿到的 auth_key 无法访问管理接口。
# 建议通过 WebUI 配置(/dashboard → 设置),不必写在这里;此处仅作说明。
# 对应 DB 设置项:admin_key
+4 -1
View File
@@ -36,7 +36,10 @@ USER appuser
EXPOSE 8080
# v0.3.0:健康检查必须用 /healthz(匿名存活探针)。
# /health 现在需要管理凭证,而 HEALTHCHECK 不带 Authorization ——
# 继续用 /health 会让镜像一直被判定为 unhealthy。
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD wget -qO- http://localhost:8080/health || exit 1
CMD wget -qO- http://localhost:8080/healthz || exit 1
ENTRYPOINT ["./tts-api"]
+105 -36
View File
@@ -6,11 +6,14 @@
- 完全兼容 OpenAI `/v1/audio/speech` API
- **引导式安装**: 首次启动自动进入 `/setup` 向导,无需手写 env
- **WebUI 后台** `/admin`: 音色管理 / 全局设置 / CORS 配置 / 鉴权
- **WebUI 后台** `/dashboard`: 音色管理 / 全局设置 / CORS 配置 / 鉴权
- **音色库**: 多 voice 动态路由,未知 / 禁用 voice 返回明确错误
- **多格式输出**: mp3 / ogg_opus / pcm / wav (内部转 pcm + 本地拼头) / aac / flac
- **鉴权 + 限流**: API Key 校验, IP 速率限制, 全局并发限制
- **观测**: Prometheus 文本格式 `/metrics` + 服务状态 `/health`, 零外部依赖
- **凭证分离**(v0.3.0): 管理后台可配独立 `admin_key`,业务调用 Key 无法访问管理接口
- **观测**: Prometheus 文本格式 + 服务状态, **默认无匿名监控端点**(v0.3.0 起)
- `/healthz` 匿名存活探针(不含任何字段)· `/api/admin/metrics` 与 `/api/admin/health` 需鉴权
- 根路径 `/metrics` 默认 404,需 `METRICS_ALLOW_CIDR` 内网白名单才注册
- **跨平台**: Windows / Linux / macOS / Docker
## 快速开始
@@ -35,7 +38,7 @@ go build -o tts-api .
3. 音色列表: 每个 voice 的对外名 + 火山 speaker ID
4. 确认提交
完成后自动跳转到 `/admin`,从这里登录管理。
完成后自动跳转到 `/dashboard`,从这里登录管理。
### 3. 调用
@@ -56,9 +59,19 @@ curl -X POST http://localhost:8080/v1/audio/speech \
| `TTS_ADMIN_KEY` | ✓ (生产) | `/api/setup` 安装 token,首次安装时校验 |
| `TTS_DB_PATH` | ✗ | DB 路径,默认 `./tts.db` |
| `PORT` | ✗ | 监听端口,默认 `8080` |
| `OPENAI_TTS_API_KEY` | ✗ (生产) | OpenAI 端鉴权 key,可不设(内网) |
| `OPENAI_TTS_API_KEY` | ✓ (生产) | 业务端鉴权 key;未设 `admin_key` 时**也用于管理后台登录** |
**所有业务配置**(`api_key` / `default_resource_id` / `default_speaker` / `default_format` / `sample_rate` / `model` / `model_type` / `explicit_language` / `enable_subtitle` / `timeout` / `cors_origins` / `cors_allow_all` / `auth_key` / `trusted_proxy_hops`) — 全部在 `/admin` 设置,持久化到 SQLite。
v0.3.0 新增(均可选):
| 变量 | 用途 |
|---|---|
| `METRICS_ALLOW_CIDR` | 根路径 `/metrics` 的内网 CIDR 白名单(逗号分隔)。不配则根路径完全不注册 |
> ⚠️ **v0.3.0 起,normal 模式下未配置任何管理凭证(DB `admin_key` / DB `auth_key` /
> env `OPENAI_TTS_API_KEY` 三者皆空)时服务将拒绝启动** —— 因为管理接口在无凭证时会拒绝
> 所有请求,启动一个进不去后台的实例没有意义。
**所有业务配置**(`api_key` / `default_resource_id` / `default_speaker` / `default_format` / `sample_rate` / `model` / `model_type` / `explicit_language` / `enable_subtitle` / `timeout` / `cors_origins` / `cors_allow_all` / `auth_key` / **`admin_key`** / `trusted_proxy_hops`) — 全部在 `/dashboard` 设置,持久化到 SQLite。
env 仍可作为 fallback 读(老用户兼容),但**新用户应通过 WebUI 配**。
@@ -70,9 +83,9 @@ env 仍可作为 fallback 读(老用户兼容),但**新用户应通过 WebUI 配
1. **凭证**: 火山 API Key (必填) + OpenAI 鉴权 Key (可选)
2. **默认路由**: 默认资源 ID (`seed-icl-2.0`) + 默认音色名 + 默认格式 + 采样率
3. **音色列表**: 每个 voice 一行,填对外名 + 火山 speaker ID
4. **确认**: 提交写入 DB,自动跳 `/admin`
4. **确认**: 提交写入 DB,自动跳 `/dashboard`
### `/admin` 日常管理
### `/dashboard` 日常管理
| Tab | 用途 |
|---|---|
@@ -121,17 +134,31 @@ OpenAI 兼容,鉴权 `Authorization: Bearer <OPENAI_TTS_API_KEY>`(若已设)。
| `pcm` | pcm | `audio/pcm` |
| `aac` / `flac` | mp3 (降级) | `audio/mpeg` |
### `GET /health`
### `GET /healthz`
无鉴权,返回:
**无鉴权**。存活探针,**只返回 `200` 与字面量 `ok`,不含任何字段**。
供 K8s `livenessProbe`/`readinessProbe`、Docker `HEALTHCHECK`、负载均衡健康检查使用 ——
这些探针默认不带 `Authorization`,不能指向需要鉴权的 `/health`。
```bash
curl http://localhost:8080/healthz
# ok
```
### `GET /health`(v0.3.0 起需鉴权)
**需管理凭证**(`Authorization: Bearer <admin_key>`)。返回完整健康数据:
```json
{
"status": "ok", // ok | not_installed | configuration_error
"service": "ByteDance TTS to OpenAI API Adapter",
"version": "v0.2.0",
"commit": "621f9f8",
"version": "v0.3.0",
"commit": "abc1234",
"uptime": "3600 seconds",
"start_time": "2026-10-04T00:00:00Z",
"memory": { "heap_alloc": 0, "heap_inuse": 0, "goroutines": 0 },
"config_status": {
"all_required_vars_set": true,
"config_error": false,
@@ -145,16 +172,41 @@ OpenAI 兼容,鉴权 `Authorization: Bearer <OPENAI_TTS_API_KEY>`(若已设)。
- 正常: HTTP 200, `status: "ok"`
- 未安装: HTTP 200, `status: "not_installed"`
- 配置损坏: HTTP 503, `status: "configuration_error"`, `error` 字段有原因
- 无凭证: HTTP **401**
> v0.3.0 之前本端点是匿名的,会泄漏版本、commit、运行时长、内存与配置错误文本,现已收口。
> 管理面板内使用鉴权版 **`GET /api/admin/health`**(返回同样的数据)。
### `GET /metrics`
Prometheus 文本格式,无鉴权。主要指标见 [观测 / Metrics](#观测--metrics)。
**默认不注册**(访问 404)。两种使用方式:
### `GET /admin`, `GET /setup`, `GET /dashboard`
1. **配置 `METRICS_ALLOW_CIDR`**(内网 CIDR 白名单,逗号分隔):
根路径 `/metrics` 仅对白名单来源开放,**非白名单返回 404**(不是 403,避免确认端点存在)。
```bash
METRICS_ALLOW_CIDR=10.0.0.0/8,172.16.0.0/12
```
⚠️ Docker 部署请填**容器内网网段**,不要填 `127.0.0.1/32`(那是容器自身回环)。
2. **使用鉴权版 `GET /api/admin/metrics`**,带 `Authorization: Bearer <管理凭证>`。
Prometheus 侧需配 `authorization: { credentials: <管理凭证> }`。
- `/admin`: 后台 (Vue SPA),需鉴权
- `/setup`: 引导式安装页 (Vue SPA),无鉴权,装完自动跳转
- `/dashboard`: 服务状态预览页 (无鉴权)
Prometheus 文本格式。主要指标见 [观测 / Metrics](#观测--metrics)。
### 管理页面与安装页
| 路径 | 说明 | 鉴权 |
|---|---|---|
| `GET /dashboard` | 管理看板 | 需鉴权(浏览器请求返回页面外壳,数据走 API) |
| `GET /dashboard/voices` | 音色管理 | 同上 |
| `GET /dashboard/settings` | 全局设置 | 同上 |
| `GET /dashboard/status` | 详细服务状态(健康 + 指标看板) | 同上 |
| `GET /dashboard/login` | 登录页 | **公开**(否则无法登录) |
| `GET /setup` | 引导式安装页 | 无鉴权,装完自动跳转 |
| `GET /admin` 等旧路径 | 301 永久重定向到对应 `/dashboard/*` | — |
> **为什么页面是"需鉴权"却又能直接打开?** 浏览器的 `Accept` 含 `text/html` 时返回页面外壳,
> 由前端根据 `sessionStorage` 里的凭证决定显示登录视图;非 HTML 请求(脚本、抓取工具)
> 无凭证一律 401。页面外壳不含任何数据,数据全部来自鉴权后的 `/api/admin/*` 接口。
## 老用户迁移 (从 v0.1.0 → v0.2.0)
@@ -162,19 +214,19 @@ v0.1.0 用 env 配 11 个 `BYTEDANCE_TTS_*` 变量。v0.2.0 起改 WebUI:
| 旧 env | v0.2.0 配置入口 |
|---|---|
| `BYTEDANCE_TTS_API_KEY` | `/admin` 设置 → API Key |
| `BYTEDANCE_TTS_RESOURCE_ID` | `/admin` 设置 → 默认资源 ID |
| `BYTEDANCE_TTS_SPEAKER` | `/admin` 设置 → 默认音色 (填 voice **名字**,不是 speaker ID) |
| `BYTEDANCE_TTS_FORMAT` | `/admin` 设置 → 默认格式 |
| `BYTEDANCE_TTS_SAMPLE_RATE` | `/admin` 设置 → 采样率 |
| `BYTEDANCE_TTS_BIT_RATE` | `/admin` 设置 → MP3 比特率 |
| `BYTEDANCE_TTS_MODEL` | `/admin` 设置 → 子模型 |
| `BYTEDANCE_TTS_MODEL_TYPE` | `/admin` 设置 → 模型类型 |
| `BYTEDANCE_TTS_EXPLICIT_LANGUAGE` | `/admin` 设置 → 显式语言 |
| `BYTEDANCE_TTS_ENABLE_SUBTITLE` | `/admin` 设置 → 启用字级时间戳 |
| `BYTEDANCE_TTS_API_KEY` | `/dashboard` 设置 → API Key |
| `BYTEDANCE_TTS_RESOURCE_ID` | `/dashboard` 设置 → 默认资源 ID |
| `BYTEDANCE_TTS_SPEAKER` | `/dashboard` 设置 → 默认音色 (填 voice **名字**,不是 speaker ID) |
| `BYTEDANCE_TTS_FORMAT` | `/dashboard` 设置 → 默认格式 |
| `BYTEDANCE_TTS_SAMPLE_RATE` | `/dashboard` 设置 → 采样率 |
| `BYTEDANCE_TTS_BIT_RATE` | `/dashboard` 设置 → MP3 比特率 |
| `BYTEDANCE_TTS_MODEL` | `/dashboard` 设置 → 子模型 |
| `BYTEDANCE_TTS_MODEL_TYPE` | `/dashboard` 设置 → 模型类型 |
| `BYTEDANCE_TTS_EXPLICIT_LANGUAGE` | `/dashboard` 设置 → 显式语言 |
| `BYTEDANCE_TTS_ENABLE_SUBTITLE` | `/dashboard` 设置 → 启用字级时间戳 |
| `BYTEDANCE_TTS_TIMEOUT` | (保留 env 暂未搬 DB) |
| `ALLOWED_ORIGINS` | `/admin` → CORS → 跨域白名单 |
| `OPENAI_TTS_API_KEY` | `/admin` 设置 → OpenAI 端鉴权 key |
| `ALLOWED_ORIGINS` | `/dashboard` → CORS → 跨域白名单 |
| `OPENAI_TTS_API_KEY` | `/dashboard` 设置 → OpenAI 端鉴权 key |
**逐步迁移建议**:
@@ -251,14 +303,30 @@ Prometheus 文本格式,无鉴权。可直接被 Prometheus 抓取或浏览器
- 反代**仅**把鉴权后的请求转发到 `:8080`
- 此时 `OPENAI_TTS_API_KEY` 可不设
**`/metrics` / `/dashboard` / `/health` 均不鉴权**,生产环境务必通过反代保护:
### v0.3.0 起各端点的暴露面
v0.3.0 已把健康与指标端点收口,**默认不再有匿名可读的监控端点**:
| 端点 | 默认状态 | 说明 |
|---|---|---|
| `/healthz` | **匿名可读** | 只回 `200 ok`,**不含任何字段**,专供探针 |
| `/health` | 需鉴权 | 完整健康数据 |
| `/metrics`(根路径) | **默认 404** | 需 `METRICS_ALLOW_CIDR` 才注册 |
| `/dashboard/*` | 需鉴权 | 管理面板(HTML 请求返回外壳,数据走 API) |
| `/api/admin/*` | 需鉴权 | 管理 API(含 `/api/admin/health`、`/api/admin/metrics`) |
| `/api/public/*` | 无鉴权 | 预留空壳,当前无业务 |
因此反代**只需**确保业务与管理接口不被匿名滥用;监控端点已由应用自身把关:
```nginx
location /metrics { allow 10.0.0.0/8; deny all; } # 仅 Prometheus 服务器
# 若未配 METRICS_ALLOW_CIDR,根路径 /metrics 本就 404,无需额外规则。
# 若希望 Prometheus 走根路径,建议在应用侧配 METRICS_ALLOW_CIDR,而不是在反代做白名单。
location /dashboard { auth_basic "admin"; auth_basic_user_file /etc/nginx/.htpasswd; }
location /health { allow 10.0.0.0/8; deny all; } # 或 K8s liveness probe 直接访问
```
> ⚠️ **探针必须指向 `/healthz`**。K8s `livenessProbe`/`readinessProbe`、Docker `HEALTHCHECK`
> 默认不带 `Authorization`,若仍指向 `/health` 会一律 401,导致 Pod 反复重启 / 容器被判不健康。
## 部署
### Docker
@@ -269,6 +337,7 @@ docker compose up -d
```
`docker-compose.yml` 已配 named volume `tts-api-data` 挂载到容器 `/data`,DB 与 lock 文件持久化,容器重启不丢配置。
其 `healthcheck` 已使用 `/healthz`。
### Linux Systemd
@@ -282,7 +351,7 @@ docker compose up -d
| `installer/` | 启动期模式检测,DB 自愈回退 |
| `setting/` | 全局配置 (TTSOptions / Auth / CORS) + 启动汇总 |
| `store/` | SQLite 数据访问 (settings / voices) + 自愈 |
| `controller/` | /v1/audio/speech、/health、/setup、/admin、/api/setup、/api/settings、/api/voices |
| `controller/` | /v1/audio/speech、/healthz、/health、/setup、/dashboard、/api/setup、/api/admin/* |
| `middleware/` | SecurityHeaders, CORS, 鉴权, 限流, 并发, 日志, 客户端 IP 提取, install 模式守卫 |
| `router/` | 路由注册 |
| `adapter/volcano/` | 火山 v3 HTTP Chunked 客户端 |
@@ -339,12 +408,12 @@ PORT=8081 ./tts-api
如有问题,请检查:
1. 服务启动后日志第一段 "环境配置汇总" — 火山必填项是否全 ✓
2. `/health` 返回 `status: "ok"` 且 `config_error: false`
3. `/admin` → 设置 tab 检查 API Key / 默认资源 ID / 默认音色
4. `/admin` → 音色管理 tab 检查 voice 是否启用
2. `/healthz` 返回 `ok`(存活);详细状态用鉴权接口 `/api/admin/health`,应返回 `status: "ok"` 且 `config_error: false`
3. `/dashboard` → 设置 tab 检查 API Key / 默认资源 ID / 默认音色
4. `/dashboard` → 音色 tab 检查 voice 是否启用
5. 火山控制台 → 在线体验同一对 resource + speaker 能合成
6. 客户端请求 URL 是否以 https:// 开头 (公网)
7. `/admin` → CORS tab 检查白名单含前端完整 origin
7. `/dashboard` → 设置 → CORS 检查白名单含前端完整 origin
## 许可证
+11 -3
View File
@@ -13,15 +13,23 @@ services:
- TTS_DB_PATH=/data/tts.db
- PORT=8080
- OPENAI_TTS_API_KEY=${OPENAI_TTS_API_KEY:-}
# 可选,装好后也可在 /admin 改
# 可选,装好后也可在 /dashboard 改
- TRUSTED_PROXY_HOPS=${TRUSTED_PROXY_HOPS:-}
# v0.3.0 新增(可选):根路径 /metrics 的内网 CIDR 白名单,逗号分隔。
# 不配置时根路径 /metrics 完全不注册(Prometheus 请改用鉴权版 /api/admin/metrics)。
# ⚠️ 这里必须填「容器内网网段」,不要填 127.0.0.1/32 ——
# 那是容器自身回环,Prometheus 在宿主机/其他容器里根本进不来。
# 例:METRICS_ALLOW_CIDR=172.16.0.0/12
- METRICS_ALLOW_CIDR=${METRICS_ALLOW_CIDR:-}
volumes:
# DB + lock 文件必须挂载,否则容器重启后配置全丢
- tts-data:/data
restart: unless-stopped
healthcheck:
# 用 wget 检查,alpine 镜像有
test: ["CMD", "wget", "-qO-", "http://localhost:8080/health"]
# v0.3.0:必须用 /healthz(匿名存活探针)。
# /health 现在需要管理凭证,而健康检查不带 Authorization ——
# 继续用 /health 会让容器一直被判定为 unhealthy。
test: ["CMD", "wget", "-qO-", "http://localhost:8080/healthz"]
interval: 30s
timeout: 5s
retries: 3
+438
View File
@@ -0,0 +1,438 @@
# Volcano-Engine-TTS-UI v0.3.0 阶段性实施步骤书
> **文件名**:`IMPLEMENT_v0.3.0.md`
> **基线**:`develop`
> **版本目标**:v0.3.0 —— 路由鉴权架构升级、健康/指标端点收口、管理入口统一到 `/dashboard`
> **关联文档**:[UPSTREAM_ADAPTER_GUIDE.md](UPSTREAM_ADAPTER_GUIDE.md)(上游适配器重构,与本改造正交,互不干扰)
>
> 本文是 v0.3.0 的**唯一实施依据**,由初版方案按实际代码基线校正而成。
> 校正说明见 [附录 A](#附录-a与初版方案的差异)。
---
## 实施进度
> **编号说明**:初版方案的"阶段 1(收紧鉴权)"与"阶段 2(收口端点)"属于同一次安全改造、
> 必须一起上线才有意义(只做前者是"有风险无收益"),已**合并为现在的「阶段 1」**,
> 后续阶段依次顺延。下表的阶段号均为合并后编号。
| 阶段 | 内容 | 状态 | 提交 |
|---|---|---|---|
| 1 | 收紧 `RequireAdmin` + 独立 `admin_key` + 启动期校验 + 收口健康/指标端点 + `/healthz` + `METRICS_ALLOW_CIDR` | ✅ 已完成 | 见下 |
| 2 | 路由分层 + 旧路径别名 + `/admin`→`/dashboard` | ✅ 已完成 | 见下 |
| 3 | 文档 / 示例配置 / Docker 注释 / CHANGELOG | ✅ 已完成 | 见下 |
| 4 | 全量回归测试(发布前) | ⏳ 待执行 | — |
**阶段 3 额外修复的两处部署陷阱**(阶段 1 给 `/health` 加鉴权引发的连带问题):
- `docker-compose.yml` 的 `healthcheck` 原指向 `/health`;该端点加鉴权后会一律 401,
导致**容器被判定为 unhealthy** → 已改为 `/healthz`
- `Dockerfile` 的 `HEALTHCHECK` 同样指向 `/health` → 已改为 `/healthz`
---
## 0. 改造目标
| # | 目标 | 验收标准 |
|---|---|---|
| 1 | **收口健康/指标端点**:`/metrics`、`/health`、`/dashboard` 不再匿名可读 | 未携带管理凭证时不可读到任何健康数据 |
| 2 | **补前置缺口**:`RequireAdmin` 在凭证未配置时不再静默放行 | 未配置凭证时管理接口拒绝访问,且启动期有明确信号 |
| 3 | **权限隔离**:业务调用凭证不得访问管理接口 | 用 TTS 调用 key 访问 `/api/admin/*` 返回 401 |
| 4 | 管理入口统一:`/admin` → `/dashboard`,旧路径 301 兼容 | 访问 `/admin` 301 到 `/dashboard` |
| 5 | API 路由分层:`/api/public/*`、`/api/user/*`、`/api/admin/*` | 三条前缀各自挂对应中间件,`/api/user/*` 为桩 |
| 6 | 存量管理接口迁移到 `/api/admin/*`,旧路径继续可用 | 新旧路径行为一致,老脚本零改动 |
| 7 | 两套用量查询占位接口 | `/api/user/usage`(管理鉴权)、`/v1/dashboard/billing/usage`(Bearer) |
| 8 | 文档 / 示例配置 / Docker 注释同步更新,标注 Breaking Change | README、`.env.example`、`docker-compose.yml`、CHANGELOG 一致 |
| 9 | **不改动** `adapter/`、`store/` 核心业务模型,**不破坏** `/v1/audio/speech` | TTS 业务接口回归通过 |
### 重要约束
- v0.3.0 **仍是单用户模式**;`/api/user/*` 仅骨架 + 桩实现,完整多用户能力放 v0.4.0。
- **不引入 Cookie 会话**(理由见 [附录 B](#附录-b为什么不做-cookie-会话))。管理鉴权继续使用
`Authorization: Bearer <key>`,前端继续用 `sessionStorage` 保存凭证。
- **每个阶段结束必须可编译、可运行、可本地回归**,拒绝大爆炸式一次性修改。
---
## 1. 现状基线(开工前必须知道的事实)
本项目的真实鉴权现状与初版方案的假设**不同**,照着初版做会返工。以下是核对过代码的结论:
| 事项 | 真实现状 | 代码位置 |
|---|---|---|
| 管理鉴权中间件 | **只有 `RequireAdmin`**,Bearer 请求头校验。**不存在 `AdminSessionMiddleware`** | `middleware/admin_auth.go:20` |
| 凭证存储 | 前端 `sessionStorage['ttsAdminKey']`,**无 Cookie** | `router/admin-shell.js:10-15` |
| 凭证来源 | 数据库 `auth_key` → 回退环境变量 `OPENAI_TTS_API_KEY` | `setting/config.go:384-392` |
| 凭证复用 | **同一个 key 既登录后台、又调 TTS 接口**(本次要拆开) | `middleware/auth.go` + `middleware/admin_auth.go` |
| 指标实现 | 自研 `telemetry` 包,**未引入 `prometheus/client_golang`**,无 `promhttp` | `router/router.go:136` |
| 客户端 IP | 已有 `middleware.GetClientIP`,支持 XFF + `TRUSTED_PROXY_HOPS` | `middleware/ratelimit.go:196` |
| CORS | 非白名单 Origin 在中间件层直接 403 | `middleware/cors.go:138-147` |
### 1.1 匿名可读端点(本次要收口的全部)
| 端点 | 泄漏内容 | 代码位置 |
|---|---|---|
| `GET /dashboard` | **HTML 看板,同时拉 `/health` + `/metrics`**,聚合度最高 | `router/router.go:120` |
| `GET /metrics` | Prometheus 全量指标(流量、模型名、错误分布、计费字符数) | `router/router.go:136` |
| `GET /health` | 版本 / commit / 运行时长 / 内存 / goroutine + 配置错误文本 | `router/router.go:119` |
| `GET /api/setup/status` | normal 模式下仍返回 `{installed, mode}` | `router/router.go:75` |
> `/api/setup/prefill` 在 normal 模式返回 404(handler 内自检),**不可达**,无需处理。
### 1.2 已有但必须保留的机制
- `InstallGuard`:安装模式下按白名单放行(`/setup`、`/api/setup`、`/health`、`/metrics`),
安装阶段无数据库、无凭证,**不得套用 `RequireAdmin`**。`middleware/installguard.go:19`
- 现有 `/api/admin/metrics` 已有 `RequireAdmin`,返回同样的 Prometheus 文本(保留复用)。
- 现有静态资源路由 `/admin/admin.css`、`/admin/admin-shell.js`(`router/router.go:90-97`),
迁移时必须同步处理,否则页面丢样式。
---
## 2. 分阶段实施
### 阶段 1|鉴权收紧 + 健康/指标端点收口(本版本核心)
> 本节由初版方案的「阶段 1(收紧 `RequireAdmin`)」与「阶段 2(收口端点)」**合并**而成。
> 合并原因:两者是同一件事的两半 —— 只做前者(收紧空凭证放行)而不同时收口端点,
> 结果是"有风险无收益"(旧部署可能起不来,而 `/health`、`/dashboard`、`/metrics` 仍匿名可读)。
> 下面 1.1–1.3 对应初版的阶段 1,1.4–1.6 对应初版的阶段 2。
**为什么先做这一步**:`RequireAdmin` 当前在凭证列表为空时**直接放行**(`middleware/admin_auth.go:29-33`)。
不修这个,后面给 `/metrics`、`/health` 套 `RequireAdmin` **等于没套**。
**改动范围**:`middleware/admin_auth.go`、`middleware/auth.go`、`setting/config.go`、`main.go`
#### 1.1 空凭证不再放行
将 `len(keys) == 0 → next.ServeHTTP` 改为拒绝 + 显式日志:
```go
keys := setting.GetAdminKeys()
if len(keys) == 0 {
log.Printf("[admin_auth] 拒绝访问:未配置管理凭证(admin_key / auth_key 均为空) - 路径=%s", r.URL.Path)
denyAdmin(w, r)
return
}
```
#### 1.2 新增独立 `admin_key`(权限隔离)
- `store`:新增可选的设置项 `admin_key`(**无需数据库迁移**,`settings` 是键值表)。
- `setting`:新增 `GetAdminKeys() []string`,取值优先级:
```
admin_key(DB) → 回退 auth_key(DB) → 回退 OPENAI_TTS_API_KEY(env)
```
- `RequireAdmin` 改用 `GetAdminKeys()`;**`middleware.ValidateAPIKey`(`/v1/audio/speech` 用)保持只看 `auth_key`**。
- 效果:配了 `admin_key` 后,业务调用凭证**无法**访问管理接口,隔离成立;不配则行为与旧版完全一致(向后兼容)。
#### 1.3 启动期校验(fail-fast)
在 `main.go` normal 模式分支加入:管理凭证为空时**拒绝启动**并打印清晰原因
(与现有配置损坏 fail-fast 风格一致)。
> ⚠️ **破坏性提示**:若你的部署目前未配置任何凭证,本阶段后服务将无法启动。
> 这是刻意的——此前那种状态下管理接口是完全裸奔的。请先配好 `auth_key`(或 `admin_key`)再升级。
#### 1.4 ✅ 鉴权部分回归
- [ ] `go build ./... && go vet ./...` 通过
- [ ] 配置 `auth_key` 后:`/api/admin/overview` 带正确 key 返回 200,错误 key 返回 401
- [ ] 不配置任何凭证时启动:进程拒绝启动并给出明确日志
- [ ] 配置独立 `admin_key` 后:用 `auth_key` 访问 `/api/admin/*` 返回 401
- [ ] `/v1/audio/speech` 用 `auth_key` 调用**不受影响**
---
#### 1.5 端点收口(本版本核心目标)
**改动范围**:`router/router.go`、`controller/tts.go`、`middleware/`(新增 CIDR 白名单)、`setting/config.go`
#### 1.5.1 `/health` 拆分为两个端点
| 端点 | 鉴权 | 返回 |
|---|---|---|
| `GET /healthz` | **无鉴权** | 仅 `200` + 字面量 `ok`,**不含任何字段** |
| `GET /health` | `RequireAdmin` | 现有完整 JSON(版本/内存/配置状态等) |
> ⚠️ **`/healthz` 必须与 `/health` 鉴权在同一次改动内落地**。分开部署会出现探针空窗,
> 导致 K8s liveness 失败、Pod 反复重启。
> 📌 探针迁移提醒:若现有 K8s `livenessProbe` / `readinessProbe` / Docker `HEALTHCHECK`
> 指向 `/health`,需同步改指 `/healthz`。README 中要写明。
#### 1.5.2 `/metrics` 收口 + 可选内网白名单
新环境变量 `METRICS_ALLOW_CIDR`(逗号分隔多个 CIDR):
- **已配置**:在根路径注册 `/metrics`,包裹 `MetricsIPAllowListMiddleware`;
非白名单 IP 返回 **404**(不返回 403,避免向扫描者确认端点存在)。
- **未配置**:根路径 `/metrics` **完全不注册**,访问 404。
- 两种情况都**始终**提供鉴权版 `GET /api/admin/metrics`(已存在,供面板使用)。
中间件实现要点:复用 `middleware.GetClientIP(r)`(已处理 XFF / `TRUSTED_PROXY_HOPS`),
用标准库 `net/netip` 解析 CIDR。
> ⚠️ **Docker 环境**:`METRICS_ALLOW_CIDR` 必须填**容器内网网段**(如 `172.16.0.0/12`),
> **不要填 `127.0.0.1/32`** —— 那是容器自身的回环,Prometheus 在宿主机或其他容器里进不来。
#### 1.5.3 `/dashboard` 与 `/api/setup/status` 收口
- `/dashboard`:套 `RequireAdmin`。它是本版本**单点泄漏最严重**的端点(一个 URL 暴露全部健康数据)。
- `/api/setup/status`:套 `RequireAdmin`。
- **前端状态提示**:`/dashboard` 的 HTML 外壳本身不敏感,前端登录判断逻辑保持现状
(`sessionStorage` 无 key → 显示登录视图)。**不在服务端做页面级跳转**,原因见 [附录 B](#附录-b为什么不做-cookie-会话)。
#### 1.6 ✅ 端点收口回归
- [ ] 未配置 `METRICS_ALLOW_CIDR`:根 `/metrics`、`/health` 返回 404
- [ ] 配置 `METRICS_ALLOW_CIDR=127.0.0.1/32`:本机可访问根 `/metrics`,其他 IP 返回 404
- [ ] `/healthz` 无鉴权可访问且**不含任何字段**
- [ ] `/health` 无凭证返回 401,带管理凭证返回完整 JSON
- [ ] `/dashboard` 无凭证返回 401
- [ ] `/api/setup/status` 无凭证返回 401
- [ ] 面板内 `/api/admin/metrics`、`/api/admin/health` 正常返回
---
### 阶段 2|路由分层与路径迁移
**改动范围**:`router/router.go`、`controller/`(新增两个桩 handler + 页面路由别名)
#### 2.1 分层路由注册
```go
apiPublic := r.PathPrefix("/api/public").Subrouter()
// 预留空壳,暂不注册业务
apiUser := r.PathPrefix("/api/user").Subrouter()
apiUser.Use(middleware.RequireAdmin) // 单用户模式下与 admin 等价,v0.4.0 再分化
apiAdmin := r.PathPrefix("/api/admin").Subrouter()
apiAdmin.Use(middleware.RequireAdmin)
```
将现有管理 handler 挂到 `apiAdmin`(**复用 handler 本体,不改业务逻辑**):
| 旧路径 | 新路径 |
|---|---|
| `GET/POST /api/voices` | `GET/POST /api/admin/voices` |
| `DELETE /api/voices/{name}` | `DELETE /api/admin/voices/{name}` |
| `PATCH /api/voices/{name}/toggle` | `PATCH /api/admin/voices/{name}/toggle` |
| `GET/PUT /api/settings` | `GET/PUT /api/admin/settings` |
| `PUT /api/settings/api-key` | `PUT /api/admin/settings/api-key` |
| `PUT /api/settings/auth-key` | `PUT /api/admin/settings/auth-key` |
| `PUT /api/settings/cors` | `PUT /api/admin/settings/cors` |
| `GET /api/admin/overview` | 位置不变 |
| `GET /api/admin/metrics` | 位置不变 |
#### 2.2 旧路径兼容:用**别名**,不要重定向
**旧路径指向同一个 handler,不做 301/308 跳转。**
理由(三者对比):
| 方案 | 问题 |
|---|---|
| 301 | 会把 POST/PUT/PATCH/DELETE **降级成 GET**(RFC 7231),且浏览器**永久缓存**,客户端察觉不到路径变化 |
| 308 | 保留方法和请求体,比 301 好;但多一次往返,对非浏览器脚本仍是行为变化 |
| **别名(采用)** | 两个路径直接可用,零往返、零破坏,老脚本完全无感 |
旧路径在当前版本**保留**,并在代码注释标注"过渡期别名,未来版本移除"。
#### 2.3 页面路由迁移
```go
// 新入口
r.HandleFunc("/dashboard", middleware.RequireAdmin(serveAdmin(dashboardHTML))).Methods("GET")
r.HandleFunc("/dashboard/login", serveAdmin(adminLoginHTML)).Methods("GET")
r.HandleFunc("/dashboard/voices", middleware.RequireAdmin(serveAdmin(adminVoicesHTML))).Methods("GET")
r.HandleFunc("/dashboard/settings", middleware.RequireAdmin(serveAdmin(adminSettingsHTML))).Methods("GET")
// 静态资源:必须同步迁移,否则面板丢样式/脚本
r.HandleFunc("/dashboard/admin.css", ...) // 原 /admin/admin.css
r.HandleFunc("/dashboard/admin-shell.js", ...) // 原 /admin/admin-shell.js
// 旧入口 301 永久重定向(页面用 301 正确)
r.HandleFunc("/admin", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/dashboard", http.StatusMovedPermanently)
}).Methods("GET")
```
前端同步改动(`router/*.html`、`admin-shell.js`):
- 页面间跳转与链接:`/admin` → `/dashboard`,`/admin/login` → `/dashboard/login` 等
- API 调用地址迁移到 `/api/admin/*`
- 静态资源引用 `/admin/admin.css` → `/dashboard/admin.css`(同理 `admin-shell.js`)
- `admin-shell.js` 的 401 拦截逻辑改为跳 `/dashboard/login`
> 📌 注意:`/dashboard` 原本是**服务状态预览页**(`router/health.html`)。迁入后该页面的定位
> 变成"管理后台首页",健康/指标数据改由 `/api/admin/health`、`/api/admin/metrics` 提供。
> 若仍希望保留独立的只读状态页,请单独确认(本版本不提供匿名版本)。
#### 2.4 桩接口
| 端点 | 鉴权 | 行为 |
|---|---|---|
| `GET /api/user/usage` | `RequireAdmin` | 单用户模式返回全局用量统计(桩,v0.4.0 改为按用户过滤) |
| `GET /v1/dashboard/billing/usage` | Bearer 业务 key | 返回用量统计,对齐 OpenAI 返回格式 |
#### 2.5 ✅ 阶段 2 回归
- [ ] `/admin` 301 跳 `/dashboard`;`/dashboard` 各页面与静态资源正常加载(无 404、无样式丢失)
- [ ] 面板内音色增删改、全局设置、CORS 配置全部正常
- [ ] 旧路径 `/api/voices`、`/api/settings` **直接可用**(不是重定向),行为与新路径一致
- [ ] `GET /api/user/usage`、`GET /v1/dashboard/billing/usage` 正常返回
- [ ] 用业务 Bearer key 访问 `/api/admin/*` 返回 401
- [ ] 完整安装流程跑通,装完跳转 `/dashboard`
---
### 阶段 3|文档、配置、发布
**改动范围**:`README.md`、`.env.example`、`docker-compose.yml`、`CHANGELOG.md`
1. **README.md**
- WebUI 入口改为 `/dashboard`;注明 `/admin` 为旧兼容重定向
- 端点表更新:`/metrics`、`/health` 默认 404;新增 `/healthz`、`METRICS_ALLOW_CIDR`、`admin_key` 说明
- 公网部署安全清单更新(整节重写,明确"哪些端点不鉴权、必须靠什么挡")
- 探针迁移提醒:K8s / Docker HEALTHCHECK 改指 `/healthz`
2. **`.env.example`**:新增 `METRICS_ALLOW_CIDR` 注释(含 Docker 网段警告)
3. **`docker-compose.yml`**:`METRICS_ALLOW_CIDR` 注释示例
4. **`CHANGELOG.md`**:v0.3.0 条目,明确列出 Breaking Change
#### Release Note · Breaking Change 清单
1. **`/metrics`、`/health`、`/dashboard` 不再匿名可读**。Prometheus 抓取请用
`/api/admin/metrics`(带 Bearer),或配置 `METRICS_ALLOW_CIDR`;
探针请改用 `/healthz`。
2. **未配置任何凭证时服务拒绝启动**(此前管理接口完全裸奔)。
3. **管理凭证与业务凭证可分离**:新增 `admin_key`;配了之后业务 key 无法访问管理接口。
4. **管理接口迁移到 `/api/admin/*`**;旧路径保留为**别名(非重定向)**,未来版本移除。
5. **管理入口迁移到 `/dashboard`**;`/admin` 301 永久重定向。
---
#### 阶段 4|全量回归测试(发布前必须全部通过)
> ⚠️ **本项目测试文件不入库**(`.gitignore` 的 `*_test.go`),CI 只能守住"编译 + vet",
> **守不住行为回归**。因此下面的手工回归是发布前的唯一防线,必须逐条执行并记录结果。
### ✨ 安装流程
1. 不设 `TTS_ADMIN_KEY` 启动 → 终端打印一次性密钥(带醒目警告)
2. 不带密钥 `POST /api/setup` → 401;带密钥 → 安装成功
3. 安装完成跳转 `/dashboard`;进程重启后旧一次性密钥失效
4. 已安装状态访问 `/setup` → 自动跳 `/dashboard`
### ✨ 管理面板
5. 未登录访问 `/dashboard` → 显示登录视图;登录后完整可用
6. 访问 `/admin` → 301 跳 `/dashboard`
7. 音色 CRUD、全局设置、CORS 修改全部正常
8. 仪表盘通过 `/api/admin/health`、`/api/admin/metrics` 渲染正常
9. 用量面板读取 `/api/user/usage` 正常
### ✨ 指标与健康端点
10. 不配 `METRICS_ALLOW_CIDR` → 根 `/metrics`、`/health` 返回 404
11. 配 `METRICS_ALLOW_CIDR=127.0.0.1/32` → 本机根路径可访问,其他 IP 404
12. `/healthz` 匿名可访问且**响应体不含任何业务字段**
### ✨ 业务接口
13. `/v1/audio/speech` OpenAI TTS 合成完全正常
14. `GET /v1/dashboard/billing/usage` 合法 key → 200;非法 key → 401
15. 业务 Bearer key 访问 `/api/admin/*` → 401(隔离生效)
### ✨ 存量兼容
16. 旧管理接口路径`/api/voices`、`/api/settings` 直接可用,行为与新路径一致
### ✨ 安全场景
17. 未配置凭证时服务拒绝启动
18. 匿名无法获取任何健康 / 指标数据
19. 旧 `tts.db` 直接复用升级,**无需数据库迁移**
### ✨ 本地自动化
20. `go build ./... && go vet ./...` 通过
21. `go test ./... -count=1` 全绿
(含抓出过 `VoiceList` 语义 bug 的那批集成测试,是本次改造最重要的安全网)
---
## 3. 发布 v0.3.0
1. 阶段 4 全部通过;Docker 镜像构建测试通过
2. `git tag v0.3.0` 并推送
3. 创建 Release,粘贴 Release Notes,**明确列出 5 条 Breaking Change**
4. 创建维护分支 `v0.3.x`,用于后续 bugfix 与安全补丁
5. `develop` 继续迭代 v0.4.0;上游适配器重构可并行
---
## 4. v0.4.0 后续规划(仅规划,不在 v0.3.0 实现)
v0.3.0 已把**路由分层**和**权限隔离**打好;v0.4.0 只需填充业务逻辑。
1. `store` 新增 `users` 表,区分普通用户 / 管理员角色
2. 引入真正的会话机制(此时才需要 Cookie + CSRF,或采用 Bearer-per-user)
3. `/api/user/*` 从桩转真实业务:个人用量、个人 API 密钥管理、个人调用日志
4. `/api/admin/*` 实现用户 CRUD、配额管理;音色归属绑定用户
5. SPA 扩展个人中心、密钥管理页面
6. `/v1/dashboard/billing/usage` 按 API-Key 归属过滤统计
---
## 5. 风险注意事项
| # | 风险 | 对策 |
|---|---|---|
| 1 | **`/healthz` 与 `/health` 加鉴权分批上线** → 探针空窗、Pod 重启循环 | 必须同一批改动内落地 |
| 2 | **未配凭证导致服务启动失败**(阶段 1 的破坏性结果) | 升级前先配 `auth_key`;README 与 Release Note 明确提示 |
| 3 | Docker 内 `METRICS_ALLOW_CIDR` 填 `127.0.0.1/32` | 必须填容器内网网段 |
| 4 | **禁止把 `RequireAdmin` 套到 `/setup`** | 安装阶段无数据库、无凭证,保留 `InstallGuard` |
| 5 | 旧路径若用重定向会导致 POST 降级 / 客户端无感 | 采用**别名**,不重定向 |
| 6 | 静态资源路由遗漏导致面板丢样式 | 迁移时同步 `/dashboard/admin.css`、`/dashboard/admin-shell.js` |
| 7 | **CI 跑不到测试**(测试文件不入库) | 每阶段结束本地执行 `go test ./... -count=1` |
| 8 | 误改 `adapter/`、`store/` 业务模型 | 本改造不涉及;改动仅限 router / middleware / controller / 前端 / 文档 |
---
## 附录 A|与初版方案的差异
| 初版方案 | 本版 | 原因 |
|---|---|---|
| 阶段 0 "重命名 `AdminSessionMiddleware`" | **删除该阶段** | 该中间件**不存在**;实为新建 Cookie 会话层,会把"低风险重构"变成破坏性变更 |
| 新增 `UserSessionMiddleware` | **不做** | 没有会话机制;`/dashboard` 页面请求不带 `sessionStorage`,服务端无从判断登录态 |
| 新增 `CSRFMiddleware` | **不做** | Bearer 认证免疫 CSRF(浏览器不会自动附带 `Authorization`);CORS 已在中间件层 403 非白名单 Origin |
| Cookie `HttpOnly` / `SameSite=Strict` | **不做** | 不使用 Cookie |
| `promhttp.Handler()` | `metrics.Meter.Handler()` | 项目未引入 `prometheus/client_golang` |
| `controller.AdminSPAPageHandler` / `AdminLoginPageHandler` | `serveAdmin(adminHTML)` 等 | 这两个 handler 不存在 |
| 漏点只列 `/metrics`、`/health` | 补入 **`/dashboard`**、`/api/setup/status` | `/dashboard` 聚合度最高;实测确认这两个也匿名可达 |
| 旧 API 路径 301 重定向 | **别名**,不重定向 | 301 会把 POST 降级为 GET 且被永久缓存 |
| 未提静态资源路由 | 明确 `/dashboard/admin.css` 等 | 遗漏会导致面板丢样式 |
| `RequireAdmin` 空凭证放行未处理 | **阶段 1 专门修复 + fail-fast** | 不修则后续所有鉴权改动形同虚设 |
### 初版保留不变的部分
分阶段 + 每阶段可编译可回归的原则、`InstallGuard` 保持不动、`/v1/dashboard/billing/usage`
走 Bearer、`/api/public` 与 `/api/user` 留桩、Docker CIDR 网段警告、v0.4.0 规划、发布流程。
---
## 附录 B|为什么不做 Cookie 会话
1. **不解决问题**:目标是"健康数据不外泄",用现有 `RequireAdmin` 即可 100% 达成,
不需要会话层。
2. **服务端拿不到 SPA 登录态**:前端凭证存 `sessionStorage`,**不随请求发送**,
所以服务端无法判断 `/dashboard` 页面请求是否已登录。初版要的"未登录访问 `/dashboard`
自动跳登录页"在服务端**做不到**——除非引入 Cookie,而这正是初版的前提缺失之处。
3. **CSRF 需求随之消失**:Cookie 才需要 CSRF 防护;Bearer + `sessionStorage` 天然免疫。
不做 Cookie 就不必实现 token 发放与轮换,也避开同源 SPA 下 `SameSite=Strict` 的坑。
4. **破坏现有客户端**:现有用 `Authorization: Bearer` 调管理 API 的脚本会全部 401,
与"给外部脚本过渡期"的目标冲突。
5. **真正的权限问题另有其解**:当前"一个 key 既是管理密码又是业务密钥"才是实质缺陷,
用新增可选 `admin_key` 即可分离,成本约 20 行,无需引入会话。
---
*文档版本:v2 · 校正基线:`develop` @ `49f57e4`(2026-10-04)*
*初版:v1(外部产出)· 校正原因见附录 A*
+140
View File
@@ -0,0 +1,140 @@
# v0.3.0 Release Notes
> 发布日期:待定 · 基线:`develop`
> 主题:**路由鉴权架构升级 · 健康与指标端点收口 · 管理入口统一**
---
## 一句话概括
v0.3.0 把**默认对外的监控面收窄到只有一个匿名存活探针**,
并把**管理凭证与业务调用凭证拆开**,同时把管理入口统一到 `/dashboard`。
业务接口 `/v1/audio/speech` 行为不变。
---
## ⚠️ Breaking Changes(升级前必读)
### 1. 监控端点不再匿名可读
| 端点 | v0.2.x | v0.3.0 |
|---|---|---|
| `/healthz` | 不存在 | **匿名可读**,只回 `200 ok`(不含任何字段) |
| `/health` | 匿名,返回完整健康数据 | **需管理凭证**,否则 401 |
| `/metrics`(根路径) | 匿名 | **默认 404**;需配置 `METRICS_ALLOW_CIDR` 才注册 |
| `/dashboard` | 匿名(服务状态预览页) | **需鉴权**(且已成为管理看板,见第 3 条) |
| `/api/setup/status` | 匿名 | **需管理凭证** |
**你需要做的**:
- **K8s 探针 / Docker HEALTHCHECK** 改指 **`/healthz`**。
继续指向 `/health` 会一律 401 → Pod 反复重启 / 容器被判 unhealthy。
(本仓库的 `docker-compose.yml` 与 `Dockerfile` 已同步修改。)
- **Prometheus** 二选一:
- 改用鉴权版 `/api/admin/metrics`,并在 `scrape_configs` 配
`authorization: { credentials: <管理凭证> }`;
- 或配置 `METRICS_ALLOW_CIDR` 走根路径白名单。
⚠️ Docker 中**不要填 `127.0.0.1/32`**(那是容器自身回环),
请填容器内网网段,例如 `172.16.0.0/12`。
### 2. 未配置管理凭证时服务拒绝启动
`RequireAdmin` 此前在凭证列表为空时**直接放行**(等于管理接口完全裸奔)。
现在该情况返回 401;并且 **normal 模式下若 `admin_key` / `auth_key` /
`OPENAI_TTS_API_KEY` 三者皆为空,服务将 fail-fast 拒绝启动**。
**你需要做的**:升级前确认三者至少配置一个,否则服务起不来。
### 3. 管理入口从 `/admin` 迁至 `/dashboard`
- `/dashboard`、`/dashboard/voices`、`/dashboard/settings` 为新入口
- 详细服务状态页迁至 **`/dashboard/status`**(原 `/dashboard` 是状态预览页)
- `/admin` 及子路径 **301 永久重定向**到对应新路径,书签与脚本请尽快迁移
### 4. 管理 API 正式路径改为 `/api/admin/*`
- 新:`/api/admin/voices`、`/api/admin/settings`(含 `api-key` / `auth-key` / `admin-key`)
- 旧:`/api/voices`、`/api/settings` **仍可用**(**别名**,不是重定向),计划后续版本移除
> 为什么用别名而不是 308:301 会把 `POST`/`PUT`/`PATCH`/`DELETE` 降级成 `GET`
> 且被浏览器永久缓存;308 虽保留方法,但对非浏览器脚本仍是行为变化。
> 别名零往返、零破坏,老脚本完全无感。
---
## ✨ 新特性
### 独立管理凭证 `admin_key`
管理后台登录凭证与业务调用凭证分离。取值优先级:
```
admin_key(DB) → auth_key(DB) → OPENAI_TTS_API_KEY(env)
```
- **不配置**:回退使用 `auth_key`,**行为与 v0.2.x 完全一致**
- **配置后**:业务调用方持有的 `auth_key` **无法访问管理接口**(返回 401)
配置方式:`PUT /api/admin/settings/admin-key`,或在 `/dashboard → 设置` 中操作。
凭证**只写不读** —— `GET /api/admin/settings` 只返回打码值与来源标记。
### 内网 CIDR 白名单 `METRICS_ALLOW_CIDR`
逗号分隔,支持裸 IP 自动补掩码。非白名单来源返回 **404**(而非 403,
避免向扫描者确认端点存在)。未配置时根路径 `/metrics` **完全不注册**。
### 匿名存活探针 `/healthz`
刻意**不返回任何字段**(无版本、无内存、无配置状态、无模式),只回答"进程还在不在"。
运维要细节请走鉴权后的 `/health` 或 `/api/admin/health`。
---
## 🔧 修复
- `RequireAdmin` 空凭证放行(安全缺口,见 Breaking Change 第 2 条)
- `/api/setup/status` 在 normal 模式下仍匿名暴露 `{installed, mode}`
- `docker-compose.yml` / `Dockerfile` 的健康检查端点(随鉴权改动失效)
- 指标写入对空接收者安全(`nil *Counter` / `*Gauge` / `*Histogram` 不再 panic)
- 上游 `HTTPClient` 未初始化时返回错误而非 panic
---
## ✅ 验证方式
```bash
go build ./...
go vet ./...
go test ./... -count=1
```
> 注意:本仓库测试源码**不入库**(`.gitignore` 的 `*_test.go`),
> 干净克隆上 `go test` 只会打印 `no test files`。
> 自动化测试需在开发机(测试文件所在处)执行。
> 发布前请按 [IMPLEMENT_v0.3.0.md](IMPLEMENT_v0.3.0.md) 第 3 节逐条手工回归。
端到端实测结果(真实服务器):
| 检查 | 结果 |
|---|---|
| `/healthz` 匿名 | `200` + 字面量 `ok` |
| `/health` 无凭证 / 带凭证 | `401` / `200` |
| 根 `/metrics` 未配 CIDR | `404`(不注册) |
| `/admin`、`/admin/login`、`/admin/voices`、`/admin/settings` | 全部 `301` 至对应 `/dashboard/*` |
| `/dashboard*` 浏览器请求 / 脚本请求 | `200` 页面外壳 / `401` |
| `/api/admin/*` 与 `/api/*` 别名 | 均 `200`(无 301/308) |
| 配独立 `admin_key` 后业务 key 访问管理接口 | `401` |
| 业务接口 `/v1/audio/speech` | 行为不变 |
---
## 📦 升级步骤
1. 确认已配置 `admin_key` / `auth_key` / `OPENAI_TTS_API_KEY` 至少一个
2. 更新探针指向 `/healthz`
3. 更新 Prometheus 抓取配置(鉴权版或 `METRICS_ALLOW_CIDR`)
4. 替换镜像 / 二进制并重启
5. 访问 `/dashboard` 登录验证;确认旧 `/admin` 书签仍能自动跳转
6. 按需在 `/dashboard → 设置` 配置独立 `admin_key`
**无需数据库迁移** —— 旧的 `tts.db` 可直接复用。