controller.dart 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  1. import 'dart:async';
  2. import 'dart:io';
  3. import 'package:flutter_sound/flutter_sound.dart';
  4. import 'package:get/get.dart';
  5. import 'package:path_provider/path_provider.dart';
  6. import '../../../../common/api/index.dart';
  7. import '../../../../common/models/index.dart';
  8. import '../../../../common/routers/index.dart';
  9. import '../../../../common/services/index.dart';
  10. import '../../../../common/utils/index.dart';
  11. import '../../../apps/meeting/index.dart';
  12. import '../../../apps/process/process_picker/index.dart';
  13. import '../../../common/create_form/index.dart';
  14. import '../../../common/portal/index.dart';
  15. import 'index.dart';
  16. class SpeechAssistantChatController extends GetxController {
  17. SpeechAssistantChatController();
  18. final state = SpeechAssistantChatState();
  19. /// 录音
  20. FlutterSoundRecorder? _myRecorder = FlutterSoundRecorder();
  21. bool _mRecorderIsInited = false;
  22. String _mPath = "";
  23. final _maxLength = 60; // 最长录音时间60秒
  24. int _recordSecond = 0;
  25. StreamSubscription? _mRecordingDataSubscription;
  26. bool _ttsOpen = false;
  27. /// 在 onInit() 之后调用 1 帧。这是进入的理想场所
  28. @override
  29. void onReady() {
  30. state.btnTitle = 'im_chat_tools_voice'.tr;
  31. _loadInitCommandList();
  32. initTheRecorder();
  33. _ttsOpen = ProgramCenterService.to.extendParam()['needTTS'] ?? false;
  34. super.onReady();
  35. }
  36. @override
  37. void onClose() {
  38. _myRecorder?.closeRecorder();
  39. _myRecorder = null;
  40. if (_ttsOpen) {
  41. FlutterTTSHelper.instance.stop();
  42. }
  43. super.onClose();
  44. }
  45. /// 初始化录音功能
  46. Future<void> initTheRecorder() async {
  47. // 检查录音权限
  48. var isPermission = await O2Utils.microphonePermission();
  49. if (!isPermission) {
  50. OLogger.e('没有权限,无法录音');
  51. Loading.showError('im_chat_error_need_record_permission'.tr);
  52. return;
  53. }
  54. // 开启录音工具
  55. await _myRecorder?.openRecorder();
  56. //设置订阅计时器
  57. await _myRecorder
  58. ?.setSubscriptionDuration(const Duration(milliseconds: 10));
  59. // 初始化录音路径
  60. var tempDir = await getTemporaryDirectory();
  61. _mPath = '${tempDir.path}/o2_speech_assistant.pcm'; // pcm 格式录音 baidu 语音需要
  62. _mRecorderIsInited = true;
  63. }
  64. /// 开始录音
  65. Future<void> startRecorder() async {
  66. if (!_mRecorderIsInited) {
  67. Loading.showError('im_chat_error_record_not_init'.tr);
  68. return;
  69. }
  70. if (state.status.value != SpeechStatus.idle) {
  71. OLogger.i('正在数据加载中!');
  72. return;
  73. }
  74. if (_ttsOpen) {
  75. FlutterTTSHelper.instance.stop();
  76. }
  77. state.status.value = SpeechStatus.speaking;
  78. // 初始化数字
  79. _recordSecond = 0;
  80. // 初始化文件
  81. var outputFile = File(_mPath);
  82. if (outputFile.existsSync()) {
  83. await outputFile.delete();
  84. }
  85. _myRecorder?.startRecorder(
  86. toFile: _mPath,
  87. codec: Codec.pcm16,
  88. );
  89. /// 监听录音
  90. _mRecordingDataSubscription = _myRecorder?.onProgress?.listen((e) {
  91. final date = DateTime.fromMillisecondsSinceEpoch(
  92. e.duration.inMilliseconds,
  93. isUtc: true);
  94. // var txt = date.hms();
  95. _recordSecond = date.second; //
  96. //设置了最大录音时长
  97. if (date.second >= _maxLength) {
  98. stopRecorder();
  99. return;
  100. }
  101. //更新录音时长
  102. // state.btnTitle = txt;
  103. });
  104. }
  105. Future<void> stopRecorder() async {
  106. if (state.status.value != SpeechStatus.speaking) {
  107. OLogger.i('正在数据加载中!');
  108. return;
  109. }
  110. await _myRecorder!.stopRecorder();
  111. _cancelRecorderSubscriptions();
  112. OLogger.d("record time $_recordSecond");
  113. if (_recordSecond < 1) {
  114. Loading.showError('im_chat_error_record_time_too_short'.tr);
  115. state.status.value = SpeechStatus.idle;
  116. } else {
  117. _sendFileToServer();
  118. }
  119. OLogger.d('stop end ......');
  120. }
  121. /// 取消录音监听
  122. void _cancelRecorderSubscriptions() {
  123. if (_mRecordingDataSubscription != null) {
  124. _mRecordingDataSubscription?.cancel();
  125. _mRecordingDataSubscription = null;
  126. }
  127. }
  128. /// 发送到后台
  129. Future<void> _sendFileToServer() async {
  130. state.status.value = SpeechStatus.thinking;
  131. File file = File(_mPath);
  132. final ids = await FileAssembleService.to.uploadFileForSpeechAssistant(file);
  133. if (ids != null) {
  134. OLogger.d('文件 id ${ids.id}');
  135. _thinking(fileId: ids.id);
  136. } else {
  137. state.status.value = SpeechStatus.idle;
  138. OLogger.e('上传文件失败!');
  139. }
  140. }
  141. /// 随机命令列表,初始化展示用
  142. Future<void> _loadInitCommandList() async {
  143. Map<String, dynamic> body = <String, dynamic>{};
  144. body['type'] = 'init';
  145. final result =
  146. await ProgramCenterService.to.executeScript(ProgramCenterService.to.speechScript(), body);
  147. var list = result == null ? [] : result as List;
  148. state.initCommandList.addAll(list.map((e) => e as String).toList());
  149. }
  150. Future<void> _thinking({String? fileId, String? inputText}) async {
  151. Map<String, dynamic> body = <String, dynamic>{};
  152. body['personId'] = O2ApiManager.instance.o2User?.id ?? '';
  153. if (fileId != null && fileId.isNotEmpty) {
  154. body['speechFileId'] = fileId;
  155. } else if (inputText != null && inputText.isNotEmpty) {
  156. body['inputText'] = inputText;
  157. }
  158. final result =
  159. await ProgramCenterService.to.executeScript(ProgramCenterService.to.speechScript(), body);
  160. if (result != null) {
  161. ImSpeechAssistantResponse res =
  162. ImSpeechAssistantResponse.fromJson(result);
  163. if (res.msg == null || res.msg!.isEmpty == true) {
  164. _answerMsg(res.err!);
  165. } else {
  166. res.createTime = DateTime.now();
  167. res.side = ImSpeechAssistantResponse.rightSide;
  168. state.chatList.add(res);
  169. final command = res.command;
  170. if (command == null) {
  171. _answerMsg('im_chat_speech_assistant_msg_unknown_command'.tr);
  172. } else {
  173. final answer = command.answer ?? '';
  174. final processId = command.processId ?? '';
  175. final portalId = command.portalId ?? '';
  176. final commandName = command.name ?? '';
  177. if (answer.isNotEmpty) {
  178. // 有答案 直接回复
  179. _answerMsg(answer);
  180. } else if (processId.isNotEmpty) {
  181. // 有流程 就启动流程工作
  182. _answerMsg(
  183. 'im_chat_speech_assistant_msg_open_start_level_process'.tr);
  184. _startWorkByProcess(processId);
  185. } else if (portalId.isNotEmpty) {
  186. _answerMsg('im_chat_speech_assistant_msg_open_app'
  187. .trArgs([res.msg ?? '']));
  188. _openPortal(portalId, command.pageId);
  189. } else if (commandName.isNotEmpty) {
  190. // 有命令执行对应的命令
  191. _doCommand(commandName);
  192. } else if (res.err != null && res.err!.isNotEmpty == true) {
  193. // 有错误,比如不认识的命令 直接回复
  194. _answerMsg(res.err!);
  195. } else {
  196. _answerMsg('im_chat_speech_assistant_msg_unknown_command'.tr);
  197. }
  198. }
  199. }
  200. } else {
  201. Loading.toast('request_error'.tr);
  202. }
  203. state.status.value = SpeechStatus.idle;
  204. OLogger.d('_thinking 完成!');
  205. }
  206. // 执行特殊命令,一般是打开某个应用
  207. _doCommand(String command) {
  208. switch (command) {
  209. case 'task':
  210. _answerMsg('im_chat_speech_assistant_msg_open_app'
  211. .trArgs([O2NativeAppEnum.task.name]));
  212. Get.toNamed(O2OARoutes.appTask);
  213. break;
  214. case 'taskCompleted':
  215. _answerMsg('im_chat_speech_assistant_msg_open_app'
  216. .trArgs([O2NativeAppEnum.taskcompleted.name]));
  217. Get.toNamed(O2OARoutes.appTaskcompleted);
  218. break;
  219. case 'read':
  220. _answerMsg('im_chat_speech_assistant_msg_open_app'
  221. .trArgs([O2NativeAppEnum.read.name]));
  222. Get.toNamed(O2OARoutes.appRead);
  223. break;
  224. case 'readCompleted':
  225. _answerMsg('im_chat_speech_assistant_msg_open_app'
  226. .trArgs([O2NativeAppEnum.readcompleted.name]));
  227. Get.toNamed(O2OARoutes.appReadcompleted);
  228. break;
  229. case 'startProcess':
  230. _answerMsg('im_chat_speech_assistant_msg_open_start_process'.tr);
  231. // 打开启动流程工具
  232. _startProcess();
  233. break;
  234. case 'meeting':
  235. _answerMsg('im_chat_speech_assistant_msg_open_app'
  236. .trArgs([O2NativeAppEnum.meeting.name]));
  237. // 打开会议管理
  238. MeetingPage.startMeetingApp();
  239. break;
  240. case 'createMeeting':
  241. _answerMsg('im_chat_speech_assistant_msg_open_meeting_create'.tr);
  242. // 打开创建会议
  243. MeetingPage.startMeetingApp(openCreate: true);
  244. break;
  245. case 'attendance':
  246. _answerMsg('im_chat_speech_assistant_msg_open_app'
  247. .trArgs([O2NativeAppEnum.attendance.name]));
  248. // 打开考勤打卡
  249. FastCheckInService.instance.stop(); // 进入考勤 先关闭极速打卡
  250. Get.toNamed(O2OARoutes.appAttendance);
  251. break;
  252. case 'bbs': // 论坛
  253. _answerMsg('im_chat_speech_assistant_msg_open_app'
  254. .trArgs([O2NativeAppEnum.bbs.name]));
  255. Get.toNamed(O2OARoutes.appBBS);
  256. break;
  257. case 'yunpan': // 网盘
  258. _answerMsg('im_chat_speech_assistant_msg_open_app'
  259. .trArgs([O2NativeAppEnum.yunpan.name]));
  260. Get.toNamed(FileAssembleService.to.isV3()
  261. ? O2OARoutes.appCloudDiskV3
  262. : O2OARoutes.appYunpan);
  263. break;
  264. case 'cms': // 信息中心
  265. _answerMsg('im_chat_speech_assistant_msg_open_app'
  266. .trArgs([O2NativeAppEnum.cms.name]));
  267. Get.toNamed(O2OARoutes.appCms);
  268. break;
  269. case 'calendar': // 信息中心
  270. _answerMsg('im_chat_speech_assistant_msg_open_app'
  271. .trArgs([O2NativeAppEnum.calendar.name]));
  272. Get.toNamed(O2OARoutes.appCalendar);
  273. break;
  274. case 'mindMap': // 信息中心
  275. _answerMsg('im_chat_speech_assistant_msg_open_app'
  276. .trArgs([O2NativeAppEnum.mindMap.name]));
  277. Get.toNamed(O2OARoutes.appMindMap);
  278. break;
  279. default:
  280. _answerMsg('im_chat_speech_assistant_msg_unknown_command'.tr);
  281. break;
  282. }
  283. }
  284. /// 回复信息
  285. _answerMsg(String msg) {
  286. state.chatList.add(ImSpeechAssistantResponse(
  287. msg: msg,
  288. side: ImSpeechAssistantResponse.leftSide,
  289. createTime: DateTime.now()));
  290. if (_ttsOpen) {
  291. FlutterTTSHelper.instance.speak(msg);
  292. }
  293. }
  294. /// 新建工作
  295. Future<void> _startProcess() async {
  296. // 选择启动流程
  297. var result = await ProcessPickerPage.startPicker(ProcessPickerMode.process);
  298. if (result != null && result is ProcessData) {
  299. CreateFormPage.startProcess(true, process: result);
  300. }
  301. }
  302. /// 启动流程,创建工作
  303. /// 先查询请假流程对象,然后启动流程
  304. Future<void> _startWorkByProcess(String processId) async {
  305. if (processId.isEmpty) {
  306. _answerMsg('im_chat_speech_assistant_msg_not_found_level_process'.tr);
  307. return;
  308. }
  309. final processInfo = await ProcessSurfaceService.to.getProcess(processId);
  310. if (processInfo == null) {
  311. _answerMsg('im_chat_speech_assistant_msg_not_found_level_process'.tr);
  312. return;
  313. }
  314. CreateFormPage.startProcess(true, process: processInfo);
  315. }
  316. /// 打开门户
  317. void _openPortal(String portalId, String? pageId) {
  318. PortalPage.open(portalId, pageId: pageId);
  319. }
  320. }