From a3d8c31812b04492e4650e7b3c989d1f807e4198 Mon Sep 17 00:00:00 2001 From: wolfplus Date: Tue, 27 May 2025 10:26:43 +0100 Subject: [PATCH] add --- .../ios/Classes/AgentServiceImpl.swift | 303 +----------------- .../ios/Classes/BleAccessorySetup.swift | 61 +--- .../ble_service/ios/Classes/BleService.swift | 4 +- 3 files changed, 15 insertions(+), 353 deletions(-) diff --git a/local_plugins/agent_service/ios/Classes/AgentServiceImpl.swift b/local_plugins/agent_service/ios/Classes/AgentServiceImpl.swift index c0ced928e..9705d3706 100644 --- a/local_plugins/agent_service/ios/Classes/AgentServiceImpl.swift +++ b/local_plugins/agent_service/ios/Classes/AgentServiceImpl.swift @@ -8,43 +8,26 @@ import os.log import agent_service import open_ai_service -// 导入AudioSessionHub,确保可以使用它的功能 -// 由于它在同一个目录中,不需要额外的模块名 - -/// 代理服务事件监听器接口 protocol AgentServiceListener: AnyObject { - /// 当事件发生时调用 - /// - Parameters: - /// - eventName: 事件名称 - /// - data: 事件数据 func onEvent(eventName: String, data: [String: Any]) } -/// 代理服务实现类,负责语音识别、OpenAI对话和语音合成 class AgentServiceImpl: NSObject { - // MARK: - 常量 private let TAG = "AgentServiceImpl" - // MARK: - 单例实现 static let shared = AgentServiceImpl() - // MARK: - 属性 - - // 监听器数组 private var listeners = [AgentServiceListener]() private let listenersLock = NSLock() - // Azure 语音相关 private var azureSpeechKey: String = "" private var azureSpeechRegion: String = "" private var isSpeaking: Bool = false - // 语音识别和合成服务 private var azureAsrHelper: AzureAsrHelper? internal var azureTtsHelper: AzureTtsHelper? - // OpenAI相关 private var openAIService: OpenAIService? private var apiKey: String = "" private var baseUrl: String = "https://api.openai.com/v1/chat/completions" @@ -52,53 +35,33 @@ class AgentServiceImpl: NSObject { private var systemPrompt: String = "" private var chatHistory: [[String: Any]] = [] - // MCP服务相关 private var mcpServer: String = "" - // 状态标志 private var isInitialized: Bool = false private var isRecognizing: Bool = false private var hasSpeechDetected: Bool = false internal var isAiStreaming: Bool = false - // 空闲检测相关 private var idleTimer: DispatchSourceTimer? - private let maxIdleSeconds: TimeInterval = 10 // 最大空闲秒数 - - // 音频播放器 + private let maxIdleSeconds: TimeInterval = 10 + internal var audioPlayer: AudioPlayer? - - // 日志对象 internal let logger = OSLog(subsystem: "com.yunqiinnovation.agent_service", category: "AgentServiceImpl") - // MARK: - 初始化 - - /// 私有初始化方法 private override init() { super.init() - - // 初始化音频播放器 audioPlayer = AudioPlayer() - // 初始化语音助手服务 azureAsrHelper = AzureAsrHelper() azureTtsHelper = AzureTtsHelper() - // 初始化OpenAI服务 openAIService = OpenAIService() - // 设置BleService回调 BleService.shared.setDelegate(self) - - os_log("AgentServiceImpl已初始化", log: logger, type: .debug) } - // MARK: - 监听器管理 - - /// 添加事件监听器 - /// - Parameter listener: 要添加的监听器 func addListener(_ listener: AgentServiceListener) { listenersLock.lock() if !listeners.contains(where: { $0 === listener }) { @@ -107,25 +70,18 @@ class AgentServiceImpl: NSObject { listenersLock.unlock() } - /// 移除事件监听器 - /// - Parameter listener: 要移除的监听器 func removeListener(_ listener: AgentServiceListener) { listenersLock.lock() listeners.removeAll(where: { $0 === listener }) listenersLock.unlock() } - /// 清除所有监听器 func clearListeners() { listenersLock.lock() listeners.removeAll() listenersLock.unlock() } - /// 向所有监听器发送事件 - /// - Parameters: - /// - eventName: 事件名称 - /// - data: 事件数据 internal func sendEvent(name eventName: String, data: [String: Any]) { listenersLock.lock() let currentListeners = self.listeners @@ -136,29 +92,19 @@ class AgentServiceImpl: NSObject { } } - /// 发送错误事件 - /// - Parameter message: 错误信息 private func sendError(_ message: String, code: String = "ERROR") { sendEvent(name: "error", data: ["code": code, "message": message]) } - // MARK: - 初始化配置 - - /// 初始化系统提示词 private func initSystemPrompt() { systemPrompt = """ 你是小言,一个具备专业能力的智能助手,需根据用户场景灵活切换回答模式,确保服务精准高效。 """ } - /// 初始化配置 - /// - Parameter config: 配置参数 - /// - Returns: 是否初始化成功 func initialize(config: [String: Any]) -> Bool { if isInitialized { return true } - os_log("初始化配置: %{public}@", log: logger, type: .info, config) - // 设置OpenAI配置 if let openaiApiKey = config["openaiApiKey"] as? String { self.apiKey = openaiApiKey } else { @@ -174,14 +120,12 @@ class AgentServiceImpl: NSObject { self.model = openaiModel } - // 设置系统提示词 if let customSystemPrompt = config["systemPrompt"] as? String, !customSystemPrompt.isEmpty { self.systemPrompt = customSystemPrompt } else { initSystemPrompt() } - // 设置Azure语音配置 if let azureSpeechKey = config["azureSpeechKey"] as? String { self.azureSpeechKey = azureSpeechKey } else { @@ -196,29 +140,21 @@ class AgentServiceImpl: NSObject { return false } - // 设置MCP服务 if let mcpServer = config["mcpServer"] as? String { self.mcpServer = mcpServer } - // 初始化Azure语音服务 let asrInitSuccess = initializeAzureSpeech() - // 初始化OpenAI服务 let openaiInitSuccess = initializeOpenAIService() isInitialized = asrInitSuccess && openaiInitSuccess return isInitialized } - // MARK: - Azure 语音服务 - - /// 初始化Azure语音服务 private func initializeAzureSpeech() -> Bool { - // 添加TTS事件监听器 azureTtsHelper?.addListener(self) - // 初始化ASR服务 guard let asrSuccess = azureAsrHelper?.initialize( subscriptionKey: azureSpeechKey, region: azureSpeechRegion, @@ -229,9 +165,8 @@ class AgentServiceImpl: NSObject { return false } - // 初始化TTS服务 guard let ttsSuccess = azureTtsHelper?.initialize( - ttsAppId: "", // Azure TTS不需要appId + ttsAppId: "", ttsAppToken: azureSpeechKey, ttsResource: azureSpeechRegion, language: "zh-CN" @@ -240,15 +175,11 @@ class AgentServiceImpl: NSObject { return false } - // 设置TTS语音 _ = azureTtsHelper?.setVoice("zh-CN-XiaoxiaoNeural") return true } - // MARK: - OpenAI 服务 - - /// 初始化OpenAI服务 private func initializeOpenAIService() -> Bool { guard let openAIService = openAIService else { sendError("OpenAI服务未创建", code: "OPENAI_INIT_ERROR") @@ -270,22 +201,16 @@ class AgentServiceImpl: NSObject { return true } - // MARK: - 空闲检测 - - /// 启动空闲检测 private func startIdleCheck() { - stopIdleCheck() // 先停止现有的检查 + stopIdleCheck() guard isRecognizing else { return } - - // 使用DispatchSourceTimer,iOS最佳实践 idleTimer = DispatchSource.makeTimerSource(queue: DispatchQueue.main) idleTimer?.schedule(deadline: .now() + maxIdleSeconds) idleTimer?.setEventHandler { [weak self] in guard let self = self else { return } - if self.isRecognizing && !self.hasSpeechDetected && !self.isSpeaking && !self.isAiStreaming { self.stopRecognition() self.sendEvent(name: "auto_stop", data: [ @@ -297,64 +222,44 @@ class AgentServiceImpl: NSObject { idleTimer?.resume() } - /// 停止空闲检测 private func stopIdleCheck() { guard let timer = idleTimer else { return } - // os_log("停止空闲检测", log: logger, type: .debug) timer.cancel() idleTimer = nil } - /// 重启空闲检测 private func restartIdleCheck() { if isRecognizing { - // os_log("重启空闲检测", log: logger, type: .debug) startIdleCheck() } } - // MARK: - 语音识别 - - /// 开始语音识别 - /// - Parameter useBle: 是否使用蓝牙设备作为音频源 - /// - Returns: 是否成功启动识别 func startRecognition(useBle: Bool = false) -> Bool { if !isInitialized { sendError("服务未初始化", code: "NOT_INITIALIZED") return false } - // 1. 如果不使用蓝牙,则检查麦克风权限 if !useBle { guard AVAudioSession.sharedInstance().recordPermission == .granted else { - os_log("无麦克风权限", log: logger, type: .error) sendError("无麦克风权限", code: "PERMISSION_DENIED") return false } } - // 2. 防止重复启动 if isRecognizing { stopRecognition() } - // 3. 选择合适的音频源类型 let audioSourceType: AzureAsrHelper.AudioSourceType = useBle ? .external : .microphone - os_log("使用音频源: %{public}@", log: logger, type: .debug, useBle ? "external" : "microphone") - // 4. 设置适当的音频会话模式 if useBle { - // BLE模式:使用external音频源,不设置音频会话 - // Azure SDK会通过拉流获取数据,无需系统音频会话管理 AudioSessionHub.shared.begin(.playback) } else { - - // 麦克风模式:需要播放和录音 AudioSessionHub.shared.begin(.voice) } - // 5. 启动语音识别(直接使用self作为回调) guard let success = azureAsrHelper?.startContinuousRecognition( callback: self, audioSourceType: audioSourceType @@ -366,41 +271,27 @@ class AgentServiceImpl: NSObject { return true } - /// 停止语音识别 - /// - Returns: 是否成功停止识别 func stopRecognition() -> Bool { if !isRecognizing { return true } - // 停止语音识别 guard let success = azureAsrHelper?.stopContinuousRecognition(), success else { return false } - // 回调已集成到主类中,无需额外清理 - return true } - /// 推送音频数据 - /// - Parameter audioData: 音频数据 - /// - Returns: 是否成功推送 func pushAudioData(_ audioData: Data) -> Bool { if !isRecognizing || !isInitialized { return false } - // 将音频数据推送到语音识别引擎 azureAsrHelper?.pushAudioData(data: audioData) return true } - // MARK: - 语音合成 - - /// 语音合成播放文本 - /// - Parameter text: 要播放的文本 - /// - Returns: 是否成功开始播放 func speakText(_ text: String) -> Bool { if !isInitialized { sendError("服务未初始化", code: "NOT_INITIALIZED") @@ -411,18 +302,14 @@ class AgentServiceImpl: NSObject { return false } - // 使用TTS服务进行语音合成 return azureTtsHelper?.speakOnce(text) ?? false } - /// 停止语音合成 - /// - Returns: 是否成功停止合成 func stopTts() -> Bool { if !isSpeaking { return true } - // 停止TTS服务 let success = azureTtsHelper?.stop() ?? false if success { @@ -434,24 +321,19 @@ class AgentServiceImpl: NSObject { return success } - /// 打断当前响应 - /// - Returns: 是否成功打断 func interruptCurrentResponse() -> Bool { var interrupted = false - // 停止TTS播放 if isSpeaking { interrupted = stopTts() || interrupted } - // 停止AI流输出 if isAiStreaming { isAiStreaming = false _ = openAIService?.cancelCurrentStream() interrupted = true } - // 发送打断事件 if interrupted { sendEvent(name: "response_interrupted", data: ["status": "interrupted"]) } @@ -459,13 +341,6 @@ class AgentServiceImpl: NSObject { return interrupted } - // MARK: - 文本处理 - - /// 处理文本输入 - /// - Parameters: - /// - text: 文本内容 - /// - speakResponse: 是否朗读响应 - /// - Returns: 是否成功处理 func processTextInput(_ text: String, speakResponse: Bool) -> Bool { if !isInitialized { sendError("服务未初始化", code: "NOT_INITIALIZED") @@ -477,66 +352,44 @@ class AgentServiceImpl: NSObject { return false } - // 使用OpenAI处理文本 processWithOpenAI(text: text, speakResponse: speakResponse) return true } - /// 使用OpenAI处理文本消息 - /// - Parameters: - /// - text: 用户输入文本 - /// - speakResponse: 是否使用TTS朗读回复 private func processWithOpenAI(text: String, speakResponse: Bool = true) { - os_log("用户问题: %{public}@", log: logger, type: .info, text) - guard let openAIService = openAIService else { sendError("OpenAI服务未初始化", code: "OPENAI_NOT_INITIALIZED") return } - // 创建用户消息 let userMessage = openAIService.createUserMessage(content: text) processWithOpenAIInternal(userMessage: userMessage, displayText: text, speakResponse: speakResponse) } - /// 内部方法:通用的OpenAI处理逻辑 - /// - Parameters: - /// - userMessage: 用户消息(可以是文本或图片格式) - /// - displayText: 用于显示和存储的文本 - /// - speakResponse: 是否朗读回复 - /// - hasImage: 是否包含图片 private func processWithOpenAIInternal(userMessage: [String: Any], displayText: String, speakResponse: Bool = true, hasImage: Bool = false) { guard let openAIService = openAIService else { sendError("OpenAI服务未初始化", code: "OPENAI_NOT_INITIALIZED") return } - // 如果有正在进行的AI流式输出,先停止它 if isAiStreaming { _ = interruptCurrentResponse() } - // 设置状态为正在流式输出 isAiStreaming = true - // 构建完整的消息列表 var messages: [[String: Any]] = [] - // 添加系统提示词 if !systemPrompt.isEmpty { messages.append(openAIService.createSystemMessage(content: systemPrompt)) } - // 添加历史消息 messages.append(contentsOf: chatHistory) - // 添加当前用户消息 messages.append(userMessage) - // 将用户消息添加到历史记录(注意:要去掉图片数据再保存) addToHistoryMessages(openAIService.createUserMessage(content: displayText)) - // 创建流式回调 let callback = OpenAIStreamCallback( agentService: self, speakResponse: speakResponse, @@ -544,18 +397,9 @@ class AgentServiceImpl: NSObject { hasImage: hasImage ) - // 发送流式请求 openAIService.sendMessageStream(messages: messages, callback: callback) } - // MARK: - 图片处理 - - /// 处理图片输入 - /// - Parameters: - /// - imagePath: 图片路径 - /// - text: 文本描述 - /// - speakResponse: 是否朗读响应 - /// - Returns: 是否成功处理 func processImageInput(imagePath: String, text: String, speakResponse: Bool) -> Bool { if !isInitialized { sendError("服务未初始化", code: "NOT_INITIALIZED") @@ -572,17 +416,14 @@ class AgentServiceImpl: NSObject { return false } - // 发送图片处理中事件 sendEvent(name: "image_processing", data: [ "status": "processing", "imagePath": imagePath ]) - // 异步处理图片 DispatchQueue.global(qos: .userInitiated).async { [weak self] in guard let self = self else { return } - // 将图片转换为Base64格式 guard let imageBase64 = openAIService.fileToBase64(filePath: imagePath) else { DispatchQueue.main.async { self.sendEvent(name: "error", data: [ @@ -594,17 +435,14 @@ class AgentServiceImpl: NSObject { } DispatchQueue.main.async { - // 通知图片准备完成 self.sendEvent(name: "image_ready", data: [ "status": "ready", "imagePath": imagePath ]) - // 创建带图片的用户消息 let userMessage = openAIService.createUserMessageWithImage(text: text, imageBase64: imageBase64) let displayText = text.isEmpty ? "[图片]" : text - // 处理包含图片的消息 self.processWithOpenAIInternal( userMessage: userMessage, displayText: displayText, @@ -617,21 +455,14 @@ class AgentServiceImpl: NSObject { return true } - // MARK: - 聊天历史管理 - - /// 添加消息到历史记录,保持最近10条 internal func addToHistoryMessages(_ message: [String: Any]) { chatHistory.append(message) - // 如果超过10条,删除最早的消息 while chatHistory.count > 10 { chatHistory.removeFirst() } } - /// 自动处理函数调用结果 - /// - Parameter functionCallResult: 函数调用结果 - /// - Returns: 处理后的元数据字符串 @discardableResult internal func autoHandleFunctionCallResult(_ functionCallResult: [String: Any]) -> String { guard let metaStr = functionCallResult["meta"] as? String, !metaStr.isEmpty else { @@ -644,10 +475,7 @@ class AgentServiceImpl: NSObject { return metaStr } - // 处理音乐卡片 if let cardMusic = meta["card_music"] as? [String: Any] { - os_log("检查到音乐卡片: %{public}@", log: logger, type: .info, cardMusic) - let id = cardMusic["id"] as? String ?? "" let url = cardMusic["url"] as? String ?? "" let name = cardMusic["name"] as? String ?? "" @@ -665,68 +493,43 @@ class AgentServiceImpl: NSObject { return metaStr } catch { - os_log("解析函数调用结果元数据失败: %{public}@", log: logger, type: .error, error.localizedDescription) return metaStr } } - /// 处理音乐播放 - /// - Parameter musicData: 音乐数据 private func processMusicPlay(_ musicData: [String: Any]) { - // 发送音乐播放事件 sendEvent(name: "music_play", data: musicData) - - // 这里可以添加实际的音乐播放逻辑 - // 例如调用音乐服务插件 - os_log("处理音乐播放: %{public}@", log: logger, type: .info, musicData["title"] as? String ?? "未知") } - /// 清除聊天历史 - /// - Returns: 是否成功清除 func clearChatHistory() -> Bool { chatHistory.removeAll() return true } - // MARK: - 资源释放 - - /// 释放资源 - /// - Returns: 是否成功释放 func dispose() -> Bool { - // 停止语音识别 if isRecognizing { stopRecognition() } - // 停止语音合成 if isSpeaking { stopTts() } - // 停止空闲检测 stopIdleCheck() - // 移除BleService代理 BleService.shared.setDelegate(nil) - // 释放Azure Speech资源 azureTtsHelper?.removeListener(self) azureAsrHelper?.dispose() azureTtsHelper?.dispose() - // ASR回调已集成到主类中,无需额外清理 - - // 释放OpenAI服务 openAIService?.cancelAll() openAIService = nil - // 释放音频播放器 audioPlayer = nil - // 清除监听器 clearListeners() - // 释放音频会话 AudioSessionHub.shared.appDidEnterBackground() isInitialized = false @@ -735,7 +538,6 @@ class AgentServiceImpl: NSObject { } } -// MARK: - OpenAI 流式回调实现 class OpenAIStreamCallback: StreamCallback { private weak var agentService: AgentServiceImpl? private let speakResponse: Bool @@ -755,10 +557,8 @@ class OpenAIStreamCallback: StreamCallback { responseBuilder += token - // 发送流式回复token agentService.sendEvent(name: "assistant_token", data: ["token": token]) - // 如果需要语音播报,则合成语音 if speakResponse { agentService.azureTtsHelper?.speakStream(token) } @@ -767,9 +567,6 @@ class OpenAIStreamCallback: StreamCallback { func onComplete() { guard let agentService = agentService else { return } - os_log("AI完整回复: %{public}@", log: agentService.logger, type: .info, responseBuilder) - - // 如果需要语音播报,则完成语音流 if speakResponse { agentService.azureTtsHelper?.flushStream() } @@ -777,7 +574,6 @@ class OpenAIStreamCallback: StreamCallback { let response = responseBuilder if !response.isEmpty { - // 发送完整回复,包含是否有图片的标记 var responseData: [String: Any] = [ "text": response, "userInput": displayText @@ -787,7 +583,6 @@ class OpenAIStreamCallback: StreamCallback { } agentService.sendEvent(name: "assistant_response", data: responseData) - // 添加AI回复到历史记录 let assistantMessage: [String: Any] = [ "role": "assistant", "content": response @@ -795,39 +590,31 @@ class OpenAIStreamCallback: StreamCallback { agentService.addToHistoryMessages(assistantMessage) } - // 标记AI流式输出已完成 agentService.isAiStreaming = false } func onError(_ error: Error) { guard let agentService = agentService else { return } - os_log("AI处理出错: %{public}@", log: agentService.logger, type: .error, error.localizedDescription) agentService.sendEvent(name: "error", data: [ "code": "AI_ERROR", "message": error.localizedDescription ]) - // 标记AI流式输出已完成 agentService.isAiStreaming = false } func onFunctionCall(_ functionCall: [String: Any]) { guard let agentService = agentService else { return } - os_log("收到函数调用: %{public}@", log: agentService.logger, type: .info, functionCall) - - // 播放函数调用提示音(循环播放) agentService.audioPlayer?.playCallingSound() - // 发送函数调用事件,与Android版本保持一致 let name = functionCall["name"] as? String ?? "" agentService.sendEvent(name: "function_call", data: [ "name": name, "arguments": functionCall ]) - // 如果是退出交互函数,停止识别 if name == "exit_interaction" { agentService.stopRecognition() } @@ -836,25 +623,17 @@ class OpenAIStreamCallback: StreamCallback { func onFunctionCallResult(_ functionCall: [String: Any], _ functionCallResult: [String: Any]) { guard let agentService = agentService else { return } - os_log("函数调用结果: %{public}@", log: agentService.logger, type: .info, functionCallResult) - - // 停止函数调用提示音(循环播放) agentService.audioPlayer?.stopCallingSound() - // 发送函数调用结果事件,与Android版本保持一致 agentService.sendEvent(name: "function_call_result", data: [ "function_call": functionCall, "result": functionCallResult ]) - // 自动处理函数调用结果(如音乐播放等) agentService.autoHandleFunctionCallResult(functionCallResult) } } - - -// MARK: - TtsEventListener 实现 extension AgentServiceImpl: TtsEventListener { func onEvent(_ event: TtsEvent) { switch event.type { @@ -904,95 +683,65 @@ extension AgentServiceImpl: TtsEventListener { } } -// MARK: - BleService.Callback 实现 extension AgentServiceImpl: BleService.Callback { func onScanResult(devices: [[String: Any]]) { - // 不处理扫描结果 } func onConnectionStateChanged(state: Int) { - // 可以在这里处理连接状态变化,例如通知UI更新 } func onAudioDataReceived(data: Data) { - // 直接将音频数据推送到Azure Speech进行识别 pushAudioData(data) } func onWakeupSignalReceived() { - os_log("收到BLE设备唤醒信号", log: logger, type: .info) - - // 停止当前TTS,避免冲突 stopTts() - // 如果已经在识别,则重新启动 stopRecognition() - // 使用BLE模式启动识别 startRecognition(useBle: true) } func onDeviceInfoReceived(infoType: Int, infoData: [String: Any]) { - // 可以处理设备信息,例如电量、设备名称等 } } -// MARK: - 音频播放器 - -/// 简单的音频播放器,用于播放提示音 class AudioPlayer { private var player: AVAudioPlayer? private var callingPlayer: AVAudioPlayer? private let logger = OSLog(subsystem: "com.yunqiinnovation.agent_service", category: "AudioPlayer") private let audioQueue = DispatchQueue(label: "agent.audio.player", qos: .userInitiated) - /// 播放开始识别提示音 func playStartSound() { playSound(named: "start", fileType: "mp3") } - /// 播放停止识别提示音 func playStopSound() { playSound(named: "stop", fileType: "mp3") } - /// 播放函数调用提示音(循环播放) func playCallingSound() { playLoopSound(named: "calling", fileType: "mp3") } - /// 停止函数调用提示音 func stopCallingSound() { callingPlayer?.stop() callingPlayer = nil - os_log("停止函数调用提示音", log: logger, type: .info) } - /// 播放音频文件 - /// - Parameters: - /// - named: 音频文件名 - /// - fileType: 音频文件类型 (默认mp3) private func playSound(named: String, fileType: String = "mp3") { - // 直接从插件Bundle查找资源(已确认这种方式可以找到) if let path = Bundle(for: type(of: self)).path(forResource: named, ofType: fileType) { let url = URL(fileURLWithPath: path) safePlayAudio(url: url) return } - os_log("未找到音频文件: %{public}@.%{public}@", log: logger, type: .error, named, fileType) } - /// 播放循环音频文件 - /// - Parameters: - /// - named: 音频文件名 - /// - fileType: 音频文件类型 (默认mp3) private func playLoopSound(named: String, fileType: String = "mp3") { - // 先停止之前的循环播放 stopCallingSound() - // 直接从插件Bundle查找资源 if let path = Bundle(for: type(of: self)).path(forResource: named, ofType: fileType) { let url = URL(fileURLWithPath: path) safePlayLoopAudio(url: url) @@ -1002,69 +751,46 @@ class AudioPlayer { os_log("未找到循环音频文件: %{public}@.%{public}@", log: logger, type: .error, named, fileType) } - /// 安全播放音频文件,处理各种异常情况 private func safePlayAudio(url: URL) { do { - // 1. 确保没有活跃的播放器 player?.stop() player = nil - // 3. 创建播放器 player = try AVAudioPlayer(contentsOf: url) - // 4. 确保播放器创建成功 guard let player = player else { - os_log("无法创建音频播放器", log: logger, type: .error) return } - // 5. 设置音量和准备播放 - player.volume = 0.7 // 降低音量以避免冲突 + player.volume = 0.7 player.prepareToPlay() - // 6. 异步播放,避免阻塞主线程 audioQueue.async { [weak self] in guard let self = self else { return } - // 开始播放 let playResult = player.play() - - // 等待播放完成 let playTime = player.duration + 0.5 Thread.sleep(forTimeInterval: playTime) - - // 播放完成不再需要处理AudioSessionHub } } catch { os_log("播放音频失败: %{public}@", log: logger, type: .error, error.localizedDescription) } } - /// 安全播放循环音频文件 private func safePlayLoopAudio(url: URL) { do { - // 创建循环播放器 callingPlayer = try AVAudioPlayer(contentsOf: url) - // 确保播放器创建成功 guard let callingPlayer = callingPlayer else { - os_log("无法创建循环音频播放器", log: logger, type: .error) return } - // 设置循环播放 - callingPlayer.numberOfLoops = -1 // -1 表示无限循环 - callingPlayer.volume = 0.5 // 降低音量 + callingPlayer.numberOfLoops = -1 + callingPlayer.volume = 0.5 callingPlayer.prepareToPlay() - // 开始播放 let playResult = callingPlayer.play() - if playResult { - os_log("开始播放函数调用提示音(循环)", log: logger, type: .info) - } else { - os_log("播放函数调用提示音失败", log: logger, type: .error) - } } catch { os_log("播放循环音频失败: %{public}@", log: logger, type: .error, error.localizedDescription) @@ -1072,7 +798,6 @@ class AudioPlayer { } } -// MARK: - ASR回调实现(使用Extension模式) extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback { func onResult(_ text: String, _ detectedLanguage: String) { if !text.isEmpty { @@ -1082,30 +807,23 @@ extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback { } sendEvent(name: "recognition_result", data: data) - // 处理识别结果,发送到OpenAI processTextInput(text, speakResponse: true) } - // 重置状态,继续识别 let previousHasSpeech = hasSpeechDetected hasSpeechDetected = false - // 只有在之前检测到语音的情况下才重启检测 if previousHasSpeech { restartIdleCheck() } } func onRecognizing(_ recognizing: String, _ detectedLanguage: String) { - os_log("识别中: %{public}@", log: logger, type: .info, recognizing) - // 只有在有实际语音内容时才更新状态 if !recognizing.isEmpty { - // 检测到语音,更新状态 let previousHasSpeech = hasSpeechDetected hasSpeechDetected = true if !previousHasSpeech { - os_log("检测到语音,重启空闲检测", log: logger, type: .debug) restartIdleCheck() } @@ -1115,7 +833,6 @@ extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback { } sendEvent(name: "recognizing", data: data) - // 如果TTS正在播放或AI正在生成,则触发打断 if isSpeaking || isAiStreaming { interruptCurrentResponse() } @@ -1123,19 +840,17 @@ extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback { } func onSessionStarted() { - os_log("开始识别", log: logger, type: .info) sendEvent(name: "recognition_started", data: ["status": "started"]) isRecognizing = true - hasSpeechDetected = false // 重置语音检测状态 + hasSpeechDetected = false startIdleCheck() audioPlayer?.playStartSound() } func onSessionStopped() { - os_log("停止识别", log: logger, type: .info) sendEvent(name: "recognition_stopped", data: ["status": "stopped"]) isRecognizing = false - hasSpeechDetected = false // 重置语音检测状态 + hasSpeechDetected = false stopIdleCheck() audioPlayer?.playStopSound() } diff --git a/local_plugins/ble_service/ios/Classes/BleAccessorySetup.swift b/local_plugins/ble_service/ios/Classes/BleAccessorySetup.swift index e9c561c83..93ba3ac23 100644 --- a/local_plugins/ble_service/ios/Classes/BleAccessorySetup.swift +++ b/local_plugins/ble_service/ios/Classes/BleAccessorySetup.swift @@ -28,7 +28,6 @@ class BleAccessorySetup: NSObject { // MARK: - 公共方法 func setBleService(_ service: Any) { self.bleService = service as? BleService - os_log("已设置BLE服务实例", log: logger, type: .debug) } func setDeviceDiscoveredCallback(_ callback: @escaping (UUID) -> Void) { @@ -38,17 +37,14 @@ class BleAccessorySetup: NSObject { // 处理发现的设备并连接 func connectToAccessory(uuid: UUID) { guard let service = bleService else { - os_log("BleService未设置,无法连接到设备", log: logger, type: .error) return } - // 连接设备 service.connectToAccessory(uuid: uuid) } // 从视图控制器显示设备选择器 - 主要入口点 func showDevicePickerFromViewController(_ viewController: UIViewController) { - // 初始化会话(如果尚未初始化) initializeSession() } @@ -61,15 +57,11 @@ class BleAccessorySetup: NSObject { // MARK: - 会话管理 func initializeSession() { - // 检查是否已在初始化或已初始化 if isInitializing || (session != nil && isSessionActivated) { - os_log("会话已在初始化或已激活", log: logger, type: .debug) return } - // 开始初始化过程 isInitializing = true - os_log("初始化设备发现会话", log: logger, type: .info) do { session = try ASAccessorySession() @@ -87,21 +79,17 @@ class BleAccessorySetup: NSObject { session = nil isSessionActivated = false isInitializing = false - os_log("会话已重置", log: logger, type: .debug) } // MARK: - 设备选择器显示 private func showDevicePicker() { guard let session = session, isSessionActivated else { - os_log("会话未激活,无法显示设备选择器", log: logger, type: .error) return } - // 创建设备描述符 let descriptor = ASDiscoveryDescriptor() descriptor.bluetoothCompanyIdentifier = ASBluetoothCompanyIdentifier(deviceCompanyIdentifier) - // 创建显示项 let deviceImage = UIImage(systemName: "headphones") ?? UIImage() let displayItem = ASPickerDisplayItem( name: deviceDisplayName, @@ -109,18 +97,13 @@ class BleAccessorySetup: NSObject { descriptor: descriptor ) - os_log("显示设备选择器", log: logger, type: .info) DispatchQueue.main.async { session.showPicker(for: [displayItem]) { [weak self] error in guard let self = self else { return } if let error = error { let nsError = error as NSError - os_log("设备选择器错误: %{public}@, 域: %{public}@, 错误码: %d", - log: self.logger, type: .error, - error.localizedDescription, nsError.domain, nsError.code) - } else { - os_log("设备选择器已显示", log: self.logger, type: .debug) + os_log("设备选择器错误: %{public}@", log: self.logger, type: .error, error.localizedDescription) } } } @@ -139,64 +122,48 @@ class BleAccessorySetup: NSObject { handleAccessoryRemoved(event.accessory) case .accessoryChanged: - os_log("设备已变更", log: logger, type: .debug) + break case .pickerDidPresent: - os_log("选择器已显示", log: logger, type: .info) + break case .pickerDidDismiss: - os_log("选择器已关闭", log: logger, type: .info) + break case .pickerSetupFailed: os_log("选择器设置失败: %{public}@", log: logger, type: .error, event.error?.localizedDescription ?? "未知错误") default: - os_log("未处理的事件类型: %d", log: logger, type: .debug, event.eventType.rawValue) + break } } private func handleSessionActivated() { - os_log("会话已激活", log: logger, type: .info) isSessionActivated = true isInitializing = false - // 检查是否有已授权设备 if let accessories = session?.accessories, !accessories.isEmpty { - // 二次进入:已有授权设备,可以先创建Central再连接 - os_log("发现%d个已授权设备", log: logger, type: .info, accessories.count) - - // 初始化BleService的Central if let service = bleService { - // 确保CBCentralManager已初始化 let _ = service.getCentralManager() - // 连接所有授权设备 for accessory in accessories { if let uuid = accessory.bluetoothIdentifier { - os_log("连接已授权设备: %{public}@", log: logger, type: .info, uuid.uuidString) onDeviceDiscovered?(uuid) connectToAccessory(uuid: uuid) } } } } else { - // 首次配对:不创建Central,直接显示设备选择器 - os_log("没有已授权设备,显示设备选择器", log: logger, type: .info) showDevicePicker() } } private func handleAccessoryAdded(_ accessory: ASAccessory?) { - os_log("发现已授权设备: %{public}@", log: logger, type: .info, accessory?.bluetoothIdentifier?.uuidString ?? "Unknown") - guard let accessory = accessory, let uuid = accessory.bluetoothIdentifier else { - os_log("发现设备但缺少UUID信息", log: logger, type: .error) return } - - // 通知发现设备并连接 onDeviceDiscovered?(uuid) connectToAccessory(uuid: uuid) } @@ -206,28 +173,22 @@ class BleAccessorySetup: NSObject { return } - os_log("设备已移除授权: %{public}@", log: logger, type: .info, uuid.uuidString) - - // 断开连接 if let service = bleService { service.disconnect() } } private func handleSessionInitError(_ error: Error) { - os_log("初始化会话失败: %{public}@", log: logger, type: .error, error.localizedDescription) isInitializing = false session = nil } // MARK: - 蓝牙状态检查 func isBluetoothAvailable() -> Bool { - // 使用AVAudioSession检查蓝牙连接 do { let audioSession = AVAudioSession.sharedInstance() let outputs = audioSession.currentRoute.outputs - // 检查蓝牙音频输出设备 for output in outputs { if output.portType == .bluetoothA2DP || output.portType == .bluetoothHFP || @@ -235,33 +196,21 @@ class BleAccessorySetup: NSObject { return true } } - - os_log("未检测到蓝牙音频设备", log: logger, type: .debug) } catch { os_log("检查蓝牙状态出错: %{public}@", log: logger, type: .error, error.localizedDescription) } - // 默认返回true避免阻止功能 return true } // MARK: - 清除配对关系 func clearAssociations() { - os_log("清除设备配对关系", log: logger, type: .info) - - // 先断开当前连接 resetBluetoothState() - // 清除会话授权关系 #if canImport(AccessorySetupKit) && swift(>=5.9) if #available(iOS 18.0, *) { - // 方法1: 重置整个会话 resetSession() - - // 重新初始化会话 initializeSession() - - os_log("会话已重置,所有授权关系已清除", log: logger, type: .info) } #endif } diff --git a/local_plugins/ble_service/ios/Classes/BleService.swift b/local_plugins/ble_service/ios/Classes/BleService.swift index ba38787da..038e98fe3 100644 --- a/local_plugins/ble_service/ios/Classes/BleService.swift +++ b/local_plugins/ble_service/ios/Classes/BleService.swift @@ -104,9 +104,7 @@ public class BleService: NSObject { frameSize: 640 // 40ms at 16kHz = 640 samples ) ?? false - if opusStarted { - os_log("Opus解码流启动成功,将在整个BLE生命周期内保持活跃", log: logger, type: .info) - } else { + if !opusStarted { os_log("Opus解码流启动失败", log: logger, type: .error) } }