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.
409 lines
12 KiB
409 lines
12 KiB
import 'dart:async';
|
|
import 'dart:convert';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:agent_service/agent_service.dart';
|
|
import 'package:logger/logger.dart';
|
|
import 'package:chat_storage/chat_storage.dart';
|
|
|
|
class Message {
|
|
final bool isUser;
|
|
final String text;
|
|
final DateTime timestamp;
|
|
final bool isRecognizing; // 是否为语音识别中的临时消息
|
|
|
|
Message({
|
|
required this.isUser,
|
|
required this.text,
|
|
DateTime? timestamp,
|
|
this.isRecognizing = false,
|
|
}) : this.timestamp = timestamp ?? DateTime.now();
|
|
}
|
|
|
|
class AgentController extends GetxController {
|
|
static AgentController get to => Get.find();
|
|
|
|
final logger = Logger();
|
|
|
|
// 聊天消息列表
|
|
final messages = <Message>[].obs;
|
|
|
|
// 控制器
|
|
final TextEditingController textController = TextEditingController();
|
|
final ScrollController scrollController = ScrollController();
|
|
|
|
// 状态标志
|
|
final isInitialized = false.obs;
|
|
final isListening = false.obs;
|
|
final isSpeaking = false.obs;
|
|
final isProcessing = false.obs;
|
|
|
|
// 输入模式控制
|
|
final isTextInputMode = true.obs;
|
|
|
|
// 当前输入的文本内容
|
|
final currentText = ''.obs;
|
|
|
|
// 流事件订阅
|
|
StreamSubscription<AgentServiceEvent>? _eventSubscription;
|
|
|
|
// 用于跟踪AI回复状态
|
|
bool _isNewAssistantResponse = true;
|
|
String _currentResponseId = '';
|
|
|
|
// 会话ID
|
|
final String sessionId = 'default_agent';
|
|
|
|
// 是否已加载聊天记录
|
|
final isHistoryLoaded = false.obs;
|
|
|
|
@override
|
|
void onInit() {
|
|
super.onInit();
|
|
_loadChatHistory();
|
|
_subscribeToEvents();
|
|
}
|
|
|
|
@override
|
|
void onClose() {
|
|
AgentService.stopConversation();
|
|
AgentService.stopTts();
|
|
textController.dispose();
|
|
scrollController.dispose();
|
|
_eventSubscription?.cancel();
|
|
super.onClose();
|
|
}
|
|
|
|
// 加载聊天历史记录
|
|
Future<void> _loadChatHistory() async {
|
|
try {
|
|
logger.i('正在加载聊天历史...');
|
|
final result = await ChatStorage.getMessages(
|
|
sessionId: sessionId,
|
|
page: 1,
|
|
pageSize: 20,
|
|
);
|
|
|
|
if (result.messages.isNotEmpty) {
|
|
// 转换并添加消息到列表
|
|
final historyMessages = result.messages.map((msg) => Message(
|
|
isUser: msg.sender == 'user',
|
|
text: msg.message,
|
|
timestamp: DateTime.parse(msg.timestamp),
|
|
)).toList();
|
|
|
|
// 消息按时间排序,最新的消息在底部
|
|
historyMessages.sort((a, b) => a.timestamp.compareTo(b.timestamp));
|
|
messages.assignAll(historyMessages);
|
|
|
|
logger.i('成功加载 ${historyMessages.length} 条聊天记录');
|
|
} else {
|
|
// 没有历史记录时添加欢迎消息
|
|
_addWelcomeMessage();
|
|
logger.i('没有历史记录,添加欢迎消息');
|
|
}
|
|
} catch (e) {
|
|
logger.e('加载聊天历史失败: $e');
|
|
_addWelcomeMessage();
|
|
} finally {
|
|
isHistoryLoaded.value = true;
|
|
|
|
messages.refresh();
|
|
// 在加载完成后直接定位到底部,无需动画
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
scrollController.jumpTo(scrollController.position.maxScrollExtent);
|
|
});
|
|
|
|
}
|
|
}
|
|
|
|
// 添加欢迎消息
|
|
void _addWelcomeMessage() {
|
|
messages.add(Message(
|
|
isUser: false,
|
|
text: '你好!我是DeepVoice AI助手,我能帮你:\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();
|
|
_scrollToBottom();
|
|
}
|
|
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,
|
|
timestamp: messages[index].timestamp,
|
|
);
|
|
messages.refresh();
|
|
} else {
|
|
// 如果没有找到识别中的消息,添加新消息
|
|
messages.add(Message(isUser: true, text: text));
|
|
}
|
|
|
|
isProcessing.value = true;
|
|
// 标记为新的AI回复
|
|
_isNewAssistantResponse = true;
|
|
// 滚动到底部
|
|
_scrollToBottom();
|
|
} else if (index >= 0) {
|
|
// 如果识别结果为空,移除临时的识别消息
|
|
messages.removeAt(index);
|
|
messages.refresh();
|
|
}
|
|
break;
|
|
|
|
case AgentServiceEventType.ttsStarted:
|
|
isSpeaking.value = true;
|
|
break;
|
|
|
|
case AgentServiceEventType.ttsCompleted:
|
|
case AgentServiceEventType.ttsStopped:
|
|
case AgentServiceEventType.ttsCanceled:
|
|
isSpeaking.value = false;
|
|
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,
|
|
timestamp: messages[lastIndex].timestamp,
|
|
);
|
|
}
|
|
}
|
|
// 滚动到底部
|
|
_scrollToBottom();
|
|
}
|
|
break;
|
|
|
|
case AgentServiceEventType.assistantResponse:
|
|
isProcessing.value = false;
|
|
// 标记当前回复完成
|
|
_isNewAssistantResponse = true;
|
|
break;
|
|
|
|
case AgentServiceEventType.error:
|
|
isListening.value = false;
|
|
isSpeaking.value = false;
|
|
isProcessing.value = false;
|
|
|
|
// 移除临时的识别消息
|
|
final index = messages.lastIndexWhere((msg) => msg.isRecognizing && msg.isUser);
|
|
if (index >= 0) {
|
|
messages.removeAt(index);
|
|
messages.refresh();
|
|
}
|
|
|
|
logger.e('代理服务错误: ${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;
|
|
AgentService.processTextInput(messages[index].text, speakResponse: true);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case AgentServiceEventType.functionCall:
|
|
// final functionName = event.data['name'] ?? '';
|
|
// final arguments = event.data['arguments'] ?? '';
|
|
// logger.i('mcp: $functionName, $arguments');
|
|
break;
|
|
|
|
case AgentServiceEventType.functionCallResult:
|
|
final result = event.data['result'] ?? '';
|
|
final functionCall = event.data['function_call'] ?? '';
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
});
|
|
}
|
|
|
|
// 滚动到底部
|
|
void _scrollToBottom() {
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
if (scrollController.hasClients) {
|
|
try {
|
|
scrollController.animateTo(
|
|
scrollController.position.maxScrollExtent,
|
|
duration: Duration(milliseconds: 300),
|
|
curve: Curves.easeOut,
|
|
);
|
|
} catch (e) {
|
|
logger.e('滚动到底部失败: $e');
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// 发送文本消息
|
|
Future<void> sendTextMessage() async {
|
|
final text = textController.text.trim();
|
|
if (text.isEmpty) return;
|
|
|
|
final message = Message(isUser: true, text: text);
|
|
messages.add(message);
|
|
textController.clear();
|
|
|
|
// 滚动到底部
|
|
_scrollToBottom();
|
|
|
|
try {
|
|
isProcessing.value = true;
|
|
// 标记为新的AI回复
|
|
_isNewAssistantResponse = true;
|
|
await AgentService.processTextInput(text, speakResponse: true);
|
|
} catch (e) {
|
|
logger.e('发送文本消息失败: $e');
|
|
isProcessing.value = false;
|
|
}
|
|
}
|
|
|
|
// 开始语音输入
|
|
Future<void> startVoiceInput() async {
|
|
if (isListening.value) return;
|
|
|
|
try {
|
|
await AgentService.startConversation();
|
|
} catch (e) {
|
|
logger.e('启动语音输入失败: $e');
|
|
}
|
|
}
|
|
|
|
// 停止语音输入
|
|
Future<void> stopVoiceInput() async {
|
|
if (!isListening.value) return;
|
|
|
|
try {
|
|
await AgentService.stopConversation();
|
|
} catch (e) {
|
|
logger.e('停止语音输入失败: $e');
|
|
}
|
|
}
|
|
|
|
// 清除聊天历史
|
|
Future<void> clearChat() async {
|
|
try {
|
|
await AgentService.clearChatHistory();
|
|
|
|
|
|
// 清空本地消息列表
|
|
messages.clear();
|
|
|
|
// 添加欢迎消息
|
|
_addWelcomeMessage();
|
|
|
|
// 重置回复状态
|
|
_isNewAssistantResponse = true;
|
|
} catch (e) {
|
|
logger.e('清除聊天历史失败: $e');
|
|
}
|
|
}
|
|
|
|
// 切换输入模式
|
|
void toggleInputMode() {
|
|
isTextInputMode.toggle();
|
|
|
|
// 切换到语音模式时,直接开始语音输入
|
|
if (!isTextInputMode.value) {
|
|
startVoiceInput();
|
|
}
|
|
// 切换到文本模式时,直接停止语音输入
|
|
else {
|
|
stopVoiceInput();
|
|
}
|
|
}
|
|
}
|