You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

404 lines
13 KiB

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<MeetingUploadService>();
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<String, dynamic>.from(data)
: <String, dynamic>{};
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<String, dynamic>.from(data);
final v = map.remove(wavFileName);
if (v == null) return null;
_storage.write(_pendingLocalRecordingTitleStorageKey, map);
return v.toString();
}
void requestLocalImport() {
localImportState.value++;
}
Future<void> 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<void> 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<void> 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<void> 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<void> 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<void> _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<dynamic> decodeList = jsonDecode(element['translate']);
List<Map<String, Object?>> speakerList =
decodeList.map((e) => Map<String, Object?>.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<dynamic> decodeList = jsonDecode(translateField);
List<Map<String, Object?>> speakerList = decodeList
.map((e) => Map<String, Object?>.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<dynamic> decodeList =
jsonDecode(fullRecord['translate']);
List<Map<String, Object?>> speakerList = decodeList
.map((e) => Map<String, Object?>.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<int> _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());
}
}