Browse Source

Merge branch 'new_dev' of https://github.com/deepcloud2048/deep_voice into new-dev1

newdev_shunjiawei
fdp 1 year ago
parent
commit
c0d2abebd3
  1. 1
      assets/lottie/intro_foriday.json
  2. 26
      ios/Runner.xcodeproj/project.pbxproj
  3. 42
      ios/Runner/Info.plist
  4. 24
      ios/Runner/Runner.entitlements
  5. 4
      lib/core/bindings/initial_binding.dart
  6. 17
      lib/core/translations/language/en_us.dart
  7. 2
      lib/core/translations/language/ru_ru.dart
  8. 2
      lib/core/translations/language/th_th.dart
  9. 2
      lib/core/translations/language/tr_tr.dart
  10. 4
      lib/core/translations/language/uk_ua.dart
  11. 15
      lib/core/translations/language/zh_cn.dart
  12. 4
      lib/core/translations/language/zh_hk.dart
  13. 168
      lib/core/widgets/intro_overlay_builder.dart
  14. 15
      lib/core/widgets/select_location_map.dart
  15. 11
      lib/data/models/appconfig.dart
  16. 24
      lib/data/models/channel_app_model.dart
  17. 4
      lib/data/models/message_model.dart
  18. 22
      lib/data/models/user_Info.dart
  19. 53
      lib/data/services/ble_manager.dart
  20. 104
      lib/data/services/novice_guide_service.dart
  21. 178
      lib/data/services/version_update_service.dart
  22. 33
      lib/main.dart
  23. 136
      lib/modules/agent/controllers/agent_controller.dart
  24. 756
      lib/modules/agent/views/agent_view.dart
  25. 30
      lib/modules/agent/views/message_bubble.dart
  26. 2
      lib/modules/agent/views/music_card.dart
  27. 0
      lib/modules/agent/widgets/circular_progress_button.dart
  28. 50
      lib/modules/agent/widgets/sound_bar_widget.dart
  29. 1
      lib/modules/devices/controllers/devices_controller.dart
  30. 150
      lib/modules/home/controllers/home_controller.dart
  31. 243
      lib/modules/home/views/home_view.dart
  32. 181
      lib/modules/login/controllers/login_controller.dart
  33. 17
      lib/modules/login/views/login_view.dart
  34. 6
      lib/modules/meeting/controllers/meeting_record_controller.dart
  35. 5
      lib/modules/meeting/views/bottomSheet/share_bottom_sheet.dart
  36. 4
      lib/modules/meeting/views/meeting_record_view.dart
  37. 13
      lib/modules/music/views/music_playlist_view.dart
  38. 38
      lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart
  39. 246
      lib/modules/pairing/controllers/pairing_controller.dart
  40. 2
      lib/modules/pairing/views/pairing_view.dart
  41. 132
      lib/modules/profile/views/profile_view.dart
  42. 77
      lib/modules/settings/controllers/settings_controller.dart
  43. 8
      lib/modules/settings/views/permissions_view.dart
  44. 58
      lib/modules/settings/views/settings_view.dart
  45. 269
      lib/modules/splash/controllers/splash_controller.dart
  46. 11
      lib/modules/translation/controllers/translation_controller.dart
  47. 22
      lib/modules/translation/views/translation_view.dart
  48. 175
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
  49. 341
      local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift
  50. 15
      local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServicePlugin.swift
  51. 230
      local_plugins/agent_service/ios/agent_service/Sources/agent_service/AudioSessionHub.swift
  52. BIN
      local_plugins/agent_service/ios/agent_service/Sources/agent_service/Resources/await.mp3
  53. 45
      local_plugins/agent_service/lib/agent_service.dart
  54. 2
      local_plugins/amap_service/ios/Classes/AmapService.swift
  55. 2
      local_plugins/amap_service/ios/Classes/AmapServicePlugin.swift
  56. 2
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt
  57. 583
      local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift
  58. 17
      local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift
  59. 2
      local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureTtsHelper.swift
  60. 214
      local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/LanguageDetector.swift
  61. 11
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt
  62. 9
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt
  63. 2
      local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftBleServicePlugin.swift
  64. 59
      local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftOpusAudioProcessor.swift
  65. 11
      local_plugins/ble_service/lib/ble_service.dart
  66. 46
      local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/Utils.kt
  67. 88
      local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatApiService.swift
  68. 89
      local_plugins/chat_api/ios/chat_api/Sources/chat_api/MCPClient.swift
  69. 240
      local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift
  70. 6
      local_plugins/chat_storage/ios/chat_storage/Sources/chat_storage/ChatStorageHelper.swift
  71. 2
      local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/LocationServicePlugin.kt
  72. 6
      local_plugins/location_service/ios/location_service/Sources/location_service/LocationData.swift
  73. 38
      local_plugins/location_service/ios/location_service/Sources/location_service/LocationService.swift
  74. 2
      local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServicePlugin.kt
  75. 2
      local_plugins/music_service/ios/music_service/Sources/music_service/MusicServicePlugin.swift
  76. 2
      local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/MCPClient.kt
  77. 2
      local_plugins/open_ai_service/ios/Classes/MCPClient.swift
  78. 2
      local_plugins/open_ai_service/ios/open_ai_service/Sources/open_ai_service/MCPClient.swift
  79. 2
      local_plugins/spotify_sdk/ios/spotify_sdk.podspec
  80. 6
      pubspec.yaml

1
assets/lottie/intro_foriday.json

File diff suppressed because one or more lines are too long

26
ios/Runner.xcodeproj/project.pbxproj

@ -514,7 +514,7 @@
NEW_SETTING = ""; NEW_SETTING = "";
SDKROOT = iphoneos; SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos; SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = 1;
VALIDATE_PRODUCT = YES; VALIDATE_PRODUCT = YES;
}; };
name = Profile; name = Profile;
@ -539,8 +539,14 @@
MARKETING_VERSION = 1.0.1; MARKETING_VERSION = 1.0.1;
PRODUCT_BUNDLE_IDENTIFIER = com.saitong.voitrans; PRODUCT_BUNDLE_IDENTIFIER = com.saitong.voitrans;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = NO;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = YES;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES;
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
VERSIONING_SYSTEM = "apple-generic"; VERSIONING_SYSTEM = "apple-generic";
}; };
name = Profile; name = Profile;
@ -572,6 +578,7 @@
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = K39FB49XKY;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.0; IPHONEOS_DEPLOYMENT_TARGET = 18.0;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
@ -589,6 +596,7 @@
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = K39FB49XKY;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.0; IPHONEOS_DEPLOYMENT_TARGET = 18.0;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
@ -654,7 +662,7 @@
NEW_SETTING = ""; NEW_SETTING = "";
ONLY_ACTIVE_ARCH = YES; ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos; SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = 1;
}; };
name = Debug; name = Debug;
}; };
@ -709,7 +717,7 @@
SUPPORTED_PLATFORMS = iphoneos; SUPPORTED_PLATFORMS = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule; SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = 1;
VALIDATE_PRODUCT = YES; VALIDATE_PRODUCT = YES;
}; };
name = Release; name = Release;
@ -734,9 +742,15 @@
MARKETING_VERSION = 1.0.1; MARKETING_VERSION = 1.0.1;
PRODUCT_BUNDLE_IDENTIFIER = com.saitong.voitrans; PRODUCT_BUNDLE_IDENTIFIER = com.saitong.voitrans;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = NO;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = YES;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES;
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
VERSIONING_SYSTEM = "apple-generic"; VERSIONING_SYSTEM = "apple-generic";
}; };
name = Debug; name = Debug;
@ -761,8 +775,14 @@
MARKETING_VERSION = 1.0.1; MARKETING_VERSION = 1.0.1;
PRODUCT_BUNDLE_IDENTIFIER = com.saitong.voitrans; PRODUCT_BUNDLE_IDENTIFIER = com.saitong.voitrans;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = NO;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = YES;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES;
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
VERSIONING_SYSTEM = "apple-generic"; VERSIONING_SYSTEM = "apple-generic";
}; };
name = Release; name = Release;

42
ios/Runner/Info.plist

@ -57,14 +57,10 @@
<false/> <false/>
<key>FacebookClientToken</key> <key>FacebookClientToken</key>
<string>193d0a2b1757c41cfa557ea8e8f21c33</string> <string>193d0a2b1757c41cfa557ea8e8f21c33</string>
<!-- 明确禁用用户跟踪功能 -->
<key>SKAdNetworkItems</key>
<array/>
<!-- 禁用广告跟踪 -->
<key>NSAdvertisingAttributionReportEndpoint</key>
<string></string>
<key>LSApplicationQueriesSchemes</key> <key>LSApplicationQueriesSchemes</key>
<array> <array>
<string>App-Prefs</string>
<string>prefs</string>
<string>weixin</string> <string>weixin</string>
<string>weixinULAPI</string> <string>weixinULAPI</string>
<string>weixinURLParamsAPI</string> <string>weixinURLParamsAPI</string>
@ -86,6 +82,8 @@
</array> </array>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true/>
<key>NSAdvertisingAttributionReportEndpoint</key>
<string></string>
<key>NSAppTransportSecurity</key> <key>NSAppTransportSecurity</key>
<dict> <dict>
<key>NSAllowsArbitraryLoads</key> <key>NSAllowsArbitraryLoads</key>
@ -94,23 +92,27 @@
<true/> <true/>
</dict> </dict>
<key>NSBluetoothAlwaysUsageDescription</key> <key>NSBluetoothAlwaysUsageDescription</key>
<string>需要蓝牙权限用于连接和管理音频设备</string> <string>Bluetooth permission is required to connect and manage audio devices</string>
<key>NSBluetoothPeripheralUsageDescription</key> <key>NSBluetoothPeripheralUsageDescription</key>
<string>需要蓝牙权限用于连接和管理音频设备</string> <string>Bluetooth permission is required to connect and manage audio devices</string>
<key>NSCalendarsUsageDescription</key> <key>NSCalendarsUsageDescription</key>
<string>需要日历权限用于安排和管理您的音频会议和录音任务</string> <string>Calendar permission is required to schedule and manage your audio meetings and recording tasks</string>
<key>NSCameraUsageDescription</key> <key>NSCameraUsageDescription</key>
<string>需要相机权限用于扫描二维码和视频通话功能</string> <string>Camera permission is required for QR code scanning and video calling functions</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key> <key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>语音助手需要位置权限来为您提供基于位置的信息和服务</string> <string>Location access enables the app to provide location-based voice translation services, such as translating local place names and providing contextual information relevant to your current location during conversations.</string>
<key>NSLocationWhenInUseUsageDescription</key> <key>NSLocationWhenInUseUsageDescription</key>
<string>语音助手需要位置权限来为您提供基于位置的信息和服务</string> <string>Location access enables the app to provide location-based voice translation services, such as translating local place names and providing contextual information relevant to your current location during conversations.</string>
<key>NSMicrophoneUsageDescription</key> <key>NSMicrophoneUsageDescription</key>
<string>需要麦克风权限用于语音识别和录音功能</string> <string>Our app's voice AI interaction features (such as voice wake-up for AI, issuing commands, and real-time conversations) require microphone access. With this permission, you can interact with AI directly through speech, improving operational efficiency. We strictly protect your voice data—only used for real-time processing of interaction requests, and raw audio will not be stored.</string>
<key>NSMotionUsageDescription</key> <key>NSMotionUsageDescription</key>
<string>我们需要访问您的运动数据以提供更精准的导航服务</string> <string>This app uses motion and fitness data to detect when you're walking or moving, which helps optimize voice recognition accuracy and automatically adjust microphone sensitivity for better audio recording quality during different activities.</string>
<key>NSPhotoLibraryUsageDescription</key> <key>NSPhotoLibraryUsageDescription</key>
<string>需要相册权限用于保存录音文件和导入音频文件</string> <string>Photo library permission is required to save recording files and import audio files</string>
<key>NSUserTrackingUsageDescription</key>
<string>This identifier will be used to deliver personalized ads to you.</string>
<key>SKAdNetworkItems</key>
<array/>
<key>UIApplicationSupportsIndirectInputEvents</key> <key>UIApplicationSupportsIndirectInputEvents</key>
<true/> <true/>
<key>UIBackgroundModes</key> <key>UIBackgroundModes</key>
@ -119,17 +121,15 @@
<string>audio</string> <string>audio</string>
<string>location</string> <string>location</string>
</array> </array>
<key>UIDeviceFamily</key>
<array>
<string>1</string>
</array>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>
<string>LaunchScreen</string> <string>LaunchScreen</string>
<key>UIMainStoryboardFile</key> <key>UIMainStoryboardFile</key>
<string>Main</string> <string>Main</string>
<key>UISupportedInterfaceOrientations</key> <key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array> <array>
<string>UIInterfaceOrientationPortrait</string> <string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string> <string>UIInterfaceOrientationPortraitUpsideDown</string>

24
ios/Runner/Runner.entitlements

@ -2,6 +2,10 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>com.apple.developer.applesignin</key>
<array>
<string>Default</string>
</array>
<key>com.apple.developer.associated-domains</key> <key>com.apple.developer.associated-domains</key>
<array> <array>
<string>applinks:voitrans.net</string> <string>applinks:voitrans.net</string>
@ -10,9 +14,21 @@
<true/> <true/>
<key>com.apple.external-accessory.wireless-configuration</key> <key>com.apple.external-accessory.wireless-configuration</key>
<true/> <true/>
<key>com.apple.developer.applesignin</key> <key>com.apple.security.app-sandbox</key>
<array> <true/>
<string>Default</string> <key>com.apple.security.device.audio-input</key>
</array> <true/>
<key>com.apple.security.device.bluetooth</key>
<true/>
<key>com.apple.security.device.camera</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.personal-information.calendars</key>
<true/>
<key>com.apple.security.personal-information.location</key>
<true/>
<key>com.apple.security.personal-information.photos-library</key>
<true/>
</dict> </dict>
</plist> </plist>

4
lib/core/bindings/initial_binding.dart

@ -8,6 +8,7 @@ import '../../../data/services/speech_impl/azure_ast_service.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../../data/services/meeting/meeting_task_service.dart'; import '../../data/services/meeting/meeting_task_service.dart';
import '../../data/services/meeting/meeting_upload_service.dart'; import '../../data/services/meeting/meeting_upload_service.dart';
import '../../data/services/novice_guide_service.dart';
import '../../data/services/speech_factory.dart'; import '../../data/services/speech_factory.dart';
import '../../data/services/volcano_translation_service.dart'; import '../../data/services/volcano_translation_service.dart';
import '../../data/services/volcano_ai_service.dart'; import '../../data/services/volcano_ai_service.dart';
@ -68,6 +69,9 @@ class InitialBinding extends Bindings {
// 会议助手音频上传 // 会议助手音频上传
Get.lazyPut<UserportraitManager>(() => UserportraitManager(), fenix: true); Get.lazyPut<UserportraitManager>(() => UserportraitManager(), fenix: true);
// 新手引导
Get.lazyPut<NoviceGuideService>(() => NoviceGuideService(), fenix: true);
// 杰理蓝牙服务 // 杰理蓝牙服务
// // 语言管理器(需要最先初始化) // // 语言管理器(需要最先初始化)

17
lib/core/translations/language/en_us.dart

@ -436,6 +436,8 @@ const Map<String, String> enUS = {
'deleteAccount': 'Delete Account', 'deleteAccount': 'Delete Account',
'deleteAccountTips': 'deleteAccountTips':
'Are you sure you want to permanently delete your account? This action cannot be undone.', 'Are you sure you want to permanently delete your account? This action cannot be undone.',
'clean': 'Clear Cache', // 清除缓存
'cleanTips': 'Are you sure you want to clear personal cache?', // 确定要清除个人缓存吗?
//Profile 错误信息 //Profile 错误信息
'networkConnectionFailed': 'networkConnectionFailed':
@ -742,7 +744,7 @@ const Map<String, String> enUS = {
// 配对模块 // 配对模块
// 配对页面 // 配对页面
"devicePairing": "Device Pairing", "devicePairing": "Device Pairing",
"connectYourDeepSoundHeadset": "Connect Your DeepSound Headset", "connectYourDeepSoundHeadset": "Connect Your Voitrans Headset",
"bind": "Bind", "bind": "Bind",
"authorizeBind": "Authorize Binding", "authorizeBind": "Authorize Binding",
"enterCode": "Please enter code", "enterCode": "Please enter code",
@ -813,6 +815,19 @@ const Map<String, String> enUS = {
'deviceAuthorizationFailedCheckCode': 'deviceAuthorizationFailedCheckCode':
'Device authorization failed, please check authorization code', 'Device authorization failed, please check authorization code',
///iOS蓝牙引导窗
'bluetoothGuideTitle': 'Manual Bluetooth Activation Required', // 需要手动开启蓝牙
'bluetoothGuideSteps': 'Please follow these steps:', // 请按照以下步骤操作:
'bluetoothGuideStep1':
'Enable Bluetooth: Settings → Bluetooth → Turn On', // 开启蓝牙:设置 → 蓝牙 → 开启
'bluetoothGuideStep2':
'Connect Device: Find your device in "My Devices" and tap to connect', // 连接设备:在"我的设备"中找到您的设备并点击连接
'bluetoothGuideStep3':
'Return to app to continue pairing process', // 返回应用继续配对流程
'bluetoothGuideTip':
'Tip: You can also long press the Bluetooth icon in Control Center to quickly access Bluetooth settings', // 提示:也可以从控制中心长按蓝牙图标快速进入蓝牙设置
'iKnow': 'I Know', // 我知道了
// 用户反馈页 // 用户反馈页
"feedback": "Feedback", "feedback": "Feedback",
'feedbackInfo': 'Provide feedback or suggestions', 'feedbackInfo': 'Provide feedback or suggestions',

2
lib/core/translations/language/ru_ru.dart

@ -702,7 +702,7 @@ const Map<String, String> ruRU = {
// 配对模块 // 配对模块
// 配对页面 (pairing_view.dart & pairing_view_new.dart) // 配对页面 (pairing_view.dart & pairing_view_new.dart)
"devicePairing": "Сопряжение устройства", "devicePairing": "Сопряжение устройства",
"connectYourDeepSoundHeadset": "Подключите ваши наушники DeepSound", "connectYourDeepSoundHeadset": "Подключите ваши наушники Voitrans",
"bind": "Привязать", "bind": "Привязать",
"authorizeBind": "Авторизовать привязку", "authorizeBind": "Авторизовать привязку",
"enterCode": "Пожалуйста, введите код", "enterCode": "Пожалуйста, введите код",

2
lib/core/translations/language/th_th.dart

@ -736,7 +736,7 @@ const Map<String, String> thTH = {
'retryAuthorization': 'ลองอนุญาตอีกครั้ง', 'retryAuthorization': 'ลองอนุญาตอีกครั้ง',
'pleaseGrantNecessaryPermissions': 'กรุณาให้สิทธิ์ที่จำเป็น', 'pleaseGrantNecessaryPermissions': 'กรุณาให้สิทธิ์ที่จำเป็น',
'checkFailedRetry': 'การตรวจสอบล้มเหลว โปรดลองอีกครั้ง', 'checkFailedRetry': 'การตรวจสอบล้มเหลว โปรดลองอีกครั้ง',
'pleaseConnectDeepSoundDevice': 'กรุณาเชื่อมต่ออุปกรณ์ DeepSound', 'pleaseConnectDeepSoundDevice': 'กรุณาเชื่อมต่ออุปกรณ์ Voitrans',
'verificationFailedRetry': 'การตรวจสอบล้มเหลว โปรดลองอีกครั้ง', 'verificationFailedRetry': 'การตรวจสอบล้มเหลว โปรดลองอีกครั้ง',
'invalidDeviceAddress': 'ที่อยู่อุปกรณ์ไม่ถูกต้อง', 'invalidDeviceAddress': 'ที่อยู่อุปกรณ์ไม่ถูกต้อง',
'bleDeviceAssociationFailed': 'การเชื่อมโยงอุปกรณ์ BLE ล้มเหลว', 'bleDeviceAssociationFailed': 'การเชื่อมโยงอุปกรณ์ BLE ล้มเหลว',

2
lib/core/translations/language/tr_tr.dart

@ -695,7 +695,7 @@ const Map<String, String> trTR = {
// 配对模块 // 配对模块
// 配对页面 (pairing_view.dart & pairing_view_new.dart) // 配对页面 (pairing_view.dart & pairing_view_new.dart)
"devicePairing": "Cihaz Eşleştirme", "devicePairing": "Cihaz Eşleştirme",
"connectYourDeepSoundHeadset": "DeepSound Kulaklığınızı Bağlayın", "connectYourDeepSoundHeadset": "Voitrans Kulaklığınızı Bağlayın",
"bind": "Bağla", "bind": "Bağla",
"authorizeBind": "Bağlamayı Yetkilendir", "authorizeBind": "Bağlamayı Yetkilendir",
"enterCode": "Lütfen kodu girin", "enterCode": "Lütfen kodu girin",

4
lib/core/translations/language/uk_ua.dart

@ -702,7 +702,7 @@ const Map<String, String> ukUA = {
// 配对模块 // 配对模块
// 配对页面 (pairing_view.dart & pairing_view_new.dart) // 配对页面 (pairing_view.dart & pairing_view_new.dart)
"devicePairing": "Спарювання пристрою", "devicePairing": "Спарювання пристрою",
"connectYourDeepSoundHeadset": "Підключіть ваші навушники DeepSound", "connectYourDeepSoundHeadset": "Підключіть ваші навушники Voitrans",
"bind": "Прив'язати", "bind": "Прив'язати",
"authorizeBind": "Авторизувати прив'язку", "authorizeBind": "Авторизувати прив'язку",
"enterCode": "Будь ласка, введіть код", "enterCode": "Будь ласка, введіть код",
@ -757,7 +757,7 @@ const Map<String, String> ukUA = {
'retryAuthorization': 'Повторити авторизацію', 'retryAuthorization': 'Повторити авторизацію',
'pleaseGrantNecessaryPermissions': 'Будь ласка, надайте необхідні дозволи', 'pleaseGrantNecessaryPermissions': 'Будь ласка, надайте необхідні дозволи',
'checkFailedRetry': 'Перевірка не вдалася, спробуйте ще раз', 'checkFailedRetry': 'Перевірка не вдалася, спробуйте ще раз',
'pleaseConnectDeepSoundDevice': 'Будь ласка, підключіть пристрій DeepSound', 'pleaseConnectDeepSoundDevice': 'Будь ласка, підключіть пристрій Voitrans',
'verificationFailedRetry': 'Перевірка не вдалася, спробуйте ще раз', 'verificationFailedRetry': 'Перевірка не вдалася, спробуйте ще раз',
'invalidDeviceAddress': 'Недійсна адреса пристрою', 'invalidDeviceAddress': 'Недійсна адреса пристрою',
'bleDeviceAssociationFailed': 'Не вдалося асоціювати пристрій BLE', 'bleDeviceAssociationFailed': 'Не вдалося асоціювати пристрій BLE',

15
lib/core/translations/language/zh_cn.dart

@ -411,6 +411,8 @@ const Map<String, String> zhCN = {
"logoutTips": "确定要退出当前账号吗?", "logoutTips": "确定要退出当前账号吗?",
'deleteAccount': '注销账号', 'deleteAccount': '注销账号',
'deleteAccountTips': '确定要永久删除您的账号吗?此操作不可撤销', 'deleteAccountTips': '确定要永久删除您的账号吗?此操作不可撤销',
'clean': '清除缓存',
'cleanTips': '确定要清除个人缓存吗?',
// Profile 错误信息 // Profile 错误信息
'networkConnectionFailed': '网络连接失败,请检查网络设置后重试', 'networkConnectionFailed': '网络连接失败,请检查网络设置后重试',
@ -684,7 +686,7 @@ const Map<String, String> zhCN = {
// 配对模块 // 配对模块
// 配对页面 (pairing_view.dart & pairing_view_new.dart) // 配对页面 (pairing_view.dart & pairing_view_new.dart)
"devicePairing": "设备配对", "devicePairing": "设备配对",
"connectYourDeepSoundHeadset": "连接您的 DeepSound 耳机", "connectYourDeepSoundHeadset": "连接您的 Voitrans 耳机",
"bind": "绑定", "bind": "绑定",
"authorizeBind": "授权绑定", "authorizeBind": "授权绑定",
"enterCode": "请输入代码", "enterCode": "请输入代码",
@ -736,7 +738,7 @@ const Map<String, String> zhCN = {
'retryAuthorization': '重新授权', 'retryAuthorization': '重新授权',
'pleaseGrantNecessaryPermissions': '请授予必要权限', 'pleaseGrantNecessaryPermissions': '请授予必要权限',
'checkFailedRetry': '检查失败,请重试', 'checkFailedRetry': '检查失败,请重试',
'pleaseConnectDeepSoundDevice': '请连接 DeepSound 设备', 'pleaseConnectDeepSoundDevice': '请连接 Voitrans 设备',
'verificationFailedRetry': '验证失败,请重试', 'verificationFailedRetry': '验证失败,请重试',
'invalidDeviceAddress': '设备地址无效', 'invalidDeviceAddress': '设备地址无效',
'bleDeviceAssociationFailed': 'BLE设备关联失败', 'bleDeviceAssociationFailed': 'BLE设备关联失败',
@ -746,6 +748,15 @@ const Map<String, String> zhCN = {
'bleConnectionFailed': 'BLE连接失败', 'bleConnectionFailed': 'BLE连接失败',
'deviceAuthorizationFailedCheckCode': '设备授权失败,请检查授权码', 'deviceAuthorizationFailedCheckCode': '设备授权失败,请检查授权码',
///iOS蓝牙引导窗
'bluetoothGuideTitle': '需要手动开启蓝牙', // 需要手动开启蓝牙
'bluetoothGuideSteps': '请按照以下步骤操作:', // 请按照以下步骤操作:
'bluetoothGuideStep1': '开启蓝牙:设置 → 蓝牙 → 开启', // 开启蓝牙:设置 → 蓝牙 → 开启
'bluetoothGuideStep2': '连接设备:在"我的设备"中找到您的设备并点击连接', // 连接设备:在"我的设备"中找到您的设备并点击连接
'bluetoothGuideStep3': '返回应用继续配对流程', // 返回应用继续配对流程
'bluetoothGuideTip': '提示:也可以从控制中心长按蓝牙图标快速进入蓝牙设置', // 提示:也可以从控制中心长按蓝牙图标快速进入蓝牙设置
'iKnow': '我知道了', // 我知道了
//用户反馈页 //用户反馈页
"feedback": "用户反馈", "feedback": "用户反馈",
'feedbackInfo': '提供反馈或建议', 'feedbackInfo': '提供反馈或建议',

4
lib/core/translations/language/zh_hk.dart

@ -648,7 +648,7 @@ const Map<String, String> zhHK = {
// 配對模塊 // 配對模塊
// 配對頁面 (pairing_view.dart & pairing_view_new.dart) // 配對頁面 (pairing_view.dart & pairing_view_new.dart)
"devicePairing": "設備配對", "devicePairing": "設備配對",
"connectYourDeepSoundHeadset": "連接您的 DeepSound 耳機", "connectYourDeepSoundHeadset": "連接您的 Voitrans 耳機",
"bind": "綁定", "bind": "綁定",
"authorizeBind": "授權綁定", "authorizeBind": "授權綁定",
"enterCode": "請輸入代碼", "enterCode": "請輸入代碼",
@ -695,7 +695,7 @@ const Map<String, String> zhHK = {
'retryAuthorization': '重新授權', 'retryAuthorization': '重新授權',
'pleaseGrantNecessaryPermissions': '請授予必要權限', 'pleaseGrantNecessaryPermissions': '請授予必要權限',
'checkFailedRetry': '檢查失敗,請重試', 'checkFailedRetry': '檢查失敗,請重試',
'pleaseConnectDeepSoundDevice': '請連接 DeepSound 設備', 'pleaseConnectDeepSoundDevice': '請連接 Voitrans 設備',
'verificationFailedRetry': '驗證失敗,請重試', 'verificationFailedRetry': '驗證失敗,請重試',
'invalidDeviceAddress': '設備地址無效', 'invalidDeviceAddress': '設備地址無效',
'bleDeviceAssociationFailed': 'BLE設備關聯失敗', 'bleDeviceAssociationFailed': 'BLE設備關聯失敗',

168
lib/core/widgets/intro_overlay_builder.dart

@ -0,0 +1,168 @@
import 'package:flutter/material.dart';
import 'package:flutter_intro/flutter_intro.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:lottie/lottie.dart';
class IntroOverlayBuilder {
static Widget homeBuilder(
StepWidgetParams params,
bool isWakeup, {
Function? onNext,
}) {
return Column(
children: [
isWakeup
? _AnimatedLottieWidget()
: SizedBox(
width: 200.w,
height: 200.w,
child: Lottie.asset('assets/lottie/intro_foriday.json'),
),
Padding(
padding: EdgeInsets.only(bottom: 20.w),
child: Text(
isWakeup ? '我在呢~' : '对着麦克风说:“小言小言”',
style: TextStyle(
fontSize: 15.sp,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
if (onNext != null)
_button(
'下一步',
() {
params.onNext!();
onNext();
},
),
],
);
}
static Widget aiBuilder(
StepWidgetParams params,
String text, {
String buttonText = '下一步',
bool alignment = true,
Function? onNext,
Function? onFinish,
}) {
return Column(
crossAxisAlignment:
alignment ? CrossAxisAlignment.start : CrossAxisAlignment.end,
children: [
Padding(
padding: EdgeInsets.only(bottom: 10.w),
child: Text(
text,
style: TextStyle(
fontSize: 15.sp,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
Row(
children: [
if (onNext != null)
_button(
buttonText,
() {
params.onNext!();
onNext();
},
),
if (onFinish != null)
_button(
buttonText,
() {
params.onFinish();
onFinish();
},
),
],
),
],
);
}
static Widget _button(String text, Function() onTap) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 5.w),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20.r),
border: Border.all(color: Colors.white),
),
child: Text(
text,
style: TextStyle(
fontSize: 14.sp,
color: Colors.white,
),
),
),
);
}
}
// 新增的动画Widget
class _AnimatedLottieWidget extends StatefulWidget {
@override
_AnimatedLottieWidgetState createState() => _AnimatedLottieWidgetState();
}
class _AnimatedLottieWidgetState extends State<_AnimatedLottieWidget>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _scaleAnimation;
@override
void initState() {
super.initState();
// 创建动画控制器,持续时间1.5秒
_animationController = AnimationController(
duration: const Duration(milliseconds: 1000),
vsync: this,
);
// 创建缩放动画,从0.8到1.2再回到1.0
_scaleAnimation = Tween<double>(
begin: 0.8,
end: 1.2,
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.elasticInOut,
));
// 启动动画并设置为循环
_animationController.repeat(reverse: true);
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _scaleAnimation,
builder: (context, child) {
return Transform.scale(
scale: _scaleAnimation.value,
child: SizedBox(
width: 200.w,
height: 200.w,
child: Lottie.asset('assets/lottie/intro_foriday.json'),
),
);
},
);
}
}

15
lib/core/widgets/select_location_map.dart

@ -1,15 +0,0 @@
import 'package:flutter/material.dart';
class SelectLocationMap extends StatefulWidget {
const SelectLocationMap({super.key});
@override
State<SelectLocationMap> createState() => _SelectLocationMapState();
}
class _SelectLocationMapState extends State<SelectLocationMap> {
@override
Widget build(BuildContext context) {
return const Placeholder();
}
}

11
lib/data/models/appconfig.dart

@ -4,9 +4,20 @@ import '../../../data/models/appconfig_model.dart';
class AppConfig { class AppConfig {
static late UserGetAppConfigResp config; static late UserGetAppConfigResp config;
// 新增一个私有的静态布尔值来跟踪初始化状态
static bool _isInitialized = false;
/// 检查AppConfig单例是否已经被初始化
/// 外部通过调用此方法来安全地检查状态
static bool isInitialized() {
return _isInitialized;
}
// 可选:提供初始化方法 // 可选:提供初始化方法
static void initialize(UserGetAppConfigResp resp) { static void initialize(UserGetAppConfigResp resp) {
config = resp; config = resp;
_isInitialized = true; // <-- 新增此行
} }
//环境变量 //环境变量

24
lib/data/models/channel_app_model.dart

@ -177,6 +177,30 @@ enum UserChannel {
); );
} }
// 从字符串名称创建枚举
static UserChannel fromName(String name) {
switch (name.toUpperCase()) {
case 'CHINA_HUAWEI':
return UserChannel.CHINA_HUAWEI;
case 'CHINA_XIAOMI':
return UserChannel.CHINA_XIAOMI;
case 'CHINA_OPPO':
return UserChannel.CHINA_OPPO;
case 'CHINA_VIVO':
return UserChannel.CHINA_VIVO;
case 'CHINA_HONOR':
return UserChannel.CHINA_HONOR;
case 'OFFICIAL':
return UserChannel.OFFICIAL;
case 'GLOBAL_APPLE':
return UserChannel.GLOBAL_APPLE;
case 'GLOBAL_GOOGLE':
return UserChannel.GLOBAL_GOOGLE;
default:
return UserChannel.UNKNOWN;
}
}
// JSON 序列化时返回数字值 // JSON 序列化时返回数字值
int toJson() => value; int toJson() => value;

4
lib/data/models/message_model.dart

@ -1,3 +1,7 @@
import 'dart:io';
import 'package:path_provider/path_provider.dart';
class Message { class Message {
final bool isUser; //标识信息是否来自用户还是AI final bool isUser; //标识信息是否来自用户还是AI
final String text; //信息文本内容 final String text; //信息文本内容

22
lib/data/models/user_Info.dart

@ -1,9 +1,15 @@
import 'dart:ffi'; import 'package:get_storage/get_storage.dart';
import '../../../core/utils/logger.dart';
class User { class User {
// 单例实例 // 单例实例
static User? _instance; static User? _instance;
/// 安全地检查用户是否已经登录(即单例是否已初始化)
static bool isLoggedIn() {
return _instance != null;
}
// 单例访问器(增加空安全保护) // 单例访问器(增加空安全保护)
static User get instance { static User get instance {
if (_instance == null) { if (_instance == null) {
@ -12,6 +18,18 @@ class User {
return _instance!; return _instance!;
} }
// --- 新增 ---
/// 清理所有与用户会话相关的数据
static Future<void> clearUserSession() async {
final GetStorage storage = GetStorage();
// 1. 清理本地存储
await storage.remove('logintoken');
await storage.remove('user_info');
// 2. 清理内存中的单例
_instance = null;
Logger.i("User", "用户会话数据已安全清理。");
}
// 最终属性(不可变) // 最终属性(不可变)
String name; String name;
String uid; String uid;
@ -72,7 +90,7 @@ class User {
// 修复1:安全获取嵌套的 user 字段 // 修复1:安全获取嵌套的 user 字段
final userData = json['user'] as Map<String, dynamic>?; // 允许空值 final userData = json['user'] as Map<String, dynamic>?; // 允许空值
final devicesData = json['devices'] as List<dynamic>?; // 允许空值 final devicesData = json['devices'] as List<dynamic>?; // 允许空值
print("数据获取fronTokenJson${userData} ${devicesData}"); print("数据获取fronTokenJson$userData $devicesData");
// 更新单例实例 // 更新单例实例
return _instance = User._( return _instance = User._(
token: '', token: '',

53
lib/data/services/ble_manager.dart

@ -9,6 +9,7 @@ import 'package:get_storage/get_storage.dart';
import 'dart:convert'; import 'dart:convert';
import 'package:agent_service/agent_service.dart'; import 'package:agent_service/agent_service.dart';
import 'bluetooth_manager.dart'; import 'bluetooth_manager.dart';
import 'language_manager.dart';
// 重新导出BLE连接状态常量,让外部可以通过BleManager访问 // 重新导出BLE连接状态常量,让外部可以通过BleManager访问
export 'package:ble_service/ble_service.dart' show BleConnectionState; export 'package:ble_service/ble_service.dart' show BleConnectionState;
@ -556,26 +557,44 @@ class BleManager extends GetxService {
// 根据APP语言动态配置语音识别语言 // 根据APP语言动态配置语音识别语言
List<String> supportedLanguages = []; List<String> supportedLanguages = [];
try { try {
// 使用 GetX 获取 APP 当前语言环境 (Locale) final storage = GetStorage();
final appLocale = Get.locale; final languageManager = Get.find<LanguageManager>();
if (appLocale != null) { // 从存储中读取用户设置的语言
final lang = appLocale.languageCode; // e.g., 'en' String? savedLanguage = storage.read('language');
final country = appLocale.countryCode; // e.g., 'US'
if (savedLanguage != null && savedLanguage != 'system') {
// 构造 Azure SDK 需要的格式 (e.g., 'en-US') // 用户设置了特定语言
if (country != null && country.isNotEmpty) { final parts = savedLanguage.split('_');
supportedLanguages.add('$lang-$country'); if (parts.length == 2) {
} else { final asrCode = languageManager.getAsrCodeByShortCode(parts[0]);
supportedLanguages.add(lang); // fallback only language code if (asrCode != null) {
supportedLanguages.add(asrCode);
} else {
// 直接使用语言-国家格式
supportedLanguages.add('${parts[0]}-${parts[1]}');
}
}
} else {
// 跟随系统语言或未设置
final appLocale = Get.locale;
if (appLocale != null) {
final lang = appLocale.languageCode;
final country = appLocale.countryCode;
// 尝试从语言管理器获取对应的ASR代码
final asrCode = languageManager.getAsrCodeByShortCode(lang);
if (asrCode != null) {
supportedLanguages.add(asrCode);
} else if (country != null && country.isNotEmpty) {
// 构造 Azure SDK 需要的格式 (e.g., 'en-US')
supportedLanguages.add('$lang-$country');
} else {
supportedLanguages.add('en-US'); // 默认英语
}
} }
} }
// 添加 en-US 作为备选
// if (!supportedLanguages.contains('en-US')) {
// supportedLanguages.add('en-US');
// }
// 最终兜底 // 最终兜底
if (supportedLanguages.isEmpty) { if (supportedLanguages.isEmpty) {
supportedLanguages.add('en-US'); supportedLanguages.add('en-US');

104
lib/data/services/novice_guide_service.dart

@ -0,0 +1,104 @@
import 'dart:async';
import 'package:ble_service/ble_service.dart';
import 'package:flutter_intro/flutter_intro.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
class NoviceGuideService extends GetxService {
final GetStorage _storage = GetStorage();
final BleService _bleService = BleService();
StreamSubscription<dynamic>? _statusSubscription;
bool isHomeGuide = true;
bool isAIGuide = true;
RxBool isWakeup = false.obs;
int aiStep = 0;
List aiStepsList = [
{
'start': '对小言说:“今天天气怎么样”',
'input': '今天天气怎么样',
'send': '小言会为你实时为你播报天气',
'end': '小言能实时为你播报天气,试试其他指令吧',
},
{
'start': '对小言说:“请播放一组摇滚乐”',
'input': '请播放一组摇滚乐',
'send': '小言会为你搜寻到一组音乐',
'end': '小言已成功为您搜寻到一组音乐,试试其他指令吧',
},
{
'start': '对小言说:“导航到深圳机场”',
'input': '导航到深圳机场',
'send': '小言会为你规划路线',
'end': '小言已成功为您导航到深圳机场,试试其他指令吧',
},
{
'start': '对小言说:“明天下午3点创建会议提醒”',
'input': '明天下午3点创建会议提醒',
'send': '小言会为你创建会议提醒',
'end': '小言已成功为您创建会议提醒,试试其他指令吧',
},
{
'start': '对小言说:“拨打妈妈的电话”',
'input': '拨打妈妈的电话',
'send': '小言会为你拨打电话',
'end': '小言已成功为您拨打电话,试试其他指令吧',
},
{
'start': '对小言说:“给爸爸发短信说晚上回家吃饭’”',
'input': '给爸爸发短信说晚上回家吃饭’',
'send': '小言会为你发送短信',
'end': '短信已发送,语音操控更便捷',
},
];
@override
void onInit() {
super.onInit();
// isHomeGuide = _storage.read('home_guide') ?? false;
// isAIGuide = _storage.read('ai_guide') ?? false;
}
@override
void onClose() {
closeWakeupListen();
super.onClose();
}
void startHomeGuide() async {
if (!isHomeGuide) {
await Future.delayed(const Duration(milliseconds: 200));
isHomeGuide = true;
_storage.write('home_guide', true);
Intro.of(Get.context!).start(group: 'home');
}
}
void startAIGuide() async {
if (!isAIGuide) {
await Future.delayed(const Duration(milliseconds: 200));
isAIGuide = true;
_storage.write('ai_guide', true);
Intro.of(Get.context!).start(group: 'ai');
}
}
void startWakeupListen() {
_bleService.setWakeup(false);
_statusSubscription = _bleService.onStatusUpdated.listen((event) {
if (event is Map && event['type'] == 'wakeup') {
isWakeup.value = true;
}
});
}
void closeWakeupListen() {
_bleService.setWakeup(true);
_statusSubscription?.cancel();
}
}

178
lib/data/services/version_update_service.dart

@ -5,6 +5,7 @@ import 'package:package_info_plus/package_info_plus.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:logger/logger.dart'; import 'package:logger/logger.dart';
import 'package:device_info_plus/device_info_plus.dart'; import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import '../models/channel_app_model.dart'; import '../models/channel_app_model.dart';
import 'network/api.dart'; import 'network/api.dart';
import 'dart:math' as math; import 'dart:math' as math;
@ -13,15 +14,62 @@ import 'dart:math' as math;
class VersionUpdateService extends GetxService { class VersionUpdateService extends GetxService {
static VersionUpdateService get to => Get.find(); static VersionUpdateService get to => Get.find();
final Logger _logger = Logger(); // 静态应用信息(内存缓存)
static String? _appVersion = ""; //应用版本号
static String? _buildNumber = ""; //构建号
static String? _platform = ""; //平台类型
static String? _deviceBrand = ""; //设备品牌
static UserChannel? _channel; //渠道
static String? _latestVersion = ""; //最新版本号+构建号
static bool? _hasUpdate = false; //是否有更新
static bool? _isForceUpdate = false; //是否强制更新
static String? _updateUrl = ""; //更新链接
static bool? _isShowTourists = false; //是否显示游客登录
static DateTime? _lastCheckTime; //上次检查时间
static const int _cacheIntervalMinutes = 10; // 可配置化
// 添加 getter 方法来访问缓存的静态数据
/// 获取缓存的应用版本
String get cachedAppVersion => _appVersion ?? '';
/// 获取缓存的构建号
String get cachedBuildNumber => _buildNumber ?? '';
/// 获取缓存的平台信息
String get cachedPlatform => _platform ?? '';
/// 获取缓存的设备品牌
String get cachedDeviceBrand => _deviceBrand ?? '';
/// 获取缓存的渠道信息
UserChannel get cachedChannel => _channel ?? UserChannel.UNKNOWN;
/// 获取缓存的最新版本
String get cachedLatestVersion => _latestVersion ?? '';
/// 获取缓存的是否有更新
bool get cachedHasUpdate => _hasUpdate ?? false;
/// 获取缓存的是否强制更新
bool get cachedIsForceUpdate => _isForceUpdate ?? false;
/// 获取缓存的更新地址
String get cachedUpdateUrl => _updateUrl ?? '';
/// 获取缓存的游客登录状态
bool get cachedShowTourists => _isShowTourists ?? false;
// 游客登录显示状态 /// 获取上次检查时间
final isShowTourists = false.obs; DateTime? get lastCheckTime => _lastCheckTime;
final Logger _logger = Logger();
/// 获取当前应用版本信息 /// 获取当前应用版本信息
Future<String> getCurrentVersion() async { Future<String> getCurrentVersion() async {
try { try {
final packageInfo = await PackageInfo.fromPlatform(); final packageInfo = await PackageInfo.fromPlatform();
_appVersion = packageInfo.version;
_buildNumber = packageInfo.buildNumber;
return '${packageInfo.version}+${packageInfo.buildNumber}'; return '${packageInfo.version}+${packageInfo.buildNumber}';
} catch (e) { } catch (e) {
_logger.e('获取当前版本信息失败: $e'); _logger.e('获取当前版本信息失败: $e');
@ -31,14 +79,6 @@ class VersionUpdateService extends GetxService {
/// 获取当前平台信息 /// 获取当前平台信息
Future<PlatformInfo> getCurrentPlatformInfo() async { Future<PlatformInfo> getCurrentPlatformInfo() async {
if (kIsWeb) {
return const PlatformInfo(
platform: 'web',
storeType: AppStoreType.official,
channel: UserChannel.OFFICIAL,
);
}
if (Platform.isIOS) { if (Platform.isIOS) {
return const PlatformInfo( return const PlatformInfo(
platform: 'ios', platform: 'ios',
@ -66,10 +106,14 @@ class VersionUpdateService extends GetxService {
final androidInfo = await deviceInfo.androidInfo; final androidInfo = await deviceInfo.androidInfo;
final brand = androidInfo.brand.toLowerCase(); final brand = androidInfo.brand.toLowerCase();
return const PlatformInfo( _deviceBrand = brand;
String channelFromEnv = dotenv.env['Channel'] ?? 'OFFICIAL';
_channel = UserChannel.fromName(channelFromEnv);
return PlatformInfo(
platform: 'android', platform: 'android',
storeType: AppStoreType.official, storeType: AppStoreType.official,
channel: UserChannel.OFFICIAL, channel: _channel ?? UserChannel.OFFICIAL,
); );
// switch (brand) { // switch (brand) {
@ -121,10 +165,27 @@ class VersionUpdateService extends GetxService {
/// 检查版本更新 /// 检查版本更新
Future<VersionUpdateInfo> checkForUpdate() async { Future<VersionUpdateInfo> checkForUpdate() async {
if (_lastCheckTime != null &&
DateTime.now().difference(_lastCheckTime!).inMinutes <
_cacheIntervalMinutes) {
return VersionUpdateInfo(
hasUpdate: _hasUpdate ?? false,
currentVersion: _appVersion ?? '1.0.0',
latestVersion: (_latestVersion ?? '1.0.0').split('+')[0],
updateType: _hasUpdate!
? (_isForceUpdate! ? UpdateType.force : UpdateType.optional)
: UpdateType.none,
);
}
try { try {
final currentVersion = await getCurrentVersion(); final currentVersion = await getCurrentVersion();
final platformInfo = await getCurrentPlatformInfo(); final platformInfo = await getCurrentPlatformInfo();
_platform = platformInfo.platform;
_channel = platformInfo.channel;
_lastCheckTime = DateTime.now();
// 调用后端接口获取最新版本信息 // 调用后端接口获取最新版本信息
final response = final response =
await Api.getChannelApp({'channel': platformInfo.channel.value}); await Api.getChannelApp({'channel': platformInfo.channel.value});
@ -144,9 +205,11 @@ class VersionUpdateService extends GetxService {
// 直接从response中获取app信息 // 直接从response中获取app信息
final appInfo = AppInfo.fromJson(response['app']); final appInfo = AppInfo.fromJson(response['app']);
_logger.d('获取到的最新版本信息: ${appInfo.toJson()}'); _logger.d('获取到的最新版本信息: ${appInfo.toJson()}');
_latestVersion = appInfo.version;
_updateUrl = appInfo.address;
// 设置游客登录显示状态 // 设置游客登录显示状态
isShowTourists.value = appInfo.tourists; _isShowTourists = appInfo.tourists;
_logger.d('从服务器获取游客登录配置: ${appInfo.tourists}'); _logger.d('从服务器获取游客登录配置: ${appInfo.tourists}');
// if (channelAppResponse.code != 0 || channelAppResponse.data == null) { // if (channelAppResponse.code != 0 || channelAppResponse.data == null) {
@ -160,18 +223,17 @@ class VersionUpdateService extends GetxService {
// } // }
// final appInfo = channelAppResponse.data!.app; // final appInfo = channelAppResponse.data!.app;
final hasUpdate = _compareVersions(currentVersion, appInfo.version); _hasUpdate = _compareVersions(currentVersion, appInfo.version);
// 判断更新类型:如果主版本号不同则强制更新 // 判断更新类型:如果主版本号不同则强制更新
UpdateType updateType = UpdateType.none; UpdateType updateType = UpdateType.none;
if (hasUpdate) { if (_hasUpdate!) {
final isForceUpdate = _isForceUpdate = _shouldForceUpdate(currentVersion, appInfo.version);
_shouldForceUpdate(currentVersion, appInfo.version); updateType = _isForceUpdate! ? UpdateType.force : UpdateType.optional;
updateType = isForceUpdate ? UpdateType.force : UpdateType.optional;
} }
return VersionUpdateInfo( return VersionUpdateInfo(
hasUpdate: hasUpdate, hasUpdate: _hasUpdate!,
currentVersion: currentVersion.split('+')[0], currentVersion: currentVersion.split('+')[0],
latestVersion: appInfo.version.split('+')[0], latestVersion: appInfo.version.split('+')[0],
appInfo: appInfo, appInfo: appInfo,
@ -180,7 +242,7 @@ class VersionUpdateService extends GetxService {
} catch (e) { } catch (e) {
_logger.e('检查版本更新失败: $e'); _logger.e('检查版本更新失败: $e');
// 发生错误时,保持默认值 false // 发生错误时,保持默认值 false
isShowTourists.value = false; _isShowTourists = false;
final currentVersion = await getCurrentVersion(); final currentVersion = await getCurrentVersion();
return VersionUpdateInfo( return VersionUpdateInfo(
hasUpdate: false, hasUpdate: false,
@ -270,84 +332,16 @@ class VersionUpdateService extends GetxService {
return false; return false;
} }
final platformInfo = await getCurrentPlatformInfo();
try { try {
switch (platformInfo.storeType) { return await _openDownloadUrl(updateInfo.appInfo!.address!);
case AppStoreType.appStore:
return await _openAppStore();
case AppStoreType.googlePlay:
return await _openGooglePlay();
case AppStoreType.huawei:
return await _openHuaweiStore();
case AppStoreType.xiaomi:
return await _openXiaomiStore();
case AppStoreType.oppo:
return await _openOppoStore();
case AppStoreType.vivo:
return await _openVivoStore();
case AppStoreType.honor:
return await _openHonorStore();
case AppStoreType.official:
return await _openOfficialDownload(updateInfo.appInfo!.address!);
default:
return await _openOfficialDownload(updateInfo.appInfo!.address!);
}
} catch (e) { } catch (e) {
_logger.e('执行更新失败: $e'); _logger.e('执行更新失败: $e');
return false; return false;
} }
} }
/// 打开iOS App Store /// 打开下载链接
Future<bool> _openAppStore() async { Future<bool> _openDownloadUrl(String downloadUrl) async {
// 这里需要替换为实际的App Store ID
const appStoreUrl = 'https://apps.apple.com/app/id你的应用ID';
return await _launchUrl(appStoreUrl);
}
/// 打开Google Play
Future<bool> _openGooglePlay() async {
// 这里需要替换为实际的包名
const packageName = 'com.yourcompany.deepvoice';
const playStoreUrl =
'https://play.google.com/store/apps/details?id=$packageName';
return await _launchUrl(playStoreUrl);
}
/// 打开华为应用市场
Future<bool> _openHuaweiStore() async {
const huaweiStoreUrl = 'appmarket://details?id=com.yourcompany.deepvoice';
return await _launchUrl(huaweiStoreUrl);
}
/// 打开小米应用商店
Future<bool> _openXiaomiStore() async {
const xiaomiStoreUrl = 'mimarket://details?id=com.yourcompany.deepvoice';
return await _launchUrl(xiaomiStoreUrl);
}
/// 打开OPPO软件商店
Future<bool> _openOppoStore() async {
const oppoStoreUrl =
'oppomarket://details?packagename=com.yourcompany.deepvoice';
return await _launchUrl(oppoStoreUrl);
}
/// 打开vivo应用商店
Future<bool> _openVivoStore() async {
const vivoStoreUrl = 'vivomarket://details?id=com.yourcompany.deepvoice';
return await _launchUrl(vivoStoreUrl);
}
/// 打开荣耀应用市场
Future<bool> _openHonorStore() async {
const honorStoreUrl = 'honormarket://details?id=com.yourcompany.deepvoice';
return await _launchUrl(honorStoreUrl);
}
/// 打开官网下载
Future<bool> _openOfficialDownload(String downloadUrl) async {
return await _launchUrl(downloadUrl); return await _launchUrl(downloadUrl);
} }

33
lib/main.dart

@ -1,5 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_intro/flutter_intro.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart';
@ -8,8 +10,6 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'core/bindings/initial_binding.dart'; import 'core/bindings/initial_binding.dart';
import 'routes/app_pages.dart'; import 'routes/app_pages.dart';
import 'routes/app_routes.dart'; import 'routes/app_routes.dart';
import 'dart:io';
import 'package:window_size/window_size.dart';
import 'core/translations/app_translations.dart'; import 'core/translations/app_translations.dart';
import 'core/themes/app_theme.dart'; import 'core/themes/app_theme.dart';
import 'core/themes/theme_service.dart'; import 'core/themes/theme_service.dart';
@ -19,6 +19,8 @@ void main() async {
// 确保Flutter绑定初始化 // 确保Flutter绑定初始化
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
// 初始化存储 - 在创建应用之前初始化 // 初始化存储 - 在创建应用之前初始化
await GetStorage.init(); await GetStorage.init();
@ -28,28 +30,6 @@ void main() async {
// 初始化登录服务(包含Firebase初始化) // 初始化登录服务(包含Firebase初始化)
LoginService(); LoginService();
// 设置窗口大小(仅在 macOS 下生效)
if (Platform.isMacOS) {
const double width = 375.0;
const double height = 812.0;
setWindowTitle('Voitrans');
setWindowMinSize(const Size(width, height));
setWindowMaxSize(const Size(width, height));
// 居中显示窗口
final screen = await getCurrentScreen();
if (screen != null) {
final screenFrame = screen.frame;
final windowFrame = Rect.fromCenter(
center: screenFrame.center,
width: width,
height: height,
);
setWindowFrame(windowFrame);
}
}
// 运行应用 // 运行应用
runApp(const MainApp()); runApp(const MainApp());
} }
@ -92,7 +72,10 @@ class MainApp extends StatelessWidget {
data: MediaQuery.of(context).copyWith( data: MediaQuery.of(context).copyWith(
textScaler: TextScaler.noScaling, textScaler: TextScaler.noScaling,
), ),
child: widget!, child: Intro(
padding: EdgeInsets.zero,
child: widget!,
),
); );
}, },
), ),

136
lib/modules/agent/controllers/agent_controller.dart

@ -3,6 +3,9 @@ import 'dart:convert';
import 'dart:ffi'; import 'dart:ffi';
import 'dart:io'; import 'dart:io';
import 'package:amap_service/amap_service.dart'; import 'package:amap_service/amap_service.dart';
import '../../../data/services/ble_manager.dart';
import '../../../data/services/bluetooth_manager.dart';
import '../../../data/services/novice_guide_service.dart';
import '/core/utils/upload_oss.dart'; import '/core/utils/upload_oss.dart';
import '/data/models/user_Info.dart'; import '/data/models/user_Info.dart';
import '/data/services/music_manager.dart'; import '/data/services/music_manager.dart';
@ -39,6 +42,11 @@ class AgentController extends GetxController {
final TextEditingController textController = TextEditingController(); final TextEditingController textController = TextEditingController();
final ScrollController scrollController = ScrollController(); final ScrollController scrollController = ScrollController();
final noviceGuideService = Get.find<NoviceGuideService>();
// 确保服务已初始化
// ========== 依赖服务 ==========
final BluetoothManager _bluetoothManager = Get.find<BluetoothManager>();
final BleManager _bleManager = Get.find<BleManager>();
//音乐播放管理 //音乐播放管理
final _musiceManager = Get.find<MusicManager>(); final _musiceManager = Get.find<MusicManager>();
@ -118,7 +126,7 @@ class AgentController extends GetxController {
String _currentResponseId = ''; String _currentResponseId = '';
final TAG = 'AgentController'; final TAG = 'AgentController';
var AppDir = "".obs;
// 会话ID // 会话ID
final String sessionId = 'default_agent'; final String sessionId = 'default_agent';
@ -145,6 +153,7 @@ class AgentController extends GetxController {
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
_initAppDir();
_loadTtsPreference(); //加载TTS偏好设置 _loadTtsPreference(); //加载TTS偏好设置
_setupScrollListener(); //设置滚动监听器 _setupScrollListener(); //设置滚动监听器
_loadChatHistory(); //加载聊天历史记录 _loadChatHistory(); //加载聊天历史记录
@ -153,11 +162,13 @@ class AgentController extends GetxController {
_setupMediaSessionCallbacks(); //设置媒体会话回调 _setupMediaSessionCallbacks(); //设置媒体会话回调
_setupTextInputListener(); //设置文本输入监听器 _setupTextInputListener(); //设置文本输入监听器
startVoiceInput(); //开启语音输入 startVoiceInput(); //开启语音输入
} }
Future<void> _initAppDir() async {
AppDir.value = (await getApplicationDocumentsDirectory()).path;
}
// 初始化音乐播放状态 // 初始化音乐播放状态
Future<void> _initMusicStatus() async { Future<void> _initMusicStatus() async {
try { try {
@ -285,7 +296,7 @@ class AgentController extends GetxController {
page: 1, //从第一页开始 page: 1, //从第一页开始
pageSize: pageSize, //每页20条记录 pageSize: pageSize, //每页20条记录
); );
Logger.i(TAG, '加载聊天历史记录: $result');
if (result.messages.isNotEmpty) { if (result.messages.isNotEmpty) {
// 转换并添加消息到列表 // 转换并添加消息到列表
final historyMessages = _convertChatMessagesToMessages(result.messages); final historyMessages = _convertChatMessagesToMessages(result.messages);
@ -297,7 +308,8 @@ class AgentController extends GetxController {
// 更新分页状态 // 更新分页状态
currentPage.value = 1; currentPage.value = 1;
hasMoreMessages.value = result.totalPages > 1; hasMoreMessages.value = result.totalPages > 1;
print(
'liwei-------------读取历史记录 ${AppDir.value}${historyMessages[0].imagePath}');
Logger.i(TAG, Logger.i(TAG,
'成功加载 ${historyMessages.length} 条聊天记录,总页数: ${result.totalPages}'); '成功加载 ${historyMessages.length} 条聊天记录,总页数: ${result.totalPages}');
_logMessageDetails(historyMessages); _logMessageDetails(historyMessages);
@ -415,7 +427,7 @@ class AgentController extends GetxController {
'meta: ${msg.meta != null ? "有" : "无"}' 'meta: ${msg.meta != null ? "有" : "无"}'
'}'); '}');
} }
Logger.d(TAG, '===== 历史消息详情结束 ====='); Logger.d(TAG, '===== 历史消息详情结束 ==lishi===');
} }
// 设置滚动监听器 // 设置滚动监听器
@ -557,16 +569,16 @@ class AgentController extends GetxController {
break; break;
case AgentServiceEventType.ttsStarted: case AgentServiceEventType.ttsStarted:
// isSpeaking.value = true; isSpeaking.value = true;
// Logger.i(TAG, 'TTS开始播放'); print('TTS开始播放');
break; break;
case AgentServiceEventType.ttsCompleted: case AgentServiceEventType.ttsCompleted:
case AgentServiceEventType.ttsStopped: case AgentServiceEventType.ttsStopped:
case AgentServiceEventType.ttsCanceled: case AgentServiceEventType.ttsCanceled:
// isSpeaking.value = false; isSpeaking.value = false;
// Logger.i(TAG, 'TTS停止播放, $event.type'); Logger.i(TAG, 'TTS停止播放, $event.type');
break; break;
case AgentServiceEventType.playbackStarted: case AgentServiceEventType.playbackStarted:
@ -897,10 +909,14 @@ class AgentController extends GetxController {
} }
// 复制图片到应用永久存储目录 // 复制图片到应用永久存储目录
final permanentImagePath = await _copyImageToPermanentStorage(imagePath); // 复制图片到应用永久存储目录
final imagePathResult = await _copyImageToPermanentStorage(imagePath);
final permanentImagePath = imagePathResult['fullPath']!;
final relativeImagePath = imagePathResult['relativePath']!;
var ImageUrl = await UploadOss.upload( var ImageUrl = await UploadOss.upload(
filepath: imagePath, rootDir: 'User/${User.instance.uid}/ChatImages'); filepath: permanentImagePath,
Logger.i(TAG, '图片上传: $imagePath URL: $ImageUrl'); rootDir: 'User/${User.instance.uid}/ChatImages');
Logger.i(TAG, '图片上传: $permanentImagePath URL: $ImageUrl');
// 显示的文本,如果没有提供则使用默认值 // 显示的文本,如果没有提供则使用默认值
final displayText = text?.isNotEmpty == true ? text! : '[图片]'; final displayText = text?.isNotEmpty == true ? text! : '[图片]';
@ -909,7 +925,7 @@ class AgentController extends GetxController {
isUser: true, isUser: true,
text: displayText, text: displayText,
hasImage: true, hasImage: true,
imagePath: permanentImagePath, imagePath: relativeImagePath,
); );
messages.add(message); messages.add(message);
@ -929,7 +945,7 @@ class AgentController extends GetxController {
// 调用Agent Service处理图片,考虑TTS状态 // 调用Agent Service处理图片,考虑TTS状态
await AgentService.processImageInput( await AgentService.processImageInput(
permanentImagePath, relativeImagePath,
ImageUrl, ImageUrl,
text: text ?? '', text: text ?? '',
speakResponse: isTtsEnabled.value, speakResponse: isTtsEnabled.value,
@ -942,12 +958,16 @@ class AgentController extends GetxController {
} }
// 将图片复制到应用的永久存储目录 // 将图片复制到应用的永久存储目录
Future<String> _copyImageToPermanentStorage(String sourcePath) async { Future<Map<String, String>> _copyImageToPermanentStorage(
String sourcePath) async {
try { try {
final File sourceFile = File(sourcePath); final File sourceFile = File(sourcePath);
if (!await sourceFile.exists()) { if (!await sourceFile.exists()) {
Logger.e(TAG, '源图片文件不存在: $sourcePath'); Logger.e(TAG, '源图片文件不存在: $sourcePath');
return sourcePath; // 如果源文件不存在,返回原路径 return {
'fullPath': sourcePath,
'relativePath': sourcePath
}; // 如果源文件不存在,返回原路径
} }
// 创建应用文档目录下的chatImages文件夹 // 创建应用文档目录下的chatImages文件夹
@ -961,15 +981,16 @@ class AgentController extends GetxController {
final fileName = final fileName =
'${DateTime.now().millisecondsSinceEpoch}_${sourcePath.split('/').last}'; '${DateTime.now().millisecondsSinceEpoch}_${sourcePath.split('/').last}';
final targetPath = '${imagesDir.path}/$fileName'; final targetPath = '${imagesDir.path}/$fileName';
final relativePath = '/chatImages/$fileName';
// 复制文件 // 复制文件
await sourceFile.copy(targetPath); await sourceFile.copy(targetPath);
Logger.i(TAG, '图片已复制到永久存储: $targetPath'); Logger.i(TAG, '图片已复制到永久存储: $targetPath');
return targetPath; return {'fullPath': targetPath, 'relativePath': relativePath};
} catch (e) { } catch (e) {
Logger.e(TAG, '复制图片到永久存储失败: $e'); Logger.e(TAG, '复制图片到永久存储失败: $e');
return sourcePath; // 出错时返回原路径 return {'fullPath': sourcePath, 'relativePath': sourcePath}; // 出错时返回原路径
} }
} }
@ -1013,8 +1034,6 @@ class AgentController extends GetxController {
} }
} }
// 开始语音输入 // 开始语音输入
Future<void> startVoiceInput() async { Future<void> startVoiceInput() async {
if (isListening.value) return; if (isListening.value) return;
@ -1036,6 +1055,11 @@ class AgentController extends GetxController {
try { try {
await AgentService.recognizeCallback(); await AgentService.recognizeCallback();
if (_bluetoothManager.currentDeviceRx.value != null) {
// await AgentService.restoreOriginalAudioState();
} else {
// await AgentService.disableBluetoothAudio();
}
} catch (e) { } catch (e) {
Logger.e(TAG, '启动识别器失败: $e'); Logger.e(TAG, '启动识别器失败: $e');
} }
@ -1309,29 +1333,71 @@ class AgentController extends GetxController {
return; return;
} }
// 切换到语音模式并开始语音输入 // 打电话模式
isTextInputMode.value = false; isTextInputMode.value = false;
// 打电话模式不使用按住说话模式,而是持续语音输入 Logger.i(TAG, '打电话模式已启动,开始语音输入');
//isPushToTalkMode.value = false;
// 开始语音识别
Logger.i(TAG, '开始打电话模式,开始语音输入');
await AgentService.startConversation(); await AgentService.startConversation();
//}
} }
// 结束打电话模式 // 结束打电话模式
void endPhoneCall() async { void endPhoneCall() async {
Logger.i(TAG, '🔥 结束打电话模式'); Logger.i(TAG, '🔥 结束打电话模式');
// 退出打电话模式 try {
isPhoneCallMode.value = false; // 如果正在录音,先停止录音
if (isListening.value) {
final result = await AgentService.stopConversation();
// 停止语音输入 if (result) {
//await stopVoiceInput(); isListening.value = false;
// 开始语音识别 }
await AgentService.stopConversation(); }
// 恢复之前的输入模式,而不是强制切换到文字模式
_restorePreviousInputMode(); // 退出打电话模式
isPhoneCallMode.value = false;
_restorePreviousInputMode();
} catch (e) {
Logger.e(TAG, '结束打电话模式失败: $e');
// 即使出现异常,也要确保退出打电话模式
isPhoneCallMode.value = false;
isListening.value = false;
_restorePreviousInputMode();
}
}
// 打电话模式专用:切换录音状态
Future<void> togglePhoneCallRecording() async {
Logger.i(TAG, '🎤 打电话模式切换录音状态');
// 只有在打电话模式下才能使用此方法
if (!isPhoneCallMode.value) {
Logger.w(TAG, '不在打电话模式,无法切换录音状态');
return;
}
try {
if (isListening.value) {
// 当前正在录音,停止录音
Logger.i(TAG, '停止打电话模式录音');
final result = await AgentService.stopConversation();
// 手动更新状态,确保UI同步
if (result) {
isListening.value = false;
}
} else {
// 当前未录音,开始录音
Logger.i(TAG, '开始打电话模式录音');
final result = await AgentService.startConversation();
// 手动更新状态,确保UI同步
if (result) {
isListening.value = true;
}
}
} catch (e) {
Logger.e(TAG, '打电话模式录音状态切换失败: $e');
}
} }
// 耳机唤醒通话模式(专用入口) // 耳机唤醒通话模式(专用入口)

756
lib/modules/agent/views/agent_view.dart

@ -2,7 +2,9 @@ import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'dart:math'; import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_intro/flutter_intro.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../../../core/widgets/intro_overlay_builder.dart';
import '../controllers/agent_controller.dart'; import '../controllers/agent_controller.dart';
import '../controllers/message_action_controller.dart'; import '../controllers/message_action_controller.dart';
import 'message_bubble.dart'; import 'message_bubble.dart';
@ -196,47 +198,79 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
children: [ children: [
// 聊天内容 // 聊天内容
Expanded( Expanded(
child: Obx( child: Stack(
() => Align( children: [
alignment: Alignment.topCenter, IntroStepBuilder(
child: ListView.builder( group: 'ai',
controller: controller.scrollController, order: 3,
shrinkWrap: true, overlayBuilder: (params) {
reverse: true, return IntroOverlayBuilder.aiBuilder(
padding: EdgeInsets.symmetric( params,
vertical: 16.h, horizontal: 16.w), controller.noviceGuideService.aiStepsList[
itemCount: controller.messages.length + controller.noviceGuideService.aiStep]['end'],
(controller.hasMoreMessages.value ? 1 : 0), onFinish: () async {
itemBuilder: (context, index) { if (controller.noviceGuideService.aiStep < 5) {
// 如果是最后一项且有更多消息,显示加载指示器 await Future.delayed(
if (index == controller.messages.length && const Duration(milliseconds: 300));
controller.hasMoreMessages.value) { controller.noviceGuideService.aiStep++;
return _buildLoadMoreIndicator(); Intro.of(Get.context!)
} .start(group: 'ai', reset: true);
}
final message = controller },
.messages[controller.messages.length - 1 - index]; );
return Padding(
padding: EdgeInsets.only(bottom: 8.h),
child: GestureDetector(
onLongPressStart:
(LongPressStartDetails details) {
messageActionController.onLongPressMessage(
context, isDarkMode, details, message);
},
child: MessageBubble(
isUser: message.isUser,
message: message.text,
timestamp: message.timestamp,
isRecognizing: message.isRecognizing,
hasImage: message.hasImage,
imagePath: message.imagePath,
meta: message.meta,
),
));
}, },
builder: (BuildContext context, GlobalKey key) => Align(
alignment: Alignment.bottomCenter,
child: SizedBox(
key: key,
width: double.infinity,
height: 430.w,
),
),
), ),
), Obx(
() => Align(
alignment: Alignment.topCenter,
child: ListView.builder(
controller: controller.scrollController,
shrinkWrap: true,
reverse: true,
padding: EdgeInsets.symmetric(
vertical: 16.h, horizontal: 16.w),
itemCount: controller.messages.length +
(controller.hasMoreMessages.value ? 1 : 0),
itemBuilder: (context, index) {
// 如果是最后一项且有更多消息,显示加载指示器
if (index == controller.messages.length &&
controller.hasMoreMessages.value) {
return _buildLoadMoreIndicator();
}
final message = controller.messages[
controller.messages.length - 1 - index];
return Padding(
padding: EdgeInsets.only(bottom: 8.h),
child: GestureDetector(
onLongPressStart:
(LongPressStartDetails details) {
messageActionController.onLongPressMessage(
context, isDarkMode, details, message);
},
child: MessageBubble(
isUser: message.isUser,
message: message.text,
timestamp: message.timestamp,
isRecognizing: message.isRecognizing,
hasImage: message.hasImage,
imagePath: message.imagePath,
meta: message.meta,
),
));
},
),
),
),
],
), ),
), ),
@ -388,96 +422,127 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
if (controller.isTextInputMode.value) { if (controller.isTextInputMode.value) {
// 文本输入模式 - 限制最大高度并只向上扩展 // 文本输入模式 - 限制最大高度并只向上扩展
return Container( return IntroStepBuilder(
constraints: BoxConstraints( group: 'ai',
minHeight: 44.h, order: 1,
maxHeight: 120.h, overlayBuilder: (params) {
), return IntroOverlayBuilder.aiBuilder(
padding: EdgeInsets.symmetric( params,
horizontal: 16.w), controller.noviceGuideService
decoration: BoxDecoration( .aiStepsList[
color: isDarkMode controller.noviceGuideService
? Colors.grey[800] .aiStep]['start'],
: Colors.grey[200], buttonText: '确定输入',
borderRadius: onNext: () {
BorderRadius.circular(22.w), controller.textController.text =
border: Border.all( controller.noviceGuideService
.aiStepsList[
controller
.noviceGuideService
.aiStep]['input'];
},
);
},
onWidgetLoad: () {
controller.noviceGuideService
.startAIGuide();
},
builder: (BuildContext context,
GlobalKey key) =>
Container(
key: key,
constraints: BoxConstraints(
minHeight: 44.h,
maxHeight: 120.h,
),
padding: EdgeInsets.symmetric(
horizontal: 16.w),
decoration: BoxDecoration(
color: isDarkMode color: isDarkMode
? Colors.grey[800]! ? Colors.grey[800]
: Colors.grey.withOpacity(0.1), : Colors.grey[200],
width: 1.w, borderRadius:
BorderRadius.circular(22.w),
border: Border.all(
color: isDarkMode
? Colors.grey[800]!
: Colors.grey.withOpacity(0.1),
width: 1.w,
),
), ),
), child: Row(
child: Row( crossAxisAlignment: CrossAxisAlignment
crossAxisAlignment: CrossAxisAlignment .end, // 对齐到底部,实现向上扩展
.end, // 对齐到底部,实现向上扩展 children: [
children: [ Expanded(
Expanded( child: Obx(() {
child: Obx(() { // 图片模式下使用更高的输入框
// 图片模式下使用更高的输入框 final isImageActive = controller
final isImageActive = controller .isImageInputActive.value;
.isImageInputActive.value;
return TextField( return TextField(
controller: controller:
controller.textController, controller.textController,
maxLines: isImageActive maxLines: isImageActive
? 3 ? 3
: 5, // 设置合理的最大行数 : 5, // 设置合理的最大行数
minLines: isImageActive ? 2 : 1, minLines:
keyboardType: TextInputType isImageActive ? 2 : 1,
.multiline, // 启用多行键盘 keyboardType: TextInputType
textInputAction: TextInputAction .multiline, // 启用多行键盘
.newline, // 支持回车换行 textInputAction:
onTap: () { TextInputAction
// 当点击输入框时,如果'+'选项已展开,则收缩 .newline, // 支持回车换行
if (controller onTap: () {
.isPlusOptionsExpanded // 当点击输入框时,如果'+'选项已展开,则收缩
.value) { if (controller
controller .isPlusOptionsExpanded
.collapsePlusOptions(); .value) {
} controller
}, .collapsePlusOptions();
style: TextStyle( }
fontSize: 15.sp, },
color: isDarkMode style: TextStyle(
? Colors.white
: Colors.grey[800],
),
decoration: InputDecoration(
hintText: isImageActive
? 'addQuestionAboutImage'
.tr // 添加对图片的问题或描述...
: 'inputMessage'
.tr, // 输入消息...
hintStyle: TextStyle(
color: Colors.grey[500],
fontSize: 15.sp, fontSize: 15.sp,
color: isDarkMode
? Colors.white
: Colors.grey[800],
), ),
filled: false, decoration: InputDecoration(
border: InputBorder.none, hintText: isImageActive
focusedBorder: ? 'addQuestionAboutImage'
OutlineInputBorder( .tr // 添加对图片的问题或描述...
borderSide: BorderSide( : 'inputMessage'
color: isDarkMode .tr, // 输入消息...
? Colors.grey[800]! hintStyle: TextStyle(
: Colors.grey[200]!, color: Colors.grey[500],
width: 1.w, fontSize: 15.sp,
),
filled: false,
border: InputBorder.none,
focusedBorder:
OutlineInputBorder(
borderSide: BorderSide(
color: isDarkMode
? Colors.grey[800]!
: Colors.grey[200]!,
width: 1.w,
),
), ),
contentPadding:
EdgeInsets.symmetric(
vertical:
isImageActive
? 12.h
: 8.h),
), ),
contentPadding: onSubmitted: (_) =>
EdgeInsets.symmetric( controller.sendMessage(),
vertical: );
isImageActive }),
? 12.h ),
: 8.h), ],
), ),
onSubmitted: (_) =>
controller.sendMessage(),
);
}),
),
],
), ),
); );
} else { } else {
@ -615,111 +680,143 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
SizedBox(width: 8.w), SizedBox(width: 8.w),
// 动态按钮区域:展开按钮 + 发送按钮 或 图片选择按钮 // 动态按钮区域:展开按钮 + 发送按钮 或 图片选择按钮
Obx(() { Builder(builder: (context) {
final hasText = controller.hasTextInput.value; StepWidgetParams? widgetParams;
final hasImage = return IntroStepBuilder(
controller.isImageInputActive.value; group: 'ai',
final shouldShowSend = hasText || hasImage; order: 2,
final shouldShowExpand = onHighlightWidgetTap: () {
controller.shouldShowExpandButton.value; widgetParams!.onNext!();
controller.sendMessage();
},
overlayBuilder: (params) {
widgetParams = params;
return IntroOverlayBuilder.aiBuilder(
params,
'点击发送,${controller.noviceGuideService.aiStepsList[controller.noviceGuideService.aiStep]['send']}',
alignment: false,
);
},
builder:
(BuildContext context, GlobalKey key) =>
Obx(
key: key,
() {
final hasText =
controller.hasTextInput.value;
final hasImage =
controller.isImageInputActive.value;
final shouldShowSend =
hasText || hasImage;
final shouldShowExpand = controller
.shouldShowExpandButton.value;
if (shouldShowSend) { if (shouldShowSend) {
// 显示发送按钮(可能还有展开按钮) // 显示发送按钮(可能还有展开按钮)
return Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
// 展开按钮(在发送按钮上方) // 展开按钮(在发送按钮上方)
if (shouldShowExpand && hasText) if (shouldShowExpand && hasText)
Padding( Padding(
padding: padding: EdgeInsets.only(
EdgeInsets.only(bottom: 40.h), bottom: 40.h),
child: GestureDetector( child: GestureDetector(
onTap: () {
// 打开底部弹出编辑模式
controller
.openFullScreenEdit();
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor:
Colors.transparent,
builder: (context) =>
const FullScreenEditor(),
).then((_) {
// 弹出页关闭时的回调
controller
.closeFullScreenEdit();
});
},
child: Container(
width: 32.w,
height: 32.w,
decoration: BoxDecoration(
color: isDarkMode
? Colors.grey[700]
: Colors.grey[300],
borderRadius:
BorderRadius.circular(
16.w),
),
child: Icon(
Icons.expand_less,
color: isDarkMode
? Colors.grey[300]
: Colors.grey[700],
size: 20.sp,
),
),
),
),
// 发送按钮
GestureDetector(
onTap: () =>
controller.sendMessage(),
child: Container(
width: 44.w,
height: 44.w,
decoration: BoxDecoration(
color: primaryColor,
borderRadius:
BorderRadius.circular(
22.w),
),
child: Icon(
Icons.send,
color: Colors.white,
size: 22.sp,
),
),
),
],
);
} else {
// 显示'+'选择按钮
return Obx(() {
final isExpanded = controller
.isPlusOptionsExpanded.value;
return GestureDetector(
onTap: () { onTap: () {
// 打开底部弹出编辑模式 controller.togglePlusOptions();
controller.openFullScreenEdit();
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor:
Colors.transparent,
builder: (context) =>
const FullScreenEditor(),
).then((_) {
// 弹出页关闭时的回调
controller
.closeFullScreenEdit();
});
}, },
child: Container( child: Container(
width: 32.w, width: 44.w,
height: 32.w, height: 44.w,
decoration: BoxDecoration( decoration: BoxDecoration(
color: isDarkMode color: isDarkMode
? Colors.grey[700] ? Colors.grey[800]
: Colors.grey[300], : Colors.grey[200],
borderRadius: borderRadius:
BorderRadius.circular(16.w), BorderRadius.circular(22.w),
), ),
child: Icon( child: Icon(
Icons.expand_less, isExpanded
? Icons.close
: Icons.add,
color: isDarkMode color: isDarkMode
? Colors.grey[300] ? Colors.grey[300]
: Colors.grey[700], : Colors.grey[700],
size: 20.sp, size: 22.sp,
), ),
), ),
), );
), });
// 发送按钮 }
GestureDetector( },
onTap: () => controller.sendMessage(), ),
child: Container( );
width: 44.w,
height: 44.w,
decoration: BoxDecoration(
color: primaryColor,
borderRadius:
BorderRadius.circular(22.w),
),
child: Icon(
Icons.send,
color: Colors.white,
size: 22.sp,
),
),
),
],
);
} else {
// 显示'+'选择按钮
return Obx(() {
final isExpanded =
controller.isPlusOptionsExpanded.value;
return GestureDetector(
onTap: () {
controller.togglePlusOptions();
},
child: Container(
width: 44.w,
height: 44.w,
decoration: BoxDecoration(
color: isDarkMode
? Colors.grey[800]
: Colors.grey[200],
borderRadius:
BorderRadius.circular(22.w),
),
child: Icon(
isExpanded ? Icons.close : Icons.add,
color: isDarkMode
? Colors.grey[300]
: Colors.grey[700],
size: 22.sp,
),
),
);
});
}
}), }),
], ],
), ),
@ -919,212 +1016,60 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
Widget _buildPhoneCallOverlay(BuildContext context, bool isDarkMode) { Widget _buildPhoneCallOverlay(BuildContext context, bool isDarkMode) {
return Container( return Container(
color: Colors.transparent, // 设置整个覆盖层为透明 color: Colors.transparent, // 设置整个覆盖层为透明
child: SafeArea( child: Column(
child: Column(
children: [
// 顶部标题栏 - 保持背景
// Container(
// color: isDarkMode
// ? Colors.black.withOpacity(0.8)
// : Colors.white.withOpacity(0.8),
// child: _buildPhoneCallHeader(isDarkMode),
// ),
// 中间音频可视化区域 - 完全透明
const Expanded(
child: SizedBox(), // 空的占位符,完全透明
),
// 底部控制按钮 - 保持背景
Container(
color: isDarkMode ? Colors.black : Colors.white,
child: _buildPhoneCallControls(isDarkMode),
),
],
),
),
);
}
// 构建打电话模式标题栏
Widget _buildPhoneCallHeader(bool isDarkMode) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
children: [ children: [
// 返回按钮 // 中间空白区域 - 完全透明
GestureDetector( const Expanded(
onTap: () => controller.endPhoneCall(), child: SizedBox(), // 空的占位符,完全透明
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[800] : Colors.grey[200],
borderRadius: BorderRadius.circular(20),
),
child: Icon(
Icons.arrow_back,
color: isDarkMode ? Colors.white : Colors.black,
size: 20,
),
),
), ),
const SizedBox(width: 16), // 底部控制按钮 - 包含音频波纹
Container(
// 标题 color: isDarkMode ? Colors.black : Colors.white,
Expanded( child: _buildPhoneCallControls(isDarkMode),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'aiVoiceCall'.tr, // AI语音通话
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.black,
),
),
Obx(() {
final status = controller.isListening.value
? 'inCall'.tr // 正在通话中...
: controller.isSpeaking.value
? 'aiReplying'.tr // AI正在回复...
: 'waitingToStart'.tr; // 等待开始...
return Text(
status,
style: TextStyle(
fontSize: 14,
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
),
);
}),
],
),
), ),
], ],
), ),
); );
} }
// 构建打电话模式音频可视化
Widget _buildPhoneCallVisualization(bool isDarkMode) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 音频可视化圆圈
Obx(() {
final isActive =
controller.isListening.value || controller.isSpeaking.value;
return AnimatedContainer(
duration: const Duration(milliseconds: 300),
width: isActive ? 200 : 160,
height: isActive ? 200 : 160,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: isDarkMode
? Colors.blue[900]?.withOpacity(0.3)
: Colors.blue[100],
border: Border.all(
color: Colors.blue,
width: isActive ? 3 : 2,
),
),
child: Icon(
Icons.phone,
size: isActive ? 80 : 60,
color: Colors.blue,
),
);
}),
const SizedBox(height: 32),
// 状态文字
Obx(() {
String statusText = 'clickToStartCall'.tr; // 点击开始通话
if (controller.isListening.value) {
statusText = 'listeningToYou'.tr; // 正在听您说话...
} else if (controller.isSpeaking.value) {
statusText = 'aiReplying'.tr; // AI正在回复...
} else if (controller.isProcessing.value) {
statusText = 'processing'.tr; // 正在处理...
}
return Text(
statusText,
style: TextStyle(
fontSize: 16,
color: isDarkMode ? Colors.grey[300] : Colors.grey[700],
fontWeight: FontWeight.w500,
),
);
}),
],
),
);
}
// 构建打电话模式控制按钮 // 构建打电话模式控制按钮
Widget _buildPhoneCallControls(bool isDarkMode) { Widget _buildPhoneCallControls(bool isDarkMode) {
return Container( return Container(
padding: EdgeInsets.symmetric(horizontal: 32.w, vertical: 24.h), padding: EdgeInsets.symmetric(horizontal: 32.w, vertical: 24.h),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.center, // 确保垂直居中对齐
children: [ children: [
// 静音按钮 // 录音控制按钮(左侧) - 控制录音输入功能
Obx(() => _buildPhoneControlButton( Obx(() => _buildPhoneControlButton(
icon: controller.isTtsEnabled.value icon: controller.isListening.value
? Icons.volume_up ? Icons.mic // 录音中显示麦克风图标
: Icons.volume_off, : Icons.mic_off, // 未录音显示麦克风关闭图标
onTap: () => controller.toggleTts(), onTap: () => controller.togglePhoneCallRecording(),
isDarkMode: isDarkMode, isDarkMode: isDarkMode,
isActive: controller.isTtsEnabled.value, isActive: controller.isListening.value,
// backgroundColor: controller.isListening.value
// ? Colors.red // 录音中显示红色背景
// : Theme.of(context).primaryColor, // 未录音显示主题色
)), )),
// 主要通话按钮 // 音频波纹组件(中间) - 仅显示动态波形效果,无其他UI元素
Obx(() { Container(
final isActive = controller.isListening.value; width: 120.w, // 设置固定宽度
return GestureDetector( height: 64.h, // 与按钮高度一致
// onTap: () { alignment: Alignment.center, // 确保波纹居中
// if (isActive) { child: const AgentSoundBarsWidget(barCount: 12), // 适应宽度的波纹条数
// controller.stopVoiceInput(); ),
// } else {
// controller.startVoiceInput();
// }
// },
child: Container(
width: 80.w,
height: 80.h,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: isActive ? Colors.red : Colors.green,
boxShadow: [
BoxShadow(
color: (isActive ? Colors.red : Colors.green)
.withOpacity(0.3),
blurRadius: 12.w,
spreadRadius: 2.w,
),
],
),
child: Icon(
isActive ? Icons.call_end : Icons.call,
color: Colors.white,
size: 36.sp,
),
),
);
}),
// 结束通话按钮 // 关闭打电话模式按钮(右侧) - 退出打电话模式
_buildPhoneControlButton( _buildPhoneControlButton(
icon: Icons.call_end, icon: Icons.close,
onTap: () => controller.endPhoneCall(), onTap: () => controller.endPhoneCall(),
isDarkMode: isDarkMode, isDarkMode: isDarkMode,
isActive: false, isActive: false,
backgroundColor: Colors.red, //backgroundColor: Colors.red,
iconColor: Colors.redAccent,
), ),
], ],
), ),
@ -1138,24 +1083,31 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
required bool isDarkMode, required bool isDarkMode,
required bool isActive, required bool isActive,
Color? backgroundColor, Color? backgroundColor,
Color? iconColor,
}) { }) {
return GestureDetector( return GestureDetector(
onTap: onTap, onTap: onTap,
child: Container( child: Container(
width: 56.w, width: 64.w, // 按钮尺寸
height: 56.h, height: 64.h,
decoration: BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle, shape: BoxShape.circle,
color: backgroundColor ?? color: backgroundColor ??
(isDarkMode ? Colors.grey[800] : Colors.grey[200]), (isDarkMode ? Colors.grey[800] : Colors.grey[250]),
border: isActive ? Border.all(color: Colors.blue, width: 2.w) : null, border: Border.all(color: Colors.blue, width: 2.w),
boxShadow: [
BoxShadow(
color: (backgroundColor ?? Colors.grey).withValues(alpha: 0.3),
blurRadius: 8.w,
spreadRadius: 1.w,
offset: Offset(0, 2.h),
),
],
), ),
child: Icon( child: Icon(
icon, icon,
color: backgroundColor != null color: iconColor ?? (isDarkMode ? Colors.white : Colors.black),
? Colors.white size: 28.sp, // 图标尺寸
: (isDarkMode ? Colors.white : Colors.black),
size: 24.sp,
), ),
), ),
); );

30
lib/modules/agent/views/message_bubble.dart

@ -46,28 +46,20 @@ class MessageBubble extends GetView<AgentController> {
isUser ? CrossAxisAlignment.end : CrossAxisAlignment.start, isUser ? CrossAxisAlignment.end : CrossAxisAlignment.start,
children: [ children: [
Container( Container(
padding: hasImage padding: (hasImage && message == '[图片]')
? (message == '[图片]' ? EdgeInsets.zero
? EdgeInsets.zero : const EdgeInsets.all(12),
: isUser
? const EdgeInsets.fromLTRB(
12, 12, 16, 12) // 用户消息右侧多一点内边距
: const EdgeInsets.fromLTRB(
16, 12, 12, 12)) // AI消息左侧多一点内边距
: isUser
? const EdgeInsets.fromLTRB(
12, 12, 16, 12) // 用户消息右侧多一点内边距
: const EdgeInsets.fromLTRB(
16, 12, 12, 12), // AI消息左侧多一点内边距
decoration: BoxDecoration( decoration: BoxDecoration(
color: _getBubbleColor(context), color: _getBubbleColor(context),
borderRadius: BorderRadius.circular(18).copyWith( borderRadius: BorderRadius.only(
topLeft: isUser topLeft: isUser
? const Radius.circular(18) ? const Radius.circular(18)
: const Radius.circular(4), : const Radius.circular(4),
topRight: !isUser topRight: isUser
? const Radius.circular(18) ? const Radius.circular(4)
: const Radius.circular(4), : const Radius.circular(18),
bottomLeft: const Radius.circular(18),
bottomRight: const Radius.circular(18),
), ),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
@ -177,7 +169,7 @@ class MessageBubble extends GetView<AgentController> {
child: ClipRRect( child: ClipRRect(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
child: Image.file( child: Image.file(
File(imagePath!), File('${controller.AppDir.value}$imagePath'),
width: 200, width: 200,
height: 200, height: 200,
fit: BoxFit.cover, fit: BoxFit.cover,
@ -210,7 +202,7 @@ class MessageBubble extends GetView<AgentController> {
child: ClipRRect( child: ClipRRect(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
child: Image.file( child: Image.file(
File(imagePath!), File('${controller.AppDir.value}$imagePath'),
width: 200, width: 200,
height: 200, height: 200,
fit: BoxFit.cover, fit: BoxFit.cover,

2
lib/modules/agent/views/music_card.dart

@ -3,7 +3,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../../../routes/app_routes.dart'; import '../../../routes/app_routes.dart';
import '../controllers/agent_controller.dart'; import '../controllers/agent_controller.dart';
import 'circular_progress_button.dart'; import '../widgets/circular_progress_button.dart';
/// 构建音乐项目 /// 构建音乐项目
Widget buildMusicItem(BuildContext context, int id, String name, String url, Widget buildMusicItem(BuildContext context, int id, String name, String url,

0
lib/modules/agent/views/circular_progress_button.dart → lib/modules/agent/widgets/circular_progress_button.dart

50
lib/modules/agent/widgets/sound_bar_widget.dart

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../controllers/agent_controller.dart'; import '../controllers/agent_controller.dart';
/// 声音柱动画组件 /// 声音柱动画组件 (已优化)
/// 用于显示语音输入时的音频波形效果 /// 用于显示语音输入时的音频波形效果
class SoundBarWidget extends StatefulWidget { class SoundBarWidget extends StatefulWidget {
final double delay; final double delay;
@ -37,9 +37,8 @@ class _SoundBarWidgetState extends State<SoundBarWidget>
parent: _controller, parent: _controller,
curve: Curves.easeInOut, curve: Curves.easeInOut,
), ),
)..addListener(() { );
setState(() {}); // 移除了 .addListener(() { setState(() {}); })
});
_startOrStopAnimation(); _startOrStopAnimation();
} }
@ -54,6 +53,9 @@ class _SoundBarWidgetState extends State<SoundBarWidget>
/// 开始或停止动画 /// 开始或停止动画
void _startOrStopAnimation() { void _startOrStopAnimation() {
// 确保组件仍然挂载在树上
if (!mounted) return;
if (widget.isActive) { if (widget.isActive) {
Future.delayed(Duration(milliseconds: (widget.delay * 1000).toInt()), () { Future.delayed(Duration(milliseconds: (widget.delay * 1000).toInt()), () {
if (mounted && widget.isActive) { if (mounted && widget.isActive) {
@ -61,9 +63,10 @@ class _SoundBarWidgetState extends State<SoundBarWidget>
} }
}); });
} else { } else {
_controller.reset(); // 停止并平滑地过渡到初始状态
// 设置为小高度的静态状态 _controller.stop();
_controller.animateTo(0.1, duration: const Duration(milliseconds: 300)); _controller.animateTo(0.0,
duration: const Duration(milliseconds: 200), curve: Curves.easeOut);
} }
} }
@ -77,21 +80,28 @@ class _SoundBarWidgetState extends State<SoundBarWidget>
Widget build(BuildContext context) { Widget build(BuildContext context) {
final primaryColor = Theme.of(context).primaryColor; final primaryColor = Theme.of(context).primaryColor;
return Container( // 使用 AnimatedBuilder 来优化性能
width: 2.5, return AnimatedBuilder(
height: widget.isActive ? _animation.value : 3, animation: _animation,
margin: const EdgeInsets.symmetric(horizontal: 1), builder: (context, child) {
decoration: BoxDecoration( return Container(
color: widget.isActive width: 2.5,
? primaryColor.withOpacity(0.8) // 当动画不活跃时,高度固定为3;活跃时使用动画值
: Colors.grey.withOpacity(0.4), height: widget.isActive ? _animation.value : 3,
borderRadius: BorderRadius.circular(1.5), margin: const EdgeInsets.symmetric(horizontal: 1),
), decoration: BoxDecoration(
color: widget.isActive
? primaryColor.withOpacity(0.8)
: Colors.grey.withOpacity(0.4),
borderRadius: BorderRadius.circular(1.5),
),
);
},
); );
} }
} }
/// 声音柱组合组件 /// 声音柱组合组件 (无需改动)
/// 包含多个声音柱的组合显示 /// 包含多个声音柱的组合显示
class SoundBarsWidget extends StatelessWidget { class SoundBarsWidget extends StatelessWidget {
final bool isActive; final bool isActive;
@ -106,7 +116,7 @@ class SoundBarsWidget extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Row( return Row(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(barCount, (index) { children: List.generate(barCount, (index) {
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 1), padding: const EdgeInsets.symmetric(horizontal: 1),
@ -120,7 +130,7 @@ class SoundBarsWidget extends StatelessWidget {
} }
} }
/// 带控制器集成的声音柱组件 /// 带控制器集成的声音柱组件 (无需改动)
/// 自动从AgentController获取状态 /// 自动从AgentController获取状态
class AgentSoundBarsWidget extends StatelessWidget { class AgentSoundBarsWidget extends StatelessWidget {
final int barCount; final int barCount;

1
lib/modules/devices/controllers/devices_controller.dart

@ -57,6 +57,7 @@ class DevicesController extends GetxController {
User.instance.removeUserDevice(device.id); User.instance.removeUserDevice(device.id);
_bleManager.clearPairedDevices(); _bleManager.clearPairedDevices();
_bleManager.disconnect(); _bleManager.disconnect();
await Future.delayed(const Duration(seconds: 1));
Get.toNamed(Routes.pairing); Get.toNamed(Routes.pairing);
return; return;
} else { } else {

150
lib/modules/home/controllers/home_controller.dart

@ -1,13 +1,11 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'dart:async'; import 'dart:async';
import '../../../data/services/novice_guide_service.dart';
import '../../../routes/app_routes.dart'; import '../../../routes/app_routes.dart';
import '../../../core/utils/logger.dart'; import '../../../core/utils/logger.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../../data/services/bluetooth_manager.dart'; import '../../../data/services/bluetooth_manager.dart';
import '../../../data/services/ble_manager.dart'; import '../../../data/services/ble_manager.dart';
import '../../../data/services/version_update_service.dart';
import '../../common/widgets/version_update_dialog.dart';
import 'package:get_storage/get_storage.dart';
/// 连接状态枚举 /// 连接状态枚举
enum ConnectionStatus { enum ConnectionStatus {
@ -24,7 +22,7 @@ class HomeController extends GetxController {
final _isFullyConnected = false.obs; final _isFullyConnected = false.obs;
String? _lastConnectedDeviceName; String? _lastConnectedDeviceName;
bool get isFullyConnected => _isFullyConnected.value; bool get isFullyConnected => _isFullyConnected.value;
RxBool get isFullyConnectedRx => _isFullyConnected; RxBool get isFullyConnectedRx => _isFullyConnected;
BluetoothDevice? get currentDevice => _bluetoothManager.currentDevice; BluetoothDevice? get currentDevice => _bluetoothManager.currentDevice;
@ -53,14 +51,9 @@ class HomeController extends GetxController {
final _bluetoothManager = Get.find<BluetoothManager>(); final _bluetoothManager = Get.find<BluetoothManager>();
final _bleManager = Get.find<BleManager>(); final _bleManager = Get.find<BleManager>();
final _storage = GetStorage();
// 会话级别的版本检查标记(静态变量,整个应用会话期间保持) final noviceGuideService = Get.find<NoviceGuideService>();
static bool _hasCheckedVersionInSession = false;
// 版本检查相关常量
static const String _lastVersionCheckKey = 'last_version_check_timestamp';
static const int _versionCheckIntervalHours = 24; // 24小时间隔
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
@ -78,12 +71,6 @@ class HomeController extends GetxController {
void _initWithCachedInfo() { void _initWithCachedInfo() {
_setupStateListeners(); _setupStateListeners();
_checkConnectionStatus(); _checkConnectionStatus();
// 延迟检查版本更新,避免影响页面加载
// 混合检查机制:会话检查 + 24小时间隔检查
Future.delayed(const Duration(seconds: 2), () {
_checkVersionUpdateIfNeeded();
});
} }
void _setupStateListeners() { void _setupStateListeners() {
@ -94,13 +81,14 @@ class HomeController extends GetxController {
update(); update();
}); });
_classicBluetoothWorker = ever(_bluetoothManager.currentDeviceRx, (BluetoothDevice? device) { _classicBluetoothWorker =
ever(_bluetoothManager.currentDeviceRx, (BluetoothDevice? device) {
if (device != null) { if (device != null) {
_lastConnectedDeviceName = device.name; _lastConnectedDeviceName = device.name;
} }
_checkConnectionStatus(); _checkConnectionStatus();
}); });
_bleConnectionWorker = ever(_bleManager.connectionStateRx, (int state) { _bleConnectionWorker = ever(_bleManager.connectionStateRx, (int state) {
_checkConnectionStatus(); _checkConnectionStatus();
}); });
@ -121,30 +109,30 @@ class HomeController extends GetxController {
void exploreAIFeatures() { void exploreAIFeatures() {
Get.toNamed(Routes.agent); Get.toNamed(Routes.agent);
} }
Future<void> _checkConnectionStatus() async { Future<void> _checkConnectionStatus() async {
try { try {
final currentBleState = await _bleManager.getConnectionState(); final currentBleState = await _bleManager.getConnectionState();
bool hasClassicBluetooth = currentDevice != null; bool hasClassicBluetooth = currentDevice != null;
bool hasBleConnection = currentBleState == 2; bool hasBleConnection = currentBleState == 2;
bool wasConnected = _isFullyConnected.value; bool wasConnected = _isFullyConnected.value;
bool isNowConnected = hasClassicBluetooth && hasBleConnection; bool isNowConnected = hasClassicBluetooth && hasBleConnection;
if (isNowConnected && currentDevice == null) { if (isNowConnected && currentDevice == null) {
isNowConnected = false; isNowConnected = false;
} }
if (_isFullyConnected.value != isNowConnected) { if (_isFullyConnected.value != isNowConnected) {
_isFullyConnected.value = isNowConnected; _isFullyConnected.value = isNowConnected;
_isFullyConnected.refresh(); _isFullyConnected.refresh();
update(); update();
if (!wasConnected && isNowConnected) { if (!wasConnected && isNowConnected) {
await _bleManager.fetchDeviceInfo(); await _bleManager.fetchDeviceInfo();
} }
} }
update(); update();
} catch (e) { } catch (e) {
Logger.e(_tag, '${'checkConnectionStatusFailed'.tr}: $e'); Logger.e(_tag, '${'checkConnectionStatusFailed'.tr}: $e');
@ -165,26 +153,28 @@ class HomeController extends GetxController {
} }
bool get shouldShowDeviceInfo => currentDevice != null; bool get shouldShowDeviceInfo => currentDevice != null;
String getDeviceName() { String getDeviceName() {
if (_isFullyConnected.value && currentDevice == null) { if (_isFullyConnected.value && currentDevice == null) {
_checkConnectionStatus(); _checkConnectionStatus();
} }
final deviceName = currentDevice?.name; final deviceName = currentDevice?.name;
if (deviceName != null && deviceName.isNotEmpty) { if (deviceName != null && deviceName.isNotEmpty) {
return deviceName; return deviceName;
} }
final bleDeviceName = deviceInfo.value?.name; final bleDeviceName = deviceInfo.value?.name;
if (bleDeviceName != null && bleDeviceName.isNotEmpty) { if (bleDeviceName != null && bleDeviceName.isNotEmpty) {
return bleDeviceName; return bleDeviceName;
} }
if (!_isFullyConnected.value && _lastConnectedDeviceName != null && _lastConnectedDeviceName!.isNotEmpty) { if (!_isFullyConnected.value &&
_lastConnectedDeviceName != null &&
_lastConnectedDeviceName!.isNotEmpty) {
return _lastConnectedDeviceName!; return _lastConnectedDeviceName!;
} }
return 'unknownEquipment'.tr; return 'unknownEquipment'.tr;
} }
@ -270,104 +260,4 @@ class HomeController extends GetxController {
? deviceInfo.value!.firmwareVersion ? deviceInfo.value!.firmwareVersion
: 'unknown'.tr; : 'unknown'.tr;
} }
/// 混合检查机制:判断是否需要进行版本检查
/// 检查条件:1. 会话内未检查过 OR 2. 距离上次检查超过24小时
void _checkVersionUpdateIfNeeded() {
try {
final now = DateTime.now();
final lastCheckTimestamp = _storage.read(_lastVersionCheckKey);
bool shouldCheck = false;
String reason = '';
// 条件1:会话内未检查过
if (!_hasCheckedVersionInSession) {
shouldCheck = true;
reason = '会话内首次检查';
}
// 条件2:距离上次检查超过24小时
else if (lastCheckTimestamp != null) {
final lastCheckTime = DateTime.fromMillisecondsSinceEpoch(lastCheckTimestamp);
final timeDifference = now.difference(lastCheckTime);
if (timeDifference.inHours >= _versionCheckIntervalHours) {
shouldCheck = true;
reason = '距离上次检查已超过${_versionCheckIntervalHours}小时';
}
}
// 条件3:从未检查过(首次使用应用)
else {
shouldCheck = true;
reason = '首次使用应用';
}
if (shouldCheck) {
Logger.i(_tag, '开始版本检查 - 原因: $reason');
_performVersionCheck();
} else {
Logger.i(_tag, '跳过版本检查 - 会话内已检查且未超过时间间隔');
}
} catch (e) {
Logger.e(_tag, '版本检查判断失败: $e');
}
}
/// 执行版本检查(在后台进行,不阻塞启动流程)
Future<void> _performVersionCheck() async {
try {
final versionService = VersionUpdateService.to;
final updateInfo = await versionService.checkForUpdate();
if (updateInfo.hasUpdate) {
Logger.i(_tag, '发现新版本: ${updateInfo.latestVersion}');
// 显示更新对话框
await VersionUpdateDialog.show(updateInfo);
} else {
Logger.i(_tag, '当前已是最新版本: ${updateInfo.currentVersion}');
}
// 更新检查记录
_updateVersionCheckRecord();
} catch (e) {
Logger.e(_tag, '版本检查失败: $e');
// 即使检查失败,也更新记录,避免频繁重试
_updateVersionCheckRecord();
}
}
/// 更新版本检查记录
void _updateVersionCheckRecord() {
try {
// 更新时间戳
final now = DateTime.now();
_storage.write(_lastVersionCheckKey, now.millisecondsSinceEpoch);
// 标记本次会话已检查过版本
_hasCheckedVersionInSession = true;
Logger.i(_tag, '版本检查记录已更新: ${now.toIso8601String()}');
} catch (e) {
Logger.e(_tag, '更新版本检查记录失败: $e');
}
}
/// 重置版本检查标记(可用于强制重新检查)
/// 注意:这会清除会话标记,但不会清除时间戳记录
static void resetVersionCheckFlag() {
_hasCheckedVersionInSession = false;
Logger.i('HomeCtrl', '版本检查会话标记已重置');
}
/// 完全重置版本检查状态(包括时间戳)
/// 用于测试或特殊情况下强制重新检查
void forceResetVersionCheck() {
try {
_hasCheckedVersionInSession = false;
_storage.remove(_lastVersionCheckKey);
Logger.i(_tag, '版本检查状态已完全重置');
} catch (e) {
Logger.e(_tag, '重置版本检查状态失败: $e');
}
}
} }

243
lib/modules/home/views/home_view.dart

@ -1,8 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_intro/flutter_intro.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../../core/widgets/intro_overlay_builder.dart';
import '../controllers/home_controller.dart'; import '../controllers/home_controller.dart';
import '../../../routes/app_pages.dart';
import '../../../routes/app_routes.dart'; import '../../../routes/app_routes.dart';
import '../../../core/widgets/common_bottom_nav.dart'; import '../../../core/widgets/common_bottom_nav.dart';
@ -85,12 +86,63 @@ class HomeView extends GetView<HomeController> {
// 顶部图片 // 顶部图片
_buildEarbudsImage(), _buildEarbudsImage(),
SizedBox(height: 20.h), SizedBox(height: 20.h),
// 设备状态卡片 // 设备状态卡片
_buildDeviceStatusCard(isDarkMode), IntroStepBuilder(
group: 'home',
order: 1,
onWidgetLoad: () {
controller.noviceGuideService.startHomeGuide();
},
overlayBuilder: (params) {
return IntroOverlayBuilder.aiBuilder(
params,
'欢迎使用Voitrans!首先,让我们唤醒你的专属 AI—— 小言',
onNext: () {
controller.noviceGuideService.startWakeupListen();
},
);
},
builder: (BuildContext context, GlobalKey key) => SizedBox(
key: key,
child: _buildDeviceStatusCard(isDarkMode),
),
),
SizedBox(height: 20.h), SizedBox(height: 20.h),
IntroStepBuilder(
group: 'home',
order: 2,
getOverlayPosition: ({
required Offset offset,
required Size screenSize,
required Size size,
}) {
return OverlayPosition(
top: 200.h,
width: screenSize.width,
crossAxisAlignment: CrossAxisAlignment.end,
);
},
overlayBuilder: (params) {
return Obx(
() => IntroOverlayBuilder.homeBuilder(
params,
controller.noviceGuideService.isWakeup.value,
onNext: () {
controller.noviceGuideService.closeWakeupListen();
},
),
);
},
builder: (BuildContext context, GlobalKey key) => SizedBox(
key: key,
),
),
// 热门AI体验 // 热门AI体验
_buildSectionTitle(isDarkMode), _buildSectionTitle(isDarkMode),
SizedBox(height: 16.h), SizedBox(height: 16.h),
@ -219,7 +271,7 @@ class HomeView extends GetView<HomeController> {
), ),
], ],
), ),
padding: EdgeInsets.all(16.w), padding: EdgeInsets.fromLTRB(16.w, 16.h, 16.w, 16.h),
child: Column( child: Column(
children: [ children: [
// 状态标题行 // 状态标题行
@ -242,7 +294,7 @@ class HomeView extends GetView<HomeController> {
if (!controller.isFullyConnectedRx.value) { if (!controller.isFullyConnectedRx.value) {
return _buildConnectButton(); return _buildConnectButton();
} else { } else {
return const SizedBox(); return _buildDeviceButton(isDarkMode);
} }
}), }),
], ],
@ -365,12 +417,42 @@ class HomeView extends GetView<HomeController> {
), ),
SizedBox(height: 12.h), SizedBox(height: 12.h),
_buildBatteryInfoGrid(isDarkMode), _buildBatteryInfoGrid(isDarkMode),
// SizedBox(height: 16.h),
// _buildDeviceDetailsCard(isDarkMode),
], ],
); );
} }
// 设备管理按钮
Widget _buildDeviceButton(bool isDarkMode) {
return ElevatedButton(
onPressed: () {
Get.toNamed(Routes.devices); // 跳转到设备管理页面
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
minimumSize: Size(double.infinity, 44.h),
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.r),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.device_hub, size: 16.sp), // 设备图标
SizedBox(width: 8.w),
Text(
'deviceManagement'.tr,
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w500,
),
),
],
),
);
}
// 电池信息网格布局 // 电池信息网格布局
Widget _buildBatteryInfoGrid(bool isDarkMode) { Widget _buildBatteryInfoGrid(bool isDarkMode) {
return Obx(() { return Obx(() {
@ -383,6 +465,10 @@ class HomeView extends GetView<HomeController> {
final rightEarCharging = controller.rightEarCharging; final rightEarCharging = controller.rightEarCharging;
final caseCharging = controller.caseCharging; final caseCharging = controller.caseCharging;
// 判断是否显示充电盒:充电盒电量大于0且设备已连接
final shouldShowCase =
caseBattery.value > 0 && controller.isFullyConnectedRx.value;
// 使用更紧凑的布局 // 使用更紧凑的布局
return Container( return Container(
padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 4.w), padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 4.w),
@ -416,20 +502,23 @@ class HomeView extends GetView<HomeController> {
isDarkMode: isDarkMode, isDarkMode: isDarkMode,
), ),
// // 分隔线 // 只有在充电盒电量大于0且设备已连接时才显示充电盒
// Container( if (shouldShowCase) ...[
// height: 30.h, // 分隔线
// width: 1, Container(
// color: isDarkMode ? Colors.grey[800] : Colors.grey[300], height: 30.h,
// ), width: 1,
color: isDarkMode ? Colors.grey[800] : Colors.grey[300],
// // 充电盒 ),
// _buildCompactBatteryItem(
// label: '充电盒', // 充电盒
// batteryLevel: caseBattery.value, _buildCompactBatteryItem(
// isCharging: caseCharging.value, label: '充电盒',
// isDarkMode: isDarkMode, batteryLevel: caseBattery.value,
// ), isCharging: caseCharging.value,
isDarkMode: isDarkMode,
),
]
], ],
), ),
); );
@ -463,6 +552,8 @@ class HomeView extends GetView<HomeController> {
batteryColor = Colors.red[600]!; batteryColor = Colors.red[600]!;
} }
bool isBatteryZero = batteryLevel == 0;
return Expanded( return Expanded(
child: Padding( child: Padding(
padding: EdgeInsets.symmetric(vertical: 6.h), padding: EdgeInsets.symmetric(vertical: 6.h),
@ -481,32 +572,45 @@ class HomeView extends GetView<HomeController> {
), ),
SizedBox(height: 4.h), SizedBox(height: 4.h),
// 电池指示器 // 电池指示器 - 当电量为0时,显示横线
Row( isBatteryZero
mainAxisAlignment: MainAxisAlignment.center, ? Container(
children: [ width: 20.w, // 横线宽度
Icon( height: 12.sp, // 设置与图标相同的高度,确保对齐
batteryIcon, alignment: Alignment.center, // 居中对齐
size: 12.sp, child: Container(
color: batteryColor, width: 10.w,
), height: 1.5.h, // 实际横线高度
SizedBox(width: 2.w), color: isDarkMode
Text( ? Colors.grey[600]
'$batteryLevel%', : Colors.grey[400], // 横线颜色
style: TextStyle( ),
fontSize: 11.sp, )
fontWeight: FontWeight.w500, : Row(
color: batteryColor, mainAxisAlignment: MainAxisAlignment.center,
), children: [
), Icon(
if (isCharging) batteryIcon,
Icon( size: 12.sp,
Icons.bolt, color: batteryColor,
size: 10.sp, ),
color: Colors.green[600], SizedBox(width: 2.w),
Text(
'$batteryLevel%',
style: TextStyle(
fontSize: 11.sp,
fontWeight: FontWeight.w500,
color: batteryColor,
),
),
if (isCharging)
Icon(
Icons.bolt,
size: 10.sp,
color: Colors.green[600],
),
],
), ),
],
),
], ],
), ),
), ),
@ -623,23 +727,38 @@ class HomeView extends GetView<HomeController> {
// 左侧大卡片 - AI助理 // 左侧大卡片 - AI助理
Expanded( Expanded(
flex: 1, flex: 1,
child: _buildFeatureCard( child: IntroStepBuilder(
title: 'aiAssistant'.tr, // AI全能助手 group: 'home',
description: 'aiAssistantInfo'.tr, // 24小时专属顾问,解答问题、提供建议 order: 3,
icon: Icons.assistant, onHighlightWidgetTap: () {
gradient: LinearGradient( Intro.of(Get.context!).dispose();
begin: Alignment.topLeft, controller.exploreAIFeatures();
end: Alignment.bottomRight, },
colors: [ overlayBuilder: (params) {
Color(0xFF0066CC).withOpacity(0.3), return IntroOverlayBuilder.aiBuilder(
Color(0xFF5E5CE6).withOpacity(0.3), params,
], '太棒了!你已成功唤醒小言,接下来试试让它帮你做件事吧',
);
},
builder: (BuildContext context, GlobalKey key) => _buildFeatureCard(
key: key,
title: 'aiAssistant'.tr, // AI全能助手
description: 'aiAssistantInfo'.tr, // 24小时专属顾问,解答问题、提供建议
icon: Icons.assistant,
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Color(0xFF0066CC).withOpacity(0.3),
Color(0xFF5E5CE6).withOpacity(0.3),
],
),
onTap: controller.exploreAIFeatures,
iconOnTop: true,
height: 175.h, // 增加高度
isDarkMode: isDarkMode,
iconColor: const Color(0xFF0066CC),
), ),
onTap: controller.exploreAIFeatures,
iconOnTop: true,
height: 175.h, // 增加高度
isDarkMode: isDarkMode,
iconColor: const Color(0xFF0066CC),
), ),
), ),
@ -702,6 +821,7 @@ class HomeView extends GetView<HomeController> {
// 特性卡片构建方法 // 特性卡片构建方法
Widget _buildFeatureCard({ Widget _buildFeatureCard({
GlobalKey? key,
required String title, required String title,
required String description, required String description,
required IconData icon, required IconData icon,
@ -713,6 +833,7 @@ class HomeView extends GetView<HomeController> {
Color iconColor = Colors.white, Color iconColor = Colors.white,
}) { }) {
return GestureDetector( return GestureDetector(
key: key,
onTap: onTap, onTap: onTap,
child: Container( child: Container(
height: height ?? 175.h, height: height ?? 175.h,

181
lib/modules/login/controllers/login_controller.dart

@ -1,21 +1,23 @@
import '../../../data/models/appconfig.dart'; import 'dart:async';
import '../../../data/models/appconfig_model.dart'; import 'dart:io';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:logger/web.dart'; import 'package:logger/web.dart';
import '../../../routes/app_routes.dart'; import 'package:get_storage/get_storage.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'dart:convert'; import 'dart:convert';
import 'facebookLogin.dart'; import 'facebookLogin.dart';
import 'googleLogin.dart'; import 'googleLogin.dart';
import 'weixinLogin.dart'; import 'weixinLogin.dart';
import 'appleLogin.dart'; import 'appleLogin.dart';
import '../../../routes/app_routes.dart';
import '../../../data/models/appconfig.dart';
import '../../../data/models/appconfig_model.dart';
import '../../../data/services/network/api.dart'; import '../../../data/services/network/api.dart';
import '../../../../data/models/user_Info.dart'; import '../../../../data/models/user_Info.dart';
import 'package:get_storage/get_storage.dart'; import '../../../data/models/channel_app_model.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'dart:io';
import 'package:package_info_plus/package_info_plus.dart';
import '../../../data/services/version_update_service.dart'; import '../../../data/services/version_update_service.dart';
import '../../../modules/common/widgets/privacy_policy_dialog.dart'; import '../../../modules/common/widgets/privacy_policy_dialog.dart';
@ -25,20 +27,20 @@ enum LoginViewState {
} }
// 用户渠道枚举 // 用户渠道枚举
enum UserChannel { // enum UserChannel {
UNKNOWN(0), // 未知渠道 // UNKNOWN(0), // 未知渠道
CHINA_HUAWEI(1), // 中国-华为 // CHINA_HUAWEI(1), // 中国-华为
CHINA_XIAOMI(2), // 中国-小米 // CHINA_XIAOMI(2), // 中国-小米
CHINA_OPPO(3), // 中国-OPPO // CHINA_OPPO(3), // 中国-OPPO
CHINA_VIVO(4), // 中国-VIVO // CHINA_VIVO(4), // 中国-VIVO
CHINA_HONOR(5), // 中国-荣耀 // CHINA_HONOR(5), // 中国-荣耀
OFFICIAL(100), // 官方渠道 // OFFICIAL(100), // 官方渠道
GLOBAL_APPLE(101), // 全球-苹果 // GLOBAL_APPLE(101), // 全球-苹果
GLOBAL_GOOGLE(102); // 全球-谷歌 // GLOBAL_GOOGLE(102); // 全球-谷歌
const UserChannel(this.value); // const UserChannel(this.value);
final int value; // final int value;
} // }
class LoginController extends GetxController { class LoginController extends GetxController {
static LoginController get to => Get.find(); static LoginController get to => Get.find();
@ -210,6 +212,7 @@ class LoginController extends GetxController {
String get contactValue => _contactValue.value; String get contactValue => _contactValue.value;
String get displayContact => _displayContact.value; String get displayContact => _displayContact.value;
List<String> get verificationCode => _verificationCode; List<String> get verificationCode => _verificationCode;
final versionService = VersionUpdateService.to;
// 文本控制器 - 延迟初始化 // 文本控制器 - 延迟初始化
TextEditingController? _contactController; TextEditingController? _contactController;
@ -272,20 +275,9 @@ class LoginController extends GetxController {
// 检查游客登录配置 // 检查游客登录配置
Future<void> _checkGuestLoginConfig() async { Future<void> _checkGuestLoginConfig() async {
try { try {
final versionService = VersionUpdateService.to; _isGuestLoginVisible.value = versionService.cachedShowTourists;
// 先检查版本更新,这会同时设置 isShowTourists 的值
await versionService.checkForUpdate();
// 直接使用 VersionUpdateService 中的 isShowTourists 值
_isGuestLoginVisible.value = versionService.isShowTourists.value;
Logger().d('游客登录显示状态: ${_isGuestLoginVisible.value}'); Logger().d('游客登录显示状态: ${_isGuestLoginVisible.value}');
// 监听 isShowTourists 的变化
versionService.isShowTourists.listen((value) {
_isGuestLoginVisible.value = value;
Logger().d('游客登录显示状态更新: $value');
});
} catch (e) { } catch (e) {
Logger().e('检查游客登录配置失败: $e'); Logger().e('检查游客登录配置失败: $e');
// 默认不显示游客登录 // 默认不显示游客登录
@ -327,9 +319,7 @@ class LoginController extends GetxController {
// 新增:初始化APP信息(版本号、渠道、国家代码) // 新增:初始化APP信息(版本号、渠道、国家代码)
Future<void> _initAppInfo() async { Future<void> _initAppInfo() async {
try { try {
// 获取APP版本号 _appVersion = versionService.cachedAppVersion;
final packageInfo = await PackageInfo.fromPlatform();
_appVersion = '${packageInfo.version}+${packageInfo.buildNumber}';
Logger().d('appVersion: $_appVersion'); Logger().d('appVersion: $_appVersion');
// 获取国家代码 // 获取国家代码
@ -337,7 +327,7 @@ class LoginController extends GetxController {
Logger().d('countryCode: $_countryCode'); Logger().d('countryCode: $_countryCode');
// 获取用户渠道(根据平台和设备信息确定) // 获取用户渠道(根据平台和设备信息确定)
_userChannel = UserChannel.OFFICIAL; _userChannel = versionService.cachedChannel;
Logger().d('userChannel: ${_userChannel.value}'); Logger().d('userChannel: ${_userChannel.value}');
} catch (e) { } catch (e) {
Logger().e('获取APP信息失败: $e'); Logger().e('获取APP信息失败: $e');
@ -843,54 +833,79 @@ class LoginController extends GetxController {
// 使用登陆请求服务器 // 使用登陆请求服务器
Future<void> requestLogin(Map params) async { Future<void> requestLogin(Map params) async {
Logger().d("请求后台数据login=$params"); try {
var response = await Api.login({ Logger().d("请求后台数据login=$params");
"mail": params["mail"], // 在Api.login调用时添加超时
"phone": params["phone"], var response = await Api.login({
"stype": params["stype"], "mail": params["mail"],
"vcode": params["vcode"], "phone": params["phone"],
"ttoken": params["ttoken"], "stype": params["stype"],
"phonemodel": _appVersion + _countryCode + _userChannel.toString(), "vcode": params["vcode"],
"phonemac": params["phonemac"], "ttoken": params["ttoken"],
"name": params["name"], "phonemodel": _appVersion + _countryCode + _userChannel.toString(),
"avatar": params["avatar"], "phonemac": params["phonemac"],
}); "name": params["name"],
if (response != null) { "avatar": params["avatar"],
Logger().d("请求后台数据login回复数据=$response"); }).timeout(
const Duration(seconds: 30), // 30秒超时
// final instance = await GetStorage(); onTimeout: () {
await _storage.write('loginInfo', jsonEncode(response)); throw TimeoutException('登录请求超时', const Duration(seconds: 30));
// 2. 将响应数据转换为User对象 },
try { );
Logger().d('响应数据: ${response.toString()}'); // 调试输出原始数据 if (response != null) {
Logger().d("请求后台数据login回复数据=$response");
if (response['token'].toString() != '') {
// 新增:检查 user 字段是否存在且类型正确 await _storage.write('loginInfo', jsonEncode(response));
// if (response.containsKey('data') && response['data'] is Map) {
final user = User.fromJson(response); try {
Logger().d('用户初始化成功: ${response['token'].toString()}'); Logger().d('响应数据: ${response.toString()}');
await _storage.write("logintoken", response['token'].toString());
// 存储到 SharedPreferences (仅存储用户数据) if (response['token'].toString() != '') {
//final prefs = await GetStorage(); final user = User.fromJson(response);
await _storage.write('user_info', user.toJson()); Logger().d('用户初始化成功: ${response['token'].toString()}');
await _storage.write("logintoken", response['token'].toString());
//获取app的配置信息 await _storage.write('user_info', user.toJson());
await getAppConfig();
Get.offAllNamed(Routes.pairing); // 获取app的配置信息
// } else { await getAppConfig();
// throw FormatException('API 响应缺少 user 字段'); Get.offAllNamed(Routes.pairing);
// } } else {
} else { throw Exception('接口返回错误: ${response['msg']}');
throw Exception('接口返回错误: ${response['msg']}'); }
} catch (e, stack) {
Logger().d('用户数据处理失败: $e');
Logger().d('堆栈跟踪: $stack');
Get.snackbar(
'error'.tr,
'userDataParseFailed'.tr, // 用户数据解析失败
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red.withOpacity(0.1),
colorText: Colors.red,
);
} }
} catch (e, stack) { } else {
// 新增:打印堆栈跟踪 // API返回null的情况
Logger().d('用户数据处理失败: $e'); Get.snackbar(
Logger().d('堆栈跟踪: $stack'); 'error'.tr,
Get.snackbar('错误', '用户数据解析失败,请检查网络'); 'networkError'.tr, // 网络错误,请重试
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red.withOpacity(0.1),
colorText: Colors.red,
);
} }
} catch (e) {
// Get.offAll(() => IndexWidget()); // 网络请求异常
Logger().e('登录请求失败: $e');
Get.snackbar(
'error'.tr,
'loginFailed'.tr, // 登录失败
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red.withOpacity(0.1),
colorText: Colors.red,
);
} finally {
// 确保无论成功还是失败都重置加载状态
_isLoading.value = false;
} }
} }

17
lib/modules/login/views/login_view.dart

@ -13,12 +13,17 @@ class LoginView extends GetView<LoginController> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark; final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Scaffold( return GestureDetector(
backgroundColor: isDarkMode ? Colors.black : Colors.grey[50], onTap: () {
body: SafeArea( FocusScope.of(context).unfocus();
child: Obx(() => controller.viewState == LoginViewState.welcome },
? _buildWelcomeView(isDarkMode) child: Scaffold(
: _buildVerificationView(context, isDarkMode)), backgroundColor: isDarkMode ? Colors.black : Colors.grey[50],
body: SafeArea(
child: Obx(() => controller.viewState == LoginViewState.welcome
? _buildWelcomeView(isDarkMode)
: _buildVerificationView(context, isDarkMode)),
),
), ),
); );
} }

6
lib/modules/meeting/controllers/meeting_record_controller.dart

@ -138,7 +138,7 @@ class MeetingRecordController extends GetxController
switch (audioType.value) { switch (audioType.value) {
case 0: case 0:
fileName.value = "现场录音"; fileName.value = "现场录音";
_audioSourceType = true; _audioSourceType = false;
break; break;
case 1: case 1:
fileName.value = "音、视频录音"; fileName.value = "音、视频录音";
@ -272,9 +272,7 @@ class MeetingRecordController extends GetxController
void _startHardwareServices() { void _startHardwareServices() {
switch (audioType.value) { switch (audioType.value) {
case 0: case 0:
// Start audio recording // _bleManager.openEncoder();
_asrService.setAudioConfig(sampleRate: 16000, channels: 1);
_bleManager.openEncoder();
break; break;
case 1: case 1:
_asrService.setAudioConfig(sampleRate: 16000, channels: 1); _asrService.setAudioConfig(sampleRate: 16000, channels: 1);

5
lib/modules/meeting/views/bottomSheet/share_bottom_sheet.dart

@ -103,7 +103,7 @@ class _ShareBottomSheetState extends State<ShareBottomSheet> {
final result = await SharePlus.instance.share(ShareParams( final result = await SharePlus.instance.share(ShareParams(
title: _controller.meetingData.value.title, title: _controller.meetingData.value.title,
subject: subject:
'DeepSound WEB,由DeepSound.Ai提供支持-实时记录会议纪要,多语言翻译。', 'Voitrans WEB,由DeepSound.Ai提供支持-实时记录会议纪要,多语言翻译。',
uri: Uri.parse( uri: Uri.parse(
'http://web.ideapsound.com/share/$id/$token', 'http://web.ideapsound.com/share/$id/$token',
), ),
@ -281,7 +281,8 @@ class _ShareBottomSheetState extends State<ShareBottomSheet> {
title, title,
style: TextStyle( style: TextStyle(
fontSize: 14.sp, fontSize: 14.sp,
color: Colors.black87.withValues(alpha: onTap != null ? 1 : 0.5), color:
Colors.black87.withValues(alpha: onTap != null ? 1 : 0.5),
), ),
), ),
), ),

4
lib/modules/meeting/views/meeting_record_view.dart

@ -95,7 +95,7 @@ class MeetingRecordView extends GetView<MeetingRecordController> {
unselectedLabelColor: Colors.grey, unselectedLabelColor: Colors.grey,
tabs: const [ tabs: const [
Tab(text: '音频'), Tab(text: '音频'),
Tab(text: '文本'), // Tab(text: '文本'),
], ],
), ),
Expanded( Expanded(
@ -103,7 +103,7 @@ class MeetingRecordView extends GetView<MeetingRecordController> {
controller: controller.tabController, controller: controller.tabController,
children: [ children: [
_buildAudioTab(context, isDarkMode), _buildAudioTab(context, isDarkMode),
_buildTextTab(context, isDarkMode), // _buildTextTab(context, isDarkMode),
], ],
), ),
), ),

13
lib/modules/music/views/music_playlist_view.dart

@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../../core/utils/logger.dart'; import '../../../core/utils/logger.dart';
import '../../agent/controllers/agent_controller.dart'; import '../../agent/controllers/agent_controller.dart';
import '../../agent/views/circular_progress_button.dart'; import '../../agent/widgets/circular_progress_button.dart';
class MusicPlaylistView extends StatelessWidget { class MusicPlaylistView extends StatelessWidget {
const MusicPlaylistView({Key? key}) : super(key: key); const MusicPlaylistView({Key? key}) : super(key: key);
@ -115,7 +115,9 @@ class MusicPlaylistView extends StatelessWidget {
); );
} }
final title = currentSong['name'] ?? currentSong['title'] ?? 'unknownSong'.tr; // 未知歌曲 final title = currentSong['name'] ??
currentSong['title'] ??
'unknownSong'.tr; // 未知歌曲
final artist = currentSong['singer'] ?? final artist = currentSong['singer'] ??
currentSong['sgener'] ?? currentSong['sgener'] ??
currentSong['artist'] ?? currentSong['artist'] ??
@ -192,7 +194,8 @@ class MusicPlaylistView extends StatelessWidget {
final id = item["id"] ?? 0; final id = item["id"] ?? 0;
final name = item["name"] ?? 'unknownSong'.tr; // 未知歌曲 final name = item["name"] ?? 'unknownSong'.tr; // 未知歌曲
final url = item["url"] ?? ""; final url = item["url"] ?? "";
final singer = item["singer"] ?? item["sgener"] ?? 'unknownArtist'.tr; // 未知歌手 final singer =
item["singer"] ?? item["sgener"] ?? 'unknownArtist'.tr; // 未知歌手
final image = item["image"] ?? ""; final image = item["image"] ?? "";
return _buildMusicItem( return _buildMusicItem(
@ -628,8 +631,8 @@ class MusicPlaylistView extends StatelessWidget {
} }
// 播放选中的歌曲 // 播放选中的歌曲
Future<void> _playSelectedSong(AgentController controller, Map<String, dynamic> song, Future<void> _playSelectedSong(AgentController controller,
List<Map<String, dynamic>> playlist) async { Map<String, dynamic> song, List<Map<String, dynamic>> playlist) async {
// 防止重复点击导致的状态混乱 // 防止重复点击导致的状态混乱
if (controller.isLoadingMusic.value) { if (controller.isLoadingMusic.value) {
return; return;

38
lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart

@ -1,4 +1,5 @@
import 'package:classic_bluetooth/classic_bluetooth.dart'; import 'package:classic_bluetooth/classic_bluetooth.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:voitrans/data/models/appconfig_model.dart'; import 'package:voitrans/data/models/appconfig_model.dart';
import '/routes/app_routes.dart'; import '/routes/app_routes.dart';
import 'package:logger/web.dart'; import 'package:logger/web.dart';
@ -214,10 +215,9 @@ class OtaUpgradeController extends GetxController {
updatepackageaddress = product!.updatepackageaddress!; updatepackageaddress = product!.updatepackageaddress!;
// 版本检查 // 版本检查
bool isUpdate = _isNewVersionAvailable(product.version!); bool hasUpdate = _isNewVersionAvailable(product.version!);
isUpdateAvailable.value = hasUpdate;
isUpdateAvailable.value = isUpdate;
} catch (e) { } catch (e) {
// 统一错误处理 // 统一错误处理
errorMessage.value = e.toString(); errorMessage.value = e.toString();
@ -238,22 +238,36 @@ class OtaUpgradeController extends GetxController {
} }
} }
// 版本比较方法优化 // 版本比较方法优化 - 支持升级和降级
bool _isNewVersionAvailable(String latest) { bool _isNewVersionAvailable(String latestVersion) {
try { try {
List<int> currentParts = currentVersion.value List<int> currentParts = currentVersion.value
.split('.') .split('.')
.map((e) => int.parse(e.trim())) .map((e) => int.parse(e.trim()))
.toList(); .toList();
List<int> latestParts = List<int> latestParts =
latest.split('.').map((e) => int.parse(e.trim())).toList(); latestVersion.split('.').map((e) => int.parse(e.trim())).toList();
// 确保两个版本号长度一致,短的用0补齐
int maxLength = currentParts.length > latestParts.length
? currentParts.length
: latestParts.length;
for (int i = 0; i < latestParts.length; i++) { while (currentParts.length < maxLength) {
int current = (i < currentParts.length) ? currentParts[i] : 0; currentParts.add(0);
if (latestParts[i] > current) return true;
if (latestParts[i] < current) return false;
} }
return false; while (latestParts.length < maxLength) {
latestParts.add(0);
}
// 比较每一段,只要有不同就返回true(支持升级和降级)
for (int i = 0; i < maxLength; i++) {
if (latestParts[i] != currentParts[i]) {
return true; // 版本不同,允许更新(升级或降级)
}
}
return false; // 版本完全相同,无需更新
} catch (e) { } catch (e) {
Logger().d('版本比较错误: $e'); Logger().d('版本比较错误: $e');
return false; return false;
@ -654,7 +668,7 @@ class OtaUpgradeController extends GetxController {
color: Colors.green[600], color: Colors.green[600],
size: 28, size: 28,
), ),
SizedBox(width: 12), SizedBox(width: 12.w),
Text( Text(
'升级成功', '升级成功',
style: TextStyle( style: TextStyle(

246
lib/modules/pairing/controllers/pairing_controller.dart

@ -1,12 +1,10 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:app_settings/app_settings.dart'; import 'package:app_settings/app_settings.dart';
import 'package:voitrans/data/models/appconfig_model.dart'; import 'package:voitrans/data/models/appconfig_model.dart';
import '/data/models/appconfig.dart'; import '/data/models/appconfig.dart';
import '/data/models/user_Info.dart'; import '/data/models/user_Info.dart';
import '/data/services/network/api.dart'; import '/data/services/network/api.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'dart:async'; import 'dart:async';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
@ -46,7 +44,7 @@ enum ErrorType {
} }
/// 配对控制器 - 基于简化状态机的实现 /// 配对控制器 - 基于简化状态机的实现
class PairingController extends GetxController { class PairingController extends GetxController with WidgetsBindingObserver {
static const String _tag = 'PairingCtrl'; static const String _tag = 'PairingCtrl';
static PairingController get to => Get.find(); static PairingController get to => Get.find();
@ -102,6 +100,11 @@ class PairingController extends GetxController {
super.onInit(); super.onInit();
Logger.i(_tag, '配对控制器初始化'); Logger.i(_tag, '配对控制器初始化');
// 注册应用生命周期监听器
Logger.i(_tag, '正在注册应用生命周期监听器');
WidgetsBinding.instance.addObserver(this);
Logger.i(_tag, '应用生命周期监听器注册完成');
// 确保服务已初始化 // 确保服务已初始化
_bluetoothManager = Get.find<BluetoothManager>(); _bluetoothManager = Get.find<BluetoothManager>();
_bleManager = Get.find<BleManager>(); _bleManager = Get.find<BleManager>();
@ -125,6 +128,10 @@ class PairingController extends GetxController {
@override @override
void onClose() { void onClose() {
Logger.i(_tag, '配对控制器销毁'); Logger.i(_tag, '配对控制器销毁');
// 移除应用生命周期监听器
WidgetsBinding.instance.removeObserver(this);
// 清理所有Worker // 清理所有Worker
for (var worker in _workers) { for (var worker in _workers) {
worker.dispose(); worker.dispose();
@ -137,6 +144,109 @@ class PairingController extends GetxController {
super.onClose(); super.onClose();
} }
/// 应用生命周期状态变化回调
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
// 使用多种日志级别确保输出
Logger.i(_tag, '=== 应用生命周期状态变化: $state ===');
print('PairingCtrl: 应用生命周期状态变化: $state'); // 直接使用print确保输出
switch (state) {
case AppLifecycleState.resumed:
// 应用从后台切回前台
Logger.i(_tag, '=== 应用从后台切回前台 ===');
print('PairingCtrl: 应用从后台切回前台');
_onAppResumed();
break;
case AppLifecycleState.paused:
// 应用切到后台
Logger.i(_tag, '应用切到后台');
print('PairingCtrl: 应用切到后台');
_onAppPaused();
break;
case AppLifecycleState.inactive:
// 应用处于非活跃状态(如来电话时)
Logger.i(_tag, '应用处于非活跃状态');
print('PairingCtrl: 应用处于非活跃状态');
break;
case AppLifecycleState.detached:
// 应用即将被销毁
Logger.i(_tag, '应用即将被销毁');
print('PairingCtrl: 应用即将被销毁');
break;
case AppLifecycleState.hidden:
// 应用被隐藏(iOS 13+)
Logger.i(_tag, '应用被隐藏');
print('PairingCtrl: 应用被隐藏');
break;
}
}
/// 应用从后台切回前台时的处理逻辑
void _onAppResumed() {
Logger.i(_tag, '处理应用从后台切回前台事件');
// 刷新蓝牙连接状态
_bluetoothManager.refreshDeviceConnection();
// 根据当前配对状态执行相应操作
switch (_currentState.value) {
case PairingState.waitingClassic:
// 如果在等待经典蓝牙连接,重新检查连接状态
Logger.i(_tag, '重新检查经典蓝牙连接状态');
_checkCurrentStatus();
break;
case PairingState.waitingBle:
// 如果在等待BLE连接,重新检查BLE状态
Logger.i(_tag, '重新检查BLE连接状态');
_checkBleConnectionAfterResume();
break;
case PairingState.error:
// 如果在错误状态,根据错误类型决定是否自动重试
if (_errorType.value == ErrorType.bluetoothOff) {
Logger.i(_tag, '检查蓝牙是否已重新开启');
_checkCurrentStatus();
}
break;
default:
// 其他状态暂不处理
break;
}
}
/// 应用切到后台时的处理逻辑
void _onAppPaused() {
Logger.i(_tag, '处理应用切到后台事件');
// 可以在这里暂停一些不必要的操作,如蓝牙扫描等
if (_currentState.value == PairingState.associating) {
Logger.i(_tag, '应用切到后台,暂停BLE扫描');
_bleManager.stopScan();
}
}
/// 应用恢复后检查BLE连接状态
Future<void> _checkBleConnectionAfterResume() async {
try {
final currentBleState = await _bleManager.getConnectionState();
Logger.i(_tag, '应用恢复后BLE连接状态: $currentBleState');
if (currentBleState == 2) {
// BLE已连接,直接完成配对
Logger.i(_tag, '检测到BLE已连接,完成配对');
_bleTimeoutTimer?.cancel();
_transition(PairingState.completed);
} else {
// BLE未连接,重新启动连接流程
Logger.i(_tag, '重新启动BLE连接流程');
_startBleConnection();
}
} catch (e) {
Logger.e(_tag, '检查BLE连接状态失败: $e');
}
}
// ==================== 监听器设置 ==================== // ==================== 监听器设置 ====================
/// 集中设置所有监听器 /// 集中设置所有监听器
@ -181,7 +291,8 @@ class PairingController extends GetxController {
/// 处理蓝牙开关状态变化 /// 处理蓝牙开关状态变化
void _handleBluetoothStateChange(String state) { void _handleBluetoothStateChange(String state) {
if (state == 'off') { if (state == 'off') {
_setError(ErrorType.bluetoothOff, 'pleaseEnableBluetooth'.tr); _setError(
ErrorType.bluetoothOff, 'connectDeepSoundViaSystemBluetooth'.tr);
} else if (state == 'on' && _errorType.value == ErrorType.bluetoothOff) { } else if (state == 'on' && _errorType.value == ErrorType.bluetoothOff) {
// 蓝牙重新开启,恢复流程 // 蓝牙重新开启,恢复流程
Logger.i(_tag, '检测到蓝牙已开启,自动恢复配对流程'); Logger.i(_tag, '检测到蓝牙已开启,自动恢复配对流程');
@ -201,7 +312,7 @@ class PairingController extends GetxController {
case PairingState.waitingClassic: case PairingState.waitingClassic:
if (device != null) { if (device != null) {
// 有设备连接,进入验证阶段 // 有设备连接,进入验证阶段
Logger.i(_tag, '检测到经典蓝牙设备连接,开始验证'); Logger.d(_tag, '检测到经典蓝牙设备连接,开始验证');
_transition(PairingState.verifying); _transition(PairingState.verifying);
_verifyDevice(device); _verifyDevice(device);
} }
@ -336,7 +447,7 @@ class PairingController extends GetxController {
switch (_errorType.value) { switch (_errorType.value) {
case ErrorType.bluetoothOff: case ErrorType.bluetoothOff:
showConnectButton.value = true; showConnectButton.value = true;
connectButtonText.value = 'enableBluetooth'.tr; //开启蓝牙 connectButtonText.value = 'goToBluetoothSettings'.tr; //开启蓝牙
break; break;
case ErrorType.noClassicDevice: case ErrorType.noClassicDevice:
showConnectButton.value = true; showConnectButton.value = true;
@ -400,7 +511,8 @@ class PairingController extends GetxController {
final arePermissionsOk = results[1] as bool; final arePermissionsOk = results[1] as bool;
if (!isBluetoothOk) { if (!isBluetoothOk) {
_setError(ErrorType.bluetoothOff, 'pleaseEnableBluetooth'.tr); _setError(
ErrorType.bluetoothOff, 'connectDeepSoundViaSystemBluetooth'.tr);
return; return;
} }
@ -621,7 +733,7 @@ class PairingController extends GetxController {
); );
product = match.isNotEmpty ? match.first : null; product = match.isNotEmpty ? match.first : null;
} }
final currentBleState = await _bleManager.getConnectionState();
final names = AppConfig.allproductNames(); final names = AppConfig.allproductNames();
// 检查是否已有绑定的设备 // 检查是否已有绑定的设备
print("device.address${device.address}"); print("device.address${device.address}");
@ -643,6 +755,17 @@ class PairingController extends GetxController {
// 已经绑定 // 已经绑定
return 9; return 9;
} }
} else {
//未绑定设备
Logger.i(_tag, '验证时BLE状态: $currentBleState');
if (currentBleState == 2) {
//已连接 清理
// BleConnectionState.connected
Logger.i(_tag, 'BLE已连接,断开并清理缓存');
_bleManager.clearPairedDevices();
_bleManager.disconnect();
}
} }
if (Platform.isAndroid) { if (Platform.isAndroid) {
@ -670,11 +793,15 @@ class PairingController extends GetxController {
print( print(
'获取到 deviceName: $deviceName address: ${device.address} names:$names'); '获取到 deviceName: $deviceName address: ${device.address} names:$names');
if (names.contains(deviceName)) { if (names.contains(deviceName)) {
if (currentBleState == 2) {
//已连接直接返回
return 9;
}
await _bleManager.startScan(deviceName); await _bleManager.startScan(deviceName);
await Future.delayed(const Duration(seconds: 1)); await Future.delayed(const Duration(seconds: 1));
// 尝试最多读取 3 次设备信息,每次间隔 1 秒 // 尝试最多读取 3 次设备信息,每次间隔 1 秒
List<Map<String, String>>? deviceinfos; List<Map<String, String>>? deviceinfos;
for (int i = 0; i < 3; i++) { for (int i = 0; i < 15; i++) {
// 使用设备名称获取设备列表 // 使用设备名称获取设备列表
deviceinfos = await _bleManager.getDeviceList(deviceName); deviceinfos = await _bleManager.getDeviceList(deviceName);
print('获取到 deviceinfos: $deviceinfos'); print('获取到 deviceinfos: $deviceinfos');
@ -918,12 +1045,12 @@ class PairingController extends GetxController {
case ErrorType.bluetoothOff: case ErrorType.bluetoothOff:
// 引导用户去开启蓝牙 // 引导用户去开启蓝牙
Logger.i(_tag, '引导用户开启蓝牙'); Logger.i(_tag, '引导用户开启蓝牙');
AppSettings.openAppSettings(type: AppSettingsType.bluetooth); _openBluetoothSettings();
break; break;
case ErrorType.noClassicDevice: case ErrorType.noClassicDevice:
// 引导用户去蓝牙设置连接设备 // 引导用户去蓝牙设置连接设备
Logger.i(_tag, '引导用户去蓝牙设置'); Logger.i(_tag, '引导用户去蓝牙设置');
AppSettings.openAppSettings(type: AppSettingsType.bluetooth); _openBluetoothSettings();
break; break;
default: default:
// 其他错误情况,刷新并重试 // 其他错误情况,刷新并重试
@ -934,7 +1061,7 @@ class PairingController extends GetxController {
} else if (_currentState.value == PairingState.waitingClassic) { } else if (_currentState.value == PairingState.waitingClassic) {
// 等待经典蓝牙连接状态,引导去设置 // 等待经典蓝牙连接状态,引导去设置
Logger.i(_tag, '等待经典蓝牙连接,引导用户去蓝牙设置'); Logger.i(_tag, '等待经典蓝牙连接,引导用户去蓝牙设置');
AppSettings.openAppSettings(type: AppSettingsType.bluetooth); _openBluetoothSettings();
} else { } else {
// 其他情况,刷新设备连接 // 其他情况,刷新设备连接
_bluetoothManager.refreshDeviceConnection(); _bluetoothManager.refreshDeviceConnection();
@ -992,7 +1119,98 @@ class PairingController extends GetxController {
/// 打开蓝牙设置 /// 打开蓝牙设置
void openBluetoothSettings() { void openBluetoothSettings() {
AppSettings.openAppSettings(type: AppSettingsType.bluetooth); _openBluetoothSettings();
}
/// 打开蓝牙设置 - 简化版本
void _openBluetoothSettings() async {
if (Platform.isIOS) {
// iOS平台:直接显示用户指导对话框
_showBluetoothGuideDialog();
} else {
// Android平台使用原有方式
AppSettings.openAppSettings(type: AppSettingsType.bluetooth);
}
}
/// 显示蓝牙设置指导对话框
void _showBluetoothGuideDialog() {
Get.dialog(
AlertDialog(
title: Text('bluetoothGuideTitle'.tr),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('bluetoothGuideSteps'.tr),
SizedBox(height: 16.h),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('1. ',
style: TextStyle(
fontWeight: FontWeight.bold, fontSize: 14.sp)),
Expanded(
child: Text('bluetoothGuideStep1'.tr,
style: TextStyle(fontSize: 14.sp))),
],
),
SizedBox(height: 8.h),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('2. ',
style: TextStyle(
fontWeight: FontWeight.bold, fontSize: 14.sp)),
Expanded(
child: Text('bluetoothGuideStep2'.tr,
style: TextStyle(fontSize: 14.sp))),
],
),
SizedBox(height: 8.h),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('3. ',
style: TextStyle(
fontWeight: FontWeight.bold, fontSize: 14.sp)),
Expanded(
child: Text('bluetoothGuideStep3'.tr,
style: TextStyle(fontSize: 14.sp))),
],
),
SizedBox(height: 16.h),
Container(
padding: EdgeInsets.all(12.w),
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.1),
borderRadius: BorderRadius.circular(8.r),
border: Border.all(color: Colors.blue.withOpacity(0.3)),
),
child: Row(
children: [
Icon(Icons.info_outline, color: Colors.blue, size: 20.sp),
SizedBox(width: 8.w),
Expanded(
child: Text(
'bluetoothGuideTip'.tr,
style:
TextStyle(fontSize: 12.sp, color: Colors.blue[700]),
),
),
],
),
),
],
),
actions: [
TextButton(
onPressed: () => Get.back(),
child: Text('iKnow'.tr, style: TextStyle(fontSize: 14.sp)),
),
],
),
);
} }
/// 清除设备关联(在需要重新配对时调用) /// 清除设备关联(在需要重新配对时调用)

2
lib/modules/pairing/views/pairing_view.dart

@ -481,7 +481,7 @@ class _PairingViewState extends State<PairingView>
final errorType = controller.errorType; final errorType = controller.errorType;
switch (errorType) { switch (errorType) {
case ErrorType.bluetoothOff: case ErrorType.bluetoothOff:
return 'bluetoothTurnedOff'.tr; // 蓝牙已关闭,请打开蓝牙 return 'connectDeepSoundViaSystemBluetooth'.tr; // 蓝牙已关闭,请打开蓝牙
case ErrorType.noPermission: case ErrorType.noPermission:
return 'lackPermissions'.tr; // 缺少蓝牙权限 return 'lackPermissions'.tr; // 缺少蓝牙权限
case ErrorType.noClassicDevice: case ErrorType.noClassicDevice:

132
lib/modules/profile/views/profile_view.dart

@ -740,72 +740,72 @@ class EditProfileView extends GetView<ProfileController> {
}, },
isDarkMode: isDarkMode, isDarkMode: isDarkMode,
), ),
// Divider( Divider(
// height: 1, height: 1,
// color: isDarkMode color: isDarkMode
// ? Colors.white.withOpacity(0.1) ? Colors.white.withOpacity(0.1)
// : Colors.grey[200]), : Colors.grey[200]),
// // 清理缓存 // 清理缓存
// _buildSimpleNavigationSetting( _buildSimpleNavigationSetting(
// title: 'clean'.tr, // 清理缓存 title: 'clean'.tr, // 清理缓存
// icon: Icons.logout, icon: Icons.cleaning_services,
// iconBgColor: isDarkMode iconBgColor: isDarkMode
// ? Colors.red[900]!.withOpacity(0.3) ? Colors.red[900]!.withOpacity(0.3)
// : Colors.red[50]!, : Colors.red[50]!,
// iconColor: isDarkMode ? Colors.red[300]! : Colors.red[600]!, iconColor: isDarkMode ? Colors.red[300]! : Colors.red[600]!,
// titleColor: titleColor:
// isDarkMode ? Colors.red[300]! : Colors.red[600]!, isDarkMode ? Colors.red[300]! : Colors.red[600]!,
// onTap: () { onTap: () {
// // 退出登录确认对话框 // 退出登录确认对话框
// Get.dialog( Get.dialog(
// AlertDialog( AlertDialog(
// backgroundColor: backgroundColor:
// isDarkMode ? Colors.grey[900] : Colors.white, isDarkMode ? Colors.grey[900] : Colors.white,
// title: Text( title: Text(
// 'clean'.tr, // 清理缓存 'clean'.tr, // 清除缓存
// style: TextStyle( style: TextStyle(
// color: isDarkMode ? Colors.white : Colors.black87, color: isDarkMode ? Colors.white : Colors.black87,
// ), ),
// ), ),
// content: Text( content: Text(
// 'cleanTips'.tr, // 确定要退出当前账号吗? 'cleanTips'.tr, // 确定要清除个人缓存吗?
// style: TextStyle( style: TextStyle(
// color: color:
// isDarkMode ? Colors.white70 : Colors.black54, isDarkMode ? Colors.white70 : Colors.black54,
// ), ),
// ), ),
// actions: [ actions: [
// TextButton( TextButton(
// onPressed: () => Get.back(), onPressed: () => Get.back(),
// child: Text( child: Text(
// 'cancel'.tr, // 取消 'cancel'.tr, // 取消
// style: TextStyle( style: TextStyle(
// color: isDarkMode color: isDarkMode
// ? Colors.white70 ? Colors.white70
// : Colors.black54, : Colors.black54,
// ), ),
// ), ),
// ), ),
// TextButton( TextButton(
// onPressed: () { onPressed: () {
// Get.back(); Get.back();
// controller.clean(); controller.clean();
// }, },
// child: Text( child: Text(
// 'confirm'.tr, // 确定 'confirm'.tr, // 确定
// style: TextStyle( style: TextStyle(
// color: isDarkMode color: isDarkMode
// ? Colors.red[300] ? Colors.red[300]
// : Colors.red[600], : Colors.red[600],
// ), ),
// ), ),
// ), ),
// ], ],
// ), ),
// ); );
// }, },
// isDarkMode: isDarkMode, isDarkMode: isDarkMode,
// ), ),
Divider( Divider(
height: 1, height: 1,
color: isDarkMode color: isDarkMode

77
lib/modules/settings/controllers/settings_controller.dart

@ -1,12 +1,15 @@
import 'package:agent_service/agent_service.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:get_storage/get_storage.dart'; import 'package:get_storage/get_storage.dart';
import '../../../core/themes/theme_service.dart'; import '../../../core/themes/theme_service.dart';
import '../../../data/services/speech_factory.dart'; import '../../../data/services/speech_factory.dart';
import '../../../routes/app_routes.dart';
import '../models/language_model.dart'; import '../models/language_model.dart';
import 'package:logger/logger.dart'; import 'package:logger/logger.dart';
import '../../../data/services/ble_manager.dart'; import '../../../data/services/ble_manager.dart';
import '../../../data/services/language_manager.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
@ -244,11 +247,9 @@ class SettingsController extends GetxController {
final PackageInfo packageInfo = await PackageInfo.fromPlatform(); final PackageInfo packageInfo = await PackageInfo.fromPlatform();
appVersion.value = packageInfo.version; appVersion.value = packageInfo.version;
buildNumber.value = packageInfo.buildNumber; buildNumber.value = packageInfo.buildNumber;
print('版本信息: ${packageInfo.version}+${packageInfo.buildNumber}');
} catch (e) { } catch (e) {
appVersion.value = '未知'; appVersion.value = '未知';
buildNumber.value = '0'; buildNumber.value = '0';
print('获取版本信息失败: $e');
} }
} }
@ -381,18 +382,88 @@ class SettingsController extends GetxController {
} }
} }
void setLanguage(LanguageModel data) { Future<void> setLanguage(LanguageModel data) async {
if (data.value != 'system') { if (data.value != 'system') {
List languageList = data.value.split('_'); List languageList = data.value.split('_');
Get.updateLocale(Locale(languageList[0], languageList[1])); Get.updateLocale(Locale(languageList[0], languageList[1]));
} else { } else {
Get.updateLocale(Get.deviceLocale!); Get.updateLocale(Get.deviceLocale!);
} }
print("languageList: ${data.value}");
language.value = data; language.value = data;
_storage.write('language', data.value); _storage.write('language', data.value);
// 更新agent服务的语音识别语言设置
await _updateAgentLanguageSettings(data);
// 延迟一下确保语言更新完成,然后重新进入应用
await Future.delayed(const Duration(milliseconds: 500));
// 重新进入应用 - 回到启动页面重新初始化
Get.offAllNamed(Routes.splash);
Get.back(); Get.back();
} }
/// 更新agent服务的语音识别语言设置
Future<void> _updateAgentLanguageSettings(LanguageModel languageModel) async {
try {
final languageManager = Get.find<LanguageManager>();
List<String> supportedLanguages = [];
// 根据选择的语言设置对应的ASR语言代码
if (languageModel.value == 'system') {
// 跟随系统语言
final deviceLocale = Get.deviceLocale;
if (deviceLocale != null) {
final lang = deviceLocale.languageCode;
final country = deviceLocale.countryCode;
if (country != null && country.isNotEmpty) {
supportedLanguages.add('$lang-$country');
} else {
// 根据语言代码映射到对应的ASR代码
final asrCode = languageManager.getAsrCodeByShortCode(lang);
if (asrCode != null) {
supportedLanguages.add(asrCode);
} else {
supportedLanguages.add('en-US'); // 默认英语
}
}
} else {
supportedLanguages.add('en-US'); // 默认英语
}
} else {
// 根据选择的语言值映射到ASR代码
final parts = languageModel.value.split('_');
if (parts.length == 2) {
final asrCode = languageManager.getAsrCodeByShortCode(parts[0]);
if (asrCode != null) {
supportedLanguages.add(asrCode);
} else {
// 直接使用语言-国家格式
supportedLanguages.add('${parts[0]}-${parts[1]}');
}
}
}
// 确保至少有一个语言
if (supportedLanguages.isEmpty) {
supportedLanguages.add('en-US');
}
Logger().i('更新agent服务语音识别语言: ${supportedLanguages.join(', ')}');
// 调用agent服务更新语言设置
final success = await AgentService.setSupportedLanguages(supportedLanguages);
if (success) {
Logger().i('agent服务语言设置更新成功');
} else {
Logger().w('agent服务语言设置更新失败');
}
} catch (e) {
Logger().e('更新agent服务语言设置时出错: $e');
}
}
// 加载当前语音服务类型 // 加载当前语音服务类型
void _loadCurrentSpeechServiceType() { void _loadCurrentSpeechServiceType() {
try { try {

8
lib/modules/settings/views/permissions_view.dart

@ -156,7 +156,7 @@ class PermissionsView extends GetView<SettingsController> {
return 'phone'.tr; // 电话 return 'phone'.tr; // 电话
case Permission.sms: case Permission.sms:
return 'sms'.tr; // 短信 return 'sms'.tr; // 短信
case Permission.calendarFullAccess: case Permission.calendarWriteOnly:
return 'calendarWrite'.tr; // 日历写入 return 'calendarWrite'.tr; // 日历写入
case Permission.calendarFullAccess: case Permission.calendarFullAccess:
return 'calendarFullAccess'.tr; // 日历完整访问 return 'calendarFullAccess'.tr; // 日历完整访问
@ -174,7 +174,7 @@ class PermissionsView extends GetView<SettingsController> {
return 'phonePermissionDesc'.tr; // 用于拨打电话功能 return 'phonePermissionDesc'.tr; // 用于拨打电话功能
case Permission.sms: case Permission.sms:
return 'smsPermissionDesc'.tr; // 用于发送短信功能 return 'smsPermissionDesc'.tr; // 用于发送短信功能
case Permission.calendarFullAccess: case Permission.calendarWriteOnly:
return 'calendarWritePermissionDesc'.tr; // 用于创建日历事件和提醒 return 'calendarWritePermissionDesc'.tr; // 用于创建日历事件和提醒
case Permission.calendarFullAccess: case Permission.calendarFullAccess:
return 'calendarFullAccessPermissionDesc'.tr; // 用于读取和管理日历事件及提醒 return 'calendarFullAccessPermissionDesc'.tr; // 用于读取和管理日历事件及提醒
@ -349,7 +349,7 @@ class PermissionsView extends GetView<SettingsController> {
return 'phone'.tr; // 电话 return 'phone'.tr; // 电话
case Permission.sms: case Permission.sms:
return 'sms'.tr; // 短信 return 'sms'.tr; // 短信
case Permission.calendarFullAccess: case Permission.calendarWriteOnly:
return 'calendarWrite'.tr; // 日历写入 return 'calendarWrite'.tr; // 日历写入
case Permission.calendarFullAccess: case Permission.calendarFullAccess:
return 'calendarFullAccess'.tr; // 日历完整访问 return 'calendarFullAccess'.tr; // 日历完整访问
@ -367,7 +367,7 @@ class PermissionsView extends GetView<SettingsController> {
return 'phonePermissionDetailDesc'.tr; // 拨打电话功能需要电话权限 return 'phonePermissionDetailDesc'.tr; // 拨打电话功能需要电话权限
case Permission.sms: case Permission.sms:
return 'smsPermissionDetailDesc'.tr; // 发送短信功能需要短信权限 return 'smsPermissionDetailDesc'.tr; // 发送短信功能需要短信权限
case Permission.calendarFullAccess: case Permission.calendarWriteOnly:
return 'calendarWritePermissionDetailDesc'.tr; // 创建日历事件需要日历写入权限 return 'calendarWritePermissionDetailDesc'.tr; // 创建日历事件需要日历写入权限
case Permission.calendarFullAccess: case Permission.calendarFullAccess:
return 'calendarFullAccessPermissionDetailDesc'.tr; // 管理日历事件需要完整日历权限 return 'calendarFullAccessPermissionDetailDesc'.tr; // 管理日历事件需要完整日历权限

58
lib/modules/settings/views/settings_view.dart

@ -16,13 +16,6 @@ import '../../profile/views/unified_avatar.dart';
class SettingsView extends GetView<SettingsController> { class SettingsView extends GetView<SettingsController> {
const SettingsView({Key? key}) : super(key: key); const SettingsView({Key? key}) : super(key: key);
// 正确获取用户名方式
// String get userName => User.getname();
// String get deviceInfo => User.getdeviceInfo();
// String get userId => User.getuserId();
// String get standing => User.getstanding();
// String get avatar => User.getnavatar();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// 直接使用 Theme.of(context) 获取当前主题 // 直接使用 Theme.of(context) 获取当前主题
@ -425,29 +418,32 @@ class SettingsView extends GetView<SettingsController> {
}, },
isDarkMode: isDarkMode, isDarkMode: isDarkMode,
), ),
Divider( // if (Platform.isAndroid) ...[
height: 1, // Divider(
color: isDarkMode // height: 1,
? Colors.white.withOpacity(0.1) // color: isDarkMode
: Colors.grey[200]), // ? Colors.white.withOpacity(0.1)
// : Colors.grey[200]),
// // 隐私设置
// _buildSimpleNavigationSetting(
// title: 'privacySettings'.tr, // 隐私设置
// subtitle: 'privacySettingsInfo'.tr, // 管理数据收集和权限
// icon: Icons.shield_outlined,
// iconBgColor: isDarkMode
// ? Colors.purple[900]!.withOpacity(0.3)
// : Colors.purple[100]!,
// iconColor:
// isDarkMode ? Colors.purple[300]! : Colors.purple[600]!,
// titleColor: isDarkMode ? Colors.white : null,
// subtitleColor: isDarkMode ? Colors.white70 : null,
// onTap: () {
// AppSettings.openAppSettings();
// },
// isDarkMode: isDarkMode,
// ),
// ],
// 隐私设置
_buildSimpleNavigationSetting(
title: 'privacySettings'.tr, // 隐私设置
subtitle: 'privacySettingsInfo'.tr, // 管理数据收集和权限
icon: Icons.shield_outlined,
iconBgColor: isDarkMode
? Colors.purple[900]!.withOpacity(0.3)
: Colors.purple[100]!,
iconColor:
isDarkMode ? Colors.purple[300]! : Colors.purple[600]!,
titleColor: isDarkMode ? Colors.white : null,
subtitleColor: isDarkMode ? Colors.white70 : null,
onTap: () {
AppSettings.openAppSettings();
},
isDarkMode: isDarkMode,
),
Divider( Divider(
height: 1, height: 1,
color: isDarkMode color: isDarkMode
@ -595,13 +591,14 @@ class SettingsView extends GetView<SettingsController> {
onTap: () => Get.toNamed(Routes.OTA_UPGRADE), onTap: () => Get.toNamed(Routes.OTA_UPGRADE),
isDarkMode: isDarkMode, isDarkMode: isDarkMode,
), ),
// if (Platform.isAndroid) ...[
Divider( Divider(
height: 1, height: 1,
color: isDarkMode color: isDarkMode
? Colors.white.withOpacity(0.1) ? Colors.white.withOpacity(0.1)
: Colors.grey[200]), : Colors.grey[200]),
// 添加权限管理设置项 // 权限管理设置项
_buildSimpleNavigationSetting( _buildSimpleNavigationSetting(
title: 'permissionManagement'.tr, // 权限管理 title: 'permissionManagement'.tr, // 权限管理
subtitle: 'permissionManagementInfo'.tr, // 管理应用权限设置 subtitle: 'permissionManagementInfo'.tr, // 管理应用权限设置
@ -618,6 +615,7 @@ class SettingsView extends GetView<SettingsController> {
}, },
isDarkMode: isDarkMode, isDarkMode: isDarkMode,
), ),
// ]
], ],
), ),
), ),

269
lib/modules/splash/controllers/splash_controller.dart

@ -1,218 +1,213 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import '../../../data/models/appconfig.dart'; import 'package:app_tracking_transparency/app_tracking_transparency.dart';
import '../../../data/models/appconfig_model.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart'; import 'package:get_storage/get_storage.dart';
import '../../../data/models/appconfig.dart';
import '../../../data/models/appconfig_model.dart';
import '../../../data/models/user_Info.dart'; import '../../../data/models/user_Info.dart';
import '../../../routes/app_routes.dart'; import '../../../routes/app_routes.dart';
import '../../../core/themes/theme_service.dart';
import '../../../data/services/network/api.dart'; import '../../../data/services/network/api.dart';
import '../../../data/services/version_update_service.dart'; import '../../../data/services/version_update_service.dart';
import '../../../modules/common/widgets/version_update_dialog.dart'; import '../../../modules/common/widgets/version_update_dialog.dart';
import '../../../modules/common/widgets/privacy_policy_dialog.dart'; import '../../../modules/common/widgets/privacy_policy_dialog.dart';
import '../../common/webview_page.dart'; import '../../../core/utils/logger.dart';
class SplashController extends GetxController { class SplashController extends GetxController {
final GetStorage _storage = GetStorage(); final GetStorage _storage = GetStorage();
static const String _tag = 'SplashCtrl';
// 进度条动画控制 // 进度条动画控制
final _loadingProgress = 0.0.obs; final _loadingProgress = 0.0.obs;
double get loadingProgress => _loadingProgress.value; double get loadingProgress => _loadingProgress.value;
// 资源加载状态 // 使用一个反应式变量来决定是否可以导航,替代所有布尔标记
final _isResourceLoaded = false.obs; final _canNavigate = false.obs;
bool get isResourceLoaded => _isResourceLoaded.value;
final _hasAcceptedPrivacy = false.obs; //是否同意隐私政策
// 版本号
//final String version = 'V1.0.0';
// 主题服务
final ThemeService _themeService = ThemeService();
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
// 监听 _canNavigate 状态,一旦为 true,立即且仅执行一次导航
// 首先检查隐私政策状态 once(_canNavigate, (_) => _navigateToTargetPage());
final hasAcceptedPrivacy = _storage.read('hasAcceptedPrivacy') ?? false; // 启动整个初始化流程
WidgetsBinding.instance.addPostFrameCallback((_) { _initializeApp();
if (!hasAcceptedPrivacy) {
// 优先显示隐私政策弹窗,暂停其他初始化
_showPrivacyDialog();
} else {
// 已同意隐私政策,正常启动流程
_startNormalFlow();
}
});
} }
void _startNormalFlow() { /// 启动并协调所有初始化任务
_startLoadingAnimation(); Future<void> _initializeApp() async {
_loadAppResources(); // 步骤 1: 检查并等待隐私政策同意
} final hasAccepted = await _checkAndRequestPrivacy();
if (!hasAccepted) {
// 如果用户拒绝隐私政策,应用会退出,后续代码不执行
return;
}
void _onPrivacyAccepted() { // 步骤 2: 并行执行所有启动任务,并等待它们全部完成
_storage.write('hasAcceptedPrivacy', true); // Future.wait 会等待数组中所有的 Future 都完成后才继续
Get.back(); // 关闭弹窗 await Future.wait([
_startNormalFlow(); // 开始正常流程 _startLoadingAnimation(), // 任务1: 最小动画时长
} _performParallelTasks(), // 任务2: 其他所有后台任务
]);
void _onPrivacyRejected() { // 步骤 3: 所有任务(动画和后台)都已完成,触发导航
exit(0); // 退出应用 _canNavigate.value = true;
} }
// 显示隐私政策弹窗 /// 检查隐私政策,如果未同意,则显示弹窗并等待用户操作
void _showPrivacyDialog() { /// 返回用户是否同意
PrivacyPolicyDialog.show( Future<bool> _checkAndRequestPrivacy() async {
onAccept: () { if (_storage.read<bool>('hasAcceptedPrivacy') ?? false) {
_onPrivacyAccepted(); return true;
_hasAcceptedPrivacy.value = true; }
},
onReject: _onPrivacyRejected, final completer = Completer<bool>();
canDismiss: false, // 禁止通过返回键关闭弹窗 WidgetsBinding.instance.addPostFrameCallback((_) {
); PrivacyPolicyDialog.show(
onAccept: () {
_storage.write('hasAcceptedPrivacy', true);
Get.back(); // 关闭弹窗
if (!completer.isCompleted) completer.complete(true);
},
onReject: () {
if (!completer.isCompleted) completer.complete(false);
exit(0); // 退出应用
},
canDismiss: false,
);
});
return completer.future;
} }
// 初始化地图SDK /// 动画任务:返回一个在动画结束时完成的 Future
// void _initMapSDK() { Future<void> _startLoadingAnimation() {
// try { const totalDuration = 1500; // 动画总时长1.5秒
// AmapFlutter().initSdk( const steps = totalDuration ~/ 30;
// iosKey: "a4a1394fe817c2f86a424b897b4a9af4",
// androidKey: "78e7946dd602778df82c6ae143a88184",
// );
// } catch (e) {
// print('地图SDK初始化失败: $e');
// }
// }
// 进度条动画(持续2秒)
void _startLoadingAnimation() {
const totalDuration = 1500; // 动画总时长增加到1.5秒
const steps = totalDuration ~/ 30; // 30的间隔,更平滑的动画
const stepValue = 1.0 / steps; const stepValue = 1.0 / steps;
final completer = Completer<void>();
Timer.periodic(const Duration(milliseconds: 30), (timer) { Timer.periodic(const Duration(milliseconds: 30), (timer) {
if (_loadingProgress.value < 1.0) { if (_loadingProgress.value < 1.0) {
_loadingProgress.value += stepValue; _loadingProgress.value += stepValue;
} else { } else {
_loadingProgress.value = 1.0;
timer.cancel(); timer.cancel();
_checkNavigateCondition(); if (!completer.isCompleted) completer.complete();
} }
}); });
}
// 异步加载应用资源 return completer.future;
Future<void> _loadAppResources() async { }
try {
// 模拟资源加载(如API配置、本地缓存等)
await Future.delayed(const Duration(milliseconds: 1500));
// 实际资源加载代码 /// 将所有并行的后台任务打包成一个 Future
// await _loadImages(); Future<void> _performParallelTasks() async {
// await _initDatabase(); // 同样使用 Future.wait,确保此函数在所有子任务完成后才结束
await Future.wait([
_requestTrackingPermissionAsync(),
_validateTokenAsync(),
_performVersionCheckAsync(),
]);
Logger.d(_tag, '所有后台任务已完成');
}
// 检查版本更新(在后台进行,不阻塞启动流程) /// 异步请求追踪权限
//_checkVersionUpdate(); Future<void> _requestTrackingPermissionAsync() async {
try {
await AppTrackingTransparency.requestTrackingAuthorization();
Logger.d(_tag, '用户追踪权限请求完成');
} catch (e) {
Logger.e(_tag, '用户追踪权限请求失败: $e');
// 即使失败,也视为完成,不阻塞主流程
}
}
_isResourceLoaded.value = true; /// 异步验证Token和加载应用配置
_checkNavigateCondition(); Future<void> _validateTokenAsync() async {
try {
final token = _storage.read('logintoken');
if (token != null && token.isNotEmpty) {
final isLoggedIn = await getLoginToken();
if (isLoggedIn) {
// 只有登录成功,才获取App配置
await getAppConfig();
} else {
await User.clearUserSession(); //
}
}
} catch (e) { } catch (e) {
Get.snackbar('错误', '资源加载失败,请检查网络连接'); Logger.e(_tag, 'Token验证或配置加载失败: $e');
_isResourceLoaded.value = true; // 允许用户继续使用 await User.clearUserSession();
print('资源加载错误: $e');
} }
} }
// 检查跳转条件 /// 异步执行版本检查
void _checkNavigateCondition() { Future<void> _performVersionCheckAsync() async {
if (_loadingProgress.value >= 1.0 && _isResourceLoaded.value) { try {
_navigateToLogin(); final versionService = VersionUpdateService.to;
final updateInfo = await versionService.checkForUpdate();
if (updateInfo.hasUpdate) {
Logger.i(_tag, '发现新版本: ${updateInfo.latestVersion}');
final completer = Completer<void>();
// 在主线程中显示更新对话框
WidgetsBinding.instance.addPostFrameCallback((_) {
// 使用 whenComplete 确保无论弹窗如何关闭,Future 都会完成
VersionUpdateDialog.show(updateInfo).whenComplete(() {
if (!completer.isCompleted) completer.complete();
});
});
// 等待弹窗交互结束
await completer.future;
} else {
Logger.i(_tag, '当前已是最新版本');
}
} catch (e) {
Logger.e(_tag, '版本检查失败: $e');
// 即使检查失败,也视为任务完成
} }
} }
// 获取登录Token并跳转 /// 根据最终状态跳转到目标页面
Future<void> _navigateToLogin() async { void _navigateToTargetPage() {
bool isLoggedIn = false; // 导航时直接检查最终的数据状态,而不是依赖中间变量
final token = _storage.read('logintoken'); final token = _storage.read('logintoken');
print('getLoginToken:进来了${token}'); final isAppConfigInitialized = AppConfig.isInitialized();
if (token != null && token.isNotEmpty) { // 检查登录状态和配置初始化状态
isLoggedIn = await getLoginToken(); if (token != null && token.isNotEmpty && isAppConfigInitialized) {
}
if (isLoggedIn) {
await getAppConfig();
Get.offAllNamed(Routes.pairing); Get.offAllNamed(Routes.pairing);
} else { } else {
Get.offAllNamed(Routes.login); // 跳转到登录页 Get.offAllNamed(Routes.login);
} }
} }
/// 异步获取登录Token
Future<bool> getLoginToken() async { Future<bool> getLoginToken() async {
try { try {
print('getLoginToken:进来了');
//if (User.getname() != "") {
final response = await Api.tokenlogin(); final response = await Api.tokenlogin();
print('getLoginToken: $response'); if (response?['user'] != null && response['user'] != '') {
if (response == null) return false;
if (response['user'] != '') {
print('getLoginToken:进来了');
// 根据API设计调整
final user = User.fromJson(response); final user = User.fromJson(response);
// 存储用户信息(示例)
await _storage.write('user_info', user.toJson()); await _storage.write('user_info', user.toJson());
return true; return true;
} }
//return true;
//}
return false; return false;
} catch (e) { } catch (e) {
print('Token获取失败: $e'); Logger.e(_tag, 'Token获取失败: $e');
return false; return false;
} }
} }
Future<bool> getAppConfig() async { Future<bool> getAppConfig() async {
try { try {
print('getAppConfig:进来了');
//if (User.getname() != "") {
final response = await Api.getappconfig(); final response = await Api.getappconfig();
print('getAppConfig: $response');
if (response == null) return false; if (response == null) return false;
// 根据API设计调整
final resp = UserGetAppConfigResp.fromJson(response); final resp = UserGetAppConfigResp.fromJson(response);
print('getAppConfig:进来了');
// 存储用户信息(示例)
// _storage.write('ENV', resp.env);
// _storage.write('AGENTS', resp.agents);
// _storage.write('MCPS', resp.mcps);
AppConfig.initialize(resp); AppConfig.initialize(resp);
return true; return true;
} catch (e) { } catch (e) {
print('getAppConfig 取失败: $e'); Logger.e(_tag, 'getAppConfig 获取失败: $e');
return false; return false;
} }
} }
/// 检查版本更新(后台执行,不阻塞启动流程)
// void _checkVersionUpdate() {
// // 延迟执行版本检查,避免影响启动速度
// Timer(const Duration(seconds: 1), () async {
// try {
// final versionService = VersionUpdateService.to;
// final updateInfo = await versionService.checkForUpdate();
// if (updateInfo.hasUpdate) {
// // 显示更新对话框
// await VersionUpdateDialog.show(updateInfo);
// }
// } catch (e) {
// print('版本检查失败: $e');
// // 版本检查失败不影响用户使用,静默处理
// }
// });
// }
} }

11
lib/modules/translation/controllers/translation_controller.dart

@ -10,6 +10,7 @@ import 'package:intl/intl.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import '../../../data/services/ast_service.dart'; import '../../../data/services/ast_service.dart';
import '../../../data/services/bluetooth_manager.dart';
import '../../../data/services/music_manager.dart'; import '../../../data/services/music_manager.dart';
import '../../../data/services/volcano_translation_service.dart'; import '../../../data/services/volcano_translation_service.dart';
import '../../../data/services/tts_service.dart'; import '../../../data/services/tts_service.dart';
@ -31,6 +32,8 @@ class TranslationController extends GetxController {
static const String TAG = 'TranslationController'; static const String TAG = 'TranslationController';
// 服务 // 服务
final BluetoothManager _bluetoothManager = Get.find<BluetoothManager>();
final BleManager _bleManager = Get.find<BleManager>();
final AsrService _asrService = Get.find<AsrService>(); final AsrService _asrService = Get.find<AsrService>();
final VolcanoTranslationService _translationService = final VolcanoTranslationService _translationService =
Get.find<VolcanoTranslationService>(); Get.find<VolcanoTranslationService>();
@ -511,7 +514,7 @@ class TranslationController extends GetxController {
@override @override
void onClose() { void onClose() {
stopRecognition(); stopAll();
_recognitionSubscription?.cancel(); _recognitionSubscription?.cancel();
_recognitionSubscription = null; _recognitionSubscription = null;
_voiceInteractionSubscription?.cancel(); _voiceInteractionSubscription?.cancel();
@ -634,7 +637,11 @@ class TranslationController extends GetxController {
if (currentMode.value == 'faceToFace') { if (currentMode.value == 'faceToFace') {
_asrService.stopContinuousRecognition(); _asrService.stopContinuousRecognition();
} else { } else {
await _ttsService.setAudioOutputDevice(0); if (_bluetoothManager.currentDeviceRx.value != null) {
restoreOriginalAudioState();
} else {
disableBluetoothAudio();
}
await _asrService.startContinuousRecognition(_audioSourceType); await _asrService.startContinuousRecognition(_audioSourceType);
// 开始ASR活跃时长计时 // 开始ASR活跃时长计时
_startAsrActiveTracking(); _startAsrActiveTracking();

22
lib/modules/translation/views/translation_view.dart

@ -723,13 +723,15 @@ class TranslationView extends GetView<TranslationController> {
// 显示语言选择对话框 // 显示语言选择对话框
void _showLanguageSelectionDialog(bool isSource, bool isDarkMode) { void _showLanguageSelectionDialog(bool isSource, bool isDarkMode) {
Get.dialog( Get.dialog(Dialog(
Dialog( backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white,
backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white, shape: RoundedRectangleBorder(
shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16.r), // 统一圆角尺寸为16
borderRadius: BorderRadius.circular(16.r), // 统一圆角尺寸为16 ),
), alignment: isSource ? Alignment.centerLeft : Alignment.centerRight,
insetPadding: EdgeInsets.symmetric(horizontal: 20.w), insetPadding: EdgeInsets.symmetric(horizontal: 10.w),
child: SizedBox(
width: 0.4.sw,
child: Padding( child: Padding(
// 使用Padding替代Container的padding // 使用Padding替代Container的padding
padding: EdgeInsets.all(12.w), // 统一内边距 padding: EdgeInsets.all(12.w), // 统一内边距
@ -868,7 +870,7 @@ class TranslationView extends GetView<TranslationController> {
), ),
), ),
), ),
); ));
} }
// 构建加载更多指示器 // 构建加载更多指示器
@ -1096,7 +1098,7 @@ class TranslationView extends GetView<TranslationController> {
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.BOTTOM,
backgroundColor: Get.isDarkMode ? Colors.grey[800] : Colors.grey[200], backgroundColor: Get.isDarkMode ? Colors.grey[800] : Colors.grey[200],
colorText: Get.isDarkMode ? Colors.white : Colors.black, colorText: Get.isDarkMode ? Colors.white : Colors.black,
duration: const Duration(seconds: 2), duration: const Duration(milliseconds: 800),
); );
} catch (e) { } catch (e) {
// 显示删除失败提示 // 显示删除失败提示
@ -1106,7 +1108,7 @@ class TranslationView extends GetView<TranslationController> {
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red[400], backgroundColor: Colors.red[400],
colorText: Colors.white, colorText: Colors.white,
duration: const Duration(seconds: 3), duration: const Duration(milliseconds: 1500),
); );
} }
} }

175
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt

@ -81,6 +81,7 @@ object AgentService : CoroutineScope {
// 服务端连接 // 服务端连接
private var serverurl = "" private var serverurl = ""
private var usertoken = "" private var usertoken = ""
private var language = ""
// 历史聊天消息缓存 // 历史聊天消息缓存
private val historyMessages = JSONArray() private val historyMessages = JSONArray()
@ -197,8 +198,8 @@ object AgentService : CoroutineScope {
//获取语音识别的语种 //获取语音识别的语种
val supportedLanguagesConfig = config["supportedLanguages"] as? List<String> val supportedLanguagesConfig = config["supportedLanguages"] as? List<String>
val supportedLanguages = supportedLanguagesConfig?.toTypedArray() val supportedLanguages = supportedLanguagesConfig?.toTypedArray()
?: arrayOf("zh-CN", "en-US") // 默认支持多语言 ?: arrayOf("zh-CN") // 默认支持多语言
language = supportedLanguages[0]
val asrInitSuccess = initialize( val asrInitSuccess = initialize(
subscriptionKey = config["azureSpeechKey"]?.toString() ?: "", subscriptionKey = config["azureSpeechKey"]?.toString() ?: "",
region = config["azureSpeechRegion"]?.toString() ?: "", region = config["azureSpeechRegion"]?.toString() ?: "",
@ -316,6 +317,8 @@ object AgentService : CoroutineScope {
_isTtsSpeaking.set(true) _isTtsSpeaking.set(true)
restartIdleCheck() restartIdleCheck()
sendEvent("tts_started", mapOf("status" to "started")) sendEvent("tts_started", mapOf("status" to "started"))
// 停止气泡音,避免与TTS同时播放
audioPlayer?.stopAudio()
} }
TtsEventType.SYNTHESIS_COMPLETED -> { TtsEventType.SYNTHESIS_COMPLETED -> {
@ -328,11 +331,15 @@ object AgentService : CoroutineScope {
_isTtsSpeaking.set(false) _isTtsSpeaking.set(false)
restartIdleCheck() restartIdleCheck()
sendEvent("tts_canceled", mapOf("status" to "canceled")) sendEvent("tts_canceled", mapOf("status" to "canceled"))
// TTS取消时停止气泡音
audioPlayer?.stopAudio()
} }
TtsEventType.PLAYBACK_STARTED -> { //开始AI播报内容 TtsEventType.PLAYBACK_STARTED -> { //开始AI播报内容
restartIdleCheck() restartIdleCheck()
sendEvent("playback_started", mapOf("status" to "playback_started")) sendEvent("playback_started", mapOf("status" to "playback_started"))
// 确保气泡音已停止,避免与TTS播放重叠
audioPlayer?.stopAudio()
// 发送AI说话开始广播 // 发送AI说话开始广播
val intent = val intent =
Intent("com.yunqiinnovation.music_service.AI_SAY_START").apply { Intent("com.yunqiinnovation.music_service.AI_SAY_START").apply {
@ -355,6 +362,8 @@ object AgentService : CoroutineScope {
TtsEventType.ERROR -> { TtsEventType.ERROR -> {
_isTtsSpeaking.set(false) _isTtsSpeaking.set(false)
restartIdleCheck() restartIdleCheck()
// TTS错误时停止气泡音
audioPlayer?.stopAudio()
val params = event.params val params = event.params
val code = params["errorCode"] as? String ?: "UNKNOWN_ERROR" val code = params["errorCode"] as? String ?: "UNKNOWN_ERROR"
val message = params["errorMessage"] as? String ?: "未知错误" val message = params["errorMessage"] as? String ?: "未知错误"
@ -411,7 +420,7 @@ object AgentService : CoroutineScope {
try { try {
delay(maxIdleSeconds * 1000L) delay(maxIdleSeconds * 1000L)
if (isActive && isRecognitionActive && !hasSpeechDetected && !isTtsSpeaking && !isAiStreaming) { if (isActive && isRecognitionActive && !isTtsSpeaking && !isAiStreaming) {
stopRecognition() stopRecognition()
sendEvent( sendEvent(
"auto_stop", "auto_stop",
@ -513,8 +522,8 @@ object AgentService : CoroutineScope {
override fun onRecognizing(recognizing: String, detectedLanguage: String) { override fun onRecognizing(recognizing: String, detectedLanguage: String) {
if (recognizing.isNotEmpty()) { if (recognizing.isNotEmpty()) {
// 检测到语音,更新状态 // 检测到语音,更新状态
val previousHasSpeech = hasSpeechDetected val previousHasSpeech = hasSpeechDetected
_hasSpeechDetected.set(true) _hasSpeechDetected.set(true)
if (!previousHasSpeech) { if (!previousHasSpeech) {
restartIdleCheck() restartIdleCheck()
@ -663,6 +672,8 @@ object AgentService : CoroutineScope {
// 并行执行停止操作,加快响应速度 // 并行执行停止操作,加快响应速度
stopTts() stopTts()
stopAiStream() stopAiStream()
// 停止气泡音,避免在打断后继续播放
audioPlayer?.stopAudio()
// 记录打断耗时 // 记录打断耗时
@ -815,16 +826,12 @@ object AgentService : CoroutineScope {
// 先添加系统提示词 // 先添加系统提示词
if (systemPrompt.isNotEmpty()) { if (systemPrompt.isNotEmpty()) {
// val location = LocationService.getCachedLocation() ?: mapOf( val mapdata = UserPortrait.getAll().toMutableMap()
// "user_location" to "定位未开启", mapdata["language"] = language
// "user_lat" to "定位未开启", // Log.d(TAG, "liwei--------- 用户画像: ${mapdata}")
// "user_lng" to "定位未开启",
// )
val mapdata = UserPortrait.getAll()
Log.d(TAG, "liwei--------- 用户画像: ${mapdata}")
val _systemPrompt = Utils.fillTemplate(systemPrompt, mapdata) val _systemPrompt = Utils.fillTemplate(systemPrompt, mapdata)
messagesWithSystemPrompt.put(createSystemMessage(_systemPrompt)) messagesWithSystemPrompt.put(createSystemMessage(_systemPrompt))
Log.d(TAG, "liwei--------- 系统提示词: ${_systemPrompt}") // Log.d(TAG, "liwei--------- 系统提示词: ${_systemPrompt}")
} }
// 再添加历史消息 // 再添加历史消息
@ -893,6 +900,8 @@ object AgentService : CoroutineScope {
responseBuilder.append(token) responseBuilder.append(token)
if (speakResponse && reply && broadcast) { if (speakResponse && reply && broadcast) {
ttsService?.speakStream(token) ttsService?.speakStream(token)
// 在开始流式TTS时立即停止气泡音
audioPlayer?.stopAudio()
} }
if (reply && token.isNotEmpty()) { if (reply && token.isNotEmpty()) {
// 发送流式回复token // 发送流式回复token
@ -905,10 +914,12 @@ object AgentService : CoroutineScope {
override fun onComplete(sessionid: String) { override fun onComplete(sessionid: String) {
try { try {
if (sessionid == currsessionId) {
audioPlayer?.stopAudio()
}
// 视情况决定是否朗读回复 // 视情况决定是否朗读回复
if (speakResponse && reply && broadcast && sessionid == currsessionId) { if (speakResponse && reply && broadcast && sessionid == currsessionId) {
ttsService?.flushStream() ttsService?.flushStream()
audioPlayer?.stopAudio()
} }
val response = responseBuilder.toString() val response = responseBuilder.toString()
// 发送完整回复,包含是否有图片的标记 // 发送完整回复,包含是否有图片的标记
@ -1104,7 +1115,7 @@ object AgentService : CoroutineScope {
} }
/** /**
* 设置支持的语音识别语言 * 设置支持的语音识别语言,同时更新TTS语言设置
*/ */
fun setSupportedLanguages(languages: List<String>): Boolean { fun setSupportedLanguages(languages: List<String>): Boolean {
if (!isInitialized) { if (!isInitialized) {
@ -1118,33 +1129,50 @@ object AgentService : CoroutineScope {
sendEvent("error", mapOf("code" to "EMPTY_LANGUAGES", "message" to "语言列表不能为空")) sendEvent("error", mapOf("code" to "EMPTY_LANGUAGES", "message" to "语言列表不能为空"))
return false return false
} }
language = languages[0]
return try { return try {
Log.i(TAG, "设置支持的语言: ${languages.joinToString(", ")}") Log.i(TAG, "设置支持的语言: ${languages.joinToString(", ")}")
// 重新初始化ASR服务以应用新的语言设置
val success = azureAsrHelper?.initialize(
val asrSuccess = azureAsrHelper?.initialize(
subscriptionKey = this.azureSpeechKey, subscriptionKey = this.azureSpeechKey,
region = this.azureSpeechRegion, region = this.azureSpeechRegion,
supportedLanguages = languages.toTypedArray(), supportedLanguages = languages.toTypedArray(),
audioSourceType = audioSourceType audioSourceType = audioSourceType
) ?: false ) ?: false
if (success) { if (asrSuccess) {
Log.i(TAG, "语言设置更新成功")
recognizeCallback()
} else {
Log.w(TAG, "Azure ASR初始化失败")
}
// 同时更新TTS语言设置
val ttsSuccess = updateTtsLanguage(languages.first()) // 使用第一个语言作为TTS语言
val overallSuccess = asrSuccess && ttsSuccess
if (overallSuccess) {
Log.i(TAG, "ASR和TTS语言设置更新成功")
sendEvent("languages_updated", mapOf( sendEvent("languages_updated", mapOf(
"languages" to languages, "languages" to languages,
"status" to "success" "status" to "success",
"asr_success" to asrSuccess,
"tts_success" to ttsSuccess
)) ))
} else { } else {
Log.e(TAG, "语言设置更新失败") Log.e(TAG, "语言设置更新失败 - ASR: $asrSuccess, TTS: $ttsSuccess")
sendEvent("error", mapOf( sendEvent("error", mapOf(
"code" to "LANGUAGE_UPDATE_FAILED", "code" to "LANGUAGE_UPDATE_FAILED",
"message" to "语言设置更新失败" "message" to "语言设置更新失败 - ASR: $asrSuccess, TTS: $ttsSuccess"
)) ))
} }
success overallSuccess
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "设置支持语言异常: ${e.message}") Log.e(TAG, "设置支持语言异常: ${e.message}")
sendEvent("error", mapOf( sendEvent("error", mapOf(
@ -1155,6 +1183,105 @@ object AgentService : CoroutineScope {
} }
} }
/**
* 更新TTS语言设置
*
* @param languageCode ASR语言代码,如 "zh-CN", "en-US"
* @return 是否更新成功
*/
private fun updateTtsLanguage(languageCode: String): Boolean {
return try {
Log.i(TAG, "更新TTS语言设置: $languageCode")
// 根据ASR语言代码映射到对应的TTS语音角色
val voiceName = getTtsVoiceNameByAsrCode(languageCode)
if (voiceName.isNotEmpty()) {
// 设置TTS语音角色
val success = ttsService?.setVoice(voiceName) ?: false
if (success) {
Log.i(TAG, "TTS语音角色设置成功: $voiceName")
} else {
Log.w(TAG, "TTS语音角色设置失败: $voiceName")
}
success
} else {
Log.w(TAG, "未找到语言代码 $languageCode 对应的TTS语音角色")
false
}
} catch (e: Exception) {
Log.e(TAG, "更新TTS语言设置异常: ${e.message}")
false
}
}
/**
* 根据ASR语言代码获取对应的TTS语音角色名称
* 与LanguageManager中的映射保持一致
*
* @param asrCode ASR语言代码,如 "zh-CN", "en-US"
* @return TTS语音角色名称
*/
private fun getTtsVoiceNameByAsrCode(asrCode: String): String {
return when (asrCode) {
// 中文相关
"zh-CN" -> "zh-CN-XiaoxiaoNeural"
"zh-TW" -> "zh-TW-HsiaoChenNeural"
"zh-HK" -> "zh-HK-HiuMaanNeural"
// 英语相关
"en-US" -> "en-US-AriaNeural"
// 亚洲语言
"ja-JP" -> "ja-JP-NanamiNeural"
"ko-KR" -> "ko-KR-SunHiNeural"
"th-TH" -> "th-TH-PremwadeeNeural"
"vi-VN" -> "vi-VN-HoaiMyNeural"
"id-ID" -> "id-ID-GadisNeural"
"hi-IN" -> "hi-IN-SwaraNeural"
"ms-MY" -> "ms-MY-YasminNeural"
"bn-IN" -> "bn-IN-TanishaaNeural"
"ta-IN" -> "ta-IN-PallaviNeural"
"te-IN" -> "te-IN-ShrutiNeural"
"mr-IN" -> "mr-IN-AarohiNeural"
"ur-IN" -> "ur-IN-GulNeural"
"tr-TR" -> "tr-TR-EmelNeural"
"fa-IR" -> "fa-IR-DilaraNeural"
// 欧洲语言
"fr-FR" -> "fr-FR-DeniseNeural"
"es-ES" -> "es-ES-ElviraNeural"
"pt-BR" -> "pt-BR-FranciscaNeural"
"it-IT" -> "it-IT-ElsaNeural"
"de-DE" -> "de-DE-KatjaNeural"
"ru-RU" -> "ru-RU-SvetlanaNeural"
"pl-PL" -> "pl-PL-AgnieszkaNeural"
"nl-NL" -> "nl-NL-ColetteNeural"
"sv-SE" -> "sv-SE-SofieNeural"
"cs-CZ" -> "cs-CZ-VlastaNeural"
"el-GR" -> "el-GR-AthinaNeural"
"ro-RO" -> "ro-RO-AlinaNeural"
"hu-HU" -> "hu-HU-NoemiNeural"
"uk-UA" -> "uk-UA-PolinaNeural"
"da-DK" -> "da-DK-ChristelNeural"
"fi-FI" -> "fi-FI-NooraNeural"
"nb-NO" -> "nb-NO-IselinNeural"
"hr-HR" -> "hr-HR-GabrijelaNeural"
"ca-ES" -> "ca-ES-JoanaNeural"
// 阿拉伯语和非洲语言
"ar-EG" -> "ar-EG-SalmaNeural"
"sw-KE" -> "sw-KE-ZuriNeural"
else -> {
Log.w(TAG, "未支持的语言代码: $asrCode,使用默认英语语音")
"en-US-AriaNeural"
}
}
}
/** /**
* 保存聊天消息 * 保存聊天消息
*/ */

341
local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift

@ -59,6 +59,7 @@ class AgentServiceImpl: NSObject {
internal var isAiStreaming: Bool = false internal var isAiStreaming: Bool = false
var currsessionId: String = "" var currsessionId: String = ""
var language: String = ""
private var idleTimer: DispatchSourceTimer? private var idleTimer: DispatchSourceTimer?
private let maxIdleSeconds: TimeInterval = 10 private let maxIdleSeconds: TimeInterval = 10
@ -81,6 +82,7 @@ class AgentServiceImpl: NSObject {
// chatStorageHelper 现在是单例,不需要初始化 // chatStorageHelper 现在是单例,不需要初始化
UserPortrait.shared.initialize() UserPortrait.shared.initialize()
BleService.shared.setDelegate(self) BleService.shared.setDelegate(self)
AudioSessionHub.initialize()
} }
func addListener(_ listener: AgentServiceListener) { func addListener(_ listener: AgentServiceListener) {
@ -157,6 +159,9 @@ class AgentServiceImpl: NSObject {
if let openaivisionModel = config["openaivisionModel"] as? String { if let openaivisionModel = config["openaivisionModel"] as? String {
self.visionModel = openaivisionModel self.visionModel = openaivisionModel
} }
if let supportedLanguages = config["supportedLanguages"] as? [String] {
self.language = supportedLanguages[0]
}
if let customSystemPrompt = config["systemPrompt"] as? String, !customSystemPrompt.isEmpty { if let customSystemPrompt = config["systemPrompt"] as? String, !customSystemPrompt.isEmpty {
self.systemPrompt = customSystemPrompt self.systemPrompt = customSystemPrompt
} else { } else {
@ -207,7 +212,7 @@ class AgentServiceImpl: NSObject {
azureTtsHelper?.addListener(self) azureTtsHelper?.addListener(self)
// 获取支持的语言列表,支持从配置中读取 // 获取支持的语言列表,支持从配置中读取
let supportedLanguages = config["supportedLanguages"] as? [String] ?? ["zh-CN", "en-US"] let supportedLanguages = config["supportedLanguages"] as? [String] ?? ["zh-CN"]
guard let asrSuccess = azureAsrHelper?.initialize( guard let asrSuccess = azureAsrHelper?.initialize(
subscriptionKey: azureSpeechKey, subscriptionKey: azureSpeechKey,
@ -286,8 +291,8 @@ class AgentServiceImpl: NSObject {
// 使用更安全的方式检查self是否存在 // 使用更安全的方式检查self是否存在
DispatchQueue.main.async { [weak self] in DispatchQueue.main.async { [weak self] in
guard let self = self else { return } guard let self = self else { return }
if self.isRecognizing && !self.hasSpeechDetected && !self.isSpeaking && !self.isAiStreaming { if self.isRecognizing && !self.isSpeaking && !self.isAiStreaming {
self.stopRecognition() self.stopRecognition()
self.sendEvent(name: "auto_stop", data: [ self.sendEvent(name: "auto_stop", data: [
"reason": "idle_timeout", "reason": "idle_timeout",
@ -344,8 +349,14 @@ print("ai启动语音=\(audioSourceType)=\(useBle)")
return true return true
} }
func recognizeCallback(useBle: Bool = false) -> Bool { func recognizeCallback() -> Bool {
print("重新设置识别回调=")
guard let asrsetupSuccess = azureAsrHelper?.setupEventListeners(
callback: self
), asrsetupSuccess else {
sendError("初始化语音识别服务失败", code: "ASR_INIT_ERROR")
return false
}
return true return true
} }
@ -360,7 +371,12 @@ print("ai启动语音=\(audioSourceType)=\(useBle)")
return true return true
} }
func disableBluetoothAudio() {
azureAsrHelper?.disableBluetoothAudio()
}
func restoreOriginalAudioState() {
azureAsrHelper?.restoreOriginalAudioState()
}
func pushAudioData(_ audioData: Data) -> Bool { func pushAudioData(_ audioData: Data) -> Bool {
if !isRecognizing || !isInitialized { if !isRecognizing || !isInitialized {
return false return false
@ -453,8 +469,8 @@ audioStream.saveAudioDataTo(data: audioData)
processWithChatApiServiceInternal(userMessage: userMessage, displayText: text, speakResponse: speakResponse) processWithChatApiServiceInternal(userMessage: userMessage, displayText: text, speakResponse: speakResponse)
} }
private func processWithChatApiServiceInternal(userMessage: [String: Any], displayText: String, speakResponse: Bool = true, hasImage: Bool = false) { private func processWithChatApiServiceInternal(userMessage: [String: Any], displayText: String, speakResponse: Bool = true, hasImage: Bool = false,imagePath:String = "") {
os_log("开始处理ChatAPI请求: displayText=%{public}@, speakResponse=%{public}@, hasImage=%{public}@", os_log("开始处理ChatAPI请求: displayText=%{public}@, speakResponse=%{public}@, hasImage=%{public}@",
log: logger, type: .info, displayText, speakResponse ? "是" : "否", hasImage ? "是" : "否") log: logger, type: .info, displayText, speakResponse ? "是" : "否", hasImage ? "是" : "否")
guard let chatApiService = chatApiService else { guard let chatApiService = chatApiService else {
@ -475,9 +491,16 @@ audioStream.saveAudioDataTo(data: audioData)
var messages: [[String: Any]] = [] var messages: [[String: Any]] = []
if !systemPrompt.isEmpty { if !systemPrompt.isEmpty {
var info = UserPortrait.shared.getAll()
let info: [String: String] = UserPortrait.shared.getAll() info["language"] = self.language
// 添加调试日志
print("系统提示词模板: \(systemPrompt)")
print("用户画像数据: \(info)")
let _systemPrompt = fillTemplate(systemPrompt, with: info) let _systemPrompt = fillTemplate(systemPrompt, with: info)
print("替换后的系统提示词: \(_systemPrompt)")
messages.append(chatApiService.createSystemMessage(content: _systemPrompt)) messages.append(chatApiService.createSystemMessage(content: _systemPrompt))
os_log("添加系统提示消息", log: logger, type: .info) os_log("添加系统提示消息", log: logger, type: .info)
} }
@ -494,12 +517,14 @@ audioStream.saveAudioDataTo(data: audioData)
agentService: self, agentService: self,
speakResponse: speakResponse, speakResponse: speakResponse,
displayText: displayText, displayText: displayText,
hasImage: hasImage hasImage: hasImage,
imagePath: imagePath
) )
currsessionId = UUID().uuidString currsessionId = UUID().uuidString
os_log("设置回调并调用sendMessageStream", log: logger, type: .info) os_log("设置回调并调用sendMessageStream", log: logger, type: .info)
chatApiService.setStreamCallback(currsessionId,callback) chatApiService.setStreamCallback(currsessionId,callback)
chatApiService.sendMessageStream(currsessionId,messages: messages) chatApiService.sendMessageStream(currsessionId,messages: messages)
audioPlayer?.playAwaitSound()
os_log("sendMessageStream调用完成", log: logger, type: .info) os_log("sendMessageStream调用完成", log: logger, type: .info)
} }
@ -550,7 +575,8 @@ audioStream.saveAudioDataTo(data: audioData)
userMessage: userMessage, userMessage: userMessage,
displayText: displayText, displayText: displayText,
speakResponse: speakResponse, speakResponse: speakResponse,
hasImage: true hasImage: true,
imagePath: imagePath
) )
} }
} }
@ -877,53 +903,201 @@ private func jsonToString(_ json: [String: Any]) -> String? {
} }
/** /**
* 设置支持的语音识别语言 * 设置支持的语音识别语言,同时更新TTS语言设置
* *
* @param languages 支持的语言列表,如 ["zh-CN", "en-US"] * @param languages 支持的语言列表,如 ["zh-CN", "en-US"]
* @return 是否设置成功 * @return 是否设置成功
*/ */
func setSupportedLanguages(_ languages: [String]) -> Bool { func setSupportedLanguages(_ languages: [String]) -> Bool {
os_log("设置支持的语言: %{public}@", log: logger, type: .info, languages.joined(separator: ", ")) os_log("设置支持的语言: %{public}@", log: logger, type: .info, languages.joined(separator: ", "))
if !isInitialized { if !isInitialized {
os_log("服务未初始化,无法设置语言", log: logger, type: .error) os_log("服务未初始化,无法设置语言", log: logger, type: .error)
sendError("服务未初始化", code: "NOT_INITIALIZED") sendError("服务未初始化", code: "NOT_INITIALIZED")
return false return false
} }
if languages.isEmpty { if languages.isEmpty {
os_log("语言列表为空,使用默认语言", log: logger, type: .info) os_log("语言列表为空,使用默认语言", log: logger, type: .info)
return false return false
} }
self.language = languages[0]
// 如果正在识别,需要先停止 // 如果正在识别,需要先停止
let wasRecognizing = isRecognizing let wasRecognizing = isRecognizing
if wasRecognizing { if wasRecognizing {
_ = stopRecognition() _ = stopRecognition()
} }
// 重新初始化ASR引擎,使用新的语言设置 // 重新初始化ASR引擎,使用新的语言设置
guard let success = azureAsrHelper?.initialize( guard let asrSuccess = azureAsrHelper?.initialize(
subscriptionKey: azureSpeechKey, subscriptionKey: azureSpeechKey,
region: azureSpeechRegion, region: azureSpeechRegion,
supportedLanguages: languages, supportedLanguages: languages,
audioSourceType: .microphone audioSourceType: .microphone
), success else { ), asrSuccess else {
os_log("重新初始化ASR引擎失败", log: logger, type: .error) sendError("初始化语音识别服务失败", code: "ASR_INIT_ERROR")
sendError("重新初始化ASR引擎失败", code: "ASR_INIT_ERROR") return false
}
guard let asrsetupSuccess = azureAsrHelper?.setupEventListeners(
callback: self
), asrsetupSuccess else {
sendError("初始化语音识别服务失败", code: "ASR_INIT_ERROR")
return false return false
} }
os_log("成功设置支持的语言: %{public}@", log: logger, type: .info, languages.joined(separator: ", ")) // 同时更新TTS语言设置
let ttsSuccess = updateTtsLanguage(languages.first ?? "zh-CN")
let overallSuccess = asrSuccess && ttsSuccess
if overallSuccess {
os_log("ASR和TTS语言设置更新成功: %{public}@", log: logger, type: .info, languages.joined(separator: ", "))
} else {
os_log("语言设置更新失败 - ASR: %{public}@, TTS: %{public}@", log: logger, type: .error, String(asrSuccess), String(ttsSuccess))
}
// 如果之前正在识别,重新开始 // 如果之前正在识别,重新开始
if wasRecognizing { if wasRecognizing {
_ = startRecognition() _ = startRecognition()
} }
return true return overallSuccess
} }
/**
* 更新TTS语言设置
*
* @param languageCode ASR语言代码,如 "zh-CN", "en-US"
* @return 是否更新成功
*/
private func updateTtsLanguage(_ languageCode: String) -> Bool {
os_log("更新TTS语言设置: %{public}@", log: logger, type: .info, languageCode)
// 根据ASR语言代码映射到对应的TTS语音角色
let voiceName = getTtsVoiceNameByAsrCode(languageCode)
if !voiceName.isEmpty {
// 设置TTS语音角色
let success = azureTtsHelper?.setVoice(voiceName) ?? false
if success {
os_log("TTS语音角色设置成功: %{public}@", log: logger, type: .info, voiceName)
} else {
os_log("TTS语音角色设置失败: %{public}@", log: logger, type: .error, voiceName)
}
return success
} else {
os_log("未找到语言代码 %{public}@ 对应的TTS语音角色", log: logger, type: .error, languageCode)
return false
}
}
/**
* 根据ASR语言代码获取对应的TTS语音角色名称
* 与LanguageManager中的映射保持一致
*
* @param asrCode ASR语言代码,如 "zh-CN", "en-US"
* @return TTS语音角色名称
*/
private func getTtsVoiceNameByAsrCode(_ asrCode: String) -> String {
switch asrCode {
// 中文相关
case "zh-CN":
return "zh-CN-XiaoxiaoNeural"
case "zh-TW":
return "zh-TW-HsiaoChenNeural"
case "zh-HK":
return "zh-HK-HiuMaanNeural"
// 英语相关
case "en-US":
return "en-US-AriaNeural"
// 亚洲语言
case "ja-JP":
return "ja-JP-NanamiNeural"
case "ko-KR":
return "ko-KR-SunHiNeural"
case "th-TH":
return "th-TH-PremwadeeNeural"
case "vi-VN":
return "vi-VN-HoaiMyNeural"
case "id-ID":
return "id-ID-GadisNeural"
case "hi-IN":
return "hi-IN-SwaraNeural"
case "ms-MY":
return "ms-MY-YasminNeural"
case "bn-IN":
return "bn-IN-TanishaaNeural"
case "ta-IN":
return "ta-IN-PallaviNeural"
case "te-IN":
return "te-IN-ShrutiNeural"
case "mr-IN":
return "mr-IN-AarohiNeural"
case "ur-IN":
return "ur-IN-GulNeural"
case "tr-TR":
return "tr-TR-EmelNeural"
case "fa-IR":
return "fa-IR-DilaraNeural"
// 欧洲语言
case "fr-FR":
return "fr-FR-DeniseNeural"
case "es-ES":
return "es-ES-ElviraNeural"
case "pt-BR":
return "pt-BR-FranciscaNeural"
case "it-IT":
return "it-IT-ElsaNeural"
case "de-DE":
return "de-DE-KatjaNeural"
case "ru-RU":
return "ru-RU-SvetlanaNeural"
case "pl-PL":
return "pl-PL-AgnieszkaNeural"
case "nl-NL":
return "nl-NL-ColetteNeural"
case "sv-SE":
return "sv-SE-SofieNeural"
case "cs-CZ":
return "cs-CZ-VlastaNeural"
case "el-GR":
return "el-GR-AthinaNeural"
case "ro-RO":
return "ro-RO-AlinaNeural"
case "hu-HU":
return "hu-HU-NoemiNeural"
case "uk-UA":
return "uk-UA-PolinaNeural"
case "da-DK":
return "da-DK-ChristelNeural"
case "fi-FI":
return "fi-FI-NooraNeural"
case "nb-NO":
return "nb-NO-IselinNeural"
case "hr-HR":
return "hr-HR-GabrijelaNeural"
case "ca-ES":
return "ca-ES-JoanaNeural"
// 阿拉伯语和非洲语言
case "ar-EG":
return "ar-EG-SalmaNeural"
case "sw-KE":
return "sw-KE-ZuriNeural"
default:
os_log("未支持的语言代码: %{public}@,使用默认英语语音", log: logger, type: .error, asrCode)
return "en-US-AriaNeural"
}
}
func dispose() -> Bool { func dispose() -> Bool {
print("ai释放资源") print("ai释放资源")
if isRecognizing { if isRecognizing {
@ -965,19 +1139,22 @@ class ChatApiStreamCallback: StreamCallback {
private let speakResponse: Bool private let speakResponse: Bool
private let displayText: String private let displayText: String
private let hasImage: Bool private let hasImage: Bool
private let imagePath: String
private var responseBuilder = "" private var responseBuilder = ""
private var metadata = "" private var metadata = ""
private var userMetadata:[String: Any] = [:] private var userMetadata:[String: Any] = [:]
private var reply = true; //是否回复 private var reply = true; //是否回复
private var broadcast = true //是否播发 private var broadcast = true //是否播发
init(agentService: AgentServiceImpl, speakResponse: Bool, displayText: String, hasImage: Bool) { init(agentService: AgentServiceImpl, speakResponse: Bool, displayText: String, hasImage: Bool,imagePath:String) {
self.agentService = agentService self.agentService = agentService
self.speakResponse = speakResponse self.speakResponse = speakResponse
self.displayText = displayText self.displayText = displayText
self.hasImage = hasImage self.hasImage = hasImage
self.imagePath = imagePath
self.userMetadata["hasImage"] = hasImage
self.userMetadata["imagePath"] = imagePath
} }
func onUsage(_ sessionid: String,_ prompt_tokens: Int?,_ completion_tokens: Int?,_ total_tokens: Int?){ func onUsage(_ sessionid: String,_ prompt_tokens: Int?,_ completion_tokens: Int?,_ total_tokens: Int?){
@ -994,6 +1171,8 @@ class ChatApiStreamCallback: StreamCallback {
if speakResponse && reply && broadcast{ if speakResponse && reply && broadcast{
try agentService.azureTtsHelper?.speakStream(token) try agentService.azureTtsHelper?.speakStream(token)
// 在开始流式TTS时立即停止气泡音
// try agentService.audioPlayer?.stopAwaitSound()
} }
if (reply && token != "") { if (reply && token != "") {
try agentService.sendEvent(name: "assistant_token", data: ["token": token]) try agentService.sendEvent(name: "assistant_token", data: ["token": token])
@ -1007,6 +1186,12 @@ class ChatApiStreamCallback: StreamCallback {
do { do {
guard let agentService = try agentService else { return } guard let agentService = try agentService else { return }
if sessionId == agentService.currsessionId {
// 停止等待音效
agentService.audioPlayer?.stopAwaitSound()
}
if speakResponse && reply && broadcast && sessionId == agentService.currsessionId{ if speakResponse && reply && broadcast && sessionId == agentService.currsessionId{
agentService.azureTtsHelper?.flushStream() agentService.azureTtsHelper?.flushStream()
} }
@ -1045,6 +1230,11 @@ class ChatApiStreamCallback: StreamCallback {
do { do {
guard let agentService = try agentService else { return } guard let agentService = try agentService else { return }
if sessionId == agentService.currsessionId {
// 停止等待音效
agentService.audioPlayer?.stopAwaitSound()
}
os_log("ChatAPI错误: %{public}@", log: agentService.logger, type: .error, error.localizedDescription) os_log("ChatAPI错误: %{public}@", log: agentService.logger, type: .error, error.localizedDescription)
if (sessionId != agentService.currsessionId) { if (sessionId != agentService.currsessionId) {
return return
@ -1128,7 +1318,7 @@ extension AgentServiceImpl: TtsEventListener {
isSpeaking = true isSpeaking = true
restartIdleCheck() restartIdleCheck()
sendEvent(name: "tts_started", data: ["status": "started"]) sendEvent(name: "tts_started", data: ["status": "started"])
audioPlayer?.stopAwaitSound()
case .synthesisCompleted: case .synthesisCompleted:
isSpeaking = false isSpeaking = false
restartIdleCheck() restartIdleCheck()
@ -1208,8 +1398,13 @@ extension AgentServiceImpl: BleService.Callback {
class AudioPlayer { class AudioPlayer {
private var player: AVAudioPlayer? private var player: AVAudioPlayer?
private var callingPlayer: AVAudioPlayer? private var callingPlayer: AVAudioPlayer?
private var awaitPlayer: AVAudioPlayer? // 新增:等待音效播放器
private var awaitTimer: DispatchSourceTimer? // 新增:等待音效超时定时器
private let logger = OSLog(subsystem: "com.yunqiinnovation.agent_service", category: "AudioPlayer") private let logger = OSLog(subsystem: "com.yunqiinnovation.agent_service", category: "AudioPlayer")
private let audioQueue = DispatchQueue(label: "agent.audio.player", qos: .userInitiated) private let audioQueue = DispatchQueue(label: "agent.audio.player", qos: .userInitiated)
// 等待音效超时时间(秒)- 改为 var 以支持动态修改
private var awaitTimeoutSeconds: TimeInterval = 10.0
private var resourcesBundle: Bundle { private var resourcesBundle: Bundle {
#if SWIFT_PACKAGE #if SWIFT_PACKAGE
@ -1233,19 +1428,41 @@ class AudioPlayer {
func playStartSound() { func playStartSound() {
playSound(named: "start", fileType: "mp3") playSound(named: "start", fileType: "mp3")
} }
func playAwaitSound() {
// 先停止之前的等待音效和定时器
stopAwaitSound()
// 播放等待音效
playLoopSound(named: "await", fileType: "mp3", player: &awaitPlayer)
// 启动超时定时器
startAwaitTimeout()
}
func playStopSound() { func playStopSound() {
playSound(named: "stop", fileType: "mp3") playSound(named: "stop", fileType: "mp3")
} }
func playCallingSound() { func playCallingSound() {
playLoopSound(named: "calling", fileType: "mp3") playLoopSound(named: "calling", fileType: "mp3", player: &callingPlayer)
} }
func stopCallingSound() { func stopCallingSound() {
callingPlayer?.stop() callingPlayer?.stop()
callingPlayer = nil callingPlayer = nil
} }
func stopAwaitSound() {
// 停止音频播放
awaitPlayer?.stop()
awaitPlayer = nil
// 停止超时定时器
stopAwaitTimeout()
os_log("停止等待音效播放", log: logger, type: .debug)
}
private func playSound(named: String, fileType: String = "mp3") { private func playSound(named: String, fileType: String = "mp3") {
if let path = resourcesBundle.path(forResource: named, ofType: fileType) { if let path = resourcesBundle.path(forResource: named, ofType: fileType) {
@ -1258,12 +1475,14 @@ class AudioPlayer {
os_log("未找到音频文件: %{public}@.%{public}@ in bundle %{public}@", log: logger, type: .error, named, fileType, resourcesBundle.bundlePath) os_log("未找到音频文件: %{public}@.%{public}@ in bundle %{public}@", log: logger, type: .error, named, fileType, resourcesBundle.bundlePath)
} }
private func playLoopSound(named: String, fileType: String = "mp3") { private func playLoopSound(named: String, fileType: String = "mp3", player: inout AVAudioPlayer?) {
stopCallingSound() // 停止当前播放的循环音频
player?.stop()
player = nil
if let path = resourcesBundle.path(forResource: named, ofType: fileType) { if let path = resourcesBundle.path(forResource: named, ofType: fileType) {
let url = URL(fileURLWithPath: path) let url = URL(fileURLWithPath: path)
safePlayLoopAudio(url: url) safePlayLoopAudio(url: url, player: &player)
return return
} }
@ -1297,24 +1516,59 @@ class AudioPlayer {
} }
} }
private func safePlayLoopAudio(url: URL) { private func safePlayLoopAudio(url: URL, player: inout AVAudioPlayer?) {
do { do {
callingPlayer = try AVAudioPlayer(contentsOf: url) player = try AVAudioPlayer(contentsOf: url)
guard let callingPlayer = callingPlayer else { guard let audioPlayer = player else {
return return
} }
callingPlayer.numberOfLoops = -1 audioPlayer.numberOfLoops = -1 // 无限循环
callingPlayer.volume = 0.5 audioPlayer.volume = 0.5
callingPlayer.prepareToPlay() audioPlayer.prepareToPlay()
let playResult = callingPlayer.play() let playResult = audioPlayer.play()
} catch { } catch {
os_log("播放循环音频失败: %{public}@", log: logger, type: .error, error.localizedDescription) os_log("播放循环音频失败: %{public}@", log: logger, type: .error, error.localizedDescription)
} }
} }
private func startAwaitTimeout() {
// 停止之前的定时器
stopAwaitTimeout()
// 创建新的定时器
let timer = DispatchSource.makeTimerSource(queue: DispatchQueue.global(qos: .utility))
awaitTimer = timer
timer.schedule(deadline: .now() + awaitTimeoutSeconds)
timer.setEventHandler { [weak self] in
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
os_log("等待音效超时,自动停止播放 - 超时时间: %.0f秒",
log: self.logger, type: .info, self.awaitTimeoutSeconds)
self.stopAwaitSound()
}
}
timer.resume()
os_log("启动等待音效超时定时器,超时时间: %.0f秒",
log: logger, type: .debug, awaitTimeoutSeconds)
}
private func stopAwaitTimeout() {
awaitTimer?.cancel()
awaitTimer = nil
}
func setAwaitTimeout(_ seconds: TimeInterval) {
awaitTimeoutSeconds = max(2.0, seconds) // 最小5秒
os_log("设置等待音效超时时间: %.0f秒", log: logger, type: .info, awaitTimeoutSeconds)
}
} }
extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback { extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback {
@ -1343,6 +1597,7 @@ extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback {
hasSpeechDetected = true hasSpeechDetected = true
if !previousHasSpeech { if !previousHasSpeech {
print("onRecognizing restartIdleCheck")
restartIdleCheck() restartIdleCheck()
} }

15
local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServicePlugin.swift

@ -51,7 +51,14 @@ public class AgentServicePlugin: NSObject, FlutterPlugin {
case "stopConversation": case "stopConversation":
impl.stopRecognition() impl.stopRecognition()
result(true) result(true)
case "disableBluetoothAudio":
impl.disableBluetoothAudio();
return
case "restoreOriginalAudioState":
impl.restoreOriginalAudioState();
return
case "processTextInput": case "processTextInput":
guard let arguments = call.arguments as? [String: Any], guard let arguments = call.arguments as? [String: Any],
let text = arguments["text"] as? String else { let text = arguments["text"] as? String else {
@ -98,6 +105,10 @@ public class AgentServicePlugin: NSObject, FlutterPlugin {
let success = impl.interruptCurrentResponse() let success = impl.interruptCurrentResponse()
result(success) result(success)
case "stopAiSteam":
let success = impl.interruptCurrentResponse()
result(success)
case "dispose": case "dispose":
let success = impl.dispose() let success = impl.dispose()
result(success) result(success)
@ -172,4 +183,4 @@ extension AgentServiceEventHandler: AgentServiceListener {
} }
} }
} }
} }

230
local_plugins/agent_service/ios/agent_service/Sources/agent_service/AudioSessionHub.swift

@ -1,5 +1,7 @@
import AVFoundation import AVFoundation
import os.log import os.log
import MediaPlayer
import UIKit
public enum AudioIntent { public enum AudioIntent {
case voice case voice
@ -16,7 +18,14 @@ public class AudioSessionHub {
private var isInterrupted = false private var isInterrupted = false
private init() { private init() {
os_log("🎵 AudioSessionHub 开始初始化", log: logger, type: .info)
setupNotifications() setupNotifications()
os_log("🎵 AudioSessionHub 初始化完成", log: logger, type: .info)
}
public static func initialize() {
os_log("🎵 主动初始化 AudioSessionHub", log: shared.logger, type: .info)
_ = shared // 触发单例初始化
} }
public func begin(_ intent: AudioIntent) { public func begin(_ intent: AudioIntent) {
@ -299,12 +308,15 @@ public class AudioSessionHub {
} }
private func setupNotifications() { private func setupNotifications() {
os_log("开始设置通知监听器", log: logger, type: .info)
NotificationCenter.default.addObserver( NotificationCenter.default.addObserver(
self, self,
selector: #selector(handleAudioSessionInterruption), selector: #selector(handleAudioSessionInterruption),
name: AVAudioSession.interruptionNotification, name: AVAudioSession.interruptionNotification,
object: nil object: nil
) )
os_log("已注册音频会话中断监听器", log: logger, type: .info)
NotificationCenter.default.addObserver( NotificationCenter.default.addObserver(
self, self,
@ -312,6 +324,34 @@ public class AudioSessionHub {
name: AVAudioSession.routeChangeNotification, name: AVAudioSession.routeChangeNotification,
object: nil object: nil
) )
os_log("已注册音频路由变化监听器", log: logger, type: .info)
// 添加音频会话控制通知监听
NotificationCenter.default.addObserver(
self,
selector: #selector(handleAudioSessionBeginPlayback),
name: Notification.Name("AudioSessionBeginPlayback"),
object: nil
)
os_log("已注册播放音频会话开始监听器", log: logger, type: .info)
NotificationCenter.default.addObserver(
self,
selector: #selector(handleAudioSessionBeginVoice),
name: Notification.Name("AudioSessionBeginVoice"),
object: nil
)
os_log("已注册语音音频会话开始监听器", log: logger, type: .info)
NotificationCenter.default.addObserver(
self,
selector: #selector(handleVolumeControlRequest),
name: Notification.Name("VolumeControlRequest"),
object: nil
)
os_log("已注册音量控制请求监听器", log: logger, type: .info)
os_log("所有通知监听器设置完成", log: logger, type: .info)
} }
/// 处理音频中断 /// 处理音频中断
@ -431,7 +471,195 @@ public class AudioSessionHub {
} }
} }
/// 处理播放音频会话激活请求
@objc private func handleAudioSessionBeginPlayback(notification: Notification) {
os_log("收到播放音频会话激活请求", log: logger, type: .info)
begin(.playback)
}
/// 处理语音音频会话激活请求
@objc private func handleAudioSessionBeginVoice(notification: Notification) {
os_log("收到语音音频会话激活请求", log: logger, type: .info)
begin(.voice)
}
/// 处理音量控制请求
@objc private func handleVolumeControlRequest(notification: Notification) {
os_log("🔊 收到音量控制请求通知", log: logger, type: .info)
os_log("收到音量控制请求", log: logger, type: .info)
guard let volumeInfo = notification.object as? [String: Any],
let actionString = volumeInfo["action"] as? String else {
os_log("❌ 音量控制请求数据格式错误", log: logger, type: .error)
return
}
// 将字符串转换为 VolumeControlAction
let action: VolumeControlAction
switch actionString {
case "increase":
action = .increase
case "decrease":
action = .decrease
case "mute":
action = .mute
case "setMax":
action = .setMax
default:
os_log("未知的音量控制动作: %{public}@", log: logger, type: .error, actionString)
return
}
// 获取 value(如果有的话)
let value: Float?
if let valueObj = volumeInfo["value"], !(valueObj is NSNull) {
value = valueObj as? Float
} else {
value = nil
}
os_log("音量控制详情: %{public}@", log: logger, type: .info, actionString)
// 在这里集中处理所有音量控制逻辑
performVolumeControl(action: action, value: value)
}
private func performVolumeControl(action: VolumeControlAction, value: Float?) {
DispatchQueue.main.async {
self.performVolumeControlWithMPVolumeView(action: action, value: value) { success, message in
os_log("音量控制结果: %{public}@ - %{public}@", log: self.logger, type: .info,
success ? "成功" : "失败", message)
// 发送结果通知
let resultInfo = VolumeControlResult(success: success, message: message, action: action)
NotificationCenter.default.post(
name: Notification.Name("VolumeControlResult"),
object: resultInfo
)
}
}
}
private func performVolumeControlWithMPVolumeView(action: VolumeControlAction, value: Float?, completion: @escaping (Bool, String) -> Void) {
// 创建更大的 frame,有助于子视图正确初始化
let volumeView = MPVolumeView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
volumeView.showsVolumeSlider = true
volumeView.showsRouteButton = false
volumeView.alpha = 0.01 // 几乎透明
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let window = windowScene.windows.first else {
completion(false, "无法获取窗口")
return
}
window.addSubview(volumeView)
volumeView.setNeedsLayout()
volumeView.layoutIfNeeded()
var retryCount = 0
let maxRetries = 5
func attemptVolumeControl() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
os_log("尝试第 %d 次音量控制", log: self.logger, type: .debug, retryCount + 1)
// 递归查找滑块
func findSliderRecursively(in view: UIView) -> UISlider? {
if let slider = view as? UISlider {
return slider
}
for subview in view.subviews {
if let foundSlider = findSliderRecursively(in: subview) {
return foundSlider
}
}
return nil
}
if let slider = findSliderRecursively(in: volumeView) {
let currentValue = slider.value
let newValue: Float
switch action {
case .increase:
newValue = min(currentValue + 0.2, 1.0) // 从 0.1 改为 0.2 (20%)
case .decrease:
newValue = max(currentValue - 0.2, 0.0) // 从 0.1 改为 0.2 (20%)
case .mute:
newValue = 0.0
case .setMax:
newValue = 1.0
case .setValue(let val):
newValue = max(0.0, min(val, 1.0))
}
os_log("设置音量从 %.2f 到 %.2f", log: self.logger, type: .info, currentValue, newValue)
slider.value = newValue
slider.sendActions(for: .valueChanged)
slider.sendActions(for: .touchUpInside)
let message = self.getVolumeActionMessage(action: action)
completion(true, message)
volumeView.removeFromSuperview()
} else {
retryCount += 1
if retryCount < maxRetries {
os_log("音量控制重试 %d/%d", log: self.logger, type: .debug, retryCount, maxRetries)
attemptVolumeControl()
} else {
os_log("所有重试都失败,使用备选方案", log: self.logger, type: .error)
volumeView.removeFromSuperview()
// 备选方案:至少记录当前音量状态
let currentVolume = self.audioSession.outputVolume
let message = "音量控制请求已处理(当前音量: \(Int(currentVolume * 100))%)"
completion(true, message)
}
}
}
}
attemptVolumeControl()
}
private func getVolumeActionMessage(action: VolumeControlAction) -> String {
switch action {
case .increase:
return "音量增加成功"
case .decrease:
return "音量降低成功"
case .mute:
return "静音设置成功"
case .setMax:
return "最大音量设置成功"
case .setValue:
return "音量设置成功"
}
}
deinit { deinit {
NotificationCenter.default.removeObserver(self) NotificationCenter.default.removeObserver(self)
} }
} }
// 在文件顶部添加
enum VolumeControlAction {
case increase
case decrease
case mute
case setMax
case setValue(Float)
}
struct VolumeControlInfo {
let action: VolumeControlAction
let value: Float?
}
struct VolumeControlResult {
let success: Bool
let message: String
let action: VolumeControlAction
}

BIN
local_plugins/agent_service/ios/agent_service/Sources/agent_service/Resources/await.mp3

Binary file not shown.

45
local_plugins/agent_service/lib/agent_service.dart

@ -261,6 +261,33 @@ class AgentService {
} }
} }
/// 恢复原始音频状态
///
/// 恢复原始音频状态
/// 返回是否成功恢复原始音频状态
static Future<bool> restoreOriginalAudioState() async {
try {
final bool result =
await _channel.invokeMethod('restoreOriginalAudioState');
return result;
} on PlatformException catch (e) {
throw AgentServiceException(e.code, e.message ?? '恢复原始音频状态失败', e.details);
}
}
/// 禁用蓝牙音频
///
/// 禁用蓝牙音频
/// 返回是否成功禁用蓝牙音频
static Future<bool> disableBluetoothAudio() async {
try {
final bool result = await _channel.invokeMethod('disableBluetoothAudio');
return result;
} on PlatformException catch (e) {
throw AgentServiceException(e.code, e.message ?? '禁用蓝牙音频失败', e.details);
}
}
/// 停止对话 /// 停止对话
/// ///
/// 停止语音识别 /// 停止语音识别
@ -332,8 +359,6 @@ class AgentService {
} }
} }
/// 停止语音合成 /// 停止语音合成
static Future<bool> stopAiSteam() async { static Future<bool> stopAiSteam() async {
try { try {
@ -385,6 +410,7 @@ class AgentService {
/// ///
/// [imagePath] 图片文件路径 /// [imagePath] 图片文件路径
/// [text] 可选的文本描述或问题 /// [text] 可选的文本描述或问题
/// [imageUrl] 图片url
/// [speakResponse] 是否朗读响应 /// [speakResponse] 是否朗读响应
/// 返回是否成功处理图片 /// 返回是否成功处理图片
static Future<bool> processImageInput(String imagePath, String imageUrl, static Future<bool> processImageInput(String imagePath, String imageUrl,
@ -402,6 +428,21 @@ class AgentService {
} }
} }
/// 设置支持的语音识别语言
///
/// [languages] 支持的语言列表,如 ["zh-CN", "en-US"]
/// 返回是否设置成功
static Future<bool> setSupportedLanguages(List<String> 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<bool> dispose() async { static Future<bool> dispose() async {
try { try {

2
local_plugins/amap_service/ios/Classes/AmapService.swift

@ -24,7 +24,7 @@ public class AmapService: NSObject, AMapNaviDriveManagerDelegate {
// 设置高德地图服务 // 设置高德地图服务
// 1. 设置隐私合规 // 1. 设置隐私合规
AMapServices.shared()?.enableHTTPS = true AMapServices.shared()?.enableHTTPS = true
AMapServices.shared()?.apiKey = "0b704e172b7ade5a7a18728e8f517a05" AMapServices.shared()?.apiKey = "6144a636c24dc9f2e1e57f7597849cf7"
} }
public func startNavigation(from start: String, to end: String) { public func startNavigation(from start: String, to end: String) {

2
local_plugins/amap_service/ios/Classes/AmapServicePlugin.swift

@ -22,7 +22,7 @@ public class AmapServicePlugin: NSObject, FlutterPlugin, FlutterStreamHandler {
// 高德SDK初始化 // 高德SDK初始化
DispatchQueue.main.async { DispatchQueue.main.async {
AMapServices.shared()?.apiKey = "0b704e172b7ade5a7a18728e8f517a05" AMapServices.shared()?.apiKey = "6144a636c24dc9f2e1e57f7597849cf7"
AMapServices.shared()?.enableHTTPS = true AMapServices.shared()?.enableHTTPS = true
if #available(iOS 13.0, *) { if #available(iOS 13.0, *) {

2
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt

@ -760,7 +760,7 @@ class AzureAsrHelper(private val context: Context) {
*/ */
fun saveAudioDataTo(buffer: ByteArray) { fun saveAudioDataTo(buffer: ByteArray) {
if (audioSourceType == AudioSourceType.MICROPHONE) return if (audioSourceType == AudioSourceType.MICROPHONE||!isContinuousRecognitionActive) return
// 放入队列,由写线程写入 // 放入队列,由写线程写入
writeQueue.offer(buffer.copyOf()) writeQueue.offer(buffer.copyOf())

583
local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift

@ -32,6 +32,7 @@ public class AzureAsrHelper: NSObject {
private var subscriptionKey = "" private var subscriptionKey = ""
private var region = "" private var region = ""
// 音频源配置 // 音频源配置
public enum AudioSourceType { public enum AudioSourceType {
/** 使用设备麦克风 */ /** 使用设备麦克风 */
@ -59,10 +60,14 @@ public class AzureAsrHelper: NSObject {
* @param audioSourceType 音频源类型 * @param audioSourceType 音频源类型
* @return 初始化是否成功 * @return 初始化是否成功
*/ */
/**
* 初始化Azure语音服务
* 预初始化关键组件以提升首次启动性能
*/
public func initialize( public func initialize(
subscriptionKey: String, subscriptionKey: String,
region: String, region: String,
supportedLanguages: [String] = ["zh-CN"], supportedLanguages: [String] = ["zh-CN", "en-US"],
audioSourceType: AudioSourceType = .microphone audioSourceType: AudioSourceType = .microphone
) -> Bool { ) -> Bool {
do { do {
@ -93,23 +98,26 @@ public class AzureAsrHelper: NSObject {
} }
// 创建语音配置 // 创建语音配置
speechConfig = try SPXSpeechConfiguration(subscription: subscriptionKey, region: region) speechConfig = try SPXSpeechConfiguration(subscription: subscriptionKey, region: region)
// 针对翻译场景的性能优化
speechConfig?.setPropertyTo("800", byName: "SpeechServiceConnection_EndSilenceTimeoutMs") // optimizeForTranslation()
speechConfig?.setPropertyTo("800", byName: "Speech_SegmentationSilenceTimeoutMs")
speechConfig?.setPropertyTo("Time", byName: "Speech_SegmentationStrategy")
if isAutoDetectLanguage { if isAutoDetectLanguage {
// 直接启用语言检测模式 speechConfig?.setPropertyTo("Continuous", by: SPXPropertyId.speechServiceConnectionLanguageIdMode)
speechConfig?.setPropertyTo("Continuous", byName: "SpeechServiceConnection_LanguageIdMode") // 不在这里设置语言,让SPXAutoDetectSourceLanguageConfiguration处理
os_log("启用语言检测模式", log: log, type: .info) os_log("启用语言检测模式,支持语言: %{public}@", log: log, type: .info, supportedLanguages.joined(separator: ","))
} else { } else {
// 设置指定的识别语言
speechConfig?.speechRecognitionLanguage = currentLanguage speechConfig?.speechRecognitionLanguage = currentLanguage
os_log("固定语言模式,当前语言: %{public}@", log: log, type: .info, currentLanguage)
} }
// 录音文件类 // 录音文件类
recordfile = RecordFile() recordfile = RecordFile()
// 创建识别器
// 预初始化音频组件
preInitializeAudioComponents()
return true return true
} catch { } catch {
os_log("初始化失败: %{public}@", log: log, type: .error, error.localizedDescription) os_log("初始化失败: %{public}@", log: log, type: .error, error.localizedDescription)
@ -117,7 +125,27 @@ public class AzureAsrHelper: NSObject {
} }
} }
/**
* 预初始化音频组件
* 在初始化阶段就准备好音频流和相关组件,减少首次启动延迟
*/
private func preInitializeAudioComponents() {
if audioStream == nil {
audioStream = AudioStream(parentHelper: self)
audioStream?.initAudioRecord()
audioStream?.onAudioData = { [weak self] data in
self?.recordfile?.saveAudioDataToWav(data)
}
// 预创建音频配置
if let pushStream = audioStream?.pushAudioStream {
audioConfig = SPXAudioConfiguration(streamInput: pushStream)
os_log("预初始化音频组件完成", log: log, type: .info)
}
}
}
/** /**
* 开始连续语音识别 * 开始连续语音识别
* *
@ -129,11 +157,13 @@ public class AzureAsrHelper: NSObject {
audioSourceType: AudioSourceType = .microphone audioSourceType: AudioSourceType = .microphone
) -> Bool { ) -> Bool {
guard speechConfig != nil else { guard speechConfig != nil else {
print("语音服务未初始化")
//callback.onError("语音服务未初始化") //callback.onError("语音服务未初始化")
return false return false
} }
if _isContinuousRecognitionActive { if _isContinuousRecognitionActive {
print("连续识别已在进行中")
return true return true
} }
@ -143,6 +173,7 @@ public class AzureAsrHelper: NSObject {
do { do {
guard recognizer != nil else { guard recognizer != nil else {
if !setupRecognizer() { if !setupRecognizer() {
print("重置识别器失败")
// callback.onError("重置识别器失败") // callback.onError("重置识别器失败")
return false return false
} }
@ -165,6 +196,7 @@ public class AzureAsrHelper: NSObject {
return false return false
} }
} }
/** /**
* 停止连续语音识别 * 停止连续语音识别
@ -281,33 +313,45 @@ public class AzureAsrHelper: NSObject {
/** /**
* 设置识别器 * 设置识别器
*/ */
/**
* 设置识别器
* 优化:避免重复创建音频流,复用已初始化的组件
*/
private func setupRecognizer() -> Bool { private func setupRecognizer() -> Bool {
do { do {
print("设置识别器${_isContinuousRecognitionActive}") print("设置识别器\(_isContinuousRecognitionActive)")
// 如果正在进行连续识别,先停止 // 如果正在进行连续识别,先停止
if _isContinuousRecognitionActive { if _isContinuousRecognitionActive {
// 直接停止,不等待结果
try? recognizer?.stopContinuousRecognition() try? recognizer?.stopContinuousRecognition()
_isContinuousRecognitionActive = false _isContinuousRecognitionActive = false
} }
setupMicrophoneStream()
// 【优化】只有在音频流未初始化时才创建,避免重复初始化
if audioStream == nil || audioConfig == nil {
setupMicrophoneStream()
}
// 清理旧的识别器 // 清理旧的识别器
recognizer = nil recognizer = nil
// 创建识别器 // 创建识别器
if isAutoDetectLanguage { if isAutoDetectLanguage {
// 使用更精确的语言配置方式 // 使用正确的初始化方法
var sourceLanguageConfigs: [SPXSourceLanguageConfiguration] = [] guard let autoDetectSourceLanguageConfig = try? SPXAutoDetectSourceLanguageConfiguration(supportedLanguages) else {
for language in supportedLanguages { print("创建自动语言检测配置失败,使用默认语言配置")
if let config = try? SPXSourceLanguageConfiguration(language) { // 如果创建失败,使用默认的单语言配置
sourceLanguageConfigs.append(config) recognizer = try SPXSpeechRecognizer(
} speechConfiguration: speechConfig!,
audioConfiguration: audioConfig!
)
return true
} }
let autoDetectConfig = try SPXAutoDetectSourceLanguageConfiguration(sourceLanguageConfigurations: sourceLanguageConfigs) print("autoDetectConfig: \(autoDetectSourceLanguageConfig)")
recognizer = try SPXSpeechRecognizer( recognizer = try SPXSpeechRecognizer(
speechConfiguration: speechConfig!, speechConfiguration: speechConfig!,
autoDetectSourceLanguageConfiguration: autoDetectConfig, autoDetectSourceLanguageConfiguration: autoDetectSourceLanguageConfig,
audioConfiguration: audioConfig! audioConfiguration: audioConfig!
) )
} else { } else {
@ -320,7 +364,7 @@ public class AzureAsrHelper: NSObject {
return true return true
} catch { } catch {
os_log("创建识别器失败: %{public}@", log: log, type: .error, error.localizedDescription) os_log("创建识别器失败: %{public}@", log: log, type: .error, error.localizedDescription)
try? recognizer?.stopContinuousRecognition() try? recognizer?.stopContinuousRecognition()
return false return false
} }
} }
@ -328,31 +372,111 @@ public class AzureAsrHelper: NSObject {
/** /**
* 设置麦克风流 - 使用推流方式 * 设置麦克风流 - 使用推流方式
* 优化:避免重复初始化已存在的音频流
*/ */
private func setupMicrophoneStream() { private func setupMicrophoneStream() {
do { do {
// 【优化】检查是否已经初始化,避免重复创建
if (audioStream == nil) { if audioStream == nil {
// 创建外部音频拉流对象 audioStream = AudioStream(parentHelper: self)
// 明确指定使用外部音频源(推流模式)
audioStream = AudioStream()
audioStream?.initAudioRecord() audioStream?.initAudioRecord()
audioStream?.onAudioData = { [weak self] data in audioStream?.onAudioData = { [weak self] data in
self?.recordfile?.saveAudioDataToWav(data) self?.recordfile?.saveAudioDataToWav(data)
} }
} }
// 创建音频配置 // 【优化】检查音频配置是否已存在
if let pushStream = audioStream?.pushAudioStream { // Safely unwrap optional if audioConfig == nil, let pushStream = audioStream?.pushAudioStream {
audioConfig = SPXAudioConfiguration(streamInput: pushStream) audioConfig = SPXAudioConfiguration(streamInput: pushStream)
os_log("设置麦克风流:") os_log("设置麦克风流完成", log: log, type: .info)
} }
} catch { } catch {
os_log("设置麦克风流失败: %{public}@", log: log, type: .error, error.localizedDescription) os_log("设置麦克风流失败: %{public}@", log: log, type: .error, error.localizedDescription)
audioStream = nil audioStream = nil
} }
} }
/**
* 从语音识别结果中获取检测到的语言
* @param result 语音识别结果
* @return 检测到的语言代码,如果检测失败则返回当前语言
*/
private func getDetectedLanguage(from result: SPXSpeechRecognitionResult) -> String {
guard isAutoDetectLanguage else {
return currentLanguage
}
print("result: \(result)")
// 备用方案:从属性中获取语言
if let properties = result.properties,
let language = properties.getPropertyByName("SpeechServiceResponse_RecognitionLanguage"),
!language.isEmpty {
print("从属性中检测到语言: \(language)")
return language
}
// 最后的备用方案:从JSON响应中解析
if let properties = result.properties,
let jsonString = properties.getPropertyByName("SpeechServiceResponse_Json"),
let jsonData = jsonString.data(using: .utf8) {
do {
if let json = try JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: Any],
let language = json["Language"] as? String {
print("从JSON中检测到语言: \(language)")
return language
}
} catch {
print("JSON解析失败: \(error)")
}
}
// 使用正确的SPXAutoDetectSourceLanguageResult初始化方法
do {
let langResult = try SPXAutoDetectSourceLanguageResult(result)
if let language = langResult.language, !language.isEmpty {
print("SDK方法检测到语言: \(language)")
return language
}
} catch {
print("SDK语言检测失败: \(error.localizedDescription)")
}
print("无法检测语言,使用默认语言: \(currentLanguage)")
return currentLanguage
}
/**
* 动态切换语言(翻译场景中的语言切换)
*
* @param newSourceLanguage 新的源语言
* @param newTargetLanguage 新的目标语言
* @return 切换是否成功
*/
public func switchTranslationLanguages(
newSourceLanguage: String,
newTargetLanguage: String
) -> Bool {
let validatedSourceLang = LanguageDetector.convertToAzureLanguageCode(newSourceLanguage)
let validatedTargetLang = LanguageDetector.convertToAzureLanguageCode(newTargetLanguage)
// 更新支持的语言
self.supportedLanguages = [validatedSourceLang, validatedTargetLang]
self.currentLanguage = validatedSourceLang
// 重新配置语言检测
self.isAutoDetectLanguage = (validatedSourceLang != validatedTargetLang)
// 如果正在识别,需要重新设置识别器
if _isContinuousRecognitionActive {
let wasActive = stopContinuousRecognition()
if wasActive {
return setupRecognizer()
}
}
os_log("语言已切换: %{public}@ -> %{public}@", log: log, type: .info, validatedSourceLang, validatedTargetLang)
return true
}
/** /**
* 设置事件监听器 * 设置事件监听器
*/ */
@ -366,68 +490,50 @@ public class AzureAsrHelper: NSObject {
guard let recognizer = recognizer else { return false} guard let recognizer = recognizer else { return false}
// 正在识别事件 // 正在识别事件
recognizer.addRecognizingEventHandler { [weak self] (_, event) in recognizer.addRecognizingEventHandler { [weak self] (_, event) in
guard let self = self else { return } guard let self = self else { return }
let result = event.result let result = event.result
print("识别中文本: \(result.text)") guard !(result.text?.isEmpty ?? true) else { return }
// 优化:只处理非空结果
if !(result.text?.isEmpty ?? true) { // 使用现有的 getDetectedLanguage 方法替代 result.language
let detectedLanguage: String let detectedLanguage = self.getDetectedLanguage(from: result)
if self.isAutoDetectLanguage { let text = result.text
do {
let langResult = try SPXAutoDetectSourceLanguageResult(result) print("正在识别事件=检测到语言: \(detectedLanguage), 识别中: \(text)")
let rawLanguage = langResult.language // // 检测文本语言,避免重复调用
print("[DEBUG] 原始检测语言: \(rawLanguage ?? "nil"), 文本: \(result.text ?? "nil"), 支持的语言: \(self.supportedLanguages)") // let detectedLanguage = self.detectTextLanguage(result.text ?? "") ?? "zh-CN"
// 应用启发式语言检测纠正 // // 输出检测到的语言信息
detectedLanguage = self.correctLanguageDetection(rawLanguage: rawLanguage ?? "", text: result.text ?? "") // print("检测到的语言: \(detectedLanguage)")
print("[DEBUG] 纠正后语言: \(detectedLanguage)")
} catch { DispatchQueue.main.async {
print("[DEBUG] 语言检测异常: \(error.localizedDescription)") callback.onRecognizing(result.text ?? "", detectedLanguage)
detectedLanguage = ""
}
} else {
detectedLanguage = self.currentLanguage
print("[DEBUG] 使用当前语言: \(detectedLanguage)")
}
print("识别中事件: \(detectedLanguage)")
DispatchQueue.main.async {
callback.onRecognizing(result.text ?? "", detectedLanguage)
}
} }
} }
// 识别完成事件 // 识别完成事件
recognizer.addRecognizedEventHandler { [weak self] (_, event) in recognizer.addRecognizedEventHandler { [weak self] (_, event) in
guard let self = self else { return } guard let self = self else { return }
let result = event.result let result = event.result
print("识别完成文本: \(result.text)") guard result.reason == .recognizedSpeech,
if result.reason == .recognizedSpeech && !(result.text?.isEmpty ?? true) { !(result.text?.isEmpty ?? true) else { return }
let detectedLanguage: String
if self.isAutoDetectLanguage { // 使用现有的 getDetectedLanguage 方法替代 result.language
do { let detectedLanguage = self.getDetectedLanguage(from: result)
let langResult = try SPXAutoDetectSourceLanguageResult(result) let text = result.text
let rawLanguage = langResult.language
print("[DEBUG] 识别完成 - 原始检测语言: \(rawLanguage ?? "nil"), 文本: \(result.text ?? "nil"), 支持的语言: \(self.supportedLanguages)") print("识别完成事件=检测到语言: \(detectedLanguage), 识别中: \(text)")
// 检测文本语言,避免重复调用
// 应用启发式语言检测纠正 // let detectedLanguage = self.detectTextLanguage(result.text ?? "") ?? "zh-CN"
let correctedLanguage = self.correctLanguageDetection(rawLanguage: rawLanguage ?? "", text: result.text ?? "")
detectedLanguage = correctedLanguage.isEmpty ? (self.supportedLanguages.first ?? self.currentLanguage) : correctedLanguage // // 输出检测到的语言信息
print("[DEBUG] 识别完成 - 纠正后语言: \(detectedLanguage)") // print("检测到的语言: \(detectedLanguage)")
} catch {
print("[DEBUG] 识别完成 - 语言检测异常: \(error.localizedDescription)") // 在主线程回调结果
detectedLanguage = self.supportedLanguages.first ?? self.currentLanguage DispatchQueue.main.async {
} callback.onResult(result.text ?? "", detectedLanguage)
} else {
detectedLanguage = self.currentLanguage
print("[DEBUG] 识别完成 - 使用当前语言: \(detectedLanguage)")
}
print("识别完成事件: \(detectedLanguage)")
DispatchQueue.main.async {
callback.onResult(result.text ?? "", detectedLanguage)
}
} }
}
}
// 会话开始事件 // 会话开始事件
recognizer.addSessionStartedEventHandler { (sender, event) in recognizer.addSessionStartedEventHandler { (sender, event) in
// 直接在当前线程调用回调 // 直接在当前线程调用回调
@ -440,8 +546,8 @@ public class AzureAsrHelper: NSObject {
guard let self = self else { return } guard let self = self else { return }
print("会话结束事件:") print("会话结束事件:")
// 直接在当前线程调用回调 // 直接在当前线程调用回调
// callback.onSessionStopped() callback.onSessionStopped()
// self._isContinuousRecognitionActive = false self._isContinuousRecognitionActive = false
//self.stopAudioProcessing() //self.stopAudioProcessing()
} }
@ -466,6 +572,7 @@ public class AzureAsrHelper: NSObject {
public func disableBluetoothAudio() { public func disableBluetoothAudio() {
do { do {
print("禁用蓝牙音频功能,切换回正常音频模式")
audioStream?.setAudioOutputRoute(.speaker) audioStream?.setAudioOutputRoute(.speaker)
} catch { } catch {
print("disableBluetoothAudio") print("disableBluetoothAudio")
@ -477,6 +584,7 @@ public class AzureAsrHelper: NSObject {
public func restoreOriginalAudioState() { public func restoreOriginalAudioState() {
do { do {
print("恢复原始音频设备状态(通常是重新启用蓝牙)")
audioStream?.setAudioOutputRoute(.receiver) audioStream?.setAudioOutputRoute(.receiver)
} catch { } catch {
print("restoreOriginalAudioState") print("restoreOriginalAudioState")
@ -550,21 +658,38 @@ public class AzureAsrHelper: NSObject {
private var isRunning = false private var isRunning = false
public var isWriting = false public var isWriting = false
private let bufferSize: Int = 4096 private let bufferSize: Int = 4096
private var writeThread: DispatchQueue? private var writeThread: DispatchQueue?
private var currentRoute: AudioOutputRoute? private var currentRoute: AudioOutputRoute?
public var onAudioData: ((Data) -> Void)? public var onAudioData: ((Data) -> Void)?
// 添加对外部类的弱引用
private weak var parentHelper: AzureAsrHelper?
// 添加初始化方法,接收外部类引用
init(parentHelper: AzureAsrHelper) {
self.parentHelper = parentHelper
super.init()
}
/** /**
* 初始化 * 初始化
*/ */
public func initAudioRecord() { /**
* 初始化音频录制组件
* 包括音频格式、推流、音频引擎等核心组件的初始化
*/
public func initAudioRecord() {
print("初始化了") print("初始化了")
audioFormat = getOptimalAudioFormat() audioFormat = getOptimalAudioFormat()
pushAudioStream = SPXPushAudioInputStream() pushAudioStream = SPXPushAudioInputStream()
isRunning=true
// 初始化音频引擎
audioEngine = AVAudioEngine()
isRunning = true
// 创建新的写线程 // 创建新的写线程
writeThread = DispatchQueue(label: "audio.stream.writer") writeThread = DispatchQueue(label: "audio.stream.writer")
startWriteThread() startWriteThread()
self.setAudioOutputRoute(.receiver) //self.setAudioOutputRoute(.receiver)
} }
/// 获取最佳音频格式 (iOS 通常支持标准采样率) /// 获取最佳音频格式 (iOS 通常支持标准采样率)
@ -634,9 +759,14 @@ public class AzureAsrHelper: NSObject {
guard let dataToWrite = self.writeQueue.take() else { continue } guard let dataToWrite = self.writeQueue.take() else { continue }
print("写入数据长度: \(dataToWrite.count)") // print("写入数据长度: \(dataToWrite.count)")
self.pushAudioStream?.write(dataToWrite) do {
self.onAudioData?(dataToWrite) try self.pushAudioStream?.write(dataToWrite)
self.onAudioData?(dataToWrite)
} catch {
print("推送音频数据失败: \(error.localizedDescription)")
}
} }
} }
} }
@ -648,73 +778,101 @@ public class AzureAsrHelper: NSObject {
* @param data 音频数据字节数组 * @param data 音频数据字节数组
*/ */
public func saveAudioDataTo(data: Data) { public func saveAudioDataTo(data: Data) {
if audioSourceType != .external { // 通过父类引用调用方法
if audioSourceType != .external || !(parentHelper?.isContinuousRecognitionActive() ?? false) {
return return
} }
print("外部data=\(data)") // print("外部data=\(data)")
// 放入队列,由写线程写入 // 放入队列,由写线程写入
writeQueue.put(data) writeQueue.put(data)
} }
private func runMicrophoneCapture() { // 私有方法:启动麦克风捕获
do { /**
audioEngine = AVAudioEngine() * 启动麦克风捕获
guard let inputNode = audioEngine?.inputNode else { * 优化:复用已初始化的音频引擎,减少启动延迟
throw NSError(domain: "AudioSetup", code: 1) */
} private func runMicrophoneCapture() {
// 推荐直接用系统 format do {
let hardwareFormat = inputNode.inputFormat(forBus: 0) // 【优化】检查音频引擎是否已初始化,避免重复创建
// // ===== 新增:启用专业级语音处理 ===== if audioEngine == nil {
if #available(iOS 13.0, *) { audioEngine = AVAudioEngine()
try inputNode.setVoiceProcessingEnabled(true) }
print("Voice processing enabled")
// 如果音频引擎正在运行,先停止
} if audioEngine?.isRunning == true {
audioEngine?.stop()
}
// Create converter to target format
guard let targetFormat = audioFormat, // 获取音频输入节点(麦克风)
let converter = AVAudioConverter(from: hardwareFormat, to: targetFormat) else { guard let inputNode = audioEngine?.inputNode else {
throw NSError(domain: "AudioSetup", code: 2) throw NSError(domain: "AudioSetup", code: 1, userInfo: [NSLocalizedDescriptionKey: "无法获取音频输入节点"])
} }
inputNode.installTap(onBus: 0, bufferSize: UInt32(bufferSize), format: hardwareFormat) { // 移除之前的音频处理块,避免重复添加
[weak self] buffer, time in inputNode.removeTap(onBus: 0)
guard let self = self, self.isWriting else { return }
// 获取硬件支持的原始音频格式
// Convert to target format let hardwareFormat = inputNode.inputFormat(forBus: 0)
let convertedBuffer = AVAudioPCMBuffer(
pcmFormat: targetFormat, // iOS 13+ 启用语音处理
frameCapacity: AVAudioFrameCount(targetFormat.sampleRate * Double(buffer.frameLength) / buffer.format.sampleRate) if #available(iOS 13.0, *) {
)! try inputNode.setVoiceProcessingEnabled(true)
///print("进入 tap 回调,frameLength: \(buffer.frameLength)") }
var error: NSError?
let status = converter.convert( // 检查目标音频格式和转换器是否可用
to: convertedBuffer, guard let targetFormat = audioFormat, // 外部定义的期望音频格式
error: &error, let converter = AVAudioConverter(from: hardwareFormat, to: targetFormat) else {
withInputFrom: { inNumPackets, outStatus in throw NSError(domain: "AudioSetup", code: 2)
outStatus.pointee = .haveData }
return buffer
} // 在输入节点上安装录音回调
) inputNode.installTap(onBus: 0,
//print("转换状态: \(status.rawValue), 错误: \(String(describing: error))") bufferSize: UInt32(bufferSize), // 每次回调的缓冲区大小
format: hardwareFormat) { // 使用原始硬件格式
if status == .haveData, error == nil { [weak self] buffer, time in // 弱引用避免循环引用
let data = self.audioBufferToData(convertedBuffer)
// print("准备写入数据,大小: \(data.count)") // 确保实例存在且正在写入状态
guard let self = self, self.isWriting else { return }
self.writeQueue.put(data)
// 创建目标格式的音频缓冲区
} let convertedBuffer = AVAudioPCMBuffer(
pcmFormat: targetFormat,
// 计算转换后的帧容量(考虑采样率差异)
frameCapacity: AVAudioFrameCount(
targetFormat.sampleRate * Double(buffer.frameLength) / buffer.format.sampleRate
)
)!
var error: NSError?
// 执行音频格式转换
let status = converter.convert(
to: convertedBuffer,
error: &error,
withInputFrom: { inNumPackets, outStatus in
outStatus.pointee = .haveData // 标记有数据可用
return buffer // 返回原始音频数据
} }
// 重新激活 )
try audioSession.setActive(true, options: [.notifyOthersOnDeactivation])
try audioEngine?.start() // 转换成功且无错误
} catch { if status == .haveData, error == nil {
print("麦克风启动失败: \(error)") // 将音频缓冲区转换为二进制数据
let data = self.audioBufferToData(convertedBuffer)
// 将数据放入写入队列(后续处理)
self.writeQueue.put(data)
} }
} }
// 激活音频会话(允许录音)
try audioSession.setActive(true, options: [.notifyOthersOnDeactivation])
// 启动音频引擎
try audioEngine?.start()
} catch {
print("麦克风启动失败: \(error)")
}
}
private func audioBufferToData(_ buffer: AVAudioPCMBuffer) -> Data { private func audioBufferToData(_ buffer: AVAudioPCMBuffer) -> Data {
let frameLength = Int(buffer.frameLength) let frameLength = Int(buffer.frameLength)
let channelCount = 1 let channelCount = 1
@ -783,8 +941,7 @@ public class AzureAsrHelper: NSObject {
case bluetooth case bluetooth
} }
public func setAudioOutputRoute(_ route: AudioOutputRoute) { public func setAudioOutputRoute(_ route: AudioOutputRoute) {
guard currentRoute != route else { return }
print("setAudioOutputRoute")
do { do {
// //
if self.isWriting { if self.isWriting {
@ -794,14 +951,15 @@ public class AzureAsrHelper: NSObject {
try audioSession.setActive(false) try audioSession.setActive(false)
} }
print("setAudioOutputRoutecurrent,route=\(route)")
switch route { switch route {
case .speaker: case .speaker:
print("setAudioOutputRoutecurrent,speaker")
// 使用扬声器时必须用videoChat模式 // 使用扬声器时必须用videoChat模式
try audioSession.setCategory( try audioSession.setCategory(
.playAndRecord, .playAndRecord,
mode: .videoChat, mode: .videoChat,
options: [.allowBluetooth, .mixWithOthers, .defaultToSpeaker] options: [.defaultToSpeaker]
) )
try audioSession.overrideOutputAudioPort(.speaker) try audioSession.overrideOutputAudioPort(.speaker)
@ -810,7 +968,7 @@ public class AzureAsrHelper: NSObject {
try audioSession.setCategory( try audioSession.setCategory(
.playAndRecord, .playAndRecord,
mode: .voiceChat, mode: .voiceChat,
options: [.allowBluetooth, .mixWithOthers, .defaultToSpeaker] options: [.allowBluetooth]
) )
try audioSession.overrideOutputAudioPort(.none) try audioSession.overrideOutputAudioPort(.none)
@ -920,55 +1078,7 @@ public class AzureAsrHelper: NSObject {
} }
} }
/**
* 启发式语言检测纠正方法
* 基于文本内容特征来纠正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: - 回调函数 // MARK: - 回调函数
/** /**
@ -1017,3 +1127,54 @@ public class AzureAsrHelper: NSObject {
func onError(_ error: String) func onError(_ error: String)
} }
} }
// //ios 语言检测
// /**
// * 检测输入文本的语言,从支持的语言列表中选择最匹配的
// *
// * @param text 需要检测的文本
// * @return 从supportedLanguages中选择的最匹配语言代码
// */
// public func detectTextLanguage(_ text: String) -> String? {
// // 如果只有一种支持的语言,直接返回
// if supportedLanguages.count == 1 {
// return supportedLanguages[0]
// }
// // 获取所有可能的语言及其置信度
// let languageResults = LanguageDetector.detectLanguageWithConfidence(from: text)
// // 遍历检测结果,找到第一个在supportedLanguages中的语言
// for result in languageResults {
// let azureLanguageCode = LanguageDetector.convertToAzureLanguageCode(result.language)
// if supportedLanguages.contains(azureLanguageCode) {
// return azureLanguageCode
// }
// }
// // 如果没有找到匹配的语言,返回第一个支持的语言作为默认值
// return supportedLanguages.first
// }
// /**
// * 检测文本语言并返回详细信息
// *
// * @param text 需要检测的文本
// * @return 包含语言代码、置信度和语言名称的字典
// */
// public func detectTextLanguageWithDetails(_ text: String) -> [String: Any]? {
// let results = LanguageDetector.detectLanguageWithConfidence(from: text)
// guard let topResult = results.first else {
// return nil
// }
// let azureLanguageCode = LanguageDetector.convertToAzureLanguageCode(topResult.language)
// let languageName = LanguageDetector.getLanguageName(from: topResult.language)
// return [
// "languageCode": azureLanguageCode,
// "confidence": topResult.confidence,
// "languageName": languageName,
// "originalCode": topResult.language
// ]
// }

17
local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift

@ -184,6 +184,15 @@ import os.log
callback: currentAsrCallback! callback: currentAsrCallback!
) )
result(success) result(success)
case "disableBluetoothAudio":
azureAsrHelper.disableBluetoothAudio();
return
case "restoreOriginalAudioState":
azureAsrHelper.restoreOriginalAudioState();
return
case "stopContinuousRecognition": case "stopContinuousRecognition":
let success = azureAsrHelper.stopContinuousRecognition() let success = azureAsrHelper.stopContinuousRecognition()
@ -224,7 +233,7 @@ import os.log
return return
} }
do { do {
print("音频文件名称为: \(filePath)") print("音频文件名称为: (filePath)")
try azureAsrHelper.renameFile(filePath: filePath, newName: newName) try azureAsrHelper.renameFile(filePath: filePath, newName: newName)
result(true) result(true)
} catch { } catch {
@ -239,7 +248,7 @@ import os.log
return return
} }
do { do {
print("音频文件名称为: \(sourcePath)") print("音频文件名称为: (sourcePath)")
try azureAsrHelper.moveFile(sourcePath: sourcePath, destPath: destPath) try azureAsrHelper.moveFile(sourcePath: sourcePath, destPath: destPath)
result(true) result(true)
} catch { } catch {
@ -253,7 +262,7 @@ import os.log
return return
} }
do { do {
print("音频文件名称为: \(filePath)") print("音频文件名称为: (filePath)")
try azureAsrHelper.enableRecord(filePath: filePath) try azureAsrHelper.enableRecord(filePath: filePath)
result(true) result(true)
} catch { } catch {
@ -362,7 +371,7 @@ import os.log
let success = azureTtsHelper.flushStream() let success = azureTtsHelper.flushStream()
result(success) result(success)
case "stop": case "stopSpeaking":
let success = azureTtsHelper.stop() let success = azureTtsHelper.stop()
result(success) result(success)

2
local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureTtsHelper.swift

@ -166,7 +166,7 @@ public class AzureTtsHelper: NSObject, ITtsService {
let currentText = streamBuffer let currentText = streamBuffer
// 标点符号列表 (与Android一致) // 标点符号列表 (与Android一致)
let punctuationMarks: [Character] = [".", "。", "!", "!", "?", "?", ";", ";", ",", ",", ":", ":", "\n"] let punctuationMarks: [Character] = [".", "。", "!", "!","*", "?", "?", ";", ";", ",", ",", ":", ":", "\n"]
// 从后往前查找最后一个标点符号 // 从后往前查找最后一个标点符号
var lastPunctuationIndex = -1 var lastPunctuationIndex = -1

214
local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/LanguageDetector.swift

@ -0,0 +1,214 @@
import Foundation
import NaturalLanguage
/**
* 语言检测工具类
* 使用iOS自带的自然语言处理框架检测文本语种
*/
public class LanguageDetector {
/**
* 检测文本的主要语言
*
* @param text 需要检测的文本
* @return 语言代码(如:"zh-Hans", "en", "ja"等),如果检测失败返回nil
*/
public static func detectLanguage(from text: String) -> String? {
// 检查输入文本是否为空
guard !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
return nil
}
// 使用NLLanguageRecognizer进行语言检测
let recognizer = NLLanguageRecognizer()
recognizer.processString(text)
// 获取最可能的语言
guard let dominantLanguage = recognizer.dominantLanguage else {
return nil
}
return dominantLanguage.rawValue
}
/**
* 检测文本的语言并返回置信度
*
* @param text 需要检测的文本
* @return 语言代码和置信度的字典,按置信度降序排列
*/
public static func detectLanguageWithConfidence(from text: String) -> [(language: String, confidence: Double)] {
// 检查输入文本是否为空
guard !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
return []
}
let recognizer = NLLanguageRecognizer()
recognizer.processString(text)
// 获取语言假设及其置信度
let languageHypotheses = recognizer.languageHypotheses(withMaximum: 5)
// 转换为元组数组并按置信度排序
return languageHypotheses.map { (language: $0.key.rawValue, confidence: $0.value) }
.sorted { $0.confidence > $1.confidence }
}
/**
* 将语言代码转换为人类可读的语言名称
*
* @param languageCode 语言代码(如:"zh-Hans", "en"等)
* @return 语言的本地化名称
*/
public static func getLanguageName(from languageCode: String) -> String {
let locale = Locale.current
return locale.localizedString(forLanguageCode: languageCode) ?? languageCode
}
/**
* 检测文本语言并返回Azure语音服务支持的语言代码格式
*
* @param text 需要检测的文本
* @return Azure支持的语言代码格式(如:"zh-CN", "en-US"等)
*/
public static func detectLanguageForAzure(from text: String) -> String? {
guard let detectedLanguage = detectLanguage(from: text) else {
return nil
}
// 将iOS语言代码转换为Azure语音服务支持的格式
return convertToAzureLanguageCode(detectedLanguage)
}
/**
* 将iOS语言代码转换为Azure语音服务支持的语言代码
*
* @param iosLanguageCode iOS检测到的语言代码
* @return Azure支持的语言代码
*/
public static func convertToAzureLanguageCode(_ iosLanguageCode: String) -> String {
let languageMapping: [String: String] = [
// 中文
"zh-Hans": "zh-CN", // 简体中文
"zh-Hant": "zh-TW", // 繁体中文
"zh-HK": "zh-HK", // 香港中文
// 英语系
"en": "en-US", // 英语(美国)
"en-GB": "en-US", // 英语(英国)
"en-AU": "en-US", // 英语(澳大利亚)
"en-CA": "en-US", // 英语(加拿大)
"en-IN": "en-US", // 英语(印度)
"en-IE": "en-US", // 英语(爱尔兰)
"en-NZ": "en-US", // 英语(新西兰)
"en-ZA": "en-US", // 英语(南非)
// 亚洲语言
"ja": "ja-JP", // 日语
"ko": "ko-KR", // 韩语
"hi": "hi-IN", // 印地语
"th": "th-TH", // 泰语
"vi": "vi-VN", // 越南语
"id": "id-ID", // 印尼语
"ms": "ms-MY", // 马来语
"tl": "fil-PH", // 菲律宾语
"my": "my-MM", // 缅甸语
"km": "km-KH", // 高棉语(柬埔寨)
"lo": "lo-LA", // 老挝语
"si": "si-LK", // 僧伽罗语(斯里兰卡)
"ta": "ta-IN", // 泰米尔语
"te": "te-IN", // 泰卢固语
"bn": "bn-IN", // 孟加拉语
"gu": "gu-IN", // 古吉拉特语
"kn": "kn-IN", // 卡纳达语
"ml": "ml-IN", // 马拉雅拉姆语
"mr": "mr-IN", // 马拉地语
"pa": "pa-IN", // 旁遮普语
"ur": "ur-PK", // 乌尔都语
// 欧洲语言
"fr": "fr-FR", // 法语(法国)
"fr-CA": "fr-CA", // 法语(加拿大)
"fr-BE": "fr-BE", // 法语(比利时)
"fr-CH": "fr-CH", // 法语(瑞士)
"de": "de-DE", // 德语(德国)
"de-AT": "de-AT", // 德语(奥地利)
"de-CH": "de-CH", // 德语(瑞士)
"es": "es-ES", // 西班牙语(西班牙)
"es-MX": "es-MX", // 西班牙语(墨西哥)
"es-AR": "es-AR", // 西班牙语(阿根廷)
"es-CO": "es-CO", // 西班牙语(哥伦比亚)
"es-CL": "es-CL", // 西班牙语(智利)
"es-PE": "es-PE", // 西班牙语(秘鲁)
"es-VE": "es-VE", // 西班牙语(委内瑞拉)
"it": "it-IT", // 意大利语
"pt": "pt-BR", // 葡萄牙语(巴西)
"pt-PT": "pt-PT", // 葡萄牙语(葡萄牙)
"ru": "ru-RU", // 俄语
"nl": "nl-NL", // 荷兰语
"nl-BE": "nl-BE", // 荷兰语(比利时)
"sv": "sv-SE", // 瑞典语
"da": "da-DK", // 丹麦语
"no": "nb-NO", // 挪威语
"fi": "fi-FI", // 芬兰语
"is": "is-IS", // 冰岛语
"pl": "pl-PL", // 波兰语
"cs": "cs-CZ", // 捷克语
"sk": "sk-SK", // 斯洛伐克语
"hu": "hu-HU", // 匈牙利语
"ro": "ro-RO", // 罗马尼亚语
"bg": "bg-BG", // 保加利亚语
"hr": "hr-HR", // 克罗地亚语
"sr": "sr-RS", // 塞尔维亚语
"sl": "sl-SI", // 斯洛文尼亚语
"et": "et-EE", // 爱沙尼亚语
"lv": "lv-LV", // 拉脱维亚语
"lt": "lt-LT", // 立陶宛语
"uk": "uk-UA", // 乌克兰语
"be": "be-BY", // 白俄罗斯语
"mk": "mk-MK", // 马其顿语
"sq": "sq-AL", // 阿尔巴尼亚语
"mt": "mt-MT", // 马耳他语
"ga": "ga-IE", // 爱尔兰语
"cy": "cy-GB", // 威尔士语
"eu": "eu-ES", // 巴斯克语
"ca": "ca-ES", // 加泰罗尼亚语
"gl": "gl-ES", // 加利西亚语
// 中东和非洲语言
"ar": "ar-SA", // 阿拉伯语(沙特阿拉伯)
"ar-EG": "ar-EG", // 阿拉伯语(埃及)
"ar-AE": "ar-AE", // 阿拉伯语(阿联酋)
"ar-BH": "ar-BH", // 阿拉伯语(巴林)
"ar-DZ": "ar-DZ", // 阿拉伯语(阿尔及利亚)
"ar-IQ": "ar-IQ", // 阿拉伯语(伊拉克)
"ar-JO": "ar-JO", // 阿拉伯语(约旦)
"ar-KW": "ar-KW", // 阿拉伯语(科威特)
"ar-LB": "ar-LB", // 阿拉伯语(黎巴嫩)
"ar-LY": "ar-LY", // 阿拉伯语(利比亚)
"ar-MA": "ar-MA", // 阿拉伯语(摩洛哥)
"ar-OM": "ar-OM", // 阿拉伯语(阿曼)
"ar-QA": "ar-QA", // 阿拉伯语(卡塔尔)
"ar-SY": "ar-SY", // 阿拉伯语(叙利亚)
"ar-TN": "ar-TN", // 阿拉伯语(突尼斯)
"ar-YE": "ar-YE", // 阿拉伯语(也门)
"he": "he-IL", // 希伯来语
"fa": "fa-IR", // 波斯语(伊朗)
"tr": "tr-TR", // 土耳其语
"az": "az-AZ", // 阿塞拜疆语
"ka": "ka-GE", // 格鲁吉亚语
"hy": "hy-AM", // 亚美尼亚语
"sw": "sw-KE", // 斯瓦希里语
"am": "am-ET", // 阿姆哈拉语(埃塞俄比亚)
"zu": "zu-ZA", // 祖鲁语(南非)
"af": "af-ZA", // 南非荷兰语
// 其他语言
"el": "el-GR", // 希腊语
"eo": "eo", // 世界语
"la": "la", // 拉丁语
]
return languageMapping[iosLanguageCode] ?? iosLanguageCode
}
}

11
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt

@ -94,6 +94,9 @@ object BleService {
// 是否是ota模式 // 是否是ota模式
var isEnterOta = false var isEnterOta = false
// 是否需要唤醒
var isWakeup = true
// 是否回复指令 // 是否回复指令
var isReply = true var isReply = true
@ -1981,7 +1984,13 @@ startBytesStatistics()
private fun notifyWakeupSignalReceived() { private fun notifyWakeupSignalReceived() {
for (callback in callbacks) { for (callback in callbacks) {
try { try {
callback.onWakeupSignalReceived() if (isWakeup) {
callback.onWakeupSignalReceived()
} else {
if (callback::class.java.simpleName == "BleServicePlugin") {
callback.onWakeupSignalReceived()
}
}
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "分发唤醒信号回调异常", e) Log.e(TAG, "分发唤醒信号回调异常", e)
} }

9
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt

@ -174,7 +174,7 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, ActivityAware,
} }
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
Log.d(TAG, "Method call: ${call.method}") // Log.d(TAG, "Method call: ${call.method}")
when (call.method) { when (call.method) {
"isConnected" -> { "isConnected" -> {
@ -278,6 +278,13 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, ActivityAware,
val connected = BleService.isConnected() val connected = BleService.isConnected()
result.success(connected) result.success(connected)
} }
"setWakeup" -> {
val wakeup = call.argument<Boolean>("wakeup")
if (wakeup != null) {
BleService.isWakeup = wakeup
}
result.success("")
}
"registerBackgroundScan" -> { "registerBackgroundScan" -> {
bleCompanionManager.registerBackgroundScan() bleCompanionManager.registerBackgroundScan()
result.success(true) result.success(true)

2
local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftBleServicePlugin.swift

@ -108,7 +108,7 @@ public class SwiftBleServicePlugin: NSObject, FlutterPlugin {
case "startScan": case "startScan":
let arguments = call.arguments as? [String: Any] let arguments = call.arguments as? [String: Any]
let timeout = (arguments?["timeout"] as? Double) ?? 30.0 let timeout = (arguments?["timeout"] as? Double) ?? 15.0
os_log("🔍 开始传统BLE扫描,超时: %.1f秒", log: logger, type: .info, timeout) os_log("🔍 开始传统BLE扫描,超时: %.1f秒", log: logger, type: .info, timeout)
BleService.shared.startScan(timeout: timeout) BleService.shared.startScan(timeout: timeout)

59
local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftOpusAudioProcessor.swift

@ -26,22 +26,32 @@ class SwiftOpusAudioProcessor: NSObject {
cleanup() cleanup()
} }
/// 线程安全的解码器初始化
private func initializeOpusDecoder() -> Bool { private func initializeOpusDecoder() -> Bool {
objc_sync_enter(self)
defer { objc_sync_exit(self) }
if decoder != nil { if decoder != nil {
return true return true
} }
var error: Int32 = 0 var error: Int32 = 0
decoder = opus_decoder_create(sampleRate, channels, &error) let newDecoder = opus_decoder_create(sampleRate, channels, &error)
guard error == OPUS_OK, decoder != nil else { guard error == OPUS_OK, newDecoder != nil else {
print("Opus解码器创建失败,错误代码: \(error)")
return false return false
} }
decoder = newDecoder
return true return true
} }
/// 线程安全的解码器销毁
private func destroyOpusDecoder() { private func destroyOpusDecoder() {
objc_sync_enter(self)
defer { objc_sync_exit(self) }
if let decoder = decoder { if let decoder = decoder {
opus_decoder_destroy(decoder) opus_decoder_destroy(decoder)
self.decoder = nil self.decoder = nil
@ -112,23 +122,44 @@ class SwiftOpusAudioProcessor: NSObject {
} }
} }
// 简化的 Opus 解码方法,重点是容错处理 /// 改进的 Opus 解码方法,增强容错和线程安全
private func decodeOpusPacket(_ packet: Data) { private func decodeOpusPacket(_ packet: Data) {
guard let decoder = decoder else { return } // 添加线程安全锁
objc_sync_enter(self)
defer { objc_sync_exit(self) }
// 验证解码器状态
guard let decoder = decoder, isDecodingActive else {
print("解码器未初始化或已停止")
return
}
let maxSamples = Int(maxFrameSize) let maxSamples = Int(maxFrameSize)
var pcmBuffer = [opus_int16](repeating: 0, count: maxSamples * Int(channels)) var pcmBuffer = [opus_int16](repeating: 0, count: maxSamples * Int(channels))
let decodedSamples = packet.withUnsafeBytes { packetPtr in let decodedSamples = packet.withUnsafeBytes { packetPtr in
let uint8Ptr = packetPtr.bindMemory(to: UInt8.self).baseAddress // 确保指针有效
return opus_decode( guard let uint8Ptr = packetPtr.bindMemory(to: UInt8.self).baseAddress else {
decoder, print("无法获取数据包指针")
uint8Ptr, return opus_int32(-1)
opus_int32(packet.count), }
&pcmBuffer,
Int32(maxSamples), // 添加异常捕获机制
0 var result: opus_int32 = -1
)
// 使用 autoreleasepool 确保内存管理
autoreleasepool {
result = opus_decode(
decoder,
uint8Ptr,
opus_int32(packet.count),
&pcmBuffer,
Int32(maxSamples),
0
)
}
return result
} }
// 增强的错误处理:检查各种错误情况 // 增强的错误处理:检查各种错误情况
@ -237,4 +268,4 @@ class SwiftOpusAudioProcessor: NSObject {
func getCurrentConfig() -> (sampleRate: Int32, channels: Int32) { func getCurrentConfig() -> (sampleRate: Int32, channels: Int32) {
return (sampleRate, channels) return (sampleRate, channels)
} }
} }

11
local_plugins/ble_service/lib/ble_service.dart

@ -272,6 +272,17 @@ class BleService {
} }
} }
/// 设置是否是否需要唤醒
Future<void> setWakeup(bool wakeup) async {
try {
await _methodChannel.invokeMethod<String>('setWakeup', {
'wakeup': wakeup,
});
} catch (e) {
print('设置是否是否需要唤醒失败: $e');
}
}
/// 断开连接 /// 断开连接
Future<void> disconnect() async { Future<void> disconnect() async {
try { try {

46
local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/Utils.kt

@ -9,6 +9,8 @@ import android.provider.Settings
import android.util.Log import android.util.Log
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.aallam.openai.api.chat.ChatMessage import com.aallam.openai.api.chat.ChatMessage
import com.aallam.openai.api.chat.TextContent
import com.aallam.openai.api.chat.ListContent
import io.ktor.client.* import io.ktor.client.*
import io.ktor.client.engine.okhttp.* import io.ktor.client.engine.okhttp.*
import io.ktor.client.plugins.sse.* import io.ktor.client.plugins.sse.*
@ -169,16 +171,42 @@ fun roughTokenCount(text: String): Int {
/** /**
* 估算消息列表的总 token 数量(近似) * 估算消息列表的总 token 数量(近似)
* @param messages List of message map: role: user/assistant, content: string * @param messages List of message map: role: user/assistant, content: string or array
* @return 估算 token 数 * @return 估算 token 数
*/ */
fun estimatePromptTokens(messages: List<ChatMessage>): Int { fun estimatePromptTokens(messages: List<ChatMessage>): Int {
var tokens = 0 var totalText = ""
for (msg in messages) { for (message in messages) {
tokens += 4 // 每条消息结构开销 when (val content = message.messageContent) {
tokens += roughTokenCount(msg.role.role.lowercase()) // ChatRole 枚举转小写字符串 is TextContent? -> {
tokens += roughTokenCount(msg.content ?: "") totalText += content?.content + " "
}
is ListContent -> {
// 处理多模态内容数组
content.content.forEach { item ->
if (item is Map<*, *>) {
val type = item["type"] as? String
if (type == "text") {
val text = item["text"] as? String ?: ""
totalText += text + " "
}else if(type == "image_url"){
val text = item["image_url"] as? String ?: ""
totalText += text + " "
}
}
}
}
}
} }
tokens += 2 // 结尾 assistant 回复结构开销 return roughTokenCount(totalText)
return tokens }
} //fun estimatePromptTokens(messages: List<ChatMessage>): Int {
// var tokens = 0
// for (msg in messages) {
// tokens += 4 // 每条消息结构开销
// tokens += roughTokenCount(msg.role.role.lowercase()) // ChatRole 枚举转小写字符串
// tokens += roughTokenCount(msg.content ?: "")
// }
// tokens += 2 // 结尾 assistant 回复结构开销
// return tokens
//}

88
local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatApiService.swift

@ -68,18 +68,18 @@ public class ChatApiService: NSObject {
/// 设置流式回调 /// 设置流式回调
public func setStreamCallback(_ sessionId: String, _ callback: StreamCallback) { public func setStreamCallback(_ sessionId: String, _ callback: StreamCallback) {
print("[ChatApiService] 设置回调 for sessionId: \(sessionId)") // print("[ChatApiService] 设置回调 for sessionId: \(sessionId)")
print("[ChatApiService] 回调对象: \(callback)") // print("[ChatApiService] 回调对象: \(callback)")
self.sessionCallbacks[sessionId] = callback self.sessionCallbacks[sessionId] = callback
print("[ChatApiService] 回调设置完成,当前回调数量: \(sessionCallbacks.count)") print("[ChatApiService] 回调设置完成,当前回调数量: \(sessionCallbacks.count)")
} }
public func getSessionCallback(_ sessionId: String) -> StreamCallback? { public func getSessionCallback(_ sessionId: String) -> StreamCallback? {
let callback = self.sessionCallbacks[sessionId] let callback = self.sessionCallbacks[sessionId]
print("[ChatApiService] getSessionCallback for sessionId: \(sessionId)") // print("[ChatApiService] getSessionCallback for sessionId: \(sessionId)")
print("[ChatApiService] sessionCallbacks count: \(sessionCallbacks.count)") // print("[ChatApiService] sessionCallbacks count: \(sessionCallbacks.count)")
print("[ChatApiService] sessionCallbacks keys: \(Array(sessionCallbacks.keys))") // print("[ChatApiService] sessionCallbacks keys: \(Array(sessionCallbacks.keys))")
print("[ChatApiService] callback exists: \(callback != nil)") // print("[ChatApiService] callback exists: \(callback != nil)")
return callback return callback
} }
//移除回调函数 //移除回调函数
@ -311,7 +311,7 @@ public func sendMessageStream(_ sessionId: String, messages: [[String: Any]]) {
tools: chatTools tools: chatTools
) )
print("[ChatApiService] 发起AI问答 问题:\(messages[0]) 工具数量: \(chatTools.count)") print("[ChatApiService] 发起AI问答 问题:\(messages) 工具数量: \(chatTools.count)")
} else { } else {
query = ChatQuery( query = ChatQuery(
messages: chatMessages, messages: chatMessages,
@ -335,14 +335,14 @@ public func sendMessageStream(_ sessionId: String, messages: [[String: Any]]) {
// 处理文本内容 // 处理文本内容
if let content = delta.content { if let content = delta.content {
replyContext += content replyContext += content
print("[ChatApiService] 收到内容: \(content)") // print("[ChatApiService] 收到内容: \(content)")
print("[ChatApiService] 当前sessionId: \(sessionId)") // print("[ChatApiService] 当前sessionId: \(sessionId)")
let callback = self.getSessionCallback(sessionId) let callback = self.getSessionCallback(sessionId)
if let callback = callback { if let callback = callback {
print("[ChatApiService] 找到回调,准备调用 onToken") // print("[ChatApiService] 找到回调,准备调用 onToken")
callback.onToken(sessionId, content) callback.onToken(sessionId, content)
print("[ChatApiService] onToken 调用完成") // print("[ChatApiService] onToken 调用完成")
} else { } else {
print("[ChatApiService] 警告:未找到 sessionId \(sessionId) 的回调") print("[ChatApiService] 警告:未找到 sessionId \(sessionId) 的回调")
} }
@ -761,7 +761,44 @@ private func convertMapToJsonObject(_ map: [String: Any]) -> [String: Any] {
} }
return false return false
} }
// 解析并构建正确的图片消息
func parseImageData(_ data: [String: Any]) -> ChatQuery.ChatCompletionMessageParam? {
// 1. 校验基础字段
guard let type = data["type"] as? String, type == "image_url",
let imageUrlDict = data["image_url"] as? [String: Any],
let imageUrlString = imageUrlDict["url"] as? String else {
print("数据结构错误:缺少type或image_url.url")
return nil
}
// 2. 补充默认的detail(原结构体必须,默认auto)
let detailString = imageUrlDict["detail"] as? String ?? "auto"
let imageDetail: ChatQuery.ChatCompletionMessageParam.UserMessageParam.Content.VisionContent.ChatCompletionContentPartImageParam.ImageURL.Detail
switch detailString.lowercased() {
case "low":
imageDetail = .low
case "high":
imageDetail = .high
default:
imageDetail = .auto
}
// 3. 逐层构建结构体(按原结构体要求映射)
let imageUrlModel = ChatQuery.ChatCompletionMessageParam.UserMessageParam.Content.VisionContent.ChatCompletionContentPartImageParam.ImageURL(
url: imageUrlString,
detail: imageDetail
)
let imageContentPart = ChatQuery.ChatCompletionMessageParam.UserMessageParam.Content.VisionContent.ChatCompletionContentPartImageParam(
imageUrl: imageUrlModel
)
let visionContent = ChatQuery.ChatCompletionMessageParam.UserMessageParam.Content.VisionContent.chatCompletionContentPartImageParam(imageContentPart)
let userContent = ChatQuery.ChatCompletionMessageParam.UserMessageParam.Content.vision([visionContent])
let userMessageParam = ChatQuery.ChatCompletionMessageParam.UserMessageParam(content: userContent)
return .user(userMessageParam)
}
/// 转换为OpenAI Message格式 /// 转换为OpenAI Message格式
private func convertToChatMessages(_ messages: [[String: Any]]) throws -> [ChatQuery.ChatCompletionMessageParam] { private func convertToChatMessages(_ messages: [[String: Any]]) throws -> [ChatQuery.ChatCompletionMessageParam] {
var chatMessages: [ChatQuery.ChatCompletionMessageParam] = [] var chatMessages: [ChatQuery.ChatCompletionMessageParam] = []
@ -851,17 +888,24 @@ private func convertMapToJsonObject(_ map: [String: Any]) -> [String: Any] {
var textContent = "" var textContent = ""
for item in contentArray { for item in contentArray {
if let type = item["type"] as? String, type == "text", if let type = item["type"] as? String {
let text = item["text"] as? String { switch type {
textContent += text case "text":
if let text = item["text"] as? String {
if let chatMessage = ChatQuery.ChatCompletionMessageParam(role: messageRole, content: text) {
chatMessages.append(chatMessage)
}
}
case "image_url":
if let imageMessage = parseImageData(item) {
chatMessages.append(imageMessage)
print("图片消息解析成功")
}
default:
break
}
} }
} }
let finalContent = textContent.isEmpty ? "多媒体内容" : textContent
if let chatMessage = ChatQuery.ChatCompletionMessageParam(role: messageRole, content: finalContent) {
chatMessages.append(chatMessage)
}
} }
} }
@ -932,7 +976,7 @@ private func convertMapToJsonObject(_ map: [String: Any]) -> [String: Any] {
// 创建工具参数 // 创建工具参数
let toolParam = ChatQuery.ChatCompletionToolParam(function: functionDefinition) let toolParam = ChatQuery.ChatCompletionToolParam(function: functionDefinition)
print("解析工具 \(name):\(schema)") // print("解析工具 \(name):\(schema)")
chatTools.append(toolParam) chatTools.append(toolParam)
} }

89
local_plugins/chat_api/ios/chat_api/Sources/chat_api/MCPClient.swift

@ -28,6 +28,7 @@ public protocol FunctionHandler {
/// - 安全的JSON-RPC消息解析,包含错误修复逻辑 /// - 安全的JSON-RPC消息解析,包含错误修复逻辑
/// - 完整的SSE连接生命周期管理 /// - 完整的SSE连接生命周期管理
/// - 与Android端CustomSseClientTransport功能对等 /// - 与Android端CustomSseClientTransport功能对等
// 在 MCPSubClient 类中添加工具调用超时和重试配置
public class MCPSubClient { public class MCPSubClient {
private let serverId: String private let serverId: String
private let serverUrl: String private let serverUrl: String
@ -47,6 +48,10 @@ public class MCPSubClient {
private var retryCount = 0 private var retryCount = 0
private var reconnectTask: Task<Void, Never>? private var reconnectTask: Task<Void, Never>?
// 工具调用配置 - 添加这两行
private let maxToolCallRetries = 3
private let toolCallTimeout: TimeInterval = 30.0
// 连接状态锁 // 连接状态锁
private let connectionLock = NSLock() private let connectionLock = NSLock()
@ -178,27 +183,55 @@ public class MCPSubClient {
/// 调用工具前检查连接 /// 调用工具前检查连接
public func callTool(name: String, arguments: [String: Any]) async -> [String: Any]? { public func callTool(name: String, arguments: [String: Any]) async -> [String: Any]? {
return await callToolWithRetry(name: name, arguments: arguments, retryCount: 0)
}
private func callToolWithRetry(name: String, arguments: [String: Any], retryCount: Int) async -> [String: Any]? {
print("[MCPSubClient] 工具调用尝试 \(retryCount + 1)/\(maxToolCallRetries + 1): \(name)")
// 先检查连接状态 // 先检查连接状态
if !(await checkConnection()) { if !(await checkConnection()) {
if retryCount < maxToolCallRetries {
print("[MCPSubClient] 连接断开,尝试重连...")
if await connect() {
print("[MCPSubClient] 重连成功,重试工具调用")
return await callToolWithRetry(name: name, arguments: arguments, retryCount: retryCount + 1)
}
}
return [ return [
"content": [[ "content": [[
"type": "text", "type": "text",
"text": "Tool call failed: mcp: service not connected" "text": "工具调用失败,请重试:MCP 服务连接中断"
]], ]],
"isError": true "isError": true
] ]
} }
guard isConnectedFlag, let client = mcpClient else { guard isConnectedFlag, let client = mcpClient else {
return createErrorResult("Not connected to MCP server") if retryCount < maxToolCallRetries {
print("[MCPSubClient] MCP 客户端未连接,尝试重连...")
if await connect() {
return await callToolWithRetry(name: name, arguments: arguments, retryCount: retryCount + 1)
}
}
return createErrorResult("工具调用失败,请重试:MCP 服务未连接")
} }
do { do {
let mcpArguments = convertToMCPValue(arguments) // 使用超时控制的工具调用
let (content, isError) = try await client.callTool(name: name, arguments: mcpArguments) let result = try await withTimeout(seconds: toolCallTimeout) {
let mcpArguments = self.convertToMCPValue(arguments)
return try await client.callTool(name: name, arguments: mcpArguments)
}
let (content, isError) = result
if isError == true { if isError == true {
return createErrorResult("Tool execution failed") if retryCount < maxToolCallRetries {
print("[MCPSubClient] 工具执行失败,重试中...")
return await callToolWithRetry(name: name, arguments: arguments, retryCount: retryCount + 1)
}
return createErrorResult("工具调用失败,请重试:工具执行错误")
} }
var contextText = "" var contextText = ""
@ -225,13 +258,45 @@ public class MCPSubClient {
} }
} }
print("[MCPSubClient] 工具调用成功: \(name)")
return [ return [
"content": [["type": "text", "text": contextText.trimmingCharacters(in: .whitespacesAndNewlines)]], "content": [["type": "text", "text": contextText.trimmingCharacters(in: .whitespacesAndNewlines)]],
"isError": false "isError": false
] ]
} catch is TimeoutError {
print("[MCPSubClient] 工具调用超时: \(name)")
if retryCount < maxToolCallRetries {
print("[MCPSubClient] 超时重试中...")
return await callToolWithRetry(name: name, arguments: arguments, retryCount: retryCount + 1)
}
return createErrorResult("工具调用失败,请重试:连接超时")
} catch { } catch {
return createErrorResult("Tool call failed: \(error.localizedDescription)") print("[MCPSubClient] 工具调用异常: \(name), 错误: \(error.localizedDescription)")
// 检查是否是连接相关错误
let errorDescription = error.localizedDescription.lowercased()
if errorDescription.contains("connection") ||
errorDescription.contains("network") ||
errorDescription.contains("timeout") ||
errorDescription.contains("disconnected") {
if retryCount < maxToolCallRetries {
print("[MCPSubClient] 检测到连接错误,尝试重连并重试...")
// 标记连接断开
isConnectedFlag = false
// 尝试重连
if await connect() {
return await callToolWithRetry(name: name, arguments: arguments, retryCount: retryCount + 1)
}
}
} else if retryCount < maxToolCallRetries {
print("[MCPSubClient] 其他错误,直接重试...")
return await callToolWithRetry(name: name, arguments: arguments, retryCount: retryCount + 1)
}
return createErrorResult("工具调用失败,请重试:\(error.localizedDescription)")
} }
} }
@ -414,7 +479,7 @@ public class MCPClient {
// 添加连接成功后的工具列表汇总输出 // 添加连接成功后的工具列表汇总输出
if connectedCount > 0 { if connectedCount > 0 {
print("[MCPClient] 🎉 MCP 连接完成,共连接 \(connectedCount) 个服务器") print("[MCPClient] 🎉 MCP 连接完成,共连接 \(connectedCount) 个服务器")
print("[MCPClient] 📋 所有可用工具汇总:") // print("[MCPClient] 📋 所有可用工具汇总:")
let allTools = getToolMaps() let allTools = getToolMaps()
for (index, toolMap) in allTools.enumerated() { for (index, toolMap) in allTools.enumerated() {
@ -425,9 +490,9 @@ public class MCPClient {
} }
} }
print("[MCPClient] 📊 工具总数: \(allTools.count)") // print("[MCPClient] 📊 工具总数: \(allTools.count)")
print("[MCPClient] 🔧 本地函数: \(localFunctionDefs.count) 个") // print("[MCPClient] 🔧 本地函数: \(localFunctionDefs.count) 个")
print("[MCPClient] 🌐 远程工具: \(allTools.count - localFunctionDefs.count) 个") // print("[MCPClient] 🌐 远程工具: \(allTools.count - localFunctionDefs.count) 个")
} }
return connectedCount > 0 return connectedCount > 0
@ -443,7 +508,7 @@ public class MCPClient {
parameters: Any, parameters: Any,
handler: FunctionHandler handler: FunctionHandler
) -> Bool { ) -> Bool {
print("[MCPClient] 注册本地函数: \(name)") // print("[MCPClient] 注册本地函数: \(name)")
localFunctions[name] = handler localFunctions[name] = handler
let functionDef: [String: Any] = [ let functionDef: [String: Any] = [
@ -479,7 +544,7 @@ public class MCPClient {
for client in subClients.values { for client in subClients.values {
let tools = client.getToolMaps() let tools = client.getToolMaps()
print("[MCPClient] 读取工具列表: \(tools)") // print("[MCPClient] 读取工具列表: \(tools)")
allToolMaps.append(contentsOf: tools) allToolMaps.append(contentsOf: tools)
} }

240
local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift

@ -6,6 +6,32 @@ import MessageUI
import location_service import location_service
import user_portrait // 添加这行 import user_portrait // 添加这行
import MediaPlayer
import AVFoundation
// 在文件顶部添加通知名称定义
extension Notification.Name {
static let audioSessionBeginPlayback = Notification.Name("AudioSessionBeginPlayback")
static let audioSessionBeginVoice = Notification.Name("AudioSessionBeginVoice")
static let volumeControlRequest = Notification.Name("VolumeControlRequest")
}
// 音量控制请求的数据结构
// 移除结构体定义,直接使用 map 传输
// 删除以下代码:
// struct VolumeControlInfo {
// let action: VolumeAction
// let value: Float?
//
// enum VolumeAction {
// case increase
// case decrease
// case mute
// case setMax
// case setValue(Float)
// }
// }
/// 结果结构体 /// 结果结构体
public struct Result { public struct Result {
let success: Bool let success: Bool
@ -66,6 +92,13 @@ public class SystemFunctionHandler: NSObject {
// 注册打开录音机功能 // 注册打开录音机功能
registerOpenRecorderFunction(client: client) registerOpenRecorderFunction(client: client)
// 注册音量控制函数
// 在registerOpenRecorderFunction方法之后添加以下方法
registerAddVolumeFunction(client: client)
registerReduceVolumeFunction(client: client)
registerMuteVolumeFunction(client: client)
registerMaxVolumeFunction(client: client)
// 注册用户画像更新功能 // 注册用户画像更新功能
registerSetUserProfileFieldFunction(client: client) registerSetUserProfileFieldFunction(client: client)
@ -311,6 +344,90 @@ public class SystemFunctionHandler: NSObject {
handler: handler handler: handler
) )
} }
/// 注册增加音量函数
private func registerAddVolumeFunction(client: MCPClient) {
client.registerLocalFunction(
name: "add_volume",
description: "增大音量",
parameters: [
"type": "object",
"properties": [:] as [String: Any],
"required": [] as [String]
],
handler: AddVolumeHandler()
)
}
/// 注册减少音量函数
private func registerReduceVolumeFunction(client: MCPClient) {
client.registerLocalFunction(
name: "reduce_volume",
description: "降低音量",
parameters: [
"type": "object",
"properties": [:] as [String: Any],
"required": [] as [String]
],
handler: ReduceVolumeHandler()
)
}
/// 注册静音函数
private func registerMuteVolumeFunction(client: MCPClient) {
client.registerLocalFunction(
name: "mute_volume",
description: "设置静音",
parameters: [
"type": "object",
"properties": [:] as [String: Any],
"required": [] as [String]
],
handler: MuteVolumeHandler()
)
}
/// 注册最大音量函数
private func registerMaxVolumeFunction(client: MCPClient) {
client.registerLocalFunction(
name: "max_volume",
description: "设置最大音量",
parameters: [
"type": "object",
"properties": [:] as [String: Any],
"required": [] as [String]
],
handler: MaxVolumeHandler()
)
}
/// 注册设置用户画像字段函数
private func registerSetUserProfileFieldFunction(client: MCPClient) {
let parameters: [String: Any] = [
"type": "object",
"properties": [
"key": [
"type": "string",
"description": "画像字段名,例如:age、tone、工作经历、device_preference"
],
"value": [
"type": "string",
"description": "字段对应的值"
]
],
"required": ["key", "value"]
]
let handler: SetUserProfileFieldHandler = SetUserProfileFieldHandler()
_ = client.registerLocalFunction(
name: "set_user_profile_field",
description: "设置或更新用户画像中的某一个字段",
parameters: parameters,
handler: handler
)
}
} }
// MARK: - 功能处理器实现 // MARK: - 功能处理器实现
@ -345,8 +462,7 @@ private class GetCurrentLocationHandler: FunctionHandler {
guard let latStr = locationInfo["user_lat"] as? String, guard let latStr = locationInfo["user_lat"] as? String,
let lngStr = locationInfo["user_lng"] as? String, let lngStr = locationInfo["user_lng"] as? String,
let latitude = Double(latStr), let latitude = Double(latStr),
let longitude = Double(lngStr), let longitude = Double(lngStr) else {
let address = locationInfo["user_location"] as? String else {
print("[GetCurrentLocationHandler] 位置数据格式错误") print("[GetCurrentLocationHandler] 位置数据格式错误")
continuation.resume(returning: self.createErrorResponse(message: "位置数据格式错误,请重试")) continuation.resume(returning: self.createErrorResponse(message: "位置数据格式错误,请重试"))
return return
@ -354,8 +470,8 @@ private class GetCurrentLocationHandler: FunctionHandler {
print("[GetCurrentLocationHandler] 位置获取成功: lat=\(latitude), lng=\(longitude)") print("[GetCurrentLocationHandler] 位置获取成功: lat=\(latitude), lng=\(longitude)")
let message = "当前位置:\(address)(纬度 \(latitude),经度 \(longitude))" // let message = "当前位置:\(address)(纬度 \(latitude),经度 \(longitude))"
let successResponse = self.createSuccessResponse(message: message, latitude: latitude, longitude: longitude, address: address) let successResponse = self.createSuccessResponse(latitude: latitude, longitude: longitude)
print("[GetCurrentLocationHandler] 返回位置结果") print("[GetCurrentLocationHandler] 返回位置结果")
continuation.resume(returning: successResponse) continuation.resume(returning: successResponse)
@ -369,14 +485,12 @@ private class GetCurrentLocationHandler: FunctionHandler {
} }
} }
private func createSuccessResponse(message: String, latitude: Double, longitude: Double, address: String) -> String { private func createSuccessResponse(latitude: Double, longitude: Double) -> String {
let result: [String: Any] = [ let result: [String: Any] = [
"success": true, "success": true,
"message": message,
"data": [ "data": [
"latitude": latitude, "latitude": latitude,
"longitude": longitude, "longitude": longitude,
"address": address,
"timestamp": Date().timeIntervalSince1970 * 1000 "timestamp": Date().timeIntervalSince1970 * 1000
] ]
] ]
@ -678,7 +792,7 @@ private class GetCurrentTimeHandler: FunctionHandler {
private class MediaPlayHandler: FunctionHandler { private class MediaPlayHandler: FunctionHandler {
func handle(arguments: [String: Any]) async throws -> String { func handle(arguments: [String: Any]) async throws -> String {
NotificationCenter.default.post(name: NSNotification.Name("MediaPlay"), object: nil) NotificationCenter.default.post(name: NSNotification.Name("MediaPlay"), object: nil)
return "{\"result\": \"已开始播放媒体\"}" return "{\"notreply\": true,\"result\": \"已开始播放媒体\"}"
} }
} }
@ -686,7 +800,7 @@ private class MediaPlayHandler: FunctionHandler {
private class MediaPauseHandler: FunctionHandler { private class MediaPauseHandler: FunctionHandler {
func handle(arguments: [String: Any]) async throws -> String { func handle(arguments: [String: Any]) async throws -> String {
NotificationCenter.default.post(name: NSNotification.Name("MediaPause"), object: nil) NotificationCenter.default.post(name: NSNotification.Name("MediaPause"), object: nil)
return "{\"result\": \"已暂停媒体播放\"}" return "{\"broadcast\": true,\"result\": \"已暂停媒体播放\"}"
} }
} }
@ -694,7 +808,7 @@ private class MediaPauseHandler: FunctionHandler {
private class MediaPreviousHandler: FunctionHandler { private class MediaPreviousHandler: FunctionHandler {
func handle(arguments: [String: Any]) async throws -> String { func handle(arguments: [String: Any]) async throws -> String {
NotificationCenter.default.post(name: NSNotification.Name("MediaPrevious"), object: nil) NotificationCenter.default.post(name: NSNotification.Name("MediaPrevious"), object: nil)
return "{\"result\": \"已切换到上一首\"}" return "{\"notreply\": true,\"result\": \"已切换到上一首\"}"
} }
} }
@ -702,7 +816,7 @@ private class MediaPreviousHandler: FunctionHandler {
private class MediaNextHandler: FunctionHandler { private class MediaNextHandler: FunctionHandler {
func handle(arguments: [String: Any]) async throws -> String { func handle(arguments: [String: Any]) async throws -> String {
NotificationCenter.default.post(name: NSNotification.Name("MediaNext"), object: nil) NotificationCenter.default.post(name: NSNotification.Name("MediaNext"), object: nil)
return "{\"result\": \"已切换到下一首\"}" return "{\"notreply\": true,\"result\": \"已切换到下一首\"}"
} }
} }
@ -716,7 +830,7 @@ private class OpenRecorderHandler: FunctionHandler {
} }
} }
return "{\"result\": \"无法打开录音机应用\"}" return "{\"notreply\": true,\"result\": \"无法打开录音机应用\"}"
} }
} }
@ -725,50 +839,24 @@ private class OpenRecorderHandler: FunctionHandler {
// ... // ...
// } // }
/// 注册设置用户画像字段函数
private func registerSetUserProfileFieldFunction(client: MCPClient) {
let parameters: [String: Any] = [
"type": "object",
"properties": [
"key": [
"type": "string",
"description": "画像字段名,例如:age、tone、工作经历、device_preference"
],
"value": [
"type": "string",
"description": "字段对应的值"
]
],
"required": ["key", "value"]
]
let handler = SetUserProfileFieldHandler()
_ = client.registerLocalFunction(
name: "set_user_profile_field",
description: "设置或更新用户画像中的某一个字段",
parameters: parameters,
handler: handler
)
}
/// 设置用户画像字段处理器 /// 设置用户画像字段处理器
private class SetUserProfileFieldHandler: FunctionHandler { private class SetUserProfileFieldHandler: FunctionHandler {
func handle(arguments: [String: Any]) async throws -> String { func handle(arguments: [String: Any]) async throws -> String {
guard let key = arguments["key"] as? String, guard let key = arguments["key"] as? String,
let value = arguments["value"] as? String else { let value = arguments["value"] as? String else {
throw ChatApiException("缺少必要参数:key 和 value") return "设置成功"
} }
// 验证参数 // 验证参数
if key.isEmpty { if key.isEmpty {
throw ChatApiException("画像字段名不能为空") return "设置成功"
} }
if value.isEmpty { if value.isEmpty {
throw ChatApiException("字段值不能为空") return "设置成功"
} }
let result = await setUserProfileField(key: key, value: value) let result = await setUserProfileField(key: key, value: value)
return "{\"result\": \"\(result.message)\", \"key\": \"\(key)\", \"value\": \"\(value)\"}\"}" return "设置成功"
} }
private func setUserProfileField(key: String, value: String) async -> Result { private func setUserProfileField(key: String, value: String) async -> Result {
@ -799,3 +887,67 @@ private class SetUserProfileFieldHandler: FunctionHandler {
} }
} }
} }
private class AddVolumeHandler: FunctionHandler {
func handle(arguments: [String: Any]) async throws -> String {
// 使用 map 传输数据
let volumeInfo: [String: Any] = [
"action": "increase",
"value": NSNull()
]
NotificationCenter.default.post(
name: .volumeControlRequest,
object: volumeInfo
)
return "{\"broadcast\": false,\"result\": \"音量增加请求已发送\"}"
}
}
/// 减少音量处理器
private class ReduceVolumeHandler: FunctionHandler {
func handle(arguments: [String: Any]) async throws -> String {
let volumeInfo: [String: Any] = [
"action": "decrease",
"value": NSNull()
]
NotificationCenter.default.post(
name: .volumeControlRequest,
object: volumeInfo
)
return "{\"broadcast\": false,\"result\": \"音量降低请求已发送\"}"
}
}
/// 静音处理器
private class MuteVolumeHandler: FunctionHandler {
func handle(arguments: [String: Any]) async throws -> String {
let volumeInfo: [String: Any] = [
"action": "mute",
"value": NSNull()
]
NotificationCenter.default.post(
name: .volumeControlRequest,
object: volumeInfo
)
return "{\"broadcast\": false,\"result\": \"静音请求已发送\"}"
}
}
/// 最大音量处理器
private class MaxVolumeHandler: FunctionHandler {
func handle(arguments: [String: Any]) async throws -> String {
let volumeInfo: [String: Any] = [
"action": "setMax",
"value": NSNull()
]
NotificationCenter.default.post(
name: .volumeControlRequest,
object: volumeInfo
)
return "{\"broadcast\": false,\"result\": \"最大音量请求已发送\"}"
}
}

6
local_plugins/chat_storage/ios/chat_storage/Sources/chat_storage/ChatStorageHelper.swift

@ -186,7 +186,7 @@ public class ChatStorageHelper {
"totalCount": totalCount, "totalCount": totalCount,
"totalPages": totalPages "totalPages": totalPages
] ]
os_log("getMessages: 读取历史记录: %{public}@", log: logger, type: .error, messagesArray)
do { do {
let jsonData = try JSONSerialization.data(withJSONObject: result, options: []) let jsonData = try JSONSerialization.data(withJSONObject: result, options: [])
if let jsonString = String(data: jsonData, encoding: .utf8) { if let jsonString = String(data: jsonData, encoding: .utf8) {
@ -199,7 +199,7 @@ public class ChatStorageHelper {
let errmsg = String(cString: sqlite3_errmsg(db)!) let errmsg = String(cString: sqlite3_errmsg(db)!)
os_log("getMessages: SQL准备失败: %{public}@", log: logger, type: .error, errmsg) os_log("getMessages: SQL准备失败: %{public}@", log: logger, type: .error, errmsg)
} }
// 返回空结果,但保持格式一致 // 返回空结果,但保持格式一致
let emptyResult: [String: Any] = [ let emptyResult: [String: Any] = [
"messages": [], "messages": [],
@ -370,4 +370,4 @@ public class ChatStorageHelper {
return messages return messages
} }
} }

2
local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/LocationServicePlugin.kt

@ -25,7 +25,7 @@ class LocationServicePlugin: FlutterPlugin, MethodCallHandler, ActivityAware, Pl
} }
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
Log.d(TAG, "Method call: ${call.method}") // Log.d(TAG, "Method call: ${call.method}")
when (call.method) { when (call.method) {
"location" -> LocationService.getCurrentLocation(result) "location" -> LocationService.getCurrentLocation(result)
"requestPermission" -> LocationService.requestLocationPermission(activityBinding?.activity, result) "requestPermission" -> LocationService.requestLocationPermission(activityBinding?.activity, result)

6
local_plugins/location_service/ios/location_service/Sources/location_service/LocationData.swift

@ -4,13 +4,13 @@ import Foundation
public struct LocationData: Codable { public struct LocationData: Codable {
public let latitude: Double public let latitude: Double
public let longitude: Double public let longitude: Double
public let address: String
public let timestamp: TimeInterval public let timestamp: TimeInterval
public let latitudeandlongitude: String
public init(latitude: Double, longitude: Double, address: String, timestamp: TimeInterval) { public init(latitude: Double, longitude: Double,timestamp: TimeInterval) {
self.latitude = latitude self.latitude = latitude
self.longitude = longitude self.longitude = longitude
self.address = address
self.timestamp = timestamp self.timestamp = timestamp
self.latitudeandlongitude = "\(latitude),\(longitude)"
} }
} }

38
local_plugins/location_service/ios/location_service/Sources/location_service/LocationService.swift

@ -27,7 +27,6 @@ public class LocationService: NSObject {
super.init() super.init()
setupLocationManager() setupLocationManager()
} }
private func setupLocationManager() { private func setupLocationManager() {
DispatchQueue.main.async { [weak self] in DispatchQueue.main.async { [weak self] in
self?.locationManager = CLLocationManager() self?.locationManager = CLLocationManager()
@ -35,15 +34,26 @@ public class LocationService: NSObject {
self?.locationManager?.desiredAccuracy = kCLLocationAccuracyBest self?.locationManager?.desiredAccuracy = kCLLocationAccuracyBest
self?.locationManager?.distanceFilter = 10 self?.locationManager?.distanceFilter = 10
// 初始化时获取当前状态 // 不需要手动获取状态,delegate 方法会自动被调用
self?.authorizationStatus = CLLocationManager.authorizationStatus() print("[LocationService] LocationManager 初始化完成,等待权限状态回调")
self?.isLocationServicesEnabled = CLLocationManager.locationServicesEnabled()
print("[LocationService] LocationManager 初始化完成")
print("[LocationService] 初始权限状态: \(self?.authorizationStatus.rawValue ?? -1)")
print("[LocationService] 位置服务启用: \(self?.isLocationServicesEnabled ?? false)")
} }
} }
// private func setupLocationManager() {
// 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)")
// }
// }
/// 获取当前位置 - 主要接口 /// 获取当前位置 - 主要接口
public func getCurrentLocation(completion: @escaping (Result<[String: Any], Error>) -> Void) { public func getCurrentLocation(completion: @escaping (Result<[String: Any], Error>) -> Void) {
@ -190,11 +200,11 @@ public class LocationService: NSObject {
} }
// 使用缓存的状态信息 // 使用缓存的状态信息
print("[LocationService] 开始位置请求") // print("[LocationService] 开始位置请求")
print("[LocationService] 缓存的权限状态: \(self.authorizationStatus.rawValue)") // print("[LocationService] 缓存的权限状态: \(self.authorizationStatus.rawValue)")
print("[LocationService] 缓存的位置服务状态: \(self.isLocationServicesEnabled)") // print("[LocationService] 缓存的位置服务状态: \(self.isLocationServicesEnabled)")
print("[LocationService] 当前线程: \(Thread.current)") // print("[LocationService] 当前线程: \(Thread.current)")
print("[LocationService] 是否主线程: \(Thread.isMainThread)") // print("[LocationService] 是否主线程: \(Thread.isMainThread)")
// 验证权限状态 // 验证权限状态
if !self.checkLocationPermission() { if !self.checkLocationPermission() {
@ -238,7 +248,6 @@ public class LocationService: NSObject {
let locationData = LocationData( let locationData = LocationData(
latitude: latitude, latitude: latitude,
longitude: longitude, longitude: longitude,
address: "",
timestamp: Date().timeIntervalSince1970 * 1000 timestamp: Date().timeIntervalSince1970 * 1000
) )
completion(.success(locationData)) completion(.success(locationData))
@ -273,7 +282,6 @@ public class LocationService: NSObject {
return [ return [
"user_lat": String(locationData.latitude), "user_lat": String(locationData.latitude),
"user_lng": String(locationData.longitude), "user_lng": String(locationData.longitude),
"user_location": locationData.address,
"timestamp": locationData.timestamp "timestamp": locationData.timestamp
] ]
} }

2
local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServicePlugin.kt

@ -24,7 +24,7 @@ class MusicServicePlugin: FlutterPlugin, MethodCallHandler {
} }
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
Log.d(TAG, "Method call: ${call.method}") // Log.d(TAG, "Method call: ${call.method}")
when (call.method) { when (call.method) {
"play" -> { "play" -> {
val song = call.argument<Map<String, Any>>("song") val song = call.argument<Map<String, Any>>("song")

2
local_plugins/music_service/ios/music_service/Sources/music_service/MusicServicePlugin.swift

@ -13,7 +13,7 @@ public class MusicServicePlugin: NSObject, FlutterPlugin {
} }
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
print("Method call: \(call.method)") // print("Method call: \(call.method)")
switch call.method { switch call.method {
case "play": case "play":

2
local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/MCPClient.kt

@ -768,7 +768,7 @@ class MCPClient(private val context: Context? = null) : AutoCloseable {
// 保存函数定义 // 保存函数定义
localFunctionDefs[name] = ObjectMapper().writeValueAsString(functionDef) localFunctionDefs[name] = ObjectMapper().writeValueAsString(functionDef)
Log.d(TAG, "注册本地函数成功: $name") // Log.d(TAG, "注册本地函数成功: $name")
return true return true
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "注册本地函数失败: ${e.message}", e) Log.e(TAG, "注册本地函数失败: ${e.message}", e)

2
local_plugins/open_ai_service/ios/Classes/MCPClient.swift

@ -312,7 +312,7 @@ public class MCPClient {
let jsonString = String(data: jsonData, encoding: .utf8) ?? "" let jsonString = String(data: jsonData, encoding: .utf8) ?? ""
localFunctionDefs[name] = jsonString localFunctionDefs[name] = jsonString
print("\(tag) 成功注册本地函数: \(name)") // print("\(tag) 成功注册本地函数: \(name)")
return true return true
} catch { } catch {
print("\(tag) 注册本地函数失败: \(error.localizedDescription)") print("\(tag) 注册本地函数失败: \(error.localizedDescription)")

2
local_plugins/open_ai_service/ios/open_ai_service/Sources/open_ai_service/MCPClient.swift

@ -312,7 +312,7 @@ public class MCPClient {
let jsonString = String(data: jsonData, encoding: .utf8) ?? "" let jsonString = String(data: jsonData, encoding: .utf8) ?? ""
localFunctionDefs[name] = jsonString localFunctionDefs[name] = jsonString
print("\(tag) 成功注册本地函数: \(name)") // print("\(tag) 成功注册本地函数: \(name)")
return true return true
} catch { } catch {
print("\(tag) 注册本地函数失败: \(error.localizedDescription)") print("\(tag) 注册本地函数失败: \(error.localizedDescription)")

2
local_plugins/spotify_sdk/ios/spotify_sdk.podspec

@ -20,7 +20,7 @@ Unofficial Spotify Flutter SDK.
# Flutter.framework does not contain a i386 slice. Only x86_64 simulators are supported. # 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.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' }
s.swift_version = '5.0' s.swift_version = '5.0'
s.prepare_command = './prepare-iOS-SDK.sh' # s.prepare_command = './prepare-iOS-SDK.sh'
s.vendored_frameworks = 'ios-sdk/SpotifyiOS.xcframework' s.vendored_frameworks = 'ios-sdk/SpotifyiOS.xcframework'
s.preserve_paths = 'ios-sdk/SpotifyiOS.xcframework' s.preserve_paths = 'ios-sdk/SpotifyiOS.xcframework'
end end

6
pubspec.yaml

@ -1,7 +1,7 @@
name: voitrans name: voitrans
description: "Voitrans - AI Voice Assistant." description: "Voitrans - AI Voice Assistant."
publish_to: "none" publish_to: "none"
version: 1.0.3+11 version: 1.0.6+20
environment: environment:
sdk: ">=3.3.0 <4.0.0" sdk: ">=3.3.0 <4.0.0"
@ -27,6 +27,7 @@ dependencies:
parchment: ^1.24.0 parchment: ^1.24.0
htmltopdfwidgets: ^1.1.0 htmltopdfwidgets: ^1.1.0
wifi_iot: ^0.3.19+2 wifi_iot: ^0.3.19+2
flutter_intro: ^3.4.0
permission_handler: ^10.4.5 permission_handler: ^10.4.5
web_socket_channel: ^2.4.0 web_socket_channel: ^2.4.0
flutter_dotenv: ^5.1.0 flutter_dotenv: ^5.1.0
@ -59,7 +60,7 @@ dependencies:
font_awesome_flutter: ^10.8.0 font_awesome_flutter: ^10.8.0
lottie: ^3.3.1 lottie: ^3.3.1
app_settings: 5.2.0 app_settings: 5.2.0
fluwx: ^5.4.1 fluwx: ^5.7.1
url_launcher: ^6.3.1 url_launcher: ^6.3.1
firebase_core: ^3.12.1 firebase_core: ^3.12.1
firebase_auth: ^5.5.1 firebase_auth: ^5.5.1
@ -69,6 +70,7 @@ dependencies:
flutter_image_compress: ^2.4.0 flutter_image_compress: ^2.4.0
common_utils: ^2.1.0 common_utils: ^2.1.0
image_gallery_saver_plus: ^4.0.1 image_gallery_saver_plus: ^4.0.1
app_tracking_transparency: ^2.0.1
dio: ^5.8.0+1 dio: ^5.8.0+1
sqflite: ^2.4.2 sqflite: ^2.4.2
path: ^1.9.1 path: ^1.9.1

Loading…
Cancel
Save