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.

233 lines
8.3 KiB

import 'dart:convert';
import 'dart:math';
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>();
// 构造函数
VolcanoTranslationService();
// 初始化服务
Future<bool> initialize({
String? accessKey,
String? secretKey,
String? region,
}) async {
try {
_accessKey = accessKey ?? dotenv.env['VOLCANO_TRANSLATION_ACCESS_KEY'] ?? '';
_secretKey = secretKey ?? dotenv.env['VOLCANO_TRANSLATION_SECRET_KEY'] ?? '';
_region = region ?? dotenv.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;
}
}
}