From 122ec3d57195a92ff4b523c8580f850cdaa9901e Mon Sep 17 00:00:00 2001 From: fdp <1286779656@qq.com> Date: Tue, 22 Jul 2025 20:24:46 +0800 Subject: [PATCH 1/7] =?UTF-8?q?=E5=8A=A0=E5=85=A5ios=E9=80=9A=E8=BF=87uuid?= =?UTF-8?q?=E5=81=9A=E8=BF=87=E6=BB=A4=E7=9A=84=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/azure_speech/AzureAsrHelper.swift | 2 +- .../Sources/ble_service/BleConst.swift | 8 +- .../Sources/ble_service/BleService.swift | 82 +++++++++++++++++-- 3 files changed, 82 insertions(+), 10 deletions(-) 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 9425fa62d..a83e14115 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 @@ -560,7 +560,7 @@ private func detectLanguage(from result: SPXSpeechRecognitionResult) -> String { guard let self = self else { return } while self.isRunning { - print("是否写入: \(self.isWriting)") + // print("是否写入: \(self.isWriting)") if !self.isWriting { usleep(10_000) continue diff --git a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleConst.swift b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleConst.swift index 41d31caa7..790d615cc 100644 --- a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleConst.swift +++ b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleConst.swift @@ -24,6 +24,12 @@ class BleConst { /** 客户端特征配置描述符UUID */ static let CLIENT_CHAR_CONFIG_UUID = CBUUID(string: "00002902-0000-1000-8000-00805f9b34fb") + /** 扫描服务UUID - 用于后台扫描过滤 */ + static let SERVICE_UUID = CBUUID(string: "FD7B") + + /** 扫描服务UUID1 - 用于后台扫描过滤 */ + static let SERVICE_UUID1 = CBUUID(string: "FF11") + /** BLE扫描Action */ static let ACTION_SCAN_RESULT = "com.yunqiinnovation.ble_service.ACTION_BLE_SCAN" @@ -90,4 +96,4 @@ class BleConst { static let AUDIO_CHANNEL_RIGHT: UInt8 = 0x02 /** 立体声 */ static let AUDIO_CHANNEL_STEREO: UInt8 = 0x03 -} \ No newline at end of file +} \ No newline at end of file diff --git a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift index 5c10e0399..3f85aaa1e 100644 --- a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift +++ b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift @@ -229,11 +229,14 @@ private var cmdReplyType: UInt8 = 0 isScanning = true - // 移除服务UUID过滤,扫描所有设备 + // // 移除服务UUID过滤,扫描所有设备 + // manager.scanForPeripherals(withServices: nil, options: [ + // CBCentralManagerScanOptionAllowDuplicatesKey: true // 允许重复以获取更新的广播数据 + // ]) + //let serviceUUIDs = [BleConst.SERVICE_UUID, BleConst.SERVICE_UUID1] manager.scanForPeripherals(withServices: nil, options: [ - CBCentralManagerScanOptionAllowDuplicatesKey: true // 允许重复以获取更新的广播数据 + CBCentralManagerScanOptionAllowDuplicatesKey: true ]) - os_log("🔍 开始扫描所有BLE设备(无过滤),超时时间: %.1f秒", log: logger, type: .info, timeout) // 设置扫描超时 @@ -383,6 +386,7 @@ private var cmdReplyType: UInt8 = 0 // 遍历所有已发现的设备,查找匹配的设备名 for (deviceId, deviceInfo) in discoveredDevices { + print("deviceInfo: \(deviceInfo)") if let name = deviceInfo["name"] as? String, name == deviceName { // 获取对应的广播数据 var deviceData = deviceInfo @@ -1216,14 +1220,76 @@ extension BleService: CBCentralManagerDelegate { os_log("📱 设备RSSI变化较大 - 名称: %{public}@, UUID: %{public}@, RSSI: %d → %d", log: logger, type: .debug, deviceName, deviceId, oldRssi, newRssi) } } else { - // 新设备,需要上报 + // 新设备,需要上报并打印详细信息 shouldNotify = true - os_log("📱 发现新设备 - 名称: %{public}@, UUID: %{public}@", log: logger, type: .info, deviceName, deviceId) - // 打印 hex 数据 - 直接从 deviceInfo 获取 - if let hex = deviceInfo["hex"] as? String { - os_log("📡 设备 hex 数据: %{public}@", log: logger, type: .info, hex) + // 打印详细的设备信息 + os_log("📱 ===== 发现新BLE设备 =====", log: logger, type: .info) + os_log("📱 设备名称: %{public}@", log: logger, type: .info, deviceName) + os_log("📱 设备UUID(MAC地址): %{public}@", log: logger, type: .info, deviceId) + os_log("📱 RSSI信号强度: %d dBm", log: logger, type: .info, RSSI.intValue) + os_log("📱 发现时间: %{public}@", log: logger, type: .info, Date().description) + + // 打印服务UUID信息 + if let serviceUUIDs = advertisementData[CBAdvertisementDataServiceUUIDsKey] as? [CBUUID] { + var uuidDetails: [String] = [] + for uuid in serviceUUIDs { + let uuidString = uuid.uuidString + // 检查是否为16位UUID(通常显示为4个字符) + if uuidString.count == 4 { + let fullUUID = "0000\(uuidString)-0000-1000-8000-00805F9B34FB" + uuidDetails.append("\(uuidString) (完整: \(fullUUID))") + } else { + uuidDetails.append(uuidString) + } + } + os_log("📱 服务UUIDs: %{public}@", log: logger, type: .info, uuidDetails.joined(separator: ", ")) + os_log("📱 服务UUID数量: %d", log: logger, type: .info, serviceUUIDs.count) + } else { + os_log("📱 服务UUIDs: 无", log: logger, type: .info) + } + + // 打印制造商数据 + if let manufacturerData = advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data { + let hexString = manufacturerData.map { String(format: "%02X", $0) }.joined(separator: " ") + os_log("📱 制造商数据: %{public}@", log: logger, type: .info, hexString) + } else { + os_log("📱 制造商数据: 无", log: logger, type: .info) } + + // 打印服务数据 + if let serviceData = advertisementData[CBAdvertisementDataServiceDataKey] as? [CBUUID: Data] { + for (serviceUUID, data) in serviceData { + let hexString = data.map { String(format: "%02X", $0) }.joined(separator: " ") + os_log("📱 服务数据[%{public}@]: %{public}@", log: logger, type: .info, serviceUUID.uuidString, hexString) + } + } else { + os_log("📱 服务数据: 无", log: logger, type: .info) + } + + // 打印本地名称 + if let localName = advertisementData[CBAdvertisementDataLocalNameKey] as? String { + os_log("📱 本地名称: %{public}@", log: logger, type: .info, localName) + } + + // 打印TX功率等级 + if let txPowerLevel = advertisementData[CBAdvertisementDataTxPowerLevelKey] as? NSNumber { + os_log("📱 TX功率等级: %d dBm", log: logger, type: .info, txPowerLevel.intValue) + } + + // 打印是否可连接 + if let isConnectable = advertisementData[CBAdvertisementDataIsConnectable] as? NSNumber { + os_log("📱 可连接状态: %{public}@", log: logger, type: .info, isConnectable.boolValue ? "是" : "否") + } + + // 打印完整的原始广播数据(十六进制) + if let hex = deviceInfo["hex"] as? String, !hex.isEmpty { + os_log("📱 原始广播数据(HEX): %{public}@", log: logger, type: .info, hex) + } else { + os_log("📱 原始广播数据(HEX): 无", log: logger, type: .info) + } + + os_log("📱 =========================", log: logger, type: .info) } // 更新缓存(无论是否上报都要更新) From 95aadb22690f7cdf4da5eccb7bae1350394ab433 Mon Sep 17 00:00:00 2001 From: fdp <1286779656@qq.com> Date: Wed, 23 Jul 2025 16:31:36 +0800 Subject: [PATCH 2/7] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dios=E4=B8=8A=E4=BC=A0?= =?UTF-8?q?=E4=B8=8D=E4=BA=86=E5=90=8E=E5=8F=B0=E7=AE=A1=E7=90=86=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ios/Podfile.lock | 18 +++++++++--------- ios/Runner.xcodeproj/project.pbxproj | 3 +++ .../amap_service/ios/amap_service.podspec | 2 +- .../music_service/ios/music_service.podspec | 2 +- local_plugins/ota/ios/ota.podspec | 2 +- .../spotify_sdk/ios/spotify_sdk.podspec | 4 ++-- .../ios/user_portrait/Package.swift | 2 +- .../volcano_speech/ios/volcano_speech.podspec | 2 +- pubspec.yaml | 2 +- 9 files changed, 20 insertions(+), 17 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index ad056d922..b48f5642d 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -72,15 +72,15 @@ PODS: - QCloudTrack/Beacon (6.4.7) - record_darwin (1.0.0): - Flutter - - SDWebImage (5.21.0): - - SDWebImage/Core (= 5.21.0) - - SDWebImage/Core (5.21.0) + - SDWebImage (5.21.1): + - SDWebImage/Core (= 5.21.1) + - SDWebImage/Core (5.21.1) - SDWebImageWebPCoder (0.14.6): - libwebp (~> 1.0) - SDWebImage/Core (~> 5.17) - spotify_sdk (0.0.1): - Flutter - - tencentcloud_cos_sdk_plugin (1.2.4): + - tencentcloud_cos_sdk_plugin (1.2.3): - Flutter - QCloudCOSXML (= 6.4.7) - WechatOpenSDK-XCFramework (2.0.4) @@ -163,7 +163,7 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/wifi_iot/ios" SPEC CHECKSUMS: - amap_service: 1c3e7e07f51cbbdff03719a4a079c60d015c1c0b + amap_service: 5d40a69bd91031df3da423894b75a0236e26da96 AMapFoundation: 9885c48fc3a78fdfb84a0299a2293e56ea3c9fec AMapNavi: fceb4715f9a3b5a25f330facebee2aa2bdba42dd audio_waveforms: a6dde7fe7c0ea05f06ffbdb0f7c1b2b2ba6cedcf @@ -183,16 +183,16 @@ SPEC CHECKSUMS: keyboard_height_plugin: ef70a8181b29f27670e9e2450814ca6b6dc05b05 libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8 Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d - ota: 20c3e522c57e7ba70ce118109e73650acc9fec76 + ota: 2256716620ec8ecfc2aea220679557f94b13915f permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d QCloudCore: 49e7571984a8e5fd8f0fd58c2f3b806255796223 QCloudCOSXML: 7205e76aa9cf613468222615483c9d7c074e4298 QCloudTrack: 3b53a7fc4fe3920e407f2aa73f2452992a61f7f3 record_darwin: fb1f375f1d9603714f55b8708a903bbb91ffdb0a - SDWebImage: f84b0feeb08d2d11e6a9b843cb06d75ebf5b8868 + SDWebImage: f29024626962457f3470184232766516dee8dfea SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380 - spotify_sdk: a48400bb29f70c4fe251ebfdc9135c37097ac5ca - tencentcloud_cos_sdk_plugin: 7bed564dbe72df23e7b5cacb1e51b1a20cb1639c + spotify_sdk: 60c0a0c8d154f2c67f5598f2d652c3877e2b570a + tencentcloud_cos_sdk_plugin: c3d78b2c3bae42b5a20009dc047f3f1621e6272b WechatOpenSDK-XCFramework: 36fb2bea0754266c17184adf4963d7e6ff98b69f wifi_iot: f645260a2be8608517b2a9bf4c39b98e97003acc diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 2bd851492..6782611c6 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -511,6 +511,7 @@ GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 18.0; MTL_ENABLE_DEBUG_INFO = NO; + NEW_SETTING = ""; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; @@ -649,6 +650,7 @@ GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 18.0; MTL_ENABLE_DEBUG_INFO = YES; + NEW_SETTING = ""; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; @@ -701,6 +703,7 @@ GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 18.0; MTL_ENABLE_DEBUG_INFO = NO; + NEW_SETTING = ""; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; diff --git a/local_plugins/amap_service/ios/amap_service.podspec b/local_plugins/amap_service/ios/amap_service.podspec index 5e90480f9..b33af0daa 100644 --- a/local_plugins/amap_service/ios/amap_service.podspec +++ b/local_plugins/amap_service/ios/amap_service.podspec @@ -22,6 +22,6 @@ A new Flutter project. 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } - s.platform = :ios, '12.0' + s.platform = :ios, '16.0' s.swift_version = '5.0' end diff --git a/local_plugins/music_service/ios/music_service.podspec b/local_plugins/music_service/ios/music_service.podspec index d9aa1dcb3..61019dd4b 100644 --- a/local_plugins/music_service/ios/music_service.podspec +++ b/local_plugins/music_service/ios/music_service.podspec @@ -15,7 +15,7 @@ A new Flutter project. s.source = { :path => '.' } s.source_files = 'Classes/**/*' s.dependency 'Flutter' - s.platform = :ios, '12.0' + s.platform = :ios, '16.0' # Flutter.framework does not contain a i386 slice. s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } diff --git a/local_plugins/ota/ios/ota.podspec b/local_plugins/ota/ios/ota.podspec index 39c28fd0a..b4e84fa29 100644 --- a/local_plugins/ota/ios/ota.podspec +++ b/local_plugins/ota/ios/ota.podspec @@ -15,7 +15,7 @@ A new Flutter project. s.source = { :path => '.' } s.source_files = 'Classes/**/*' s.dependency 'Flutter' - s.platform = :ios, '12.0' + s.platform = :ios, '16.0' s.vendored_frameworks = [ 'Frameworks/DFUnits.framework', 'Frameworks/JL_AdvParse.framework', diff --git a/local_plugins/spotify_sdk/ios/spotify_sdk.podspec b/local_plugins/spotify_sdk/ios/spotify_sdk.podspec index 7be779eb9..dbb91fa4b 100644 --- a/local_plugins/spotify_sdk/ios/spotify_sdk.podspec +++ b/local_plugins/spotify_sdk/ios/spotify_sdk.podspec @@ -15,8 +15,8 @@ Unofficial Spotify Flutter SDK. s.source = { :path => '.' } s.source_files = 'Classes/**/*' s.dependency 'Flutter' - s.platform = :ios, '12.0' - s.ios.deployment_target = '12.0' + s.platform = :ios, '16.0' + s.ios.deployment_target = '16.0' # Flutter.framework does not contain a i386 slice. Only x86_64 simulators are supported. s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } s.swift_version = '5.0' diff --git a/local_plugins/user_portrait/ios/user_portrait/Package.swift b/local_plugins/user_portrait/ios/user_portrait/Package.swift index 4d8e0e3fc..3519e97aa 100644 --- a/local_plugins/user_portrait/ios/user_portrait/Package.swift +++ b/local_plugins/user_portrait/ios/user_portrait/Package.swift @@ -6,7 +6,7 @@ import PackageDescription let package = Package( name: "user_portrait", platforms: [ - .iOS("12.0") + .iOS("16.0") ], products: [ .library(name: "user-portrait", targets: ["user_portrait"]) diff --git a/local_plugins/volcano_speech/ios/volcano_speech.podspec b/local_plugins/volcano_speech/ios/volcano_speech.podspec index 9a70baed1..42b24bd77 100644 --- a/local_plugins/volcano_speech/ios/volcano_speech.podspec +++ b/local_plugins/volcano_speech/ios/volcano_speech.podspec @@ -15,7 +15,7 @@ A Flutter plugin for Volcano Speech services. s.source = { :path => '.' } s.source_files = 'Classes/**/*' s.dependency 'Flutter' - s.platform = :ios, '12.0' + s.platform = :ios, '16.0' # Flutter.framework does not contain a i386 slice. s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } diff --git a/pubspec.yaml b/pubspec.yaml index a6c743df9..f8b2f463c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: deep_voice description: "A new Flutter project." publish_to: "none" -version: 1.0.0+6 +version: 1.0.1+7 environment: sdk: ">=3.3.0 <4.0.0" From 7b9111bdfa5e31acd56c5923667dcfd420bcafff Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Wed, 23 Jul 2025 18:28:20 +0800 Subject: [PATCH 3/7] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/data/models/appconfig_model.dart | 6 +++--- lib/data/models/appconfig_model.g.dart | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/data/models/appconfig_model.dart b/lib/data/models/appconfig_model.dart index 7a91df50d..7baecff06 100644 --- a/lib/data/models/appconfig_model.dart +++ b/lib/data/models/appconfig_model.dart @@ -76,9 +76,9 @@ class DBProduct { final int devicetype; final String devicename; final String scanuuid; - final String version; - final String updatedescription; - final String updatepackageaddress; + final String? version; + final String? updatedescription; + final String? updatepackageaddress; DBProduct({ required this.id, diff --git a/lib/data/models/appconfig_model.g.dart b/lib/data/models/appconfig_model.g.dart index c83d063fb..b8e08075e 100644 --- a/lib/data/models/appconfig_model.g.dart +++ b/lib/data/models/appconfig_model.g.dart @@ -77,9 +77,9 @@ DBProduct _$DBProductFromJson(Map json) => DBProduct( devicename: json['devicename'] as String, devicetype: json['devicetype'] as int, scanuuid: json['scanuuid'] as String, - version: json['version'] as String, - updatedescription: json['updatedescription'] as String, - updatepackageaddress: json['updatepackageaddress'] as String, + version: json['version'] as String?, + updatedescription: json['updatedescription'] as String?, + updatepackageaddress: json['updatepackageaddress'] as String?, ); Map _$DBProductToJson(DBProduct instance) => { From d52ee866a92fda20394d4c6d4ae0c18cbd11e68f Mon Sep 17 00:00:00 2001 From: fdp <1286779656@qq.com> Date: Wed, 23 Jul 2025 20:04:35 +0800 Subject: [PATCH 4/7] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=80=BB=E8=BE=91?= =?UTF-8?q?=EF=BC=8C=E4=BF=AE=E5=A4=8D=E7=89=88=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controllers/pairing_controller.dart | 28 +++++++++++++------ .../Sources/ble_service/BleService.swift | 2 +- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/lib/modules/pairing/controllers/pairing_controller.dart b/lib/modules/pairing/controllers/pairing_controller.dart index 819ca1d39..336e3b81a 100644 --- a/lib/modules/pairing/controllers/pairing_controller.dart +++ b/lib/modules/pairing/controllers/pairing_controller.dart @@ -660,14 +660,26 @@ class PairingController extends GetxController { String? uuids = deviceInfo["id"]; _verifiedDeviceAddress = uuids; print('找到匹配设备,获取到 uuids: $uuids'); - // 检查是否已经绑定 - // if (User.instance.earphonelicense != "" && - // User.instance.earphonemac == device.address) { - // // 已经绑定 - // return 10; - // } else { - return 1; - // } + //检查是否已经绑定 + final existingDevice = User.instance.devices.firstWhere( + (d) => + d.devicetype == 2 && + d.license != "" && + d.devicemac == device.address, + orElse: () => UserDevice( + id: -1, + uid: '', + devicetype: -1, + devicename: '', + devicemac: '', + license: ''), + ); + if (existingDevice.id != -1) { + // 已经绑定 + return 10; + } else { + return 1; + } } } else if (Platform.isAndroid) { // Android设备,提取15-24字节的授权码 diff --git a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift index 6fbf53154..66b8d73e2 100644 --- a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift +++ b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift @@ -235,7 +235,7 @@ private var cmdReplyType: UInt8 = 0 // ]) let serviceUUIDs = [BleConst.SERVICE_UUID, BleConst.SERVICE_UUID1] - manager.scanForPeripherals(withServices: serviceUUIDs, options: [ + manager.scanForPeripherals(withServices: nil, options: [ CBCentralManagerScanOptionAllowDuplicatesKey: true // 允许重复以获取更新的广播数据 ]) os_log("🔍 开始扫描所有BLE设备(无过滤),超时时间: %.1f秒", log: logger, type: .info, timeout) From 68a0bc8202135e3588c95c61885efa7481329d6c Mon Sep 17 00:00:00 2001 From: fdp <1286779656@qq.com> Date: Wed, 23 Jul 2025 22:06:46 +0800 Subject: [PATCH 5/7] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=BF=BB=E8=AF=91?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/data/services/ble_manager.dart | 3 +- .../Sources/azure_speech/AzureAsrHelper.swift | 147 +++++++++++++----- 2 files changed, 109 insertions(+), 41 deletions(-) diff --git a/lib/data/services/ble_manager.dart b/lib/data/services/ble_manager.dart index 96bea2fc6..b55927d5a 100644 --- a/lib/data/services/ble_manager.dart +++ b/lib/data/services/ble_manager.dart @@ -562,7 +562,8 @@ class BleManager extends GetxService { 'volcanoAppId': '2316081933', 'volcanoToken': 'mcJTh6OelBqU_L78lgGd77Nyrc_-5-En', 'serverurl': serverurl, - 'usertoken': jsonString + 'usertoken': jsonString, + 'supportedLanguages': ['zh-CN'], }; // 记录日志(不记录敏感信息) 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 a83e14115..24b60e444 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 @@ -272,7 +272,15 @@ public class AzureAsrHelper: NSObject { // 创建识别器 if isAutoDetectLanguage { - let autoDetectConfig = try SPXAutoDetectSourceLanguageConfiguration(supportedLanguages) + // 使用更精确的语言配置方式 + var sourceLanguageConfigs: [SPXSourceLanguageConfiguration] = [] + for language in supportedLanguages { + if let config = try? SPXSourceLanguageConfiguration(language) { + sourceLanguageConfigs.append(config) + } + } + + let autoDetectConfig = try SPXAutoDetectSourceLanguageConfiguration(sourceLanguageConfigurations: sourceLanguageConfigs) recognizer = try SPXSpeechRecognizer( speechConfiguration: speechConfig!, autoDetectSourceLanguageConfiguration: autoDetectConfig, @@ -336,10 +344,31 @@ public class AzureAsrHelper: NSObject { recognizer.addRecognizingEventHandler { [weak self] (_, event) in guard let self = self else { return } let result = event.result - let detectedLanguage = self.detectLanguage(from: result) - print("识别中事件: \(detectedLanguage)") - DispatchQueue.main.async { - callback.onRecognizing(result.text ?? "", detectedLanguage) + print("识别中文本: \(result.text)") + // 优化:只处理非空结果 + if !(result.text?.isEmpty ?? true) { + let detectedLanguage: String + if self.isAutoDetectLanguage { + do { + let langResult = try SPXAutoDetectSourceLanguageResult(result) + let rawLanguage = langResult.language + print("[DEBUG] 原始检测语言: \(rawLanguage ?? "nil"), 文本: \(result.text ?? "nil"), 支持的语言: \(self.supportedLanguages)") + + // 应用启发式语言检测纠正 + detectedLanguage = self.correctLanguageDetection(rawLanguage: rawLanguage ?? "", text: result.text ?? "") + print("[DEBUG] 纠正后语言: \(detectedLanguage)") + } catch { + print("[DEBUG] 语言检测异常: \(error.localizedDescription)") + detectedLanguage = "" + } + } else { + detectedLanguage = self.currentLanguage + print("[DEBUG] 使用当前语言: \(detectedLanguage)") + } + print("识别中事件: \(detectedLanguage)") + DispatchQueue.main.async { + callback.onRecognizing(result.text ?? "", detectedLanguage) + } } } @@ -347,8 +376,27 @@ public class AzureAsrHelper: NSObject { recognizer.addRecognizedEventHandler { [weak self] (_, event) in guard let self = self else { return } let result = event.result - if result.reason == .recognizedSpeech { - let detectedLanguage = self.detectLanguage(from: result) + print("识别完成文本: \(result.text)") + if result.reason == .recognizedSpeech && !(result.text?.isEmpty ?? true) { + let detectedLanguage: String + if self.isAutoDetectLanguage { + do { + let langResult = try SPXAutoDetectSourceLanguageResult(result) + let rawLanguage = langResult.language + print("[DEBUG] 识别完成 - 原始检测语言: \(rawLanguage ?? "nil"), 文本: \(result.text ?? "nil"), 支持的语言: \(self.supportedLanguages)") + + // 应用启发式语言检测纠正 + let correctedLanguage = self.correctLanguageDetection(rawLanguage: rawLanguage ?? "", text: result.text ?? "") + detectedLanguage = correctedLanguage.isEmpty ? (self.supportedLanguages.first ?? self.currentLanguage) : correctedLanguage + print("[DEBUG] 识别完成 - 纠正后语言: \(detectedLanguage)") + } catch { + print("[DEBUG] 识别完成 - 语言检测异常: \(error.localizedDescription)") + detectedLanguage = self.supportedLanguages.first ?? self.currentLanguage + } + } else { + detectedLanguage = self.currentLanguage + print("[DEBUG] 识别完成 - 使用当前语言: \(detectedLanguage)") + } print("识别完成事件: \(detectedLanguage)") DispatchQueue.main.async { callback.onResult(result.text ?? "", detectedLanguage) @@ -387,38 +435,6 @@ public class AzureAsrHelper: NSObject { return true } -// 提取的辅助方法 -private func detectLanguage(from result: SPXSpeechRecognitionResult) -> String { - if isAutoDetectLanguage { - let langResult = try SPXAutoDetectSourceLanguageResult(result) - return langResult.language ?? "" - } else { - return currentLanguage - } -} - /** - * 获取检测到的语言 - */ - private func getDetectedLanguage(from result: SPXSpeechRecognitionResult) -> String { - if !isAutoDetectLanguage { - return "" - } - - // 尝试从属性中获取语言 - if let properties = result.properties, - let language = properties.getPropertyByName("SpeechServiceResponse_RecognitionLanguage") { - return language - } - - // 尝试另一种方式获取语言 - do { - let langResult = try SPXAutoDetectSourceLanguageResult(result) - print("获取检测到的语言") - return langResult.language ?? "" - } catch { - return "" - } - } /** * 禁用蓝牙音频功能,切换回正常音频模式 @@ -853,6 +869,57 @@ private func detectLanguage(from result: SPXSpeechRecognitionResult) -> String { } } } + + /** + * 启发式语言检测纠正方法 + * 基于文本内容特征来纠正Azure语音服务的语言检测结果 + */ + private func correctLanguageDetection(rawLanguage: String, text: String) -> String { + // 如果原始检测为空,返回空 + if rawLanguage.isEmpty || text.isEmpty { + return rawLanguage + } + + let trimmedText = text.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() + + // 英文特征检测 + let englishPattern = "^[a-z\\s\\.,!?;:'-]+$" + let englishRegex = try? NSRegularExpression(pattern: englishPattern, options: .caseInsensitive) + let englishMatches = englishRegex?.numberOfMatches(in: trimmedText, options: [], range: NSRange(location: 0, length: trimmedText.count)) ?? 0 + + // 中文特征检测 + let chinesePattern = "[\\u4e00-\\u9fff]" + let chineseRegex = try? NSRegularExpression(pattern: chinesePattern, options: []) + let chineseMatches = chineseRegex?.numberOfMatches(in: trimmedText, options: [], range: NSRange(location: 0, length: trimmedText.count)) ?? 0 + + // 常见英文单词列表 + let commonEnglishWords = ["hello", "hi", "please", "thank", "you", "yes", "no", "good", "morning", "afternoon", "evening", "night", "how", "are", "what", "where", "when", "why", "who", "can", "could", "would", "should", "will", "the", "and", "or", "but", "if", "then", "this", "that", "these", "those"] + + let hasCommonEnglishWords = commonEnglishWords.contains { word in + trimmedText.contains(word) + } + + print("[DEBUG] 语言纠正分析 - 文本: '\(trimmedText)', 英文匹配: \(englishMatches > 0), 中文字符: \(chineseMatches), 常见英文词: \(hasCommonEnglishWords), 原始语言: \(rawLanguage)") + + // 纠正逻辑 + if rawLanguage == "zh-CN" { + // 如果Azure检测为中文,但文本明显是英文特征 + if (englishMatches > 0 && chineseMatches == 0) || hasCommonEnglishWords { + print("[DEBUG] 纠正: \(rawLanguage) -> en-US (检测到英文特征)") + return "en-US" + } + } else if rawLanguage == "en-US" { + // 如果Azure检测为英文,但包含中文字符 + if chineseMatches > 0 { + print("[DEBUG] 纠正: \(rawLanguage) -> zh-CN (检测到中文字符)") + return "zh-CN" + } + } + + // 如果没有明确的纠正理由,保持原始检测结果 + return rawLanguage + } + // MARK: - 回调函数 /** * 连续识别回调接口 @@ -899,4 +966,4 @@ private func detectLanguage(from result: SPXSpeechRecognitionResult) -> String { */ func onError(_ error: String) } -} +} From 1126c50db0dfbd3af2e83388b384cfda205931e6 Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Thu, 24 Jul 2025 18:02:46 +0800 Subject: [PATCH 6/7] =?UTF-8?q?=E4=B8=8A=E4=BC=A0ai=E5=8A=A9=E6=89=8B?= =?UTF-8?q?=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../agent_service/AgentServiceImpl.swift | 21 +- .../ios/Classes/AmapService.swift | 2 +- .../ios/Classes/AmapServicePlugin.swift | 4 +- .../Sources/chat_api/ChatApiService.swift | 44 +- .../chat_api/CustomSseClientTransport.swift | 98 +++- .../chat_api/Sources/chat_api/MCPClient.swift | 235 +++++++- .../chat_api/SystemFunctionHandler.swift | 207 +++++-- .../location_service/LocationService.swift | 551 +++++++++--------- .../LocationServicePlugin.swift | 40 +- 9 files changed, 823 insertions(+), 379 deletions(-) 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 753dd87c4..29dd1d360 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 @@ -464,6 +464,7 @@ audioStream.saveAudioDataTo(data: audioData) os_log("中断当前AI流式响应", log: logger, type: .info) _ = interruptCurrentResponse() } + stopTts(); isAiStreaming = true os_log("设置AI流式状态为true", log: logger, type: .info) @@ -689,7 +690,6 @@ private func handleJSONDictionary(_ jsonDict: [String: Any], toolName: String) - notreply = _notreply } metadata["notreply"] = notreply - // 处理卡片数据 if let cardMusic = jsonDict["card_music"] as? [String: Any] { processMusicCard(cardMusic) @@ -958,6 +958,7 @@ class ChatApiStreamCallback: StreamCallback { } func onToken(_ sessionId:String,_ token: String) { do { +// print("liwei--------------- AI Call onToken! sessionId:\(sessionId) token:\(token)") guard let agentService = try agentService else { return } if (sessionId != agentService.currsessionId) { return @@ -1044,18 +1045,12 @@ class ChatApiStreamCallback: StreamCallback { let name = functionCall["name"] as? String ?? "" os_log("收到函数调用: %{public}@", log: agentService.logger, type: .info, name) print("onFunctionCall=functionCall\(functionCall)") - // try agentService.audioPlayer?.playCallingSound() - + try agentService.sendEvent(name: "function_call", data: [ "name": name, "arguments": functionCall ]) - - if name == "exit_interaction" { - os_log("执行退出交互", log: agentService.logger, type: .info) - agentService.stopRecognition() - } - + }catch{ print("liwei--------------- AI Call onFunctionCall 异常: \(error)") } @@ -1067,20 +1062,22 @@ class ChatApiStreamCallback: StreamCallback { guard let agentService = agentService else { return } let name = functionCall["name"] as? String ?? "" - os_log("收到函数调用结果: %{public}@", log: agentService.logger, type: .info, functionCallResult) + // agentService.audioPlayer?.stopCallingSound() let resultmap = agentService.autoHandleFunctionCallResult(toolName:name,functionCallResult:functionCallResult) metadata = resultmap["meta"] as? String ?? "" broadcast = resultmap["broadcast"] as? Bool ?? true reply = !(resultmap["notreply"] as? Bool ?? true) - os_log("卡片数据: %{public}@", log: agentService.logger, type: .info, metadata) agentService.sendEvent(name: "function_call_result", data: [ "function_call": functionCall, "result": functionCallResult, "meta":metadata, ]) - os_log("处理函数调用结果完成", log: agentService.logger, type: .info) + os_log("收到函数调用结果! broadcast: %{public}@ reply: %{public}@", + log: agentService.logger, + type: .info, + "\(broadcast)", "\(reply)") }catch{ print("liwei--------------- AI Call onFunctionCallResult 异常: \(error)") } diff --git a/local_plugins/amap_service/ios/Classes/AmapService.swift b/local_plugins/amap_service/ios/Classes/AmapService.swift index 30f6d1308..c962cc002 100644 --- a/local_plugins/amap_service/ios/Classes/AmapService.swift +++ b/local_plugins/amap_service/ios/Classes/AmapService.swift @@ -24,7 +24,7 @@ public class AmapService: NSObject, AMapNaviDriveManagerDelegate { // 设置高德地图服务 // 1. 设置隐私合规 AMapServices.shared()?.enableHTTPS = true - AMapServices.shared()?.apiKey = "aa60a03be9f6531bcc64029e60f95d03" + AMapServices.shared()?.apiKey = "0b704e172b7ade5a7a18728e8f517a05" } public func startNavigation(from start: String, to end: String) { diff --git a/local_plugins/amap_service/ios/Classes/AmapServicePlugin.swift b/local_plugins/amap_service/ios/Classes/AmapServicePlugin.swift index 05e7c0ec4..4be2e55a8 100644 --- a/local_plugins/amap_service/ios/Classes/AmapServicePlugin.swift +++ b/local_plugins/amap_service/ios/Classes/AmapServicePlugin.swift @@ -22,7 +22,7 @@ public class AmapServicePlugin: NSObject, FlutterPlugin, FlutterStreamHandler { // 高德SDK初始化 DispatchQueue.main.async { - AMapServices.shared()?.apiKey = "aa60a03be9f6531bcc64029e60f95d03" + AMapServices.shared()?.apiKey = "0b704e172b7ade5a7a18728e8f517a05" AMapServices.shared()?.enableHTTPS = true if #available(iOS 13.0, *) { @@ -101,4 +101,4 @@ public class AmapServicePlugin: NSObject, FlutterPlugin, FlutterStreamHandler { result(FlutterMethodNotImplemented) } } -} \ No newline at end of file +} diff --git a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatApiService.swift b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatApiService.swift index ad6b53219..cdf5cfa3a 100644 --- a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatApiService.swift +++ b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatApiService.swift @@ -67,12 +67,24 @@ public class ChatApiService: NSObject { // MARK: - 公共方法 /// 设置流式回调 - public func setStreamCallback(_ sessionId:String,_ callback: StreamCallback) { + public func setStreamCallback(_ sessionId: String, _ callback: StreamCallback) { + print("[ChatApiService] 设置回调 for sessionId: \(sessionId)") + print("[ChatApiService] 回调对象: \(callback)") self.sessionCallbacks[sessionId] = callback + print("[ChatApiService] 回调设置完成,当前回调数量: \(sessionCallbacks.count)") } - public func getSessionCallback(_ sessionId:String)->StreamCallback?{ - return self.sessionCallbacks[sessionId] + public func getSessionCallback(_ sessionId: String) -> StreamCallback? { + let callback = self.sessionCallbacks[sessionId] + print("[ChatApiService] getSessionCallback for sessionId: \(sessionId)") + print("[ChatApiService] sessionCallbacks count: \(sessionCallbacks.count)") + print("[ChatApiService] sessionCallbacks keys: \(Array(sessionCallbacks.keys))") + print("[ChatApiService] callback exists: \(callback != nil)") + return callback + } + //移除回调函数 + public func removeSessionCallback(_ sessionId: String){ + self.sessionCallbacks.removeValue(forKey: sessionId) } /// 初始化ChatAPI服务 @@ -289,6 +301,7 @@ public func sendMessageStream(_ sessionId: String, messages: [[String: Any]]) { let query: ChatQuery if !tools.isEmpty { let chatTools = self.convertToOpenAITools(tools) + query = ChatQuery( messages: chatMessages, model: currentModel, @@ -296,6 +309,8 @@ public func sendMessageStream(_ sessionId: String, messages: [[String: Any]]) { temperature: 0.7, tools: chatTools ) + + print("[ChatApiService] 发起AI问答 问题:\(messages[0]) 工具数量: \(chatTools.count)") } else { query = ChatQuery( messages: chatMessages, @@ -319,7 +334,17 @@ public func sendMessageStream(_ sessionId: String, messages: [[String: Any]]) { // 处理文本内容 if let content = delta.content { replyContext += content - self.getSessionCallback(sessionId)?.onToken(sessionId, content) + print("[ChatApiService] 收到内容: \(content)") + print("[ChatApiService] 当前sessionId: \(sessionId)") + + let callback = self.getSessionCallback(sessionId) + if let callback = callback { + print("[ChatApiService] 找到回调,准备调用 onToken") + callback.onToken(sessionId, content) + print("[ChatApiService] onToken 调用完成") + } else { + print("[ChatApiService] 警告:未找到 sessionId \(sessionId) 的回调") + } } // 收集工具调用信息 @@ -690,11 +715,10 @@ private func convertMapToJsonObject(_ map: [String: Any]) -> [String: Any] { currentStreamTask = nil // 2. 通知旧会话被中止 - getSessionCallback(oldSessionId)?.onComplete(currSessionId) +// getSessionCallback(oldSessionId)?.onComplete(currSessionId) // 3. 清理状态 currSessionId = "" - sessionCallbacks.removeValue(forKey: oldSessionId) toolCalls.removeAll() } } @@ -703,7 +727,7 @@ private func convertMapToJsonObject(_ map: [String: Any]) -> [String: Any] { private func clearCurrentSession() { let oldSessionId = currSessionId currSessionId = "" - sessionCallbacks.removeValue(forKey: oldSessionId) + self.sessionCallbacks.removeValue(forKey: oldSessionId) currentStreamTask = nil toolCalls.removeAll() } @@ -876,14 +900,17 @@ private func convertMapToJsonObject(_ map: [String: Any]) -> [String: Any] { var chatTools: [ChatQuery.ChatCompletionToolParam] = [] for tool in tools { + guard let type = tool["type"] as? String, type == "function", let function = tool["function"] as? [String: Any], let name = function["name"] as? String, let description = function["description"] as? String, let parameters: [String: Any] = function["parameters"] as? [String: Any] else { + print("无法将 parameters 转换为 JSON 数据") continue } + // 将字典转换为 JSON 数据 guard let jsonData = try? JSONSerialization.data(withJSONObject: parameters, options: []) else { print("无法将 parameters 转换为 JSON 数据") @@ -904,6 +931,7 @@ private func convertMapToJsonObject(_ map: [String: Any]) -> [String: Any] { // 创建工具参数 let toolParam = ChatQuery.ChatCompletionToolParam(function: functionDefinition) + print("解析工具 \(name):\(schema)") chatTools.append(toolParam) } @@ -922,4 +950,4 @@ private class LocalFunctionHandler: FunctionHandler { func handle(arguments: [String: Any]) async throws -> String { return "LOCAL_FUNCTION:\(functionName)" } -} +} diff --git a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/CustomSseClientTransport.swift b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/CustomSseClientTransport.swift index 2b9f58eb6..e5da6b689 100644 --- a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/CustomSseClientTransport.swift +++ b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/CustomSseClientTransport.swift @@ -17,6 +17,9 @@ public actor CustomSseClientTransport: Transport { private let reconnectionTime: TimeInterval private let session: URLSession + // 断线回调 + private let onConnectionLost: (() -> Void)? + // URL解析结果 private var hostPart: String = "" private var pathPart: String = "" @@ -26,6 +29,7 @@ public actor CustomSseClientTransport: Transport { // 连接状态 private var isConnected = false private var isInitialized = false + private var connectionMonitorTask: Task? // 消息流 private let messageStream: AsyncThrowingStream @@ -57,11 +61,13 @@ public actor CustomSseClientTransport: Transport { urlString: String, reconnectionTime: TimeInterval = 1.0, session: URLSession = .shared, - logger: Logger? = nil + logger: Logger? = nil, + onConnectionLost: (() -> Void)? = nil ) { self.originalUrlString = urlString self.reconnectionTime = reconnectionTime self.session = session + self.onConnectionLost = onConnectionLost self.logger = logger ?? Logger( label: "com.yunqiinnovation.chat_api.CustomSseClientTransport", factory: { _ in SwiftLogNoOpLogHandler() } @@ -93,6 +99,9 @@ public actor CustomSseClientTransport: Transport { _ = try await waitForEndpoint() isConnected = true + + // 启动连接监控 + startConnectionMonitoring() } /// 断开传输连接 @@ -104,6 +113,10 @@ public actor CustomSseClientTransport: Transport { isConnected = false isInitialized = false + // 停止连接监控 + connectionMonitorTask?.cancel() + connectionMonitorTask = nil + // 取消SSE任务 sseTask?.cancel() sseTask = nil @@ -120,6 +133,11 @@ public actor CustomSseClientTransport: Transport { endpointContinuation = nil } + /// 检查连接是否活跃 + public func isConnectionActive() -> Bool { + return isConnected && isInitialized && eventSource != nil + } + /// 发送数据 public func send(_ data: Data) async throws { guard isConnected, let endpointUrl = endpointUrl else { @@ -329,10 +347,17 @@ public actor CustomSseClientTransport: Transport { /// 处理SSE错误 private func handleSseError(_ error: String) { + logger.error("SSE错误: \(error)") + let mcpError = MCPError.internalError("SSE error: \(error)") endpointContinuation?.resume(throwing: mcpError) endpointContinuation = nil messageContinuation.finish(throwing: mcpError) + + // 触发连接断开处理 + Task { + await handleConnectionLost() + } } /// 处理SSE连接打开 @@ -382,4 +407,73 @@ public actor CustomSseClientTransport: Transport { private func handleOtherEvent(_ data: String) { safeParseMessage(data) } -} \ No newline at end of file + + // MARK: - 连接监控和错误处理 + + /// 启动连接监控 + private func startConnectionMonitoring() { + connectionMonitorTask = Task { [weak self] in + await self?.monitorConnectionHealth() + } + } + + /// 监控连接健康状态 + private func monitorConnectionHealth() async { + while isInitialized && !Task.isCancelled { + // 检查连接状态 + if !isConnectionActive() { + await handleConnectionLost() + break + } + + // 每5秒检查一次连接状态 + try? await Task.sleep(nanoseconds: 5_000_000_000) + } + } + + /// 处理连接丢失 - 简化版本,只重置状态 + private func handleConnectionLost() async { + guard isConnected else { return } + + logger.warning("SSE连接丢失,重置连接状态") + + // 重置连接状态 + await resetConnectionState() + + // 通知外部连接丢失 + onConnectionLost?() + } + + /// 重置连接状态 + private func resetConnectionState() async { + // 标记连接已断开 + isConnected = false + + // 停止连接监控 + connectionMonitorTask?.cancel() + connectionMonitorTask = nil + + // 清理连接资源 + await cleanupConnection() + + // 重置端点信息 + endpointUrl = nil + + // 如果有等待中的端点请求,取消它 + endpointContinuation?.resume(throwing: MCPError.internalError("Connection lost")) + endpointContinuation = nil + + logger.info("连接状态已重置,等待下次工具调用时重连") + } + + /// 清理连接资源 + private func cleanupConnection() async { + // 取消SSE任务 + sseTask?.cancel() + sseTask = nil + + // 关闭EventSource + await eventSource?.close() + eventSource = nil + } +} diff --git a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/MCPClient.swift b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/MCPClient.swift index aba509fc5..b42708dca 100644 --- a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/MCPClient.swift +++ b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/MCPClient.swift @@ -39,38 +39,71 @@ public class MCPSubClient { private var toolMaps: [[String: Any]] = [] private var isConnectedFlag = false - public init(serverId: String, serverUrl: String,filterTools:String) { + // 重连配置 + private let maxRetryAttempts = 3 + private let initialReconnectDelay: TimeInterval = 1.0 + private let maxReconnectDelay: TimeInterval = 30.0 + private var currentReconnectDelay: TimeInterval = 1.0 + private var retryCount = 0 + private var reconnectTask: Task? + + // 连接状态锁 + private let connectionLock = NSLock() + + public init(serverId: String, serverUrl: String, filterTools: String) { self.serverId = serverId self.serverUrl = serverUrl self.filterTools = filterTools + self.currentReconnectDelay = initialReconnectDelay } public func connect() async -> Bool { + return await connectionLock.withLock { + await performConnect() + } + } + + private func performConnect() async -> Bool { + if isConnectedFlag { + return true + } + do { // 1. 创建MCP客户端 - mcpClient = Client(name: "deep_voice_\(serverId)", version: "1.0.0") + self.mcpClient = Client(name: "deep_voice_\(serverId)", version: "1.0.0") - // 2. 创建自定义SSE传输层 + // 2. 创建自定义SSE传输层,添加断线回调 let swiftLogger = Logger(label: "mcp.transport.custom_sse.\(serverId)") let transport = CustomSseClientTransport( urlString: serverUrl, reconnectionTime: 1.0, - logger: swiftLogger + logger: swiftLogger, + onConnectionLost: { [weak self] in + Task { + await self?.handleConnectionLost() + } + } ) - // 3. 连接到服务器 - _ = try await mcpClient!.connect(transport: transport) + // 3. 连接到服务器(添加超时) + try await withTimeout(seconds: 10) { + try await self.mcpClient!.connect(transport: transport) + } // 4. 获取工具列表 - let (toolList, _) = try await mcpClient!.listTools() + let (toolList, _) = try await self.mcpClient!.listTools() // 5. 处理工具列表 processTools(toolList) isConnectedFlag = true + retryCount = 0 // 重置重试计数 + currentReconnectDelay = initialReconnectDelay // 重置延迟 + return true } catch { + await handleConnectionError(error as! Error) return false } } @@ -130,7 +163,32 @@ public class MCPSubClient { ] } + /// 检查连接状态 + /// 检查连接状态 + public func checkConnection() async -> Bool { + if !isConnectedFlag { + return await connect() + } + + // 检查传输层连接状态 + // 这里可以添加ping测试或其他连接验证 + + return isConnectedFlag + } + + /// 调用工具前检查连接 public func callTool(name: String, arguments: [String: Any]) async -> [String: Any]? { + // 先检查连接状态 + if !(await checkConnection()) { + return [ + "content": [[ + "type": "text", + "text": "Tool call failed: mcp: service not connected" + ]], + "isError": true + ] + } + guard isConnectedFlag, let client = mcpClient else { return createErrorResult("Not connected to MCP server") } @@ -227,6 +285,74 @@ public class MCPSubClient { } isConnectedFlag = false } + + /// 处理连接断开 + private func handleConnectionLost() async { + await connectionLock.withLock { + if isConnectedFlag { + print("[\(serverId)] 连接已断开,更新状态") + isConnectedFlag = false + + // 启动重连逻辑 + startReconnection() + } + } + } + + /// 启动重连 + private func startReconnection() { + // 取消之前的重连任务 + reconnectTask?.cancel() + + reconnectTask = Task { [weak self] in + await self?.performReconnection() + } + } + + /// 执行重连 + private func performReconnection() async { + while retryCount < maxRetryAttempts && !Task.isCancelled { + retryCount += 1 + + print("[\(serverId)] 尝试重连 (\(retryCount)/\(maxRetryAttempts)),延迟 \(currentReconnectDelay)秒") + + try? await Task.sleep(for: .seconds(currentReconnectDelay)) + + if await connect() { + print("[\(serverId)] 重连成功") + return + } + + // 指数退避 + currentReconnectDelay = min(currentReconnectDelay * 2, maxReconnectDelay) + } + + if retryCount >= maxRetryAttempts { + print("[\(serverId)] 已达到最大重试次数,停止重连") + } + } + + /// 处理连接错误 + private func handleConnectionError(_ error: Error) async { + print("[\(serverId)] 连接错误: \(error.localizedDescription)") + isConnectedFlag = false + + // 如果是MCP特定错误,可以进行特殊处理 + if let mcpError = error as? MCPError { + // 处理MCP特定错误 + print("[\(serverId)] MCP错误类型: \(mcpError)") + } + + // 如果还有重试机会,启动重连 + // if retryCount < maxRetryAttempts { + // startReconnection() + // } + } + + /// 获取连接状态 + public func getConnectionStatus() -> Bool { + return isConnectedFlag + } } /// MCP客户端 @@ -285,6 +411,25 @@ public class MCPClient { } } + // 添加连接成功后的工具列表汇总输出 + if connectedCount > 0 { + print("[MCPClient] 🎉 MCP 连接完成,共连接 \(connectedCount) 个服务器") + print("[MCPClient] 📋 所有可用工具汇总:") + + let allTools = getToolMaps() + for (index, toolMap) in allTools.enumerated() { + if let function = toolMap["function"] as? [String: Any], + let name = function["name"] as? String, + let description = function["description"] as? String { + print("[MCPClient] \(index + 1). \(name) - \(description)") + } + } + + print("[MCPClient] 📊 工具总数: \(allTools.count)") + print("[MCPClient] 🔧 本地函数: \(localFunctionDefs.count) 个") + print("[MCPClient] 🌐 远程工具: \(allTools.count - localFunctionDefs.count) 个") + } + return connectedCount > 0 } @@ -298,6 +443,7 @@ public class MCPClient { parameters: Any, handler: FunctionHandler ) -> Bool { + print("[MCPClient] 注册本地函数: \(name)") localFunctions[name] = handler let functionDef: [String: Any] = [ @@ -307,6 +453,9 @@ public class MCPClient { ] localFunctionDefs[name] = functionDef + print("[MCPClient] 本地函数 \(name) 注册成功") + print("[MCPClient] 当前本地函数列表: \(Array(localFunctions.keys))") + return true } @@ -329,43 +478,69 @@ public class MCPClient { } for client in subClients.values { - allToolMaps.append(contentsOf: client.getToolMaps()) + let tools = client.getToolMaps() + print("[MCPClient] 读取工具列表: \(tools)") + allToolMaps.append(contentsOf: tools) } return allToolMaps } public func getToolType(name: String) -> ToolType? { + print("[MCPClient] 检查工具类型: \(name)") + print("[MCPClient] 本地函数列表: \(Array(localFunctions.keys))") + if localFunctions.keys.contains(name) { + print("[MCPClient] \(name) 是本地函数") return .localFunction } if subClients.values.contains(where: { $0.containsTool(name: name) }) { + print("[MCPClient] \(name) 是 MCP 工具") return .mcpTool } + print("[MCPClient] \(name) 未找到") return nil } public func callTool(name: String, arguments: [String: Any]) async -> [String: Any]? { + print("[MCPClient] 开始调用工具: \(name)") + print("[MCPClient] 参数: \(arguments)") + print("[MCPClient] 本地函数列表: \(Array(localFunctions.keys))") + print("[MCPClient] 子客户端数量: \(subClients.count)") + + // 检查本地函数 if let handler = localFunctions[name] { + print("[MCPClient] 找到本地函数: \(name),开始执行") do { let result = try await handler.handle(arguments: arguments) + print("[MCPClient] 本地函数 \(name) 执行成功") return ["context": result] } catch { + print("[MCPClient] 本地函数 \(name) 执行失败: \(error.localizedDescription)") return [ "content": [["type": "text", "text": "Function call failed: \(error.localizedDescription)"]], "isError": true ] } + } else { + print("[MCPClient] 未找到本地函数: \(name)") } - for client in subClients.values { + // 检查MCP工具 + print("[MCPClient] 开始检查 MCP 工具") + for (serverId, client) in subClients { + print("[MCPClient] 检查服务器 \(serverId) 是否包含工具 \(name)") if client.containsTool(name: name) { - return await client.callTool(name: name, arguments: arguments) + print("[MCPClient] 在服务器 \(serverId) 中找到工具 \(name),开始远程调用") + let result = await client.callTool(name: name, arguments: arguments) + print("[MCPClient] 远程工具 \(name) 调用完成") + return result } } + print("[MCPClient] 工具 \(name) 未找到") return [ "content": [["type": "text", "text": "Tool not found: \(name)"]], "isError": true @@ -386,7 +561,7 @@ public class MCPClient { } public func isConnected() -> Bool { - return !subClients.isEmpty + return !subClients.isEmpty && subClients.values.allSatisfy { $0.getConnectionStatus() } } public func disconnectAll() async { @@ -395,4 +570,40 @@ public class MCPClient { } subClients.removeAll() } -} \ No newline at end of file +} + +// 扩展NSLock以支持async/await +extension NSLock { + func withLock(_ body: () async throws -> T) async rethrows -> T { + lock() + defer { unlock() } + return try await body() + } +} + +// 添加超时支持 +func withTimeout(seconds: TimeInterval, operation: @escaping () async throws -> T) async throws -> T { + return try await withThrowingTaskGroup(of: T.self) { group in + group.addTask { + try await operation() + } + + group.addTask { + try await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000)) + throw TimeoutError() + } + + guard let result = try await group.next() else { + throw TimeoutError() + } + + group.cancelAll() + return result + } +} + +struct TimeoutError: Swift.Error { + var localizedDescription: String { + return "操作超时" + } +} diff --git a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift index 41c3df391..6a553d692 100644 --- a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift +++ b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift @@ -312,58 +312,186 @@ public class SystemFunctionHandler: NSObject { // MARK: - 功能处理器实现 /// 获取当前位置处理器 +// 移除这个重复的函数定义(第454-470行) +// private func withTimeout(seconds: TimeInterval, operation: @escaping () async throws -> T) async throws -> T { +// ... +// } + +// 在 GetCurrentLocationHandler 中修改超时处理 private class GetCurrentLocationHandler: FunctionHandler { func handle(arguments: [String: Any]) async throws -> String { - // 检查位置权限 - guard LocationService.shared.hasLocationPermission() else { - let errorResult: [String: Any] = [ - "success": false, - "message": "位置权限被拒绝。请前往 设置 > 隐私与安全 > 定位服务 > Deep Voice 启用定位权限" - ] - let jsonData = try JSONSerialization.data(withJSONObject: errorResult) - return String(data: jsonData, encoding: .utf8) ?? "{\"error\": \"JSON序列化失败\"}" - } + print("[GetCurrentLocationHandler] 开始处理位置请求") - // 先尝试获取缓存的位置数据 - if let cachedLocation = LocationService.shared.getCachedLocationData() { - let message = "当前位置:\(cachedLocation.address ?? "未知地址")(纬度 \(cachedLocation.latitude),经度 \(cachedLocation.longitude))" + return await withCheckedContinuation { continuation in + // 设置超时 + let timeoutTask = Task { + try await Task.sleep(nanoseconds: 15_000_000_000) // 15秒 + continuation.resume(returning: self.createErrorResponse(message: "位置获取超时,请检查网络连接后重试")) + } - // 异步触发最新定位(不等待结果) - Task { - do { - _ = try await LocationService.shared.getCurrentLocation() - } catch { - print("后台更新位置失败: \(error)") + LocationService.shared.getCurrentLocation { result in + timeoutTask.cancel() + + switch result { + case .success(let locationInfo): + print("[GetCurrentLocationHandler] 位置获取成功") + + // 解析位置数据 + guard let latStr = locationInfo["user_lat"] as? String, + let lngStr = locationInfo["user_lng"] as? String, + let latitude = Double(latStr), + let longitude = Double(lngStr), + let address = locationInfo["user_location"] as? String else { + print("[GetCurrentLocationHandler] 位置数据格式错误") + continuation.resume(returning: self.createErrorResponse(message: "位置数据格式错误,请重试")) + return + } + + print("[GetCurrentLocationHandler] 位置获取成功: lat=\(latitude), lng=\(longitude)") + + let message = "当前位置:\(address)(纬度 \(latitude),经度 \(longitude))" + let successResponse = self.createSuccessResponse(message: message, latitude: latitude, longitude: longitude, address: address) + + print("[GetCurrentLocationHandler] 返回位置结果") + continuation.resume(returning: successResponse) + + case .failure(let error): + print("[GetCurrentLocationHandler] 位置获取失败: \(error.localizedDescription)") + let errorMessage = self.getUserFriendlyErrorMessage(error) + continuation.resume(returning: self.createErrorResponse(message: errorMessage)) } } - - let result: [String: Any] = [ - "success": true, - "message": message - ] - let jsonData = try JSONSerialization.data(withJSONObject: result) - return String(data: jsonData, encoding: .utf8) ?? "{\"error\": \"JSON序列化失败\"}" } + } + + private func createSuccessResponse(message: String, latitude: Double, longitude: Double, address: String) -> String { + let result: [String: Any] = [ + "success": true, + "message": message, + "data": [ + "latitude": latitude, + "longitude": longitude, + "address": address, + "timestamp": Date().timeIntervalSince1970 * 1000 + ] + ] - // 如果没有缓存数据,同步获取位置 do { - let locationData = try await LocationService.shared.getCurrentLocation() - let message = "当前位置:\(locationData.address ?? "未知地址")(纬度 \(locationData.latitude),经度 \(locationData.longitude))" - - let result: [String: Any] = [ - "success": true, - "message": message - ] let jsonData = try JSONSerialization.data(withJSONObject: result) return String(data: jsonData, encoding: .utf8) ?? "{\"error\": \"JSON序列化失败\"}" } catch { - let errorResult: [String: Any] = [ - "success": false, - "message": "获取位置失败:\(error.localizedDescription)" - ] + return "{\"error\": \"JSON序列化失败\"}" + } + } + + private func createErrorResponse(message: String) -> String { + let errorResult: [String: Any] = [ + "success": false, + "message": message + ] + + do { let jsonData = try JSONSerialization.data(withJSONObject: errorResult) return String(data: jsonData, encoding: .utf8) ?? "{\"error\": \"JSON序列化失败\"}" + } catch { + return "{\"error\": \"JSON序列化失败\"}" + } + } + + private func getUserFriendlyErrorMessage(_ error: Error) -> String { + let errorDescription = error.localizedDescription.lowercased() + + if errorDescription.contains("permission") || errorDescription.contains("权限") { + return "需要位置权限才能获取当前位置,请在设置中开启位置权限" + } else if errorDescription.contains("network") || errorDescription.contains("网络") { + return "网络连接异常,请检查网络后重试" + } else if errorDescription.contains("service") || errorDescription.contains("服务") { + return "位置服务暂时不可用,请稍后重试" + } else { + return "获取位置失败:\(error.localizedDescription)" + } + } +} + +// MARK: - 支持结构体 + +private struct LocationResult: Codable { + let success: Bool + let message: String + let data: LocationResultData? + let error: LocationErrorInfo? + let executionTime: TimeInterval + let timestamp: Double + + init(success: Bool, message: String, data: LocationResultData? = nil, error: LocationErrorInfo? = nil, executionTime: TimeInterval) { + self.success = success + self.message = message + self.data = data + self.error = error + self.executionTime = executionTime + self.timestamp = Date().timeIntervalSince1970 * 1000 + } + + func toJSONString() throws -> String { + let encoder = JSONEncoder() + encoder.outputFormatting = .prettyPrinted + let jsonData = try encoder.encode(self) + return String(data: jsonData, encoding: .utf8) ?? "{\"error\": \"JSON序列化失败\"}" + } +} + +private struct LocationResultData: Codable { + let latitude: Double + let longitude: Double + let address: String? + let timestamp: Double + let executionTime: TimeInterval +} + +private struct LocationErrorInfo: Codable { + let code: String + let type: String + let description: String +} + + +// MARK: - 自定义错误类型 + +private enum LocationError: Error { + case timeout + case permissionDenied + case serviceUnavailable + case invalidData + case networkError + + var code: String { + switch self { + case .timeout: return "TIMEOUT" + case .permissionDenied: return "PERMISSION_DENIED" + case .serviceUnavailable: return "SERVICE_UNAVAILABLE" + case .invalidData: return "INVALID_DATA" + case .networkError: return "NETWORK_ERROR" + } + } + + var type: String { + switch self { + case .timeout: return "timeout" + case .permissionDenied: return "permission" + case .serviceUnavailable: return "service" + case .invalidData: return "data" + case .networkError: return "network" + } + } + + var userFriendlyMessage: String { + switch self { + case .timeout: return "位置获取超时,请检查网络连接后重试" + case .permissionDenied: return "需要位置权限才能获取当前位置,请在设置中开启位置权限" + case .serviceUnavailable: return "位置服务暂时不可用,请稍后重试" + case .invalidData: return "位置数据格式错误,请重试" + case .networkError: return "网络连接异常,请检查网络后重试" } } } @@ -587,3 +715,8 @@ private class OpenRecorderHandler: FunctionHandler { return "{\"result\": \"无法打开录音机应用\"}" } } + +// 同时移除 LocationError 枚举定义(第472-510行),因为它没有被使用 +// private enum LocationError: Error { +// ... +// } 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 04c991abc..2312b1fc4 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 @@ -9,349 +9,368 @@ public extension Notification.Name { public class LocationService: NSObject { private static let TAG = "LocationService" - private static let CACHE_PREFS_NAME = "location_cache" - private static let PERMISSION_REQUEST_CODE = 1001 public static let shared = LocationService() private var locationManager: CLLocationManager? - private var initialized = false - private var permissionRetryTimer: Timer? - - // 存储当前的定位请求回调(Flutter用) + private var cachedLocationData: LocationData? private var currentLocationCallback: ((Result<[String: Any], Error>) -> Void)? - // 存储权限请求回调(Flutter用) - private var permissionRequestCallback: ((Result<[String: Bool], Error>) -> Void)? - // 存储服务间调用的回调 - private var serviceCallbacks: [LocationCallback] = [] + // 缓存有效期(5分钟) + private let cacheValidityDuration: TimeInterval = 5 * 60 - // 缓存的位置数据 - private var cachedLocationData: LocationData? + // 权限状态缓存 + private var authorizationStatus: CLAuthorizationStatus = .notDetermined + private var isLocationServicesEnabled: Bool = false private override init() { super.init() - } - - /// 检查应用是否在前台运行 - public func isAppInForeground() -> Bool { - return UIApplication.shared.applicationState == .active - } - - /// 初始化定位服务 - public func initialize() { - if initialized { return } - - if !checkPermissions() { - print("\(LocationService.TAG): 没有定位权限,等待用户授权中...") - startPermissionMonitor() - return - } - setupLocationManager() - initialized = true - print("\(LocationService.TAG): 定位服务初始化完成") } private func setupLocationManager() { - locationManager = CLLocationManager() - locationManager?.delegate = self - locationManager?.desiredAccuracy = kCLLocationAccuracyBest - locationManager?.distanceFilter = 10 - } - - private func startPermissionMonitor() { - permissionRetryTimer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { [weak self] _ in - guard let self = self else { return } - let hasPermission = self.checkPermissions() - if hasPermission && !self.initialized { - print("\(LocationService.TAG): 权限已授予,重新初始化定位服务") - self.initialize() - self.stopPermissionMonitor() - } + DispatchQueue.main.async { [weak self] in + self?.locationManager = CLLocationManager() + self?.locationManager?.delegate = self + self?.locationManager?.desiredAccuracy = kCLLocationAccuracyBest + self?.locationManager?.distanceFilter = 10 + + // 初始化时获取当前状态 + self?.authorizationStatus = CLLocationManager.authorizationStatus() + self?.isLocationServicesEnabled = CLLocationManager.locationServicesEnabled() + + print("[LocationService] LocationManager 初始化完成") + print("[LocationService] 初始权限状态: \(self?.authorizationStatus.rawValue ?? -1)") + print("[LocationService] 位置服务启用: \(self?.isLocationServicesEnabled ?? false)") } } - private func stopPermissionMonitor() { - permissionRetryTimer?.invalidate() - permissionRetryTimer = nil - } - - /// 检查定位权限 - private func checkPermissions() -> Bool { - return hasForegroundLocationPermission() - } - - private func hasForegroundLocationPermission() -> Bool { - let status = CLLocationManager.authorizationStatus() - return status == .authorizedWhenInUse || status == .authorizedAlways - } - - private func hasBackgroundLocationPermission() -> Bool { - return CLLocationManager.authorizationStatus() == .authorizedAlways - } - - /// 检查定位权限状态(Flutter用) - public func checkLocationPermission(completion: @escaping (Result<[String: Bool], Error>) -> Void) { - let hasPermission = checkPermissions() - let result: [String: Bool] = [ - "hasPermission": hasPermission, - "hasForegroundPermission": hasForegroundLocationPermission(), - "hasBackgroundPermission": hasBackgroundLocationPermission() - ] - completion(.success(result)) - } - - /// 请求定位权限(Flutter用) - public func requestLocationPermission(completion: @escaping (Result<[String: Bool], Error>) -> Void) { - if checkPermissions() { - completion(.success(["granted": true])) + /// 获取当前位置 - 主要接口 + public func getCurrentLocation(completion: @escaping (Result<[String: Any], Error>) -> Void) { + print("[LocationService] 开始获取当前位置") + + // 1. 检查缓存数据是否存在且有效 + if let cachedData = getCachedLocationIfValid() { + print("[LocationService] 返回缓存位置数据") + let locationInfo = formatLocationData(cachedData) + completion(.success(locationInfo)) + + // 异步更新缓存 + print("[LocationService] 异步更新位置缓存") + asyncUpdateLocation() return } - permissionRequestCallback = completion - locationManager?.requestWhenInUseAuthorization() + // 2. 缓存不存在或已过期,检查权限并同步获取位置 + print("[LocationService] 缓存无效,开始同步获取位置") + syncGetLocation(completion: completion) } - /// 按需获取当前位置(Flutter用) - public func getCurrentLocation(completion: @escaping (Result<[String: Any], Error>) -> Void) { - if !checkPermissions() { - completion(.failure(NSError(domain: "LocationService", code: -2, userInfo: [NSLocalizedDescriptionKey: "需要定位权限"]))) - return + /// 检查缓存是否有效 + private func getCachedLocationIfValid() -> LocationData? { + guard let cached = cachedLocationData else { + print("[LocationService] 无缓存数据") + return nil } - if !initialized { - completion(.failure(NSError(domain: "LocationService", code: -3, userInfo: [NSLocalizedDescriptionKey: "定位服务未初始化"]))) - return - } + let currentTime = Date().timeIntervalSince1970 * 1000 + let cacheAge = currentTime - cached.timestamp - if currentLocationCallback != nil { - completion(.failure(NSError(domain: "LocationService", code: -4, userInfo: [NSLocalizedDescriptionKey: "正在定位中,请稍后再试"]))) - return + if cacheAge < cacheValidityDuration * 1000 { + print("[LocationService] 缓存有效,年龄: \(cacheAge/1000)秒") + return cached + } else { + print("[LocationService] 缓存已过期,年龄: \(cacheAge/1000)秒") + return nil } - - // 保存回调,开始定位 - currentLocationCallback = completion - locationManager?.requestLocation() } - // MARK: - 供其他iOS服务使用的接口 - - /// 检查是否有定位权限(供其他服务使用) - public func hasLocationPermission() -> Bool { - return checkPermissions() + /// 异步更新位置缓存 + private func asyncUpdateLocation() { + // 检查权限 + guard checkLocationPermission() else { + print("[LocationService] 异步更新失败:无位置权限") + return + } + + // 异步请求位置更新 + DispatchQueue.global(qos: .background).async { [weak self] in + self?.requestLocationUpdate { result in + switch result { + case .success(let locationData): + print("[LocationService] 异步更新位置缓存成功") + self?.cachedLocationData = locationData + case .failure(let error): + print("[LocationService] 异步更新位置缓存失败: \(error.localizedDescription)") + } + } + } } - /// 异步获取当前位置(供其他iOS服务使用) - public func getCurrentLocation() async throws -> LocationData { - return try await withCheckedThrowingContinuation { continuation in - getCurrentLocation { result in + /// 同步获取位置 + private func syncGetLocation(completion: @escaping (Result<[String: Any], Error>) -> Void) { + // 检查权限 + guard checkLocationPermission() else { + print("[LocationService] 无位置权限,尝试请求权限") + requestLocationPermission { [weak self] result in switch result { - case .success(let locationInfo): - // 从返回的字典中提取位置信息 - if let latStr = locationInfo["user_lat"] as? String, - let lngStr = locationInfo["user_lng"] as? String, - let latitude = Double(latStr), - let longitude = Double(lngStr), - let address = locationInfo["user_location"] as? String { - - let locationData = LocationData( - latitude: latitude, - longitude: longitude, - address: address, - timestamp: Date().timeIntervalSince1970 * 1000 - ) - continuation.resume(returning: locationData) + case .success(let granted): + if granted { + print("[LocationService] 权限获取成功,重新获取位置") + self?.syncGetLocation(completion: completion) } else { - continuation.resume(throwing: NSError(domain: "LocationService", code: -5, userInfo: [NSLocalizedDescriptionKey: "位置数据格式错误"])) + completion(.failure(NSError(domain: "LocationService", code: -1, userInfo: [NSLocalizedDescriptionKey: "位置权限被拒绝"]))) } case .failure(let error): - continuation.resume(throwing: error) + completion(.failure(error)) } } - } - } - - /// 获取当前位置(供其他iOS服务使用) - public func requestLocation(callback: LocationCallback) { - if !checkPermissions() { - callback.onLocationError(errorCode: -2, errorMessage: "没有定位权限") - return - } - - if !initialized { - callback.onLocationError(errorCode: -3, errorMessage: "定位服务未初始化") return } - // 添加到回调列表 - serviceCallbacks.append(callback) - - // 如果没有正在进行的定位请求,开始定位 - if currentLocationCallback == nil && serviceCallbacks.count == 1 { - locationManager?.requestLocation() + // 有权限,直接获取位置 + requestLocationUpdate { [weak self] result in + switch result { + case .success(let locationData): + print("[LocationService] 同步获取位置成功") + self?.cachedLocationData = locationData + let locationInfo = self?.formatLocationData(locationData) ?? [:] + completion(.success(locationInfo)) + case .failure(let error): + print("[LocationService] 同步获取位置失败: \(error.localizedDescription)") + completion(.failure(error)) + } } } - /// 同步获取缓存的位置信息(供其他服务使用) - /// 注意:这个方法返回的可能是过期的位置信息 - public func getCachedLocationData() -> LocationData? { - // 优先返回内存中的缓存 - if let cached = cachedLocationData { - return cached - } - - // 从UserDefaults中读取缓存 - return loadCachedLocationFromUserDefaults() + /// 检查位置权限 - 使用缓存的状态 + private func checkLocationPermission() -> Bool { + return authorizationStatus == .authorizedWhenInUse || authorizationStatus == .authorizedAlways } - private func saveCachedLocationToUserDefaults(_ locationData: LocationData) { - do { - let encoder = JSONEncoder() - let data = try encoder.encode(locationData) - UserDefaults.standard.set(data, forKey: "latitude") - UserDefaults.standard.set(String(locationData.latitude), forKey: "latitude") - UserDefaults.standard.set(String(locationData.longitude), forKey: "longitude") - UserDefaults.standard.set(locationData.address, forKey: "address") - UserDefaults.standard.set(Double(locationData.timestamp), forKey: "timestamp") - UserDefaults.standard.synchronize() - } catch { - print("\(LocationService.TAG): 保存位置缓存失败: \(error.localizedDescription)") + /// 请求位置权限 - 异步处理 + private func requestLocationPermission(completion: @escaping (Result) -> Void) { + print("[LocationService] 当前缓存的权限状态: \(authorizationStatus.rawValue)") + + switch authorizationStatus { + case .notDetermined: + print("[LocationService] 请求位置权限") + // 保存回调,等待权限结果 + permissionCallback = completion + DispatchQueue.main.async { [weak self] in + self?.locationManager?.requestWhenInUseAuthorization() + } + case .denied, .restricted: + print("[LocationService] 位置权限被拒绝或受限") + completion(.success(false)) + case .authorizedWhenInUse, .authorizedAlways: + print("[LocationService] 已有位置权限") + completion(.success(true)) + @unknown default: + completion(.failure(NSError(domain: "LocationService", code: -2, userInfo: [NSLocalizedDescriptionKey: "未知权限状态"]))) } } - private func loadCachedLocationFromUserDefaults() -> LocationData? { - do { - guard let latitudeStr = UserDefaults.standard.string(forKey: "latitude"), - let longitudeStr = UserDefaults.standard.string(forKey: "longitude"), - let latitude = Double(latitudeStr), - let longitude = Double(longitudeStr) else { - return nil + /// 请求位置更新 - 优化版本 + private func requestLocationUpdate(completion: @escaping (Result) -> Void) { + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + + guard let locationManager = self.locationManager else { + print("[LocationService] 错误:LocationManager 未初始化") + completion(.failure(NSError(domain: "LocationService", code: -3, userInfo: [NSLocalizedDescriptionKey: "LocationManager 未初始化"]))) + return } - let address = UserDefaults.standard.string(forKey: "address") ?? "未知地址" - let timestampDouble = UserDefaults.standard.double(forKey: "timestamp") + // 检查是否已有请求在进行 + if self.currentLocationCallback != nil { + print("[LocationService] 错误:位置请求正在进行中,拒绝新请求") + completion(.failure(NSError(domain: "LocationService", code: -4, userInfo: [NSLocalizedDescriptionKey: "位置请求正在进行中"]))) + return + } - if timestampDouble == 0 { return nil } + // 使用缓存的状态信息 + print("[LocationService] 开始位置请求") + print("[LocationService] 缓存的权限状态: \(self.authorizationStatus.rawValue)") + print("[LocationService] 缓存的位置服务状态: \(self.isLocationServicesEnabled)") + print("[LocationService] 当前线程: \(Thread.current)") + print("[LocationService] 是否主线程: \(Thread.isMainThread)") - return LocationData( - latitude: latitude, - longitude: longitude, - address: address, - timestamp: TimeInterval(timestampDouble) - ) - } catch { - print("\(LocationService.TAG): 读取位置缓存失败: \(error.localizedDescription)") - return nil - } - } - - /// 停止定位服务 - public func stop() { - locationManager?.stopUpdatingLocation() - locationManager = nil - currentLocationCallback = nil - permissionRequestCallback = nil - serviceCallbacks.removeAll() - initialized = false - stopPermissionMonitor() - } - - private func handleLocationResult(_ location: CLLocation?, error: Error?) { - // 处理Flutter回调 - let flutterCallback = currentLocationCallback - currentLocationCallback = nil - - if let location = location, error == nil { - print("\(LocationService.TAG): 定位成功: \(location.coordinate.latitude), \(location.coordinate.longitude)") + // 验证权限状态 + if !self.checkLocationPermission() { + print("[LocationService] 错误:无位置权限") + completion(.failure(NSError(domain: "LocationService", code: -5, userInfo: [NSLocalizedDescriptionKey: "无位置权限"]))) + return + } - // 进行逆地理编码获取地址 - let geocoder = CLGeocoder() - geocoder.reverseGeocodeLocation(location) { [weak self] placemarks, geocodeError in - guard let self = self else { return } - - let address = placemarks?.first?.name ?? "未知地址" - - // 缓存位置数据 - let locationData = LocationData( - latitude: location.coordinate.latitude, - longitude: location.coordinate.longitude, - address: address, - timestamp: TimeInterval(Date().timeIntervalSince1970) * 1000 - ) - self.cachedLocationData = locationData - self.saveCachedLocationToUserDefaults(locationData) - - // 通知Flutter - if let callback = flutterCallback { - let result: [String: Any] = [ - "user_location": address, - "user_lat": String(location.coordinate.latitude), - "user_lng": String(location.coordinate.longitude) - ] - callback(.success(result)) - } - - // 通知其他iOS服务 - self.serviceCallbacks.forEach { callback in - callback.onLocationSuccess( - latitude: location.coordinate.latitude, - longitude: location.coordinate.longitude, - address: address - ) - } - - // 清空服务回调列表(单次定位) - self.serviceCallbacks.removeAll() + if !self.isLocationServicesEnabled { + print("[LocationService] 错误:位置服务未启用") + completion(.failure(NSError(domain: "LocationService", code: -6, userInfo: [NSLocalizedDescriptionKey: "位置服务未启用"]))) + return } - } else { - let errorMessage = error?.localizedDescription ?? "未知错误" - print("\(LocationService.TAG): 定位失败: \(errorMessage)") - // 通知Flutter - if let callback = flutterCallback { - callback(.failure(NSError(domain: "LocationService", code: -1, userInfo: [NSLocalizedDescriptionKey: "定位失败: \(errorMessage)"]))) + // 强制重新设置 delegate + if locationManager.delegate == nil { + print("[LocationService] 警告:Delegate 为空,重新设置") + locationManager.delegate = self } - // 通知其他iOS服务 - serviceCallbacks.forEach { callback in - callback.onLocationError(errorCode: -1, errorMessage: errorMessage) + // 验证 delegate + if locationManager.delegate === self { + print("[LocationService] Delegate 验证成功") + } else { + print("[LocationService] 错误:Delegate 设置失败") + completion(.failure(NSError(domain: "LocationService", code: -8, userInfo: [NSLocalizedDescriptionKey: "Delegate 设置失败"]))) + return } - // 清空服务回调列表 - serviceCallbacks.removeAll() + // 设置回调 + self.currentLocationCallback = { result in + switch result { + case .success(let locationInfo): + print("[LocationService] 位置回调成功,开始解析数据") + if let latStr = locationInfo["user_lat"] as? String, + let lngStr = locationInfo["user_lng"] as? String, + let latitude = Double(latStr), + let longitude = Double(lngStr) { + + print("[LocationService] 位置数据解析成功: lat=\(latitude), lng=\(longitude)") + let locationData = LocationData( + latitude: latitude, + longitude: longitude, + address: "", + timestamp: Date().timeIntervalSince1970 * 1000 + ) + completion(.success(locationData)) + } else { + print("[LocationService] 错误:位置数据格式错误,原始数据: \(locationInfo)") + completion(.failure(NSError(domain: "LocationService", code: -7, userInfo: [NSLocalizedDescriptionKey: "位置数据格式错误"]))) + } + case .failure(let error): + print("[LocationService] 位置回调失败: \(error.localizedDescription)") + completion(.failure(error)) + } + } + + print("[LocationService] 调用 locationManager.requestLocation()") + locationManager.requestLocation() + print("[LocationService] requestLocation() 调用完成,等待回调") + + // 30秒超时机制 + DispatchQueue.main.asyncAfter(deadline: .now() + 30) { + if self.currentLocationCallback != nil { + print("[LocationService] 位置请求超时") + let callback = self.currentLocationCallback + self.currentLocationCallback = nil + callback?(.failure(NSError(domain: "LocationService", code: -9, userInfo: [NSLocalizedDescriptionKey: "位置请求超时"]))) + } + } } } + + /// 格式化位置数据 + private func formatLocationData(_ locationData: LocationData) -> [String: Any] { + return [ + "user_lat": String(locationData.latitude), + "user_lng": String(locationData.longitude), + "user_location": locationData.address, + "timestamp": locationData.timestamp + ] + } + + // MARK: - 权限回调 + private var permissionCallback: ((Result) -> Void)? } // MARK: - CLLocationManagerDelegate extension LocationService: CLLocationManagerDelegate { + // 替换 didUpdateLocations 方法中的逆地理编码部分 public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { - guard let location = locations.last else { return } - handleLocationResult(location, error: nil) + print("[LocationService] didUpdateLocations 被调用,位置数量: \(locations.count)") + + guard let location = locations.last else { + print("[LocationService] 错误:locations 数组为空") + return + } + + print("[LocationService] 位置更新成功: lat=\(location.coordinate.latitude), lng=\(location.coordinate.longitude)") + print("[LocationService] 位置精度: \(location.horizontalAccuracy)m") + + // 直接使用经纬度,不进行逆地理编码 + let locationInfo: [String: Any] = [ + "user_lat": String(location.coordinate.latitude), + "user_lng": String(location.coordinate.longitude), + "timestamp": Date().timeIntervalSince1970 * 1000 + ] + + print("[LocationService] 准备调用位置回调") + // 调用回调 + let callback = currentLocationCallback + currentLocationCallback = nil + callback?(.success(locationInfo)) + print("[LocationService] 位置回调已调用") } public func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { - handleLocationResult(nil, error: error) + print("[LocationService] didFailWithError 被调用") + print("[LocationService] 位置获取失败: \(error.localizedDescription)") + print("[LocationService] 错误代码: \((error as NSError).code)") + print("[LocationService] 错误域: \((error as NSError).domain)") + + let callback = currentLocationCallback + currentLocationCallback = nil + print("[LocationService] 准备调用错误回调") + callback?(.failure(error)) + print("[LocationService] 错误回调已调用") } public func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { - print("\(LocationService.TAG): 定位权限状态变更: \(status.rawValue)") + print("[LocationService] didChangeAuthorization 被调用") + print("[LocationService] 权限状态变更: \(status.rawValue) (\(authorizationStatusString(status)))") + + // 更新缓存的状态 + authorizationStatus = status + isLocationServicesEnabled = CLLocationManager.locationServicesEnabled() + + print("[LocationService] 更新缓存状态 - 权限: \(authorizationStatus.rawValue), 服务启用: \(isLocationServicesEnabled)") + + guard let callback = permissionCallback else { + print("[LocationService] 无权限回调,忽略权限变更") + return + } switch status { case .authorizedWhenInUse, .authorizedAlways: - if !initialized { - initialize() - } - permissionRequestCallback?(.success(["granted": true])) - permissionRequestCallback = nil + print("[LocationService] 权限获取成功") + permissionCallback = nil + callback(.success(true)) case .denied, .restricted: - permissionRequestCallback?(.success(["granted": false])) - permissionRequestCallback = nil + print("[LocationService] 权限被拒绝或受限") + permissionCallback = nil + callback(.success(false)) case .notDetermined: - break + print("[LocationService] 权限状态仍未确定,继续等待") + // 继续等待用户选择 + permissionCallback = callback @unknown default: - break + print("[LocationService] 未知权限状态") + permissionCallback = nil + callback(.failure(NSError(domain: "LocationService", code: -10, userInfo: [NSLocalizedDescriptionKey: "未知权限状态"]))) + } + } + + // 辅助方法:将权限状态转换为可读字符串 + private func authorizationStatusString(_ status: CLAuthorizationStatus) -> String { + switch status { + case .notDetermined: return "notDetermined" + case .restricted: return "restricted" + case .denied: return "denied" + case .authorizedAlways: return "authorizedAlways" + case .authorizedWhenInUse: return "authorizedWhenInUse" + @unknown default: return "unknown" } } } + +// Remove the duplicate LocationData definition from here +// The LocationData struct is already defined in LocationData.swift diff --git a/local_plugins/location_service/ios/location_service/Sources/location_service/LocationServicePlugin.swift b/local_plugins/location_service/ios/location_service/Sources/location_service/LocationServicePlugin.swift index 80b383126..ba8222b63 100644 --- a/local_plugins/location_service/ios/location_service/Sources/location_service/LocationServicePlugin.swift +++ b/local_plugins/location_service/ios/location_service/Sources/location_service/LocationServicePlugin.swift @@ -13,16 +13,10 @@ public class LocationServicePlugin: NSObject, FlutterPlugin { registrar.addMethodCallDelegate(instance, channel: channel) self.channel = channel - instance.addStartLocationMonitor() } public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { switch call.method { - case "location": - result(nil) - case "initialize": - LocationService.shared.initialize() - result(nil) case "getCurrentLocation": LocationService.shared.getCurrentLocation { locationResult in switch locationResult { @@ -32,41 +26,9 @@ public class LocationServicePlugin: NSObject, FlutterPlugin { result(FlutterError(code: "LOCATION_ERROR", message: error.localizedDescription, details: nil)) } } - case "checkPermission": - LocationService.shared.checkLocationPermission { permissionResult in - switch permissionResult { - case .success(let permissions): - result(permissions) - case .failure(let error): - result(FlutterError(code: "PERMISSION_ERROR", message: error.localizedDescription, details: nil)) - } - } - case "requestPermission": - LocationService.shared.requestLocationPermission { permissionResult in - switch permissionResult { - case .success(let permissions): - result(permissions) - case .failure(let error): - result(FlutterError(code: "PERMISSION_ERROR", message: error.localizedDescription, details: nil)) - } - } - case "stop": - LocationService.shared.stop() - result(nil) default: result(FlutterMethodNotImplemented) } } - - private func addStartLocationMonitor() { - NotificationCenter.default.addObserver( - forName: .locationStartEvent, - object: nil, - queue: .main - ) { notification in - print("📥 收到启动定位服务事件") - LocationService.shared.initialize() - } - } } -#endif \ No newline at end of file +#endif From e2b0a70e0b2e16e2c8bfb03266c2e4ace75d6e9e Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Fri, 25 Jul 2025 10:32:52 +0800 Subject: [PATCH 7/7] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E8=A7=A3=E7=BB=91?= =?UTF-8?q?=E6=B5=81=E7=A8=8B=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ios/Runner.xcodeproj/project.pbxproj | 8 ++++---- lib/data/services/network/api.dart | 4 ++-- lib/modules/devices/controllers/devices_controller.dart | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index b830d289d..c91c43c66 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 60; objects = { /* Begin PBXBuildFile section */ @@ -252,7 +252,7 @@ ); mainGroup = 97C146E51CF9000F007C117D; packageReferences = ( - 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */, + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, D013B7972DFA83E400B2B69B /* XCRemoteSwiftPackageReference "flutterfire" */, ); productRefGroup = 97C146EF1CF9000F007C117D /* Products */; @@ -721,7 +721,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = 537SFY28XS; + DEVELOPMENT_TEAM = K39FB49XKY; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "Deep Voice"; @@ -800,7 +800,7 @@ /* End XCConfigurationList section */ /* Begin XCLocalSwiftPackageReference section */ - 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = { + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = { isa = XCLocalSwiftPackageReference; relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; }; diff --git a/lib/data/services/network/api.dart b/lib/data/services/network/api.dart index 0301daa87..08733ee66 100644 --- a/lib/data/services/network/api.dart +++ b/lib/data/services/network/api.dart @@ -146,10 +146,10 @@ class Api { } //解绑定设备 - static unbindearphone([params]) { + static unbinddevice([params]) { return DioManager().request( NWMethod.post, - '/api/home/user_unbindearphone', + '/api/home/user_unbinddevice', params: params, ); } diff --git a/lib/modules/devices/controllers/devices_controller.dart b/lib/modules/devices/controllers/devices_controller.dart index 6121ed7f8..1a1fe15a8 100644 --- a/lib/modules/devices/controllers/devices_controller.dart +++ b/lib/modules/devices/controllers/devices_controller.dart @@ -35,7 +35,7 @@ class DevicesController extends GetxController { } void unbindearphone(UserDevice device) async { - final response = await Api.unbindearphone({ + final response = await Api.unbinddevice({ "id": device.id, }); if (response != null) {