fix(voice): CloneVoice 的 200-with-message 泄漏收口到 dashscopeOKBodyError

M41 同族的最后一条缺口:HTTP 200 的响应里用顶层 message 报业务失败,而这句话
是上游自由文本 —— create_voice 的 input.url 是参考音频的公开地址(可能是预签名
URL,带凭据),上游报错常把它原样回显。

- ERROR 只留「打到了、但 200 是个业务失败」这个形状事实 + 体积摘要;
  message 原文不进 error,code/type 若真的带回来也只以令牌形态出镜
- errors.Is(err, ErrProviderFailed) 不变(熔断口径不动)
- 用例断言失败仍被识别、且 error 文本里搜不到哨兵正文
This commit is contained in:
AskaEth committed 2026-10-26 13:50:10 +08:00
1 parent 66dda084cd
commit 9e88605cc8
3 files changed
+54 -1

No files matched your search

@@ -94,6 +94,27 @@ func decodeDashscopeErrorFields(body []byte) dashscopeErrorFields {
return dashscopeErrorFields{code: wire.Code, requestID: wire.RequestID}
}
// dashscopeOKBodyError 处理 DashScope 的「HTTP 200 但业务失败」形状:音色复刻
// (create_voice)用顶层 message 报错,因此**不走** classifyDashscopeStatus ——
// 200 这条路必须单独收口,否则它就成了这一族里唯一还漏的缺口。
//
// 只出镜一个**形状事实**(打到了、但 200 响应是个业务失败)加上体积摘要;
// message 原文一律不进 error:它是上游自由文本,而这条请求的 input.url 是参考
// 音频的公开地址(可能是预签名 URL,带凭据),上游报错经常把它整段回显出来。
// 病因分类保留在 ErrProviderFailed(再试也不会好,与既有的熔断口径一致);
// 确实带回来的 code/type 也只以令牌形态出镜(过 dashscopeErrorToken)。
func dashscopeOKBodyError(endpoint string, body []byte) error {
info, ok := trace.UpstreamError(trace.HostOf(endpoint), http.StatusOK, "", body).(*trace.UpstreamErrorInfo)
if !ok {
// helper 的构造是固定的,走不到这里;真走到也只少一条诊断。
return fmt.Errorf("voice: %w: dashscope reported a business failure on a 200 response", ErrProviderFailed)
}
info.Code = dashscopeErrorToken(info.Code)
info.Type = dashscopeErrorToken(info.Type)
return fmt.Errorf("voice: %w: dashscope reported a business failure on a 200 response: %w",
ErrProviderFailed, info)
}
// dashscopeErrorToken 只放行「像协议令牌」的短串(字母、数字与 `_ . -`):code
// 在 DashScope 是枚举(InvalidParameter / Throttling.RateQuota / Arrearage …),
// request_id 是十六进制/UUID 形状。两者都是自由文本面,不像令牌的一律丢弃 ——