Compare commits
13
Commits
a238e5c2a4
..
v0.2.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d3517eb5c | ||
|
|
cdc9a7c94b | ||
|
|
c00c46e7a1 | ||
|
|
3b3aa3b708 | ||
|
|
695b3ecf25 | ||
|
|
7343d5aa5c | ||
|
|
171503d775 | ||
|
|
91b0c8acee | ||
|
|
d1e7f9a4dd | ||
|
|
4a8c563b32 | ||
|
|
cd70157aea | ||
|
|
ac614be190 | ||
|
|
880c199ce4 |
@@ -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
@@ -7,7 +7,15 @@ RUN go mod download
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ tts-api.exe
|
||||
|--------|------|--------|
|
||||
| `BYTEDANCE_TTS_TIMEOUT` | 单次合成超时 | `30s` |
|
||||
| `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 生效 | 无 |
|
||||
|
||||
### 复刻 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`(启发式) |
|
||||
| `PORT` | 服务监听端口 | `8080` |
|
||||
| `ALLOWED_ORIGINS` | CORS 跨域白名单(逗号分隔,调试可设 `*`;空则拒绝所有跨域) | 无 |
|
||||
@@ -243,6 +243,8 @@ CORS拦截: 来源="https://..." 路径=/v1/audio/speech 方法=POST 客户端=.
|
||||
|
||||
## API 使用说明
|
||||
|
||||
> ⚠️ **公网部署前必读**:如果你的服务暴露在公网,**必须**设置 `OPENAI_TTS_API_KEY` 或由前置反代(nginx / caddy)承担鉴权。未设置时 `Authorization` 头完全跳过校验,任何能访问 `:8080` 的人都能调用 TTS 合成,消耗你的火山额度。详见[部署 → 公网安全清单](#公网部署安全清单)。
|
||||
|
||||
### OpenAI 兼容接口
|
||||
|
||||
**端点:** `POST /v1/audio/speech`
|
||||
@@ -268,7 +270,7 @@ CORS拦截: 来源="https://..." 路径=/v1/audio/speech 方法=POST 客户端=.
|
||||
- `input` — 要合成的文本
|
||||
- `voice` — 发音人(OpenAI 兼容,实际用 `BYTEDANCE_TTS_SPEAKER`)
|
||||
- `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 等工具可直接基于上面指标做面板。
|
||||
|
||||
### ⚠️ 公网部署:监控端点无鉴权
|
||||
|
||||
`/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` 传入。
|
||||
|
||||
### 公网部署安全清单
|
||||
|
||||
公网直接暴露(`: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`
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/volcano-tts/tts-api/common"
|
||||
@@ -94,10 +95,13 @@ func Synthesis(
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
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)
|
||||
return nil, &UpstreamError{
|
||||
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",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,8 +15,6 @@ const (
|
||||
MaxRequestBodySize = 1024 * 1024
|
||||
RateLimitRequests = 100
|
||||
RateLimitWindow = time.Minute
|
||||
MaxResponseTimes = 100
|
||||
MaxErrors = 10
|
||||
MaxConcurrentRequests = 10
|
||||
CleanupInterval = time.Hour
|
||||
MaxModelNameLength = 64
|
||||
|
||||
+3
-4
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/volcano-tts/tts-api/middleware"
|
||||
"github.com/volcano-tts/tts-api/setting"
|
||||
"github.com/volcano-tts/tts-api/telemetry"
|
||||
"github.com/volcano-tts/tts-api/version"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -46,9 +47,6 @@ func resolveClientFormat(reqFmt string) string {
|
||||
}
|
||||
return strings.ToLower(reqFmt)
|
||||
}
|
||||
if reqFmt == "" {
|
||||
return setting.TTSOptions.Format
|
||||
}
|
||||
return setting.TTSOptions.Format
|
||||
}
|
||||
|
||||
@@ -232,7 +230,8 @@ func HealthHandler(w http.ResponseWriter, r *http.Request) {
|
||||
resp := dto.HealthResponse{
|
||||
Status: status,
|
||||
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()),
|
||||
StartTime: startTime.Format(time.RFC3339),
|
||||
Memory: collectMemorySnapshot(),
|
||||
|
||||
@@ -7,6 +7,7 @@ type HealthResponse struct {
|
||||
Status string `json:"status"`
|
||||
Service string `json:"service"`
|
||||
Version string `json:"version"`
|
||||
Commit string `json:"commit"`
|
||||
Uptime string `json:"uptime"`
|
||||
StartTime string `json:"start_time"`
|
||||
Memory map[string]interface{} `json:"memory"`
|
||||
|
||||
@@ -58,15 +58,6 @@ type V3Usage struct {
|
||||
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 返回的最终结果。
|
||||
// Format 与 AudioData 的实际编码一致;controller 据此设置响应 Content-Type。
|
||||
type SynthesisResult struct {
|
||||
|
||||
+4
-1
@@ -131,7 +131,10 @@ func (AdapterRecorder) UpstreamFinished(speaker, model, format, status string, d
|
||||
if audioBytes > 0 {
|
||||
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)})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,13 +9,6 @@ import (
|
||||
"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 {
|
||||
if len(setting.Auth.APIKeys) == 0 {
|
||||
return true
|
||||
|
||||
@@ -13,13 +13,6 @@ var (
|
||||
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 {
|
||||
if origin == "" || origin == "null" || origin == "nil" {
|
||||
return false
|
||||
|
||||
@@ -3,6 +3,7 @@ package middleware
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -23,6 +24,9 @@ func Logger(next http.Handler) http.Handler {
|
||||
next.ServeHTTP(rec, r)
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
package middleware
|
||||
|
||||
// 本文件提供带 metrics 埋点的限流 / 并发中间件版本;
|
||||
// 由于原 ratelimit_middleware.go 在本仓库的云盘同步下被永久占用,
|
||||
// 这里用独立实现覆盖路由使用入口,旧实现保留为未引用代码。
|
||||
//
|
||||
// 行为与原 ratelimit_middleware.go 完全一致,只是多了 metrics 调用。
|
||||
// 本文件提供带 metrics 埋点的限流 / 并发中间件版本。
|
||||
// 相比 router 实际使用的实现,本版本额外做了:
|
||||
// - 加 metrics 埋点(限流拒绝 / 并发拒绝计数)
|
||||
// - 仅对 /v1/ 下的业务请求生效,监控路径(/health /metrics /dashboard)不消耗配额
|
||||
|
||||
import (
|
||||
"log"
|
||||
@@ -14,7 +13,7 @@ import (
|
||||
"github.com/volcano-tts/tts-api/metrics"
|
||||
)
|
||||
|
||||
// RateLimitWithMetrics 是 middleware.RateLimit 的可埋点版本。
|
||||
// RateLimitWithMetrics 是限流中间件,带埋点 + 路径过滤。
|
||||
func RateLimitWithMetrics(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// 仅对 /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 {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// 仅对 /v1/ 下的业务请求统计并发和加锁,监控路径不占用并发槽位
|
||||
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
|
||||
"github.com/volcano-tts/tts-api/adapter/volcano"
|
||||
"github.com/volcano-tts/tts-api/common"
|
||||
"github.com/volcano-tts/tts-api/dto"
|
||||
)
|
||||
|
||||
// 全部环境变量读取的单一入口:其它包不允许直接 os.Getenv,只读这里的全局 Config。
|
||||
@@ -317,7 +316,3 @@ func CheckStaticFiles() {
|
||||
log.Println("警告: health.html 不存在,/dashboard 路由将返回 404")
|
||||
}
|
||||
}
|
||||
|
||||
// 保留 dto.ByteDanceTTSConfig 引用避免 import 警告;
|
||||
// 新代码不应再使用这个类型,设置已在 TTSOptions 中。
|
||||
var _ = dto.ByteDanceTTSConfig{}
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
Reference in New Issue
Block a user