You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

98 lines
4.5 KiB

package user
import (
"context"
"strings"
"time"
"yunyan/comm"
"yunyan/lego/sys/log"
"yunyan/pb"
)
// @Summary 实时机器翻译
// @Description 同声传译/面对面翻译用;服务端调用后台「第三方服务配置」里启用的 MT 服务
// @Tags User
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param user body pb.UserTranslateReq true "翻译请求"
// @Success 200 {object} comm.HttpResult{data=pb.UserTranslateResp} "成功返回"
// @Router /api/home/user_translate [post]
//
// 语言码:客户端传 BCP-47(zh-CN/en-US),这里统一归一成服务商要的短码。
// 阿里云对语言码传错不会报错,只会返回空或原文,所以归一放服务端收口。
func (this *apiComp) Translate(session comm.IUserSession, req *pb.UserTranslateReq) (resp *pb.UserTranslateResp, errdata *pb.ErrorData) {
text := strings.TrimSpace(req.Text)
to := comm.BCP47ToShortLang(req.To)
from := comm.BCP47ToShortLang(req.From)
if text == "" || to == "" {
errdata = &pb.ErrorData{Code: pb.ErrorCode_TranslateParamInvalid, Message: "原文或目标语言为空"}
return
}
// 同语种直接回原文,别浪费一次调用(同传里源=目标的情况很常见)
if from != "" && from == to {
resp = &pb.UserTranslateResp{Text: req.Text}
return
}
// VIP 是翻译的准入闸门(自购 或 当前绑定设备赋予,见 comm.ResolveVip)。
// 文本/图片翻译与三段式(同传/面对面)的机器翻译那一跳都经过这里,拦住即整体不可用;
// 通话翻译走端到端直连服务商,拦不到——它的服务端手段是不下发凭据(svcresolve.go)。
if vip, _, verr := comm.ResolveUserVip(session.GetUserId(), nil); verr == nil && !vip.Active(time.Now().Unix()) {
errdata = &pb.ErrorData{Code: pb.ErrorCode_VipRequired, Message: "翻译功能需要会员,请续费或绑定带会员的设备"}
return
}
svcId, provider, tr, err := this.module.translatemodel.resolveTranslator()
if err != nil {
this.module.Error("Translate: 翻译服务解析失败",
log.Field{Key: "svc", Value: svcId}, log.Field{Key: "provider", Value: provider},
log.Field{Key: "err", Value: err.Error()})
errdata = &pb.ErrorData{Code: pb.ErrorCode_TranslateNotConfigured, Message: "翻译服务不可用"}
return
}
if tr == nil {
// 没配,或者配的是还没接的服务商
this.module.Warn("Translate: 无可用翻译服务",
log.Field{Key: "svc", Value: svcId}, log.Field{Key: "provider", Value: provider})
errdata = &pb.ErrorData{Code: pb.ErrorCode_TranslateNotConfigured, Message: "未配置机器翻译服务"}
return
}
// from 为空即自动检测:交给服务商识别,省掉一次单独的语种识别调用,
// 检测出的短码随响应回带,客户端据此把「自动检测」显示成具体语种。
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
translated, detected, err := tr.TranslateDetect(ctx, from, to, text)
if err != nil {
this.module.Error("Translate: 服务商调用失败",
log.Field{Key: "uid", Value: session.GetUserId()}, log.Field{Key: "svc", Value: svcId},
log.Field{Key: "from", Value: from}, log.Field{Key: "to", Value: to},
log.Field{Key: "err", Value: err.Error()})
// ⚠️ 「这个语种服务商压根不支持」和「这次调用失败了」要分开说:
// 前者重试一万次也没用,报「请稍后再试」只会让用户一直点。
// 2026-09-15 实测:乌克兰语(uk) 阿里所有方向都回 10005、克罗地亚语(hr) 回 10033,
// 而客户端的语言表是按 Azure 语音语种列的,比阿里翻译能覆盖的多出这几个。
if comm.IsLangUnsupportedErr(err) {
errdata = &pb.ErrorData{
Code: pb.ErrorCode_TranslateProviderError,
Message: "该语言暂不支持翻译,请换一个目标语言",
}
return
}
errdata = &pb.ErrorData{Code: pb.ErrorCode_TranslateProviderError, Message: "翻译失败,请稍后再试"}
return
}
if strings.TrimSpace(translated) == "" {
// 返回空通常意味着语言码不被支持——报错比悄悄返回空串强,
// 否则客户端会显示一片空白,谁也不知道发生了什么。
this.module.Warn("Translate: 服务商返回空结果",
log.Field{Key: "svc", Value: svcId}, log.Field{Key: "from", Value: from}, log.Field{Key: "to", Value: to})
errdata = &pb.ErrorData{Code: pb.ErrorCode_TranslateProviderError, Message: "翻译结果为空"}
return
}
resp = &pb.UserTranslateResp{Text: translated, Provider: provider, Svcid: svcId, DetectedFrom: detected}
return
}