From 38ae361e9e22d4fdecd8bc30d34532d3a6d5cbf6 Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Tue, 14 Oct 2025 16:01:33 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=8A=E4=BC=A0iOS=20=E7=BD=91=E7=BB=9C?= =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../agent_service/AgentServiceImpl.swift | 47 +++-- .../ble_service/SwiftBleServicePlugin.swift | 2 +- .../Sources/chat_api/ChatApiService.swift | 190 +++++++++++++----- .../chat_api/Sources/chat_api/MCPClient.swift | 20 +- .../chat_api/NetworkStateMonitor.swift | 179 +++++++++++++++++ 5 files changed, 352 insertions(+), 86 deletions(-) create mode 100644 local_plugins/chat_api/ios/chat_api/Sources/chat_api/NetworkStateMonitor.swift 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 282175fa4..01976fc8e 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 @@ -1712,24 +1712,25 @@ class ChatApiStreamCallback: StreamCallback { do { guard let agentService = try agentService else { return } - if sessionid == agentService.currsessionId { - // 停止等待音效 - agentService.audioPlayer?.stopAwaitSound() - - -// os_log("ChatAPI错误: code:%{public}@ message:%{public}@", log: agentService.logger, type: .error, code,message) if (sessionid != agentService.currsessionId) { return } + // 停止等待音效 + agentService.audioPlayer?.stopAwaitSound() + try agentService.sendEvent(name: "error", data: [ "sessionid":sessionid, "code": code, "message": message ]) -// agentService.isAiStreaming = false - } - os_log("因错误设置AI流式状态为false", log: agentService.logger, type: .info) + os_log( + "liwei--------------- AI Call onError code: %{public}d message: %{public}@ ", + log: agentService.logger, + type: .info, + code, // 直接传 Int 类型 + message // 假设 message 是 String 类型 + ) }catch{ print("liwei--------------- AI Call onError 异常: \(error)") } @@ -1966,16 +1967,23 @@ class AudioPlayer { } func stopAwaitSound() { - // 停止音频播放 - awaitPlayer?.stop() - awaitPlayer = nil - - - - // 停止超时定时器 - stopAwaitTimeout() - - os_log("停止等待音效播放", log: logger, type: .debug) + // 确保在主线程执行音频停止操作 + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + + // 停止音频播放 + if let player = self.awaitPlayer { + if player.isPlaying { + player.stop() + } + self.awaitPlayer = nil + } + + // 停止超时定时器 + self.stopAwaitTimeout() + + os_log("停止等待音效播放", log: self.logger, type: .debug) + } } private func playSound(named: String, fileType: String = "mp3") { @@ -2181,7 +2189,6 @@ extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback { func onError(sessionid:String ,_ errorCode: Int, _ error: String) { let data: [String: Any] = ["sessionid":sessionid,"code":errorCode, "message": error.isEmpty ? "未知错误" : error] - audioPlayer?.stopAwaitSound() sendEvent(name: "error", data: data) isRecognizing = false // 新增:出错时复位"启动中/待停止"状态 diff --git a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftBleServicePlugin.swift b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftBleServicePlugin.swift index eb7fc5215..7368fe88c 100644 --- a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftBleServicePlugin.swift +++ b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftBleServicePlugin.swift @@ -141,7 +141,7 @@ public class SwiftBleServicePlugin: NSObject, FlutterPlugin { case "getPairedMacAddress": let pairedUUID = BleService.shared.getPairedMacAddress() - result(nil) + result(pairedUUID) case "clearAssociations": BleService.shared.clearAssociations() result(true) diff --git a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatApiService.swift b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatApiService.swift index 17f4ab905..19e02ba87 100644 --- a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatApiService.swift +++ b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatApiService.swift @@ -60,9 +60,15 @@ public class ChatApiService: NSObject { private var toolCalls: [Int: ToolCallInfo] = [:] private var currSessionId = "" + // 网络监控 - 延迟初始化 + private var networkMonitor: NetworkStateMonitor! + // MARK: - 初始化 public override init() { super.init() + // 在这里初始化网络监控 + networkMonitor = NetworkStateMonitor() + networkMonitor.initialize(listener: self) } // MARK: - 公共方法 @@ -278,6 +284,11 @@ public func sendMessageStream(_ sessionId: String, messages: [[String: Any]]) { getSessionCallback(sessionId)?.onError(sessionId,1002,"服务未初始化") return } + //网络检测 + if (!checkNetworkStatus()) { + getSessionCallback(sessionId)?.onError(sessionId,1000,"当前网络不可用") + return + } // 保存当前消息和回调 currentMessages = messages @@ -433,7 +444,6 @@ private func roughTokenCount(text: String) -> Int { } /// 处理工具调用 - /// 处理工具调用 private func processToolCalls(_ sessionId:String) async -> Bool { // 验证工具调用集合不为空 if toolCalls.isEmpty { @@ -467,8 +477,6 @@ private func processToolCalls(_ sessionId:String) async -> Bool { // 转换为JSON对象格式 let jsonFunctionCall = convertMapToJsonObject(functionCall) getSessionCallback(sessionId)?.onFunctionCall(sessionId, jsonFunctionCall) - // 通知上层工具调用事件 - // getSessionCallback(sessionId)?.onFunctionCall(sessionId, functionCall) // 在后台处理工具调用 Task { [weak self] in @@ -479,73 +487,86 @@ private func processToolCalls(_ sessionId:String) async -> Bool { // 解析参数 let args = try self.parseJsonArguments(firstToolCall.arguments) - // 通过MCP客户端处理工具调用 + // 通过MCP客户端处理工具调用,添加8秒超时 var result: [String: Any] = [:] var isError = false var errorStr = "" + if let client = self.mcpClient { - // 调用MCP工具 - if let toolResult = await client.callTool(name: firstToolCall.name, arguments: args) { - print("[Session: \(sessionId)] AI调用工具结果: \(firstToolCall.name), 参数: \(args), 结果: \(toolResult)") - - // 统一结果格式 - if toolResult["isError"] as? Bool == true { - if let content = toolResult["content"] as? [[String: Any]], - let firstContent = content.first, - let errorText = firstContent["text"] as? String { - errorStr = errorText - result = ["context": errorText] + // 检查网络状态 + if !self.checkNetworkStatus() { + print("[Session: \(sessionId)] 网络连接不可用,工具调用失败") + result = ["context": "网络连接不可用,请检查网络设置"] + isError = true + } else { + // 使用8秒超时调用MCP工具 + do { + let toolResult = try await withTimeout(seconds: 8.0) { + return await client.callTool(name: firstToolCall.name, arguments: args) + } + + if let toolResult = toolResult { + print("[Session: \(sessionId)] AI调用工具结果: \(firstToolCall.name), 参数: \(args), 结果: \(toolResult)") + + // 统一结果格式 + if toolResult["isError"] as? Bool == true { + if let content = toolResult["content"] as? [[String: Any]], + let firstContent = content.first, + let errorText = firstContent["text"] as? String { + errorStr = errorText + result = ["context": errorText] + } else { + result = ["context": "Tool execution failed"] + } + } else if let context = toolResult["context"] { + // 本地函数结果 + result = ["context": context] + } else if let content = toolResult["content"] as? [[String: Any]], + let firstContent = content.first, + let text = firstContent["text"] as? String { + // MCP工具结果 + result = ["context": text] + } else { + print("[Session: \(sessionId)] MCP工具调用返回无法解析的结果") + result = ["context": "Tool call failed"] + } } else { - result = ["context": "Tool execution failed"] + print("[Session: \(sessionId)] MCP工具调用返回nil") + result = ["context": "Tool call failed"] } - } else if let context = toolResult["context"] { - // 本地函数结果 - result = ["context": context] - } else if let content = toolResult["content"] as? [[String: Any]], - let firstContent = content.first, - let text = firstContent["text"] as? String { - // MCP工具结果 - result = ["context": text] - } else { - print("[Session: \(sessionId)] MCP工具调用返回无法解析的结果") - result = ["context": "Tool call failed"] + } catch is TimeoutError { + print("[Session: \(sessionId)] 工具调用超时: \(firstToolCall.name)") + result = ["context": "工具调用超时,请重试"] + isError = true + } catch { + print("[Session: \(sessionId)] 工具调用异常: \(error.localizedDescription)") + result = ["context": "工具调用失败: \(error.localizedDescription)"] + isError = true } - } else { - print("[Session: \(sessionId)] MCP工具调用返回nil") - result = ["context": "Tool call failed"] } } else { // 工具不存在 result = ["context": "Tool not found: \(firstToolCall.name)"] } + // 再次检查会话是否仍然有效 if sessionId == self.currSessionId { -// if (firstToolCall.name != "set_user_profile_field"){ - // 处理结果 - if (!isError){ - self.getSessionCallback(sessionId)?.onFunctionCallResult( - sessionId, - functionCall, - result - ) - } - //else{ mcp 的错误需要交给ai处理 不需要返回到上层 -// self.getSessionCallback(sessionId)?.onError( -// sessionId, -// ChatApiException(errorStr) -// ) -// } - // 将结果发送回OpenAI继续对话 - await self.sendFunctionCallResultInternal( - sessionId: sessionId, - messages: self.currentMessages, - functionCall: functionCall, - functionResult: self.jsonToString(result) ?? "{}" + // 处理结果 + if (!isError){ + self.getSessionCallback(sessionId)?.onFunctionCallResult( + sessionId, + functionCall, + result ) -// }else{ -// self.getSessionCallback(sessionId)?.onComplete(sessionId) -// self.clearCurrentSession() -// } + } + + // 将结果发送回OpenAI继续对话 + await self.sendFunctionCallResultInternal( + sessionId: sessionId, + messages: self.currentMessages, + functionCall: functionCall, + functionResult: self.jsonToString(result) ?? "{}" + ) } } } catch { @@ -996,6 +1017,65 @@ private func convertMapToJsonObject(_ map: [String: Any]) -> [String: Any] { return chatTools } + + + /** + * 检查网络状态 + * @return 网络是否可用 + */ + private func checkNetworkStatus() -> Bool { + // 直接使用 NetworkStateMonitor 的 checkNetworkStatus 方法 + let isAvailable = networkMonitor.checkNetworkStatus() + return isAvailable + } + + + + /// 分析网络错误并返回错误代码和消息 + private func analyzeNetworkError(_ error: Error) -> (code: Int, message: String) { + let errorDescription = error.localizedDescription.lowercased() + + // 使用 NetworkStateMonitor 的网络错误检测 + if networkMonitor.isNetworkRelatedError(reason: errorDescription, errorDetails: "") { + if errorDescription.contains("timeout") { + return (1004, "网络请求超时") + } else if errorDescription.contains("connection") { + if errorDescription.contains("abort") { + return (1005, "网络连接中断") + } else if errorDescription.contains("reset") { + return (1006, "网络连接重置") + } else { + return (1007, "网络连接失败") + } + } else if errorDescription.contains("dns") || errorDescription.contains("host") { + return (1008, "域名解析失败") + } else if errorDescription.contains("ssl") || errorDescription.contains("tls") { + return (1009, "SSL/TLS 连接失败") + } else { + return (1010, "网络错误: \(error.localizedDescription)") + } + } + + // 非网络错误 + return (1003, "流式请求失败: \(error.localizedDescription)") + } + +} +// MARK: - NetworkStateListener +extension ChatApiService: NetworkStateMonitor.NetworkStateListener { + public func onNetworkAvailable() { + print("[ChatApiService] 网络连接恢复") + // 可以在这里通知上层网络恢复 + } + + public func onNetworkLost() { + print("[ChatApiService] 网络连接丢失") + // 网络丢失时中止当前请求 + if !currSessionId.isEmpty { + getSessionCallback(currSessionId)?.onError(currSessionId, 1001, "网络连接丢失") + abortCurrentSession() + } + } } /// 本地函数处理器 diff --git a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/MCPClient.swift b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/MCPClient.swift index 77ed130fc..ba4965442 100644 --- a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/MCPClient.swift +++ b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/MCPClient.swift @@ -450,7 +450,7 @@ public class MCPClient { private func initializeSystemFunctions() { do { - let handler = SystemFunctionHandler() + let handler: SystemFunctionHandler = SystemFunctionHandler() handler.registerAllFunctions(client: self) } catch { @@ -460,8 +460,8 @@ public class MCPClient { private func initializeMusicSystemFunction() { do { // 初始化QQ音乐功能 - let qqMusicHandler = MusicSystemFunctionHandler() - qqMusicHandler.registerAllFunctions(client: self) + let musicHandler: MusicSystemFunctionHandler = MusicSystemFunctionHandler() + musicHandler.registerAllFunctions(client: self) } catch { // 静默处理错误 } @@ -469,7 +469,7 @@ public class MCPClient { private func initializeQQMusicSystemFunction() { do { // 初始化QQ音乐功能 - let qqMusicHandler = QQMusicSystemFunctionHandler() + let qqMusicHandler: QQMusicSystemFunctionHandler = QQMusicSystemFunctionHandler() qqMusicHandler.registerAllFunctions(client: self) } catch { // 静默处理错误 @@ -479,26 +479,26 @@ public class MCPClient { public func connectToSSE(mcpConfigJson: String) async throws -> Bool { // 清除现有连接 - for client in subClients.values { + for client: MCPSubClient in subClients.values { await client.close() } subClients.removeAll() - guard let data = mcpConfigJson.data(using: .utf8) else { + guard let data: Data = mcpConfigJson.data(using: .utf8) else { return false } - guard let config = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { + guard let config: [String : Any] = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { return false } - guard let mcpServers = config["mcpServers"] as? [String: Any] else { + guard let mcpServers: [String : Any] = config["mcpServers"] as? [String: Any] else { return false } var connectedCount = 0 - for (serverId, serverConfig) in mcpServers { - guard let configDict = serverConfig as? [String: Any] else { + for (serverId: String, serverConfig) in mcpServers { + guard let configDict: [String : Any] = serverConfig as? [String: Any] else { continue } diff --git a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/NetworkStateMonitor.swift b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/NetworkStateMonitor.swift new file mode 100644 index 000000000..7d2a4135a --- /dev/null +++ b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/NetworkStateMonitor.swift @@ -0,0 +1,179 @@ +import Foundation +import SystemConfiguration +import Network + +/// 网络状态监听器(iOS) +public final class NetworkStateMonitor { + private let tag = "NetworkStateMonitor" + + // iOS 12+ + private var monitor: NWPathMonitor? + private let monitorQueue = DispatchQueue(label: "NetworkStateMonitor.queue") + + // iOS 10–11 回退 + private var reachability: SCNetworkReachability? + private var reachabilityQueue: DispatchQueue? + + private(set) var isNetworkAvailable: Bool = true + private weak var networkStateListener: NetworkStateListener? + + /// 网络状态监听接口 + public protocol NetworkStateListener: AnyObject { + func onNetworkAvailable() + func onNetworkLost() + } + + public init() {} + + /// 初始化网络状态监听 + public func initialize(listener: NetworkStateListener? = nil) { + networkStateListener = listener + initNetworkMonitoring() + } + + /// 检查当前网络状态 + /// - Returns: true 表示网络可用 + public func checkNetworkStatus() -> Bool { + if #available(iOS 12.0, *) { + // NWPathMonitor 只在回调里更新状态,因此这里返回缓存的 isNetworkAvailable + return isNetworkAvailable + } else { + // 使用 Reachability 即时检测 + var zeroAddress = sockaddr_in() + zeroAddress.sin_len = UInt8(MemoryLayout.size) + zeroAddress.sin_family = sa_family_t(AF_INET) + let ref = withUnsafePointer(to: &zeroAddress) { + $0.withMemoryRebound(to: sockaddr.self, capacity: 1) { + SCNetworkReachabilityCreateWithAddress(nil, $0) + } + } + guard let reachRef = ref else { return false } + var flags = SCNetworkReachabilityFlags() + if !SCNetworkReachabilityGetFlags(reachRef, &flags) { return false } + return Self.isReachable(flags: flags) + } + } + + /// 释放资源 + public func dispose() { + if #available(iOS 12.0, *) { + monitor?.cancel() + monitor = nil + } else { + if let reachability = reachability { + SCNetworkReachabilitySetDispatchQueue(reachability, nil) + SCNetworkReachabilitySetCallback(reachability, nil, nil) + } + reachability = nil + reachabilityQueue = nil + } + networkStateListener = nil + } + + /// 判断是否为网络相关错误(关键字简单匹配) + public func isNetworkRelatedError(reason: String, errorDetails: String) -> Bool { + let networkErrorKeywords = [ + "network","connection","timeout","unreachable", + "dns","socket","ssl","tls","certificate", + "网络","连接","超时","不可达" + ] + let combined = (reason + " " + errorDetails).lowercased() + return networkErrorKeywords.contains { combined.contains($0.lowercased()) } + } +} + +// MARK: - Private +private extension NetworkStateMonitor { + func initNetworkMonitoring() { + if #available(iOS 12.0, *) { + let monitor = NWPathMonitor() + self.monitor = monitor + monitor.pathUpdateHandler = { [weak self] path in + guard let self = self else { return } + let hasInternet = (path.status == .satisfied) + // 只有状态变化时才回调 + if hasInternet && !self.isNetworkAvailable { + self.isNetworkAvailable = true + self.networkStateListener?.onNetworkAvailable() + Self.log(self.tag, "网络连接可用") + } else if !hasInternet && self.isNetworkAvailable { + self.isNetworkAvailable = false + self.networkStateListener?.onNetworkLost() + Self.log(self.tag, "网络连接丢失") + } else { + Self.log(self.tag, "网络能力变化,是否有网络: \(hasInternet)") + } + } + monitor.start(queue: monitorQueue) + } else { + // iOS 10–11 使用 Reachability 回退 + var zeroAddress = sockaddr_in() + zeroAddress.sin_len = UInt8(MemoryLayout.size) + zeroAddress.sin_family = sa_family_t(AF_INET) + reachability = withUnsafePointer(to: &zeroAddress) { + $0.withMemoryRebound(to: sockaddr.self, capacity: 1) { + SCNetworkReachabilityCreateWithAddress(nil, $0) + } + } + guard let reachability = reachability else { + Self.log(tag, "初始化网络监听失败: Reachability 创建失败") + return + } + let queue = DispatchQueue(label: "NetworkStateMonitor.reachability") + reachabilityQueue = queue + + var context = SCNetworkReachabilityContext( + version: 0, + info: UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque()), + retain: nil, + release: nil, + copyDescription: nil + ) + let callback: SCNetworkReachabilityCallBack = { (_, flags, info) in + guard let info = info else { return } + let monitor = Unmanaged.fromOpaque(info).takeUnretainedValue() + let hasInternet = NetworkStateMonitor.isReachable(flags: flags) + if hasInternet && !monitor.isNetworkAvailable { + monitor.isNetworkAvailable = true + monitor.networkStateListener?.onNetworkAvailable() + NetworkStateMonitor.log(monitor.tag, "网络连接可用") + } else if !hasInternet && monitor.isNetworkAvailable { + monitor.isNetworkAvailable = false + monitor.networkStateListener?.onNetworkLost() + NetworkStateMonitor.log(monitor.tag, "网络连接丢失") + } else { + NetworkStateMonitor.log(monitor.tag, "网络能力变化,是否有网络: \(hasInternet)") + } + } + + if SCNetworkReachabilitySetCallback(reachability, callback, &context), + SCNetworkReachabilitySetDispatchQueue(reachability, queue) { + // 触发一次初始查询 + var flags = SCNetworkReachabilityFlags() + if SCNetworkReachabilityGetFlags(reachability, &flags) { + self.isNetworkAvailable = Self.isReachable(flags: flags) + } + } else { + Self.log(tag, "初始化网络监听失败: 设置回调/队列失败") + SCNetworkReachabilitySetCallback(reachability, nil, nil) + SCNetworkReachabilitySetDispatchQueue(reachability, nil) + self.reachability = nil + } + } + } + + static func isReachable(flags: SCNetworkReachabilityFlags) -> Bool { + // 经典 Reachability 判定 + let reachable = flags.contains(.reachable) + let requiresConnection = flags.contains(.connectionRequired) + let canConnectAutomatically = flags.contains(.connectionOnTraffic) || flags.contains(.connectionOnDemand) + let canConnectWithoutUser = canConnectAutomatically && !flags.contains(.interventionRequired) + return reachable && (!requiresConnection || canConnectWithoutUser) + } + + static func log(_ tag: String, _ message: String) { + #if DEBUG + print("[\(tag)] \(message)") + #endif + } +}