diff --git a/lib/modules/agent/controllers/agent_controller.dart b/lib/modules/agent/controllers/agent_controller.dart index 26fb7a46d..124cc4f57 100644 --- a/lib/modules/agent/controllers/agent_controller.dart +++ b/lib/modules/agent/controllers/agent_controller.dart @@ -507,9 +507,10 @@ class AgentController extends GetxController { } else { // 只有在非按住说话模式且非打电话模式下才自动切换到文字模式 // 按住说话模式下应该保持在语音模式,等待下次按住 - if (!isPhoneCallMode.value) { - isTextInputMode.value = true; - } + // if (!isPhoneCallMode.value && !isPushToTalkActive.value) { + // Logger.i(TAG, '切换文本输入模式'); + // isTextInputMode.value = false; + // } } Logger.i(TAG, '识别停止'); diff --git a/lib/modules/agent/views/agent_view.dart b/lib/modules/agent/views/agent_view.dart index 910b9ccdc..22fc128f0 100644 --- a/lib/modules/agent/views/agent_view.dart +++ b/lib/modules/agent/views/agent_view.dart @@ -196,7 +196,17 @@ class _AgentViewState extends State with WidgetsBindingObserver { IntroStepBuilder( group: 'ai', order: 3, - getOverlayPosition: IntroOverlayBuilder.overlayPosition, + getOverlayPosition: ({ + required Offset offset, + required Size screenSize, + required Size size, + }) { + return OverlayPosition( + top: 200.h, + width: screenSize.width, + crossAxisAlignment: CrossAxisAlignment.end, + ); + }, overlayBuilder: (params) { return Obx( () => IntroOverlayBuilder.homeBuilder( @@ -242,8 +252,6 @@ class _AgentViewState extends State with WidgetsBindingObserver { ? IntroStepBuilder( group: 'ai', order: 4, - getOverlayPosition: - IntroOverlayBuilder.overlayPosition, overlayBuilder: (params) { return IntroOverlayBuilder.aiBuilder( params, @@ -503,8 +511,6 @@ class _AgentViewState extends State with WidgetsBindingObserver { return IntroStepBuilder( group: 'ai', order: 1, - getOverlayPosition: - IntroOverlayBuilder.overlayPosition, overlayBuilder: (params) { return IntroOverlayBuilder.aiBuilder( params, @@ -667,19 +673,30 @@ class _AgentViewState extends State with WidgetsBindingObserver { : Colors.blue[100]) : (isDarkMode ? Colors.grey[800] - : Colors.grey[200]), + : const Color.fromARGB( + 255, 255, 255, 255)), borderRadius: BorderRadius.circular(22.w), - border: Border.all( - color: isPushToTalkActive - ? Colors.blue - : (isDarkMode - ? Colors.grey[800]! - : Colors.grey - .withOpacity(0.1)), - width: - isPushToTalkActive ? 2.w : 1.w, - ), + boxShadow: [ + BoxShadow( + color: + Colors.black.withOpacity(0.2), + spreadRadius: 1, + blurRadius: 2, + offset: const Offset(0, 1), + ), + ], + // border: Border.all( + // color: isPushToTalkActive + // ? Colors.blue + // : (isDarkMode + // ? Colors.grey[800]! + // : const Color.fromARGB( + // 255, 255, 255, 255) + // .withOpacity(0.1)), + // width: + // isPushToTalkActive ? 2.w : 1.w, + // ), ), child: Row( children: [ @@ -739,143 +756,149 @@ class _AgentViewState extends State with WidgetsBindingObserver { SizedBox(width: 8.w), // 动态按钮区域:展开按钮 + 发送按钮 或 图片选择按钮 - IntroStepBuilder( - group: 'ai', - order: 2, - getOverlayPosition: - IntroOverlayBuilder.overlayPosition, - overlayBuilder: (params) { - return IntroOverlayBuilder.aiBuilder( - params, - '点击发送,${controller.noviceGuideService.aiStepsList[controller.noviceGuideService.aiStep]['send']}', - alignment: false, - onTap: () async { - await controller - .noviceGuideService.aiStepsList[ - controller.noviceGuideService - .aiStep]['method'](); - params.onNext!(); - controller.sendMessage(); - }, - ); - }, - builder: (BuildContext context, GlobalKey key) => - Obx( - key: key, - () { - final hasText = controller.hasTextInput.value; - final hasImage = - controller.isImageInputActive.value; - final shouldShowSend = hasText || hasImage; - final shouldShowExpand = - controller.shouldShowExpandButton.value; + Builder(builder: (context) { + StepWidgetParams? widgetParams; + return IntroStepBuilder( + group: 'ai', + order: 2, + onHighlightWidgetTap: () async { + await controller + .noviceGuideService.aiStepsList[ + controller.noviceGuideService.aiStep] + ['method'](); + widgetParams!.onNext!(); + controller.sendMessage(); + }, + overlayBuilder: (params) { + widgetParams = params; + return IntroOverlayBuilder.aiBuilder( + params, + '点击发送,${controller.noviceGuideService.aiStepsList[controller.noviceGuideService.aiStep]['send']}', + alignment: false, + ); + }, + builder: + (BuildContext context, GlobalKey key) => + Obx( + key: key, + () { + final hasText = + controller.hasTextInput.value; + final hasImage = + controller.isImageInputActive.value; + final shouldShowSend = hasText || hasImage; + final shouldShowExpand = + controller.shouldShowExpandButton.value; - if (shouldShowSend) { - // 显示发送按钮(可能还有展开按钮) - return Column( - children: [ - // 展开按钮(在发送按钮上方) - if (shouldShowExpand && hasText) - Padding( - padding: - EdgeInsets.only(bottom: 40.h), - child: GestureDetector( - onTap: () { - // 打开底部弹出编辑模式 - controller.openFullScreenEdit(); - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: - Colors.transparent, - builder: (context) => - const FullScreenEditor(), - ).then((_) { - // 弹出页关闭时的回调 + if (shouldShowSend) { + // 显示发送按钮(可能还有展开按钮) + return Column( + children: [ + // 展开按钮(在发送按钮上方) + if (shouldShowExpand && hasText) + Padding( + padding: + EdgeInsets.only(bottom: 40.h), + child: GestureDetector( + onTap: () { + // 打开底部弹出编辑模式 controller - .closeFullScreenEdit(); - }); - }, - child: Container( - width: 32.w, - height: 32.w, - decoration: BoxDecoration( - color: isDarkMode - ? Colors.grey[700] - : Colors.grey[300], - borderRadius: - BorderRadius.circular( - 16.w), - ), - child: Icon( - Icons.expand_less, - color: isDarkMode - ? Colors.grey[300] - : Colors.grey[700], - size: 20.sp, + .openFullScreenEdit(); + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: + Colors.transparent, + builder: (context) => + const FullScreenEditor(), + ).then((_) { + // 弹出页关闭时的回调 + controller + .closeFullScreenEdit(); + }); + }, + child: Container( + width: 32.w, + height: 32.w, + decoration: BoxDecoration( + color: isDarkMode + ? Colors.grey[700] + : Colors.grey[300], + borderRadius: + BorderRadius.circular( + 16.w), + ), + child: Icon( + Icons.expand_less, + color: isDarkMode + ? Colors.grey[300] + : Colors.grey[700], + size: 20.sp, + ), ), ), ), + // 发送按钮 + GestureDetector( + onTap: () => + controller.sendMessage(), + child: Container( + width: 44.w, + height: 44.w, + margin: EdgeInsets.symmetric( + vertical: 2.w), + decoration: BoxDecoration( + color: primaryColor, + borderRadius: + BorderRadius.circular(22.w), + ), + child: Icon( + Icons.send, + color: Colors.white, + size: 22.sp, + ), + ), ), - // 发送按钮 - GestureDetector( - onTap: () => controller.sendMessage(), + ], + ); + } else { + // 显示'+'选择按钮 + return Obx(() { + final isExpanded = controller + .isPlusOptionsExpanded.value; + return GestureDetector( + onTap: () { + controller.togglePlusOptions(); + }, child: Container( width: 44.w, height: 44.w, margin: EdgeInsets.symmetric( vertical: 2.w), decoration: BoxDecoration( - color: primaryColor, + color: isDarkMode + ? Colors.grey[800] + : Colors.grey[200], borderRadius: BorderRadius.circular(22.w), ), child: Icon( - Icons.send, - color: Colors.white, + isExpanded + ? Icons.close + : Icons.add, + color: isDarkMode + ? Colors.grey[300] + : Colors.grey[700], size: 22.sp, ), ), - ), - ], - ); - } else { - // 显示'+'选择按钮 - return Obx(() { - final isExpanded = controller - .isPlusOptionsExpanded.value; - return GestureDetector( - onTap: () { - controller.togglePlusOptions(); - }, - child: Container( - width: 44.w, - height: 44.w, - margin: EdgeInsets.symmetric( - vertical: 2.w), - decoration: BoxDecoration( - color: isDarkMode - ? Colors.grey[800] - : Colors.grey[200], - borderRadius: - BorderRadius.circular(22.w), - ), - child: Icon( - isExpanded - ? Icons.close - : Icons.add, - color: isDarkMode - ? Colors.grey[300] - : Colors.grey[700], - size: 22.sp, - ), - ), - ); - }); - } - }, - ), - ), + ); + }); + } + }, + ), + ); + }), ], ), 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 fac77eafc..ed21cefaa 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 @@ -375,9 +375,9 @@ class AgentServiceImpl: NSObject { let audioSourceType: AzureAsrHelper.AudioSourceType = useBle ? .external : .microphone if useBle { - AudioSessionHub.shared.begin(.playback) + // AudioSessionHub.shared.begin(.playback) } else { - AudioSessionHub.shared.begin(.voice) + //AudioSessionHub.shared.begin(.voice) } print("ai启动语音=\(audioSourceType)=\(useBle)") guard let success = azureAsrHelper?.startContinuousRecognition( @@ -1487,7 +1487,7 @@ class AudioPlayer { private let audioQueue = DispatchQueue(label: "agent.audio.player", qos: .userInitiated) // 等待音效超时时间(秒)- 改为 var 以支持动态修改 - private var awaitTimeoutSeconds: TimeInterval = 10.0 + private var awaitTimeoutSeconds: TimeInterval = 30.0 private var resourcesBundle: Bundle { #if SWIFT_PACKAGE diff --git a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AudioSessionHub.swift b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AudioSessionHub.swift index ea6aedcda..a742994dd 100644 --- a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AudioSessionHub.swift +++ b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AudioSessionHub.swift @@ -28,196 +28,196 @@ public class AudioSessionHub { _ = shared // 触发单例初始化 } - public func begin(_ intent: AudioIntent) { - if intent == currentIntent && audioSession.isOtherAudioPlaying == false { - os_log("音频意图未变化且无其他音频播放,跳过设置: %{public}@", log: logger, type: .debug, String(describing: intent)) - return - } + // public func begin(_ intent: AudioIntent) { + // if intent == currentIntent && audioSession.isOtherAudioPlaying == false { + // os_log("音频意图未变化且无其他音频播放,跳过设置: %{public}@", log: logger, type: .debug, String(describing: intent)) + // return + // } - os_log("开始设置音频会话,意图: %{public}@", log: logger, type: .info, String(describing: intent)) - currentIntent = intent + // os_log("开始设置音频会话,意图: %{public}@", log: logger, type: .info, String(describing: intent)) + // currentIntent = intent - do { - switch intent { + // do { + // switch intent { - case .voice: - os_log("设置语音模式: category=playAndRecord, mode=voiceChat", log: logger, type: .info) + // case .voice: + // os_log("设置语音模式: category=playAndRecord, mode=voiceChat", log: logger, type: .info) - // 第一步:设置音频类别和模式 - try audioSession.setCategory(.playAndRecord, - mode: .voiceChat, - options: [.allowBluetooth, .defaultToSpeaker, .mixWithOthers]) - os_log("✓ 音频类别设置成功: playAndRecord + voiceChat", log: logger, type: .info) + // // 第一步:设置音频类别和模式 + // try audioSession.setCategory(.playAndRecord, + // mode: .voiceChat, + // options: [.allowBluetooth, .defaultToSpeaker, .mixWithOthers]) + // os_log("✓ 音频类别设置成功: playAndRecord + voiceChat", log: logger, type: .info) - // 第二步:检查并设置蓝牙输入 - os_log("检查可用输入设备...", log: logger, type: .info) - if let availableInputs = audioSession.availableInputs { - os_log("可用输入设备数量: %d", log: logger, type: .info, availableInputs.count) - for (index, input) in availableInputs.enumerated() { - os_log("输入设备[%d]: %{public}@ (%{public}@)", log: logger, type: .info, - index, input.portName, input.portType.rawValue) - } + // // 第二步:检查并设置蓝牙输入 + // os_log("检查可用输入设备...", log: logger, type: .info) + // if let availableInputs = audioSession.availableInputs { + // os_log("可用输入设备数量: %d", log: logger, type: .info, availableInputs.count) + // for (index, input) in availableInputs.enumerated() { + // os_log("输入设备[%d]: %{public}@ (%{public}@)", log: logger, type: .info, + // index, input.portName, input.portType.rawValue) + // } - // 查找蓝牙HFP设备 - if let bluetoothInput = availableInputs.first(where: { $0.portType == .bluetoothHFP }) { - os_log("找到蓝牙HFP设备,尝试设置为首选输入: %{public}@", log: logger, type: .info, bluetoothInput.portName) - try audioSession.setPreferredInput(bluetoothInput) - os_log("✓ 蓝牙HFP输入设置成功: %{public}@", log: logger, type: .info, bluetoothInput.portName) - } else { - os_log("未找到蓝牙HFP设备", log: logger, type: .info) - } - } else { - os_log("⚠️ availableInputs 为 nil", log: logger, type: .default) - } + // // 查找蓝牙HFP设备 + // if let bluetoothInput = availableInputs.first(where: { $0.portType == .bluetoothHFP }) { + // os_log("找到蓝牙HFP设备,尝试设置为首选输入: %{public}@", log: logger, type: .info, bluetoothInput.portName) + // try audioSession.setPreferredInput(bluetoothInput) + // os_log("✓ 蓝牙HFP输入设置成功: %{public}@", log: logger, type: .info, bluetoothInput.portName) + // } else { + // os_log("未找到蓝牙HFP设备", log: logger, type: .info) + // } + // } else { + // os_log("⚠️ availableInputs 为 nil", log: logger, type: .default) + // } - case .playback: - os_log("设置播放模式: category=playback, mode=default", log: logger, type: .info) + // case .playback: + // os_log("设置播放模式: category=playback, mode=default", log: logger, type: .info) - // 检查当前音频会话状态 - os_log("设置前状态检查:", log: logger, type: .info) - os_log(" - 当前类别: %{public}@", log: logger, type: .info, audioSession.category.rawValue) - os_log(" - 当前模式: %{public}@", log: logger, type: .info, audioSession.mode.rawValue) - os_log(" - 当前是否激活: %{public}@", log: logger, type: .info, audioSession.isOtherAudioPlaying ? "是" : "否") - os_log(" - 其他音频播放: %{public}@", log: logger, type: .info, audioSession.isOtherAudioPlaying ? "是" : "否") + // // 检查当前音频会话状态 + // os_log("设置前状态检查:", log: logger, type: .info) + // os_log(" - 当前类别: %{public}@", log: logger, type: .info, audioSession.category.rawValue) + // os_log(" - 当前模式: %{public}@", log: logger, type: .info, audioSession.mode.rawValue) + // os_log(" - 当前是否激活: %{public}@", log: logger, type: .info, audioSession.isOtherAudioPlaying ? "是" : "否") + // os_log(" - 其他音频播放: %{public}@", log: logger, type: .info, audioSession.isOtherAudioPlaying ? "是" : "否") - // 如果当前已经激活,先尝试停用 - do { - if audioSession.isOtherAudioPlaying { - os_log("检测到其他音频正在播放,先尝试停用当前会话", log: logger, type: .info) - try audioSession.setActive(false, options: .notifyOthersOnDeactivation) - os_log("✓ 成功停用当前音频会话", log: logger, type: .info) - } - } catch let deactivateError as NSError { - os_log("⚠️ 停用当前音频会话失败: %{public}@ (继续尝试设置)", log: logger, type: .default, deactivateError.localizedDescription) - } + // // 如果当前已经激活,先尝试停用 + // do { + // if audioSession.isOtherAudioPlaying { + // os_log("检测到其他音频正在播放,先尝试停用当前会话", log: logger, type: .info) + // try audioSession.setActive(false, options: .notifyOthersOnDeactivation) + // os_log("✓ 成功停用当前音频会话", log: logger, type: .info) + // } + // } catch let deactivateError as NSError { + // os_log("⚠️ 停用当前音频会话失败: %{public}@ (继续尝试设置)", log: logger, type: .default, deactivateError.localizedDescription) + // } - // 尝试渐进式设置:先尝试最简单的设置 - var settingSuccess = false + // // 尝试渐进式设置:先尝试最简单的设置 + // var settingSuccess = false - // 策略1: 只设置类别,不设置选项 - if !settingSuccess { - do { - os_log("尝试策略1: 仅设置类别 (playback)", log: logger, type: .info) - try audioSession.setCategory(.playback) - os_log("✓ 策略1成功: 音频类别设置为 playback", log: logger, type: .info) - settingSuccess = true - } catch let error as NSError { - os_log("策略1失败: %{public}@ (OSStatus: %d)", log: logger, type: .info, error.localizedDescription, error.code) - } - } + // // 策略1: 只设置类别,不设置选项 + // if !settingSuccess { + // do { + // os_log("尝试策略1: 仅设置类别 (playback)", log: logger, type: .info) + // try audioSession.setCategory(.playback) + // os_log("✓ 策略1成功: 音频类别设置为 playback", log: logger, type: .info) + // settingSuccess = true + // } catch let error as NSError { + // os_log("策略1失败: %{public}@ (OSStatus: %d)", log: logger, type: .info, error.localizedDescription, error.code) + // } + // } - // 策略2: 设置类别和模式,不设置选项 - if !settingSuccess { - do { - os_log("尝试策略2: 设置类别和模式 (playback + default)", log: logger, type: .info) - try audioSession.setCategory(.playback, mode: .default) - os_log("✓ 策略2成功: 音频类别和模式设置完成", log: logger, type: .info) - settingSuccess = true - } catch let error as NSError { - os_log("策略2失败: %{public}@ (OSStatus: %d)", log: logger, type: .info, error.localizedDescription, error.code) - } - } + // // 策略2: 设置类别和模式,不设置选项 + // if !settingSuccess { + // do { + // os_log("尝试策略2: 设置类别和模式 (playback + default)", log: logger, type: .info) + // try audioSession.setCategory(.playback, mode: .default) + // os_log("✓ 策略2成功: 音频类别和模式设置完成", log: logger, type: .info) + // settingSuccess = true + // } catch let error as NSError { + // os_log("策略2失败: %{public}@ (OSStatus: %d)", log: logger, type: .info, error.localizedDescription, error.code) + // } + // } - // 策略3: 设置类别、模式和单个选项 - if !settingSuccess { - do { - os_log("尝试策略3: 设置类别、模式和混音选项", log: logger, type: .info) - try audioSession.setCategory(.playback, mode: .default, options: [.mixWithOthers]) - os_log("✓ 策略3成功: 音频类别、模式和混音选项设置完成", log: logger, type: .info) - settingSuccess = true - } catch let error as NSError { - os_log("策略3失败: %{public}@ (OSStatus: %d)", log: logger, type: .info, error.localizedDescription, error.code) - } - } + // // 策略3: 设置类别、模式和单个选项 + // if !settingSuccess { + // do { + // os_log("尝试策略3: 设置类别、模式和混音选项", log: logger, type: .info) + // try audioSession.setCategory(.playback, mode: .default, options: [.mixWithOthers]) + // os_log("✓ 策略3成功: 音频类别、模式和混音选项设置完成", log: logger, type: .info) + // settingSuccess = true + // } catch let error as NSError { + // os_log("策略3失败: %{public}@ (OSStatus: %d)", log: logger, type: .info, error.localizedDescription, error.code) + // } + // } - // 策略4: 设置类别、模式和蓝牙选项 - if !settingSuccess { - do { - os_log("尝试策略4: 设置类别、模式和蓝牙选项", log: logger, type: .info) - try audioSession.setCategory(.playback, mode: .default, options: [.allowBluetoothA2DP]) - os_log("✓ 策略4成功: 音频类别、模式和蓝牙选项设置完成", log: logger, type: .info) - settingSuccess = true - } catch let error as NSError { - os_log("策略4失败: %{public}@ (OSStatus: %d)", log: logger, type: .info, error.localizedDescription, error.code) - } - } + // // 策略4: 设置类别、模式和蓝牙选项 + // if !settingSuccess { + // do { + // os_log("尝试策略4: 设置类别、模式和蓝牙选项", log: logger, type: .info) + // try audioSession.setCategory(.playback, mode: .default, options: [.allowBluetoothA2DP]) + // os_log("✓ 策略4成功: 音频类别、模式和蓝牙选项设置完成", log: logger, type: .info) + // settingSuccess = true + // } catch let error as NSError { + // os_log("策略4失败: %{public}@ (OSStatus: %d)", log: logger, type: .info, error.localizedDescription, error.code) + // } + // } - // 策略5: 完整设置(原始方案) - if !settingSuccess { - os_log("尝试策略5: 完整设置 (playback + default + 所有选项)", log: logger, type: .info) - try audioSession.setCategory(.playback, - mode: .default, - options: [.allowBluetoothA2DP, .mixWithOthers]) - os_log("✓ 策略5成功: 完整音频设置完成", log: logger, type: .info) - settingSuccess = true - } + // // 策略5: 完整设置(原始方案) + // if !settingSuccess { + // os_log("尝试策略5: 完整设置 (playback + default + 所有选项)", log: logger, type: .info) + // try audioSession.setCategory(.playback, + // mode: .default, + // options: [.allowBluetoothA2DP, .mixWithOthers]) + // os_log("✓ 策略5成功: 完整音频设置完成", log: logger, type: .info) + // settingSuccess = true + // } - if !settingSuccess { - os_log("❌ 所有设置策略都失败了", log: logger, type: .error) - throw NSError(domain: "AudioSessionHub", code: -1, userInfo: [NSLocalizedDescriptionKey: "所有音频类别设置策略都失败"]) - } + // if !settingSuccess { + // os_log("❌ 所有设置策略都失败了", log: logger, type: .error) + // throw NSError(domain: "AudioSessionHub", code: -1, userInfo: [NSLocalizedDescriptionKey: "所有音频类别设置策略都失败"]) + // } - } + // } - // 第三步:激活音频会话 - os_log("尝试激活音频会话...", log: logger, type: .info) - try audioSession.setActive(true, options: .notifyOthersOnDeactivation) - os_log("✓ 音频会话激活成功", log: logger, type: .info) + // // 第三步:激活音频会话 + // os_log("尝试激活音频会话...", log: logger, type: .info) + // try audioSession.setActive(true, options: .notifyOthersOnDeactivation) + // os_log("✓ 音频会话激活成功", log: logger, type: .info) - os_log("✅ 音频会话完整设置成功:%{public}@", log: logger, type: .info, String(describing: intent)) + // os_log("✅ 音频会话完整设置成功:%{public}@", log: logger, type: .info, String(describing: intent)) - // 输出当前音频路由信息 - let hasBluetoothDevice = hasBluetoothAudioDevice() - let hasBluetoothMic = hasBluetoothMicrophone() - let currentInput = getCurrentInputDevice() + // // 输出当前音频路由信息 + // let hasBluetoothDevice = hasBluetoothAudioDevice() + // let hasBluetoothMic = hasBluetoothMicrophone() + // let currentInput = getCurrentInputDevice() - os_log("📊 音频状态总览:", log: logger, type: .info) - os_log(" - 蓝牙音频设备: %{public}@", log: logger, type: .info, hasBluetoothDevice ? "已连接" : "未连接") - os_log(" - 蓝牙麦克风: %{public}@", log: logger, type: .info, hasBluetoothMic ? "已连接" : "未连接") - os_log(" - 当前输入设备: %{public}@", log: logger, type: .info, currentInput) + // os_log("📊 音频状态总览:", log: logger, type: .info) + // os_log(" - 蓝牙音频设备: %{public}@", log: logger, type: .info, hasBluetoothDevice ? "已连接" : "未连接") + // os_log(" - 蓝牙麦克风: %{public}@", log: logger, type: .info, hasBluetoothMic ? "已连接" : "未连接") + // os_log(" - 当前输入设备: %{public}@", log: logger, type: .info, currentInput) - if hasBluetoothDevice { - os_log("📋 详细路由信息:\n%{public}@", log: logger, type: .debug, getCurrentAudioRoute()) - } - } catch let error as NSError { - let osStatusCode = error.code - let domain = error.domain - let description = error.localizedDescription + // if hasBluetoothDevice { + // os_log("📋 详细路由信息:\n%{public}@", log: logger, type: .debug, getCurrentAudioRoute()) + // } + // } catch let error as NSError { + // let osStatusCode = error.code + // let domain = error.domain + // let description = error.localizedDescription - os_log("❌ 设置音频会话失败", log: logger, type: .error) - os_log(" - 错误域: %{public}@", log: logger, type: .error, domain) - os_log(" - OSStatus代码: %d (0x%X)", log: logger, type: .error, osStatusCode, osStatusCode) - os_log(" - 错误描述: %{public}@", log: logger, type: .error, description) - os_log(" - 当前意图: %{public}@", log: logger, type: .error, String(describing: intent)) + // os_log("❌ 设置音频会话失败", log: logger, type: .error) + // os_log(" - 错误域: %{public}@", log: logger, type: .error, domain) + // os_log(" - OSStatus代码: %d (0x%X)", log: logger, type: .error, osStatusCode, osStatusCode) + // os_log(" - 错误描述: %{public}@", log: logger, type: .error, description) + // os_log(" - 当前意图: %{public}@", log: logger, type: .error, String(describing: intent)) - // 解析具体的 OSStatus 错误 - switch osStatusCode { - case -50: - os_log(" ➤ 错误分析: OSStatus -50 通常表示参数错误 (kAudioSessionInvalidPropertyError)", log: logger, type: .error) - case -560557673: // kAudioSessionNotInitialized - os_log(" ➤ 错误分析: 音频会话未初始化", log: logger, type: .error) - case -560030580: // kAudioSessionBadPropertySizeError - os_log(" ➤ 错误分析: 属性大小错误", log: logger, type: .error) - case -560161140: // kAudioSessionIncompatibleCategory - os_log(" ➤ 错误分析: 音频类别不兼容", log: logger, type: .error) - default: - os_log(" ➤ 错误分析: 未知的OSStatus错误代码", log: logger, type: .error) - } + // // 解析具体的 OSStatus 错误 + // switch osStatusCode { + // case -50: + // os_log(" ➤ 错误分析: OSStatus -50 通常表示参数错误 (kAudioSessionInvalidPropertyError)", log: logger, type: .error) + // case -560557673: // kAudioSessionNotInitialized + // os_log(" ➤ 错误分析: 音频会话未初始化", log: logger, type: .error) + // case -560030580: // kAudioSessionBadPropertySizeError + // os_log(" ➤ 错误分析: 属性大小错误", log: logger, type: .error) + // case -560161140: // kAudioSessionIncompatibleCategory + // os_log(" ➤ 错误分析: 音频类别不兼容", log: logger, type: .error) + // default: + // os_log(" ➤ 错误分析: 未知的OSStatus错误代码", log: logger, type: .error) + // } - // 输出当前音频会话状态用于调试 - os_log("🔍 当前音频会话状态:", log: logger, type: .error) - os_log(" - 类别: %{public}@", log: logger, type: .error, audioSession.category.rawValue) - os_log(" - 模式: %{public}@", log: logger, type: .error, audioSession.mode.rawValue) - os_log(" - 是否激活: %{public}@", log: logger, type: .error, audioSession.isOtherAudioPlaying ? "是" : "否") - os_log(" - 其他音频播放: %{public}@", log: logger, type: .error, audioSession.isOtherAudioPlaying ? "是" : "否") - } - } + // // 输出当前音频会话状态用于调试 + // os_log("🔍 当前音频会话状态:", log: logger, type: .error) + // os_log(" - 类别: %{public}@", log: logger, type: .error, audioSession.category.rawValue) + // os_log(" - 模式: %{public}@", log: logger, type: .error, audioSession.mode.rawValue) + // os_log(" - 是否激活: %{public}@", log: logger, type: .error, audioSession.isOtherAudioPlaying ? "是" : "否") + // os_log(" - 其他音频播放: %{public}@", log: logger, type: .error, audioSession.isOtherAudioPlaying ? "是" : "否") + // } + // } public func appWillEnterForeground() { os_log("📱 应用即将进入前台", log: logger, type: .info) if let intent = currentIntent { os_log("恢复之前的音频意图: %{public}@", log: logger, type: .info, String(describing: intent)) - begin(intent) + //begin(intent) } else { os_log("无之前的音频意图需要恢复", log: logger, type: .info) } @@ -385,7 +385,7 @@ public class AudioSessionHub { if options.contains(.shouldResume), let intent = currentIntent { os_log("恢复之前的音频意图: %{public}@", log: logger, type: .info, String(describing: intent)) // 恢复之前的音频意图 - begin(intent) + //begin(intent) os_log("✓ 音频会话中断结束,已恢复", log: logger, type: .info) } else { os_log("不需要恢复音频会话", log: logger, type: .info) @@ -420,7 +420,7 @@ public class AudioSessionHub { // 重新应用当前意图以确保正确路由 if let intent = currentIntent { os_log("重新应用当前音频意图: %{public}@", log: logger, type: .info, String(describing: intent)) - begin(intent) + // begin(intent) } else { os_log("无当前音频意图需要重新应用", log: logger, type: .info) } @@ -444,7 +444,7 @@ public class AudioSessionHub { // 重新应用当前意图以确保正确路由 if let intent = currentIntent { os_log("重新应用当前音频意图: %{public}@", log: logger, type: .info, String(describing: intent)) - begin(intent) + // begin(intent) } else { os_log("无当前音频意图需要重新应用", log: logger, type: .info) } @@ -474,13 +474,13 @@ public class AudioSessionHub { /// 处理播放音频会话激活请求 @objc private func handleAudioSessionBeginPlayback(notification: Notification) { os_log("收到播放音频会话激活请求", log: logger, type: .info) - begin(.playback) + // begin(.playback) } /// 处理语音音频会话激活请求 @objc private func handleAudioSessionBeginVoice(notification: Notification) { os_log("收到语音音频会话激活请求", log: logger, type: .info) - begin(.voice) + // begin(.voice) } /// 处理音量控制请求 diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift index 3000c5ee7..f5d2cf052 100644 --- a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift +++ b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift @@ -27,6 +27,16 @@ public class AzureAsrHelper: NSObject { private var networkMonitor: NetworkStateMonitor = NetworkStateMonitor() private var continuousCallback: ContinuousRecognizeCallback? private var isNetworkRecovering = false + + // 网络丢失计数器 + private var networkLostCount = 0 + // 最大网络丢失次数(3秒检测) + private let maxNetworkLostCount = 3 + + // 防抖机制相关变量 + private var lastNetworkLostTime: Int64 = 0 + private let networkLostDebounceInterval: Int64 = 3000 // 3秒防抖间隔 + // 状态管理 private var _isContinuousRecognitionActive = false @@ -141,61 +151,52 @@ public class AzureAsrHelper: NSObject { private func handleNetworkAvailable() { os_log("网络已恢复", log: log, type: .info) - DispatchQueue.main.async { [weak self] in - guard let self = self else { return } + if !self.isNetworkRecovering { self.isNetworkRecovering = true os_log("网络恢复,准备重新启动识别", log: self.log, type: .info) - - // 如果之前在进行连续识别,重新启动 - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { - self.restartContinuousRecognition() - } } - } + } - /** - * 重新启动连续识别(网络恢复时使用) + + /** + * 处理网络连接丢失(带防抖机制) */ - private func restartContinuousRecognition() { - os_log("重新启动连续识别", log: log, type: .info) + private func handleNetworkLost() { + let currentTime = Int64(Date().timeIntervalSince1970 * 1000) // 转换为毫秒 - // 检查continuousCallback是否为空 - guard let callback = continuousCallback else { - os_log("连续识别回调为空,无法重新启动", log: log, type: .info) + // 防抖机制:如果距离上次触发时间小于防抖间隔,则忽略本次触发 + if currentTime - lastNetworkLostTime < networkLostDebounceInterval { + os_log("网络丢失事件被防抖过滤,距离上次触发仅%{public}dms", log: log, type: .debug, currentTime - lastNetworkLostTime) return } - do { - _ = setupEventListeners(callback: callback) - isNetworkRecovering = false - os_log("连续识别重新启动成功", log: log, type: .info) - } catch { - os_log("重新启动连续识别失败: %@", log: log, type: .error, error.localizedDescription) - isNetworkRecovering = false - } - } - - - /** - * 立即停止音频处理 - */ - private func stopAudioProcessingImmediately() { - audioStream?.stopMicrophoneCapture() + // 更新最后触发时间 + lastNetworkLostTime = currentTime - if _isContinuousRecognitionActive { - do { - try recognizer?.stopContinuousRecognition() - _isContinuousRecognitionActive = false - os_log("因网络问题强制停止识别", log: log, type: .info) - } catch { - _isContinuousRecognitionActive = false - os_log("强制停止识别失败: %@", log: log, type: .error, error.localizedDescription) + // 这里启动一个3秒定时器,3秒后检查网络是否恢复 + DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) { [weak self] in + guard let self = self else { return } + + os_log("网络不可用", log: self.log, type: .error) + let isNetworkAvailable = self.checkNetworkStatus() + + if isNetworkAvailable { + // 如果网络恢复,重置计数器和防抖时间 + os_log("网络已恢复,重置计数器", log: self.log, type: .info) + self.lastNetworkLostTime = 0 + self.isNetworkRecovering = true + } else { + self.continuousCallback?.onError(1000, "网络连接不可用,请检查网络设置") + os_log("网络仍未恢复,继续检测", log: self.log, type: .debug) + self.networkLostCount = 0 + self.stopContinuousRecognition() } } } + /** * 预初始化音频组件 * 在初始化阶段就准备好音频流和相关组件,减少首次启动延迟 @@ -245,13 +246,11 @@ public class AzureAsrHelper: NSObject { do { - guard recognizer != nil else { - if !setupRecognizer() { - print("重置识别器失败") - // callback.onError("重置识别器失败") - return false + if recognizer == nil { + // 检查continuousCallback是否为空,只有在不为空时才设置事件监听器 + if let callback = self.continuousCallback { + setupEventListeners(callback: callback) } - return false } print("startContinuousRecognition:\(audioSourceType)") @@ -266,7 +265,7 @@ public class AzureAsrHelper: NSObject { return true } catch { - audioStream?.stopMicrophoneCapture() + stopContinuousRecognition() _isContinuousRecognitionActive = false //callback.onError("启动连续识别失败: \(error.localizedDescription)") return false @@ -300,13 +299,10 @@ public class AzureAsrHelper: NSObject { do { _isContinuousRecognitionActive = false - if audioSourceType == .external { - //pushAudioData(data: Data()) - } - audioStream?.stopMicrophoneCapture() // 停止连续识别 try recognizer.stopContinuousRecognition() + self.recognizer = nil print("stopContinuousRecognition") return true } catch { @@ -320,6 +316,7 @@ public class AzureAsrHelper: NSObject { // 【修复】在 catch 块中安全地停止识别器,避免再次抛出异常 do { try recognizer.stopContinuousRecognition() + self.recognizer = nil } catch { // 如果强制停止也失败,记录错误但不再抛出异常 os_log("强制停止识别器失败: %@", log: log, type: .error, error.localizedDescription) @@ -623,18 +620,9 @@ public class AzureAsrHelper: NSObject { os_log("识别取消: %{public}@", log: self.log, type: .error, errorDetails) - // 检查是否为网络相关错误 - if self.isNetworkRelatedError(errorDetails) { - os_log("检测到网络相关错误,停止音频处理", log: self.log, type: .info) - self.stopAudioProcessingImmediately() - - // 如果不是正在恢复网络,通知上层网络错误 - if !self.isNetworkRecovering { - callback.onError(1000,"启动连续识别失败,网络连接已断开") - } - } else { + callback.onCanceled(reason, errorDetails) - } + } return true } @@ -845,75 +833,17 @@ extension AzureAsrHelper: NetworkStateMonitor.NetworkStateListener { func onNetworkAvailable() { os_log("网络已恢复", log: log, type: .info) - DispatchQueue.main.async { [weak self] in - self?.handleNetworkAvailable() - } + self.handleNetworkAvailable() + } - /** - * 处理网络丢失事件 - */ + + func onNetworkLost() { os_log("网络连接丢失", log: log, type: .error) - // 立即停止音频处理 - stopAudioProcessingImmediately() - - DispatchQueue.main.async { [weak self] in - self?.continuousCallback?.onError(1000,"网络连接已断开") - } + self.handleNetworkLost() } } -// //ios 语言检测 -// /** -// * 检测输入文本的语言,从支持的语言列表中选择最匹配的 -// * -// * @param text 需要检测的文本 -// * @return 从supportedLanguages中选择的最匹配语言代码 -// */ -// public func detectTextLanguage(_ text: String) -> String? { -// // 如果只有一种支持的语言,直接返回 -// if supportedLanguages.count == 1 { -// return supportedLanguages[0] -// } - -// // 获取所有可能的语言及其置信度 -// let languageResults = LanguageDetector.detectLanguageWithConfidence(from: text) - -// // 遍历检测结果,找到第一个在supportedLanguages中的语言 -// for result in languageResults { -// let azureLanguageCode = LanguageDetector.convertToAzureLanguageCode(result.language) -// if supportedLanguages.contains(azureLanguageCode) { -// return azureLanguageCode -// } -// } - -// // 如果没有找到匹配的语言,返回第一个支持的语言作为默认值 -// return supportedLanguages.first -// } - -// /** -// * 检测文本语言并返回详细信息 -// * -// * @param text 需要检测的文本 -// * @return 包含语言代码、置信度和语言名称的字典 -// */ -// public func detectTextLanguageWithDetails(_ text: String) -> [String: Any]? { -// let results = LanguageDetector.detectLanguageWithConfidence(from: text) -// guard let topResult = results.first else { -// return nil -// } - -// let azureLanguageCode = LanguageDetector.convertToAzureLanguageCode(topResult.language) -// let languageName = LanguageDetector.getLanguageName(from: topResult.language) - -// return [ -// "languageCode": azureLanguageCode, -// "confidence": topResult.confidence, -// "languageName": languageName, -// "originalCode": topResult.language -// ] -// } - diff --git a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/QQMusicSystemFunctionHandler.swift b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/QQMusicSystemFunctionHandler.swift index 2e492fb3b..001176fe5 100644 --- a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/QQMusicSystemFunctionHandler.swift +++ b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/QQMusicSystemFunctionHandler.swift @@ -131,39 +131,63 @@ private class QQMusicSearchHandler: FunctionHandler { } return await withCheckedContinuation { continuation in - // 直接调用QPlayAutoManager的搜索接口 - QPlayAutoManager.shared.search(keyword: keyword, searchType: .song) { items in - var result: [String: Any] - - if let items = items, !items.isEmpty { - let songs = items.map { item -> [String: Any] in - return [ - "mid": item.mid ?? "", - "name": item.name ?? "", - "artist": item.singer?.name ?? "", - "album": item.album ?? "", - "duration": item.duration, - "pic": item.coverUri ?? "" + // 调用更新后的搜索接口,包含错误处理 + QPlayAutoManager.shared.search( + keyword: keyword, + searchType: .song, + completion: { items in + var result: [String: Any] + + if let items = items, !items.isEmpty { + let songs = items.map { item -> [String: Any] in + return [ + "mid": item.mid ?? "", + "name": item.name ?? "", + "artist": item.singer?.name ?? "", + "album": item.album ?? "", + "duration": item.duration, + "pic": item.coverUri ?? "" + ] + } + result = [ + "success": true, + "message": "Found \(items.count) songs", + "broadcast": false, + "data": songs ] + } else { + result = ["success": false, "error": "No songs found"] } - result = [ - "success": true, - "message": "Found \(items.count) songs and started playing", - "broadcast":false, - "data": songs + + do { + let jsonData = try JSONSerialization.data(withJSONObject: result) + let jsonString = String(data: jsonData, encoding: .utf8) ?? "{}" + continuation.resume(returning: jsonString) + } catch { + continuation.resume(returning: "{\"success\": false, \"error\": \"JSON serialization failed\"}") + } + }, + onError: { error in + // 处理搜索错误 + var errorMessage = "Search failed" + if let error = error { + errorMessage = "Search failed: \(error.localizedDescription)" + } + + let result: [String: Any] = [ + "success": false, + "error": errorMessage ] - } else { - result = ["success": false, "error": "No songs found"] - } - - do { - let jsonData = try JSONSerialization.data(withJSONObject: result) - let jsonString = String(data: jsonData, encoding: .utf8) ?? "{}" - continuation.resume(returning: jsonString) - } catch { - continuation.resume(returning: "{\"success\": false, \"error\": \"JSON serialization failed\"}") + + do { + let jsonData = try JSONSerialization.data(withJSONObject: result) + let jsonString = String(data: jsonData, encoding: .utf8) ?? "{}" + continuation.resume(returning: jsonString) + } catch { + continuation.resume(returning: "{\"success\": false, \"error\": \"Search failed and JSON serialization failed\"}") + } } - } + ) } } } diff --git a/local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/LocationService.kt b/local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/LocationService.kt index bf217f258..7b785c320 100644 --- a/local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/LocationService.kt +++ b/local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/LocationService.kt @@ -141,12 +141,15 @@ object LocationService { callback.onLocationError(-1, "定位服务未初始化") return } - - if (!checkPermissions(ctx)) { - callback.onLocationError(-2, "没有定位权限") + val p = checkPermissions(ctx) + if (p == 1) { + callback.onLocationError(-2, "定位权限没有") + return + } + if (p == 2) { + callback.onLocationError(-2, "后台定位权限没有") return } - if (!initialized) { callback.onLocationError(-3, "定位服务未初始化") return @@ -238,7 +241,7 @@ object LocationService { override fun run() { val ctx = appcontext ?: return val hasPermission = checkPermissions(ctx) - if (hasPermission && !initialized) { + if (hasPermission ==0 && !initialized) { Log.d(TAG, "权限已授予,重新初始化定位服务") initialize(ctx) stopPermissionMonitor() @@ -256,12 +259,26 @@ object LocationService { permissionRetryRunnable = null } - private fun checkPermissions(context: Context): Boolean { - return if (isAppInForeground(context)) { - hasForegroundLocationPermission(context) - } else { - hasBackgroundLocationPermission(context) + + /** + * 检查位置权限状态并返回对应编码 + * @return 0:已获得所需全部权限;1:未获得前台权限;2:前台权限已获得但后台权限未授予(仅Android 10+) + */ + private fun checkPermissions(context: Context): Int { + // 先检查前台权限是否已授予 + val hasForegroundPermission = hasForegroundLocationPermission(context) + if (!hasForegroundPermission) { + return 1 // 前台权限未授予 } + + // 前台权限已授予,再根据前后台状态检查后台权限 + if (!isAppInForeground(context)) { + // 应用在后台:Android 10+ 需要检查后台权限 + return 2 // 后台权限未授予 + } + + // 所有所需权限均已授予 + return 0 } private fun hasForegroundLocationPermission(context: Context): Boolean { @@ -306,7 +323,7 @@ object LocationService { val ctx = appcontext ?: activity // 如果已经有权限,直接返回成功 - if (checkPermissions(ctx)) { + if (checkPermissions(ctx) == 0) { result.success(mapOf("granted" to true)) return } diff --git a/local_plugins/location_service/ios/location_service/Sources/location_service/LocationService.swift b/local_plugins/location_service/ios/location_service/Sources/location_service/LocationService.swift index a626a1e37..b55764459 100644 --- a/local_plugins/location_service/ios/location_service/Sources/location_service/LocationService.swift +++ b/local_plugins/location_service/ios/location_service/Sources/location_service/LocationService.swift @@ -195,8 +195,15 @@ public class LocationService: NSObject { // 检查是否已有请求在进行 if self.currentLocationCallback != nil { - completion(.failure(NSError(domain: "LocationService", code: -4, userInfo: [NSLocalizedDescriptionKey: "位置请求正在进行中"]))) - return + print("[LocationService] 检测到重复请求,重置之前的请求") + self.resetLocationRequest() + } + + // 实时检查权限状态 + let currentStatus = CLLocationManager.authorizationStatus() + if currentStatus != self.authorizationStatus { + print("[LocationService] 权限状态已变更,更新缓存") + self.authorizationStatus = currentStatus } // 检查权限 @@ -205,6 +212,12 @@ public class LocationService: NSObject { return } + // 检查位置服务是否启用 + if !CLLocationManager.locationServicesEnabled() { + completion(.failure(NSError(domain: "LocationService", code: -6, userInfo: [NSLocalizedDescriptionKey: "位置服务未启用"]))) + return + } + // 设置回调 self.currentLocationCallback = { result in switch result { @@ -228,7 +241,15 @@ public class LocationService: NSObject { } } - print("[LocationService] 开始正常定位") + // 设置超时定时器 + self.locationTimeoutTimer = Timer.scheduledTimer(withTimeInterval: self.locationTimeout, repeats: false) { [weak self] _ in + print("[LocationService] 位置请求超时") + let callback = self?.currentLocationCallback + self?.resetLocationRequest() + callback?(.failure(NSError(domain: "LocationService", code: -8, userInfo: [NSLocalizedDescriptionKey: "位置请求超时"]))) + } + + print("[LocationService] 开始正常定位,超时时间: \(self.locationTimeout)秒") // 使用单次定位 locationManager.requestLocation() } @@ -253,7 +274,7 @@ public class LocationService: NSObject { ) let callback = currentLocationCallback - currentLocationCallback = nil + resetLocationRequest() // 使用重置方法 callback?(.success(locationInfo)) } @@ -266,6 +287,18 @@ public class LocationService: NSObject { "timestamp": locationData.timestamp ] } + + // 添加超时定时器 + private var locationTimeoutTimer: Timer? + private let locationTimeout: TimeInterval = 15.0 // 15秒超时 + + // 添加重置方法 + private func resetLocationRequest() { + print("[LocationService] 重置位置请求状态") + currentLocationCallback = nil + locationTimeoutTimer?.invalidate() + locationTimeoutTimer = nil + } } // MARK: - CLLocationManagerDelegate @@ -304,7 +337,7 @@ extension LocationService: CLLocationManagerDelegate { print("[LocationService] 位置获取失败: \(error.localizedDescription)") let callback = currentLocationCallback - currentLocationCallback = nil + resetLocationRequest() // 使用重置方法 callback?(.failure(error)) } @@ -356,5 +389,3 @@ extension LocationService: CLLocationManagerDelegate { } } -// Remove the duplicate LocationData definition from here -// The LocationData struct is already defined in LocationData.swift diff --git a/local_plugins/qq_music/ios/qq_music/Sources/qq_music/QPlayAutoManager.swift b/local_plugins/qq_music/ios/qq_music/Sources/qq_music/QPlayAutoManager.swift index 04998a22c..f78bdf4c3 100644 --- a/local_plugins/qq_music/ios/qq_music/Sources/qq_music/QPlayAutoManager.swift +++ b/local_plugins/qq_music/ios/qq_music/Sources/qq_music/QPlayAutoManager.swift @@ -316,7 +316,27 @@ import QPlayAutoSDK } // MARK: - 登录状态管理 - // 在第206行之前添加新的方法 + // Add the error enum at the top of the class + enum QPlayAutoError: Error { + case qqMusicNotInstalled + case loginFailed + case connectionFailed + case searchFailed(String) // 新增搜索失败错误 + + var localizedDescription: String { + switch self { + case .qqMusicNotInstalled: + return "QQ音乐未安装" + case .loginFailed: + return "登录失败" + case .connectionFailed: + return "连接失败" + case .searchFailed(let reason): + return "搜索失败: \(reason)" + } + } + } + private func ensureQQMusicInstalledAndLoginAndExecute( _ operation: @escaping () -> Void, onQQMusicNotInstalled: (() -> Void)? = nil, @@ -330,15 +350,37 @@ import QPlayAutoSDK } // QQ音乐已安装,继续检查登录状态 - ensureLoginAndExecute(operation, onLoginFailed: onLoginFailed) + ensureLoginAndExecute(operation) { error in + switch error { + case .loginFailed: + onLoginFailed?() + case .qqMusicNotInstalled: + // This shouldn't happen since we already checked above + onQQMusicNotInstalled?() + default: + onLoginFailed?() + } + } } - // 修改现有的ensureLoginAndExecute方法(第206行开始) - private func ensureLoginAndExecute(_ operation: @escaping () -> Void, onLoginFailed: (() -> Void)? = nil) { + // 修改现有的ensureLoginAndExecute方法 + private func ensureLoginAndExecute( + _ operation: @escaping () -> Void, + onFailure: ((QPlayAutoError) -> Void)? = nil + ) { + // 首先检查QQ音乐是否已安装 + guard isQQMusicInstalled else { + print("QPlayAutoManager: QQ音乐未安装,无法执行操作") + onFailure?(.qqMusicNotInstalled) + return + } + if isLoginOK { // 已登录,直接执行操作 operation() } else { + + // 未登录,先登录 print("QPlayAutoManager: 未登录,正在尝试登录...") performLogin { [weak self] success in @@ -347,7 +389,7 @@ import QPlayAutoSDK operation() } else { print("QPlayAutoManager: 登录失败,无法执行操作") - onLoginFailed?() + onFailure?(.loginFailed) } } } @@ -533,33 +575,34 @@ import QPlayAutoSDK } @objc public func pause() { - ensureLoginAndExecute { + ensureLoginAndExecute({ QPlayAutoSDK.playerPlayPause() - } onLoginFailed: { - print("QPlayAutoManager: 暂停失败 - 登录失败") - } + }, onFailure: { error in + print("QPlayAutoManager: 暂停失败 - \(error)") + }) } + // Fix seek method (lines 571-578) @objc public func seek(to position: TimeInterval) { - ensureLoginAndExecute { - QPlayAutoSDK.playerSeek(Int(position)) - } onLoginFailed: { - print("QPlayAutoManager: 跳转播放位置失败 - 登录失败") - } + ensureLoginAndExecute { + QPlayAutoSDK.playerSeek(Int(position)) + } onFailure: { error in + print("QPlayAutoManager: 跳转播放位置失败 - \(error)") + } } // MARK: - 扩展功能方法(需要添加登录检查) /// 设置播放模式 @objc public func setPlayMode(_ playMode: QPlayAutoPlayMode, completion: @escaping (Bool) -> Void) { - ensureLoginAndExecute { - QPlayAutoSDK.setPlayMode(playMode) { (success, dict) in - completion(success) - } - } onLoginFailed: { - print("QPlayAutoManager: 设置播放模式失败 - 登录失败") - completion(false) - } + ensureLoginAndExecute { + QPlayAutoSDK.setPlayMode(playMode) { (success, dict) in + completion(success) + } + } onFailure: { error in + print("QPlayAutoManager: 设置播放模式失败 - \(error)") + completion(false) + } } /// 收藏/取消收藏歌曲 @@ -568,11 +611,12 @@ import QPlayAutoSDK QPlayAutoSDK.setFavoriteStateWithSong(song, isFavorite: isFavorite) { errorCode in completion(errorCode == 0) } - } onLoginFailed: { - print("QPlayAutoManager: 设置收藏状态失败 - 登录失败") - completion(false) + }onFailure: { error in + print("QPlayAutoManager: 设置收藏状态失败 - 登录失败 - \(error)") + completion(false) } - } + } + /// 获取相似歌曲 @objc public func requestSimilarSongs(song: QPlayAutoListItem, completion: @escaping ([QPlayAutoListItem]?, Error?) -> Void) { @@ -585,12 +629,12 @@ import QPlayAutoSDK completion(nil, error) } } - } onLoginFailed: { - print("QPlayAutoManager: 请求相似歌曲失败 - 登录失败") - let error = NSError(domain: "QPlayAutoManager", code: -1, userInfo: [NSLocalizedDescriptionKey: "QQ音乐未安装"]) + }onFailure: { error in + print("QPlayAutoManager: 请求相似歌曲失败 - 登录失败 - \(error)") completion(nil, error) - } - } + } + } + /// 获取歌词 @objc public func requestLyric(song: QPlayAutoListItem, completion: @escaping (QPlayAutoLyric?, Error?) -> Void) { @@ -603,12 +647,11 @@ import QPlayAutoSDK completion(nil, error) } } - } onLoginFailed: { - print("QPlayAutoManager: 请求歌词失败 - 登录失败") - let error = NSError(domain: "QPlayAutoManager", code: -1, userInfo: [NSLocalizedDescriptionKey: "QQ音乐未安装"]) + } onFailure: { error in + print("QPlayAutoManager: 请求相似歌曲失败 - 登录失败 - \(error)") completion(nil, error) - } - } + } + } /// 获取歌曲列表 @objc public func requestSonglist(type: QPlayAutoSongListType, completion: @escaping ([QPlayAutoListItem]?, Error?) -> Void) { @@ -621,12 +664,11 @@ import QPlayAutoSDK completion(nil, error) } } - } onLoginFailed: { - print("QPlayAutoManager: 请求歌单失败 - 登录失败") - let error = NSError(domain: "QPlayAutoManager", code: -1, userInfo: [NSLocalizedDescriptionKey: "QQ音乐未安装"]) + } onFailure: { error in + print("QPlayAutoManager: 请求相似歌曲失败 - 登录失败 - \(error)") completion(nil, error) - } - } + } + } /// 播放指定歌曲列表 @objc public func playAtIndex(songList: [QPlayAutoListItem], playIndex: Int, completion: @escaping (Bool) -> Void) { @@ -634,11 +676,11 @@ import QPlayAutoSDK QPlayAutoSDK.play(atIndex: songList, play: playIndex) { errorCode in completion(errorCode == 0) } - } onLoginFailed: { - print("QPlayAutoManager: 播放指定索引歌曲失败 - QQ音乐未安装") + } onFailure: { error in + print("QPlayAutoManager: 设置收藏状态失败 - 登录失败 - \(error)") completion(false) - } - } + } + } // MARK: - 状态查询 @objc public var isConnected: Bool { @@ -729,17 +771,21 @@ import QPlayAutoSDK @objc public func search( keyword: String, searchType: QPlayAutoSearchType, - completion: @escaping ([QPlayAutoListItem]?) -> Void + completion: @escaping ([QPlayAutoListItem]?) -> Void, + onError: ((Error?) -> Void)? = nil ) { ensureLoginAndExecute { QPlayAutoSDK.search(keyword, type: searchType, firstPage: true) { (error, items) in completion(items) } - } onLoginFailed: { - print("QPlayAutoManager: 搜索失败 - 登录失败") - completion(nil) + }onFailure: { error in + print("QPlayAutoManager: 搜索失败 - 登录失败 - \(error)") + let loginError = NSError(domain: "QPlayAutoManager", code: -1, userInfo: [ + NSLocalizedDescriptionKey: "登录失败: \(error)" + ]) + onError?(loginError) + } } - } @objc public func getItems( playlistId: String, @@ -756,11 +802,12 @@ import QPlayAutoSDK completion(nil) } } - } onLoginFailed: { - print("QPlayAutoManager: 获取播放列表失败 - 登录失败") + } onFailure: { error in + print("QPlayAutoManager: 设置收藏状态失败 - 登录失败 - \(error)") completion(nil) - } - } + } + } + // 添加公开的登录方法 @objc public func login(completion: @escaping (Bool) -> Void) {