diff --git a/.gitignore b/.gitignore index 79c113f..642ebc8 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,5 @@ app.*.map.json /android/app/debug /android/app/profile /android/app/release + +.vscode/ \ No newline at end of file diff --git a/example/lib/main.dart b/example/lib/main.dart index 5348bce..ad9b26c 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,9 +1,48 @@ import 'package:flutter/material.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:provider/provider.dart'; -import 'package:ai_chat_assistant/app.dart'; -import 'package:ai_chat_assistant/services/message_service.dart'; -import 'package:ai_chat_assistant/enums/vehicle_command_type.dart'; +import 'package:ai_chat_assistant/ai_chat_assistant.dart'; + +class VehicleCommandClent extends VehicleCommandHandler { + @override + AIChatCommandCubit? get commandCubit => super.commandCubit; + + @override + Future<(bool, Map?)> executeCommand(VehicleCommand command) async { + // 在这里实现具体的车控命令执行逻辑 + print('执行车控命令: ${command.type}, 参数: ${command.params}'); + + if (commandCubit != null) { + + commandCubit?.emit(AIChatCommandState( + commandId: command.commandId, + commandType: command.type, + params: command.params, + status: AIChatCommandStatus.executing, + timestamp: DateTime.now(), + errorMessage: null, + result: null, + )); + } + + // 模拟命令执行完成 + await Future.delayed(const Duration(seconds: 2)); + + if (commandCubit != null) { + commandCubit?.emit(AIChatCommandState( + commandId: command.commandId, + commandType: command.type, + params: command.params, + status: AIChatCommandStatus.success, + timestamp: DateTime.now(), + errorMessage: null, + result: {'message': '命令执行成功'}, + )); + } + + return Future.value((true, {'message': '命令已执行'})); + } +} void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -14,20 +53,17 @@ void main() async { } // 初始化 AI Chat Assistant,注册车控命令回调 - ChatAssistantApp.initialize( - commandCallback: (VehicleCommandType type, Map? params) async { - // 这里是示例的车控命令处理逻辑 - print('收到车控命令: $type, 参数: $params'); - return Future.value((true, {'message': '命令已执行'})); - }, - ); + // ChatAssistantApp.initialize( + // commandCallback: (VehicleCommandType type, Map? params) async { + // // 这里是示例的车控命令处理逻辑 + // print('收到车控命令: $type, 参数: $params'); + // return Future.value((true, {'message': '命令已执行'})); + // }, + // ); - runApp( - ChangeNotifierProvider( - create: (_) => MessageService(), - child: const MyApp(), - ), - ); + AIChatAssistantManager.instance.setupCommandHandle(commandHandler: VehicleCommandClent()); + + runApp(const MyApp()); } class MyApp extends StatelessWidget { diff --git a/lib/ai_chat_assistant.dart b/lib/ai_chat_assistant.dart new file mode 100644 index 0000000..0d0a5f8 --- /dev/null +++ b/lib/ai_chat_assistant.dart @@ -0,0 +1,26 @@ + +// widgets +export 'screens/full_screen.dart'; +export 'screens/part_screen.dart'; +export 'app.dart'; +export 'widgets/floating_icon.dart'; + +// easy bloc +export 'bloc/easy_bloc.dart'; +export 'bloc/ai_chat_cubit.dart'; +export 'bloc/command_state.dart'; + +// services +export 'manager.dart'; +export 'services/message_service.dart'; +export 'services/command_service.dart'; + +// types && enums && models +export 'models/vehicle_cmd.dart'; +export 'models/vehicle_cmd_response.dart'; +export 'models/chat_message.dart'; +export 'models/vehicle_status_info.dart'; + +export 'enums/vehicle_command_type.dart'; +export 'enums/message_status.dart'; +export 'enums/message_service_state.dart'; \ No newline at end of file diff --git a/lib/bloc/ai_chat_cubit.dart b/lib/bloc/ai_chat_cubit.dart new file mode 100644 index 0000000..080dc48 --- /dev/null +++ b/lib/bloc/ai_chat_cubit.dart @@ -0,0 +1,24 @@ +import '../enums/vehicle_command_type.dart'; +import '../services/command_service.dart'; +import 'easy_bloc.dart'; +import 'command_state.dart'; + +class AIChatCommandCubit extends EasyCubit { + AIChatCommandCubit() : super(const AIChatCommandState()); + + // 重置状态 + void reset() { + emit(const AIChatCommandState()); + } + + // 生成唯一命令ID + String _generateCommandId() { + return '${DateTime.now().millisecondsSinceEpoch}_${state.commandType?.name ?? 'unknown'}'; + } + + // 检查当前是否有命令在执行 + bool get isExecuting => state.status == AIChatCommandStatus.executing; + + // 获取当前命令ID + String? get currentCommandId => state.commandId; +} \ No newline at end of file diff --git a/lib/bloc/command_state.dart b/lib/bloc/command_state.dart new file mode 100644 index 0000000..f3557b8 --- /dev/null +++ b/lib/bloc/command_state.dart @@ -0,0 +1,68 @@ +import '../enums/vehicle_command_type.dart'; + +// AI Chat Command States +enum AIChatCommandStatus { + idle, + executing, + success, + failure, + cancelled, +} + +class AIChatCommandState { + final String? commandId; + final VehicleCommandType? commandType; + final Map? params; + final AIChatCommandStatus status; + final String? errorMessage; + final Map? result; + final DateTime? timestamp; + + const AIChatCommandState({ + this.commandId, + this.commandType, + this.params, + this.status = AIChatCommandStatus.idle, + this.errorMessage, + this.result, + this.timestamp, + }); + + AIChatCommandState copyWith({ + String? commandId, + VehicleCommandType? commandType, + Map? params, + AIChatCommandStatus? status, + String? errorMessage, + Map? result, + DateTime? timestamp, + }) { + return AIChatCommandState( + commandId: commandId ?? this.commandId, + commandType: commandType ?? this.commandType, + params: params ?? this.params, + status: status ?? this.status, + errorMessage: errorMessage ?? this.errorMessage, + result: result ?? this.result, + timestamp: timestamp ?? this.timestamp, + ); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is AIChatCommandState && + other.commandId == commandId && + other.commandType == commandType && + other.status == status && + other.errorMessage == errorMessage; + } + + @override + int get hashCode { + return commandId.hashCode ^ + commandType.hashCode ^ + status.hashCode ^ + errorMessage.hashCode; + } +} \ No newline at end of file diff --git a/lib/bloc/easy_bloc.dart b/lib/bloc/easy_bloc.dart new file mode 100644 index 0000000..e460689 --- /dev/null +++ b/lib/bloc/easy_bloc.dart @@ -0,0 +1,78 @@ +import 'dart:async'; + +import 'package:meta/meta.dart'; + +// 简易的Bloc, 简易的状态管理 + +abstract class EasyBlocBase { + + EasyBlocBase(this.state): assert(state != null); + + final StreamController _stateController = StreamController.broadcast(); + + Stream get stream => _stateController.stream; + State state; + + bool _emitted = false; + + // 添加监听器方法 + StreamSubscription listen( + void Function(State state) onData, { + Function? onError, + void Function()? onDone, + bool? cancelOnError, + }) { + return stream.listen( + onData, + onError: onError, + onDone: onDone, + cancelOnError: cancelOnError, + ); + } + + // 新状态 + void emit(State newState) { + if (_stateController.isClosed) return; + if (newState == state && _emitted) return; + this.state = newState; + _stateController.add(state); + _emitted = true; + } + + @mustCallSuper + Future close() async { + await _stateController.close(); + } +} + +class EasyBloc extends EasyBlocBase { + EasyBloc(State initialState) : super(initialState) { + _eventController.stream.listen(_mapEventToState); + } + + final StreamController _eventController = StreamController(); + + // 子类需要实现这个方法来处理事件 + @mustBeOverridden + void _mapEventToState(Event event) { + // 默认不做任何处理,如果不实现会抛出异常 + throw UnimplementedError('_mapEventToState must be implemented by subclasses'); + } + + // 添加事件 + void add(Event event) { + if (_eventController.isClosed) return; + _eventController.add(event); + } + + @override + Future close() async { + await _eventController.close(); + await super.close(); + } +} + +class EasyCubit extends EasyBlocBase { + EasyCubit(State initialState) : super(initialState); +} + diff --git a/lib/enums/message_service_state.dart b/lib/enums/message_service_state.dart index 1531820..2318272 100644 --- a/lib/enums/message_service_state.dart +++ b/lib/enums/message_service_state.dart @@ -3,4 +3,4 @@ enum MessageServiceState { recording, recognizing, replying, -} \ No newline at end of file +} \ No newline at end of file diff --git a/lib/manager.dart b/lib/manager.dart new file mode 100644 index 0000000..53106d2 --- /dev/null +++ b/lib/manager.dart @@ -0,0 +1,52 @@ +import 'dart:async'; +import 'package:flutter/widgets.dart'; + +import 'bloc/ai_chat_cubit.dart'; +import 'bloc/command_state.dart'; +import 'models/vehicle_cmd.dart'; + +/// 车辆命令处理器抽象类 +abstract class VehicleCommandHandler { + AIChatCommandCubit? commandCubit; + /// 执行车辆控制命令 + Future<(bool, Map?)> executeCommand(VehicleCommand command); +} + +class AIChatAssistantManager { + static final AIChatAssistantManager _instance = AIChatAssistantManager._internal(); + + static AIChatAssistantManager get instance => _instance; + + AIChatAssistantManager._internal() { + // 初始化代码 + debugPrint('AIChatAssistant 单例创建'); + _commandCubit = AIChatCommandCubit(); + } + + AIChatCommandCubit? _commandCubit; + + VehicleCommandHandler? commandClient; + + /// 初始化命令处理器 + void setupCommandHandle({ + required VehicleCommandHandler commandHandler, + }) { + commandClient = commandHandler; + commandClient?.commandCubit = _commandCubit; + } + + /// 获取命令状态流 + Stream get commandStateStream { + if (_commandCubit == null) { + throw StateError('AIChatAssistant 未初始化'); + } + return _commandCubit!.stream; + } + + /// 释放资源 + void dispose() { + _commandCubit?.close(); + _commandCubit = null; + } + +} diff --git a/lib/models/vehicle_cmd.dart b/lib/models/vehicle_cmd.dart index 0ca9732..ac959cc 100644 --- a/lib/models/vehicle_cmd.dart +++ b/lib/models/vehicle_cmd.dart @@ -1,11 +1,15 @@ import '../enums/vehicle_command_type.dart'; +/// 车辆控制命令类 class VehicleCommand { final VehicleCommandType type; final Map? params; final String error; + late String commandId; - VehicleCommand({required this.type, this.params, this.error = ''}); + VehicleCommand({required this.type, this.params, this.error = ''}) { + commandId = DateTime.now().millisecondsSinceEpoch.toString(); + } // 从字符串创建命令(用于从API响应解析) factory VehicleCommand.fromString( diff --git a/lib/screens/part_screen.dart b/lib/screens/part_screen.dart index c73a028..e6b1909 100644 --- a/lib/screens/part_screen.dart +++ b/lib/screens/part_screen.dart @@ -57,10 +57,16 @@ class _PartScreenState extends State { } void _openFullScreen() async { + final messageService = context.read(); + widget.onHide?.call(); + await Navigator.of(context).push( MaterialPageRoute( - builder: (context) => const FullScreen(), + builder: (context) => ChangeNotifierProvider.value( + value: messageService, // 传递同一个单例实例 + child: const FullScreen(), + ), ), ); } diff --git a/lib/services/command_service.dart b/lib/services/command_service.dart index 7f54c9e..d2c066d 100644 --- a/lib/services/command_service.dart +++ b/lib/services/command_service.dart @@ -5,7 +5,7 @@ import '../models/vehicle_status_info.dart'; typedef CommandCallback = Future<(bool, Map? params)> Function( VehicleCommandType type, Map? params); -/// 命令处理器类 - 负责处理来自AI的车辆控制命令 +/// 命令处理器类 - 负责处理来自AI的车辆控制命令(使用回调的方式交给App去实现具体的车控逻辑) class CommandService { /// 保存主应用注册的回调函数 static CommandCallback? onCommandReceived; diff --git a/lib/services/control_recognition_service.dart b/lib/services/control_recognition_service.dart index f8a27b1..fad8c63 100644 --- a/lib/services/control_recognition_service.dart +++ b/lib/services/control_recognition_service.dart @@ -4,6 +4,7 @@ import '../models/vehicle_cmd.dart'; import '../models/vehicle_cmd_response.dart'; import 'vehicle_state_service.dart'; +/// 车辆命令服务 - 负责与后端交互以获取和处理车辆控制命令 class VehicleCommandService { // final VehicleStateService vehicleStateService = VehicleStateService(); diff --git a/lib/services/message_service.dart b/lib/services/message_service.dart index da89c53..9bf7e1b 100644 --- a/lib/services/message_service.dart +++ b/lib/services/message_service.dart @@ -1,5 +1,6 @@ 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'; @@ -7,11 +8,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:uuid/uuid.dart'; -import '../enums/vehicle_command_type.dart'; -import '../enums/message_service_state.dart'; -import '../enums/message_status.dart'; -import '../models/chat_message.dart'; -import '../models/vehicle_cmd.dart'; import '../services/chat_sse_service.dart'; import '../services/classification_service.dart'; import '../services/control_recognition_service.dart'; @@ -20,48 +16,72 @@ import '../services/control_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 final MessageService _instance = MessageService._internal(); + static MessageService? _instance; - factory MessageService() => _instance; + static MessageService get instance { + return _instance ??= MessageService._internal(); + } + + // 提供工厂构造函数供 Provider 使用 + factory MessageService() => instance; Completer? _asrCompleter; MessageService._internal() { - _asrChannel.setMethodCallHandler((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; - } - }); + // 注册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 _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 TextClassificationService _classificationService = TextClassificationService(); final VehicleCommandService _vehicleCommandService = VehicleCommandService(); final List _messages = []; @@ -122,11 +142,7 @@ class MessageService extends ChangeNotifier { 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)); + id: uuid, text: text, isUser: isUser, timestamp: DateTime.now(), status: status)); notifyListeners(); return uuid; } @@ -226,8 +242,7 @@ class MessageService extends ChangeNotifier { if (_isReplyAborted) { return; } - final vehicleCommandResponse = - await _vehicleCommandService.getCommandFromText(text); + final vehicleCommandResponse = await _vehicleCommandService.getCommandFromText(text); if (vehicleCommandResponse == null) { if (_isReplyAborted) { return; @@ -235,10 +250,7 @@ class MessageService extends ChangeNotifier { String msg = isChinese ? "无法识别车辆控制命令,请重试" : "Cannot recognize the vehicle control command, please try again"; - replaceMessage( - id: _latestAssistantMessageId!, - text: msg, - status: MessageStatus.normal); + replaceMessage(id: _latestAssistantMessageId!, text: msg, status: MessageStatus.normal); } else { if (_isReplyAborted) { return; @@ -259,8 +271,7 @@ class MessageService extends ChangeNotifier { if (_isReplyAborted) { return; } - if (command.type == VehicleCommandType.unknown || - command.error.isNotEmpty) { + if (command.type == VehicleCommandType.unknown || command.error.isNotEmpty) { continue; } if (command.type == VehicleCommandType.openAC) { @@ -268,8 +279,8 @@ class MessageService extends ChangeNotifier { } bool isSuccess; if (containOpenAC && command.type == VehicleCommandType.changeACTemp) { - isSuccess = await Future.delayed(const Duration(milliseconds: 2000), - () => processCommand(command, isChinese)); + isSuccess = await Future.delayed( + const Duration(milliseconds: 2000), () => processCommand(command, isChinese)); } else { isSuccess = await processCommand(command, isChinese); } @@ -285,8 +296,7 @@ class MessageService extends ChangeNotifier { if (_isReplyAborted || successCommandList.isEmpty) { return; } - String controlResponse = - await _vehicleCommandService.getControlResponse(successCommandList); + String controlResponse = await _vehicleCommandService.getControlResponse(successCommandList); if (_isReplyAborted || controlResponse.isEmpty) { return; } @@ -297,20 +307,22 @@ class MessageService extends ChangeNotifier { Future processCommand(VehicleCommand command, bool isChinese) async { String msg = ""; MessageStatus status = MessageStatus.normal; - final (isSuccess, result) = await CommandService.executeCommand( - command.type, - params: command.params); + 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"; + 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\""; + msg = isChinese ? "已为您找到车辆位置:\"$address\"" : "The vehicle location is \"$address\""; status = MessageStatus.success; } } @@ -386,8 +398,7 @@ class MessageService extends ChangeNotifier { status: MessageStatus.completed, ); } else { - replaceMessage( - id: messageId, text: responseText, status: MessageStatus.thinking); + replaceMessage(id: messageId, text: responseText, status: MessageStatus.thinking); } } catch (e) { abortReply(); @@ -401,8 +412,7 @@ class MessageService extends ChangeNotifier { if (index == -1 || messages[index].status != MessageStatus.thinking) { return; } - replaceMessage( - id: _latestAssistantMessageId!, status: MessageStatus.aborted); + replaceMessage(id: _latestAssistantMessageId!, status: MessageStatus.aborted); } void removeMessageById(String id) { diff --git a/lib/widgets/floating_icon.dart b/lib/widgets/floating_icon.dart index 6c7a04a..d83c1ed 100644 --- a/lib/widgets/floating_icon.dart +++ b/lib/widgets/floating_icon.dart @@ -96,9 +96,14 @@ class _FloatingIconState extends State with TickerProviderStateMix // 显示全屏界面 void _showFullScreen() async { _hidePartScreen(); + final messageService = context.read(); + await Navigator.of(context).push( MaterialPageRoute( - builder: (context) => const FullScreen(), + builder: (context) => ChangeNotifierProvider.value( + value: messageService, // 传递同一个单例实例 + child: const FullScreen(), + ), ), ); } @@ -164,7 +169,11 @@ class _FloatingIconState extends State with TickerProviderStateMix // } // } // }); - + + return ChangeNotifierProvider(create: (_) => MessageService(), child: _buildFloatingIcon()); + } + + Widget _buildFloatingIcon() { return Stack( children: [ if (_isShowPartScreen) diff --git a/pubspec.lock b/pubspec.lock index e5009f2..a1f9a2b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -211,7 +211,7 @@ packages: source: hosted version: "0.11.1" meta: - dependency: transitive + dependency: "direct main" description: name: meta sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 @@ -496,5 +496,5 @@ packages: source: hosted version: "1.1.0" sdks: - dart: ">=3.6.2 <4.0.0" + dart: ">=3.6.0 <4.0.0" flutter: ">=3.27.0" diff --git a/pubspec.yaml b/pubspec.yaml index 602e418..457a3bc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,6 +7,7 @@ environment: dependencies: flutter: sdk: flutter + meta: ^1.15.0 fluttertoast: ^8.2.12 record: ^6.0.0 http: ^1.4.0