From 6ffb3333cb1471b2674008e204a0a621ebc2ab9a Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Fri, 10 Oct 2025 19:31:41 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=8A=E4=BC=A0ai=E4=BB=A3=E7=A0=81=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/data/models/message_model.dart | 23 ++ .../agent/controllers/agent_controller.dart | 197 +++++++++----- lib/modules/agent/views/agent_view.dart | 8 + lib/modules/agent/views/message_bubble.dart | 236 ++++++++++++++-- .../agent_service/AgentService.kt | 84 +++--- .../agent_service/AgentServicePlugin.kt | 12 + .../yunqiinnovation/agent_service/BleAgent.kt | 4 +- .../agent_service/AgentServiceImpl.swift | 45 ++-- .../agent_service/AgentServicePlugin.swift | 6 +- .../agent_service/lib/agent_service.dart | 7 +- .../azure_speech/AzureAsrHelper.kt | 41 +-- .../Sources/azure_speech/AzureAsrHelper.swift | 28 +- .../azure_speech/AzureSpeechPlugin.swift | 12 +- .../chat_api/ChatApiService.kt | 253 ++++++++++-------- .../chat_api/CustomSseClientTransport.kt | 16 +- .../chat_storage/ChatStorageHelper.kt | 52 ++-- .../chat_storage/ChatStoragePlugin.kt | 14 +- .../chat_storage/ChatStorageHelper.swift | 33 +-- .../chat_storage/SwiftChatStoragePlugin.swift | 12 +- .../chat_storage/lib/chat_storage.dart | 24 +- 20 files changed, 761 insertions(+), 346 deletions(-) diff --git a/lib/data/models/message_model.dart b/lib/data/models/message_model.dart index 09744a939..e2c534200 100644 --- a/lib/data/models/message_model.dart +++ b/lib/data/models/message_model.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:path_provider/path_provider.dart'; class Message { + final String sessionid; // 会话ID final bool isUser; //标识信息是否来自用户还是AI final String text; //信息文本内容 final DateTime timestamp; // 消息时间戳 @@ -10,9 +11,12 @@ class Message { final bool hasImage; // 是否包含图片 final String? imagePath; // 图片路径 final Map? meta; //卡片数据 + final List? functionCalls; // 函数调用 + bool isToolCallsExpanded; // 工具调用是否展开 Message({ // 构造函数 + required this.sessionid, // 会话ID required this.isUser, required this.text, DateTime? timestamp, @@ -20,11 +24,14 @@ class Message { this.hasImage = false, this.imagePath, this.meta, + this.functionCalls, + this.isToolCallsExpanded = false, // 默认不展开 }) : this.timestamp = timestamp ?? DateTime.now(); // 从JSON构造函数 factory Message.fromJson(Map json) { return Message( + sessionid: json['sessionid'] as String, // 会话ID isUser: json['isUser'] as bool, text: json['text'] as String, timestamp: DateTime.parse(json['timestamp'] as String), @@ -32,6 +39,8 @@ class Message { hasImage: json['hasImage'] as bool? ?? false, imagePath: json['imagePath'] as String?, meta: json['meta'] as Map?, + functionCalls: json['functionCalls'] as List?, + isToolCallsExpanded: json['isToolCallsExpanded'] as bool? ?? false, ); } @@ -45,12 +54,15 @@ class Message { 'hasImage': hasImage, 'imagePath': imagePath, 'meta': meta, + 'functionCalls': functionCalls, + 'isToolCallsExpanded': isToolCallsExpanded, }; } // 创建一个加载中的消息 factory Message.loading() { return Message( + sessionid: '', // 会话ID isUser: false, text: '', timestamp: DateTime.now(), @@ -60,6 +72,7 @@ class Message { // 复制并修改 Message copyWith({ + required String sessionid, // 会话ID bool? isUser, String? text, DateTime? timestamp, @@ -67,8 +80,11 @@ class Message { bool? hasImage, String? imagePath, Map? meta, + List? functionCalls, + bool? isToolCallsExpanded, }) { return Message( + sessionid: sessionid, // 会话ID isUser: isUser ?? this.isUser, text: text ?? this.text, timestamp: timestamp ?? this.timestamp, @@ -76,6 +92,13 @@ class Message { hasImage: hasImage ?? this.hasImage, imagePath: imagePath ?? this.imagePath, meta: meta ?? this.meta, + functionCalls: functionCalls ?? this.functionCalls, + isToolCallsExpanded: isToolCallsExpanded ?? this.isToolCallsExpanded, ); } + + // 切换工具调用展开状态 + void toggleToolCallsExpanded() { + isToolCallsExpanded = !isToolCallsExpanded; + } } diff --git a/lib/modules/agent/controllers/agent_controller.dart b/lib/modules/agent/controllers/agent_controller.dart index 6119f49ef..c11722a84 100644 --- a/lib/modules/agent/controllers/agent_controller.dart +++ b/lib/modules/agent/controllers/agent_controller.dart @@ -1,25 +1,21 @@ import 'dart:async'; import 'dart:convert'; -import 'dart:ffi'; import 'dart:io'; import 'package:amap_service/amap_service.dart'; -import 'package:http/http.dart' as http; import 'package:package_info_plus/package_info_plus.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'package:uuid/uuid.dart'; import 'package:voitrans/data/models/appconfig.dart'; import 'package:voitrans/data/models/song_model.dart'; import 'package:voitrans/data/services/ble_manager.dart'; import 'package:voitrans/data/services/bluetooth_manager.dart'; import 'package:voitrans/data/services/qq_music.dart'; import 'package:flutter_intro/flutter_intro.dart'; -import '../../../data/services/ble_manager.dart'; -import '../../../data/services/bluetooth_manager.dart'; import '../../../data/services/novice_guide_service.dart'; import '/core/utils/upload_oss.dart'; import '/data/models/user_Info.dart'; import '/data/services/music_manager.dart'; import '/data/services/navigation_manager.dart'; -import '/data/services/network/api.dart'; import 'package:ble_service/ble_service.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; @@ -46,6 +42,7 @@ class AgentController extends GetxController with WidgetsBindingObserver { // 聊天消息列表 final messages = [].obs; + final currsessionid = ''.obs; // UI控制器 final TextEditingController textController = TextEditingController(); @@ -138,7 +135,7 @@ class AgentController extends GetxController with WidgetsBindingObserver { final TAG = 'AgentController'; var AppDir = "".obs; // 会话ID - final String sessionId = 'default_agent'; + final String agentId = 'default_agent'; // 分页加载相关状态 final isLoadingMore = false.obs; // 是否正在加载更多消息 @@ -312,7 +309,7 @@ class AgentController extends GetxController with WidgetsBindingObserver { Logger.i(TAG, '正在加载聊天历史...'); //通过本地插件ChatStorage获取聊天记录 final result = await ChatStorage.getMessages( - sessionId: sessionId, //会话ID + agentId: agentId, //会话ID page: 1, //从第一页开始 pageSize: pageSize, //每页20条记录 ); @@ -367,7 +364,7 @@ class AgentController extends GetxController with WidgetsBindingObserver { Logger.i(TAG, '正在加载第 $nextPage 页历史消息...'); final result = await ChatStorage.getMessages( - sessionId: sessionId, + agentId: agentId, page: nextPage, pageSize: pageSize, ); @@ -421,6 +418,7 @@ class AgentController extends GetxController with WidgetsBindingObserver { : null; return Message( + sessionid: msg.sessionId, // 会话ID isUser: msg.sender == 'user', text: msg.message, meta: meta, @@ -488,6 +486,7 @@ class AgentController extends GetxController with WidgetsBindingObserver { // 添加初始欢迎消息 void _addWelcomeMessage() { messages.add(Message( + sessionid: "", // 会话ID isUser: false, text: 'welcomeMessage'.tr, // 你好!我是**小言**,我能帮你: )); @@ -504,6 +503,7 @@ class AgentController extends GetxController with WidgetsBindingObserver { // isTextInputMode.refresh(); // 只标记识别开始,不添加消息 Logger.i(TAG, '识别开始'); + break; case AgentServiceEventType.recognitionStopped: isListening.value = false; @@ -527,25 +527,29 @@ class AgentController extends GetxController with WidgetsBindingObserver { break; case AgentServiceEventType.recognizing: + final sessionid = event.data['sessionid'] ?? ''; final text = event.data['text'] ?? ''; currentText.value = text; // 保留当前文本,以便其他地方使用 Logger.i(TAG, '识别中间结果: $text'); if (text.isNotEmpty) { // 查找是否有正在识别中的消息 - final index = messages - .lastIndexWhere((msg) => msg.isRecognizing && msg.isUser); + final index = messages.lastIndexWhere( + (msg) => msg.sessionid == sessionid && msg.isUser); if (index >= 0) { // 更新已有的识别中消息 messages[index] = Message( + sessionid: sessionid, // 会话ID isUser: true, text: text, isRecognizing: true, timestamp: messages[index].timestamp, ); } else { + currsessionid.value = event.data['sessionid'] ?? ''; // 没有识别中的消息,创建新消息 messages.add(Message( + sessionid: sessionid, // 会话ID isUser: true, text: text, isRecognizing: true, @@ -556,26 +560,33 @@ class AgentController extends GetxController with WidgetsBindingObserver { break; case AgentServiceEventType.recognitionResult: + final sessionid = event.data['sessionid'] ?? ''; final text = event.data['text'] ?? ''; Logger.i(TAG, '识别最终结果: $text'); // 查找是否有正在识别中的消息 - final index = - messages.lastIndexWhere((msg) => msg.isRecognizing && msg.isUser); + final index = messages.lastIndexWhere( + (msg) => msg.sessionid == sessionid && msg.isUser); if (text.isNotEmpty) { if (index >= 0) { // 将识别中的消息转换为最终消息 messages[index] = Message( + sessionid: sessionid, isUser: true, text: text, isRecognizing: false, timestamp: messages[index].timestamp, + functionCalls: messages[index].functionCalls, + isToolCallsExpanded: messages[index].isToolCallsExpanded, ); // messages.refresh(); } else { // 如果没有找到识别中的消息,添加新消息 - messages - .add(Message(isUser: true, text: text, isRecognizing: false)); + messages.add(Message( + sessionid: sessionid, + isUser: true, + text: text, + isRecognizing: false)); } isProcessing.value = true; @@ -590,24 +601,31 @@ class AgentController extends GetxController with WidgetsBindingObserver { break; //识别取消(网络波动会触发) case AgentServiceEventType.recognitionCanceled: + final sessionid = event.data['sessionid'] ?? ''; // 查找是否有正在识别中的消息 - final index = - messages.lastIndexWhere((msg) => msg.isRecognizing && msg.isUser); + final index = messages.lastIndexWhere( + (msg) => msg.sessionid == sessionid && msg.isUser); if (currentText.value.isNotEmpty) { if (index >= 0) { // 将识别中的消息转换为最终消息 messages[index] = Message( + sessionid: sessionid, // 会话ID isUser: true, text: currentText.value, isRecognizing: false, timestamp: messages[index].timestamp, + functionCalls: messages[index].functionCalls, + isToolCallsExpanded: messages[index].isToolCallsExpanded, ); // messages.refresh(); } else { // 如果没有找到识别中的消息,添加新消息 messages.add(Message( - isUser: true, text: currentText.value, isRecognizing: false)); + sessionid: sessionid, + isUser: true, + text: currentText.value, + isRecognizing: false)); } isProcessing.value = true; @@ -654,29 +672,27 @@ class AgentController extends GetxController with WidgetsBindingObserver { case AgentServiceEventType.assistantToken: if (!isProcessing.value) isProcessing.value = true; - + final sessionid = event.data['sessionid'] ?? ''; final token = event.data['token'] ?? ''; - final responseId = event.data['responseId'] ?? ''; // Logger.i(TAG, 'AI回复Token: $token, responseId: $responseId'); if (token.isNotEmpty) { // 如果是新的回复或者响应ID改变,创建新消息 - if (_isNewAssistantResponse || - (_currentResponseId != responseId && responseId.isNotEmpty)) { - messages.add(Message(isUser: false, text: token)); + final index = messages.lastIndexWhere( + (msg) => msg.sessionid == sessionid && !msg.isUser); + if (index == -1) { + messages.add( + Message(sessionid: sessionid, 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, - ); - } + messages[index] = Message( + sessionid: sessionid, + isUser: false, + text: messages[index].text + token, + meta: messages[index].meta, + timestamp: messages[index].timestamp, + functionCalls: messages[index].functionCalls, + isToolCallsExpanded: messages[index].isToolCallsExpanded, + ); } // 使用reverse:true时通常不需要手动滚动 } @@ -713,10 +729,10 @@ class AgentController extends GetxController with WidgetsBindingObserver { case AgentServiceEventType.autoStop: isListening.value = false; - + final sessionid = event.data['sessionid'] ?? ''; // 移除临时的识别消息 - final index = - messages.lastIndexWhere((msg) => msg.isRecognizing && msg.isUser); + final index = messages.lastIndexWhere( + (msg) => msg.sessionid == sessionid && msg.isUser); if (index >= 0) { // 只有当消息为空或文本太短时才移除 if (messages[index].text.trim().length < 2) { @@ -725,21 +741,25 @@ class AgentController extends GetxController with WidgetsBindingObserver { } else { // 否则转换为正式消息 messages[index] = Message( + sessionid: sessionid, isUser: true, text: messages[index].text, timestamp: messages[index].timestamp, + functionCalls: messages[index].functionCalls, + isToolCallsExpanded: messages[index].isToolCallsExpanded, ); // messages.refresh(); // 发送此消息到AI处理 isProcessing.value = true; _isNewAssistantResponse = true; - _handleAutoStop(index); + _handleAutoStop(sessionid, index); } } break; case AgentServiceEventType.functionCall: + final sessionid = event.data['sessionid'] ?? ''; final functionName = event.data['name'] ?? ''; final arguments = event.data['arguments'] ?? ''; // debugPrint('Fultter 开始调用 mcp: $functionName, $arguments'); @@ -748,15 +768,39 @@ class AgentController extends GetxController with WidgetsBindingObserver { endPhoneCall(); //退出语音交互,同时退出打电话模式 } } - + final index = messages.lastIndexWhere( + (msg) => msg.sessionid == sessionid && !msg.isUser); + if (index == -1) { + messages.add(Message( + sessionid: sessionid, + isUser: false, + text: '', + functionCalls: [functionName])); + _isNewAssistantResponse = false; + } else { + // 否则更新最后一条消息 + final lastIndex = messages.length - 1; + + if (lastIndex >= 0 && !messages[lastIndex].isUser) { + messages[lastIndex] = Message( + sessionid: sessionid, + isUser: false, + text: messages[lastIndex].text, + meta: messages[lastIndex].meta, + timestamp: messages[lastIndex].timestamp, + functionCalls: messages[lastIndex].functionCalls ?? [] + ..add(functionName), + ); + } + } break; case AgentServiceEventType.functionCallResult: - final responseId = event.data['responseId'] ?? ''; + final sessionid = event.data['sessionid'] ?? ''; // final result = event.data['result']; final metaStr = event.data['meta'] ?? ''; final functionCall = event.data['function_call'] ?? ''; - Logger.i(TAG, 'Flutter 结束调用 mcp: $metaStr'); + Logger.i(TAG, 'Flutter 结束调用 $sessionid mcp: $metaStr'); if (metaStr.isNotEmpty) { Map metaResult = {}; metaResult = jsonDecode(metaStr) as Map; @@ -771,25 +815,31 @@ class AgentController extends GetxController with WidgetsBindingObserver { //不回复直接返回 break; } + final index = messages.lastIndexWhere( + (msg) => msg.sessionid == sessionid && !msg.isUser); + Logger.i(TAG, 'Flutter 结束调用 卡片数据 $index $metaResult'); // 判断是否为新的回复或响应ID是否改变 - if (_isNewAssistantResponse || _currentResponseId != responseId) { + if (index == -1) { // 添加新消息 - messages - .add(Message(isUser: false, text: "", meta: metaResult)); + messages.add(Message( + sessionid: sessionid, + 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, - ); - } + messages[index] = Message( + sessionid: sessionid, + isUser: false, + text: messages[index].text, + meta: metaResult, + timestamp: messages[index].timestamp, + functionCalls: messages[index].functionCalls, + isToolCallsExpanded: messages[index].isToolCallsExpanded, + ); } + messages.refresh(); if (metaResult.containsKey("maps_route_navigation")) { final maps_route_navigation = metaResult["maps_route_navigation"]; @@ -847,6 +897,9 @@ class AgentController extends GetxController with WidgetsBindingObserver { } } } + } else { + // 其他函数调用 + Logger.i(TAG, 'Flutter 结束调用 解析失败 $metaStr'); } } if (functionCall.contains("music")) { @@ -893,6 +946,7 @@ class AgentController extends GetxController with WidgetsBindingObserver { // ✅ 创建新的 Message 替换原来的 messages[index] = Message( + sessionid: result.sessionid, isUser: result.isUser, text: result.text, meta: newMeta, @@ -1002,10 +1056,13 @@ class AgentController extends GetxController with WidgetsBindingObserver { // 标记为新的AI回复 _isNewAssistantResponse = true; newAIReplyCompleted.value = false; + currsessionid.value = const Uuid().v4(); + final sessionid = currsessionid.value; // 根据TTS状态决定是否朗读 - await AgentService.processTextInput(text, + AgentService.processTextInput(sessionid, text, speakResponse: isTtsEnabled.value); - final message = Message(isUser: true, text: text); + final message = Message(sessionid: sessionid, isUser: true, text: text); + Logger.i(TAG, '发送AI消息:$sessionid $text'); messages.add(message); textController.clear(); } catch (e) { @@ -1029,12 +1086,17 @@ class AgentController extends GetxController with WidgetsBindingObserver { var ImageUrl = await UploadOss.upload( filepath: permanentImagePath, rootDir: 'User/${User.instance.uid}/ChatImages'); - Logger.i(TAG, '图片上传: $permanentImagePath URL: $ImageUrl'); + // 显示的文本,如果没有提供则使用默认值 final displayText = text?.isNotEmpty == true ? text! : '[图片]'; + currsessionid.value = const Uuid().v4(); + final sessionid = currsessionid.value; + Logger.i(TAG, + '发送AI图片消息:$sessionid $displayText $permanentImagePath URL: $ImageUrl'); // 添加用户消息 final message = Message( + sessionid: sessionid, isUser: true, text: displayText, hasImage: true, @@ -1055,9 +1117,11 @@ class AgentController extends GetxController with WidgetsBindingObserver { // 保存消息到持久化存储 //await _saveMessageToStorage(message); - + currsessionid.value = const Uuid().v4(); + final sessionid = currsessionid.value; // 调用Agent Service处理图片,考虑TTS状态 await AgentService.processImageInput( + sessionid, relativeImagePath, ImageUrl, text: text ?? '', @@ -1855,11 +1919,13 @@ class AgentController extends GetxController with WidgetsBindingObserver { } // 修改自动停止处理,遵循TTS设置 - void _handleAutoStop(int index) { + void _handleAutoStop(String sessionid, int index) { // 发送此消息到AI处理 isProcessing.value = true; + // 标记为新的AI回复 _isNewAssistantResponse = true; - AgentService.processTextInput(messages[index].text, + currsessionid.value = sessionid; + AgentService.processTextInput(sessionid, messages[index].text, speakResponse: isTtsEnabled.value); } @@ -2612,4 +2678,15 @@ class AgentController extends GetxController with WidgetsBindingObserver { Get.snackbar('错误', '打开苹果地图时发生错误'); } } + + Future updateToolCallsExpanded( + String sessionid, + bool isExpanded, + ) async { + final message = messages.firstWhere( + (msg) => msg.sessionid == sessionid && msg.isUser == false, + ); + message.isToolCallsExpanded = isExpanded; + messages.refresh(); + } } diff --git a/lib/modules/agent/views/agent_view.dart b/lib/modules/agent/views/agent_view.dart index c9c456491..3f4dd0dd1 100644 --- a/lib/modules/agent/views/agent_view.dart +++ b/lib/modules/agent/views/agent_view.dart @@ -287,6 +287,7 @@ class _AgentViewState extends State with WidgetsBindingObserver { isDarkMode, details, message); }, child: MessageBubble( + sessionid: message.sessionid, isUser: message.isUser, message: message.text, timestamp: message.timestamp, @@ -294,6 +295,9 @@ class _AgentViewState extends State with WidgetsBindingObserver { hasImage: message.hasImage, imagePath: message.imagePath, meta: message.meta, + functionCalls: message.functionCalls, + isToolCallsExpanded: + message.isToolCallsExpanded, ), ), ), @@ -307,6 +311,7 @@ class _AgentViewState extends State with WidgetsBindingObserver { context, isDarkMode, details, message); }, child: MessageBubble( + sessionid: message.sessionid, isUser: message.isUser, message: message.text, timestamp: message.timestamp, @@ -314,6 +319,9 @@ class _AgentViewState extends State with WidgetsBindingObserver { hasImage: message.hasImage, imagePath: message.imagePath, meta: message.meta, + functionCalls: message.functionCalls, + isToolCallsExpanded: + message.isToolCallsExpanded, ), ), ); diff --git a/lib/modules/agent/views/message_bubble.dart b/lib/modules/agent/views/message_bubble.dart index f5b24bd04..fed515b23 100644 --- a/lib/modules/agent/views/message_bubble.dart +++ b/lib/modules/agent/views/message_bubble.dart @@ -18,6 +18,7 @@ import '../controllers/agent_controller.dart'; /// 消息气泡组件 class MessageBubble extends GetView { + final String sessionid; final bool isUser; final String message; final DateTime timestamp; @@ -25,9 +26,11 @@ class MessageBubble extends GetView { final bool hasImage; final String? imagePath; final Map? meta; - + final List? functionCalls; + final bool isToolCallsExpanded; const MessageBubble({ Key? key, + required this.sessionid, required this.isUser, required this.message, required this.timestamp, @@ -35,6 +38,8 @@ class MessageBubble extends GetView { this.hasImage = false, this.imagePath, this.meta, + this.functionCalls, + this.isToolCallsExpanded = false, }) : super(key: key); @override @@ -75,7 +80,24 @@ class MessageBubble extends GetView { ), ], ), - child: _buildMessageContent(context), + child: Stack( + children: [ + // 主要消息内容 + Padding( + padding: (!isUser && + functionCalls != null && + functionCalls!.isNotEmpty) + ? const EdgeInsets.only(top: 20) + : EdgeInsets.zero, + child: _buildMessageContent(context), + ), + //工具调用流程显示(仅在AI消息且有functionCalls时显示) + if (!isUser && + functionCalls != null && + functionCalls!.isNotEmpty) + _buildFunctionCallsWidget(context), + ], + ), ), const SizedBox(height: 4), // 识别中的消息不显示时间 @@ -89,7 +111,7 @@ class MessageBubble extends GetView { Text( _formatTimestamp(timestamp), style: TextStyle( - fontSize: 10, + fontSize: 14, color: Theme.of(context).brightness == Brightness.dark ? Colors.grey[400] : Colors.grey[500], @@ -160,10 +182,21 @@ class MessageBubble extends GetView { ); } - if (!isUser && message.isEmpty && meta == null) { + if (!isUser && message.isEmpty && functionCalls == null && meta == null) { return _buildTypingIndicator(); } + // 如果只有工具调用而没有文本内容,显示一个占位符确保气泡有足够宽度 + if (!isUser && message.isEmpty && functionCalls != null && meta == null) { + return Container( + constraints: const BoxConstraints( + minWidth: 200, // 确保最小宽度 + minHeight: 20, // 确保最小高度 + ), + child: const SizedBox.shrink(), // 透明占位符 + ); + } + // 如果是图片消息,显示图片 if (hasImage && imagePath != null) { // 如果消息是默认的[图片]标记,只显示图片 @@ -369,9 +402,12 @@ class MessageBubble extends GetView { // } } + // 创建消息内容Widget + Widget messageContent; + // 使用Markdown渲染AI消息,用户消息仍然使用普通文本 if (!isUser) { - return MarkdownBody( + messageContent = MarkdownBody( data: message, styleSheet: MarkdownStyleSheet( p: TextStyle( @@ -442,21 +478,34 @@ class MessageBubble extends GetView { ), //selectable: true, ); + } else { + // 用户消息仍然使用普通文本 + messageContent = Text( + message, + style: TextStyle( + color: isUser + ? Colors.white + : (Theme.of(context).brightness == Brightness.dark + ? Colors.white.withOpacity(0.9) + : Colors.black87), + fontSize: 14, + height: 1.4, + fontWeight: FontWeight.w400, + ), + ); } - // 用户消息仍然使用普通文本 - return Text( - message, - style: TextStyle( - color: isUser - ? Colors.white - : (Theme.of(context).brightness == Brightness.dark - ? Colors.white.withOpacity(0.9) - : Colors.black87), - fontSize: 14, - height: 1.4, - fontWeight: FontWeight.w400, - ), - ); + + // 如果是AI消息且有工具调用,确保最小宽度以防止工具调用列表越界 + if (!isUser && functionCalls != null && functionCalls!.isNotEmpty) { + return Container( + constraints: const BoxConstraints( + minWidth: 200, // 确保最小宽度,防止工具调用列表越界 + ), + child: messageContent, + ); + } + + return messageContent; } /// 构建输入中的指示器 @@ -550,6 +599,155 @@ class MessageBubble extends GetView { return ''; } } + + /// 构建工具调用流程组件 + Widget _buildFunctionCallsWidget(BuildContext context) { + return Positioned( + top: 0, + left: 0, + right: 0, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 0, vertical: 0), // 减少垂直padding + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 可点击的标题行 + GestureDetector( + onTap: () async { + await controller.updateToolCallsExpanded( + sessionid, + !isToolCallsExpanded, + ); + }, + child: Container( + height: 20, // 增加固定高度,给字体更多空间 + decoration: BoxDecoration( + color: Theme.of(context).brightness == Brightness.dark + ? Colors.grey[800]?.withOpacity(0.8) + : Colors.grey[100]?.withOpacity(0.9), + borderRadius: BorderRadius.circular(6), + border: Border.all( + color: Theme.of(context).brightness == Brightness.dark + ? Colors.white.withOpacity(0.1) + : Colors.black.withOpacity(0.1), + width: 0.5, + ), + ), + child: Row( + children: [ + // 显示最后一个工具名称 + Expanded( + child: Text( + functionCalls != null && functionCalls!.isNotEmpty + ? functionCalls!.last + : '工具调用', + style: TextStyle( + fontSize: 12, // 适当增大字体 + color: + Theme.of(context).brightness == Brightness.dark + ? Colors.white.withOpacity(0.7) + : Colors.black54, + fontWeight: FontWeight.w500, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(width: 4), + // 箭头放在右侧 + Icon( + isToolCallsExpanded + ? Icons.keyboard_arrow_up // 展开时显示向上箭头 + : Icons.keyboard_arrow_down, // 收起时显示向下箭头 + size: 14, // 减小图标大小 + color: Theme.of(context).brightness == Brightness.dark + ? Colors.white.withOpacity(0.7) + : Colors.black54, + ), + ], + ), + ), + ), + // 展开的工具调用列表 + if (isToolCallsExpanded && functionCalls != null) + Container( + margin: const EdgeInsets.only(top: 2), // 减少顶部间距 + padding: const EdgeInsets.all(6), // 减少内边距 + constraints: const BoxConstraints( + maxHeight: 120, // 限制最大高度,避免超界 + ), + decoration: BoxDecoration( + // 添加更强的背景色来遮挡下层UI + color: Theme.of(context).brightness == Brightness.dark + ? Colors.grey[900]?.withOpacity(0.95) // 深色模式下使用深灰色背景 + : Colors.white.withOpacity(0.95), // 浅色模式下使用白色背景 + borderRadius: BorderRadius.circular(4), // 减小圆角 + border: Border.all( + color: Theme.of(context).brightness == Brightness.dark + ? Colors.white.withOpacity(0.2) + : Colors.black.withOpacity(0.2), + width: 0.5, + ), + // 添加阴影增强遮挡效果 + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: SingleChildScrollView( + // 添加滚动,防止超界 + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: functionCalls!.asMap().entries.map((entry) { + int index = entry.key; + String call = entry.value; + return Padding( + padding: EdgeInsets.only( + bottom: index < functionCalls!.length - 1 + ? 2 + : 0), // 减少间距 + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '${index + 1}. ', + style: TextStyle( + fontSize: 12, // 修改为更小的字体 + color: Theme.of(context).brightness == + Brightness.dark + ? Colors.white.withOpacity(0.6) + : Colors.black45, + fontWeight: FontWeight.w500, + ), + ), + Expanded( + child: Text( + call, + style: TextStyle( + fontSize: 12, // 修改为更小的字体 + color: Theme.of(context).brightness == + Brightness.dark + ? Colors.white.withOpacity(0.8) + : Colors.black87, + height: 1.1, // 减小行高 + ), + ), + ), + ], + ), + ); + }).toList(), + ), + ), + ), + ], + ), + )); + } } /// 动画点组件 diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt index 84dfaaca3..8b8f90d47 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt @@ -27,7 +27,6 @@ import kotlinx.coroutines.sync.Mutex import com.deep_voice.speech.tts.TtsEvent import com.deep_voice.speech.tts.TtsEventListener import com.deep_voice.speech.tts.TtsEventType -import java.util.UUID import okhttp3.* import okhttp3.MediaType.Companion.toMediaType import okhttp3.RequestBody.Companion.toRequestBody @@ -88,7 +87,7 @@ object AgentService : CoroutineScope { private lateinit var chatStorageHelper: ChatStorageHelper // 会话ID,用于区分不同聊天上下文 - private val sessionId = "default_agent" + private val agentId = "default_agent" // 服务端连接 private var serverurl = "" private var usertoken = "" @@ -602,7 +601,7 @@ object AgentService : CoroutineScope { Log.d(TAG, "recognizeCallback") val success = azureAsrHelper?.setupEventListeners(object : AzureAsrHelper.ContinuousRecognizeCallback { - override fun onRecognizing(recognizing: String, detectedLanguage: String) { + override fun onRecognizing(sessiondid:String, recognizing: String, detectedLanguage: String) { if (recognizing.isNotEmpty()&&isKeepResult==true) { // 检测到语音,更新状态 @@ -646,16 +645,17 @@ object AgentService : CoroutineScope { } } - override fun onResult(result: String, detectedLanguage: String) { + override fun onResult(sessiondid:String,result: String, detectedLanguage: String) { if (result.isNotEmpty()&&isKeepResult==true) {//加入是否结束要输出最后的结果判读 sendEvent( "recognition_result", mapOf( + "sessiondid" to sessiondid, "text" to result, "language" to detectedLanguage ) ) - processWithChatApi(result) + processWithChatApi(sessiondid,result) } @@ -665,8 +665,8 @@ object AgentService : CoroutineScope { } - override fun onSessionStarted() { - sendEvent("recognition_started", mapOf("status" to "started")) + override fun onSessionStarted(sessiondid:String) { + sendEvent("recognition_started", mapOf("sessiondid" to sessiondid, "status" to "started")) // 使用模式感知的空闲检测启动方法 audioPlayer?.playAudio(R.raw.start) @@ -674,33 +674,35 @@ object AgentService : CoroutineScope { - override fun onSessionStopped() { - sendEvent("recognition_stopped", mapOf("status" to "stopped")) + override fun onSessionStopped(sessiondid:String) { + sendEvent("recognition_stopped", mapOf("sessiondid" to sessiondid, "status" to "stopped")) _isRecognitionActive.set(false) stopIdleCheck() audioPlayer?.playAudio(R.raw.stop) } - override fun onCanceled(reason: String, errorDetails: String) { + override fun onCanceled(sessiondid: String,reason: String, errorDetails: String) { _isRecognitionActive.set(false) stopIdleCheck() Log.d(TAG, "recognition_canceled:") BleService.closeCodec() sendEvent( "recognition_canceled", mapOf( + "sessiondid" to sessiondid, "reason" to reason, "details" to errorDetails ) ) } - override fun onError(code: Int, error: String) { + override fun onError(sessiondid:String, code: Int, error: String) { _isRecognitionActive.set(false) stopIdleCheck() BleService.closeCodec() Log.d(TAG, "语音识别错误: $error") sendEvent( "error", mapOf( + "sessiondid" to sessiondid, "code" to code, "message" to error ) @@ -802,7 +804,7 @@ object AgentService : CoroutineScope { /** * 处理文本输入 */ - fun processTextInput(text: String, speakResponse: Boolean = false): Boolean { + fun processTextInput(sessionid: String,text: String, speakResponse: Boolean = false): Boolean { if (!isInitialized) { Log.e(TAG, "服务未初始化") sendEvent("error", mapOf("code" to "NOT_INITIALIZED", "message" to "服务未初始化")) @@ -814,17 +816,16 @@ object AgentService : CoroutineScope { sendEvent("error", mapOf("code" to "EMPTY_TEXT", "message" to "文本输入不能为空")) return false } - // 使用ChatAPI处理文本 - processWithChatApi(text, speakResponse) + processWithChatApi(sessionid,text, speakResponse) return true } /** * 使用ChatAPI处理语音识别结果 */ - private fun processWithChatApi(text: String) { - processWithChatApi(text, true) + private fun processWithChatApi(sessionid:String,text: String) { + processWithChatApi(sessionid,text, true) } /** @@ -834,6 +835,7 @@ object AgentService : CoroutineScope { * @param speakResponse 是否使用TTS朗读回复 */ private fun processWithChatApi( + sessionid: String, text: String, speakResponse: Boolean = true ) { @@ -841,7 +843,7 @@ object AgentService : CoroutineScope { // 创建用户文本消息并处理 val userMessage = createUserMessage(text) - processWithChatApiInternal(userMessage, text, speakResponse) + processWithChatApiInternal(sessionid,userMessage, text, speakResponse) } /** @@ -852,6 +854,7 @@ object AgentService : CoroutineScope { * @param speakResponse 是否朗读回复 */ private fun processImageWithChatApi( + sessionid: String, imageBase64: String, text: String = "", speakResponse: Boolean = false @@ -861,7 +864,7 @@ object AgentService : CoroutineScope { Log.d(TAG, "liwei--------- 图像Mesage: ${userMessage}") // 图片描述用于存储 val displayText = text.ifEmpty { "[图片]" } - processWithChatApiInternal(userMessage, displayText, speakResponse, true) + processWithChatApiInternal(sessionid,userMessage, displayText, speakResponse, true) } /** @@ -874,21 +877,21 @@ object AgentService : CoroutineScope { * @param imagePath 图片路径(如果有) */ private fun processWithChatApiInternal( + sessionid: String, userMessage: JSONObject, displayText: String, speakResponse: Boolean = true, hasImage: Boolean = false, imagePath: String? = null ) { - Log.d(TAG, "内部方法:通用的ChatAPI处理逻辑") // 如果有正在进行的AI流式输出,先停止它 audioPlayer?.stopAudio() stopAiStream() stopTts() + currsessionId = sessionid // 记录新的AI任务 currentAiJob = launch { try { - currsessionId = UUID.randomUUID().toString() // 设置状态为正在流式输出 _isAiStreaming.set(true) Log.d(TAG, "processWithChatApiInternal") @@ -958,7 +961,7 @@ object AgentService : CoroutineScope { } // Log.e("ChatApiService", "liwei---------sendMessageStream messages:${messagesWithSystemPrompt}") chatApiService.sendMessageStream( - currsessionId, + sessionid, messagesWithSystemPrompt, object : StreamCallback { @@ -984,15 +987,17 @@ object AgentService : CoroutineScope { override fun onToken(sessionid: String, token: String) { try { if (sessionid != currsessionId) { +// Log.d(TAG, "liwei--------------- AI Call onToken 会话过期 $sessionid-------$currsessionId") return } responseBuilder.append(token) +// Log.d(TAG, "liwei--------------- AI Call onToken:$token speakResponse:$speakResponse reply:$reply broadcast:$broadcast") if (speakResponse && reply && broadcast && token.isNotEmpty()) { ttsService?.speakStream(token) } if (reply && token.isNotEmpty()) { // 发送流式回复token - sendEvent("assistant_token", mapOf("token" to token)) + sendEvent("assistant_token", mapOf("sessionid" to sessionid,"token" to token)) } } catch (e: Exception) { Log.e(TAG, "liwei--------------- AI Call onToken 异常", e) @@ -1011,6 +1016,7 @@ object AgentService : CoroutineScope { val response = responseBuilder.toString() // 发送完整回复,包含是否有图片的标记 val responseData = mutableMapOf( + "sessionid" to sessionid, "text" to response, "userInput" to displayText ) @@ -1027,6 +1033,7 @@ object AgentService : CoroutineScope { } // 保存聊天记录 saveChatMessage( + sessionid, displayText, response, aiMetadata, @@ -1045,6 +1052,7 @@ object AgentService : CoroutineScope { Log.e(TAG, "AI处理出错", e) sendEvent( "error", mapOf( + "sessionid" to sessionid, "code" to "AI_ERROR", "message" to e.message.toString() ) @@ -1064,6 +1072,7 @@ object AgentService : CoroutineScope { val name = call.getString("name") sendEvent( "function_call", mapOf( + "sessionid" to sessionid, "name" to name, "arguments" to call.toString() ) @@ -1072,6 +1081,10 @@ object AgentService : CoroutineScope { BleService.closeCodec() stopRecognition() } + + //播报工具调用 + ttsService?.speakStream("正在调用工具:${name}") + } catch (e: Exception) { Log.e(TAG, "liwei--------------- AI Call onFunctionCall 异常", e) } @@ -1083,6 +1096,11 @@ object AgentService : CoroutineScope { functionCallResult: JSONObject ) { try { + Log.d( + TAG, + "liwei--------------- AI Call onFunctionCallResult ${sessionid}:${functionCallResult.toString()}", + + ) audioPlayer?.stopAudio() val name = functionCall.get("name") as String; val resultmap = autoHandleFcunCallResult(name, functionCallResult); @@ -1093,6 +1111,7 @@ object AgentService : CoroutineScope { aiMetadata = resultmap["meta"] as String; sendEvent( "function_call_result", mapOf( + "sessionid" to sessionid, "function_call" to functionCall.toString(), "result" to functionCallResult.toString(), "meta" to aiMetadata, @@ -1101,16 +1120,12 @@ object AgentService : CoroutineScope { }else{ sendEvent( "function_call_result", mapOf( + "sessionid" to sessionid, "function_call" to functionCall.toString(), "result" to functionCallResult.toString(), ) ) } - - Log.e( - TAG, - "liwei--------------- AI Call onFunctionCallResult broadcast:$broadcast reply:$reply iscard:$iscard" - ) } catch (e: Exception) { Log.e( TAG, @@ -1127,6 +1142,7 @@ object AgentService : CoroutineScope { Log.e(TAG, "liwei--------------- AI处理出错", e) sendEvent( "error", mapOf( + "sessionid" to sessionid, "code" to errorType, "message" to e.message.toString() ) @@ -1152,7 +1168,7 @@ object AgentService : CoroutineScope { } // 获取最近消息 - val recentMessages = chatStorageHelper.getRecentMessages(sessionId, 10) + val recentMessages = chatStorageHelper.getRecentMessages(agentId, 10) if (recentMessages.isEmpty()) { return } @@ -1389,6 +1405,7 @@ object AgentService : CoroutineScope { * 保存聊天消息 */ private fun saveChatMessage( + sessionid: String, userMessage: String, assistantMessage: String, aiMetadata: String, @@ -1399,7 +1416,8 @@ object AgentService : CoroutineScope { try { // 保存用户消息 val userMessageId = chatStorageHelper.saveMessage( - sessionId = sessionId, + agentId = agentId, + sessionId = sessionid, message = userMessage, sender = "user", metadata = userMetadata @@ -1409,7 +1427,8 @@ object AgentService : CoroutineScope { if (assistantMessage.isNotEmpty() || aiMetadata.isNotEmpty()) { // 保存AI回复 val assistantMessageId = chatStorageHelper.saveMessage( - sessionId = sessionId, + agentId = agentId, + sessionId = sessionid, message = assistantMessage, sender = "assistant", metadata = aiMetadata @@ -1435,7 +1454,7 @@ object AgentService : CoroutineScope { fun clearChatHistory(callback: (Boolean) -> Unit) { launch { try { - val success = chatStorageHelper.deleteMessages(sessionId) + val success = chatStorageHelper.deleteMessages(agentId) if (success) { synchronized(historyMessages) { @@ -1542,6 +1561,7 @@ object AgentService : CoroutineScope { * 处理图片输入 */ fun processImageInput( + sessionid: String, imagePath: String, imageUrl: String, text: String = "", @@ -1593,7 +1613,7 @@ object AgentService : CoroutineScope { ) Log.d(TAG, "liwei--------- 图像Mesage: ${userMessage}") // 处理消息并传入存储路径 - processWithChatApiInternal(userMessage, displayText, speakResponse, true, imagePath) + processWithChatApiInternal(sessionid,userMessage, displayText, speakResponse, true, imagePath) } catch (e: Exception) { Log.e(TAG, "处理图片失败: ${e.message}") sendEvent( diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt index e36f91294..513221bf8 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt @@ -144,7 +144,12 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream } "processTextInput" -> { + val sessionid = call.argument("sessionid") val text = call.argument("text") + if (sessionid.isNullOrEmpty()) { + result.error("INVALID_ARGUMENTS", "回话id不能为空", null) + return + } if (text.isNullOrEmpty()) { result.error("INVALID_ARGUMENTS", "文本不能为空", null) return @@ -154,6 +159,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream try { val isProcessing = AgentService.processTextInput( + sessionid = sessionid, text = text, speakResponse = speakResponse ) @@ -166,6 +172,11 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream } "processImageInput" -> { + val sessionid = call.argument("sessionid") + if (sessionid.isNullOrEmpty()) { + result.error("INVALID_ARGUMENTS", "回话id不能为空", null) + return + } val imagePath = call.argument("imagePath") if (imagePath.isNullOrEmpty()) { result.error("INVALID_ARGUMENTS", "图片路径不能为空", null) @@ -181,6 +192,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream try { val isProcessing = AgentService.processImageInput( + sessionid=sessionid, imagePath = imagePath, imageUrl = imageUrl, text = text, diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt index 80c9b568b..03dee6109 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt @@ -85,8 +85,8 @@ Log.d(TAG, "手动启动语音识别: ") /** * 处理文本输入 */ - fun processTextInput(text: String, speakResponse: Boolean = false): Boolean { - return AgentService.processTextInput(text, speakResponse) + fun processTextInput(sessionId: String,text: String, speakResponse: Boolean = false): Boolean { + return AgentService.processTextInput(sessionId,text, speakResponse) } /** diff --git a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift index 0643f2510..cc11359ee 100644 --- a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift +++ b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift @@ -59,7 +59,7 @@ class AgentServiceImpl: NSObject { } // 会话ID,用于区分不同聊天上下文 - private let sessionId = "default_agent" + private let agentId = "default_agent" private var isInitialized: Bool = false private var isRecognizing: Bool = false @@ -565,7 +565,7 @@ audioStream.saveAudioDataTo(data: audioData) return interrupted } - func processTextInput(_ text: String, speakResponse: Bool) -> Bool { + func processTextInput(sessionid:String, _ text: String, speakResponse: Bool) -> Bool { os_log("收到文本输入: text=%{public}@, speakResponse=%{public}@", log: logger, type: .info, text, speakResponse ? "是" : "否") if !isInitialized { @@ -581,21 +581,21 @@ audioStream.saveAudioDataTo(data: audioData) } os_log("开始处理文本输入", log: logger, type: .info) - processWithChatApiService(text: text, speakResponse: speakResponse) + processWithChatApiService(sessionid: sessionid, text: text, speakResponse: speakResponse) return true } - private func processWithChatApiService(text: String, speakResponse: Bool = true) { + private func processWithChatApiService(sessionid:String,text: String, speakResponse: Bool = true) { guard let chatApiService = chatApiService else { sendError("ChatAPI服务未初始化", code: "CHATAPI_NOT_INITIALIZED") return } let userMessage = chatApiService.createUserMessage(content: text) - processWithChatApiServiceInternal(userMessage: userMessage, displayText: text, speakResponse: speakResponse) + processWithChatApiServiceInternal(sessionid: sessionid,userMessage: userMessage, displayText: text, speakResponse: speakResponse) } - private func processWithChatApiServiceInternal(userMessage: [String: Any], displayText: String, speakResponse: Bool = true, hasImage: Bool = false,imagePath:String = "") { + private func processWithChatApiServiceInternal(sessionid:String,userMessage: [String: Any], displayText: String, speakResponse: Bool = true, hasImage: Bool = false,imagePath:String = "") { os_log("开始处理ChatAPI请求: displayText=%{public}@, speakResponse=%{public}@, hasImage=%{public}@", log: logger, type: .info, displayText, speakResponse ? "是" : "否", hasImage ? "是" : "否") @@ -650,7 +650,7 @@ audioStream.saveAudioDataTo(data: audioData) hasImage: hasImage, imagePath: imagePath ) - currsessionId = UUID().uuidString + currsessionId = sessionid azureTtsHelper?.startspeak(sessionid: currsessionId) os_log("设置回调并调用sendMessageStream", log: logger, type: .info) chatApiService.setStreamCallback(currsessionId,callback) @@ -659,7 +659,7 @@ audioStream.saveAudioDataTo(data: audioData) os_log("sendMessageStream调用完成", log: logger, type: .info) } - func processImageInput(imagePath: String,imageUrl: String, text: String, speakResponse: Bool) -> Bool { + func processImageInput(sessionid: String,imagePath: String,imageUrl: String, text: String, speakResponse: Bool) -> Bool { if !isInitialized { sendError("服务未初始化", code: "NOT_INITIALIZED") return false @@ -703,6 +703,7 @@ audioStream.saveAudioDataTo(data: audioData) let displayText = text.isEmpty ? "[图片]" : text self.processWithChatApiServiceInternal( + sessionid: sessionid, userMessage: userMessage, displayText: displayText, speakResponse: speakResponse, @@ -731,7 +732,7 @@ audioStream.saveAudioDataTo(data: audioData) chatHistory.removeAll() // 获取最近10条消息 - let recentMessages = chatStorageHelper.getRecentMessages(sessionId: sessionId, limit: 10) + let recentMessages = chatStorageHelper.getRecentMessages(agentId: agentId, limit: 10) if recentMessages.isEmpty { return @@ -763,11 +764,12 @@ audioStream.saveAudioDataTo(data: audioData) /** * 保存聊天消息 */ - internal func saveChatMessage(userMessage: String, assistantMessage: String, metadata: String = "",userMetadata: String) { + internal func saveChatMessage(sessionId:String,userMessage: String, assistantMessage: String, metadata: String = "",userMetadata: String) { DispatchQueue.global(qos: .utility).async { // 保存用户消息 let userMessageId = self.chatStorageHelper.saveMessage( - sessionId: self.sessionId, + agentId: self.agentId, + sessionId: sessionId, message: userMessage, sender: "user", metadata: userMetadata @@ -777,7 +779,8 @@ audioStream.saveAudioDataTo(data: audioData) if !assistantMessage.isEmpty || !metadata.isEmpty { // 保存AI回复 let assistantMessageId = self.chatStorageHelper.saveMessage( - sessionId: self.sessionId, + agentId: self.agentId, + sessionId: sessionId, message: assistantMessage, sender: "assistant", metadata: metadata.isEmpty ? nil : metadata @@ -1348,7 +1351,7 @@ private func jsonToString(_ json: [String: Any]) -> String? { chatHistory.removeAll() DispatchQueue.global(qos: .utility).async { - let success = self.chatStorageHelper.deleteMessages(sessionId: self.sessionId, messageIds: nil as [Int]?) + let success = self.chatStorageHelper.deleteMessages(agentId:self.agentId, messageIds: nil as [Int]?) if !success { os_log("清除聊天历史失败", log: self.logger, type: .error) } @@ -1681,7 +1684,7 @@ class ChatApiStreamCallback: StreamCallback { try agentService.addToHistoryMessages(assistantMessage) } // 保存聊天记录 - try agentService.saveChatMessage(userMessage: displayText, assistantMessage: response, metadata: metadata,userMetadata:toJSONString(userMetadata)) + try agentService.saveChatMessage(sessionId: sessionId,userMessage: displayText, assistantMessage: response, metadata: metadata,userMetadata:toJSONString(userMetadata)) agentService.isAiStreaming = false os_log("设置AI流式状态为false", log: agentService.logger, type: .info) }catch{ @@ -2061,7 +2064,7 @@ class AudioPlayer { } extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback { - func onResult(_ text: String, _ detectedLanguage: String) { + func onResult(sessionid:String ,_ text: String, _ detectedLanguage: String) { if !text.isEmpty && isKeepResult { var data: [String: Any] = ["text": text] if !detectedLanguage.isEmpty { @@ -2069,7 +2072,7 @@ extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback { } sendEvent(name: "recognition_result", data: data) - processTextInput(text, speakResponse: true) + processTextInput(sessionid: sessionid,text, speakResponse: true) } let previousHasSpeech = hasSpeechDetected @@ -2080,7 +2083,7 @@ extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback { } } - func onRecognizing(_ recognizing: String, _ detectedLanguage: String) { + func onRecognizing(sessionid:String ,_ recognizing: String, _ detectedLanguage: String) { if !recognizing.isEmpty && isKeepResult { let previousHasSpeech = hasSpeechDetected hasSpeechDetected = true @@ -2102,7 +2105,7 @@ extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback { } } - func onSessionStarted() { + func onSessionStarted(sessionid:String) { sendEvent(name: "recognition_started", data: ["status": "started"]) isRecognizing = true hasSpeechDetected = false @@ -2123,7 +2126,7 @@ extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback { } } - func onSessionStopped() { + func onSessionStopped(sessionid:String) { sendEvent(name: "recognition_stopped", data: ["status": "stopped"]) isRecognizing = false hasSpeechDetected = false @@ -2133,7 +2136,7 @@ extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback { } } - func onCanceled(_ reason: String, _ errorDetails: String) { + func onCanceled(sessionid:String ,_ reason: String, _ errorDetails: String) { var data: [String: Any] = [:] if !reason.isEmpty { data["reason"] = reason @@ -2150,7 +2153,7 @@ extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback { stopIdleCheck() } - func onError(_ errorCode: Int, _ error: String) { + func onError(sessionid:String ,_ errorCode: Int, _ error: String) { let data: [String: Any] = ["message": error.isEmpty ? "未知错误" : error] sendEvent(name: "error", data: data) isRecognizing = false diff --git a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServicePlugin.swift b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServicePlugin.swift index e785c5192..08864a87f 100644 --- a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServicePlugin.swift +++ b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServicePlugin.swift @@ -104,17 +104,19 @@ public class AgentServicePlugin: NSObject, FlutterPlugin { return case "processTextInput": guard let arguments = call.arguments as? [String: Any], + let sessionid = arguments["sessionid"] as? String, let text = arguments["text"] as? String else { result(FlutterError(code: "INVALID_ARGUMENTS", message: "文本不能为空", details: nil)) return } let speakResponse = arguments["speakResponse"] as? Bool ?? false - let success = impl.processTextInput(text, speakResponse: speakResponse) + let success = impl.processTextInput(sessionid: sessionid,text, speakResponse: speakResponse) result(success) case "processImageInput": guard let arguments = call.arguments as? [String: Any], + let sessionid = arguments["sessionid"] as? String, let imageUrl = arguments["imageUrl"] as? String, let imagePath = arguments["imagePath"] as? String else { result(FlutterError(code: "INVALID_ARGUMENTS", message: "图片路径不能为空", details: nil)) @@ -123,7 +125,7 @@ public class AgentServicePlugin: NSObject, FlutterPlugin { let text = arguments["text"] as? String ?? "" let speakResponse = arguments["speakResponse"] as? Bool ?? false - let success = impl.processImageInput(imagePath: imagePath,imageUrl: imageUrl, text: text, speakResponse: speakResponse) + let success = impl.processImageInput(sessionid: sessionid,imagePath: imagePath,imageUrl: imageUrl, text: text, speakResponse: speakResponse) result(success) case "speakText": diff --git a/local_plugins/agent_service/lib/agent_service.dart b/local_plugins/agent_service/lib/agent_service.dart index c2a051c5e..502c4c9f1 100644 --- a/local_plugins/agent_service/lib/agent_service.dart +++ b/local_plugins/agent_service/lib/agent_service.dart @@ -359,10 +359,11 @@ class AgentService { /// [text] 文本内容 /// [speakResponse] 是否朗读响应 /// 返回是否成功处理文本 - static Future processTextInput(String text, + static Future processTextInput(String sessionid, String text, {bool speakResponse = false}) async { try { final bool result = await _channel.invokeMethod('processTextInput', { + 'sessionid': sessionid, 'text': text, 'speakResponse': speakResponse, }); @@ -440,10 +441,12 @@ class AgentService { /// [imageUrl] 图片url /// [speakResponse] 是否朗读响应 /// 返回是否成功处理图片 - static Future processImageInput(String imagePath, String imageUrl, + static Future processImageInput( + String sessionid, String imagePath, String imageUrl, {String text = "", bool speakResponse = false}) async { try { final bool result = await _channel.invokeMethod('processImageInput', { + 'sessionid': sessionid, 'imagePath': imagePath, 'imageUrl': imageUrl, 'text': text, diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt index 630abbdf3..7d43d01c3 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt @@ -13,6 +13,7 @@ import java.util.concurrent.TimeUnit import com.yunqiinnovation.azure_speech.tools.RecordFile import com.microsoft.cognitiveservices.speech.audio.AudioConfig import com.yunqiinnovation.azure_speech.tools.AudioRecordingForegroundService +import java.util.UUID /** @@ -58,7 +59,7 @@ class AzureAsrHelper(private val context: Context) { // 防抖机制相关变量 private var lastNetworkLostTime = 0L private val networkLostDebounceInterval = 3000L // 2秒防抖间隔 - + private var currsessionid = "" //识别回话id 关联到整个聊天过程中 /** @@ -242,7 +243,7 @@ class AzureAsrHelper(private val context: Context) { // 检查网络状态 if (!checkNetworkStatus()) { Log.e(tag, "网络不可用,无法开始语音识别") - continuousCallback?.onError(1000,"网络连接不可用,请检查网络设置") + continuousCallback?.onError(currsessionid,1000,"网络连接不可用,请检查网络设置") return false } @@ -258,7 +259,7 @@ class AzureAsrHelper(private val context: Context) { Log.d(tag, "前台服务已启动") } catch (e: Exception) { Log.e(tag, "启动前台服务失败: ${e.message}") - continuousCallback?.onError(1003, "启动前台服务失败: ${e.message}") + continuousCallback?.onError(currsessionid,1003, "启动前台服务失败: ${e.message}") return false } } @@ -268,7 +269,7 @@ class AzureAsrHelper(private val context: Context) { val callback = continuousCallback if (callback == null) { Log.e(tag, "无法重新启动连续识别:回调为空") - continuousCallback?.onError(1001,"回调为空") + continuousCallback?.onError(currsessionid,1001,"回调为空") return false } setupEventListeners(callback) @@ -308,7 +309,7 @@ class AzureAsrHelper(private val context: Context) { fun setupEventListeners(callback: ContinuousRecognizeCallback): Boolean { Log.d(tag, "设置ssssss监听器:${speechConfig ?: "null"} ") if (speechConfig == null) { - callback.onError(1002,"语音服务未初始化") + callback.onError(currsessionid,1002,"语音服务未初始化") return false } @@ -330,7 +331,7 @@ class AzureAsrHelper(private val context: Context) { } else { currentLanguage } - callback.onRecognizing(event.result.text, detectedLanguage) + callback.onRecognizing(currsessionid,event.result.text, detectedLanguage) } } ) @@ -345,7 +346,7 @@ class AzureAsrHelper(private val context: Context) { } else { currentLanguage } - callback.onResult(event.result.text, detectedLanguage) + callback.onResult(currsessionid,event.result.text, detectedLanguage) } } ) @@ -355,7 +356,8 @@ class AzureAsrHelper(private val context: Context) { EventHandler { _, _ -> // 直接在当前线程调用回调 Log.d(tag, "会话开始事件") - callback.onSessionStarted() + currsessionid = UUID.randomUUID().toString() + callback.onSessionStarted(currsessionid) } ) @@ -365,7 +367,7 @@ class AzureAsrHelper(private val context: Context) { // 直接在当前线程调用回调 Log.d(tag, "会话结束事件") if (audioSourceType == AudioSourceType.EXTERNAL) { - callback.onSessionStopped() + callback.onSessionStopped(currsessionid) audioStream?.isContinuousRecognitionActive = false } } @@ -378,11 +380,11 @@ class AzureAsrHelper(private val context: Context) { val reason = event.reason.toString() Log.d(tag, "识别被取消: reason=$reason, details=$errorDetails") - callback.onCanceled(reason,errorDetails) + callback.onCanceled(currsessionid,reason,errorDetails) } ) } catch (e: Exception) { - callback.onError(1001,"启动连续识别失败: ${e.message}") + callback.onError(currsessionid,1001,"启动连续识别失败: ${e.message}") return false } return true @@ -495,7 +497,7 @@ class AzureAsrHelper(private val context: Context) { // 确保状态被重置 audioStream.isContinuousRecognitionActive = false - + currsessionid = "" } catch (e: Exception) { // 确保状态被重置 audioStream.isContinuousRecognitionActive = false @@ -503,6 +505,7 @@ class AzureAsrHelper(private val context: Context) { recognizer = null speechConfig = null isForegroundServiceRunning = false + currsessionid = "" } } @@ -689,7 +692,7 @@ class AzureAsrHelper(private val context: Context) { * @param text 识别的文本 * @param detectedLanguage 检测到的语言 */ - fun onResult(text: String, detectedLanguage: String) + fun onResult(sessiond:String, text: String, detectedLanguage: String) /** * 识别进行中调用 @@ -697,17 +700,17 @@ class AzureAsrHelper(private val context: Context) { * @param recognizing 正在识别的文本 * @param detectedLanguage 检测到的语言 */ - fun onRecognizing(recognizing: String, detectedLanguage: String) + fun onRecognizing(sessiond:String, recognizing: String, detectedLanguage: String) /** * 会话开始时调用 */ - fun onSessionStarted() + fun onSessionStarted(sessiond:String) /** * 会话结束时调用 */ - fun onSessionStopped() + fun onSessionStopped(sessiond:String) /** @@ -716,14 +719,14 @@ class AzureAsrHelper(private val context: Context) { * @param reason 取消原因 * @param errorDetails 错误详情 */ - fun onCanceled(reason: String, errorDetails: String) + fun onCanceled(sessiond:String,reason: String, errorDetails: String) /** * 识别出错时调用 * * @param error 错误信息 */ - fun onError(code: Int,error: String) + fun onError(sessiond:String, code: Int,error: String) } @@ -781,7 +784,7 @@ class AzureAsrHelper(private val context: Context) { lastNetworkLostTime = 0L isNetworkRecovering = true } else { - continuousCallback?.onError(1000,"网络连接不可用,请检查网络设置") + continuousCallback?.onError(currsessionid,1000,"网络连接不可用,请检查网络设置") Log.d(tag,"网络仍未恢复,继续检测") networkLostCount = 0 stopAudioProcessingImmediately() diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift index 1046f1979..ca31912cf 100644 --- a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift +++ b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift @@ -47,6 +47,7 @@ public class AzureAsrHelper: NSObject { private var isAudioStarted = false private var pendingStopRequest = false + private var currsessionId = "" // 配置参数 @@ -197,7 +198,7 @@ public class AzureAsrHelper: NSObject { self.lastNetworkLostTime = 0 self.isNetworkRecovering = true } else { - self.continuousCallback?.onError(1000, "网络连接不可用,请检查网络设置") + self.continuousCallback?.onError(sessionid: currsessionId,1000, "网络连接不可用,请检查网络设置") os_log("网络仍未恢复,继续检测", log: self.log, type: .debug) self.networkLostCount = 0 self.stopContinuousRecognition() @@ -780,7 +781,7 @@ public class AzureAsrHelper: NSObject { print("正在识别事件=检测到语言: \(detectedLanguage), 识别中: \(text)") DispatchQueue.main.async { - callback.onRecognizing(result.text ?? "", detectedLanguage) + callback.onRecognizing(sessionid: self.currsessionId,result.text ?? "", detectedLanguage) } } @@ -799,14 +800,15 @@ public class AzureAsrHelper: NSObject { // 在主线程回调结果 DispatchQueue.main.async { - callback.onResult(result.text ?? "", detectedLanguage) + callback.onResult(sessionid: self.currsessionId,result.text ?? "", detectedLanguage) } } // 会话开始事件 recognizer.addSessionStartedEventHandler { (sender, event) in // 直接在当前线程调用回调 - callback.onSessionStarted() + self.currsessionId = UUID().uuidString + callback.onSessionStarted(sessionid: self.currsessionId) print("会话开始事件:") } @@ -815,7 +817,7 @@ public class AzureAsrHelper: NSObject { guard let self = self else { return } print("会话结束事件:") // 直接在当前线程调用回调 - callback.onSessionStopped() + callback.onSessionStopped(sessionid: self.currsessionId) self.audioStream?.isContinuousRecognitionActive = false //self.stopAudioProcessing() } @@ -830,7 +832,7 @@ public class AzureAsrHelper: NSObject { os_log("识别取消: %{public}@", log: self.log, type: .error, errorDetails) - callback.onCanceled(reason, errorDetails) + callback.onCanceled(sessionid: self.currsessionId,reason, errorDetails) } return true @@ -994,7 +996,7 @@ public class AzureAsrHelper: NSObject { * @param text 识别的文本 * @param detectedLanguage 检测到的语言 */ - func onResult(_ text: String, _ detectedLanguage: String) + func onResult(sessionid:String,_ text: String, _ detectedLanguage: String) /** * 识别进行中调用 @@ -1002,17 +1004,17 @@ public class AzureAsrHelper: NSObject { * @param recognizing 正在识别的文本 * @param detectedLanguage 检测到的语言 */ - func onRecognizing(_ recognizing: String, _ detectedLanguage: String) + func onRecognizing(sessionid:String ,_ recognizing: String, _ detectedLanguage: String) /** * 会话开始时调用 */ - func onSessionStarted() + func onSessionStarted(sessionid:String) /** * 会话结束时调用 */ - func onSessionStopped() + func onSessionStopped(sessionid:String) /** * 识别取消时调用 @@ -1020,14 +1022,14 @@ public class AzureAsrHelper: NSObject { * @param reason 取消原因 * @param errorDetails 错误详情 */ - func onCanceled(_ reason: String, _ errorDetails: String) + func onCanceled(sessionid:String ,_ reason: String, _ errorDetails: String) /** * 识别出错时调用 * * @param error 错误信息 */ - func onError(_ code: Int,_ error: String) + func onError(sessionid:String ,_ code: Int,_ error: String) } /** * 检查网络状态 @@ -1039,7 +1041,7 @@ private func checkNetworkStatus() -> Bool { if !isAvailable { os_log("网络不可用,无法启动语音识别", log: log, type: .error) - continuousCallback?.onError(1000,"网络连接不可用,请检查网络设置") + continuousCallback?.onError(sessionid: self.currsessionId,1000,"网络连接不可用,请检查网络设置") } return isAvailable diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift index 4a272ea12..05a6424d1 100644 --- a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift +++ b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift @@ -1208,7 +1208,7 @@ private class AsrCallbackWrapper: AzureAsrHelper.ContinuousRecognizeCallback { * @param text 识别的文本 * @param detectedLanguage 检测到的语言 */ - func onResult(_ text: String, _ detectedLanguage: String) { + func onResult(sessionid:String,_ text: String, _ detectedLanguage: String) { plugin?.sendAsrEvent([ "type": "result", "text": text, @@ -1221,7 +1221,7 @@ private class AsrCallbackWrapper: AzureAsrHelper.ContinuousRecognizeCallback { * @param text 正在识别的文本 * @param detectedLanguage 检测到的语言 */ - func onRecognizing(_ text: String, _ detectedLanguage: String) { + func onRecognizing(sessionid:String,_ text: String, _ detectedLanguage: String) { plugin?.sendAsrEvent([ "type": "recognizing", "text": text, @@ -1232,7 +1232,7 @@ private class AsrCallbackWrapper: AzureAsrHelper.ContinuousRecognizeCallback { /** * 会话开始回调 */ - func onSessionStarted() { + func onSessionStarted(sessionid:String) { plugin?.sendAsrEvent([ "type": "sessionStarted" ]) @@ -1241,7 +1241,7 @@ private class AsrCallbackWrapper: AzureAsrHelper.ContinuousRecognizeCallback { /** * 会话结束回调 */ - func onSessionStopped() { + func onSessionStopped(sessionid:String) { plugin?.sendAsrEvent([ "type": "sessionStopped" ]) @@ -1252,7 +1252,7 @@ private class AsrCallbackWrapper: AzureAsrHelper.ContinuousRecognizeCallback { * @param reason 取消原因 * @param errorDetails 错误详情 */ - func onCanceled(_ reason: String, _ errorDetails: String) { + func onCanceled(sessionid:String,_ reason: String, _ errorDetails: String) { plugin?.sendAsrEvent([ "type": "canceled", "reason": reason, @@ -1265,7 +1265,7 @@ private class AsrCallbackWrapper: AzureAsrHelper.ContinuousRecognizeCallback { * @param errorCode 错误码 * @param error 错误信息 */ - func onError(_ errorCode: Int, _ error: String) { + func onError(sessionid:String,_ errorCode: Int, _ error: String) { plugin?.sendAsrEvent([ "type": "error", "code": errorCode, diff --git a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt index d4a792234..6ab4db783 100644 --- a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt +++ b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt @@ -164,16 +164,18 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor val oldSessionId = currSessionId // 1. 取消当前的协程任务 - currentStreamJob?.cancel() + currentStreamJob?.cancel(CancellationException("New session started")) currentStreamJob = null // 2. 通知旧会话被中止 - getSessionCallback(oldSessionId)?.onComplete(currSessionId) // 直接完成当前会话 + getSessionCallback(oldSessionId)?.onComplete(oldSessionId) // 直接完成当前会话 // 3. 清理状态 currSessionId = "" sessionCallbacks.remove(oldSessionId) toolCalls.clear() + + Log.d(TAG, "会话 $oldSessionId 已被中止") } } @@ -288,7 +290,7 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor // 检查是否包含图片,决定使用哪个模型 val currentModel = if (containsImage(messages)) visionModel else model - + // 转换消息格式 val chatMessages = convertToChatMessages(messages) @@ -352,10 +354,10 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor if (!isInitialized || apiKey.isEmpty() || openAI == null) { Log.e("ChatApiService", "ChatAPI服务未初始化,无法发送消息") try { - getSessionCallback(sessionid)?.onError(sessionid,ChatApiException("ChatAPI服务未初始化")) - } catch (e: Exception) { - Log.e(TAG, "onError回调异常: ${e.message}", e) - } + getSessionCallback(sessionid)?.onError(sessionid,ChatApiException("ChatAPI服务未初始化")) + } catch (e: Exception) { + Log.e(TAG, "onError回调异常: ${e.message}", e) + } return } @@ -369,6 +371,8 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor try { // 转换消息格式 val chatMessages = try { + // ✨ 第一步:上下文精简 +// val reducedMessages = reduce(messages) convertToChatMessages(messages) } catch (e: Exception) { Log.e("ChatApiService", "转换消息格式失败: ${e.message}", e) @@ -425,12 +429,10 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor } var replyContext = ""; + // 为每个会话新增一个状态标记:是否为中间工具调用(默认false) + var isIntermediateToolCall = false // Log.d(TAG, "liwei-------------------------开始AI 对话 7") chatsFlow.collect { result -> -// val usage = result.usage -// if (usage != null){ //发现流式不返回这歌数据 -// getSessionCallback(sessionid)?.onUsage(sessionid,usage.promptTokens,usage.completionTokens,usage.totalTokens) -// } // 每次处理前检查是否仍是当前会话 if (sessionid != currSessionId) { // throw CancellationException("Session cancelled") @@ -448,8 +450,22 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor Log.w("ChatApiService", "[Session: $sessionid] delta为null") return@collect } - // Log.d(TAG, "liwei-------------------------开始AI 对话 7-3") - if (delta.toolCalls == null){ + + + // -------------------------- + // 核心修改:第一帧判断是否为工具调用 + // -------------------------- + if (!isIntermediateToolCall) { + // 第一帧(或未标记时):检查是否有工具调用字段 + val hasToolCalls = delta.toolCalls?.isNotEmpty() == true + val hasFunctionCall = delta.functionCall != null + if (hasToolCalls || hasFunctionCall) { + // 标记为中间工具调用,后续所有帧都拦截 + isIntermediateToolCall = true + } + } + //中间过程不输出 + if (!isIntermediateToolCall){ // 处理普通文本内容 delta.content?.let { content -> try { @@ -460,7 +476,15 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor // 不重新抛出异常,继续处理后续内容 } } - } + } + //else{ + // delta.content?.let { content -> + // Log.d("ChatApiService", "liwei------------ [Session: $sessionid] 中间过程不输出 $content") + // } + + // } + + // Log.d(TAG, "liwei-------------------------开始AI 对话 7-4") // 收集工具调用信息 delta.toolCalls?.forEach { toolCall -> @@ -577,113 +601,131 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor "arguments" to firstToolCall.arguments, "id" to firstToolCall.id ) + // 通知上层工具调用事件 try { getSessionCallback(sessionid)?.onFunctionCall(sessionid,convertMapToJsonObject(functionCall)) } catch (e: Exception) { Log.e(TAG, "onFunctionCall回调异常: ${e.message}", e) } + // 在当前协程作用域内处理工具调用,使用async确保生命周期管理 val toolCallDeferred = async { try { - if (sessionid == currSessionId) { - // 通过MCP客户端处理工具调用 - val functionName = firstToolCall.name - val argumentsJson = firstToolCall.arguments - var isError = false - var errorText = "" - val result = if (_mcpClient?.hasToolWithName(functionName) == true) { - // 解析参数,如果参数为空JSON对象,使用空Map - val arguments = if (argumentsJson == "{}") { - emptyMap() - } else { - _mcpClient?.parseJsonArguments(argumentsJson) ?: emptyMap() - } - // 调用MCP工具 - val toolResult = try { - withTimeout(60000) { // 60秒超时 - _mcpClient?.callTool(functionName, arguments) + // 检查会话是否仍然有效 + if (sessionid != currSessionId) { + Log.d(TAG, "[Session: $sessionid] 会话已被替换,跳过工具调用") + return@async + } + + // 通过MCP客户端处理工具调用 + val functionName = firstToolCall.name + val argumentsJson = firstToolCall.arguments + var isError = false + var errorText = "" + val result = if (_mcpClient?.hasToolWithName(functionName) == true) { + // 解析参数,如果参数为空JSON对象,使用空Map + val arguments = if (argumentsJson == "{}") { + emptyMap() + } else { + _mcpClient?.parseJsonArguments(argumentsJson) ?: emptyMap() + } + // 调用MCP工具 + val toolResult = try { + withTimeout(60000) { // 60秒超时 + // 再次检查会话状态 + if (sessionid != currSessionId) { + throw CancellationException("Session cancelled") } - } catch (e: TimeoutCancellationException) { - Log.w("ChatApiService", "[Session: $sessionid] MCP工具调用超时: $functionName") - mapOf( - "content" to listOf(mapOf( - "type" to "text", - "text" to "Tool call timeout: $functionName" - )), - "isError" to true - ) - } catch (e: Exception) { - Log.e("ChatApiService", "[Session: $sessionid] MCP工具调用异常: $functionName", e) - mapOf( - "content" to listOf(mapOf( - "type" to "text", - "text" to "Tool call failed: ${e.message}" - )), - "isError" to true - ) + _mcpClient?.callTool(functionName, arguments) } - Log.d("ChatApiService", "[Session: $sessionid] AI调用工具结果: $functionName, 参数: $arguments, 结果: $toolResult") - // 处理结果 - if (toolResult != null) { - if (toolResult["isError"] == true) { - // 处理错误情况 - isError = true - val content = toolResult["content"] as? List<*> - val firstContent = content?.firstOrNull() as? Map<*, *> - errorText = firstContent?.get("text") as? String ?: "Tool execution failed" - mapOf("context" to errorText) - } else if (toolResult.containsKey("context")) { - // 本地函数结果 - toolResult - } else { - // MCP工具结果 - val content = toolResult["content"] as? List<*> - val firstContent = content?.firstOrNull() as? Map<*, *> - val text = firstContent?.get("text") as? String ?: "" - mapOf("context" to text) - } + } catch (e: TimeoutCancellationException) { + Log.w("ChatApiService", "[Session: $sessionid] MCP工具调用超时: $functionName") + mapOf( + "content" to listOf(mapOf( + "type" to "text", + "text" to "Tool call timeout: $functionName" + )), + "isError" to true + ) + } catch (e: CancellationException) { + Log.d("ChatApiService", "[Session: $sessionid] MCP工具调用被取消: $functionName") + // 不抛出异常,直接返回取消结果 + mapOf( + "content" to listOf(mapOf( + "type" to "text", + "text" to "Tool call cancelled" + )), + "isError" to true + ) + } catch (e: Exception) { + Log.e("ChatApiService", "[Session: $sessionid] MCP工具调用异常: $functionName", e) + mapOf( + "content" to listOf(mapOf( + "type" to "text", + "text" to "Tool call failed: ${e.message}" + )), + "isError" to true + ) + } + Log.d("ChatApiService", "[Session: $sessionid] AI调用工具结果: $functionName, 参数: $arguments, 结果: $toolResult") + // 处理结果 + if (toolResult != null) { + if (toolResult["isError"] == true) { + // 处理错误情况 + isError = true + val content = toolResult["content"] as? List<*> + val firstContent = content?.firstOrNull() as? Map<*, *> + errorText = firstContent?.get("text") as? String ?: "Tool execution failed" + mapOf("context" to errorText) + + } else if (toolResult.containsKey("context")) { + // 本地函数结果 + toolResult } else { - Log.w("ChatApiService", "[Session: $sessionid] MCP工具调用返回null") - mapOf("context" to "Tool call failed") + // MCP工具结果 + val content = toolResult["content"] as? List<*> + val firstContent = content?.firstOrNull() as? Map<*, *> + val text = firstContent?.get("text") as? String ?: "" + mapOf("context" to text) } } else { - // 工具不存在 - mapOf("context" to "Tool not found: $functionName") + Log.w("ChatApiService", "[Session: $sessionid] MCP工具调用返回null") + mapOf("context" to "Tool call failed") } - if (sessionid == currSessionId) { -// if (functionName != "set_user_profile_field"){ - // 处理结果 - try { - if(!isError){ - getSessionCallback(sessionid)?.onFunctionCallResult( - sessionid, - convertMapToJsonObject(functionCall), - convertMapToJsonObject(result) - ) - }else{ - getSessionCallback(sessionid)?.onError(sessionid, ChatApiException(errorText)) - } - } catch (e: Exception) { - Log.e(TAG, "onFunctionCallResult回调异常: ${e.message}", e) - } - // 将结果发送回OpenAI继续对话 - sendFunctionCallResultInternal( - sessionid= sessionid, - messages = currentMessages, - functionCall = functionCall, - functionResult = gson.toJson(result) + } else { + // 工具不存在 + mapOf("context" to "Tool not found: $functionName") + } + + // 最后检查会话状态 + if (sessionid == currSessionId) { + try { + if(!isError){ + getSessionCallback(sessionid)?.onFunctionCallResult( + sessionid, + convertMapToJsonObject(functionCall), + convertMapToJsonObject(result) ) -// }else{ -// try { -// getSessionCallback(sessionid)?.onComplete(sessionid) -// } catch (ex: Exception) { -// Log.e(TAG, "onComplete回调异常: ${ex.message}", ex) -// } -// clearCurrentSession() -// } + }else{ + getSessionCallback(sessionid)?.onError(sessionid, ChatApiException(errorText)) + } + } catch (e: Exception) { + Log.e(TAG, "onFunctionCallResult回调异常: ${e.message}", e) } + // 将结果发送回OpenAI继续对话 + sendFunctionCallResultInternal( + sessionid= sessionid, + messages = currentMessages, + functionCall = functionCall, + functionResult = gson.toJson(result) + ) + } else { + Log.d(TAG, "[Session: $sessionid] 工具调用完成时会话已被替换,跳过结果处理") } + } catch (e: CancellationException) { + Log.d("ChatApiService", "[Session: $sessionid] 工具调用协程被取消") + // 不重新抛出CancellationException,避免传播 } catch (e: Exception) { Log.e("ChatApiService", "工具调用处理过程中出错: ${e.message}", e) if (sessionid == currSessionId) { @@ -702,9 +744,10 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor try { toolCallDeferred.await() } catch (e: CancellationException) { + Log.d(TAG, "[Session: $sessionid] 工具调用被取消") // 协程被取消,确保子任务也被取消 toolCallDeferred.cancel() - throw e + // 不重新抛出异常,让流程正常结束 } return true @@ -1045,7 +1088,7 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor } return false } - + /** * 转换为OpenAI ChatMessage格式 */ diff --git a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/CustomSseClientTransport.kt b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/CustomSseClientTransport.kt index d13fbc37a..b515dedb2 100644 --- a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/CustomSseClientTransport.kt +++ b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/CustomSseClientTransport.kt @@ -30,9 +30,10 @@ class CustomSseClientTransport( ) : AbstractTransport() { private val TAG = "CustomSseClientTransport" - private val scope by lazy { - CoroutineScope(session.coroutineContext + SupervisorJob()) - } + private val transportScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + + // ✅ 为每个会话创建独立的作用域 + private var sessionScope: CoroutineScope? = null private val initialized = AtomicBoolean(false) private var session: ClientSSESession by Delegates.notNull() @@ -98,7 +99,7 @@ class CustomSseClientTransport( * 收集SSE事件 */ private suspend fun collectEvents() { - job = scope.launch(CoroutineName("CustomSseMcpClientTransport.collect#${hashCode()}")) { + job = sessionScope?.launch(CoroutineName("CustomSseMcpClientTransport.collect#${hashCode()}")) { try { session.incoming.collect { event -> Log.e(TAG, "$serviceidString event:${event.event}-${event.data}") @@ -238,7 +239,7 @@ class CustomSseClientTransport( * 启动连接监控,定期检查连接状态 */ private fun startConnectionMonitor() { - connectionMonitorJob = scope.launch { + connectionMonitorJob = transportScope.launch { while (isActive && isConnected.get()) { try { delay(10000) // 每10秒检查一次 @@ -278,6 +279,8 @@ class CustomSseClientTransport( queryParams = urlInfo.third } + sessionScope = CoroutineScope(transportScope.coroutineContext + SupervisorJob()) + // 创建SSE会话 - 直接使用原始URL session = urlString?.let { val sseConnectUrl = if (queryParams.isNotEmpty()) { @@ -381,7 +384,8 @@ class CustomSseClientTransport( _onClose() job?.cancelAndJoin() connectionMonitorJob?.cancelAndJoin() - + sessionScope?.cancel() + transportScope.cancel() Log.d(TAG, "$serviceidString CustomSseClientTransport已关闭") } } \ No newline at end of file diff --git a/local_plugins/chat_storage/android/src/main/kotlin/com/yunqiinnovation/chat_storage/ChatStorageHelper.kt b/local_plugins/chat_storage/android/src/main/kotlin/com/yunqiinnovation/chat_storage/ChatStorageHelper.kt index 930662395..15def6f51 100644 --- a/local_plugins/chat_storage/android/src/main/kotlin/com/yunqiinnovation/chat_storage/ChatStorageHelper.kt +++ b/local_plugins/chat_storage/android/src/main/kotlin/com/yunqiinnovation/chat_storage/ChatStorageHelper.kt @@ -22,13 +22,14 @@ class ChatStorageHelper(context: Context) { /** * 保存消息 * - * @param sessionId 会话ID + * @param agentId 会话ID * @param message 消息内容 * @param sender 发送者("user" 或 "assistant") * @param metadata 元数据(可选) * @return 成功返回消息ID,失败返回-1 */ fun saveMessage( + agentId: String, sessionId: String, message: String, sender: String, @@ -42,6 +43,7 @@ class ChatStorageHelper(context: Context) { // 执行插入操作 val values = android.content.ContentValues().apply { + put(ChatDatabaseHelper.COLUMN_AGENT_ID, agentId) put(ChatDatabaseHelper.COLUMN_SESSION_ID, sessionId) put(ChatDatabaseHelper.COLUMN_MESSAGE, message) put(ChatDatabaseHelper.COLUMN_SENDER, sender) @@ -59,12 +61,12 @@ class ChatStorageHelper(context: Context) { /** * 获取指定会话的消息 * - * @param sessionId 会话ID + * @param agentId 会话ID * @param page 页码,从1开始 * @param pageSize 每页大小 * @return 包含消息列表和分页信息的JSON对象 */ - fun getMessages(sessionId: String, page: Int = 1, pageSize: Int = 20): JSONObject { + fun getMessages(agentId: String, page: Int = 1, pageSize: Int = 20): JSONObject { try { val db = dbHelper.readableDatabase val offset = (page - 1) * pageSize @@ -72,19 +74,19 @@ class ChatStorageHelper(context: Context) { // 构建查询语句 val query = """ SELECT * FROM ${ChatDatabaseHelper.TABLE_MESSAGES} - WHERE ${ChatDatabaseHelper.COLUMN_SESSION_ID} = ? + WHERE ${ChatDatabaseHelper.COLUMN_AGENT_ID} = ? ORDER BY ${ChatDatabaseHelper.COLUMN_TIMESTAMP} DESC LIMIT ? OFFSET ? """.trimIndent() - val selectionArgs = arrayOf(sessionId, pageSize.toString(), offset.toString()) + val selectionArgs = arrayOf(agentId, pageSize.toString(), offset.toString()) val cursor = db.rawQuery(query, selectionArgs) val messages = cursorToJsonArray(cursor) // 获取总记录数 - val countQuery = "SELECT COUNT(*) FROM ${ChatDatabaseHelper.TABLE_MESSAGES} WHERE ${ChatDatabaseHelper.COLUMN_SESSION_ID} = ?" - val countCursor = db.rawQuery(countQuery, arrayOf(sessionId)) + val countQuery = "SELECT COUNT(*) FROM ${ChatDatabaseHelper.TABLE_MESSAGES} WHERE ${ChatDatabaseHelper.COLUMN_AGENT_ID} = ?" + val countCursor = db.rawQuery(countQuery, arrayOf(agentId)) var totalCount = 0 if (countCursor.moveToFirst()) { totalCount = countCursor.getInt(0) @@ -125,24 +127,26 @@ class ChatStorageHelper(context: Context) { // 查询不同的会话ID及其最新消息 val query = """ - SELECT s1.${ChatDatabaseHelper.COLUMN_SESSION_ID}, + SELECT s1.${ChatDatabaseHelper.COLUMN_AGENT_ID}, + s1.${ChatDatabaseHelper.COLUMN_SESSION_ID}, s1.${ChatDatabaseHelper.COLUMN_MESSAGE}, s1.${ChatDatabaseHelper.COLUMN_SENDER}, s1.${ChatDatabaseHelper.COLUMN_TIMESTAMP} FROM ${ChatDatabaseHelper.TABLE_MESSAGES} s1 INNER JOIN ( - SELECT ${ChatDatabaseHelper.COLUMN_SESSION_ID}, + SELECT ${ChatDatabaseHelper.COLUMN_AGENT_ID}, MAX(${ChatDatabaseHelper.COLUMN_TIMESTAMP}) as max_timestamp FROM ${ChatDatabaseHelper.TABLE_MESSAGES} - GROUP BY ${ChatDatabaseHelper.COLUMN_SESSION_ID} - ) s2 ON s1.${ChatDatabaseHelper.COLUMN_SESSION_ID} = s2.${ChatDatabaseHelper.COLUMN_SESSION_ID} + GROUP BY ${ChatDatabaseHelper.COLUMN_AGENT_ID} + ) s2 ON s1.${ChatDatabaseHelper.COLUMN_AGENT_ID} = s2.${ChatDatabaseHelper.COLUMN_AGENT_ID} AND s1.${ChatDatabaseHelper.COLUMN_TIMESTAMP} = s2.max_timestamp ORDER BY s1.${ChatDatabaseHelper.COLUMN_TIMESTAMP} DESC """.trimIndent() val cursor = db.rawQuery(query, null) val sessions = JSONArray() - + + val agentIdIndex = cursor.getColumnIndex(ChatDatabaseHelper.COLUMN_AGENT_ID) val sessionIdIndex = cursor.getColumnIndex(ChatDatabaseHelper.COLUMN_SESSION_ID) val messageIndex = cursor.getColumnIndex(ChatDatabaseHelper.COLUMN_MESSAGE) val senderIndex = cursor.getColumnIndex(ChatDatabaseHelper.COLUMN_SENDER) @@ -150,13 +154,14 @@ class ChatStorageHelper(context: Context) { while (cursor.moveToNext()) { val session = JSONObject().apply { + put("agentId", cursor.getString(agentIdIndex)) put("sessionId", cursor.getString(sessionIdIndex)) put("lastMessage", cursor.getString(messageIndex)) put("lastSender", cursor.getString(senderIndex)) put("timestamp", cursor.getString(timestampIndex)) // 获取会话中的消息数量 - val countQuery = "SELECT COUNT(*) FROM ${ChatDatabaseHelper.TABLE_MESSAGES} WHERE ${ChatDatabaseHelper.COLUMN_SESSION_ID} = ?" + val countQuery = "SELECT COUNT(*) FROM ${ChatDatabaseHelper.TABLE_MESSAGES} WHERE ${ChatDatabaseHelper.COLUMN_AGENT_ID} = ?" val countCursor = db.rawQuery(countQuery, arrayOf(cursor.getString(sessionIdIndex))) if (countCursor.moveToFirst()) { put("messageCount", countCursor.getInt(0)) @@ -181,7 +186,7 @@ class ChatStorageHelper(context: Context) { * @param messageIds 消息ID列表(可选,如果提供则删除指定消息) * @return 成功返回true,失败返回false */ - fun deleteMessages(sessionId: String? = null, messageIds: List? = null): Boolean { + fun deleteMessages(agentId: String? = null, messageIds: List? = null): Boolean { try { val db = dbHelper.writableDatabase @@ -191,10 +196,10 @@ class ChatStorageHelper(context: Context) { val deleteQuery = "DELETE FROM ${ChatDatabaseHelper.TABLE_MESSAGES} WHERE ${ChatDatabaseHelper.COLUMN_ID} IN ($idsStr)" db.execSQL(deleteQuery) return true - } else if (!sessionId.isNullOrEmpty()) { + } else if (!agentId.isNullOrEmpty()) { // 删除会话中的所有消息 - val deleteQuery = "DELETE FROM ${ChatDatabaseHelper.TABLE_MESSAGES} WHERE ${ChatDatabaseHelper.COLUMN_SESSION_ID} = ?" - db.execSQL(deleteQuery, arrayOf(sessionId)) + val deleteQuery = "DELETE FROM ${ChatDatabaseHelper.TABLE_MESSAGES} WHERE ${ChatDatabaseHelper.COLUMN_AGENT_ID} = ?" + db.execSQL(deleteQuery, arrayOf(agentId)) return true } return false @@ -223,11 +228,11 @@ class ChatStorageHelper(context: Context) { /** * 查询指定会话的最近N条消息 * - * @param sessionId 会话ID + * @param agentId 会话ID * @param limit 限制数量 * @return 消息列表,按时间正序排列 */ - fun getRecentMessages(sessionId: String, limit: Int): List> { + fun getRecentMessages(agentId: String, limit: Int): List> { try { val db = dbHelper.readableDatabase @@ -235,13 +240,13 @@ class ChatStorageHelper(context: Context) { val query = """ SELECT * FROM ( SELECT * FROM ${ChatDatabaseHelper.TABLE_MESSAGES} - WHERE ${ChatDatabaseHelper.COLUMN_SESSION_ID} = ? + WHERE ${ChatDatabaseHelper.COLUMN_AGENT_ID} = ? ORDER BY ${ChatDatabaseHelper.COLUMN_TIMESTAMP} DESC LIMIT ? ) tmp ORDER BY ${ChatDatabaseHelper.COLUMN_TIMESTAMP} ASC """.trimIndent() - val selectionArgs = arrayOf(sessionId, limit.toString()) + val selectionArgs = arrayOf(agentId, limit.toString()) val cursor = db.rawQuery(query, selectionArgs) val messages = ArrayList>() @@ -282,6 +287,7 @@ class ChatStorageHelper(context: Context) { val jsonArray = JSONArray() val idIndex = cursor.getColumnIndex(ChatDatabaseHelper.COLUMN_ID) + val agentIdIndex = cursor.getColumnIndex(ChatDatabaseHelper.COLUMN_AGENT_ID) val sessionIdIndex = cursor.getColumnIndex(ChatDatabaseHelper.COLUMN_SESSION_ID) val messageIndex = cursor.getColumnIndex(ChatDatabaseHelper.COLUMN_MESSAGE) val senderIndex = cursor.getColumnIndex(ChatDatabaseHelper.COLUMN_SENDER) @@ -291,6 +297,7 @@ class ChatStorageHelper(context: Context) { while (cursor.moveToNext()) { val jsonObject = JSONObject().apply { put("id", cursor.getInt(idIndex)) + put("agentId", cursor.getString(agentIdIndex)) put("sessionId", cursor.getString(sessionIdIndex)) put("message", cursor.getString(messageIndex)) put("sender", cursor.getString(senderIndex)) @@ -328,6 +335,7 @@ class ChatDatabaseHelper(context: Context) : // 定义消息表 const val TABLE_MESSAGES = "messages" const val COLUMN_ID = "id" + const val COLUMN_AGENT_ID = "agent_id" const val COLUMN_SESSION_ID = "session_id" const val COLUMN_MESSAGE = "message" const val COLUMN_SENDER = "sender" @@ -340,6 +348,7 @@ class ChatDatabaseHelper(context: Context) : val createMessagesTable = """ CREATE TABLE $TABLE_MESSAGES ( $COLUMN_ID INTEGER PRIMARY KEY AUTOINCREMENT, + $COLUMN_AGENT_ID TEXT NOT NULL, $COLUMN_SESSION_ID TEXT NOT NULL, $COLUMN_MESSAGE TEXT NOT NULL, $COLUMN_SENDER TEXT NOT NULL, @@ -351,6 +360,7 @@ class ChatDatabaseHelper(context: Context) : db.execSQL(createMessagesTable) // 创建索引以加快查询速度 + db.execSQL("CREATE INDEX idx_agent_id ON $TABLE_MESSAGES ($COLUMN_AGENT_ID)") db.execSQL("CREATE INDEX idx_session_id ON $TABLE_MESSAGES ($COLUMN_SESSION_ID)") db.execSQL("CREATE INDEX idx_timestamp ON $TABLE_MESSAGES ($COLUMN_TIMESTAMP)") } diff --git a/local_plugins/chat_storage/android/src/main/kotlin/com/yunqiinnovation/chat_storage/ChatStoragePlugin.kt b/local_plugins/chat_storage/android/src/main/kotlin/com/yunqiinnovation/chat_storage/ChatStoragePlugin.kt index 91b610d80..bed32b4da 100644 --- a/local_plugins/chat_storage/android/src/main/kotlin/com/yunqiinnovation/chat_storage/ChatStoragePlugin.kt +++ b/local_plugins/chat_storage/android/src/main/kotlin/com/yunqiinnovation/chat_storage/ChatStoragePlugin.kt @@ -31,12 +31,14 @@ class ChatStoragePlugin: FlutterPlugin, MethodCallHandler { when (call.method) { "saveMessage" -> { try { + val agentId = call.argument("agentId") ?: "" val sessionId = call.argument("sessionId") ?: "" val message = call.argument("message") ?: "" val sender = call.argument("sender") ?: "" val metadata = call.argument("metadata") val id = chatStorageHelper.saveMessage( + agentId = agentId, sessionId = sessionId, message = message, sender = sender, @@ -55,12 +57,12 @@ class ChatStoragePlugin: FlutterPlugin, MethodCallHandler { } "getMessages" -> { try { - val sessionId = call.argument("sessionId") ?: "" + val agentId = call.argument("agentId") ?: "" val page = call.argument("page") ?: 1 val pageSize = call.argument("pageSize") ?: 20 val messagesJson = chatStorageHelper.getMessages( - sessionId = sessionId, + agentId = agentId, page = page, pageSize = pageSize ) @@ -82,11 +84,11 @@ class ChatStoragePlugin: FlutterPlugin, MethodCallHandler { } "deleteMessages" -> { try { - val sessionId = call.argument("sessionId") + val agentId = call.argument("agentId") val messageIds = call.argument>("messageIds") val success = chatStorageHelper.deleteMessages( - sessionId = sessionId, + agentId = agentId, messageIds = messageIds ) @@ -111,11 +113,11 @@ class ChatStoragePlugin: FlutterPlugin, MethodCallHandler { } "getRecentMessages" -> { try { - val sessionId = call.argument("sessionId") ?: "" + val agentId = call.argument("agentId") ?: "" val limit = call.argument("limit") ?: 10 val messages = chatStorageHelper.getRecentMessages( - sessionId = sessionId, + agentId = agentId, limit = limit ) diff --git a/local_plugins/chat_storage/ios/chat_storage/Sources/chat_storage/ChatStorageHelper.swift b/local_plugins/chat_storage/ios/chat_storage/Sources/chat_storage/ChatStorageHelper.swift index 96fa34a8e..acf2b4588 100644 --- a/local_plugins/chat_storage/ios/chat_storage/Sources/chat_storage/ChatStorageHelper.swift +++ b/local_plugins/chat_storage/ios/chat_storage/Sources/chat_storage/ChatStorageHelper.swift @@ -34,12 +34,14 @@ public class ChatStorageHelper { let createTableString = """ CREATE TABLE IF NOT EXISTS messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, + agent_id TEXT NOT NULL, session_id TEXT NOT NULL, message TEXT NOT NULL, sender TEXT NOT NULL, metadata TEXT, created_at INTEGER NOT NULL ); + CREATE INDEX IF NOT EXISTS idx_agent_id ON messages (session_id); CREATE INDEX IF NOT EXISTS idx_session_id ON messages (session_id); CREATE INDEX IF NOT EXISTS idx_created_at ON messages (created_at); """ @@ -64,11 +66,12 @@ public class ChatStorageHelper { * @param metadata 可选的元数据 * @return 插入的消息ID,失败则返回-1 */ - public func saveMessage(sessionId: String, message: String, sender: String, metadata: String?) -> Int64 { - let insertStatementString = "INSERT INTO messages (session_id, message, sender, metadata, created_at) VALUES (?, ?, ?, ?, ?);" + public func saveMessage(agentId:String,sessionId: String, message: String, sender: String, metadata: String?) -> Int64 { + let insertStatementString = "INSERT INTO messages (agent_id,session_id, message, sender, metadata, created_at) VALUES (?, ?, ?, ?, ?, ?);" var insertStatement: OpaquePointer? if sqlite3_prepare_v2(db, insertStatementString, -1, &insertStatement, nil) == SQLITE_OK { + sqlite3_bind_text(insertStatement, 1, (agentId as NSString).utf8String, -1, nil) sqlite3_bind_text(insertStatement, 1, (sessionId as NSString).utf8String, -1, nil) sqlite3_bind_text(insertStatement, 2, (message as NSString).utf8String, -1, nil) sqlite3_bind_text(insertStatement, 3, (sender as NSString).utf8String, -1, nil) @@ -107,17 +110,17 @@ public class ChatStorageHelper { * @param pageSize 每页消息数量 * @return 消息列表的JSON字符串 */ - public func getMessages(sessionId: String, page: Int, pageSize: Int) -> String { + public func getMessages(agentId: String, page: Int, pageSize: Int) -> String { let offset = (page - 1) * pageSize var messagesArray: [[String: Any]] = [] // 首先获取总记录数 - let countQuery = "SELECT COUNT(*) FROM messages WHERE session_id = ?" + let countQuery = "SELECT COUNT(*) FROM messages WHERE agent_id = ?" var countStatement: OpaquePointer? var totalCount = 0 if sqlite3_prepare_v2(db, countQuery, -1, &countStatement, nil) == SQLITE_OK { - sqlite3_bind_text(countStatement, 1, (sessionId as NSString).utf8String, -1, nil) + sqlite3_bind_text(countStatement, 1, (agentId as NSString).utf8String, -1, nil) if sqlite3_step(countStatement) == SQLITE_ROW { totalCount = Int(sqlite3_column_int(countStatement, 0)) } @@ -130,7 +133,7 @@ public class ChatStorageHelper { let queryString = """ SELECT id, message, sender, metadata, created_at FROM messages - WHERE session_id = ? + WHERE agent_id = ? ORDER BY created_at DESC LIMIT ? OFFSET ? """ @@ -138,7 +141,7 @@ public class ChatStorageHelper { var queryStatement: OpaquePointer? if sqlite3_prepare_v2(db, queryString, -1, &queryStatement, nil) == SQLITE_OK { - sqlite3_bind_text(queryStatement, 1, (sessionId as NSString).utf8String, -1, nil) + sqlite3_bind_text(queryStatement, 1, (agentId as NSString).utf8String, -1, nil) sqlite3_bind_int(queryStatement, 2, Int32(pageSize)) sqlite3_bind_int(queryStatement, 3, Int32(offset)) @@ -163,7 +166,7 @@ public class ChatStorageHelper { var messageDict: [String: Any] = [ "id": id, - "sessionId": sessionId, // 添加sessionId字段 + "agentId": agentId, // 添加agentId字段 "message": message, "sender": sender, "timestamp": timestamp // 使用timestamp而不是created_at @@ -227,13 +230,13 @@ public class ChatStorageHelper { * @param messageIds 可选的消息ID列表,指定则删除特定消息 * @return 是否删除成功 */ - public func deleteMessages(sessionId: String?, messageIds: [Int]?) -> Bool { - if let sessionId = sessionId { - let deleteString = "DELETE FROM messages WHERE session_id = ?;" + public func deleteMessages(agentId: String?, messageIds: [Int]?) -> Bool { + if let agentId = agentId { + let deleteString = "DELETE FROM messages WHERE agent_id = ?;" var deleteStatement: OpaquePointer? if sqlite3_prepare_v2(db, deleteString, -1, &deleteStatement, nil) == SQLITE_OK { - sqlite3_bind_text(deleteStatement, 1, (sessionId as NSString).utf8String, -1, nil) + sqlite3_bind_text(deleteStatement, 1, (agentId as NSString).utf8String, -1, nil) if sqlite3_step(deleteStatement) == SQLITE_DONE { sqlite3_finalize(deleteStatement) @@ -302,7 +305,7 @@ public class ChatStorageHelper { * @param limit 限制数量 * @return 消息列表,按时间正序排列 */ - public func getRecentMessages(sessionId: String, limit: Int) -> [[String: Any]] { + public func getRecentMessages(agentId: String, limit: Int) -> [[String: Any]] { var messages: [[String: Any]] = [] // 首先检查数据库中是否有该会话的消息 @@ -310,7 +313,7 @@ public class ChatStorageHelper { var countStatement: OpaquePointer? if sqlite3_prepare_v2(db, countQuery, -1, &countStatement, nil) == SQLITE_OK { - sqlite3_bind_text(countStatement, 1, (sessionId as NSString).utf8String, -1, nil) + sqlite3_bind_text(countStatement, 1, (agentId as NSString).utf8String, -1, nil) if sqlite3_step(countStatement) == SQLITE_ROW { _ = Int(sqlite3_column_int(countStatement, 0)) } @@ -331,7 +334,7 @@ public class ChatStorageHelper { var queryStatement: OpaquePointer? if sqlite3_prepare_v2(db, queryString, -1, &queryStatement, nil) == SQLITE_OK { - sqlite3_bind_text(queryStatement, 1, (sessionId as NSString).utf8String, -1, nil) + sqlite3_bind_text(queryStatement, 1, (agentId as NSString).utf8String, -1, nil) sqlite3_bind_int(queryStatement, 2, Int32(limit)) while sqlite3_step(queryStatement) == SQLITE_ROW { diff --git a/local_plugins/chat_storage/ios/chat_storage/Sources/chat_storage/SwiftChatStoragePlugin.swift b/local_plugins/chat_storage/ios/chat_storage/Sources/chat_storage/SwiftChatStoragePlugin.swift index c281ce955..23db5fa13 100644 --- a/local_plugins/chat_storage/ios/chat_storage/Sources/chat_storage/SwiftChatStoragePlugin.swift +++ b/local_plugins/chat_storage/ios/chat_storage/Sources/chat_storage/SwiftChatStoragePlugin.swift @@ -20,6 +20,7 @@ public class SwiftChatStoragePlugin: NSObject, FlutterPlugin { case "saveMessage": do { guard let args = call.arguments as? [String: Any], + let agentId = args["agentId"] as? String, let sessionId = args["sessionId"] as? String, let message = args["message"] as? String, let sender = args["sender"] as? String else { @@ -30,6 +31,7 @@ public class SwiftChatStoragePlugin: NSObject, FlutterPlugin { let metadata = args["metadata"] as? String let id = ChatStorageHelper.shared.saveMessage( + agentId:agentId, sessionId: sessionId, message: message, sender: sender, @@ -49,7 +51,7 @@ public class SwiftChatStoragePlugin: NSObject, FlutterPlugin { case "getMessages": do { guard let args = call.arguments as? [String: Any], - let sessionId = args["sessionId"] as? String else { + let agentId = args["agentId"] as? String else { result(FlutterError(code: "INVALID_ARGS", message: "参数无效", details: nil)) return } @@ -58,7 +60,7 @@ public class SwiftChatStoragePlugin: NSObject, FlutterPlugin { let pageSize = args["pageSize"] as? Int ?? 20 let messagesJson = ChatStorageHelper.shared.getMessages( - sessionId: sessionId, + agentId: agentId, page: page, pageSize: pageSize ) @@ -77,11 +79,11 @@ public class SwiftChatStoragePlugin: NSObject, FlutterPlugin { return } - let sessionId = args["sessionId"] as? String + let agentId = args["agentId"] as? String let messageIds = args["messageIds"] as? [Int] let success = ChatStorageHelper.shared.deleteMessages( - sessionId: sessionId, + agentId: agentId, messageIds: messageIds ) @@ -109,4 +111,4 @@ public class SwiftChatStoragePlugin: NSObject, FlutterPlugin { result(FlutterMethodNotImplemented) } } -} \ No newline at end of file +} diff --git a/local_plugins/chat_storage/lib/chat_storage.dart b/local_plugins/chat_storage/lib/chat_storage.dart index 7e474e97a..f5e9bf9d1 100644 --- a/local_plugins/chat_storage/lib/chat_storage.dart +++ b/local_plugins/chat_storage/lib/chat_storage.dart @@ -6,7 +6,8 @@ import 'package:flutter/services.dart'; /// 聊天记录数据模型 class ChatMessage { final int? id; - final String sessionId; + final String agentId; // 智能体ID + final String sessionId; // 会话ID final String message; final String sender; final String timestamp; @@ -14,6 +15,7 @@ class ChatMessage { ChatMessage({ this.id, + required this.agentId, required this.sessionId, required this.message, required this.sender, @@ -24,6 +26,7 @@ class ChatMessage { Map toMap() { return { 'id': id, + 'agentid': agentId, 'sessionId': sessionId, 'message': message, 'sender': sender, @@ -35,6 +38,7 @@ class ChatMessage { factory ChatMessage.fromMap(Map map) { return ChatMessage( id: map['id'], + agentId: map['agentId'], sessionId: map['sessionId'], message: map['message'], sender: map['sender'], @@ -44,8 +48,6 @@ class ChatMessage { } } - - /// 分页结果数据模型 class PagedResult { final List messages; @@ -69,6 +71,7 @@ class ChatStorage { /// 保存一条聊天记录 static Future> saveMessage({ + required String agentId, required String sessionId, required String message, required String sender, @@ -76,6 +79,7 @@ class ChatStorage { }) async { try { final result = await _channel.invokeMethod('saveMessage', { + 'agentId': agentId, 'sessionId': sessionId, 'message': message, 'sender': sender, @@ -89,13 +93,13 @@ class ChatStorage { /// 获取指定会话的聊天记录(支持分页) static Future getMessages({ - required String sessionId, + required String agentId, int page = 1, int pageSize = 20, }) async { try { final String result = await _channel.invokeMethod('getMessages', { - 'sessionId': sessionId, + 'agentId': agentId, 'page': page, 'pageSize': pageSize, }); @@ -125,20 +129,18 @@ class ChatStorage { } } - - /// 删除聊天记录,可按会话ID或消息ID列表删除 static Future> deleteMessages({ - String? sessionId, + String? agentId, List? messageIds, }) async { try { - if (sessionId == null && (messageIds == null || messageIds.isEmpty)) { + if (agentId == null && (messageIds == null || messageIds.isEmpty)) { return {'success': false, 'error': '必须提供sessionId或messageIds'}; } final result = await _channel.invokeMethod('deleteMessages', { - 'sessionId': sessionId, + 'agentId': agentId, 'messageIds': messageIds, }); return Map.from(result); @@ -156,6 +158,4 @@ class ChatStorage { return {'success': false, 'error': e.toString()}; } } - - }