|
|
|
@ -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() |
|
|
|
} |
|
|
|
|