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.
 
 
 
 
 
 

97 lines
4.6 KiB

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<ServerTranslationService>();
static const _tag = 'ServerTrans';
/// 与旧实现保持同样的接口形态。服务端无需客户端预初始化,恒为 true。
bool get isInitialized => true;
/// 保留这个方法只为兼容调用方(translation_controller 会先调 initialize)。
Future<bool> 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<String?> 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;
}
}
}