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.
1116 lines
36 KiB
1116 lines
36 KiB
import 'dart:async';
|
|
import 'dart:convert';
|
|
import 'dart:ffi';
|
|
import 'dart:io';
|
|
import 'package:amap_service/amap_service.dart';
|
|
import 'package:deep_voice/core/utils/upload_oss.dart';
|
|
import 'package:deep_voice/data/models/user_Info.dart';
|
|
import 'package:deep_voice/data/services/location_manager.dart';
|
|
import 'package:deep_voice/data/services/music_manager.dart';
|
|
import 'package:deep_voice/data/services/navigation_manager.dart';
|
|
import 'package:deep_voice/data/services/network/api.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:deep_voice/core/utils/logger.dart';
|
|
import 'package:path_provider/path_provider.dart';
|
|
import 'package:photo_view/photo_view.dart';
|
|
import '../../../core/utils/permission_util.dart';
|
|
import '../../../data/models/message_model.dart';
|
|
|
|
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 currentId = 0.obs;
|
|
|
|
// 播放进度相关状态
|
|
var currentPosition = 0.obs; // 当前播放位置(毫秒)
|
|
var duration = 0.obs; // 总时长(毫秒)
|
|
var progress = 0.0.obs; // 播放进度(0.0-1.0)
|
|
|
|
// 各种状态标志
|
|
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;
|
|
|
|
// 播放进度更新定时器
|
|
Timer? _progressTimer;
|
|
|
|
// 用于跟踪AI回复状态
|
|
bool _isNewAssistantResponse = true;
|
|
String _currentResponseId = '';
|
|
|
|
final TAG = 'AgentController';
|
|
|
|
// 会话ID
|
|
final String sessionId = 'default_agent';
|
|
|
|
// 是否已加载聊天记录
|
|
final isHistoryLoaded = false.obs;
|
|
|
|
// 添加加载状态跟踪
|
|
final RxBool isLoadingMusic = false.obs;
|
|
final RxString loadingMusicId = ''.obs;
|
|
|
|
// 设置加载状态的方法
|
|
void setLoadingMusic(String id, bool loading) {
|
|
loadingMusicId.value = loading ? id : '';
|
|
isLoadingMusic.value = loading;
|
|
}
|
|
|
|
@override
|
|
void onInit() {
|
|
super.onInit();
|
|
_loadTtsPreference(); //加载TTS偏好设置
|
|
_loadChatHistory(); //加载聊天历史记录
|
|
_subscribeToEvents(); //订阅代理服务事件
|
|
_initMusicStatus(); //初始化音乐状态
|
|
}
|
|
|
|
// 初始化音乐播放状态
|
|
Future<void> _initMusicStatus() async {
|
|
try {
|
|
await musicStatus();
|
|
Logger.d('AgentController', '音乐状态初始化完成');
|
|
} catch (e) {
|
|
Logger.e('AgentController', '音乐状态初始化失败: $e');
|
|
}
|
|
}
|
|
|
|
@override
|
|
void onClose() {
|
|
AgentService.stopConversation(); // 停止当前会话
|
|
AgentService.stopTts(); // 停止当前语音
|
|
textController.dispose();
|
|
scrollController.dispose();
|
|
_eventSubscription?.cancel();
|
|
_amapeventSubscription?.cancel();
|
|
_progressTimer?.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, '正在加载聊天历史...');
|
|
//通过本地插件ChatStorage获取聊天记录
|
|
final result = await ChatStorage.getMessages(
|
|
sessionId: sessionId, //会话ID
|
|
page: 1, //从第一页开始
|
|
pageSize: 20, //每页20条记录
|
|
);
|
|
|
|
if (result.messages.isNotEmpty) {
|
|
// 转换并添加消息到列表
|
|
final historyMessages = result.messages.map((msg) {
|
|
final meta = msg.metadata != null && msg.metadata != ""
|
|
? jsonDecode(msg.metadata!)
|
|
: null; // 如果为空,使用空对象
|
|
|
|
// 从元数据中提取图片信息
|
|
final hasImage = meta != null && meta.containsKey('hasImage')
|
|
? meta['hasImage'] as bool
|
|
: false;
|
|
final imagePath = meta != null && meta.containsKey('imagePath')
|
|
? meta['imagePath'] as String?
|
|
: null;
|
|
|
|
return Message(
|
|
isUser: msg.sender == 'user',
|
|
text: msg.message,
|
|
meta: meta,
|
|
timestamp: DateTime.parse(msg.timestamp),
|
|
hasImage: hasImage,
|
|
imagePath: imagePath,
|
|
);
|
|
}).toList();
|
|
|
|
// 消息按时间排序,最新的消息在底部
|
|
historyMessages.sort((a, b) => a.timestamp.compareTo(b.timestamp));
|
|
messages.assignAll(historyMessages);
|
|
|
|
Logger.i(TAG, '成功加载 ${historyMessages.length} 条聊天记录');
|
|
// 详细打印每条消息的内容和属性,便于调试
|
|
Logger.d(TAG, '===== 历史消息详情开始 =====');
|
|
for (int i = 0; i < historyMessages.length; i++) {
|
|
final msg = historyMessages[i];
|
|
Logger.d(
|
|
TAG,
|
|
'消息[$i]: {'
|
|
'isUser: ${msg.isUser}, '
|
|
'text: "${msg.text.length > 50 ? msg.text.substring(0, 50) + "..." : msg.text}", '
|
|
'time: ${msg.timestamp}, '
|
|
'hasImage: ${msg.hasImage}, '
|
|
'imagePath: ${msg.imagePath ?? "无"}, '
|
|
'meta: ${msg.meta != null ? "有" : "无"}'
|
|
'}');
|
|
}
|
|
Logger.d(TAG, '===== 历史消息详情结束 =====');
|
|
} 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: '你好!我是**小言**,我能帮你:\n\n'
|
|
'• 答问题和提供信息\n\n'
|
|
'• 实时语音翻译\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();
|
|
// 只标记识别开始,不添加消息
|
|
Logger.i(TAG, '识别开始');
|
|
break;
|
|
case AgentServiceEventType.recognitionStopped:
|
|
isListening.value = false;
|
|
isTextInputMode.value = true;
|
|
// isTextInputMode.refresh();
|
|
Logger.i(TAG, '识别停止');
|
|
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();
|
|
}
|
|
break;
|
|
|
|
case AgentServiceEventType.recognitionResult:
|
|
final text = event.data['text'] ?? '';
|
|
Logger.i(TAG, '识别结果: $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 metaStr = event.data['meta'] ?? '';
|
|
// final functionCall = event.data['function_call'] ?? '';
|
|
Logger.i(TAG, 'Flutter 结束调用 mcp: $metaStr');
|
|
if (metaStr.isNotEmpty) {
|
|
Map<String, dynamic> metaResult = {};
|
|
metaResult = jsonDecode(metaStr) as Map<String, dynamic>;
|
|
if (metaResult.isNotEmpty) {
|
|
var notreply = false;
|
|
if (metaResult.containsKey("notreply")) {
|
|
// 如果有导航卡片,直接添加
|
|
notreply = metaResult["notreply"] as bool;
|
|
}
|
|
if (notreply) {
|
|
Logger.i(TAG, 'Flutter 结束调用 不回复');
|
|
//不回复直接返回
|
|
break;
|
|
}
|
|
// 判断是否为新的回复或响应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,
|
|
);
|
|
}
|
|
}
|
|
if (metaResult.containsKey("maps_route_navigation")) {
|
|
//导航
|
|
final start = metaResult["maps_route_navigation"]
|
|
["card_navigation"]["start"] as String;
|
|
final end = metaResult["maps_route_navigation"]
|
|
["card_navigation"]["end"] as String;
|
|
// Logger.i(TAG, 'liwei-------- 打开导航页面: $start $end ');
|
|
Future.delayed(Duration(seconds: 2), () {
|
|
// Logger.i(TAG, 'liwei-------- 打开导航页面: $start $end ');
|
|
// 2秒后执行的代码
|
|
openNavigation(start, end, "");
|
|
});
|
|
}
|
|
}
|
|
}
|
|
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;
|
|
}
|
|
});
|
|
}
|
|
|
|
// 发送消息(包括所有支持的消息类型)
|
|
//场景1:用户在文本输入框中输入文本,点击发送按钮,调用sendMessage()方法。
|
|
//场景2:当用户选择图片后在文本框输入内容并发送时,调用sendMessage()转发给sendImageMessage()。
|
|
Future<void> sendMessage() async {
|
|
// 收回键盘
|
|
FocusManager.instance.primaryFocus?.unfocus();
|
|
// 1. 处理图片+文本的混合输入
|
|
if (isImageInputActive.value && selectedImagePath.value != null) {
|
|
final text = textController.text.trim();
|
|
final imagePath = selectedImagePath.value!; // 使用!强制断言非空
|
|
await sendImageMessage(imagePath, text: text);
|
|
|
|
// 清除图片输入状态
|
|
clearImageInput();
|
|
return;
|
|
}
|
|
|
|
// 2. 处理纯文本消息
|
|
final text = textController.text.trim();
|
|
if (text.isEmpty) return;
|
|
|
|
final message = Message(isUser: true, text: text);
|
|
messages.add(message);
|
|
textController.clear();
|
|
|
|
// 使用reverse:true时通常不需要手动滚动
|
|
// 3. 调用AI服务处理
|
|
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 permanentImagePath = await _copyImageToPermanentStorage(imagePath);
|
|
var ImageUrl = await UploadOss.upload(
|
|
filepath: imagePath, rootDir: 'User/${User.instance.uid}/ChatImages');
|
|
Logger.i(TAG, '图片上传: $imagePath URL: $ImageUrl');
|
|
// 显示的文本,如果没有提供则使用默认值
|
|
final displayText = text?.isNotEmpty == true ? text! : '[图片]';
|
|
|
|
// 添加用户消息
|
|
final message = Message(
|
|
isUser: true,
|
|
text: displayText,
|
|
hasImage: true,
|
|
imagePath: permanentImagePath,
|
|
);
|
|
messages.add(message);
|
|
|
|
// 清空文本输入
|
|
textController.clear();
|
|
|
|
// 使用reverse:true时通常不需要手动滚动
|
|
|
|
try {
|
|
isProcessing.value = true;
|
|
isImageProcessing.value = true;
|
|
// 标记为新的AI回复
|
|
_isNewAssistantResponse = true;
|
|
|
|
// 保存消息到持久化存储
|
|
//await _saveMessageToStorage(message);
|
|
|
|
// 调用Agent Service处理图片,考虑TTS状态
|
|
await AgentService.processImageInput(
|
|
permanentImagePath,
|
|
ImageUrl,
|
|
text: text ?? '',
|
|
speakResponse: isTtsEnabled.value,
|
|
);
|
|
} catch (e) {
|
|
Logger.e(TAG, '处理图片失败: $e');
|
|
isProcessing.value = false;
|
|
isImageProcessing.value = false;
|
|
}
|
|
}
|
|
|
|
// 将图片复制到应用的永久存储目录
|
|
Future<String> _copyImageToPermanentStorage(String sourcePath) async {
|
|
try {
|
|
final File sourceFile = File(sourcePath);
|
|
if (!await sourceFile.exists()) {
|
|
Logger.e(TAG, '源图片文件不存在: $sourcePath');
|
|
return sourcePath; // 如果源文件不存在,返回原路径
|
|
}
|
|
|
|
// 创建应用文档目录下的chatImages文件夹
|
|
final appDir = await getApplicationDocumentsDirectory();
|
|
final imagesDir = Directory('${appDir.path}/chatImages');
|
|
if (!await imagesDir.exists()) {
|
|
await imagesDir.create(recursive: true);
|
|
}
|
|
|
|
// 生成唯一文件名
|
|
final fileName =
|
|
'${DateTime.now().millisecondsSinceEpoch}_${sourcePath.split('/').last}';
|
|
final targetPath = '${imagesDir.path}/$fileName';
|
|
|
|
// 复制文件
|
|
await sourceFile.copy(targetPath);
|
|
Logger.i(TAG, '图片已复制到永久存储: $targetPath');
|
|
|
|
return targetPath;
|
|
} catch (e) {
|
|
Logger.e(TAG, '复制图片到永久存储失败: $e');
|
|
return sourcePath; // 出错时返回原路径
|
|
}
|
|
}
|
|
|
|
// 切换输入模式
|
|
void toggleInputMode() async {
|
|
// 如果当前在图片输入模式,先清除
|
|
if (isImageInputActive.value) {
|
|
clearImageInput();
|
|
return;
|
|
}
|
|
|
|
// 如果要切换到语音模式,需要先检查麦克风权限
|
|
if (isTextInputMode.value) {
|
|
// 使用 PermissionUtil 检查麦克风权限
|
|
final hasPermission = await PermissionUtil.instance.requestPermission(
|
|
// context: Get.context!,
|
|
permissionType: Permission.microphone,
|
|
permissionName: '麦克风',
|
|
explanationText: '语音识别与翻译功能需要访问麦克风。',
|
|
permanentDenialText: '您已永久拒绝麦克风权限,请前往系统设置开启权限。',
|
|
settingsButtonText: '去设置',
|
|
cancelButtonText: '取消',
|
|
);
|
|
if (!hasPermission) {
|
|
return; // 权限未获取,保持文本模式
|
|
}
|
|
}
|
|
|
|
isTextInputMode.toggle();
|
|
|
|
// 切换到语音模式时,直接开始语音输入
|
|
if (!isTextInputMode.value) {
|
|
startVoiceInput();
|
|
}
|
|
// 切换到文本模式时,直接停止语音输入
|
|
else {
|
|
stopVoiceInput();
|
|
}
|
|
}
|
|
|
|
// 开始语音输入
|
|
Future<void> startVoiceInput() async {
|
|
if (isListening.value) return;
|
|
|
|
// 检查麦克风权限
|
|
final hasPermission = await PermissionUtil.instance.requestPermission(
|
|
// context: Get.context!,
|
|
permissionType: Permission.microphone,
|
|
permissionName: '麦克风',
|
|
explanationText: '语音识别与翻译功能需要访问麦克风。',
|
|
permanentDenialText: '您已永久拒绝麦克风权限,请前往系统设置开启权限。',
|
|
settingsButtonText: '去设置',
|
|
cancelButtonText: '取消',
|
|
);
|
|
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('请翻译这张图片中的文字');
|
|
}
|
|
|
|
// 获取音乐播放状态
|
|
Future<void> musicStatus() async {
|
|
try {
|
|
var status = await _musiceManager.getStatus();
|
|
isPlaying.value = status['isPlaying'] ?? false;
|
|
currentId.value = status['id'] ?? 0;
|
|
Logger.d('AgentController',
|
|
'音乐状态更新: playing=${isPlaying.value}, url=${currentId.value}');
|
|
} catch (e) {
|
|
Logger.e('AgentController', '获取音乐状态失败: $e');
|
|
}
|
|
}
|
|
|
|
// 播放音乐
|
|
Future<void> musicPlay(Map<String, dynamic> song) async {
|
|
try {
|
|
Logger.d('AgentController', '开始播放音乐: ${song['title'] ?? song['name']}');
|
|
|
|
// 立即更新UI状态,提供即时反馈
|
|
isPlaying.value = true;
|
|
currentId.value = song['id'] ?? 0;
|
|
|
|
// 重置进度状态
|
|
currentPosition.value = 0;
|
|
duration.value = 0;
|
|
progress.value = 0.0;
|
|
|
|
final id = song['id'] ?? "";
|
|
final timestampMs = song['ts'] ?? 0;
|
|
|
|
// 检查URL是否需要刷新(只有当有时间戳时才检查)
|
|
if (timestampMs > 0) {
|
|
final now = DateTime.now();
|
|
final tsTime = DateTime.fromMillisecondsSinceEpoch(timestampMs).add(
|
|
const Duration(minutes: 10), // 转换为北京时间
|
|
);
|
|
|
|
// 判断是否过期
|
|
if (tsTime.isBefore(now)) {
|
|
Logger.d('AgentController', '音乐URL已过期,正在获取新URL...');
|
|
try {
|
|
var response = await Api.getMusicUrl({
|
|
"ids": [id]
|
|
});
|
|
print('getMusicUrl 响应数据: ${response.toString()}'); // 调试输出原始数据
|
|
final urldata = response['urls'][0] as Map<String, dynamic>;
|
|
song["url"] = urldata['url'] ?? '';
|
|
currentId.value = song['id'] ?? 0;
|
|
Logger.d('AgentController', '获取新URL成功');
|
|
} catch (e) {
|
|
Logger.e('AgentController', '获取新URL失败: $e');
|
|
// 如果获取新URL失败,尝试使用原URL
|
|
}
|
|
}
|
|
}
|
|
|
|
// 转换为MusicManager期望的格式
|
|
final musicData = <String, String>{
|
|
'id': song['id']?.toString() ?? '',
|
|
'url': song['url']?.toString() ?? '',
|
|
'title': song['title']?.toString() ?? song['name']?.toString() ?? '',
|
|
'artist':
|
|
song['artist']?.toString() ?? song['sgener']?.toString() ?? '',
|
|
'coverUrl':
|
|
song['coverUrl']?.toString() ?? song['image']?.toString() ?? '',
|
|
};
|
|
|
|
// 调用音乐管理器播放
|
|
await _musiceManager.play(musicData);
|
|
|
|
// 启动进度定时器
|
|
_startProgressTimer();
|
|
|
|
Logger.d('AgentController', '音乐播放启动成功');
|
|
} catch (e) {
|
|
Logger.e('AgentController', '播放音乐失败: $e');
|
|
// 发生错误时重置状态
|
|
isPlaying.value = false;
|
|
currentId.value = 0;
|
|
}
|
|
}
|
|
|
|
// 暂停音乐
|
|
Future<void> musicPause() async {
|
|
try {
|
|
Logger.d('AgentController', '暂停音乐播放');
|
|
await _musiceManager.pause();
|
|
isPlaying.value = false;
|
|
_stopProgressTimer();
|
|
} catch (e) {
|
|
Logger.e('AgentController', '暂停音乐失败: $e');
|
|
}
|
|
}
|
|
|
|
// 恢复音乐播放
|
|
Future<void> musicResume() async {
|
|
try {
|
|
Logger.d('AgentController', '恢复播放音乐');
|
|
await _musiceManager.resume();
|
|
isPlaying.value = true;
|
|
_startProgressTimer();
|
|
Logger.d('AgentController', '音乐恢复播放成功');
|
|
} catch (e) {
|
|
Logger.e('AgentController', '恢复播放音乐失败: $e');
|
|
isPlaying.value = false;
|
|
}
|
|
}
|
|
|
|
// 启动进度更新定时器
|
|
void _startProgressTimer() {
|
|
_stopProgressTimer(); // 先停止现有定时器
|
|
|
|
Logger.d('AgentController', '启动播放进度定时器');
|
|
_progressTimer = Timer.periodic(const Duration(milliseconds: 500), (timer) {
|
|
if (isPlaying.value && currentId.value != 0) {
|
|
updateMusicProgress();
|
|
} else if (!isPlaying.value) {
|
|
// 如果不在播放状态,停止定时器
|
|
timer.cancel();
|
|
_progressTimer = null;
|
|
Logger.d('AgentController', '播放已停止,取消进度定时器');
|
|
}
|
|
});
|
|
}
|
|
|
|
// 停止进度更新定时器
|
|
void _stopProgressTimer() {
|
|
if (_progressTimer != null) {
|
|
Logger.d('AgentController', '停止播放进度定时器');
|
|
_progressTimer?.cancel();
|
|
_progressTimer = null;
|
|
}
|
|
}
|
|
|
|
// 获取播放进度
|
|
Future<void> updateMusicProgress() async {
|
|
try {
|
|
if (!isPlaying.value || currentId.value == 0) {
|
|
return; // 如果没有在播放,直接返回
|
|
}
|
|
|
|
var progressInfo = await _musiceManager.getProgress();
|
|
final newPosition = progressInfo['currentPosition'] ?? 0;
|
|
final newDuration = progressInfo['duration'] ?? 0;
|
|
final newProgress = progressInfo['progress'] ?? 0.0;
|
|
|
|
// 只有当值发生变化时才更新,避免不必要的UI刷新
|
|
if (currentPosition.value != newPosition) {
|
|
currentPosition.value = newPosition;
|
|
}
|
|
if (duration.value != newDuration) {
|
|
duration.value = newDuration;
|
|
}
|
|
if ((progress.value - newProgress).abs() > 0.001) {
|
|
// 避免浮点数精度问题
|
|
progress.value = newProgress;
|
|
}
|
|
} catch (e) {
|
|
Logger.e('AgentController', '获取播放进度失败: $e');
|
|
}
|
|
}
|
|
|
|
// 跳转到指定位置
|
|
Future<void> seekToPosition(int positionMs) async {
|
|
try {
|
|
await _musiceManager.seekTo(positionMs);
|
|
// 立即更新进度状态
|
|
await updateMusicProgress();
|
|
} catch (e) {
|
|
Logger.e('AgentController', '跳转播放位置失败: $e');
|
|
}
|
|
}
|
|
|
|
// 修改自动停止处理,遵循TTS设置
|
|
void _handleAutoStop(int index) {
|
|
// 发送此消息到AI处理
|
|
isProcessing.value = true;
|
|
_isNewAssistantResponse = true;
|
|
AgentService.processTextInput(messages[index].text,
|
|
speakResponse: isTtsEnabled.value);
|
|
}
|
|
|
|
//去导航
|
|
void openNavigation(String start, String end, String ntype) {
|
|
_navigationManager.open(start, end, ntype);
|
|
}
|
|
|
|
// 打开图片查看器
|
|
void openImageViewer(BuildContext context, String imagePath) {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (context) => Scaffold(
|
|
appBar: AppBar(
|
|
backgroundColor: Colors.black,
|
|
iconTheme: IconThemeData(color: Colors.white),
|
|
),
|
|
body: Container(
|
|
color: Colors.black,
|
|
child: PhotoView(
|
|
imageProvider: FileImage(File(imagePath)),
|
|
minScale: PhotoViewComputedScale.contained,
|
|
maxScale: PhotoViewComputedScale.covered * 2,
|
|
backgroundDecoration: BoxDecoration(color: Colors.black),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// 播放音乐列表
|
|
Future<void> playPlaylist(List<Map<String, dynamic>> playlist) async {
|
|
try {
|
|
if (playlist.isEmpty) {
|
|
Logger.e(TAG, '播放列表为空,无法播放');
|
|
return;
|
|
}
|
|
|
|
Logger.d(TAG, '开始播放音乐列表,共${playlist.length}首歌曲');
|
|
|
|
// 设置加载状态
|
|
isLoadingMusic.value = true;
|
|
loadingMusicId.value = playlist[0]['id']?.toString() ?? '';
|
|
|
|
// 重置播放状态
|
|
isPlaying.value = true;
|
|
currentPosition.value = 0;
|
|
duration.value = 0;
|
|
progress.value = 0.0;
|
|
|
|
// 格式化歌曲列表数据
|
|
final formattedPlaylist = playlist.map((song) {
|
|
return {
|
|
'id': song['id']?.toString() ?? '',
|
|
'url': song['url']?.toString() ?? '',
|
|
'title': song['title']?.toString() ?? song['name']?.toString() ?? '',
|
|
'artist': song['artist']?.toString() ?? song['sgener']?.toString() ?? '',
|
|
'coverUrl': song['coverUrl']?.toString() ?? song['image']?.toString() ?? '',
|
|
};
|
|
}).toList();
|
|
|
|
// 调用音乐管理器播放列表
|
|
await _musiceManager.playPlaylist(formattedPlaylist);
|
|
|
|
// 更新当前播放歌曲ID
|
|
if (formattedPlaylist.isNotEmpty) {
|
|
currentId.value = int.tryParse(formattedPlaylist[0]['id'] ?? '0') ?? 0;
|
|
}
|
|
|
|
// 启动进度定时器
|
|
_startProgressTimer();
|
|
|
|
Logger.d(TAG, '音乐列表播放启动成功');
|
|
} catch (e) {
|
|
Logger.e(TAG, '播放音乐列表失败: $e');
|
|
isPlaying.value = false;
|
|
currentId.value = 0;
|
|
|
|
// 显示错误提示
|
|
Get.snackbar(
|
|
'播放失败',
|
|
'无法播放音乐列表,请稍后重试',
|
|
snackPosition: SnackPosition.BOTTOM,
|
|
backgroundColor: Colors.red[400],
|
|
colorText: Colors.white,
|
|
duration: const Duration(seconds: 2),
|
|
);
|
|
} finally {
|
|
// 清除加载状态
|
|
isLoadingMusic.value = false;
|
|
loadingMusicId.value = '';
|
|
}
|
|
}
|
|
}
|
|
|