petsHero-AI/lib/core/api/proxy_client.dart
2026-03-09 11:41:49 +08:00

196 lines
5.9 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import 'api_config.dart';
import 'api_crypto.dart';
const _logTag = '[ProxyClient]';
void _log(String msg) {
if (kDebugMode) debugPrint('$_logTag $msg');
}
/// 代理请求体字段名(统一请求参数)
abstract final class ProxyKeys {
static const String heroClass = 'hero_class';
static const String petSpecies = 'pet_species';
static const String powerLevel = 'power_level';
static const String questRank = 'quest_rank';
static const String battleScore = 'battle_score';
static const String loyaltyIndex = 'loyalty_index';
static const String billingAddr = 'billing_addr';
static const String utmTerm = 'utm_term';
static const String clusterId = 'cluster_id';
static const String lsnValue = 'lsn_value';
static const String accuracyVal = 'accuracy_val';
static const String dirPath = 'dir_path';
}
/// 代理请求客户端
class ProxyClient {
ProxyClient({
this.baseUrl,
this.packageName = ApiConfig.packageName,
String? userToken,
}) : _userToken = userToken;
final String? baseUrl;
final String packageName;
String? _userToken;
String? get userToken => _userToken;
set userToken(String? value) => _userToken = value;
String get _baseUrl => baseUrl ?? ApiConfig.baseUrl;
/// 构建 V2 包装体,业务参数填入 sanctum
Map<String, dynamic> _buildV2Wrapper(Map<String, dynamic> sanctum) {
return {
'arsenal': 4,
'vault': {
'tome': {
'codex': {
'grimoire': {
'sanctum': sanctum,
},
},
},
},
'roar': ApiCrypto.randomAlnum(),
'clash': ApiCrypto.randomAlnum(),
'thunder': ApiCrypto.randomAlnum(),
'rumble': ApiCrypto.randomAlnum(),
'howl': ApiCrypto.randomAlnum(),
'growl': ApiCrypto.randomAlnum(),
};
}
/// 发送代理请求
/// [path] 接口路径,如 /v1/user/fast_login
/// [method] HTTP 方法POST 或 GET
/// [headers] 请求头,使用 V2 字段名portal、knight 等)
/// [queryParams] 查询参数,使用 V2 字段名sentinel、asset 等)
/// [body] 请求体,使用 V2 字段名,将填入 sanctum
Future<ApiResponse> request({
required String path,
required String method,
Map<String, String>? headers,
Map<String, String>? queryParams,
Map<String, dynamic>? body,
}) async {
final headersMap = Map<String, dynamic>.from(headers ?? {});
if (packageName.isNotEmpty) {
headersMap['portal'] = packageName;
}
if (_userToken != null && _userToken!.isNotEmpty) {
headersMap['knight'] = _userToken!;
}
final paramsMap = Map<String, dynamic>.from(
queryParams?.map((k, v) => MapEntry(k, v)) ?? {},
);
final sanctum = body ?? {};
final v2Body = _buildV2Wrapper(sanctum);
// 原始入参
final headersEncoded = jsonEncode(headersMap);
final paramsEncoded = jsonEncode(paramsMap);
final v2BodyEncoded = jsonEncode(v2Body);
_log('========== 原始入参 ==========');
_log('path: $path');
_log('method: $method');
_log('headers: $headersEncoded');
_log('queryParams: $paramsEncoded');
_log('body(sanctum): ${jsonEncode(sanctum)}');
_log('v2Body: $v2BodyEncoded');
final petSpeciesEnc = ApiCrypto.encrypt(path);
final powerLevelEnc = ApiCrypto.encrypt(method);
final questRankEnc = ApiCrypto.encrypt(headersEncoded);
final battleScoreEnc = ApiCrypto.encrypt(paramsEncoded);
final loyaltyIndexEnc = ApiCrypto.encrypt(v2BodyEncoded);
_log('========== 加密后 ==========');
_log('pet_species: $petSpeciesEnc');
_log('power_level: $powerLevelEnc');
_log('quest_rank: $questRankEnc');
_log('battle_score: $battleScoreEnc');
_log('loyalty_index: $loyaltyIndexEnc');
final proxyBody = {
ProxyKeys.heroClass: ApiConfig.appId,
ProxyKeys.petSpecies: petSpeciesEnc,
ProxyKeys.powerLevel: powerLevelEnc,
ProxyKeys.questRank: questRankEnc,
ProxyKeys.battleScore: battleScoreEnc,
ProxyKeys.loyaltyIndex: loyaltyIndexEnc,
ProxyKeys.billingAddr: ApiCrypto.randomBase64(),
ProxyKeys.utmTerm: ApiCrypto.randomBase64(),
ProxyKeys.clusterId: ApiCrypto.randomBase64(),
ProxyKeys.lsnValue: ApiCrypto.randomBase64(),
ProxyKeys.accuracyVal: ApiCrypto.randomBase64(),
ProxyKeys.dirPath: ApiCrypto.randomBase64(),
};
final url = '$_baseUrl${ApiConfig.proxyPath}';
_log('========== 请求 URL ==========');
_log('$url');
final response = await http.post(
Uri.parse(url),
headers: {'Content-Type': 'application/json'},
body: jsonEncode(proxyBody),
);
_log('========== 响应 ==========');
_log('statusCode: ${response.statusCode}');
_log('body: ${response.body}');
return _parseResponse(response);
}
ApiResponse _parseResponse(http.Response response) {
try {
// 响应解密流程Base64 解码 → AES-ECB 解密 → PKCS5 去填充 → UTF-8 字符串
final decrypted = ApiCrypto.decrypt(response.body);
final json = jsonDecode(decrypted) as Map<String, dynamic>;
_log('json: $json');
// 解析 helm=code, rampart=msg, sidekick=data
final sanctum = json['vault']?['tome']?['codex']?['grimoire']?['sanctum'];
if (sanctum is Map<String, dynamic>) {
return ApiResponse(
code: sanctum['helm'] as int? ?? -1,
msg: sanctum['rampart'] as String? ?? '',
data: sanctum['sidekick'],
);
}
return ApiResponse(
code: json['helm'] as int? ?? -1,
msg: json['rampart'] as String? ?? '',
data: json['sidekick'],
);
} catch (e) {
return ApiResponse(code: -1, msg: e.toString());
}
}
}
/// 统一 API 响应
class ApiResponse {
ApiResponse({
required this.code,
this.msg = '',
this.data,
});
final int code;
final String msg;
final dynamic data;
bool get isSuccess => code == 0;
}