From c15831c398e9ce757f474adc8c3dc3804d393ea5 Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Tue, 14 Oct 2025 20:16:37 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E4=BC=98=E5=8C=96=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../agent/controllers/agent_controller.dart | 16 ++++- lib/modules/agent/views/agent_view.dart | 34 +++++++++ lib/modules/agent/views/message_bubble.dart | 18 +++-- .../agent_service/AgentServiceImpl.swift | 51 ++++++++------ .../chat_storage/ChatStorageHelper.swift | 70 ++++++++++++------- pubspec.yaml | 2 +- 6 files changed, 139 insertions(+), 52 deletions(-) diff --git a/lib/modules/agent/controllers/agent_controller.dart b/lib/modules/agent/controllers/agent_controller.dart index b857ddc49..8c6072993 100644 --- a/lib/modules/agent/controllers/agent_controller.dart +++ b/lib/modules/agent/controllers/agent_controller.dart @@ -79,7 +79,8 @@ class AgentController extends GetxController with WidgetsBindingObserver { final isTtsEnabled = true.obs; // TTS播报打断启用状态 final isTtsInterruptEnabled = true.obs; - + // 是否显示工具调用 + final isShowToolInterruptEnabled = false.obs; // 图片处理状态 final isImageProcessing = false.obs; @@ -272,6 +273,19 @@ class AgentController extends GetxController with WidgetsBindingObserver { } } + /// 切换TTS播报打断状态 + /// 当启用播报打断时,用户说话会自动停止当前的TTS播放 + Future toggleShowAIToolInterrupt() async { + isShowToolInterruptEnabled.toggle(); + + // 保存偏好设置 + try { + await AgentService.isInterrupt(); + } catch (e) { + Logger.e(TAG, '保存TTS播报打断偏好设置失败: $e'); + } + } + // 设置文本输入监听器 void _setupTextInputListener() { textController.addListener(() { diff --git a/lib/modules/agent/views/agent_view.dart b/lib/modules/agent/views/agent_view.dart index 04e0b52c3..d24352ca6 100644 --- a/lib/modules/agent/views/agent_view.dart +++ b/lib/modules/agent/views/agent_view.dart @@ -172,6 +172,39 @@ class _AgentViewState extends State with WidgetsBindingObserver { ), color: isDarkMode ? Colors.grey[800] : Colors.white, itemBuilder: (context) => [ + // 播报打断开关选项 + PopupMenuItem( + child: Obx(() => Row( + children: [ + Icon( + controller.isShowToolInterruptEnabled.value + ? Icons.build_circle + : Icons.build_circle_outlined, + color: controller.isShowToolInterruptEnabled.value + ? primaryColor + : (isDarkMode ? Colors.grey[400] : Colors.grey), + size: 20.sp, + ), + SizedBox(width: 8.w), + Text( + //'ttsInterruptToggle'.tr, + "显示工具", + style: TextStyle(fontSize: 14.sp), + ), + const Spacer(), + Switch( + value: controller.isShowToolInterruptEnabled.value, + onChanged: (value) => + controller.toggleShowAIToolInterrupt(), + activeColor: primaryColor, + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + ), + ], + )), + onTap: () => controller.toggleTtsInterrupt(), + ), + // 播报打断开关选项 PopupMenuItem( child: Obx(() => Row( @@ -204,6 +237,7 @@ class _AgentViewState extends State with WidgetsBindingObserver { )), onTap: () => controller.toggleTtsInterrupt(), ), + // 清除聊天记录选项 PopupMenuItem( child: Row( diff --git a/lib/modules/agent/views/message_bubble.dart b/lib/modules/agent/views/message_bubble.dart index bf2b04983..266ee691a 100644 --- a/lib/modules/agent/views/message_bubble.dart +++ b/lib/modules/agent/views/message_bubble.dart @@ -84,7 +84,8 @@ class MessageBubble extends GetView { ], ), child: Padding( - padding: (!isUser && + padding: (controller.isShowToolInterruptEnabled.value && + !isUser && functionCalls != null && functionCalls!.isNotEmpty) ? const EdgeInsets.only(top: 30) @@ -93,7 +94,8 @@ class MessageBubble extends GetView { ), ), //工具调用流程显示(仅在AI消息且有functionCalls时显示) - if (!isUser && + if (controller.isShowToolInterruptEnabled.value && + !isUser && functionCalls != null && functionCalls!.isNotEmpty) _buildFunctionCallsWidget(context), @@ -183,12 +185,20 @@ class MessageBubble extends GetView { ); } - if (!isUser && message.isEmpty && functionCalls == null && meta == null) { + if (!isUser && + message.isEmpty && + (functionCalls == null || + !controller.isShowToolInterruptEnabled.value) && + meta == null) { return _buildTypingIndicator(); } // 如果只有工具调用而没有文本内容,显示一个占位符确保气泡有足够宽度 - if (!isUser && message.isEmpty && functionCalls != null && meta == null) { + if (!isUser && + message.isEmpty && + functionCalls != null && + controller.isShowToolInterruptEnabled.value && + meta == null) { return Container( constraints: const BoxConstraints( minWidth: 200, // 确保最小宽度 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 000f02583..01c754fb2 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 @@ -657,6 +657,15 @@ audioStream.saveAudioDataTo(data: audioData) messages.append(userMessage) os_log("添加用户消息", log: logger, type: .info) + // 保存用户消息 + self.chatStorageHelper.saveMessage( + agentId: self.agentId, + sessionId: sessionid, + message: displayText, + sender: "user", + metadata:"" + ) + addToHistoryMessages(chatApiService.createUserMessage(content: displayText)) currsessionId = sessionid let callback = ChatApiStreamCallback( @@ -781,16 +790,9 @@ audioStream.saveAudioDataTo(data: audioData) */ internal func saveChatMessage(sessionId:String,userMessage: String, assistantMessage: String, metadata: String = "",userMetadata: String) { DispatchQueue.global(qos: .utility).async { - // 保存用户消息 - let userMessageId = self.chatStorageHelper.saveMessage( - agentId: self.agentId, - sessionId: sessionId, - message: userMessage, - sender: "user", - metadata: userMetadata - ) + - if userMessageId != -1 { +// if userMessageId != -1 { if !assistantMessage.isEmpty || !metadata.isEmpty { // 保存AI回复 let assistantMessageId = self.chatStorageHelper.saveMessage( @@ -804,9 +806,9 @@ audioStream.saveAudioDataTo(data: audioData) os_log("保存助手消息失败", log: self.logger, type: .error) } } - } else { - os_log("保存用户消息失败", log: self.logger, type: .error) - } +// } else { +// os_log("保存用户消息失败", log: self.logger, type: .error) +// } } } @@ -817,6 +819,7 @@ internal func autoHandleFunctionCallResult(toolName: String, functionCallResult: guard let contextValue = functionCallResult["context"] else { return [ "meta": "", + "iscard":false, "broadcast": true, "notreply": false, ] @@ -884,9 +887,9 @@ private func handleJSONDictionary(_ jsonDict: [String: Any], toolName: String) - metadata["isinternal"] = isinternal } else if let cardQQMusiclist = jsonDict["data"] as? [[String: Any]], toolName == "qqmusic_searchandplay_songs" { processQQMusicPlayList(cardQQMusiclist) - } else if toolName == "maps_weather" { + } else if toolName == "map_weather" { iscard = true - }else if toolName == "map_weather" { + }else if toolName == "maps_weather" { iscard = true } metadata["iscard"] = iscard @@ -900,6 +903,7 @@ private func handleJSONDictionary(_ jsonDict: [String: Any], toolName: String) - return [ "meta": metaStr, + "iscard":iscard, "broadcast": broadcast, "notreply": notreply ] @@ -1788,11 +1792,15 @@ class ChatApiStreamCallback: StreamCallback { // agentService.audioPlayer?.stopCallingSound() let resultmap = agentService.autoHandleFunctionCallResult(toolName:name,functionCallResult:functionCallResult) - metadata = resultmap["meta"] as? String ?? "" broadcast = resultmap["broadcast"] as? Bool ?? true reply = !(resultmap["notreply"] as? Bool ?? true) - iscard = !(resultmap["iscard"] as? Bool ?? false) + iscard = resultmap["iscard"] as? Bool ?? false + os_log("收到函数调用结果! broadcast: %{public}@ reply: %{public}@ %{iscard}@", + log: agentService.logger, + type: .info, + "\(broadcast)", "\(reply)", "\(iscard)") if (iscard){ + metadata = resultmap["meta"] as? String ?? "" agentService.sendEvent(name: "function_call_result", data: [ "sessionid":sessionid, "function_name": name, @@ -1800,10 +1808,13 @@ class ChatApiStreamCallback: StreamCallback { "result": functionCallResult, "meta":metadata, ]) - os_log("收到函数调用结果! broadcast: %{public}@ reply: %{public}@ %{metadata}@", - log: agentService.logger, - type: .info, - "\(broadcast)", "\(reply)", "\(metadata)") + }else{ + agentService.sendEvent(name: "function_call_result", data: [ + "sessionid":sessionid, + "function_name": name, + "function_call": functionCall, + "result": functionCallResult, + ]) } }catch{ 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 e1f0515e8..b79f17bed 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 @@ -16,21 +16,21 @@ public class ChatStorageHelper { private let logger = OSLog(subsystem: "com.yunqiinnovation.chat_storage", category: "ChatStorageHelper") private init() { - // 获取文档目录路径 + // 获取文档目录路径(不变) let fileURL = try! FileManager.default .url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false) .appendingPathComponent("chat_storage.db") dbPath = fileURL.path - // 打开数据库 + // 打开数据库(不变) if sqlite3_open(dbPath, &db) != SQLITE_OK { let errmsg = String(cString: sqlite3_errmsg(db)!) os_log("无法打开数据库: %{public}@", log: logger, type: .error, errmsg) return } - // 创建消息表 + // 关键修改:新增 UNIQUE (agent_id, session_id, sender) 联合唯一索引 let createTableString = """ CREATE TABLE IF NOT EXISTS messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -39,9 +39,10 @@ public class ChatStorageHelper { message TEXT NOT NULL, sender TEXT NOT NULL, metadata TEXT, - created_at INTEGER NOT NULL + created_at INTEGER NOT NULL, + UNIQUE (agent_id, session_id, sender) ON CONFLICT REPLACE ); - CREATE INDEX IF NOT EXISTS idx_agent_id ON messages (session_id); + CREATE INDEX IF NOT EXISTS idx_agent_id ON messages (agent_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); """ @@ -66,38 +67,46 @@ public class ChatStorageHelper { * @param metadata 可选的元数据 * @return 插入的消息ID,失败则返回-1 */ - 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 (?, ?, ?, ?, ?, ?);" + public func saveMessage(agentId: String, sessionId: String, message: String, sender: String, metadata: String?) -> Int64 { + // 关键修改:用 INSERT OR REPLACE 替换 INSERT,支持冲突时更新 + let insertStatementString = """ + INSERT OR REPLACE 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) + sqlite3_bind_text(insertStatement, 2, (sessionId as NSString).utf8String, -1, nil) + sqlite3_bind_text(insertStatement, 3, (message as NSString).utf8String, -1, nil) + sqlite3_bind_text(insertStatement, 4, (sender as NSString).utf8String, -1, nil) if let metadata = metadata { - sqlite3_bind_text(insertStatement, 4, (metadata as NSString).utf8String, -1, nil) + sqlite3_bind_text(insertStatement, 5, (metadata as NSString).utf8String, -1, nil) } else { - sqlite3_bind_null(insertStatement, 4) + sqlite3_bind_null(insertStatement, 5) } let currentTime = Int(Date().timeIntervalSince1970) - sqlite3_bind_int(insertStatement, 5, Int32(currentTime)) + sqlite3_bind_int(insertStatement, 6, Int32(currentTime)) + // 执行语句(冲突时会自动替换,返回新的 rowid) if sqlite3_step(insertStatement) == SQLITE_DONE { - let id = sqlite3_last_insert_rowid(db) + let id = sqlite3_last_insert_rowid(db) // 替换后返回新的 id(原 id 会被删除) sqlite3_finalize(insertStatement) + os_log("消息保存成功(新增/更新),id: %{public}lld", log: logger, type: .info, id) return id } else { let errmsg = String(cString: sqlite3_errmsg(db)!) - os_log("插入消息失败: %{public}@", log: logger, type: .error, errmsg) + os_log("插入/更新消息失败: %{public}@", log: logger, type: .error, errmsg) } sqlite3_finalize(insertStatement) } else { let errmsg = String(cString: sqlite3_errmsg(db)!) - os_log("插入消息语句准备失败: %{public}@", log: logger, type: .error, errmsg) + os_log("插入/更新消息语句准备失败: %{public}@", log: logger, type: .error, errmsg) } return -1 @@ -131,7 +140,7 @@ public class ChatStorageHelper { let totalPages = totalCount == 0 ? 0 : (totalCount + pageSize - 1) / pageSize let queryString = """ - SELECT id, message, sender, metadata, created_at + SELECT id, session_id, message, sender, metadata, created_at FROM messages WHERE agent_id = ? ORDER BY created_at DESC @@ -148,16 +157,19 @@ public class ChatStorageHelper { while sqlite3_step(queryStatement) == SQLITE_ROW { let id = sqlite3_column_int(queryStatement, 0) - let messagePtr = sqlite3_column_text(queryStatement, 1) + let sessionIdPtr = sqlite3_column_text(queryStatement, 1) + let sessionId = sessionIdPtr != nil ? String(cString: sessionIdPtr!) : "" + + let messagePtr = sqlite3_column_text(queryStatement, 2) let message = messagePtr != nil ? String(cString: messagePtr!) : "" - let senderPtr = sqlite3_column_text(queryStatement, 2) + let senderPtr = sqlite3_column_text(queryStatement, 3) let sender = senderPtr != nil ? String(cString: senderPtr!) : "" - let metadataPtr = sqlite3_column_text(queryStatement, 3) + let metadataPtr = sqlite3_column_text(queryStatement, 4) let metadata = metadataPtr != nil ? String(cString: metadataPtr!) : nil - let createdAt = sqlite3_column_int(queryStatement, 4) + let createdAt = sqlite3_column_int(queryStatement, 5) // 将时间戳转换为ISO 8601格式的字符串 let date = Date(timeIntervalSince1970: TimeInterval(createdAt)) @@ -167,6 +179,7 @@ public class ChatStorageHelper { var messageDict: [String: Any] = [ "id": id, "agentId": agentId, // 添加agentId字段 + "sessionId": sessionId, // 添加sessionId字段 "message": message, "sender": sender, "timestamp": timestamp // 使用timestamp而不是created_at @@ -323,7 +336,7 @@ public class ChatStorageHelper { // 构建查询语句 - 按时间倒序获取最近的N条,然后在结果中再按时间正序 let queryString = """ SELECT * FROM ( - SELECT id, message, sender, metadata, created_at + SELECT id, session_id, message, sender, metadata, created_at FROM messages WHERE agent_id = ? ORDER BY created_at DESC @@ -340,19 +353,24 @@ public class ChatStorageHelper { while sqlite3_step(queryStatement) == SQLITE_ROW { let id = sqlite3_column_int(queryStatement, 0) - let messagePtr = sqlite3_column_text(queryStatement, 1) + let sessionIdPtr = sqlite3_column_text(queryStatement, 1) + let sessionId = sessionIdPtr != nil ? String(cString: sessionIdPtr!) : "" + + + let messagePtr = sqlite3_column_text(queryStatement, 2) let message = messagePtr != nil ? String(cString: messagePtr!) : "" - let senderPtr = sqlite3_column_text(queryStatement, 2) + let senderPtr = sqlite3_column_text(queryStatement, 3) let sender = senderPtr != nil ? String(cString: senderPtr!) : "" - let metadataPtr = sqlite3_column_text(queryStatement, 3) + let metadataPtr = sqlite3_column_text(queryStatement, 4) let metadata = metadataPtr != nil ? String(cString: metadataPtr!) : nil - let createdAt = sqlite3_column_int(queryStatement, 4) + let createdAt = sqlite3_column_int(queryStatement, 5) var messageDict: [String: Any] = [ "id": id, + "sessionId":sessionId, "message": message, "sender": sender, "created_at": createdAt diff --git a/pubspec.yaml b/pubspec.yaml index 16f6cc2df..8514b8fb9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: voitrans description: "Voitrans - AI Voice Assistant." publish_to: "none" -version: 1.0.16+41 +version: 1.0.17+46 environment: sdk: ">=3.3.0 <4.0.0"