import 'package:dio/dio.dart'; import 'package:get/get.dart'; import '../../core/utils/logger.dart'; import 'network/api.dart'; /// 文本机器翻译:由**服务端**代调后台「第三方服务配置」里启用的 MT 服务。 /// /// 为什么不像以前那样客户端直连服务商: /// 旧实现直连火山,AK/SK 通过 AppConfig.env **明文下发给每一个客户端**, /// 抓包或反编译就能拿到云账号凭据。改成服务端代调后凭据不再出网关, /// 换服务商也只需在后台改配置、不用发版。(火山翻译已于 2026-08-28 整体下线。) /// /// 代价是每次翻译多一跳(App → 我们的服务端 → 服务商)。同传是流式高频调用, /// 所以服务端对翻译客户端做了 60s 缓存,避免每句都查库重建 SDK。 /// class ServerTranslationService extends GetxService { static ServerTranslationService get to => Get.find(); static const _tag = 'ServerTrans'; /// 与旧实现保持同样的接口形态。服务端无需客户端预初始化,恒为 true。 bool get isInitialized => true; /// 保留这个方法只为兼容调用方(translation_controller 会先调 initialize)。 Future initialize() async => true; /// 最近一次实际使用的服务商 / 服务配置 id,排查"到底用的哪家"时看它。 final lastProvider = ''.obs; final lastSvcId = ''.obs; /// 最近一次「自动检测」识别出的源语种短码(zh / en …)。 /// 只有 sourceLanguageCode 传空串时服务端才会回填。 final lastDetectedFrom = ''.obs; /// 翻译一段文本。失败返回 null(调用方按原样保留原文)。 /// /// 语言码传 BCP-47(zh-CN / en-US)即可,服务端会归一成服务商要的短码。 /// [sourceLanguageCode] 传空串 = 自动检测,检测结果读 [lastDetectedFrom]。 /// [cancelToken] 给同传/面对面的**中间结果**预览翻译用:说话过程中会连发好几次, /// 后一次发出时把前一次撤掉。被取消返回 null,且不打 warning——那是预期行为, /// 不是失败。 /// 实时链路(同传 / 面对面 / 通话)的翻译超时。 /// /// ⚠️ 不能用全局那 30s:那是给上传、会议总结这类慢请求留的。同传是流式的, /// 一句话等 30s 早就没有意义,而且会把后面的句子一起堵在同一条链路上—— /// 弱网下的「卡住不动」就是这么来的。 /// /// 取 12s 是有依据的:服务端 user_translate 内部的上下文只给服务商 10s /// (api_translate.go 的 context.WithTimeout),10s 拿不到结果它自己就返回失败了, /// 客户端再多等就是纯白等。留 2s 给网络往返。 static const Duration realtimeTimeout = Duration(seconds: 12); Future translateText({ required String text, required String sourceLanguageCode, required String targetLanguageCode, CancelToken? cancelToken, Duration? timeout = realtimeTimeout, }) async { final src = text.trim(); if (src.isEmpty) return null; try { final data = await Api.userTranslate({ 'text': src, 'from': sourceLanguageCode, 'to': targetLanguageCode, }, cancelToken: cancelToken, timeout: timeout); if (data is! Map) return null; final out = (data['text'] as String?)?.trim() ?? ''; lastProvider.value = (data['provider'] as String?) ?? ''; lastSvcId.value = (data['svcid'] as String?) ?? ''; lastDetectedFrom.value = (data['detected_from'] as String?) ?? ''; if (out.isEmpty) { Logger.w(_tag, '翻译返回空: $sourceLanguageCode→$targetLanguageCode'); return null; } return out; } catch (e) { // 主动取消的中间结果请求:预期行为,静默返回。 if (e is DioException && e.type == DioExceptionType.cancel) return null; // 服务端错误码见 errorcode.proto 5101-5103: // 5101 未配置 MT 服务 / 5102 参数不合法 / 5103 服务商调用失败 if (e is DioException && (e.type == DioExceptionType.receiveTimeout || e.type == DioExceptionType.sendTimeout || e.type == DioExceptionType.connectionTimeout)) { // 弱网:这一句放弃,不阻塞后面的句子。同传宁可丢一句也不能整条卡住。 Logger.w(_tag, '翻译超时($sourceLanguageCode→$targetLanguageCode),跳过这一句'); return null; } Logger.w(_tag, '翻译失败($sourceLanguageCode→$targetLanguageCode): $e'); return null; } } }