13 Commits
Author SHA1 Message Date
sun 0d3517eb5c fix: 安全加固(VUL-001~009)+ 构建版本注入 + 死代码清理' (#2) from develop into main
Docker Publish / build-and-push (push) Canceled after 0s
Reviewed-on: #2
2026-08-27 11:01:47 +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 d1e7f9a4dd chore: 删除 ratelimit_middleware.go.tmp 临时文件
与 ratelimit_middleware.go 内容完全重复(SHA256 一致,1116 字节),
且无任何代码引用 .tmp 路径,属于误提交的开发期残留文件。
2026-08-23 13:16:17 +08:00
sun 4a8c563b32 Merge branch 'develop' into main
将 develop 分支累积的 51 个提交合入 main,涵盖完整的架构重构与
可观测性建设,作为下一阶段对外发布的稳定基线。

## 主要变更

### 架构重构
- 将单体 tts_server.go (778 行) 拆分为模块化目录:adapter/volcano、
  controller、dto、middleware、router、setting、telemetry、metrics
- 引入 OpenAI /v1/audio/speech 兼容接口,火山引擎作为后端适配器
- 统一以 main.go 作为入口,集中启动顺序与信号处理

### 火山 TTS 适配器
- 重构请求/响应/合成为独立文件 (request.go / response.go /
  synthesis.go / audio.go),namespace 修正为 UnidirectionalTTS
- 适配 v3 模型参数与音色复刻路由
- 兼容上游 sentence 字段对象/字符串两种形态,处理 event 字段缺失
- 新增请求诊断日志,便于排障

### 可观测性
- 自研轻量 telemetry 库 (counter/gauge/histogram/labels/registry)
- 暴露 Prometheus 格式指标,内置 /metrics 端点
- 限流与并发中间件仅统计 /v1/ 路由,避免监控路径污染计数
- 新增 health.html 监控面板,分组展示请求、延迟、限流、字节数等

### 中间件
- CORS:支持 ALLOWED_ORIGINS 白名单,日志优化
- 限流:令牌桶 + 并发控制,带可观测埋点
- 安全/鉴权:API Key 校验、请求体大小限制
- Logger:统一请求日志,修复中文乱码与换行

### 配置与启动
- setting/config.go 集中环境变量管理,启动时打印配置摘要
- 配置文件切换到 .env.example,支持音频格式/采样率/调试日志开关

### 文档与构建
- README 重写,补充 BYTEDANCE_TTS_DEBUG 调试日志小节,移除 1.0 旧内容
- Go 版本升级 1.23 -> 1.26
- 新增 Dockerfile / docker-compose.yml,镜像构建与编排
- 补充 .dockerignore / .gitignore,清理 BOM 头与废弃 tts-api.exe

### 修复
- 修复 response.go switch 大括号结构错误、synthesis.go 类型不匹配
- 修复请求体过大时的处理、冗余代码与日志乱码
- 限流/并发中间件跳过非 /v1/ 路由,避免监控路径污染计数
2026-08-23 10:01:35 +08:00
sun cd70157aea ci: 删除无用的 Gitea CI/CD 部署配置文件
移除了原本用于部署到宝塔的工作流文件,清理冗余的CI配置
2026-05-19 18:41:59 +08:00
sun ac614be190 Merge pull request 'test' (#1) from test into main
Go CI/CD Deploy to Baota / build-and-deploy (push) Has been cancelled
Reviewed-on: #1
2026-05-18 23:29:21 +08:00
sun 880c199ce4 Reapply "ci: 添加Gitea工作流用于Go项目部署到宝塔"
Go CI/CD Deploy to Baota / build-and-deploy (push) Failing after 5m10s
This reverts commit 25e09cd54f.
2026-05-11 17:58:28 +08:00
16 changed files with 183 additions and 80 deletions
+65
View File
@@ -0,0 +1,65 @@
name: Docker Publish
on:
push:
tags:
- 'v*'
workflow_dispatch: # 允许手动触发测试
env:
REGISTRY: ghcr.io
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0 # 拉完整历史,git describe 能取到 tag
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract version from git
id: version
run: |
echo "version=$(git describe --tags --always --dirty)" >> "$GITHUB_OUTPUT"
echo "commit=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT"
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ github.repository }}
tags: |
type=semver,pattern={{version}}
type=sha,format=short
labels: |
org.opencontainers.image.version=${{ steps.version.outputs.version }}
org.opencontainers.image.revision=${{ steps.version.outputs.commit }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
VERSION=${{ steps.version.outputs.version }}
COMMIT=${{ steps.version.outputs.commit }}
+9 -1
View File
@@ -7,7 +7,15 @@ RUN go mod download
COPY . . COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o tts-api . # VERSION 由 CI/CD 传入,通常为 `git describe --tags --always --dirty` 的输出
# COMMIT 为 `git rev-parse --short HEAD`
# 本地默认 dev
ARG VERSION=dev
ARG COMMIT=dev
RUN CGO_ENABLED=0 GOOS=linux go build \
-ldflags "-X github.com/volcano-tts/tts-api/version.Version=${VERSION} \
-X github.com/volcano-tts/tts-api/version.Commit=${COMMIT}" \
-o tts-api .
FROM alpine:3.21 FROM alpine:3.21
+67 -3
View File
@@ -63,7 +63,7 @@ tts-api.exe
|--------|------|--------| |--------|------|--------|
| `BYTEDANCE_TTS_TIMEOUT` | 单次合成超时 | `30s` | | `BYTEDANCE_TTS_TIMEOUT` | 单次合成超时 | `30s` |
| `BYTEDANCE_TTS_FORMAT` | 上游实际请求的音频格式(mp3 / pcm / ogg_opus);客户端要求 wav 时内部自动转 pcm + 本地拼 WAV 头 | `mp3` | | `BYTEDANCE_TTS_FORMAT` | 上游实际请求的音频格式(mp3 / pcm / ogg_opus);客户端要求 wav 时内部自动转 pcm + 本地拼 WAV 头 | `mp3` |
| `BYTEDANCE_TTS_SAMPLE_RATE` | 上游采样率(8000 / 16000 / 22050 / 24000 / 32000 / 44100 / 48000) | `24000` | | `BYTEDANCE_TTS_SAMPLE_RATE` | 上游采样率(8000 / 16000 / 22050 / 24000 / 32000 / 44100 / 48000);**此值直接写入 WAV 头,需与上游实际 PCM 采样率一致,否则音频变速变调** | `24000` |
| `BYTEDANCE_TTS_BIT_RATE` | MP3 比特率,仅 mp3 生效 | 无 | | `BYTEDANCE_TTS_BIT_RATE` | MP3 比特率,仅 mp3 生效 | 无 |
### 复刻 2.0 扩展参数 ### 复刻 2.0 扩展参数
@@ -79,7 +79,7 @@ tts-api.exe
| 变量名 | 说明 | 默认值 | | 变量名 | 说明 | 默认值 |
|--------|------|--------| |--------|------|--------|
| `OPENAI_TTS_API_KEY` | OpenAI 兼容接口的 API Key(逗号分隔支持多个) | 无(不鉴权) | | `OPENAI_TTS_API_KEY` | 🔴 **公网必设** OpenAI 兼容接口的 API Key(逗号分隔支持多个);**未设置时鉴权完全关闭** | 无(不鉴权) |
| `TRUSTED_PROXY_HOPS` | X-Forwarded-For 解析模式(0=启发式/默认,>0=精确 N 跳) | `0`(启发式) | | `TRUSTED_PROXY_HOPS` | X-Forwarded-For 解析模式(0=启发式/默认,>0=精确 N 跳) | `0`(启发式) |
| `PORT` | 服务监听端口 | `8080` | | `PORT` | 服务监听端口 | `8080` |
| `ALLOWED_ORIGINS` | CORS 跨域白名单(逗号分隔,调试可设 `*`;空则拒绝所有跨域) | 无 | | `ALLOWED_ORIGINS` | CORS 跨域白名单(逗号分隔,调试可设 `*`;空则拒绝所有跨域) | 无 |
@@ -243,6 +243,8 @@ CORS拦截: 来源="https://..." 路径=/v1/audio/speech 方法=POST 客户端=.
## API 使用说明 ## API 使用说明
> ⚠️ **公网部署前必读**:如果你的服务暴露在公网,**必须**设置 `OPENAI_TTS_API_KEY` 或由前置反代(nginx / caddy)承担鉴权。未设置时 `Authorization` 头完全跳过校验,任何能访问 `:8080` 的人都能调用 TTS 合成,消耗你的火山额度。详见[部署 → 公网安全清单](#公网部署安全清单)。
### OpenAI 兼容接口 ### OpenAI 兼容接口
**端点:** `POST /v1/audio/speech` **端点:** `POST /v1/audio/speech`
@@ -268,7 +270,7 @@ CORS拦截: 来源="https://..." 路径=/v1/audio/speech 方法=POST 客户端=.
- `input` — 要合成的文本 - `input` — 要合成的文本
- `voice` — 发音人(OpenAI 兼容,实际用 `BYTEDANCE_TTS_SPEAKER`) - `voice` — 发音人(OpenAI 兼容,实际用 `BYTEDANCE_TTS_SPEAKER`)
- `response_format` — 输出格式:`mp3`(默认)/ `opus`(映射 ogg_opus)/ `wav` / `pcm` / `aac` / `flac`(降级到 mp3) - `response_format` — 输出格式:`mp3`(默认)/ `opus`(映射 ogg_opus)/ `wav` / `pcm` / `aac` / `flac`(降级到 mp3)
- `speed` — 语速,0.25 ~ 4.0(火山侧转换为 speech_rate [-50, 100]) - `speed` — 语速倍率,客户端接受范围 0.25 ~ 4.0;**火山实际生效范围 0.5 ~ 2.0**(speech_rate [-50, 100]),超出范围会被静默截断,客户端无感反馈
**格式映射:** **格式映射:**
@@ -358,6 +360,40 @@ scrape_configs:
`/dashboard` 展示服务状态 + 内存 + 配置信息,并内嵌 `/metrics` 预览;Grafana 等工具可直接基于上面指标做面板。 `/dashboard` 展示服务状态 + 内存 + 配置信息,并内嵌 `/metrics` 预览;Grafana 等工具可直接基于上面指标做面板。
### ⚠️ 公网部署:监控端点无鉴权
`/metrics`、`/health`、`/dashboard` **均不鉴权**,这是对齐 Prometheus 抓取场景的设计权衡:
| 端点 | 暴露内容 | 风险 |
|---|---|---|
| `/metrics` | 业务标签(speaker/model/format)、运行指标、错误计数 | 侦察面:可推断使用量、技术栈、错误模式 |
| `/health` | 服务状态、版本号、运行时长、内存 | 侦察面:版本号可用于匹配已知 CVE |
| `/dashboard` | 配置检查结果(含 `TTSConfigErr` 状态) | 信息泄露:可确认配置是否就绪 |
**部署建议**:
- **内网 / 反代后**:无影响,符合预期
- **公网直接暴露**:在前置反代(nginx / caddy)上保护这些端点,示例 nginx 配置:
```nginx
location /metrics {
auth_basic "metrics";
auth_basic_user_file /etc/nginx/.htpasswd;
allow 10.0.0.0/8; # 仅允许 Prometheus 服务器网段
deny all;
}
location /dashboard {
auth_basic "admin";
auth_basic_user_file /etc/nginx/.htpasswd;
}
location /health {
allow 10.0.0.0/8; # 或保留给监控系统访问
deny all;
}
```
- **最简方案**:反代层直接限制 `/metrics` 只能从 Prometheus 服务器 IP 访问,无需 basic auth
## 架构 ## 架构
| 包 | 职责 | | 包 | 职责 |
@@ -410,6 +446,34 @@ docker compose up -d
环境变量通过 `.env` 或 `docker-compose.yml` 传入。 环境变量通过 `.env` 或 `docker-compose.yml` 传入。
### 公网部署安全清单
公网直接暴露(`:8080` 可被互联网任意访问)时,**至少满足以下两条之一**,否则视为不安全的部署:
1. **设置 `OPENAI_TTS_API_KEY`**(推荐,最简单)
```bash
# .env
OPENAI_TTS_API_KEY=<32+ 位随机字符串>
```
客户端请求时带 `Authorization: Bearer <那个字符串>`。
2. **前置反代承担鉴权**(nginx / caddy / Cloudflare Access)
- 反代层做 basic auth、mTLS、Cloudflare Access 等任一方案
- 反代**仅**把鉴权后的请求转发到 `:8080`,Go 服务本身保持"无鉴权"
- 此时 `OPENAI_TTS_API_KEY` 可不设
**两个端点还需要单独保护**(无论上面哪种方案):
- `/metrics`:暴露业务标签与运行指标,详见[观测 / Metrics → 公网部署](#公网部署监控端点无鉴权)
- `/dashboard`:暴露配置检查结果,同上
**未做保护的典型风险**:
- 任意人 curl `POST /v1/audio/speech` → 消耗你火山账号的字符额度
- 任意人 `GET /metrics` → 推断你的使用量、技术栈、错误模式
- 任意人 `GET /dashboard` → 确认你 TTS 配置就绪状态
**内网部署 / 私网反代后**:这些警示不适用,直接用就行。
## 常见问题 ## 常见问题
### 1. `code=55000000, message=resource ID is mismatched with speaker related resource` ### 1. `code=55000000, message=resource ID is mismatched with speaker related resource`
+5 -1
View File
@@ -6,6 +6,7 @@ import (
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"log" "log"
"strings"
"time" "time"
"github.com/volcano-tts/tts-api/common" "github.com/volcano-tts/tts-api/common"
@@ -94,10 +95,13 @@ func Synthesis(
if resp.StatusCode != 200 { if resp.StatusCode != 200 {
rawBody := ReadErrorBody(resp.Body) rawBody := ReadErrorBody(resp.Body)
// rawBody 来自上游响应体,可能是攻击者控制的恶意内容(例如包含
// \n 伪造日志行)。转义后再嵌入错误消息。
safeBody := strings.NewReplacer("\n", "\\n", "\r", "\\r").Replace(rawBody)
mtr.UpstreamFinished(opts.Speaker, opts.Model, opts.Format, fmt.Sprintf("http_%d", resp.StatusCode), time.Since(started), 0, 0, 0, resp.StatusCode) mtr.UpstreamFinished(opts.Speaker, opts.Model, opts.Format, fmt.Sprintf("http_%d", resp.StatusCode), time.Since(started), 0, 0, 0, resp.StatusCode)
return nil, &UpstreamError{ return nil, &UpstreamError{
Code: resp.StatusCode, Code: resp.StatusCode,
Message: fmt.Sprintf("upstream http %d: %s", resp.StatusCode, rawBody), Message: fmt.Sprintf("upstream http %d: %s", resp.StatusCode, safeBody),
Stage: "http", Stage: "http",
} }
} }
-2
View File
@@ -15,8 +15,6 @@ const (
MaxRequestBodySize = 1024 * 1024 MaxRequestBodySize = 1024 * 1024
RateLimitRequests = 100 RateLimitRequests = 100
RateLimitWindow = time.Minute RateLimitWindow = time.Minute
MaxResponseTimes = 100
MaxErrors = 10
MaxConcurrentRequests = 10 MaxConcurrentRequests = 10
CleanupInterval = time.Hour CleanupInterval = time.Hour
MaxModelNameLength = 64 MaxModelNameLength = 64
+3 -4
View File
@@ -18,6 +18,7 @@ import (
"github.com/volcano-tts/tts-api/middleware" "github.com/volcano-tts/tts-api/middleware"
"github.com/volcano-tts/tts-api/setting" "github.com/volcano-tts/tts-api/setting"
"github.com/volcano-tts/tts-api/telemetry" "github.com/volcano-tts/tts-api/telemetry"
"github.com/volcano-tts/tts-api/version"
) )
var ( var (
@@ -46,9 +47,6 @@ func resolveClientFormat(reqFmt string) string {
} }
return strings.ToLower(reqFmt) return strings.ToLower(reqFmt)
} }
if reqFmt == "" {
return setting.TTSOptions.Format
}
return setting.TTSOptions.Format return setting.TTSOptions.Format
} }
@@ -232,7 +230,8 @@ func HealthHandler(w http.ResponseWriter, r *http.Request) {
resp := dto.HealthResponse{ resp := dto.HealthResponse{
Status: status, Status: status,
Service: "ByteDance TTS to OpenAI API Adapter", Service: "ByteDance TTS to OpenAI API Adapter",
Version: "2.0.0 (v3 API)", Version: version.Version,
Commit: version.Commit,
Uptime: fmt.Sprintf("%.0f seconds", time.Since(startTime).Seconds()), Uptime: fmt.Sprintf("%.0f seconds", time.Since(startTime).Seconds()),
StartTime: startTime.Format(time.RFC3339), StartTime: startTime.Format(time.RFC3339),
Memory: collectMemorySnapshot(), Memory: collectMemorySnapshot(),
+1
View File
@@ -7,6 +7,7 @@ type HealthResponse struct {
Status string `json:"status"` Status string `json:"status"`
Service string `json:"service"` Service string `json:"service"`
Version string `json:"version"` Version string `json:"version"`
Commit string `json:"commit"`
Uptime string `json:"uptime"` Uptime string `json:"uptime"`
StartTime string `json:"start_time"` StartTime string `json:"start_time"`
Memory map[string]interface{} `json:"memory"` Memory map[string]interface{} `json:"memory"`
-9
View File
@@ -58,15 +58,6 @@ type V3Usage struct {
TextWords int `json:"text_words"` TextWords int `json:"text_words"`
} }
// ByteDanceTTSConfig 是 setting 包的全局 TTS 配置,目前只承载鉴权 / URL / 超时;
// 完整的合成参数见 adapter/volcano.Options。
type ByteDanceTTSConfig struct {
ApiKey string
ResourceId string
URL string
Timeout time.Duration
}
// SynthesisResult 是火山适配器向 controller 返回的最终结果。 // SynthesisResult 是火山适配器向 controller 返回的最终结果。
// Format 与 AudioData 的实际编码一致;controller 据此设置响应 Content-Type。 // Format 与 AudioData 的实际编码一致;controller 据此设置响应 Content-Type。
type SynthesisResult struct { type SynthesisResult struct {
+4 -1
View File
@@ -131,7 +131,10 @@ func (AdapterRecorder) UpstreamFinished(speaker, model, format, status string, d
if audioBytes > 0 { if audioBytes > 0 {
UpstreamBytes.Add(float64(audioBytes), telemetry.Labels{"format": format}) UpstreamBytes.Add(float64(audioBytes), telemetry.Labels{"format": format})
} }
if errCode != 0 { // 上游调用只要 status != "ok" 即视为错误。原版 if errCode != 0 会漏掉
// errCode=0 的 request_error / transport_error / wrap_error / stream_error
// (code=0 的流错误) 等场景,导致 transport 类错误在 /metrics 上完全不可见。
if status != "ok" {
UpstreamErrors.Inc(telemetry.Labels{"code": codeLabel(errCode)}) UpstreamErrors.Inc(telemetry.Labels{"code": codeLabel(errCode)})
} }
} }
-7
View File
@@ -9,13 +9,6 @@ import (
"github.com/volcano-tts/tts-api/setting" "github.com/volcano-tts/tts-api/setting"
) )
// InitAPIKeys 已在 setting.InitAuthConfig 中完成,这里保留为 no-op 以维持现有调用顺序。
// 实际鉴权逻辑直接读 setting.Auth.APIKeys。
func InitAPIKeys() {
// 配置由 setting 包统一加载,日志也由 setting.LogStartupSummary 输出。
_ = setting.Auth
}
func ValidateAPIKey(r *http.Request) bool { func ValidateAPIKey(r *http.Request) bool {
if len(setting.Auth.APIKeys) == 0 { if len(setting.Auth.APIKeys) == 0 {
return true return true
-7
View File
@@ -13,13 +13,6 @@ var (
corsMaxAgeHeader = "86400" corsMaxAgeHeader = "86400"
) )
// InitCORSConfig 已在 setting.InitCORSConfig 中完成,这里保留为 no-op 以维持现有调用顺序。
// 实际 CORS 匹配逻辑直接读 setting.CORS.Origins / setting.CORS.AllowAll。
func InitCORSConfig() {
// 配置由 setting 包统一加载,日志也由 setting.LogStartupSummary 输出。
_ = setting.CORS
}
func isValidOrigin(origin string) bool { func isValidOrigin(origin string) bool {
if origin == "" || origin == "null" || origin == "nil" { if origin == "" || origin == "null" || origin == "nil" {
return false return false
+5 -1
View File
@@ -3,6 +3,7 @@ package middleware
import ( import (
"log" "log"
"net/http" "net/http"
"strings"
"time" "time"
) )
@@ -23,6 +24,9 @@ func Logger(next http.Handler) http.Handler {
next.ServeHTTP(rec, r) next.ServeHTTP(rec, r)
duration := time.Since(start) duration := time.Since(start)
log.Printf("%s %s %s %d %v", r.Method, r.RequestURI, r.RemoteAddr, rec.statusCode, duration) // r.RequestURI 是未经解析的原始请求行,攻击者可在 URL 中注入
// \n / \r 伪造日志行。转义为可见字符后再记录。
uri := strings.NewReplacer("\n", "\\n", "\r", "\\r").Replace(r.RequestURI)
log.Printf("%s %s %s %d %v", r.Method, uri, r.RemoteAddr, rec.statusCode, duration)
}) })
} }
+6 -7
View File
@@ -1,10 +1,9 @@
package middleware package middleware
// 本文件提供带 metrics 埋点的限流 / 并发中间件版本; // 本文件提供带 metrics 埋点的限流 / 并发中间件版本。
// 由于原 ratelimit_middleware.go 在本仓库的云盘同步下被永久占用, // 相比 router 实际使用的实现,本版本额外做了:
// 这里用独立实现覆盖路由使用入口,旧实现保留为未引用代码。 // - 加 metrics 埋点(限流拒绝 / 并发拒绝计数)
// // - 仅对 /v1/ 下的业务请求生效,监控路径(/health /metrics /dashboard)不消耗配额
// 行为与原 ratelimit_middleware.go 完全一致,只是多了 metrics 调用。
import ( import (
"log" "log"
@@ -14,7 +13,7 @@ import (
"github.com/volcano-tts/tts-api/metrics" "github.com/volcano-tts/tts-api/metrics"
) )
// RateLimitWithMetrics 是 middleware.RateLimit 的可埋点版本。 // RateLimitWithMetrics 是限流中间件,带埋点 + 路径过滤。
func RateLimitWithMetrics(next http.Handler) http.Handler { func RateLimitWithMetrics(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 仅对 /v1/ 下的业务请求限流,/health /metrics /dashboard 等监控路径不限流 // 仅对 /v1/ 下的业务请求限流,/health /metrics /dashboard 等监控路径不限流
@@ -32,7 +31,7 @@ func RateLimitWithMetrics(next http.Handler) http.Handler {
}) })
} }
// ConcurrencyLimitWithMetrics 是 middleware.ConcurrencyLimit 的可埋点版本。 // ConcurrencyLimitWithMetrics 是并发控制中间件,带埋点 + 路径过滤。
func ConcurrencyLimitWithMetrics(next http.Handler) http.Handler { func ConcurrencyLimitWithMetrics(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 仅对 /v1/ 下的业务请求统计并发和加锁,监控路径不占用并发槽位 // 仅对 /v1/ 下的业务请求统计并发和加锁,监控路径不占用并发槽位
-32
View File
@@ -1,32 +0,0 @@
package middleware
import (
"log"
"net/http"
)
func RateLimit(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
clientIP := GetClientIP(r)
if !GlobalRateLimiter.Allow(clientIP) {
log.Printf("警告: 已超过IP速率限制,拒绝请求 - 客户端IP: %s", clientIP)
SendJSONError(w, http.StatusTooManyRequests, "Rate limit exceeded. Please try again later.", "rate_limit_error", "rate_limit_exceeded")
return
}
next.ServeHTTP(w, r)
})
}
func ConcurrencyLimit(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
select {
case ConcurrencySem <- struct{}{}:
defer func() { <-ConcurrencySem }()
next.ServeHTTP(w, r)
default:
log.Printf("警告: 已达到最大并发请求数限制,拒绝请求 - 客户端IP: %s", GetClientIP(r))
SendJSONError(w, http.StatusServiceUnavailable, "Server is busy, maximum concurrent requests reached. Please try again later.", "concurrency_limit_error", "max_concurrent_requests")
return
}
})
}
-5
View File
@@ -10,7 +10,6 @@ import (
"github.com/volcano-tts/tts-api/adapter/volcano" "github.com/volcano-tts/tts-api/adapter/volcano"
"github.com/volcano-tts/tts-api/common" "github.com/volcano-tts/tts-api/common"
"github.com/volcano-tts/tts-api/dto"
) )
// 全部环境变量读取的单一入口:其它包不允许直接 os.Getenv,只读这里的全局 Config。 // 全部环境变量读取的单一入口:其它包不允许直接 os.Getenv,只读这里的全局 Config。
@@ -317,7 +316,3 @@ func CheckStaticFiles() {
log.Println("警告: health.html 不存在,/dashboard 路由将返回 404") log.Println("警告: health.html 不存在,/dashboard 路由将返回 404")
} }
} }
// 保留 dto.ByteDanceTTSConfig 引用避免 import 警告;
// 新代码不应再使用这个类型,设置已在 TTSOptions 中。
var _ = dto.ByteDanceTTSConfig{}
+18
View File
@@ -0,0 +1,18 @@
// Package version 提供构建时注入的版本信息。
//
// Version 和 Commit 在编译时通过 -ldflags 注入:
//
// go build -ldflags "-X github.com/volcano-tts/tts-api/version.Version=$VERSION \
// -X github.com/volcano-tts/tts-api/version.Commit=$COMMIT"
//
// 开发时默认 "dev",CI/CD 时通常由 git describe 自动算出:
// VERSION=$(git describe --tags --always --dirty)
// COMMIT=$(git rev-parse --short HEAD)
//
// /health 端点会暴露这两个值,方便运维确认"跑的到底是哪个 commit"。
package version
var (
Version = "dev"
Commit = "dev"
)