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.
250 lines
8.4 KiB
250 lines
8.4 KiB
import 'dart:convert';
|
|
import 'dart:math';
|
|
import 'package:get_storage/get_storage.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:crypto/crypto.dart';
|
|
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
|
import 'package:get/get.dart';
|
|
import '../../core/utils/logger.dart';
|
|
import 'language_manager.dart';
|
|
|
|
class VolcanoTranslationService extends GetxService {
|
|
// API 配置
|
|
late String _baseUrl;
|
|
final String _endpoint = '/'; // 修改为根路径
|
|
late final String _accessKey;
|
|
late final String _secretKey;
|
|
late final String _region;
|
|
late final String _service;
|
|
|
|
// 初始化标志
|
|
bool _isInitialized = false;
|
|
|
|
// 语言管理器
|
|
final LanguageManager _languageManager = Get.find<LanguageManager>();
|
|
final GetStorage _storage = GetStorage();
|
|
// 构造函数
|
|
VolcanoTranslationService();
|
|
|
|
// 初始化服务
|
|
Future<bool> initialize({
|
|
String? accessKey,
|
|
String? secretKey,
|
|
String? region,
|
|
}) async {
|
|
try {
|
|
final _env = _storage.read("ENV") as Map<String, String>;
|
|
_accessKey = accessKey ?? _env['VOLCANO_TRANSLATION_ACCESS_KEY'] ?? '';
|
|
_secretKey = secretKey ?? _env['VOLCANO_TRANSLATION_SECRET_KEY'] ?? '';
|
|
_region = region ?? _env['VOLCANO_TRANSLATION_REGION'] ?? 'cn-north-1';
|
|
_service = 'translate';
|
|
_baseUrl = 'https://translate.volcengineapi.com';
|
|
|
|
if (_accessKey.isEmpty || _secretKey.isEmpty) {
|
|
return false;
|
|
}
|
|
|
|
_isInitialized = true;
|
|
return true;
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// 生成签名
|
|
Map<String, String> _generateSignature(String method,
|
|
Map<String, dynamic> requestBody, Map<String, String> queryParams) {
|
|
// 1. 准备时间相关参数
|
|
final now = DateTime.now().toUtc();
|
|
final date = now.toIso8601String().split('T')[0].replaceAll('-', '');
|
|
final timestamp = now
|
|
.toIso8601String()
|
|
.replaceAll(':', '')
|
|
.replaceAll('-', '')
|
|
.split('.')[0] +
|
|
'Z';
|
|
|
|
// 2. 构建规范查询字符串 - 按照参数名ASCII排序
|
|
final sortedParams = Map.fromEntries(
|
|
queryParams.entries.toList()..sort((a, b) => a.key.compareTo(b.key)));
|
|
final List<String> queryParts = [];
|
|
sortedParams.forEach((key, value) {
|
|
// 使用RFC 3986编码,确保特殊字符被正确处理
|
|
final encodedKey = Uri.encodeComponent(key).replaceAll('+', '%20');
|
|
final encodedValue = Uri.encodeComponent(value).replaceAll('+', '%20');
|
|
queryParts.add('$encodedKey=$encodedValue');
|
|
});
|
|
final canonicalQueryString = queryParts.join('&');
|
|
|
|
// 3. 创建规范请求
|
|
final contentType = 'application/json';
|
|
final payloadHash =
|
|
sha256.convert(utf8.encode(json.encode(requestBody))).toString();
|
|
|
|
final host = 'translate.volcengineapi.com';
|
|
|
|
// 确保头部按照字典序排序,且键名小写
|
|
// 注意:只包含host和x-date头部,与示例保持一致
|
|
final canonicalHeaders = 'host:$host\nx-date:$timestamp\n';
|
|
final signedHeaders = 'host;x-date';
|
|
|
|
// 构建规范请求,格式必须与示例完全一致
|
|
final canonicalRequest =
|
|
'$method\n$_endpoint\n$canonicalQueryString\n$canonicalHeaders\n$signedHeaders\n$payloadHash';
|
|
|
|
// 4. 创建待签字符串
|
|
final algorithm = 'HMAC-SHA256';
|
|
final credentialScope = '$date/$_region/$_service/request';
|
|
final stringToSign =
|
|
'$algorithm\n$timestamp\n$credentialScope\n${sha256.convert(utf8.encode(canonicalRequest)).toString()}';
|
|
|
|
// 5. 计算签名 - 按照文档正确派生密钥
|
|
// 注意:这里是关键修复点,确保密钥派生过程完全符合文档
|
|
final kSecret = utf8.encode(_secretKey); // 直接使用原始密钥,不添加前缀
|
|
|
|
// kDate = HMAC(kSecret, Date)
|
|
var kDate = Hmac(sha256, kSecret).convert(utf8.encode(date)).bytes;
|
|
|
|
// kRegion = HMAC(kDate, Region)
|
|
var kRegion = Hmac(sha256, kDate).convert(utf8.encode(_region)).bytes;
|
|
|
|
// kService = HMAC(kRegion, Service)
|
|
var kService = Hmac(sha256, kRegion).convert(utf8.encode(_service)).bytes;
|
|
|
|
// kSigning = HMAC(kService, "request")
|
|
var kSigning = Hmac(sha256, kService).convert(utf8.encode('request')).bytes;
|
|
|
|
// 使用派生的签名密钥计算最终签名
|
|
final signature =
|
|
Hmac(sha256, kSigning).convert(utf8.encode(stringToSign)).toString();
|
|
|
|
// 6. 添加签名到请求头 - 格式必须与示例完全一致
|
|
final authorization =
|
|
'$algorithm Credential=$_accessKey/$credentialScope, SignedHeaders=$signedHeaders, Signature=$signature';
|
|
|
|
// 只返回必要的头部,与示例保持一致
|
|
return {
|
|
'Content-Type': contentType,
|
|
'X-Date': timestamp,
|
|
'Authorization': authorization,
|
|
'Host': host,
|
|
};
|
|
}
|
|
|
|
// 翻译文本
|
|
Future<String?> translateText({
|
|
required String text,
|
|
required String sourceLanguageCode,
|
|
required String targetLanguageCode,
|
|
}) async {
|
|
if (!_isInitialized) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
// 获取源语言和目标语言的短代码
|
|
String sourceCode = '';
|
|
String targetCode = 'zh';
|
|
|
|
// 从 ASR 代码获取短代码
|
|
if (sourceLanguageCode.isNotEmpty) {
|
|
final sourceShortCode =
|
|
_languageManager.getShortCodeByAsrCode(sourceLanguageCode);
|
|
if (sourceShortCode != null) {
|
|
sourceCode = sourceShortCode;
|
|
}
|
|
}
|
|
|
|
final targetShortCode =
|
|
_languageManager.getShortCodeByAsrCode(targetLanguageCode);
|
|
if (targetShortCode != null) {
|
|
targetCode = targetShortCode;
|
|
}
|
|
|
|
// 构建请求体
|
|
final requestBody = {
|
|
'SourceLanguage': sourceCode,
|
|
'TargetLanguage': targetCode,
|
|
'TextList': [text],
|
|
};
|
|
|
|
// 添加必要的查询参数 - 确保包含所有必需参数
|
|
final queryParams = {
|
|
'Action': 'TranslateText',
|
|
'Version': '2020-06-01',
|
|
'Region': _region,
|
|
'Service': _service,
|
|
};
|
|
|
|
// 生成包含查询参数的签名
|
|
final headers = _generateSignature('POST', requestBody, queryParams);
|
|
|
|
// 构建带有查询参数的URL
|
|
final queryString = Uri(queryParameters: queryParams).query;
|
|
final url = '$_baseUrl/?$queryString';
|
|
|
|
final response = await http.post(
|
|
Uri.parse(url),
|
|
headers: headers,
|
|
body: json.encode(requestBody),
|
|
);
|
|
|
|
if (response.statusCode == 200) {
|
|
final responseData = json.decode(response.body);
|
|
|
|
// 尝试从不同的响应结构中提取翻译结果
|
|
String? translation;
|
|
|
|
// 1. 标准响应结构
|
|
if (responseData['TranslationList'] != null &&
|
|
responseData['TranslationList'] is List &&
|
|
responseData['TranslationList'].isNotEmpty) {
|
|
final translationItem = responseData['TranslationList'][0];
|
|
if (translationItem is Map &&
|
|
translationItem.containsKey('Translation')) {
|
|
translation = translationItem['Translation'];
|
|
}
|
|
}
|
|
|
|
// 2. 检查是否有错误响应
|
|
if (translation == null &&
|
|
responseData['ResponseMetadata'] != null &&
|
|
responseData['ResponseMetadata'] is Map &&
|
|
responseData['ResponseMetadata']['Error'] != null) {
|
|
return null;
|
|
}
|
|
|
|
// 3. 尝试其他可能的响应结构
|
|
if (translation == null) {
|
|
if (responseData.containsKey('Translation')) {
|
|
translation = responseData['Translation'];
|
|
} else if (responseData.containsKey('Result') &&
|
|
responseData['Result'] is Map &&
|
|
responseData['Result'].containsKey('Translation')) {
|
|
translation = responseData['Result']['Translation'];
|
|
} else if (responseData.containsKey('Data') &&
|
|
responseData['Data'] is Map) {
|
|
final data = responseData['Data'];
|
|
if (data.containsKey('Translation')) {
|
|
translation = data['Translation'];
|
|
} else if (data.containsKey('TranslationList') &&
|
|
data['TranslationList'] is List &&
|
|
data['TranslationList'].isNotEmpty) {
|
|
final item = data['TranslationList'][0];
|
|
if (item is Map && item.containsKey('Translation')) {
|
|
translation = item['Translation'];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 返回提取到的翻译结果
|
|
return translation;
|
|
}
|
|
|
|
return null;
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|