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.

855 lines
26 KiB

import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:amap_service/amap_service.dart';
import 'package:deep_voice/data/services/music_manager.dart';
import 'package:deep_voice/data/services/navigation_manager.dart';
import 'package:deep_voice/routes/app_routes.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:agent_service/agent_service.dart';
import 'package:chat_storage/chat_storage.dart';
import 'package:image_picker/image_picker.dart';
import 'package:get_storage/get_storage.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:app_settings/app_settings.dart';
import 'package:deep_voice/core/utils/logger.dart';
class Message {
final bool isUser; //标识信息是否来自用户还是AI
final String text; //信息文本内容
final DateTime timestamp; // 消息时间戳
final bool isRecognizing; // 是否为语音识别中的临时消息
final bool hasImage; // 是否包含图片
final String? imagePath; // 图片路径
final Map? meta; //卡片数据
Message({
// 构造函数
required this.isUser,
required this.text,
DateTime? timestamp,
this.isRecognizing = false,
this.hasImage = false,
this.imagePath,
this.meta,
}) : this.timestamp = timestamp ?? DateTime.now();
}
class AgentController extends GetxController {
static AgentController get to => Get.find(); //单例访问模式
// 聊天消息列表
final messages = <Message>[].obs;
// UI控制器
final TextEditingController textController = TextEditingController();
final ScrollController scrollController = ScrollController();
//音乐播放管理
final _musiceManager = Get.find<MusicManager>();
final _navigationManager = Get.find<NavigationManager>();
var isPlaying = false.obs;
var currentUrl = "".obs;
// 各种状态标志
final isInitialized = false.obs;
final isListening = false.obs; //是否正在语音识别
final isSpeaking = false.obs; //是否正在播放tts
final isProcessing = false.obs;
// TTS启用状态
final isTtsEnabled = true.obs;
// 图片处理状态
final isImageProcessing = false.obs;
// 输入模式控制
final isTextInputMode = true.obs; //文本输入模式vs语音输入模式
// 当前输入的文本内容
final currentText = ''.obs;
// 图片输入相关
final isImageInputActive = false.obs; // 是否处于图片输入模式
final selectedImagePath = Rx<String?>(null); // 当前选择的图片路径
// 流事件订阅
StreamSubscription<AgentServiceEvent>? _eventSubscription;
// 导航流事件订阅
StreamSubscription<AmapServiceEvent>? _amapeventSubscription;
// 用于跟踪AI回复状态
bool _isNewAssistantResponse = true;
String _currentResponseId = '';
final TAG = 'AgentController';
// 会话ID
final String sessionId = 'default_agent';
// 是否已加载聊天记录
final isHistoryLoaded = false.obs;
@override
void onInit() {
super.onInit();
_loadTtsPreference(); //加载TTS偏好设置
_loadChatHistory(); //加载聊天历史记录
_subscribeToEvents(); //订阅代理服务事件
}
@override
void onClose() {
AgentService.stopConversation(); // 停止当前会话
AgentService.stopTts(); // 停止当前语音
textController.dispose();
scrollController.dispose();
_eventSubscription?.cancel();
super.onClose();
}
// 加载TTS偏好设置
Future<void> _loadTtsPreference() async {
try {
final storage = GetStorage();
isTtsEnabled.value = storage.read('agent_tts_enabled') ?? true;
} catch (e) {
// 如果读取失败,使用默认值
Logger.e(TAG, '加载TTS偏好设置失败: $e');
isTtsEnabled.value = true;
}
}
// 切换TTS状态
Future<void> toggleTts() async {
isTtsEnabled.toggle();
// 如果关闭TTS,停止当前语音
if (!isTtsEnabled.value && isSpeaking.value) {
await AgentService.stopTts();
}
// 保存偏好设置
try {
final storage = GetStorage();
await storage.write('agent_tts_enabled', isTtsEnabled.value);
} catch (e) {
Logger.e(TAG, '保存TTS偏好设置失败: $e');
}
}
// 加载聊天历史记录
Future<void> _loadChatHistory() async {
try {
Logger.i(TAG, '正在加载聊天历史...');
final result = await ChatStorage.getMessages(
sessionId: sessionId,
page: 1,
pageSize: 20,
);
if (result.messages.isNotEmpty) {
// 转换并添加消息到列表
final historyMessages = result.messages.map((msg) {
final meta = msg.metadata != null && msg.metadata != ""
? jsonDecode(msg.metadata!)
: null; // 如果为空,使用空对象
return Message(
isUser: msg.sender == 'user',
text: msg.message,
meta: meta,
timestamp: DateTime.parse(msg.timestamp),
);
}).toList();
// 消息按时间排序,最新的消息在底部
historyMessages.sort((a, b) => a.timestamp.compareTo(b.timestamp));
messages.assignAll(historyMessages);
Logger.i(TAG, '成功加载 ${historyMessages.length} 条聊天记录');
} else {
// 没有历史记录时添加欢迎消息
_addWelcomeMessage();
Logger.i(TAG, '没有历史记录,添加欢迎消息');
}
} catch (e) {
Logger.e(TAG, '加载聊天历史失败: $e');
_addWelcomeMessage();
} finally {
isHistoryLoaded.value = true;
messages.refresh();
// 使用reverse:true时不需要手动滚动到底部
}
}
// 添加欢迎消息
void _addWelcomeMessage() {
messages.add(Message(
isUser: false,
text: '你好!我是Emma,我能帮你:\n\n'
'• 回答问题和提供信息\n'
'• 实时语音翻译\n'
'• 推荐音乐和播客\n'
'• 语音备忘录和提醒\n'
'• 控制耳机功能\n\n'
'有什么我可以帮你的吗?',
));
}
// 订阅代理服务事件
void _subscribeToEvents() {
_eventSubscription = AgentService.events.listen((event) {
switch (event.type) {
case AgentServiceEventType.recognitionStarted:
isListening.value = true;
isTextInputMode.value = false;
isTextInputMode.refresh();
// 只标记识别开始,不添加消息
break;
case AgentServiceEventType.recognitionStopped:
isListening.value = false;
isTextInputMode.value = true;
isTextInputMode.refresh();
break;
case AgentServiceEventType.recognizing:
final text = event.data['text'] ?? '';
currentText.value = text; // 保留当前文本,以便其他地方使用
if (text.isNotEmpty) {
// 查找是否有正在识别中的消息
final index = messages
.lastIndexWhere((msg) => msg.isRecognizing && msg.isUser);
if (index >= 0) {
// 更新已有的识别中消息
messages[index] = Message(
isUser: true,
text: text,
isRecognizing: true,
timestamp: messages[index].timestamp,
);
} else {
// 没有识别中的消息,创建新消息
messages.add(Message(
isUser: true,
text: text,
isRecognizing: true,
));
}
messages.refresh();
// 使用reverse:true时通常不需要手动滚动
}
break;
case AgentServiceEventType.recognitionResult:
final text = event.data['text'] ?? '';
// 查找是否有正在识别中的消息
final index =
messages.lastIndexWhere((msg) => msg.isRecognizing && msg.isUser);
if (text.isNotEmpty) {
if (index >= 0) {
// 将识别中的消息转换为最终消息
messages[index] = Message(
isUser: true,
text: text,
isRecognizing: false,
timestamp: messages[index].timestamp,
);
messages.refresh();
} else {
// 如果没有找到识别中的消息,添加新消息
messages
.add(Message(isUser: true, text: text, isRecognizing: false));
}
isProcessing.value = true;
// 标记为新的AI回复
_isNewAssistantResponse = true;
// 使用reverse:true时通常不需要手动滚动
} else if (index >= 0) {
// 如果识别结果为空,移除临时的识别消息
messages.removeAt(index);
messages.refresh();
}
break;
case AgentServiceEventType.ttsStarted:
isSpeaking.value = true;
Logger.i(TAG, 'TTS开始播放');
break;
case AgentServiceEventType.ttsCompleted:
case AgentServiceEventType.ttsStopped:
case AgentServiceEventType.ttsCanceled:
isSpeaking.value = false;
Logger.i(TAG, 'TTS停止播放');
break;
case AgentServiceEventType.imageProcessing:
isImageProcessing.value = true;
break;
case AgentServiceEventType.imageReady:
// 图片准备就绪,但AI还没有开始处理,保持processing状态
break;
case AgentServiceEventType.assistantToken:
if (!isProcessing.value) isProcessing.value = true;
final token = event.data['token'] ?? '';
final responseId = event.data['responseId'] ?? '';
if (token.isNotEmpty) {
// 如果是新的回复或者响应ID改变,创建新消息
if (_isNewAssistantResponse ||
(_currentResponseId != responseId && responseId.isNotEmpty)) {
messages.add(Message(isUser: false, text: token));
_isNewAssistantResponse = false;
_currentResponseId = responseId;
} else {
// 否则更新最后一条消息
final lastIndex = messages.length - 1;
if (lastIndex >= 0 && !messages[lastIndex].isUser) {
messages[lastIndex] = Message(
isUser: false,
text: messages[lastIndex].text + token,
meta: messages[lastIndex].meta,
timestamp: messages[lastIndex].timestamp,
);
}
}
// 使用reverse:true时通常不需要手动滚动
}
break;
case AgentServiceEventType.assistantResponse:
isProcessing.value = false;
isImageProcessing.value = false;
// 标记当前回复完成
_isNewAssistantResponse = true;
break;
case AgentServiceEventType.error:
isListening.value = false;
isSpeaking.value = false;
isProcessing.value = false;
isImageProcessing.value = false;
// 移除临时的识别消息
final index =
messages.lastIndexWhere((msg) => msg.isRecognizing && msg.isUser);
if (index >= 0) {
messages.removeAt(index);
messages.refresh();
}
Logger.e(TAG, '代理服务错误: ${event.data['message']}');
break;
case AgentServiceEventType.autoStop:
isListening.value = false;
// 移除临时的识别消息
final index =
messages.lastIndexWhere((msg) => msg.isRecognizing && msg.isUser);
if (index >= 0) {
// 只有当消息为空或文本太短时才移除
if (messages[index].text.trim().length < 2) {
messages.removeAt(index);
messages.refresh();
} else {
// 否则转换为正式消息
messages[index] = Message(
isUser: true,
text: messages[index].text,
timestamp: messages[index].timestamp,
);
messages.refresh();
// 发送此消息到AI处理
isProcessing.value = true;
_isNewAssistantResponse = true;
_handleAutoStop(index);
}
}
break;
case AgentServiceEventType.functionCall:
final functionName = event.data['name'] ?? '';
final arguments = event.data['arguments'] ?? '';
// debugPrint('Fultter 开始调用 mcp: $functionName, $arguments');
break;
case AgentServiceEventType.functionCallResult:
final responseId = event.data['responseId'] ?? '';
final result = event.data['result'] ?? '';
final functionCall = event.data['function_call'] ?? '';
Logger.i(TAG, 'Flutter 结束调用 mcp: $result, $functionCall');
if (result.isNotEmpty) {
final parsedResult = jsonDecode(result);
final meta = parsedResult["meta"];
Map<String, dynamic> metaResult = {};
if (meta != null && meta is String && meta.isNotEmpty) {
try {
metaResult = jsonDecode(meta);
} catch (e) {
Logger.e(TAG, '解析meta数据失败: $e');
}
}
if (metaResult.isNotEmpty) {
// 判断是否为新的回复或响应ID是否改变
if (_isNewAssistantResponse || _currentResponseId != responseId) {
// 添加新消息
messages
.add(Message(isUser: false, text: "", meta: metaResult));
_isNewAssistantResponse = false;
_currentResponseId = responseId;
} else {
// 更新最后一条消息
final lastIndex = messages.length - 1;
if (lastIndex >= 0 && !messages[lastIndex].isUser) {
messages[lastIndex] = Message(
isUser: false,
text: messages[lastIndex].text,
meta: metaResult,
timestamp: messages[lastIndex].timestamp,
);
}
}
}
}
break;
default:
break;
}
});
//后台导航事件同步
_amapeventSubscription = _navigationManager.events().listen((event) {
switch (event.type) {
case AmapServiceEventType.starte:
Logger.i(TAG, '导航通知 started!');
break;
case AmapServiceEventType.stope:
Logger.i(TAG, '导航通知 stoped!');
break;
case AmapServiceEventType.update:
Logger.i(TAG, '导航通知 update! $event');
Message? result;
try {
result = messages.lastWhere(
(msg) =>
!msg.isUser &&
msg.meta?.containsKey("card_navigation") == true,
);
} catch (e) {
result = null;
}
if (result != null) {
int index = messages.indexOf(result);
if (index != -1) {
// ✅ 创建 meta 的深拷贝
final newMeta = Map<String, dynamic>.from(result.meta ?? {});
newMeta["card_navigation"]["navigation"] = event.data;
// ✅ 创建新的 Message 替换原来的
messages[index] = Message(
isUser: result.isUser,
text: result.text,
meta: newMeta,
timestamp: result.timestamp,
);
messages.refresh();
Logger.i(
TAG, '更新导航卡片! ${newMeta["card_navigation"]["navigation"]}');
}
}
break;
case AmapServiceEventType.error:
Logger.i(TAG, '导航通知 error!');
break;
case AmapServiceEventType.unknown:
Logger.i(TAG, '导航通知 unknown!');
break;
}
});
}
// 发送文本消息
Future<void> sendTextMessage() async {
// 如果图片输入是活跃的,使用选中的图片和文本框的内容
if (isImageInputActive.value && selectedImagePath.value != null) {
final text = textController.text.trim();
final imagePath = selectedImagePath.value!; // 使用!强制断言非空
await sendImageMessage(imagePath, text: text);
// 清除图片输入状态
clearImageInput();
return;
}
final text = textController.text.trim();
if (text.isEmpty) return;
final message = Message(isUser: true, text: text);
messages.add(message);
textController.clear();
// 使用reverse:true时通常不需要手动滚动
try {
isProcessing.value = true;
// 标记为新的AI回复
_isNewAssistantResponse = true;
// 根据TTS状态决定是否朗读
await AgentService.processTextInput(text,
speakResponse: isTtsEnabled.value);
} catch (e) {
Logger.e(TAG, '发送文本消息失败: $e');
isProcessing.value = false;
}
}
// 发送图片消息
Future<void> sendImageMessage(String imagePath, {String? text}) async {
if (imagePath.isEmpty || !File(imagePath).existsSync()) {
Logger.e(TAG, '图片不存在: $imagePath');
return;
}
// 显示的文本,如果没有提供则使用默认值
final displayText = text?.isNotEmpty == true ? text! : '[图片]';
// 添加用户消息
final message = Message(
isUser: true,
text: displayText,
hasImage: true,
imagePath: imagePath,
);
messages.add(message);
// 清空文本输入
textController.clear();
// 使用reverse:true时通常不需要手动滚动
try {
isProcessing.value = true;
isImageProcessing.value = true;
// 标记为新的AI回复
_isNewAssistantResponse = true;
// 调用Agent Service处理图片,考虑TTS状态
await AgentService.processImageInput(
imagePath,
text: text ?? '',
speakResponse: isTtsEnabled.value,
);
} catch (e) {
Logger.e(TAG, '处理图片失败: $e');
isProcessing.value = false;
isImageProcessing.value = false;
}
}
// 切换输入模式
void toggleInputMode() async {
// 如果当前在图片输入模式,先清除
if (isImageInputActive.value) {
clearImageInput();
return;
}
// 如果要切换到语音模式,需要先检查麦克风权限
if (isTextInputMode.value) {
final hasPermission = await _checkMicrophonePermission();
if (!hasPermission) {
return; // 权限未获取,保持文本模式
}
}
isTextInputMode.toggle();
// 切换到语音模式时,直接开始语音输入
if (!isTextInputMode.value) {
startVoiceInput();
}
// 切换到文本模式时,直接停止语音输入
else {
stopVoiceInput();
}
}
// 检查麦克风权限
Future<bool> _checkMicrophonePermission() async {
try {
// 创建待请求权限列表
List<Permission> permissionsToRequest = [];
if (Platform.isAndroid) {
// Android 需要考虑SDK版本不同的权限要求
permissionsToRequest.add(Permission.microphone);
} else if (Platform.isIOS) {
// iOS 只需要麦克风权限
permissionsToRequest.add(Permission.microphone);
}
// 批量请求所有需要的权限
final statuses = await permissionsToRequest.request();
// 检查是否所有权限都已授予
bool allGranted = true;
Permission? deniedPermission;
statuses.forEach((permission, status) {
if (!status.isGranted) {
allGranted = false;
deniedPermission = permission;
Logger.e(TAG, '权限 ${permission.toString()} 未被授予: $status');
}
});
// 单独检查麦克风权限状态,因为这是主要需求
final micStatus = await Permission.microphone.status;
// 如果所有权限都已授予,或至少麦克风权限已授予,返回true
if (allGranted || micStatus.isGranted) {
return true;
}
// 处理特定权限被永久拒绝的情况
if (micStatus.isPermanentlyDenied) {
_showPermissionDialog(true);
return false;
}
// 普通拒绝情况
if (deniedPermission != null) {
_showPermissionDialog(false);
}
return false;
} catch (e) {
Logger.e(TAG, '检查麦克风权限失败: $e');
return false;
}
}
// 获取Android SDK版本
Future<int> _getAndroidSdkVersion() async {
if (Platform.isAndroid) {
try {
// 这里我们简单返回一个较高的值,确保请求所有可能需要的权限
// 在实际应用中,你可能需要使用platform channel获取真实的SDK版本
return 31; // 对应Android 12
} catch (e) {
Logger.e(TAG, '获取Android SDK版本失败: $e');
}
}
return 0;
}
// 显示权限提示对话框
void _showPermissionDialog(bool isPermanent) {
Get.dialog(
AlertDialog(
title: const Text('需要麦克风权限'),
content: Text(isPermanent
? '语音功能需要麦克风权限。请前往设置页面手动开启权限。'
: '语音功能需要麦克风权限。请允许使用麦克风权限以继续使用语音功能。'),
actions: [
TextButton(
onPressed: () => Get.back(),
child: const Text('取消'),
),
TextButton(
onPressed: () {
Get.back();
if (isPermanent) {
AppSettings.openAppSettings();
} else {
Permission.microphone.request();
}
},
child: Text(isPermanent ? '前往设置' : '授权'),
),
],
),
);
}
// 开始语音输入
Future<void> startVoiceInput() async {
if (isListening.value) return;
// 检查麦克风权限
final hasPermission = await _checkMicrophonePermission();
if (!hasPermission) {
isTextInputMode.value = true; // 如果没有权限,切回文本模式
return;
}
try {
await AgentService.startConversation();
} catch (e) {
Logger.e(TAG, '启动语音输入失败: $e');
}
}
// 停止语音输入
Future<void> stopVoiceInput() async {
if (!isListening.value) return;
try {
await AgentService.stopConversation();
} catch (e) {
Logger.e(TAG, '停止语音输入失败: $e');
}
}
// 清除聊天历史
Future<void> clearChat() async {
try {
await AgentService.clearChatHistory();
// 清空本地消息列表
messages.clear();
// 添加欢迎消息
_addWelcomeMessage();
// 重置回复状态
_isNewAssistantResponse = true;
} catch (e) {
Logger.e(TAG, '清除聊天历史失败: $e');
}
}
// 从相册选择图片
Future<void> pickImage() async {
try {
final ImagePicker picker = ImagePicker();
final XFile? image = await picker.pickImage(
source: ImageSource.gallery,
imageQuality: 80, // 设置图片质量
maxWidth: 1024, // 限制最大宽度
maxHeight: 1024, // 限制最大高度
);
if (image != null) {
// 设置已选择的图片
selectedImagePath.value = image.path;
// 激活图片输入模式
isImageInputActive.value = true;
}
} catch (e) {
Logger.e(TAG, '选择图片失败: $e');
}
}
// 拍照获取图片
Future<void> takePhoto() async {
try {
final ImagePicker picker = ImagePicker();
final XFile? photo = await picker.pickImage(
source: ImageSource.camera,
imageQuality: 80, // 设置图片质量
maxWidth: 1024, // 限制最大宽度
maxHeight: 1024, // 限制最大高度
);
if (photo != null) {
// 设置已选择的图片
selectedImagePath.value = photo.path;
// 激活图片输入模式
isImageInputActive.value = true;
}
} catch (e) {
Logger.e(TAG, '拍照失败: $e');
}
}
// 清除图片输入状态
void clearImageInput() {
selectedImagePath.value = null;
isImageInputActive.value = false;
}
// 使用预设提示词处理图片
void useImagePrompt(String prompt) {
// 确保selectedImagePath.value不为null
if (selectedImagePath.value == null) return;
// 填充文本框同时直接发送消息
textController.text = prompt;
// 立即发送图片和提示词
final imagePath = selectedImagePath.value!;
sendImageMessage(imagePath, text: prompt);
// 发送后清除图片输入状态
clearImageInput();
}
// 预设提示词:总结图片内容
void summarizeImage() {
useImagePrompt('请总结这张图片的内容');
}
// 预设提示词:抽取图像文字
void extractTextFromImage() {
useImagePrompt('请提取这张图片中的所有文字');
}
// 预设提示词:翻译图像文字
void translateImageText() {
useImagePrompt('请翻译这张图片中的文字');
}
// 播放音乐
void musicStatus() async {
var status = await _musiceManager.getStatus();
// debugPrint('获取当前音乐播放状态 $status');
isPlaying.value = status['isPlaying'];
currentUrl.value = status['url'];
}
// 播放音乐
void musicPlay(Map<String, String> song) {
_musiceManager.play(song);
isPlaying.value = true;
currentUrl.value = song['url'] ?? '';
}
// 播放音乐
void musicPause() {
_musiceManager.pause();
isPlaying.value = false;
}
void musicResume() {
_musiceManager.resume();
isPlaying.value = true;
}
// 修改自动停止处理,遵循TTS设置
void _handleAutoStop(int index) {
// 发送此消息到AI处理
isProcessing.value = true;
_isNewAssistantResponse = true;
AgentService.processTextInput(messages[index].text,
speakResponse: isTtsEnabled.value);
}
//去导航
void openNavigation() {
_navigationManager.open();
}
}