import Foundation import AVFoundation import ble_service import speech /// 代理服务事件监听器接口 protocol AgentServiceListener { /// 当事件发生时调用 /// - 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 = Set() private let listenersLock = NSLock() // Azure 语音相关 private var azureSpeechKey: String = "" private var azureSpeechRegion: String = "" private var isRecognizing: Bool = false private var isSpeaking: Bool = false // 语音识别和合成服务 private var azureAsrHelper: AzureAsrHelper? private var azureTtsHelper: AzureTtsHelper? // OpenAI相关 private var apiKey: String = "" private var baseUrl: String = "https://api.openai.com" private var model: String = "gpt-3.5-turbo" private var systemPrompt: String = "" private var chatHistory: [[String: Any]] = [] // MCP服务相关 private var mcpServer: String = "" // 状态标志 private var isInitialized: Bool = false private var hasSpeechDetected: Bool = false private var isAiStreaming: Bool = false // 空闲检测相关 private var idleCheckTimer: Timer? private let maxIdleSeconds: TimeInterval = 10 // 最大空闲秒数 // 音频播放器 private var audioPlayer: AudioPlayer? // MARK: - 初始化 /// 私有初始化方法 private override init() { super.init() // 设置音频会话 setupAudioSession() // 初始化音频播放器 audioPlayer = AudioPlayer() // 初始化语音助手服务 azureAsrHelper = AzureAsrHelper() azureTtsHelper = AzureTtsHelper() // 设置BleService回调 BleService.shared.setDelegate(self) } // MARK: - 监听器管理 /// 添加事件监听器 /// - Parameter listener: 要添加的监听器 func addListener(_ listener: AgentServiceListener) { listenersLock.lock() listeners.insert(listener) listenersLock.unlock() } /// 移除事件监听器 /// - Parameter listener: 要移除的监听器 func removeListener(_ listener: AgentServiceListener) { listenersLock.lock() listeners.remove(listener) listenersLock.unlock() } /// 清除所有监听器 func clearListeners() { listenersLock.lock() listeners.removeAll() listenersLock.unlock() } /// 向所有监听器发送事件 /// - Parameters: /// - eventName: 事件名称 /// - data: 事件数据 private func sendEvent(name eventName: String, data: [String: Any]) { listenersLock.lock() let currentListeners = self.listeners listenersLock.unlock() for listener in currentListeners { listener.onEvent(eventName: eventName, data: data) } } /// 发送错误事件 /// - 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 } // 设置OpenAI配置 if let openaiApiKey = config["openaiApiKey"] as? String { self.apiKey = openaiApiKey } else { sendError("OpenAI API密钥缺失", code: "CONFIG_ERROR") return false } if let openaiBaseUrl = config["openaiBaseUrl"] as? String { self.baseUrl = openaiBaseUrl } if let openaiModel = config["openaiModel"] as? String { 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 { sendError("Azure语音密钥缺失", code: "CONFIG_ERROR") return false } if let azureSpeechRegion = config["azureSpeechRegion"] as? String { self.azureSpeechRegion = azureSpeechRegion } else { sendError("Azure语音区域缺失", code: "CONFIG_ERROR") return false } // 设置MCP服务 if let mcpServer = config["mcpServer"] as? String { self.mcpServer = mcpServer } // 初始化Azure语音服务 let asrInitSuccess = initializeAzureSpeech() isInitialized = asrInitSuccess return asrInitSuccess } // MARK: - 音频会话设置 /// 设置音频会话 private func setupAudioSession() { let audioSession = AVAudioSession.sharedInstance() do { try audioSession.setCategory(.playAndRecord, mode: .default, options: [.defaultToSpeaker, .allowBluetooth]) try audioSession.setActive(true, options: .notifyOthersOnDeactivation) } catch { sendError("设置音频会话失败: \(error.localizedDescription)") } } // MARK: - Azure 语音服务 /// 初始化Azure语音服务 private func initializeAzureSpeech() -> Bool { // 添加事件监听器 azureAsrHelper?.addListener(self) azureTtsHelper?.addListener(self) // 初始化ASR服务 guard let asrSuccess = azureAsrHelper?.initialize( subscriptionKey: azureSpeechKey, region: azureSpeechRegion, supportedLanguages: ["zh-CN"] ), asrSuccess else { sendError("初始化语音识别服务失败", code: "ASR_INIT_ERROR") return false } // 初始化TTS服务 guard let ttsSuccess = azureTtsHelper?.initialize( ttsAppId: "", // Azure TTS不需要appId ttsAppToken: azureSpeechKey, ttsResource: azureSpeechRegion, language: "zh-CN" ), ttsSuccess else { sendError("初始化语音合成服务失败", code: "TTS_INIT_ERROR") return false } // 设置TTS语音 _ = azureTtsHelper?.setVoice("zh-CN-XiaoxiaoNeural") return true } // MARK: - 空闲检测 /// 启动空闲检测 private func startIdleCheck() { stopIdleCheck() // 先停止现有的检查 if !isRecognizing { return } // 创建一个新的定时器 idleCheckTimer = Timer.scheduledTimer(withTimeInterval: maxIdleSeconds, repeats: false) { [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: [ "reason": "idle_timeout", "seconds": self.maxIdleSeconds ]) } } } /// 停止空闲检测 private func stopIdleCheck() { idleCheckTimer?.invalidate() idleCheckTimer = nil } /// 重启空闲检测 private func restartIdleCheck() { if isRecognizing { startIdleCheck() } } // MARK: - 语音识别 /// 开始语音识别 /// - Returns: 是否成功启动识别 func startRecognition() -> Bool { if !isInitialized { sendError("服务未初始化", code: "NOT_INITIALIZED") return false } // 防止重复启动 if isRecognizing { stopRecognition() } // 启动语音识别 guard let success = azureAsrHelper?.startContinuousRecognition(), success else { sendError("启动语音识别失败", code: "RECOGNITION_START_ERROR") return false } 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 } // 将音频数据推送到语音识别引擎 return azureAsrHelper?.pushAudioData(audioData: audioData) ?? false } // MARK: - 语音合成 /// 语音合成播放文本 /// - Parameter text: 要播放的文本 /// - Returns: 是否成功开始播放 func speakText(_ text: String) -> Bool { if !isInitialized { sendError("服务未初始化", code: "NOT_INITIALIZED") return false } if text.isEmpty { return false } // 使用TTS服务进行语音合成 return azureTtsHelper?.speakOnce(text) ?? false } /// 停止语音合成 /// - Returns: 是否成功停止合成 func stopTts() -> Bool { if !isSpeaking { return true } // 停止TTS服务 return azureTtsHelper?.stop() ?? false } /// 打断当前响应 /// - Returns: 是否成功打断 func interruptCurrentResponse() -> Bool { var interrupted = false // 停止TTS播放 if isSpeaking { interrupted = stopTts() || interrupted } // 停止AI流输出 if isAiStreaming { isAiStreaming = false interrupted = true } // 发送打断事件 if interrupted { sendEvent(name: "response_interrupted", data: ["status": "interrupted"]) } return interrupted } // MARK: - 文本处理 /// 处理文本输入 /// - Parameters: /// - text: 文本内容 /// - speakResponse: 是否朗读响应 /// - Returns: 是否成功处理 func processTextInput(_ text: String, speakResponse: Bool) -> Bool { if !isInitialized { sendError("服务未初始化", code: "NOT_INITIALIZED") return false } if text.isEmpty { sendError("文本输入不能为空", code: "EMPTY_TEXT") return false } // TODO: 实现与OpenAI的通信 // 这里只是模拟处理 // 将用户消息添加到聊天历史 let userMessage: [String: Any] = [ "role": "user", "content": text ] chatHistory.append(userMessage) // 更新状态 isAiStreaming = true // 模拟助手响应 DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in guard let self = self else { return } // 模拟流式输出 let response = "这是对"\(text)"的回复。我是AI助手,很高兴为您服务。" // 逐词输出模拟 let words = response.split(separator: " ") for (index, word) in words.enumerated() { DispatchQueue.main.asyncAfter(deadline: .now() + Double(index) * 0.1) { [weak self] in guard let self = self, self.isAiStreaming else { return } self.sendEvent(name: "assistant_token", data: ["token": String(word) + " "]) // 如果需要语音播报,则合成语音 if speakResponse { self.azureTtsHelper?.speakStream(String(word) + " ") } } } // 完整响应 DispatchQueue.main.asyncAfter(deadline: .now() + Double(words.count) * 0.1 + 0.2) { [weak self] in guard let self = self else { return } // 结束AI流式输出 self.isAiStreaming = false // 发送完整响应 self.sendEvent(name: "assistant_response", data: ["text": response]) // 将助手消息添加到聊天历史 let assistantMessage: [String: Any] = [ "role": "assistant", "content": response ] self.chatHistory.append(assistantMessage) // 如果需要语音播报,则完成语音流 if speakResponse { self.azureTtsHelper?.flushStream() } } } return true } // MARK: - 图片处理 /// 处理图片输入 /// - Parameters: /// - imagePath: 图片路径 /// - text: 文本描述 /// - speakResponse: 是否朗读响应 /// - Returns: 是否成功处理 func processImageInput(imagePath: String, text: String, speakResponse: Bool) -> Bool { if !isInitialized { sendError("服务未初始化", code: "NOT_INITIALIZED") return false } if imagePath.isEmpty { sendError("图片路径不能为空", code: "EMPTY_IMAGE_PATH") return false } // 发送图片处理中事件 sendEvent(name: "image_processing", data: [:]) // TODO: 实现与OpenAI Vision API的通信 // 这里只是模拟处理 // 模拟图片处理 DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in guard let self = self else { return } // 图片就绪事件 self.sendEvent(name: "image_ready", data: [:]) // 更新状态 self.isAiStreaming = true // 模拟AI分析结果 let imageDescription = "这是一张图片分析结果。" let additionalText = text.isEmpty ? "" : "关于您提问的"\(text)"," let response = "\(imageDescription)\(additionalText)我可以看到这是一张图片。" // 发送AI助手token流 let words = response.split(separator: " ") for (index, word) in words.enumerated() { DispatchQueue.main.asyncAfter(deadline: .now() + Double(index) * 0.1) { [weak self] in guard let self = self, self.isAiStreaming else { return } let token = String(word) + " " self.sendEvent(name: "assistant_token", data: ["token": token]) // 如果需要语音播报,则合成语音 if speakResponse { self.azureTtsHelper?.speakStream(token) } } } // 发送完整响应 DispatchQueue.main.asyncAfter(deadline: .now() + Double(words.count) * 0.1 + 0.2) { [weak self] in guard let self = self else { return } // 结束AI流式输出 self.isAiStreaming = false self.sendEvent(name: "assistant_response", data: [ "text": response, "userInput": text.isEmpty ? "[图片]" : text, "hasImage": true ]) // 如果需要语音播报,则完成语音 if speakResponse { self.azureTtsHelper?.flushStream() } } } return true } // MARK: - 聊天历史管理 /// 清除聊天历史 /// - 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资源 azureAsrHelper?.removeListener(self) azureTtsHelper?.removeListener(self) azureAsrHelper?.dispose() azureTtsHelper?.dispose() // 释放音频播放器 audioPlayer = nil // 清除监听器 clearListeners() isInitialized = false return true } } // MARK: - AsrEventListener 实现 extension AgentServiceImpl: AsrEventListener { func onEvent(_ event: AsrEvent) { switch event.type { case .sessionStarted: sendEvent(name: "recognition_started", data: [:]) isRecognizing = true startIdleCheck() audioPlayer?.playStartSound() case .sessionStopped: sendEvent(name: "recognition_stopped", data: [:]) isRecognizing = false stopIdleCheck() audioPlayer?.playStopSound() case .recognizing: if let text = event.params["text"] as? String { // 检测到语音,更新状态 let previousHasSpeech = hasSpeechDetected hasSpeechDetected = true if !previousHasSpeech { restartIdleCheck() } var data: [String: Any] = ["text": text] if let language = event.params["language"] as? String, !language.isEmpty { data["language"] = language } sendEvent(name: "recognizing", data: data) // 如果TTS正在播放或AI正在生成,则触发打断 if isSpeaking || isAiStreaming { interruptCurrentResponse() } } case .recognized: if let text = event.params["text"] as? String, !text.isEmpty { var data: [String: Any] = ["text": text] if let language = event.params["language"] as? String, !language.isEmpty { data["language"] = language } sendEvent(name: "recognition_result", data: data) // 处理识别结果,发送到OpenAI processTextInput(text, speakResponse: true) } // 重置状态,继续识别 let previousHasSpeech = hasSpeechDetected hasSpeechDetected = false if previousHasSpeech { restartIdleCheck() } case .canceled: var data: [String: Any] = [:] if let reason = event.params["reason"] as? String { data["reason"] = reason } if let errorDetails = event.params["errorDetails"] as? String { data["details"] = errorDetails } sendEvent(name: "recognition_canceled", data: data) isRecognizing = false stopIdleCheck() case .error: var data: [String: Any] = [:] if let errorCode = event.params["errorCode"] as? String { data["errorCode"] = errorCode } if let message = event.params["message"] as? String { data["message"] = message } else if let error = event.params["error"] as? String { data["message"] = error } else { data["message"] = "未知错误" } sendEvent(name: "recognition_error", data: data) isRecognizing = false stopIdleCheck() default: break } } } // MARK: - TtsEventListener 实现 extension AgentServiceImpl: TtsEventListener { func onEvent(_ event: TtsEvent) { switch event.type { case .synthesisStarted: isSpeaking = true restartIdleCheck() sendEvent(name: "tts_started", data: [:]) case .synthesisCompleted: isSpeaking = false restartIdleCheck() sendEvent(name: "tts_completed", data: [:]) case .synthesisCanceled: isSpeaking = false restartIdleCheck() var data: [String: Any] = [:] if let reason = event.params["reason"] as? String { data["reason"] = reason } if let errorDetails = event.params["errorDetails"] as? String { data["details"] = errorDetails } sendEvent(name: "tts_canceled", data: data) case .error: isSpeaking = false restartIdleCheck() var data: [String: Any] = [:] if let errorCode = event.params["errorCode"] as? String { data["errorCode"] = errorCode } if let errorMessage = event.params["errorMessage"] as? String { data["message"] = errorMessage } else { data["message"] = "TTS错误" } sendEvent(name: "error", data: data) default: break } } } // 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() { NSLog("\(TAG): 收到唤醒信号") // 停止当前TTS,避免冲突 stopTts() // 如果已经在识别,则重新启动 stopRecognition() startRecognition() } func onDeviceInfoReceived(infoType: Int, infoData: [String: Any]) { // 可以处理设备信息,例如电量、设备名称等 } } // MARK: - 音频播放器 /// 简单的音频播放器,用于播放提示音 class AudioPlayer { private var player: AVAudioPlayer? /// 播放开始识别提示音 func playStartSound() { playSound(named: "start") } /// 播放停止识别提示音 func playStopSound() { playSound(named: "stop") } /// 播放音频文件 /// - Parameter named: 音频文件名 private func playSound(named: String) { guard let path = Bundle.main.path(forResource: named, ofType: "wav") else { print("找不到音频文件: \(named).wav") return } let url = URL(fileURLWithPath: path) do { player = try AVAudioPlayer(contentsOf: url) player?.prepareToPlay() player?.play() } catch { print("播放音频失败: \(error.localizedDescription)") } } }