import 'dart:convert'; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; import '../../../core/utils/logger.dart'; import '../db/sqflite_api.dart'; import '../network/api.dart'; import '../../models/user_Info.dart'; import '../../../modules/meeting/model/transcribe_service.dart'; import 'meeting_upload_service.dart'; class MeetingTaskService extends GetxService { MeetingUploadService get _uploadService => Get.find(); final GetStorage _storage = GetStorage(); static const String _pendingLocalRecordingTitleStorageKey = 'meeting_pending_local_recording_titles'; List _taskList = []; //待处理的会议任务列表 bool _isTask = false; //是否有正在处理的任务 RxInt updateListState = 0.obs; //是否更新首页列表 RxInt localImportState = 0.obs; //是否触发本地录音导入 int meetingId = 0; RxInt tasktype = 1.obs; //任务类型,0-初始,1-等待转写中,2-转写中,3-等待总结中,4-总结中,5-完成 List textList = []; String personnel = ''; String summary = ''; String overview = ''; void setPendingLocalRecordingTitle(String wavFileName, String displayTitle) { if (wavFileName.isEmpty) return; final data = _storage.read(_pendingLocalRecordingTitleStorageKey); final map = (data is Map) ? Map.from(data) : {}; map[wavFileName] = displayTitle; _storage.write(_pendingLocalRecordingTitleStorageKey, map); } String? takePendingLocalRecordingTitle(String wavFileName) { if (wavFileName.isEmpty) return null; final data = _storage.read(_pendingLocalRecordingTitleStorageKey); if (data is! Map) return null; final map = Map.from(data); final v = map.remove(wavFileName); if (v == null) return null; _storage.write(_pendingLocalRecordingTitleStorageKey, map); return v.toString(); } void requestLocalImport() { localImportState.value++; } Future addTask( int id, String audiourl, String formlanguage, String tolanguage, bool isdistinguishspeaker, int templateid, String tid, TranscribeService serviceType, ) async { if (audiourl.isEmpty) { Map uploadData = _uploadService.uploadList.firstWhere( (element) => element['id'] == id, orElse: () => {}, ); if (uploadData.isNotEmpty) { if (uploadData['audiourl'].isEmpty) { uploadData['isTask'] = true; uploadData['formlanguage'] = formlanguage; uploadData['tolanguage'] = tolanguage; uploadData['isdistinguishspeaker'] = isdistinguishspeaker; uploadData['templateid'] = templateid; uploadData['tid'] = tid; uploadData['service_type'] = serviceType.value; } else { await submitTask( id, formlanguage, tolanguage, isdistinguishspeaker, templateid, tid, serviceType, ); } } } else { await submitTask( id, formlanguage, tolanguage, isdistinguishspeaker, templateid, tid, serviceType, ); } } Future submitTask( int id, String formlanguage, String tolanguage, bool isdistinguishspeaker, int templateid, String tid, TranscribeService serviceType, ) async { await Api.startTaskEchomeet({ 'id': id, 'formlanguage': formlanguage, 'tolanguage': tolanguage, 'isdistinguishspeaker': isdistinguishspeaker, 'templateid': templateid, 'tid': tid, 'service_type': serviceType.value, }); final seconds = await _getMeetingSeconds(id); _decrementMeetingIntegral(cost: seconds); _taskList.add({ 'id': id, 'tasktype': 1, }); await SqfliteApi.editMeetingTitle( id, {'tasktype': 1}, ); updateListState.value++; _executeTask(); } Future refreshTask( int id, int templateid, String tid, String tolanguage, TranscribeService serviceType, ) async { tasktype.value = 3; String oldSummary = ''; try { final data = await SqfliteApi.getMeetingDetails(id); oldSummary = (data['details']?['summary'] ?? '').toString(); } catch (_) {} await Api.refreshTaskEchomeet({ 'id': id, 'templateid': templateid, 'tid': tid, 'tolanguage': tolanguage, 'service_type': serviceType.value, }); _taskList.removeWhere((i) => i['id'] == id); _taskList.add({ 'id': id, 'tasktype': 3, 'oldSummary': oldSummary, 'pollAttempts': 0, }); await SqfliteApi.editMeetingTitle( id, {'tasktype': 3}, ); updateListState.value++; _executeTask(); } Future readTask(int id) async { await Api.readRecordEchomeet({'id': id}); await SqfliteApi.editMeetingTitle( id, {'tasktype': 6}, ); updateListState.value++; } void _executeTask() async { if (_isTask) return; _isTask = true; try { while (_taskList.isNotEmpty) { await Future.delayed(const Duration(seconds: 5)); try { await _queryTask(); } catch (e, st) { Logger.error('MeetingTask polling _queryTask failed: $e\n$st'); } } } finally { _isTask = false; } } Future removeTask(int id) async { _taskList.removeWhere((element) => element['id'] == id); await Api.delRecordEchomeet({ 'ids': [id], }); } void startTask(List data) { _taskList = data; _executeTask(); } void stopTask() { _taskList.clear(); } Future _queryTask() async { if (_taskList.isEmpty) return; List ids = _taskList.map((e) => e['id']).toList(); var response = await Api.getMultitermRecord({ 'ids': ids, }); List queryList = response['records'] ?? []; Logger.d( 'MeetingTask', 'poll: ids=$ids, server returned ${queryList.length} records, ' 'states=${queryList.map((e) => '${e['id']}:${e['state']}').toList()}', ); for (var element in queryList) { int id = element['id']; int newTasktype = element['state']; Map taskData = _taskList.firstWhere( (i) => i['id'] == id, orElse: () => {}, ); // 重新总结的兜底:服务端有时会立刻回 state>=5 但 summary 还是旧的, // 等真正的新内容生成后才更新。这里用提交重总结时记录的 oldSummary // 做比对,只要服务端返回的 summary 还跟旧的一样,就当作未完成继续轮询, // 避免任务被过早移出 _taskList、polling 停掉之后再也拿不到新内容。 const int kMaxStaleAttempts = 60; // ~5 分钟(按 5s 间隔) if (taskData.isNotEmpty && newTasktype >= 5) { final String oldSummary = (taskData['oldSummary'] ?? '').toString(); final String returnedSummary = (element['summary'] ?? '').toString(); int attempts = (taskData['pollAttempts'] ?? 0) as int; if (oldSummary.isNotEmpty && returnedSummary == oldSummary && attempts < kMaxStaleAttempts) { taskData['pollAttempts'] = attempts + 1; Logger.d( 'MeetingTask', 're-summary still pending for id=$id ' '(server reports state=$newTasktype but summary unchanged, ' 'attempt=${attempts + 1})', ); continue; } } if (taskData.isNotEmpty) { int oldTasktype = taskData['tasktype']; if (oldTasktype != newTasktype) { if (oldTasktype < 3 && newTasktype >= 3) { // 转写完成 List decodeList = jsonDecode(element['translate']); List> speakerList = decodeList.map((e) => Map.from(e)).toList(); await SqfliteApi.deleteMeetingSpeaker(id); await SqfliteApi.insertMeetingSpeaker(speakerList); await SqfliteApi.editMeeting( id, {'tasktype': newTasktype}, ); if (id == meetingId) { List sqlSpeakerList = await SqfliteApi.getMeetingSpeaker(id); textList = List.from(sqlSpeakerList); tasktype.value = newTasktype; } } if (oldTasktype < 5 && newTasktype >= 5) { // 总结完成 // 重新总结场景下 translate 也会被服务端按新目标语言重写, // 必须把它落进 meetingspeaker 表,否则转写 tab 仍然显示旧语言。 final translateField = element['translate']; bool translateRefreshed = false; Logger.d( 'MeetingTask', 'summary complete for id=$id: ' 'translate ${translateField == null ? 'MISSING' : 'len=${translateField.toString().length}'}, ' 'available element keys=${element.keys.toList()}', ); if (translateField != null && translateField.toString().isNotEmpty) { try { List decodeList = jsonDecode(translateField); List> speakerList = decodeList .map((e) => Map.from(e)) .toList(); await SqfliteApi.deleteMeetingSpeaker(id); await SqfliteApi.insertMeetingSpeaker(speakerList); translateRefreshed = true; } catch (e) { Logger.error( 'MeetingTask: failed to refresh meetingspeaker on ' 'summary complete for id=$id: $e', ); } } await SqfliteApi.editMeeting(id, { 'tasktype': newTasktype, 'personnel': element['personnel'] ?? '', 'summary': element['summary'] ?? '', 'overview': element['overview'] ?? '', }); // 兜底:如果 polling 响应没带 translate(服务端可能在 // /echomeet_getrecords 里不返回完整字段),主动去 // /echomeet_getallrecords 拉一次,把新翻译同步到本地 DB。 if (!translateRefreshed) { Logger.d( 'MeetingTask', 'translate missing from polling response; ' 'triggering full sync to fetch new speakers for id=$id', ); try { var allResp = await Api.getOperationAllRecord(); List allRecords = allResp['records'] ?? []; final fullRecord = allRecords.firstWhere( (r) => r['id'] == id, orElse: () => null, ); if (fullRecord != null && fullRecord['translate'] != null && fullRecord['translate'].toString().isNotEmpty) { List decodeList = jsonDecode(fullRecord['translate']); List> speakerList = decodeList .map((e) => Map.from(e)) .toList(); await SqfliteApi.deleteMeetingSpeaker(id); await SqfliteApi.insertMeetingSpeaker(speakerList); await SqfliteApi.editMeeting(id, { 'personnel': fullRecord['personnel'] ?? '', 'summary': fullRecord['summary'] ?? '', 'overview': fullRecord['overview'] ?? '', }); Logger.d( 'MeetingTask', 'full sync wrote ${speakerList.length} speaker rows ' 'for id=$id', ); } } catch (e) { Logger.error( 'MeetingTask: fallback full sync failed for id=$id: $e', ); } } if (id == meetingId) { personnel = element['personnel'] ?? ''; summary = element['summary'] ?? ''; overview = element['overview'] ?? ''; tasktype.value = newTasktype; } } await SqfliteApi.editMeetingTitle( id, {'tasktype': newTasktype}, ); taskData['tasktype'] = newTasktype; updateListState.value++; } } if (newTasktype >= 5) { _taskList.removeWhere((i) => i['id'] == id); } } } /// 获取会议音频秒数 Future _getMeetingSeconds(int id) async { try { final data = await SqfliteApi.getMeetingDetails(id); final v = data['seconds']; if (v is int) return v; if (v is num) return v.toInt(); if (v is String) return int.tryParse(v) ?? 0; return 0; } catch (_) { return 0; } } /// 减少用户会议积分 void _decrementMeetingIntegral({required int cost}) { if (!User.isLoggedIn()) return; if (cost <= 0) return; final next = User.instance.meetintegral - cost; User.instance.updateUserInfo({'meetintegral': next < 0 ? 0 : next}); //GetStorage().write('user_info', User.instance.toJson()); } }