refactor(voice): dashscopeErrorToken 收敛到 trace.ErrorToken
令牌守卫的最后一处副本:删掉 voice 本地的 dashscopeErrorToken 与 maxErrorTokenBytes,四个调用点(classifyDashscopeStatus 的 code / request_id、 dashscopeOKBodyError 的 code / type)改调 trace.ErrorToken。守卫实现与原先逐字 同形(TrimSpace、64 字节上限、字母数字与 _ . -),因此零语义变化。 同时修掉一处过期注释:原 dashscopeErrorToken 的文档指向「runtime/model 的 safeErrorToken」——那个符号在本切片里已被删除,注释指向不存在的符号会把下一个人 送去空目录。现在注释与调用点都指向 trace.ErrorToken。 顺带补一条变异可打红的用例:已有用例只断言「令牌形状的 code 被保留」(去掉守卫 照样通过,半张网),新用例断言 4xx 的 code 是散文时连 `code=` 这个键都不出现, 而 status=400 与 request_id 仍在。 验收:go build ./... + go vet ./... + go test -count=1 ./internal/runtime/multimodal/voice/... 全绿;变异(code 改回不过守卫)⇒ TestDashscopeTTS_Non200DropsFreeTextCode 变红。
This commit is contained in:
1 parent
3001fcc716
commit
5c6f4e50a0
2 files changed
+62
-29
No files matched your search
@@ -18,10 +18,6 @@ import (
|
|||||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/trace"
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/trace"
|
||||||
)
|
)
|
||||||
|
|
||||||
// maxErrorTokenBytes 是 error.code / request_id 允许出镜的长度上限(见
|
|
||||||
// dashscopeErrorToken)。
|
|
||||||
const maxErrorTokenBytes = 64
|
|
||||||
|
|
||||||
// classifyDashscopeStatus 把一个非 200 的 DashScope 响应翻成**可分类**的失败:鉴权失败 /
|
// classifyDashscopeStatus 把一个非 200 的 DashScope 响应翻成**可分类**的失败:鉴权失败 /
|
||||||
// 欠费额度 / 限流各自成一条哨兵链(都以 ErrProviderFailed 为底),其余原样归入
|
// 欠费额度 / 限流各自成一条哨兵链(都以 ErrProviderFailed 为底),其余原样归入
|
||||||
// ErrProviderFailed(切片 IM-ASR 的熔断据此判定「再试是不是白试」)。
|
// ErrProviderFailed(切片 IM-ASR 的熔断据此判定「再试是不是白试」)。
|
||||||
@@ -45,8 +41,8 @@ func classifyDashscopeStatus(endpoint string, status int, contentType string, bo
|
|||||||
// helper 的构造是固定的,走不到这里;真走到也只少一条诊断。
|
// helper 的构造是固定的,走不到这里;真走到也只少一条诊断。
|
||||||
return fmt.Errorf("voice: %w", sentinel)
|
return fmt.Errorf("voice: %w", sentinel)
|
||||||
}
|
}
|
||||||
info.Code = dashscopeErrorToken(fields.code)
|
info.Code = trace.ErrorToken(fields.code)
|
||||||
if rid := dashscopeErrorToken(fields.requestID); rid != "" {
|
if rid := trace.ErrorToken(fields.requestID); rid != "" {
|
||||||
return fmt.Errorf("voice: %w: request_id=%s: %w", sentinel, rid, info)
|
return fmt.Errorf("voice: %w: request_id=%s: %w", sentinel, rid, info)
|
||||||
}
|
}
|
||||||
return fmt.Errorf("voice: %w: %w", sentinel, info)
|
return fmt.Errorf("voice: %w: %w", sentinel, info)
|
||||||
@@ -102,35 +98,15 @@ func decodeDashscopeErrorFields(body []byte) dashscopeErrorFields {
|
|||||||
// message 原文一律不进 error:它是上游自由文本,而这条请求的 input.url 是参考
|
// message 原文一律不进 error:它是上游自由文本,而这条请求的 input.url 是参考
|
||||||
// 音频的公开地址(可能是预签名 URL,带凭据),上游报错经常把它整段回显出来。
|
// 音频的公开地址(可能是预签名 URL,带凭据),上游报错经常把它整段回显出来。
|
||||||
// 病因分类保留在 ErrProviderFailed(再试也不会好,与既有的熔断口径一致);
|
// 病因分类保留在 ErrProviderFailed(再试也不会好,与既有的熔断口径一致);
|
||||||
// 确实带回来的 code/type 也只以令牌形态出镜(过 dashscopeErrorToken)。
|
// 确实带回来的 code/type 也只以令牌形态出镜(过 trace.ErrorToken)。
|
||||||
func dashscopeOKBodyError(endpoint string, body []byte) error {
|
func dashscopeOKBodyError(endpoint string, body []byte) error {
|
||||||
info, ok := trace.UpstreamError(trace.HostOf(endpoint), http.StatusOK, "", body).(*trace.UpstreamErrorInfo)
|
info, ok := trace.UpstreamError(trace.HostOf(endpoint), http.StatusOK, "", body).(*trace.UpstreamErrorInfo)
|
||||||
if !ok {
|
if !ok {
|
||||||
// helper 的构造是固定的,走不到这里;真走到也只少一条诊断。
|
// helper 的构造是固定的,走不到这里;真走到也只少一条诊断。
|
||||||
return fmt.Errorf("voice: %w: dashscope reported a business failure on a 200 response", ErrProviderFailed)
|
return fmt.Errorf("voice: %w: dashscope reported a business failure on a 200 response", ErrProviderFailed)
|
||||||
}
|
}
|
||||||
info.Code = dashscopeErrorToken(info.Code)
|
info.Code = trace.ErrorToken(info.Code)
|
||||||
info.Type = dashscopeErrorToken(info.Type)
|
info.Type = trace.ErrorToken(info.Type)
|
||||||
return fmt.Errorf("voice: %w: dashscope reported a business failure on a 200 response: %w",
|
return fmt.Errorf("voice: %w: dashscope reported a business failure on a 200 response: %w",
|
||||||
ErrProviderFailed, info)
|
ErrProviderFailed, info)
|
||||||
}
|
}
|
||||||
|
|
||||||
// dashscopeErrorToken 只放行「像协议令牌」的短串(字母、数字与 `_ . -`):code
|
|
||||||
// 在 DashScope 是枚举(InvalidParameter / Throttling.RateQuota / Arrearage …),
|
|
||||||
// request_id 是十六进制/UUID 形状。两者都是自由文本面,不像令牌的一律丢弃 ——
|
|
||||||
// 宁可少一条分类提示,不让散文借道出镜(同口径见 runtime/model 的 safeErrorToken)。
|
|
||||||
func dashscopeErrorToken(s string) string {
|
|
||||||
s = strings.TrimSpace(s)
|
|
||||||
if s == "" || len(s) > maxErrorTokenBytes {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
for _, r := range s {
|
|
||||||
switch {
|
|
||||||
case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9':
|
|
||||||
case r == '_' || r == '.' || r == '-':
|
|
||||||
default:
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package voice
|
||||||
|
|
||||||
|
// 本文件钉住 DashScope 错误收口里的**令牌守卫**(trace.ErrorToken):code 与
|
||||||
|
// request_id 都是上游自由文本面 —— code 在 DashScope 是枚举(InvalidParameter /
|
||||||
|
// Throttling.RateQuota / Arrearage …),但网关 / 兼容层可以往里塞任何东西。
|
||||||
|
//
|
||||||
|
// 已有的用例(TestDashscopeTTS_Non200DropsSpokenText)只断言「令牌形状的 code 被
|
||||||
|
// 保留」,去掉守卫它照样通过 —— 那是个半张网。这里补上另一半:**不像令牌的 code
|
||||||
|
// 必须变空串**,而 status / request_id 这些诊断面仍在。
|
||||||
|
//
|
||||||
|
// 形状是「把守去掉就变红」:断言的是错误文本,也就是真正进日志的字符串。
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestDashscopeTTS_Non200DropsFreeTextCode 4xx 的 code 里放一段散文(带空格与标点)
|
||||||
|
// ⇒ 该字段整体消失(连 `code=` 这个键都不出现),而 status 400 与 request_id 仍在。
|
||||||
|
func TestDashscopeTTS_Non200DropsFreeTextCode(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
const prose = "Invalid value: '她刚才说的那句话'"
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
_, _ = w.Write([]byte(`{"code":"` + prose + `","request_id":"req-0123456789abcdef"}`))
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
tts := NewDashscopeTTS(config.VoiceConfig{DashscopeBaseURL: srv.URL, DashscopeAPIKey: "sk"})
|
||||||
|
_, err := tts.TTS(context.Background(), "随便说点什么")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("400 却报成功")
|
||||||
|
}
|
||||||
|
if !errors.Is(err, ErrProviderFailed) {
|
||||||
|
t.Fatalf("err = %v, want ErrProviderFailed(熔断口径不变)", err)
|
||||||
|
}
|
||||||
|
msg := err.Error()
|
||||||
|
if strings.Contains(msg, prose) {
|
||||||
|
t.Fatalf("code 里的散文进了错误:%v", msg)
|
||||||
|
}
|
||||||
|
if strings.Contains(msg, "code=") {
|
||||||
|
t.Fatalf("非令牌形状的 code 没有被丢弃:%v", msg)
|
||||||
|
}
|
||||||
|
// 诊断面不能一起丢:状态码与 request_id(对厂商报障时要给的那个)。
|
||||||
|
for _, want := range []string{"status=400", "req-0123456789abcdef"} {
|
||||||
|
if !strings.Contains(msg, want) {
|
||||||
|
t.Fatalf("错误里缺少 %q(诊断面不能一起丢):%v", want, msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user