// ignore_for_file: depend_on_referenced_packages, avoid_web_libraries_in_flutter, avoid_print import 'dart:async'; import 'package:didvan/models/ai/files_model.dart'; import 'package:didvan/services/storage/storage.dart'; import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; import 'package:http_parser/http_parser.dart'; import 'package:mime/mime.dart'; class AiApiService { // backend جدید (api_gateway) — legacy روی api.didvan.app (Liara) برای // نسخه‌های قدیم اپ زنده می‌ماند. static const String baseUrl = 'https://api2.didvan.com/ai'; static Future initial( {required final String url, required final String message, final int? chatId, final FilesModel? file, final bool? edite, final bool? webSearch}) async { final headers = { "Authorization": "Bearer ${await StorageService.getValue(key: 'token')}", 'Content-Type': 'multipart/form-data' }; var request = http.MultipartRequest('POST', Uri.parse(baseUrl + url)) ..headers.addAll(headers); request.fields['prompt'] = message; if (chatId != null) { request.fields['chatId'] = chatId.toString(); } if (edite != null) { request.fields['edit'] = edite.toString().toLowerCase(); } // وقتی toggle «جستجو در وب» در UI روشن است، این فلگ به backend // فرستاده می‌شود تا در request به kie.ai مدل را با tools:[web_search] // فرا بخواند. if (webSearch == true) { request.fields['webSearch'] = 'true'; } if (file != null) { if (file.duration != null) { request.fields['duration'] = file.duration!.inSeconds.toString(); } Uint8List bytes; String filename; String mimeType; if (kIsWeb) { // For web platform if (file.bytes != null) { bytes = file.bytes!; } else { final Uri audioUri = Uri.parse(file.path.replaceAll('%3A', ':')); final http.Response audioResponse = await http.get(audioUri); bytes = audioResponse.bodyBytes; // final f = File.fromUri(Uri.parse(file.path)); // bytes = await f.readAsBytes(); // Fetch the blob using JavaScript interop // final blob = await html.window // .fetch(file.path.replaceAll('%3A', ':')) // .then((response) => response.blob()); // // Read the blob as an array buffer // final reader = html.FileReader(); // reader.readAsArrayBuffer(blob); // await reader.onLoadEnd.first; // bytes = reader.result as Uint8List; } } else { bytes = await file.main.readAsBytes(); } if (file.isRecorded) { filename = '${DateTime.now().millisecondsSinceEpoch ~/ 1000}.mp4'; mimeType = lookupMimeType(filename, headerBytes: bytes) ?? 'audio/mp4'; } else { filename = file.name!; mimeType = lookupMimeType(filename, headerBytes: bytes) ?? 'application/octet-stream'; } request.files.add(http.MultipartFile.fromBytes( 'file', bytes, filename: filename, contentType: MediaType.parse(mimeType), )); } // print("req: ${request.files}"); // print("req: ${request.fields}"); return request; } Future>> getResponse(http.MultipartRequest req) async { // تا 2 بار retry روی خطاهای transient شبکه (HandshakeException، // SocketException، connection closed). request باید برای هر تلاش // از نو کلون شود چون MultipartRequest قابل reuse نیست. Exception? lastErr; for (int attempt = 1; attempt <= 3; attempt++) { // برای attempt های بعدی، یک کپی جدید از request می‌سازیم. final reqToSend = attempt == 1 ? req : _cloneRequest(req); try { final response = await http.Client().send(reqToSend); if (response.statusCode != 200) { final errorBody = await response.stream.bytesToString(); print('body: ${reqToSend.url} sc: ${response.statusCode}'); print('res: $errorBody'); throw Exception('sc is ${response.statusCode}'); } else { return response.stream.asBroadcastStream(); } } catch (e) { lastErr = Exception('$e'); final msg = e.toString().toLowerCase(); final isTransient = msg.contains('handshake') || msg.contains('socketexception') || msg.contains('connection terminated') || msg.contains('connection closed') || msg.contains('connection reset'); print( 'req ${reqToSend.url} attempt $attempt/3 failed (transient=$isTransient): $e'); if (!isTransient || attempt == 3) break; // backoff قبل از retry await Future.delayed(Duration(milliseconds: 500 * attempt)); } } throw lastErr ?? Exception('Failed to load data due to an exception.'); } /// MultipartRequest قابل reuse مستقیم نیست (stream یک‌بار خوانده می‌شود). /// برای retry یک کپی تازه با همان fields/files می‌سازیم. static http.MultipartRequest _cloneRequest(http.MultipartRequest original) { final copy = http.MultipartRequest(original.method, original.url); copy.headers.addAll(original.headers); copy.fields.addAll(original.fields); for (final f in original.files) { // finalize نشده باشد، چون file یک‌بار قابل خواندن است. ما // bytes را قبلاً در fromBytes ذخیره کرده‌ایم پس از همان buffer // استفاده می‌کنیم. copy.files.add(f); } return copy; } }