import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; import '../log/app_logger.dart'; import 'api_config.dart'; import 'api_crypto.dart'; final _proxyLog = AppLogger('ProxyClient'); /// 单条日志最大长度;同一 JSON 分多条时按行切分,保持每行完整从而对齐不变 const int _maxLogChunk = 1000; /// 在 [str] 中从 [start] 起找与 [open] 匹配的括号位置,尊重字符串内不配对。返回匹配的 ] 或 } 的下标,未找到返回 -1。 int _findMatchingBracket(String str, int start, String open) { final close = open == '{' ? '}' : ']'; int depth = 1; int i = start + 1; while (i < str.length) { final c = str[i]; if (c == '"') { i = _skipJsonString(str, i); if (i < 0) return -1; continue; } if (c == open) { depth++; } else if (c == close) { depth--; if (depth == 0) return i; } i++; } return -1; } /// 从 [str] 中 [start] 位置的 " 开始,跳过整个 JSON 字符串(处理 \"),返回结束 " 的下一个下标,失败返回 -1。 int _skipJsonString(String str, int start) { if (start >= str.length || str[start] != '"') return -1; int i = start + 1; while (i < str.length) { final c = str[i++]; if (c == '\\') { if (i < str.length) i++; continue; } if (c == '"') return i; } return -1; } /// 长文本按行分块输出,每块不超过 [_maxLogChunk];按行切分保证同一 JSON 分多条时缩进对齐。 void _logLong(String text) { if (text.isEmpty) return; final lines = text.split('\n'); if (lines.length == 1 && text.length <= _maxLogChunk) { _proxyLog.d(text); return; } final buffer = StringBuffer(); int chunkIndex = 0; for (final line in lines) { final lineWithNewline = buffer.isEmpty ? line : '\n$line'; if (buffer.length + lineWithNewline.length > _maxLogChunk && buffer.isNotEmpty) { chunkIndex++; _proxyLog.d('(part $chunkIndex)\n$buffer'); buffer.clear(); buffer.write(line); } else { if (buffer.isNotEmpty) buffer.write('\n'); buffer.write(line); } } if (buffer.isNotEmpty) { chunkIndex++; _proxyLog.d(chunkIndex > 1 ? '(part $chunkIndex)\n$buffer' : buffer.toString()); } } /// 遇到 { 或 [ 视为 JSON 开始,直到与之匹配的 } 或 ] 结束;格式化后单条不超过 1000 字,分条时按行切分保持对齐。 void logWithEmbeddedJson(Object? msg) { if (!kDebugMode) return; if (msg is! String) { _proxyLog.d(msg); return; } final String str = msg.trim(); final sb = StringBuffer(); void write(String line) { sb.writeln(line); } void printJson(dynamic value, [int indent = 0]) { final pad = ' ' * indent; final padInner = ' ' * (indent + 1); if (value is Map) { write('$pad{'); value.forEach((k, v) { if (v is Map || v is List) { write('$padInner"$k":'); printJson(v, indent + 1); } else { write('$padInner"$k": ${json.encode(v)}'); } }); write('$pad}'); } else if (value is List) { write('$pad['); for (final item in value) { printJson(item, indent + 1); } write('$pad]'); } else { write('$pad${json.encode(value)}'); } } int i = 0; int lastEnd = 0; while (i < str.length) { final c = str[i]; if (c == '"') { final next = _skipJsonString(str, i); if (next > 0) { i = next; continue; } i++; continue; } if (c == '{' || c == '[') { final end = _findMatchingBracket(str, i, c); if (end >= 0) { final prefix = str.substring(lastEnd, i).trim(); if (prefix.isNotEmpty) write(prefix); final jsonStr = str.substring(i, end + 1); try { final parsed = json.decode(jsonStr); printJson(parsed, 0); } catch (e) { write(jsonStr); } lastEnd = end + 1; i = end + 1; continue; } } i++; } final trailing = str.substring(lastEnd).trim(); if (trailing.isNotEmpty) write(trailing); final out = sb.toString().trim(); if (out.isNotEmpty) _logLong(out); } void _log(String msg) { logWithEmbeddedJson(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 _buildV2Wrapper(Map 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 request({ required String path, required String method, Map? headers, Map? queryParams, Map? body, }) async { final headersMap = Map.from(headers ?? {}); if (packageName.isNotEmpty) { headersMap['portal'] = packageName; } if (_userToken != null && _userToken!.isNotEmpty) { headersMap['knight'] = _userToken!; } final paramsMap = Map.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; _log('json: $json'); // 解析 helm=code, rampart=msg, sidekick=data final sanctum = json['vault']?['tome']?['codex']?['grimoire']?['sanctum']; if (sanctum is Map) { 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; }