diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index c91c43c66..8bf262c98 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 60; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index ca4a8bf61..aff31ef06 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -35,7 +35,7 @@ Editor CFBundleURLSchemes - wx5bbb7be9324f3e2d + wx4b5ec8957ad5e425 diff --git a/lib/data/services/ble_manager.dart b/lib/data/services/ble_manager.dart index b55927d5a..a793411f3 100644 --- a/lib/data/services/ble_manager.dart +++ b/lib/data/services/ble_manager.dart @@ -519,7 +519,11 @@ class BleManager extends GetxService { Future startAgentService() async { try { // 从.env文件直接读取环境变量 - final serverurl = dotenv.env['SERVER_URL'] ?? ''; + var serverurl = dotenv.env['SERVER_URL'] ?? ''; + if (Get.locale?.countryCode != 'CN') { + serverurl = dotenv.env['HW_SERVER_URL'] ?? ''; + } + var jsonString = _storage.read("logintoken"); Logger.info('启动BLE服务,serverurl: ${serverurl}'); // final azureSpeechRegion = dotenv.env['AZURE_SPEECH_REGION'] ?? 'eastasia'; @@ -549,6 +553,40 @@ class BleManager extends GetxService { // 从Flutter资源包中加载.mcp.json final jsonData = AppConfig.mcpConfig(); + // 根据APP语言动态配置语音识别语言 + List supportedLanguages = []; + try { + // 使用 GetX 获取 APP 当前语言环境 (Locale) + final appLocale = Get.locale; + + if (appLocale != null) { + final lang = appLocale.languageCode; // e.g., 'en' + final country = appLocale.countryCode; // e.g., 'US' + + // 构造 Azure SDK 需要的格式 (e.g., 'en-US') + if (country != null && country.isNotEmpty) { + supportedLanguages.add('$lang-$country'); + } else { + supportedLanguages.add(lang); // fallback only language code + } + } + + // 添加 en-US 作为备选 + if (!supportedLanguages.contains('en-US')) { + supportedLanguages.add('en-US'); + } + + // 最终兜底 + if (supportedLanguages.isEmpty) { + supportedLanguages.add('en-US'); + } + + Logger.info('动态配置语音识别语言: ${supportedLanguages.join(', ')}'); + } catch (e) { + Logger.error('获取APP语言配置失败,使用默认语言: $e'); + supportedLanguages = ['zh-CN', 'en-US']; + } + final config = { // 语音代理配置参数 'azureSpeechKey': azureSpeechKey, @@ -563,7 +601,7 @@ class BleManager extends GetxService { 'volcanoToken': 'mcJTh6OelBqU_L78lgGd77Nyrc_-5-En', 'serverurl': serverurl, 'usertoken': jsonString, - 'supportedLanguages': ['zh-CN'], + 'supportedLanguages': supportedLanguages, }; // 记录日志(不记录敏感信息) diff --git a/lib/data/services/network/dio_manager.dart b/lib/data/services/network/dio_manager.dart index abdb7850f..0496c11f9 100644 --- a/lib/data/services/network/dio_manager.dart +++ b/lib/data/services/network/dio_manager.dart @@ -1,5 +1,6 @@ import 'package:dio/dio.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:get/get.dart' hide Response; import 'auth_interceptor.dart'; import 'nw_method.dart'; @@ -9,7 +10,18 @@ class DioManager { factory DioManager() => _shared; late Dio dio; - final baseUrl = dotenv.env['SERVER_URL'] ?? ""; + // 根据地区获取服务器URL + String get baseUrl { + final countryCode = Get.deviceLocale?.countryCode?.toUpperCase(); + final isChinaMainland = countryCode == 'CN'; + + if (!isChinaMainland) { + return dotenv.env['HW_SERVER_URL'] ?? dotenv.env['SERVER_URL'] ?? ''; + } else { + return dotenv.env['SERVER_URL'] ?? ''; + } + } + DioManager._internal() { BaseOptions options = BaseOptions( baseUrl: baseUrl, diff --git a/lib/modules/agent/controllers/agent_controller.dart b/lib/modules/agent/controllers/agent_controller.dart index c29675888..dda9ed2c4 100644 --- a/lib/modules/agent/controllers/agent_controller.dart +++ b/lib/modules/agent/controllers/agent_controller.dart @@ -153,7 +153,7 @@ class AgentController extends GetxController { _setupMediaSessionCallbacks(); //设置媒体会话回调 _setupTextInputListener(); //设置文本输入监听器 - _configureSpeechRecognitionLanguages(); //配置语音识别语言 + startVoiceInput(); //开启语音输入 } @@ -1013,49 +1013,7 @@ class AgentController extends GetxController { } } -//根据APP语言配置语音识别语言 - Future _configureSpeechRecognitionLanguages() async { - try { - // 1. 使用 GetX 获取 APP 当前语言环境 (Locale) - final appLocale = Get.locale; - List languagesToSupport = []; - - if (appLocale != null) { - final lang = appLocale.languageCode; // e.g., 'en' - final country = appLocale.countryCode; // e.g., 'US' - - // 2. 构造 Azure SDK 需要的格式 (e.g., 'en-US') - if (country != null && country.isNotEmpty) { - languagesToSupport.add('$lang-$country'); - } else { - languagesToSupport.add(lang); // fallback only language code - } - } - // 3. 添加 en-US 作为备选 - if (!languagesToSupport.contains('en-US')) { - languagesToSupport.add('en-US'); - } - - // 4. 最终兜底 - if (languagesToSupport.isEmpty) { - languagesToSupport.add('en-US'); - } - - // 5. 调用原生设置 - Logger.i(TAG, '支持的语言: ${languagesToSupport.join(', ')}'); - AgentService.setSupportedLanguages(languagesToSupport).then((success) { - if (success) { - Logger.i(TAG, 'asr的语言已根据 APP 设置语言: ${languagesToSupport.join(', ')}'); - } else { - Logger.w(TAG, '根据 APP 设置asr语言失败'); - } - }); - } catch (e) { - Logger.e(TAG, "获取 APP 语言并设置失败: $e"); - AgentService.setSupportedLanguages(['zh-CN', 'en-US']); - } - } // 开始语音输入 Future startVoiceInput() async { diff --git a/lib/modules/devices/controllers/devices_controller.dart b/lib/modules/devices/controllers/devices_controller.dart index 1a1fe15a8..ba32f0c98 100644 --- a/lib/modules/devices/controllers/devices_controller.dart +++ b/lib/modules/devices/controllers/devices_controller.dart @@ -28,9 +28,8 @@ class DevicesController extends GetxController { for (var device in User.instance.devices) { if (!devices.containsKey(device.id)) { devices[device.devicetype] = []; - } else { - devices[device.devicetype]!.add(device); } + devices[device.devicetype]!.add(device); } } diff --git a/lib/modules/pairing/controllers/pairing_controller.dart b/lib/modules/pairing/controllers/pairing_controller.dart index eb488a73a..a8f0e8acc 100644 --- a/lib/modules/pairing/controllers/pairing_controller.dart +++ b/lib/modules/pairing/controllers/pairing_controller.dart @@ -613,8 +613,11 @@ class PairingController extends GetxController { final names = AppConfig.allproductNames(); // 检查是否已有绑定的设备 print("device.address${device.address}"); - final existingDevice = User.instance.devices.firstWhere( - (d) => d.devicetype == 1 && d.devicemac == device.address, + var existingDevice = User.instance.devices.firstWhere( + (d) => + d.devicetype == 1 && + d.devicename == device.name && + d.devicemac == device.address, orElse: () => UserDevice( id: -1, uid: '', @@ -627,7 +630,28 @@ class PairingController extends GetxController { // 已经绑定 return 9; } - + if (Platform.isAndroid) { + // 检查是否已有BLE绑定的设备 + existingDevice = User.instance.devices.firstWhere( + (d) => + d.devicetype == 2 && + d.devicename == device.name && + d.devicemac == device.address, + orElse: () => UserDevice( + id: -1, + uid: '', + devicetype: -1, + devicename: '', + devicemac: '', + license: ''), + ); + if (existingDevice.id != -1) { + _verifiedDeviceName = device.name; + _verifiedDeviceAddress = device.address; + // 已经绑定 + return 10; + } + } print( '获取到 deviceName: $deviceName address: ${device.address} names:$names'); if (names.contains(deviceName)) { diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt index 8229f4863..60b919843 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt @@ -74,6 +74,19 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream config["serverurl"]?.toString()?.let { intent.putExtra("serverurl", it) } config["usertoken"]?.toString()?.let { intent.putExtra("usertoken", it) } + // 添加 supportedLanguages 参数处理 + config["supportedLanguages"]?.let { languages -> + when (languages) { + is List<*> -> { + // 将 List 转换为 ArrayList 以便传递给 Intent + val languageList = languages.filterIsInstance() + if (languageList.isNotEmpty()) { + intent.putStringArrayListExtra("supportedLanguages", ArrayList(languageList)) + } + } + } + } + // 启动前台服务 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt index 93c016657..9eb03b1f3 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt @@ -51,6 +51,7 @@ class BleMediaService : Service(), CoroutineScope { private const val KEY_SERVER_URL = "serverurl" private const val KEY_USER_TOKEN = "usertoken" + private const val KEY_SUPPORTED_LANGUAGES = "supportedLanguages" // 用于跟踪服务是否正在运行 private val isRunning = AtomicBoolean(false) @@ -130,7 +131,10 @@ class BleMediaService : Service(), CoroutineScope { Log.d(TAG, "当前配置参数: $config") // 检查必要的配置是否存在 - if (config[KEY_AZURE_SPEECH_KEY].isNullOrEmpty() || config[KEY_OPENAI_API_KEY].isNullOrEmpty()) { + val azureSpeechKey = config[KEY_AZURE_SPEECH_KEY]?.toString() ?: "" + val openaiApiKey = config[KEY_OPENAI_API_KEY]?.toString() ?: "" + + if (azureSpeechKey.isEmpty() || openaiApiKey.isEmpty()) { Log.w(TAG, "语音代理初始化失败: API密钥未配置") return } @@ -159,8 +163,8 @@ class BleMediaService : Service(), CoroutineScope { /** * 从Intent中提取配置参数 */ - private fun extractConfigFromIntent(intent: Intent?): Map { - val config = mutableMapOf() + private fun extractConfigFromIntent(intent: Intent?): Map { + val config = mutableMapOf() intent?.let { // 提取可能存在的配置参数 it.getStringExtra(KEY_AZURE_SPEECH_KEY)?.let { value -> config[KEY_AZURE_SPEECH_KEY] = value } @@ -175,6 +179,13 @@ class BleMediaService : Service(), CoroutineScope { it.getStringExtra(KEY_VOLCANO_TOKEN)?.let { value -> config[KEY_VOLCANO_TOKEN] = value } it.getStringExtra(KEY_SERVER_URL)?.let { value -> config[KEY_SERVER_URL] = value } it.getStringExtra(KEY_USER_TOKEN)?.let { value -> config[KEY_USER_TOKEN] = value } + + // 提取 supportedLanguages 参数 + it.getStringArrayListExtra(KEY_SUPPORTED_LANGUAGES)?.let { languages -> + if (languages.isNotEmpty()) { + config[KEY_SUPPORTED_LANGUAGES] = languages + } + } } Log.i(TAG, "从Intent中提取到配置参数: $config") return config @@ -183,11 +194,25 @@ class BleMediaService : Service(), CoroutineScope { /** * 将配置参数保存到SharedPreferences */ - private fun saveConfigToPreferences(config: Map, prefs: SharedPreferences) { + private fun saveConfigToPreferences(config: Map, prefs: SharedPreferences) { prefs.edit().apply { config.forEach { (key, value) -> - if (value.isNotEmpty()) { - putString(key, value) + when (value) { + is String -> { + if (value.isNotEmpty()) { + putString(key, value) + } + } + is List<*> -> { + // 处理 supportedLanguages 列表 + if (key == KEY_SUPPORTED_LANGUAGES) { + val languageList = value.filterIsInstance() + if (languageList.isNotEmpty()) { + // 将语言列表转换为逗号分隔的字符串保存 + putString(key, languageList.joinToString(",")) + } + } + } } } apply() @@ -197,21 +222,34 @@ class BleMediaService : Service(), CoroutineScope { /** * 从SharedPreferences获取配置 */ - private fun getConfigFromPreferences(prefs: SharedPreferences): Map { - return mapOf( - KEY_AZURE_SPEECH_KEY to getConfigString(prefs, KEY_AZURE_SPEECH_KEY, ""), - KEY_AZURE_SPEECH_REGION to getConfigString(prefs, KEY_AZURE_SPEECH_REGION, "eastasia"), - KEY_OPENAI_API_KEY to getConfigString(prefs, KEY_OPENAI_API_KEY, ""), - KEY_OPENAI_BASE_URL to getConfigString(prefs, KEY_OPENAI_BASE_URL, "https://api.openai.com/v1"), - KEY_OPENAI_MODEL to getConfigString(prefs, KEY_OPENAI_MODEL, "gpt-3.5-turbo"), - KEY_OPENAI_VISION_MODEL to getConfigString(prefs, KEY_OPENAI_VISION_MODEL, "gpt-4-vision-preview"), - KEY_SYSTEM_PROMPT to getConfigString(prefs, KEY_SYSTEM_PROMPT, "gpt-3.5-turbo"), - KEY_MCP_SERVER to getConfigString(prefs, KEY_MCP_SERVER, ""), - KEY_VOLCANO_APP_ID to getConfigString(prefs, KEY_VOLCANO_APP_ID, ""), - KEY_VOLCANO_TOKEN to getConfigString(prefs, KEY_VOLCANO_TOKEN, ""), - KEY_SERVER_URL to getConfigString(prefs, KEY_SERVER_URL, ""), - KEY_USER_TOKEN to getConfigString(prefs, KEY_USER_TOKEN, "") - ) + private fun getConfigFromPreferences(prefs: SharedPreferences): Map { + val config = mutableMapOf() + + // 添加字符串类型的配置参数 + config[KEY_AZURE_SPEECH_KEY] = getConfigString(prefs, KEY_AZURE_SPEECH_KEY, "") + config[KEY_AZURE_SPEECH_REGION] = getConfigString(prefs, KEY_AZURE_SPEECH_REGION, "eastasia") + config[KEY_OPENAI_API_KEY] = getConfigString(prefs, KEY_OPENAI_API_KEY, "") + config[KEY_OPENAI_BASE_URL] = getConfigString(prefs, KEY_OPENAI_BASE_URL, "https://api.openai.com/v1") + config[KEY_OPENAI_MODEL] = getConfigString(prefs, KEY_OPENAI_MODEL, "gpt-3.5-turbo") + config[KEY_OPENAI_VISION_MODEL] = getConfigString(prefs, KEY_OPENAI_VISION_MODEL, "gpt-4-vision-preview") + config[KEY_SYSTEM_PROMPT] = getConfigString(prefs, KEY_SYSTEM_PROMPT, "gpt-3.5-turbo") + config[KEY_MCP_SERVER] = getConfigString(prefs, KEY_MCP_SERVER, "") + config[KEY_VOLCANO_APP_ID] = getConfigString(prefs, KEY_VOLCANO_APP_ID, "") + config[KEY_VOLCANO_TOKEN] = getConfigString(prefs, KEY_VOLCANO_TOKEN, "") + config[KEY_SERVER_URL] = getConfigString(prefs, KEY_SERVER_URL, "") + config[KEY_USER_TOKEN] = getConfigString(prefs, KEY_USER_TOKEN, "") + + // 添加 supportedLanguages 参数处理 + val supportedLanguagesStr = getConfigString(prefs, KEY_SUPPORTED_LANGUAGES, "") + if (supportedLanguagesStr.isNotEmpty()) { + // 将逗号分隔的字符串转换回语言列表 + val languageList = supportedLanguagesStr.split(",").map { it.trim() }.filter { it.isNotEmpty() } + if (languageList.isNotEmpty()) { + config[KEY_SUPPORTED_LANGUAGES] = languageList + } + } + + return config } /** diff --git a/local_plugins/agent_service/ios/agent_service/Package.swift b/local_plugins/agent_service/ios/agent_service/Package.swift index 278e827c5..cc30a9ad6 100644 --- a/local_plugins/agent_service/ios/agent_service/Package.swift +++ b/local_plugins/agent_service/ios/agent_service/Package.swift @@ -14,7 +14,9 @@ let package = Package( .package(path: "../../../azure_speech/ios/azure_speech"), .package(path: "../../../chat_api/ios/chat_api"), .package(path: "../../../chat_storage/ios/chat_storage"), - .package(path: "../../../music_service/ios/music_service") + .package(path: "../../../music_service/ios/music_service"), + .package(path: "../../../music_service/ios/user_portrait") + ], targets: [ .target( @@ -26,7 +28,8 @@ let package = Package( .product(name: "azure-speech", package: "azure_speech"), .product(name: "chat-api", package: "chat_api"), .product(name: "chat-storage", package: "chat_storage"), - .product(name: "music-service", package: "music_service") + .product(name: "music-service", package: "music_service"), + .product(name: "user-portrait", package: "user_portrait"), ], path: "Sources/agent_service", resources: [ 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 29dd1d360..ce456f1e3 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 @@ -8,6 +8,7 @@ import os.log import chat_api import chat_storage import music_service +import user_portrait protocol AgentServiceListener: AnyObject { func onEvent(eventName: String, data: [String: Any]) @@ -78,7 +79,7 @@ class AgentServiceImpl: NSObject { chatApiService = ChatApiService() // chatStorageHelper 现在是单例,不需要初始化 - + UserPortrait.shared.initialize() BleService.shared.setDelegate(self) } @@ -473,7 +474,7 @@ audioStream.saveAudioDataTo(data: audioData) if !systemPrompt.isEmpty { - let info: [String: String] = self.locationInfo ?? ["user_location": "还未获取到位置信息", "user_lat": "未开启定位", "user_lng": "未开启定位"] + let info: [String: String] = UserPortrait.shared.getAll() let _systemPrompt = fillTemplate(systemPrompt, with: info) messages.append(chatApiService.createSystemMessage(content: _systemPrompt)) os_log("添加系统提示消息", log: logger, type: .info) @@ -825,11 +826,34 @@ private func processNavigationCard(_ cardNavigation: [String: Any]) { } } } - func fillTemplate(_ template: String, with values: [String: String]) -> String { + /** + * 填充模板字符串,使用正则表达式匹配 {key} 格式的占位符 + * 对应 Kotlin 版本的 fillTemplate 方法 + */ + func fillTemplate(_ template: String, with data: [String: String]) -> String { + // 使用正则表达式匹配 {key} 格式的占位符 + let regex = try! NSRegularExpression(pattern: "\\{([a-zA-Z0-9_]+)\\}", options: []) + let range = NSRange(location: 0, length: template.utf16.count) + var result = template - for (key, value) in values { - result = result.replacingOccurrences(of: "{\(key)}", with: value) + let matches = regex.matches(in: template, options: [], range: range).reversed() + + // 从后往前替换,避免位置偏移问题 + for match in matches { + if match.numberOfRanges >= 2 { + let keyRange = match.range(at: 1) + if let swiftKeyRange = Range(keyRange, in: template) { + let key = String(template[swiftKeyRange]) + let value = data[key] ?? "未知" + + let fullMatchRange = match.range(at: 0) + if let swiftFullRange = Range(fullMatchRange, in: result) { + result.replaceSubrange(swiftFullRange, with: value) + } + } + } } + return result } func clearChatHistory() -> Bool { diff --git a/local_plugins/agent_service/lib/agent_service.dart b/local_plugins/agent_service/lib/agent_service.dart index ede6b56dc..5871bd8fe 100644 --- a/local_plugins/agent_service/lib/agent_service.dart +++ b/local_plugins/agent_service/lib/agent_service.dart @@ -332,20 +332,7 @@ class AgentService { } } - /// 设置支持的语音识别语言 - /// - /// [languages] 支持的语言列表,如 ['zh-CN', 'en-US'] - /// 返回是否设置成功 - static Future setSupportedLanguages(List languages) async { - try { - final bool result = await _channel.invokeMethod('setSupportedLanguages', { - 'languages': languages, - }); - return result; - } on PlatformException catch (e) { - throw AgentServiceException(e.code, e.message ?? '设置支持语言失败', e.details); - } - } + /// 停止语音合成 static Future stopAiSteam() async { diff --git a/local_plugins/user_portrait/ios/user_portrait/Sources/user_portrait/UserPortrait.swift b/local_plugins/user_portrait/ios/user_portrait/Sources/user_portrait/UserPortrait.swift new file mode 100644 index 000000000..e39ae8e5d --- /dev/null +++ b/local_plugins/user_portrait/ios/user_portrait/Sources/user_portrait/UserPortrait.swift @@ -0,0 +1,156 @@ +import Foundation +import UIKit + +public class UserPortrait: NSObject { + private static let TAG = "UserPortrait" + private static let PREF_NAME = "deep_voice_user" + + // 单例实例 + public static let shared = UserPortrait() + + private var appContext: UIApplication? + private var userDefaults: UserDefaults? + private var portrait: [String: Any] = [:] + + // 私有初始化方法,防止外部创建实例 + private override init() { + super.init() + } + + /** + * 初始化用户画像 + */ + public func initialize() { + do { + // 注册通知监听器 + NotificationCenter.default.addObserver( + self, + selector: #selector(handleUpdatePortrait(_:)), + name: NSNotification.Name("com.yunqiinnovation.user_portrait.UPDATE_PORTRAIT"), + object: nil + ) + + // 获取 UserDefaults + appContext = UIApplication.shared + userDefaults = UserDefaults(suiteName: UserPortrait.PREF_NAME) ?? UserDefaults.standard + + // 加载已保存的画像数据 + loadPortraitFromDefaults() + + print("\(UserPortrait.TAG): 用户画像初始化成功") + } catch { + print("\(UserPortrait.TAG): 初始化用户画像时发生异常: \(error.localizedDescription)") + } + } + + /** + * 处理更新画像通知 + */ + @objc private func handleUpdatePortrait(_ notification: Notification) { + print("\(UserPortrait.TAG): 接收到通知事件 \(notification.name.rawValue)") + + guard let userInfo = notification.userInfo, + let key = userInfo["key"] as? String, + let value = userInfo["value"] as? String else { + print("\(UserPortrait.TAG): 通知数据格式错误") + return + } + + print("\(UserPortrait.TAG): 收到 UPDATE_PORTRAIT 通知 \(key):\(value)") + + if !key.isEmpty { + print("\(UserPortrait.TAG): 接收到画像更新:\(key) = \(value)") + set(key: key, value: value) + } + } + + /** + * 从 UserDefaults 加载画像数据 + */ + private func loadPortraitFromDefaults() { + guard let defaults = userDefaults else { return } + + // 获取所有保存的键值对 + let allKeys = defaults.dictionaryRepresentation().keys + for key in allKeys { + if let value = defaults.object(forKey: key) { + portrait[key] = value + } + } + } + + /** + * 获取所有画像数据 + */ + public func getAll() -> [String: String] { + return portrait.compactMapValues { value in + return String(describing: value) + } + } + + /** + * 设置画像字段 + */ + public func set(key: String, value: String) { + userDefaults?.set(value, forKey: key) + userDefaults?.synchronize() // 立即同步到磁盘 + + // 更新内存中的画像数据 + portrait[key] = value + + print("\(UserPortrait.TAG): 设置画像字段 \(key) = \(value)") + } + + /** + * 获取指定键的画像值 + */ + public func get(key: String) -> String? { + return portrait[key] as? String + } + + /** + * 清空所有画像数据 + */ + public func clean() { + portrait.removeAll() + + // 清空 UserDefaults 中的数据 + if let defaults = userDefaults { + let allKeys = defaults.dictionaryRepresentation().keys + for key in allKeys { + defaults.removeObject(forKey: key) + } + defaults.synchronize() + } + + print("\(UserPortrait.TAG): 已清空所有画像数据") + } + + /** + * 发送更新画像通知(用于其他模块触发更新) + */ + public func sendUpdateNotification(key: String, value: String) { + let userInfo: [String: Any] = [ + "key": key, + "value": value + ] + + NotificationCenter.default.post( + name: NSNotification.Name("com.yunqiinnovation.user_portrait.UPDATE_PORTRAIT"), + object: nil, + userInfo: userInfo + ) + } + + /** + * 移除通知监听器(在应用退出时调用) + */ + public func deinitialize() { + NotificationCenter.default.removeObserver( + self, + name: NSNotification.Name("com.yunqiinnovation.user_portrait.UPDATE_PORTRAIT"), + object: nil + ) + print("\(UserPortrait.TAG): 用户画像已清理") + } +}