package translate import ( "context" "encoding/json" "fmt" "net/http" "strings" "sync" "time" "github.com/aliyun/alibaba-cloud-sdk-go/sdk" "github.com/aliyun/alibaba-cloud-sdk-go/sdk/auth/credentials" "github.com/aliyun/alibaba-cloud-sdk-go/sdk/requests" ) // Aliyun Machine Translation (alimt) TranslateGeneral // Docs: https://help.aliyun.com/document_detail/158244.html type AliTranslate struct { options Options client *sdk.Client } func newSys(options Options) (sys *AliTranslate, err error) { if options.AccessKeyId == "" || options.AccessKeySecret == "" { return nil, fmt.Errorf("aliyun translate: AccessKeyId/AccessKeySecret required") } cfg := sdk.NewConfig() cfg.HttpTransport = &http.Transport{IdleConnTimeout: 10 * time.Second} cfg.Timeout = 15 * time.Second cred := credentials.NewAccessKeyCredential(options.AccessKeyId, options.AccessKeySecret) cli, err := sdk.NewClientWithOptions(options.Region, cfg, cred) if err != nil { return nil, fmt.Errorf("new sdk client: %w", err) } sys = &AliTranslate{ options: options, client: cli, } return } // translateGeneralResponse Aliyun MT TranslateGeneral 响应 type translateGeneralResponse struct { RequestId string `json:"RequestId"` Code string `json:"Code"` Message string `json:"Message,omitempty"` Data struct { WordCount string `json:"WordCount"` Translated string `json:"Translated"` } `json:"Data"` } func (this *AliTranslate) translateOne(from, to, text string) (string, error) { req := requests.NewCommonRequest() req.Method = "POST" req.Scheme = "https" req.Domain = fmt.Sprintf("mt.%s.aliyuncs.com", this.options.Region) req.ApiName = "TranslateGeneral" req.Version = "2018-10-12" req.QueryParams["FormatType"] = this.options.FormatType req.QueryParams["SourceLanguage"] = from req.QueryParams["TargetLanguage"] = to req.QueryParams["SourceText"] = text req.QueryParams["Scene"] = this.options.Scene resp, err := this.client.ProcessCommonRequest(req) if err != nil { return "", fmt.Errorf("translate request: %w", err) } body := resp.GetHttpContentString() var out translateGeneralResponse if err = json.Unmarshal([]byte(body), &out); err != nil { return "", fmt.Errorf("unmarshal response: %w body=%s", err, body) } if out.Code != "" && out.Code != "200" { return "", fmt.Errorf("translate failed: code=%s msg=%s", out.Code, out.Message) } return out.Data.Translated, nil } // isThrottling 判断是否为阿里 MT 的用户级流控错误(ErrorCode: Throttling.User)。 // 这类错误是暂时的,退避后重试通常能成功。 func isThrottling(err error) bool { if err == nil { return false } s := err.Error() return strings.Contains(s, "Throttling") || strings.Contains(s, "flow control") } // translateRetryBackoff 限流重试的退避间隔。阿里返回的 X-Acs-Retry-After 常达 80~170s, // 全额等待会把一篇会议的翻译拖到十几分钟;这里用较短的递增退避,配合「失败降级为原文」兜底。 var translateRetryBackoff = []time.Duration{500 * time.Millisecond, 2 * time.Second, 5 * time.Second} // translateOneWithRetry 单句翻译;遇到流控按 translateRetryBackoff 退避重试。 // 非流控错误(如参数错误)不重试,直接返回。 func (this *AliTranslate) translateOneWithRetry(ctx context.Context, from, to, text string) (string, error) { var lastErr error for attempt := 0; ; attempt++ { r, e := this.translateOne(from, to, text) if e == nil { return r, nil } lastErr = e if !isThrottling(e) || attempt >= len(translateRetryBackoff) { return "", lastErr } select { case <-ctx.Done(): return "", ctx.Err() case <-time.After(translateRetryBackoff[attempt]): } } } // Translate 批量翻译,按 Concurrency 并发执行 TranslateGeneral。 // from / to 为阿里 MT 语言码:zh / en / ja / ko / ...(保持调用方传入的标准) // // 容错语义(调用方需注意): // - 单句重试后仍失败,**用原文占位**,results 始终与 texts 等长; // - 部分失败:返回 (results, err) —— results 可用,err 描述失败句数,调用方记日志后应继续; // - 全部失败:返回 (nil, err) —— 视为硬失败。 // // 这样一次限流不会把整篇会议的译文清空(进而让 AI 总结拿到空内容、报 content field is required)。 func (this *AliTranslate) Translate(ctx context.Context, from, to string, texts []string) (results []string, err error) { if len(texts) == 0 { return []string{}, nil } results = make([]string, len(texts)) errs := make([]error, len(texts)) sem := make(chan struct{}, this.options.Concurrency) var wg sync.WaitGroup for i, t := range texts { select { case <-ctx.Done(): return nil, ctx.Err() default: } wg.Add(1) sem <- struct{}{} go func(idx int, text string) { defer wg.Done() defer func() { <-sem }() // 空串/纯空白直接返回原值,阿里 MT 对空文本会报 code=10004 if strings.TrimSpace(text) == "" { results[idx] = text return } r, e := this.translateOneWithRetry(ctx, from, to, text) if e != nil { results[idx] = text // 降级:保留原文,别让整批作废 errs[idx] = e return } results[idx] = r }(i, t) } wg.Wait() failed, firstIdx := 0, -1 var firstErr error for i, e := range errs { if e != nil { failed++ if firstErr == nil { firstErr, firstIdx = e, i } } } switch { case failed == 0: return results, nil case failed == len(texts): return nil, fmt.Errorf("translate all %d failed, first index=%d: %w", failed, firstIdx, firstErr) default: return results, fmt.Errorf("translate partial failure: %d/%d failed (已用原文占位), first index=%d: %w", failed, len(texts), firstIdx, firstErr) } }