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.
193 lines
7.7 KiB
193 lines
7.7 KiB
package user
|
|
|
|
import (
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"yunyan/comm"
|
|
"yunyan/lego/core"
|
|
"yunyan/lego/core/cbase"
|
|
"yunyan/lego/sys/log"
|
|
"yunyan/lego/sys/postgres"
|
|
alitranslate "yunyan/sys/aliyun/translate"
|
|
)
|
|
|
|
// 实时机器翻译(同声传译 / 面对面翻译)的服务解析。
|
|
//
|
|
// 与实名认证同一套路子:翻译服务配在后台「第三方服务配置」(类别 comm.SvcCatMT=3),
|
|
// 存 console 共享库 postgres,凭据在服务端解密使用,**不下发客户端**。
|
|
//
|
|
// 为什么放服务端而不是像以前那样客户端直连:
|
|
// 以前客户端用火山翻译,AK/SK 是通过 AppConfig.env **明文下发给每一个客户端**的,
|
|
// 任何人抓个包或反编译就能拿到云账号凭据。改成服务端代调后凭据不再出网关。
|
|
|
|
// mtCacheTTL 翻译客户端的缓存时长。
|
|
//
|
|
// 同传是流式高频调用,每句都去查一次库、重建一次 SDK 客户端太浪费;
|
|
// 但也不能永久缓存——后台改了配置得能生效,所以给个短 TTL。
|
|
const mtCacheTTL = 60 * time.Second
|
|
|
|
// mtProviderPriority 服务端**真正接得上**的机器翻译服务商,值越小越优先。
|
|
//
|
|
// ⚠️ 后台「第三方服务配置」里能建出、也能启用的 MT 服务远不止这些
|
|
// (mt_volcano / mt_azure 都有现成模板、凭据也填得进去),但**配得出 ≠ 接得上**:
|
|
// 不在这张表里的服务商,服务端没有任何一行代码会去调它。
|
|
//
|
|
// 挑选时必须先按这张表过滤。不过滤的后果不是「少一家可选」,而是**整个翻译不可用**:
|
|
// 同一类别下启用了多家时,挑中哪一个纯看 postgres 的返回顺序,挑中没接的那家就直接
|
|
// 报「未配置机器翻译服务」,哪怕旁边正躺着一个凭据齐全的 mt_alibaba,而且错误信息
|
|
// 指向「没配置」,只会把人引去后台反复检查明明填好了的凭据。
|
|
// 2026-09-14 线上就是这样:EAIMAR 下三家 MT 全启用,随手挑中了 mt_volcano。
|
|
//
|
|
// 接新的一家:在 sys/ 下实现(bytedance=volcano、microsoft=azure、google 都已有 sys 包,
|
|
// 但只有阿里实现了自动检测源语种的 TranslateDetect),这里加一行,
|
|
// 并在 resolveTranslator 下半段按 provider 加一个构造分支。两处都改才算接上。
|
|
var mtProviderPriority = map[string]int{
|
|
"alibaba": 0,
|
|
}
|
|
|
|
// mtNormProvider 统一 provider 的大小写与空白,库里的值是人手填的。
|
|
func mtNormProvider(p string) string { return strings.TrimSpace(strings.ToLower(p)) }
|
|
|
|
// pickMTSvc 从候选里挑一个服务端接得上的机器翻译服务。
|
|
//
|
|
// 排序口径:先按作用域(本应用的行优先于全局行),再按 mtProviderPriority。
|
|
// 第二个返回值是**被跳过的未接入服务**,只用于日志——「后台配了三家,一家都没用上」
|
|
// 这件事必须能从日志里一眼看出来,否则排查只能靠猜。
|
|
//
|
|
// 抽成纯函数是为了能脱离 postgres 测试,见 model_translate_test.go。
|
|
func pickMTSvc(svcs []*comm.ThirdSvcConfig, app string) (picked *comm.ThirdSvcConfig, skipped []string) {
|
|
var bestScope, bestRank int
|
|
for _, s := range svcs {
|
|
if s == nil || !s.Enable || !comm.CategoriesHas(s.Categories, comm.SvcCatMT) {
|
|
continue
|
|
}
|
|
rank, ok := mtProviderPriority[mtNormProvider(s.Provider)]
|
|
if !ok {
|
|
skipped = append(skipped, s.Id+"/"+s.Provider)
|
|
continue
|
|
}
|
|
// 作用域:本应用的行优先于全局行(作用域归一后全局行已不再产生,留着兼容存量)
|
|
scope := 1
|
|
if app != "" && s.AppName == app {
|
|
scope = 0
|
|
}
|
|
if picked == nil || scope < bestScope || (scope == bestScope && rank < bestRank) {
|
|
picked, bestScope, bestRank = s, scope, rank
|
|
}
|
|
}
|
|
return picked, skipped
|
|
}
|
|
|
|
type modelTranslateComp struct {
|
|
cbase.ModuleCompBase
|
|
module *User
|
|
|
|
mu sync.RWMutex
|
|
cached alitranslate.ISys
|
|
cachedId string
|
|
cachedProvider string
|
|
cachedAt time.Time
|
|
|
|
// 上一次报告过的「未接入的 MT 服务」,用于日志去重,见 noteSkipped
|
|
lastSkipped string
|
|
}
|
|
|
|
func (this *modelTranslateComp) Init(service core.IService, module core.IModule, comp core.IModuleComp, opt core.IModuleOptions) (err error) {
|
|
this.ModuleCompBase.Init(service, module, comp, opt)
|
|
this.module = module.(*User)
|
|
return
|
|
}
|
|
|
|
// resolveTranslator 取本应用可用的机器翻译服务。
|
|
//
|
|
// 作用域口径与 resolveThirdSvcs 一致:应用行(app_name=<app>)优先于全局行(app_name=”),
|
|
// 候选须同时满足三条:启用、类别含 comm.SvcCatMT、**provider 在 mtProviderPriority 里**
|
|
// (即服务端真的接了这家)。挑选见 pickMTSvc。
|
|
// 返回 svcId/provider 供落日志与响应回带,便于排查"到底用的哪家"。
|
|
func (this *modelTranslateComp) resolveTranslator() (svcId, provider string, t alitranslate.ISys, err error) {
|
|
this.mu.RLock()
|
|
if this.cached != nil && time.Since(this.cachedAt) < mtCacheTTL {
|
|
svcId, provider, t = this.cachedId, this.cachedProvider, this.cached
|
|
this.mu.RUnlock()
|
|
return svcId, provider, t, nil
|
|
}
|
|
this.mu.RUnlock()
|
|
|
|
app := comm.AppName()
|
|
svcs := make([]*comm.ThirdSvcConfig, 0)
|
|
if err = postgres.Find(comm.TableSvcConfig, &svcs, "(app_name=? OR app_name='')", app); err != nil {
|
|
if err == postgres.ErrNoDocuments {
|
|
err = nil
|
|
} else {
|
|
return
|
|
}
|
|
}
|
|
|
|
picked, skipped := pickMTSvc(svcs, app)
|
|
// ⚠️ 这条只在「跳过的集合变了」时打一次。翻译是高频调用(同传每句一次),
|
|
// 无条件打会把服务器日志刷满,真正的错误反而被埋掉。
|
|
if len(skipped) > 0 && this.noteSkipped(skipped) {
|
|
// 「后台启用了、服务端没接」不是错误,但一定要能看见——它是「凭据明明填了
|
|
// 却报未配置」的唯一线索。
|
|
this.module.Warn("resolveTranslator: 跳过服务端未接入的 MT 服务",
|
|
log.Field{Key: "app", Value: app},
|
|
log.Field{Key: "skipped", Value: strings.Join(skipped, ",")},
|
|
log.Field{Key: "picked", Value: pickedIdOf(picked)})
|
|
}
|
|
if picked == nil {
|
|
return "", "", nil, nil // 未配置:调用方回 TranslateNotConfigured
|
|
}
|
|
|
|
fields, ferr := comm.ResolveSvcPlainFields(picked, nil, os.Getenv("FIELD_ENCRYPT_KEY"))
|
|
if ferr != nil {
|
|
return picked.Id, picked.Provider, nil, ferr
|
|
}
|
|
|
|
// 目前只实现了阿里云。接别家时在这里加分支,并同步 mtProviderPriority——
|
|
// 只加一处的话:只加表 → 走到这里返回 nil 仍报「未配置」;只加分支 → 上面过滤时
|
|
// 就被当成未接入跳过了,分支根本执行不到。
|
|
if mtNormProvider(picked.Provider) != "alibaba" {
|
|
return picked.Id, picked.Provider, nil, nil
|
|
}
|
|
|
|
opts := []alitranslate.Option{
|
|
alitranslate.SetAccessKeyId(strings.TrimSpace(fields["access_key_id"])),
|
|
alitranslate.SetAccessKeySecret(strings.TrimSpace(fields["access_key_secret"])),
|
|
}
|
|
if r := strings.TrimSpace(fields["region"]); r != "" {
|
|
opts = append(opts, alitranslate.SetRegion(r))
|
|
}
|
|
sys, serr := alitranslate.NewSys(opts...)
|
|
if serr != nil {
|
|
return picked.Id, picked.Provider, nil, serr
|
|
}
|
|
|
|
this.mu.Lock()
|
|
this.cached, this.cachedId, this.cachedProvider, this.cachedAt = sys, picked.Id, mtNormProvider(picked.Provider), time.Now()
|
|
this.mu.Unlock()
|
|
return picked.Id, picked.Provider, sys, nil
|
|
}
|
|
|
|
// pickedIdOf 日志用:挑不出服务时打 "-" 而不是崩在空指针上。
|
|
func pickedIdOf(s *comm.ThirdSvcConfig) string {
|
|
if s == nil {
|
|
return "-"
|
|
}
|
|
return s.Id
|
|
}
|
|
|
|
// noteSkipped 记录「上次报告过的未接入 MT 服务」,内容没变就不再重复打日志。
|
|
// 返回 true 表示这次值得打一条。
|
|
func (this *modelTranslateComp) noteSkipped(skipped []string) bool {
|
|
key := strings.Join(skipped, ",")
|
|
this.mu.Lock()
|
|
defer this.mu.Unlock()
|
|
if this.lastSkipped == key {
|
|
return false
|
|
}
|
|
this.lastSkipped = key
|
|
return true
|
|
}
|
|
|