didvan-app/lib/services/ai/ai_api_service.dart

157 lines
5.8 KiB
Dart

// 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<http.MultipartRequest> 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<Stream<List<int>>> 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;
}
}