Browse Source

services: 阿里机器翻译的错误解析兼容数字 Code,未开通时给出可操作的提示

排查客户端「翻译失败」时发现两处会把真实原因吞掉:

- Code / WordCount 两个字段阿里有时回字符串、有时回数字,struct 里写死 string
  会让整个响应 json.Unmarshal 失败,错误变成「解析失败」,真正的服务端返回码
  一个字都看不到。改用 flexStr,两种 JSON 形状都吃。
- code 10009 / "is not activated" 说的是机器翻译产品没开通或 RAM 子账号没授权
  (实测:子用户 200212789836990119 缺 AliyunAlimtFullAccess)。这属于账号侧
  配置,不是代码问题,所以单独给一条指名道姓的错误,别让它混在通用失败里。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
main
Rodger-Wang 3 weeks ago
parent
commit
90fe2d6c2a
  1. 48
      apps/services/sys/aliyun/translate/translate.go

48
apps/services/sys/aliyun/translate/translate.go

@ -1,6 +1,7 @@
package translate package translate
import ( import (
"bytes"
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
@ -41,14 +42,46 @@ func newSys(options Options) (sys *AliTranslate, err error) {
return 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 响应 // translateGeneralResponse Aliyun MT TranslateGeneral 响应
type translateGeneralResponse struct { type translateGeneralResponse struct {
RequestId string `json:"RequestId"` RequestId string `json:"RequestId"`
Code string `json:"Code"` Code flexStr `json:"Code"`
Message string `json:"Message,omitempty"` Message string `json:"Message,omitempty"`
Data struct { Data struct {
WordCount string `json:"WordCount"` WordCount flexStr `json:"WordCount"`
Translated string `json:"Translated"` Translated string `json:"Translated"`
// SourceLanguage 传 auto 时,阿里会在这里回填它检测出的源语种(短码)。 // SourceLanguage 传 auto 时,阿里会在这里回填它检测出的源语种(短码)。
// 文本翻译的「自动检测」靠它把语种显示回界面。 // 文本翻译的「自动检测」靠它把语种显示回界面。
DetectedLanguage string `json:"DetectedLanguage"` 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) return "", "", fmt.Errorf("unmarshal response: %w body=%s", err, body)
} }
if out.Code != "" && out.Code != "200" { 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 "", "", fmt.Errorf("translate failed: code=%s msg=%s", out.Code, out.Message)
} }
return out.Data.Translated, out.Data.DetectedLanguage, nil return out.Data.Translated, out.Data.DetectedLanguage, nil

Loading…
Cancel
Save