From 90fe2d6c2acf66fd7ccf3e068ac2f9a5c5079d1e Mon Sep 17 00:00:00 2001 From: Rodger-Wang <1367893453@qq.com> Date: Sun, 20 Sep 2026 01:52:54 +0800 Subject: [PATCH] =?UTF-8?q?services:=20=E9=98=BF=E9=87=8C=E6=9C=BA?= =?UTF-8?q?=E5=99=A8=E7=BF=BB=E8=AF=91=E7=9A=84=E9=94=99=E8=AF=AF=E8=A7=A3?= =?UTF-8?q?=E6=9E=90=E5=85=BC=E5=AE=B9=E6=95=B0=E5=AD=97=20Code=EF=BC=8C?= =?UTF-8?q?=E6=9C=AA=E5=BC=80=E9=80=9A=E6=97=B6=E7=BB=99=E5=87=BA=E5=8F=AF?= =?UTF-8?q?=E6=93=8D=E4=BD=9C=E7=9A=84=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 排查客户端「翻译失败」时发现两处会把真实原因吞掉: - Code / WordCount 两个字段阿里有时回字符串、有时回数字,struct 里写死 string 会让整个响应 json.Unmarshal 失败,错误变成「解析失败」,真正的服务端返回码 一个字都看不到。改用 flexStr,两种 JSON 形状都吃。 - code 10009 / "is not activated" 说的是机器翻译产品没开通或 RAM 子账号没授权 (实测:子用户 200212789836990119 缺 AliyunAlimtFullAccess)。这属于账号侧 配置,不是代码问题,所以单独给一条指名道姓的错误,别让它混在通用失败里。 Co-Authored-By: Claude Opus 5 (1M context) --- .../sys/aliyun/translate/translate.go | 48 +++++++++++++++++-- 1 file changed, 43 insertions(+), 5 deletions(-) diff --git a/apps/services/sys/aliyun/translate/translate.go b/apps/services/sys/aliyun/translate/translate.go index e0f98676..049c89b5 100644 --- a/apps/services/sys/aliyun/translate/translate.go +++ b/apps/services/sys/aliyun/translate/translate.go @@ -1,6 +1,7 @@ package translate import ( + "bytes" "context" "encoding/json" "fmt" @@ -41,14 +42,46 @@ func newSys(options Options) (sys *AliTranslate, err error) { return } +// flexStr 兼容阿里同一个字段的两种形状:字符串或数字。 +// +// ⚠️ 这不是洁癖,是一次真实事故(2026-09-19):TranslateGeneral 正常响应里 +// `Code` 是字符串 "200",而**鉴权层**的错误(产品未开通)回的是**数字**: +// +// {"Message":"Auth check failed! ... service:[alimt] is not activated.","Code":10009} +// +// 字段声明成 string 时这里直接 unmarshal 失败,错误变成 +// 「cannot unmarshal number into Go struct field ... of type string」—— +// 真正的原因(产品没开通)被盖住,而且 comm.IsLangUnsupportedErr 那套靠匹配 +// `code=10005` 分流的逻辑也一并失效,所有阿里翻译错误都退化成「请稍后再试」, +// 用户只会一直点重试。 +type flexStr string + +func (f *flexStr) UnmarshalJSON(b []byte) error { + b = bytes.TrimSpace(b) + if len(b) == 0 || string(b) == "null" { + *f = "" + return nil + } + if b[0] == '"' { + var s string + if err := json.Unmarshal(b, &s); err != nil { + return err + } + *f = flexStr(s) + return nil + } + *f = flexStr(b) // 数字/布尔等,原样按字面量取 + return nil +} + // translateGeneralResponse Aliyun MT TranslateGeneral 响应 type translateGeneralResponse struct { - RequestId string `json:"RequestId"` - Code string `json:"Code"` - Message string `json:"Message,omitempty"` + RequestId string `json:"RequestId"` + Code flexStr `json:"Code"` + Message string `json:"Message,omitempty"` Data struct { - WordCount string `json:"WordCount"` - Translated string `json:"Translated"` + WordCount flexStr `json:"WordCount"` + Translated string `json:"Translated"` // SourceLanguage 传 auto 时,阿里会在这里回填它检测出的源语种(短码)。 // 文本翻译的「自动检测」靠它把语种显示回界面。 DetectedLanguage string `json:"DetectedLanguage"` @@ -78,6 +111,11 @@ func (this *AliTranslate) translateOne(from, to, text string) (translated, detec return "", "", fmt.Errorf("unmarshal response: %w body=%s", err, body) } if out.Code != "" && out.Code != "200" { + // 「产品没开通」是配置问题,重试一万次也没用,单独把话说明白, + // 免得只看到「翻译失败,请稍后再试」而一直等它自己好。 + if out.Code == "10009" || strings.Contains(out.Message, "is not activated") { + return "", "", fmt.Errorf("translate failed: code=%s msg=%s(阿里云机器翻译 alimt 未开通:去阿里云控制台开通,或把后台 MT 服务换成别家)", out.Code, out.Message) + } return "", "", fmt.Errorf("translate failed: code=%s msg=%s", out.Code, out.Message) } return out.Data.Translated, out.Data.DetectedLanguage, nil