443 lines
15 KiB
Dart
443 lines
15 KiB
Dart
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:t_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' as Service;
|
||
import '../services/control_recognition_service.dart';
|
||
// import '../services/audio_recorder_service.dart';
|
||
// import '../services/voice_recognition_service.dart';
|
||
import 'package:fluttertoast/fluttertoast.dart';
|
||
|
||
import 'platform_tts_service.dart';
|
||
|
||
const aliSdkChannelName = 'com.example.ai_chat_assistant/ali_sdk';
|
||
|
||
// 用单例的模式创建
|
||
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":
|
||
{
|
||
debugPrint("ASR 结果: ${call.arguments}");
|
||
replaceMessage(
|
||
id: _latestUserMessageId!, text: call.arguments, status: MessageStatus.normal);
|
||
break;
|
||
}
|
||
case "onAsrStop":
|
||
{
|
||
debugPrint("ASR 停止: ${call.arguments}");
|
||
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(PlatformTtsService(aliSdkChannelName));
|
||
// final Service.ChatSseService _chatSseService = Service.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) {
|
||
Logger.e('录音开始出错: $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) {
|
||
Logger.e(e.toString());
|
||
} 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();
|
||
}
|
||
}
|