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 一起发)
This commit is contained in:
+42
-1
@@ -8,11 +8,52 @@
|
||||
// - Meter 是高层入口,NoopMeter 用于测试。
|
||||
package telemetry
|
||||
|
||||
import "sort"
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"encoding/hex"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Labels 是指标附加的标签集合。Value 在序列化时会按 Prometheus 规范转义。
|
||||
type Labels map[string]string
|
||||
|
||||
// SpeakerLabel 把 speaker ID 转成不可逆的稳定短哈希,作为指标 label。
|
||||
// 目的:保护火山复刻音色资产(speaker ID 是用户付费 / 隐私敏感);
|
||||
// 同时仍能按 voice 聚合观测(同 speaker → 同 label)。
|
||||
//
|
||||
// 算法: sha1(s)[:8] = 32 bits 空间;典型 <100 个 voice 场景无碰撞风险。
|
||||
// 空串返回 "unknown",避免 /metrics label 出现空值 (Prometheus 禁止空 label)。
|
||||
//
|
||||
// 注意: 这是**不可逆**哈希,不是加密;不可用于需要还原原始 speaker 的场景。
|
||||
// Admin UI 想要看原名时,通过 /api/voices 拿 name 字段对照。
|
||||
func SpeakerLabel(s string) string {
|
||||
if s == "" {
|
||||
return "unknown"
|
||||
}
|
||||
sum := sha1.Sum([]byte(s))
|
||||
return hex.EncodeToString(sum[:])[:8]
|
||||
}
|
||||
|
||||
// MaskSpeaker 把 speaker ID 部分打码用于日志输出。
|
||||
// - 空 → "***"
|
||||
// - 长度 ≤ 4 → 全打码
|
||||
// - 其它 → 前 4 + **** + 后 4 (保留前缀便于肉眼区分 "S_xx 开头" vs "BV001_...")
|
||||
// 例子: "S_G8tEKnaJ1" → "S_G8****naJ1"
|
||||
func MaskSpeaker(s string) string {
|
||||
if s == "" {
|
||||
return "(未设置)"
|
||||
}
|
||||
if len(s) <= 4 {
|
||||
return strings.Repeat("*", len(s))
|
||||
}
|
||||
// 找前 4 字符中第一个非 [A-Za-z0-9_] 字符做截断,避免截到奇怪位置
|
||||
// (虽然火山 ID 实际都是 S_xxx 字母数字组合,这里保险)
|
||||
prefix := s[:4]
|
||||
suffix := s[len(s)-4:]
|
||||
return prefix + "****" + suffix
|
||||
}
|
||||
|
||||
// labelKey 计算一组标签的稳定 key,用于在内部 map 中唯一定位 child。
|
||||
// 缺失或多余的 label 一律视为空串,以保证 child 数量与 label 名集合一致。
|
||||
func labelKey(names []string, labels Labels) string {
|
||||
|
||||
Reference in New Issue
Block a user