Files
ai_chat_assistant/lib/services/message_service.dart

433 lines
14 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:async';
import 'package:ai_chat_assistant/ai_chat_assistant.dart';
import 'package:ai_chat_assistant/utils/common_util.dart';
import 'package:ai_chat_assistant/utils/tts_util.dart';
import 'package:basic_intl/intl.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:uuid/uuid.dart';
import '../services/chat_sse_service.dart';
import '../services/classification_service.dart';
import '../services/control_recognition_service.dart';
// import '../services/audio_recorder_service.dart';
// import '../services/voice_recognition_service.dart';
import 'command_service.dart';
import 'package:fluttertoast/fluttertoast.dart';
// 用单例的模式创建
class MessageService extends ChangeNotifier {
static const MethodChannel _asrChannel = MethodChannel('com.example.ai_chat_assistant/ali_sdk');
static MessageService? _instance;
static MessageService get instance {
return _instance ??= MessageService._internal();
}
// 提供工厂构造函数供 Provider 使用
factory MessageService() => instance;
Completer<String>? _asrCompleter;
MessageService._internal() {
// 注册MethodChannel的handler
_asrChannel.setMethodCallHandler(_handleMethodCall);
}
@override
void dispose() {
// 取消注册MethodChannel的handler避免内存泄漏和意外回调
_asrChannel.setMethodCallHandler(null);
_asrCompleter?.completeError('Disposed');
_asrCompleter = null;
super.dispose();
}
// 提供真正的销毁方法(可选,用于应用退出时)
void destroyInstance() {
_asrChannel.setMethodCallHandler(null);
_asrCompleter?.completeError('Destroyed');
_asrCompleter = null;
super.dispose();
_instance = null;
}
Future<dynamic> _handleMethodCall(MethodCall call) async {
switch (call.method) {
case "onAsrResult":
replaceMessage(
id: _latestUserMessageId!, text: call.arguments, status: MessageStatus.normal);
break;
case "onAsrStop":
int index = findMessageIndexById(_latestUserMessageId!);
if (index == -1) {
return;
}
final message = _messages[index];
if (message.text.isEmpty) {
removeMessageById(_latestUserMessageId!);
return;
}
if (_asrCompleter != null && !_asrCompleter!.isCompleted) {
_asrCompleter!.complete(messages.last.text);
}
break;
}
}
final ChatSseService _chatSseService = ChatSseService();
// final LocalTtsService _ttsService = LocalTtsService();
// final AudioRecorderService _audioService = AudioRecorderService();
// final VoiceRecognitionService _recognitionService = VoiceRecognitionService();
final TextClassificationService _classificationService = TextClassificationService();
final VehicleCommandService _vehicleCommandService = VehicleCommandService();
final List<ChatMessage> _messages = [];
List<ChatMessage> get messages => List.unmodifiable(_messages);
String? _latestUserMessageId;
String? _latestAssistantMessageId;
MessageServiceState _state = MessageServiceState.idle;
bool get isRecording => _state == MessageServiceState.recording;
bool get isRecognizing => _state == MessageServiceState.recognizing;
bool get isReplying => _state == MessageServiceState.replying;
bool get isBusy => _state != MessageServiceState.idle;
bool _isReplyAborted = true;
void initializeEmpty() {
_messages.clear();
notifyListeners();
}
Future<void> startVoiceInput() async {
if (await Permission.microphone.status == PermissionStatus.denied) {
PermissionStatus status = await Permission.microphone.request();
if (status == PermissionStatus.permanentlyDenied) {
await Fluttertoast.showToast(
msg: Intl.getCurrentLocale().startsWith('zh')
? "请在设置中开启麦克风权限"
: "Please enable microphone in the settings");
}
return;
}
if (_state != MessageServiceState.idle) {
return;
}
try {
abortReply();
_latestUserMessageId = null;
_latestAssistantMessageId = null;
changeState(MessageServiceState.recording);
_latestUserMessageId = addMessage("", true, MessageStatus.listening);
_asrChannel.invokeMethod("startAsr");
} catch (e) {
print('录音开始出错: $e');
}
}
void changeState(MessageServiceState state) {
_state = state;
notifyListeners();
}
String addMessage(String text, bool isUser, MessageStatus status) {
String uuid = Uuid().v1();
_messages.add(ChatMessage(
id: uuid, text: text, isUser: isUser, timestamp: DateTime.now(), status: status));
notifyListeners();
return uuid;
}
Future<void> stopAndProcessVoiceInput() async {
if (_state != MessageServiceState.recording) {
changeState(MessageServiceState.idle);
return;
}
try {
_isReplyAborted = false;
changeState(MessageServiceState.recognizing);
_asrChannel.invokeMethod("stopAsr");
_asrCompleter = Completer<String>();
final recognizedText = await _asrCompleter!.future;
// final audioData = await _audioService.stopRecording();
// replaceMessage(
// id: _latestUserMessageId!,
// text: "",
// status: MessageStatus.recognizing);
// if (audioData == null || audioData.isEmpty) {
// removeMessageById(_latestUserMessageId!);
// return;
// }
// final recognizedText =
// await _recognitionService.recognizeSpeech(audioData);
// if (recognizedText == null || recognizedText.isEmpty) {
// removeMessageById(_latestUserMessageId!);
// return;
// }
// replaceMessage(
// id: _latestUserMessageId!,
// text: recognizedText,
// status: MessageStatus.normal);
changeState(MessageServiceState.replying);
await reply(recognizedText);
} catch (e) {
print(e);
} finally {
changeState(MessageServiceState.idle);
_asrCompleter = null;
}
}
int findMessageIndexById(String? id) {
return id == null ? -1 : _messages.indexWhere((msg) => msg.id == id);
}
void replaceMessage({
required String id,
String? text,
MessageStatus? status,
}) {
final index = findMessageIndexById(id);
if (index == -1) {
return;
}
final message = _messages[index];
_messages[index] = ChatMessage(
id: message.id,
text: text ?? message.text,
isUser: message.isUser,
timestamp: message.timestamp,
status: status ?? message.status,
);
notifyListeners();
}
Future<void> reply(String text) async {
_latestAssistantMessageId = addMessage("", false, MessageStatus.thinking);
bool isChinese = CommonUtil.containChinese(text);
try {
if (_isReplyAborted) {
return;
}
final category = await _classificationService.classifyText(text);
switch (category) {
case -1:
await occurError(isChinese);
break;
case 2:
await handleVehicleControl(text, isChinese);
break;
case 4:
await answerWrongQuestion(isChinese);
break;
default:
await answerQuestion(text, isChinese);
break;
}
} catch (e) {
await occurError(isChinese);
}
}
Future<void> handleVehicleControl(String text, bool isChinese) async {
if (_isReplyAborted) {
return;
}
final vehicleCommandResponse = await _vehicleCommandService.getCommandFromText(text);
if (vehicleCommandResponse == null) {
if (_isReplyAborted) {
return;
}
String msg = isChinese
? "无法识别车辆控制命令,请重试"
: "Cannot recognize the vehicle control command, please try again";
replaceMessage(id: _latestAssistantMessageId!, text: msg, status: MessageStatus.normal);
} else {
if (_isReplyAborted) {
return;
}
replaceMessage(
id: _latestAssistantMessageId!,
text: vehicleCommandResponse.tips!,
status: MessageStatus.executing);
if (!_isReplyAborted) {
if (await TtsUtil.start(isChinese) == true) {
TtsUtil.send(vehicleCommandResponse.tips!);
Future.delayed(const Duration(milliseconds: 300), () => TtsUtil.complete());
}
}
bool containOpenAC = false;
List<String> successCommandList = [];
for (var command in vehicleCommandResponse.commands) {
if (_isReplyAborted) {
return;
}
if (command.type == VehicleCommandType.unknown || command.error.isNotEmpty) {
continue;
}
if (command.type == VehicleCommandType.openAC) {
containOpenAC = true;
}
bool isSuccess;
if (containOpenAC && command.type == VehicleCommandType.changeACTemp) {
isSuccess = await Future.delayed(
const Duration(milliseconds: 2000), () => processCommand(command, isChinese));
} else {
isSuccess = await processCommand(command, isChinese);
}
if (isSuccess) {
successCommandList.add(isChinese ? command.type.chinese : command.type.english);
}
}
replaceMessage(
id: _latestAssistantMessageId!,
text: vehicleCommandResponse.tips!,
status: MessageStatus.normal);
notifyListeners();
if (_isReplyAborted || successCommandList.isEmpty) {
return;
}
String controlResponse = await _vehicleCommandService.getControlResponse(successCommandList);
if (_isReplyAborted || controlResponse.isEmpty) {
return;
}
addMessage(controlResponse, false, MessageStatus.normal);
}
}
Future<bool> processCommand(VehicleCommand command, bool isChinese) async {
String msg = "";
MessageStatus status = MessageStatus.normal;
var commandObjc = VehicleCommand(type: command.type, params: command.params);
var client = AIChatAssistantManager.instance.commandClient;
if (client == null) {
msg = isChinese ? "车辆控制服务未初始化" : "Vehicle control service is not initialized";
status = MessageStatus.failure;
addMessage(msg, false, status);
return false;
}
final (isSuccess, result) = await client.executeCommand(commandObjc);
if (command.type == VehicleCommandType.locateCar) {
msg = isChinese ? "很抱歉,定位车辆位置失败" : "Sorry, locate the vehicle unsuccessfully";
status = MessageStatus.failure;
if (isSuccess && result != null && result.isNotEmpty) {
String address = result['address'];
if (address.isNotEmpty) {
msg = isChinese ? "已为您找到车辆位置:\"$address\"" : "The vehicle location is \"$address\"";
status = MessageStatus.success;
}
}
} else {
if (!isSuccess) {
msg = isChinese
? "很抱歉,\"${command.type.chinese}\"失败"
: "Sorry, execute \"${command.type.english}\" unsuccessfully";
status = MessageStatus.failure;
}
}
if (msg.isNotEmpty) {
addMessage(msg, false, status);
}
return isSuccess;
}
Future<void> answerWrongQuestion(bool isChinese) async {
if (_isReplyAborted) {
return;
}
const chineseAnswer = "尊敬的ID.UNYX车主\n" +
"很抱歉,我暂时无法理解您的需求,请您更详细地描述,以便我能更好地为您服务。\n" +
"作为您的智能助手,我专注于以下服务:\n" +
"1. ID.UNYX车型功能咨询例如如何调节座椅记忆功能\n" +
"2. 日常用车问题解答,例如:充电注意事项有哪些?\n" +
"3. 基础车辆控制协助,例如:帮我打开空调。";
const englishAnswer = "Dear ID.UNYX Owner,\n" +
"I'm sorry that unable to understand your request currently. Please describe it in more detail so that I can better assist you.\n" +
"As your intelligent assistant, I specialize in the following services:\n" +
"1. ID.UNYX model feature consultation, such as: How do I adjust the seat memory function?\n" +
"2. Daily vehicle usage inquiries, such as: What are the charging precautions?\n" +
"3. Basic vehicle control assistance, such as: Help me turn on the air conditioning.";
replaceMessage(
id: _latestAssistantMessageId!,
text: isChinese ? chineseAnswer : englishAnswer,
status: MessageStatus.normal);
}
Future<void> answerQuestion(String text, isChinese) async {
if (_isReplyAborted) {
return;
}
_chatSseService.request(
messageId: _latestAssistantMessageId!,
text: text,
isChinese: isChinese,
onStreamResponse: handleStreamResponse,
);
}
Future<void> occurError(bool isChinese) async {
if (_isReplyAborted) {
return;
}
replaceMessage(
id: _latestAssistantMessageId!,
text: isChinese
? "很抱歉,众众暂时无法回答这个问题,请重试"
: "Sorry, I can not answer this question for the moment, please try again",
status: MessageStatus.normal);
}
void handleStreamResponse(String messageId, String responseText, bool isComplete) {
if (_isReplyAborted) {
return;
}
try {
if (isComplete) {
replaceMessage(
id: messageId,
text: responseText,
status: MessageStatus.completed,
);
} else {
replaceMessage(id: messageId, text: responseText, status: MessageStatus.thinking);
}
} catch (e) {
abortReply();
}
}
Future<void> abortReply() async {
_isReplyAborted = true;
_chatSseService.abort();
int index = findMessageIndexById(_latestAssistantMessageId);
if (index == -1 || messages[index].status != MessageStatus.thinking) {
return;
}
replaceMessage(id: _latestAssistantMessageId!, status: MessageStatus.aborted);
}
void removeMessageById(String id) {
_messages.removeWhere((msg) => msg.id == id);
notifyListeners();
}
void clearMessages() {
_messages.clear();
notifyListeners();
}
void removeNonListeningMessages() {
_messages.removeWhere((message) => message.status != MessageStatus.listening);
notifyListeners();
}
}