7 Commits
Author SHA1 Message Date
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 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 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
3 changed files with 18 additions and 53 deletions
-53
View File
@@ -1,53 +0,0 @@
const https = require('https');
const base = 'test-test.zeabur.app';
function req(method, path, headers) {
return new Promise((resolve) => {
const r = https.request({
hostname: base, port: 443, path, method, headers: headers || {},
}, (res) => {
const chunks = [];
res.on('data', (c) => chunks.push(c));
res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, body: Buffer.concat(chunks) }));
});
r.on('error', (e) => resolve({ error: e.message }));
r.end();
});
}
(async () => {
// 1. 预检 OPTIONS 请求
console.log('=== 预检: OPTIONS /v1/audio/speech (with Origin) ===');
const r1 = await req('OPTIONS', '/v1/audio/speech', {
'Origin': 'https://example.com',
'Access-Control-Request-Method': 'POST',
'Access-Control-Request-Headers': 'content-type, authorization',
});
console.log('Status:', r1.status);
console.log('Headers:');
for (const k of Object.keys(r1.headers)) {
if (k.startsWith('access-control') || k === 'vary') {
console.log(' ' + k + ': ' + r1.headers[k]);
}
}
console.log('Body:', r1.body ? r1.body.toString('utf8') : '');
// 2. 预检但无 Origin(同源或非浏览器)
console.log('\n=== OPTIONS /v1/audio/speech (NO Origin) ===');
const r2 = await req('OPTIONS', '/v1/audio/speech', {});
console.log('Status:', r2.status);
// 3. 实际 POST 请求
console.log('\n=== POST /v1/audio/speech (with Origin) ===');
const body = JSON.stringify({ model: 'tts-1', input: 'test', voice: 'alloy' });
const r3 = await req('POST', '/v1/audio/speech', {
'Origin': 'https://example.com',
'Content-Type': 'application/json',
'Authorization': 'Bearer sk-HRNfl15pp8e0KLewaydRCmvf2KfkOYd728yLJmuff5DkyaXd',
'Content-Length': Buffer.byteLength(body),
});
if (r3.body) r3.write = r3.write || (() => {});
console.log('Status:', r3.status);
console.log('Access-Control-Allow-Origin:', r3.headers['access-control-allow-origin']);
console.log('Vary:', r3.headers['vary']);
})();
+7
View File
@@ -642,6 +642,13 @@
return `${m}m`;
};
const formatBytes = (b) => {
if (!b) return "0 B";
const k = 1024, sizes = ["B", "KB", "MB", "GB"];
const i = Math.floor(Math.log(b) / Math.log(k));
return (b / Math.pow(k, i)).toFixed(1) + " " + sizes[i];
};
onMounted(() => {
refresh();
setInterval(refresh, 10000);
+11
View File
@@ -9,6 +9,7 @@ package middleware
import (
"log"
"net/http"
"strings"
"github.com/volcano-tts/tts-api/metrics"
)
@@ -16,6 +17,11 @@ import (
// RateLimitWithMetrics 是 middleware.RateLimit 的可埋点版本。
func RateLimitWithMetrics(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 仅对 /v1/ 下的业务请求限流,/health /metrics /dashboard 等监控路径不限流
if !strings.HasPrefix(r.URL.Path, "/v1/") {
next.ServeHTTP(w, r)
return
}
clientIP := GetClientIP(r)
if !GlobalRateLimiter.Allow(clientIP) {
log.Printf("警告: 已超过IP速率限制,拒绝请求 - 客户端IP: %s", clientIP)
@@ -29,6 +35,11 @@ func RateLimitWithMetrics(next http.Handler) http.Handler {
// ConcurrencyLimitWithMetrics 是 middleware.ConcurrencyLimit 的可埋点版本。
func ConcurrencyLimitWithMetrics(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 仅对 /v1/ 下的业务请求统计并发和加锁,监控路径不占用并发槽位
if !strings.HasPrefix(r.URL.Path, "/v1/") {
next.ServeHTTP(w, r)
return
}
select {
case ConcurrencySem <- struct{}{}:
metrics.ConcurrencyActive.Inc(nil)