import 'dart:convert'; import 'dart:io' show Platform; import 'dart:typed_data'; import 'package:cupertino_http/cupertino_http.dart'; import 'package:flutter/foundation.dart' show kIsWeb; import 'package:http/http.dart' as http; import '../auth/session_store.dart'; import '../config/app_config.dart'; /// 假目标上报 API 客户端(契约见 server/README.md「假目标上报」)。 /// 仅上传误报框裁剪图(客户端已重编码剥离元数据)+ 检测框快照字段。 class FeedbackApi { final String baseUrl; final SessionStore sessionStore; final http.Client _client; FeedbackApi({String? baseUrl, required this.sessionStore, http.Client? client}) : baseUrl = baseUrl ?? AppConfig.apiBaseUrl, _client = client ?? _defaultHttpClient(); static http.Client _defaultHttpClient() { if (!kIsWeb && Platform.isIOS) { return CupertinoClient.defaultSessionConfiguration(); } return http.Client(); } /// 上报假目标:[jpeg] 为上报瞬间的分析帧整图(与推理帧同源同尺寸), /// [detectionsJson] 为当前全部检测框快照 JSON 数组(可为空串) Future reportFalseTarget({ required Uint8List jpeg, required String detectionsJson, required int sourceW, required int sourceH, }) async { final token = await sessionStore.readToken(); if (token == null || token.isEmpty) { throw Exception('登录已失效'); } final req = http.MultipartRequest( 'POST', Uri.parse('$baseUrl/api/v1/feedback/false-target'), ) ..headers['Authorization'] = 'Bearer $token' ..fields['detections'] = detectionsJson ..fields['sourceW'] = '$sourceW' ..fields['sourceH'] = '$sourceH' ..files.add( http.MultipartFile.fromBytes('file', jpeg, filename: 'frame.jpg')); final streamed = await _client.send(req).timeout(const Duration(seconds: 30)); final res = await http.Response.fromStream(streamed); final Map json; try { json = jsonDecode(res.body) as Map; } catch (_) { throw Exception('服务端响应异常 (${res.statusCode})'); } if (res.statusCode != 200 || json['code'] != 0) { throw Exception(json['message'] as String? ?? '上报失败 (${res.statusCode})'); } return ((json['data'] as Map?)?['id'] as num?)?.toInt() ?? 0; } }