Browse Source

Merge branch 'ios' of https://github.com/deepcloud2048/deep_voice into liwei_ios

newdev_shunjiawei
liwei1dao 1 year ago
parent
commit
5d00c7a3de
  1. 1
      .cursorrules
  2. 2
      ios/Flutter/Profile.xcconfig
  3. 1
      ios/Flutter/Release.xcconfig
  4. 7
      ios/Podfile
  5. 271
      ios/Podfile.lock
  6. 177
      ios/Runner.xcodeproj/project.pbxproj
  7. 213
      ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
  8. 18
      ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
  9. 213
      ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved
  10. 75
      ios/Runner/Info.plist
  11. 9
      ios/Runner/Runner.entitlements
  12. 4
      lib/core/bindings/initial_binding.dart
  13. 103
      lib/data/services/ble_manager.dart
  14. 2
      lib/data/services/bluetooth_manager.dart
  15. 18
      lib/data/services/speech_factory.dart
  16. 478
      lib/data/services/speech_impl/flutter_asr_service.dart
  17. 484
      lib/data/services/speech_impl/flutter_tts_service.dart
  18. 60
      lib/modules/agent/controllers/agent_controller.dart
  19. 1
      lib/modules/home/controllers/home_controller.dart
  20. 3
      lib/modules/home/views/home_view.dart
  21. 9
      lib/modules/opus_test/bindings/opus_test_binding.dart
  22. 404
      lib/modules/opus_test/controllers/opus_test_controller.dart
  23. 445
      lib/modules/opus_test/views/opus_test_view.dart
  24. 1010
      lib/modules/pairing/controllers/pairing_controller.dart
  25. 380
      lib/modules/pairing/views/pairing_view.dart
  26. 24
      lib/modules/settings/views/settings_view.dart
  27. 10
      lib/routes/app_pages.dart
  28. 0
      local_plugins/agent_service/LICENSE
  29. 81
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
  30. 994
      local_plugins/agent_service/ios/Classes/AgentService.swift
  31. 991
      local_plugins/agent_service/ios/Classes/AgentServiceImpl.swift
  32. 11
      local_plugins/agent_service/ios/Classes/AgentServiceListener.swift
  33. 2
      local_plugins/agent_service/ios/Classes/AgentServicePlugin.h
  34. 2
      local_plugins/agent_service/ios/Classes/AgentServicePlugin.m
  35. 160
      local_plugins/agent_service/ios/Classes/AgentServicePlugin.swift
  36. 240
      local_plugins/agent_service/ios/Classes/AudioSessionHub.swift
  37. 259
      local_plugins/agent_service/ios/Classes/SwiftAgentServicePlugin.swift
  38. BIN
      local_plugins/agent_service/ios/Resources/calling.mp3
  39. BIN
      local_plugins/agent_service/ios/Resources/start.mp3
  40. BIN
      local_plugins/agent_service/ios/Resources/stop.mp3
  41. 29
      local_plugins/agent_service/ios/agent_service.podspec
  42. 1
      local_plugins/agent_service/lib/agent_service.dart
  43. 15
      local_plugins/agent_service/pubspec.yaml
  44. 2
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt
  45. 34
      local_plugins/azure_speech/ios/.gitignore
  46. 888
      local_plugins/azure_speech/ios/Classes/AzureAsrHelper.swift
  47. 17
      local_plugins/azure_speech/ios/Classes/AzureSpeechPlugin.h
  48. 588
      local_plugins/azure_speech/ios/Classes/AzureSpeechPlugin.swift
  49. 776
      local_plugins/azure_speech/ios/Classes/AzureTtsHelper.swift
  50. 26
      local_plugins/azure_speech/ios/Flutter/AppFrameworkInfo.plist
  51. 1
      local_plugins/azure_speech/ios/Flutter/Debug.xcconfig
  52. 1
      local_plugins/azure_speech/ios/Flutter/Release.xcconfig
  53. 619
      local_plugins/azure_speech/ios/Runner.xcodeproj/project.pbxproj
  54. 7
      local_plugins/azure_speech/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
  55. 8
      local_plugins/azure_speech/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
  56. 8
      local_plugins/azure_speech/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
  57. 7
      local_plugins/azure_speech/ios/Runner.xcworkspace/contents.xcworkspacedata
  58. 8
      local_plugins/azure_speech/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
  59. 8
      local_plugins/azure_speech/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
  60. 13
      local_plugins/azure_speech/ios/Runner/AppDelegate.swift
  61. 122
      local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
  62. BIN
      local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
  63. BIN
      local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
  64. BIN
      local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
  65. BIN
      local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
  66. BIN
      local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
  67. BIN
      local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
  68. BIN
      local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
  69. BIN
      local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
  70. BIN
      local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
  71. BIN
      local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
  72. BIN
      local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
  73. BIN
      local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
  74. BIN
      local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
  75. BIN
      local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
  76. BIN
      local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
  77. 23
      local_plugins/azure_speech/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
  78. BIN
      local_plugins/azure_speech/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
  79. BIN
      local_plugins/azure_speech/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
  80. BIN
      local_plugins/azure_speech/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
  81. 5
      local_plugins/azure_speech/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
  82. 37
      local_plugins/azure_speech/ios/Runner/Base.lproj/LaunchScreen.storyboard
  83. 26
      local_plugins/azure_speech/ios/Runner/Base.lproj/Main.storyboard
  84. 49
      local_plugins/azure_speech/ios/Runner/Info.plist
  85. 1
      local_plugins/azure_speech/ios/Runner/Runner-Bridging-Header.h
  86. 12
      local_plugins/azure_speech/ios/RunnerTests/RunnerTests.swift
  87. 21
      local_plugins/azure_speech/ios/azure_speech.podspec
  88. 20
      local_plugins/azure_speech/lib/main.dart
  89. 17
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt
  90. 404
      local_plugins/ble_service/ios/Classes/BleAccessorySetup.swift
  91. 1076
      local_plugins/ble_service/ios/Classes/BleService.swift
  92. 391
      local_plugins/ble_service/ios/Classes/SwiftBleServicePlugin.swift
  93. 164
      local_plugins/ble_service/ios/Classes/SwiftOpusAudioProcessor.swift
  94. 11
      local_plugins/ble_service/ios/ble_service.podspec
  95. 13
      local_plugins/ble_service/lib/ble_service.dart
  96. 58
      local_plugins/ble_service/pubspec.yaml
  97. 84
      local_plugins/chat_api/IOS_INTEGRATION_LEGACY.md
  98. 0
      local_plugins/chat_api/LICENSE
  99. 239
      local_plugins/chat_api/README.md
  100. 108
      local_plugins/chat_api/SPM_MIGRATION.md

1
.cursorrules

@ -26,6 +26,7 @@
• 并发场景首选 Kotlin 协程;仅在 Flutter 端按需使用 Dart Future / Stream。
3. 语言
• 与用户交流、注释及文档一律使用中文。
• 不要自己创建总结文档。
宗旨:保持极简,先跑通业务,再考虑优化。

2
ios/Flutter/Profile.xcconfig

@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"
#include "Generated.xcconfig"

1
ios/Flutter/Release.xcconfig

@ -1,2 +1,3 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"
#include "Generated.xcconfig"

7
ios/Podfile

@ -4,6 +4,7 @@ platform :ios, '18.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
@ -27,6 +28,7 @@ require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelpe
flutter_ios_podfile_setup
target 'Runner' do
use_frameworks!
@ -39,5 +41,10 @@ end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
# Ensure all pods use the same iOS deployment target
target.build_configurations.each do |config|
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '18.0'
end
end
end

271
ios/Podfile.lock

@ -1,55 +1,25 @@
PODS:
- app_settings (5.1.1):
- Flutter
- AppAuth (1.7.6):
- AppAuth/Core (= 1.7.6)
- AppAuth/ExternalUserAgent (= 1.7.6)
- AppAuth/Core (1.7.6)
- AppAuth/ExternalUserAgent (1.7.6):
- AppAuth/Core
- audio_session (0.0.1):
- agent_service (0.0.1):
- azure_speech
- ble_service
- chat_storage
- Flutter
- open_ai_service
- audio_waveforms (0.0.1):
- Flutter
- azure_speech (0.0.1):
- Flutter
- MicrosoftCognitiveServicesSpeech-iOS (~> 1.37.0)
- speech
- ble_service (0.0.1):
- Flutter
- YbridOpus (~> 0.8)
- chat_storage (0.0.1):
- Flutter
- classic_bluetooth (0.0.1):
- Flutter
- device_info_plus (0.0.1):
- Flutter
- DKImagePickerController/Core (4.3.9):
- DKImagePickerController/ImageDataManager
- DKImagePickerController/Resource
- DKImagePickerController/ImageDataManager (4.3.9)
- DKImagePickerController/PhotoGallery (4.3.9):
- DKImagePickerController/Core
- DKPhotoGallery
- DKImagePickerController/Resource (4.3.9)
- DKPhotoGallery (0.0.19):
- DKPhotoGallery/Core (= 0.0.19)
- DKPhotoGallery/Model (= 0.0.19)
- DKPhotoGallery/Preview (= 0.0.19)
- DKPhotoGallery/Resource (= 0.0.19)
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Core (0.0.19):
- DKPhotoGallery/Model
- DKPhotoGallery/Preview
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Model (0.0.19):
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Preview (0.0.19):
- DKPhotoGallery/Model
- DKPhotoGallery/Resource
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Resource (0.0.19):
- SDWebImage
- SwiftyGif
- FBAEMKit (17.1.0):
- FBSDKCoreKit_Basics (= 17.1.0)
- FBSDKCoreKit (17.1.0):
@ -58,44 +28,7 @@ PODS:
- FBSDKCoreKit_Basics (17.1.0)
- FBSDKLoginKit (17.1.0):
- FBSDKCoreKit (= 17.1.0)
- file_picker (0.0.1):
- DKImagePickerController/PhotoGallery
- Flutter
- Firebase/Auth (11.10.0):
- Firebase/CoreOnly
- FirebaseAuth (~> 11.10.0)
- Firebase/CoreOnly (11.10.0):
- FirebaseCore (~> 11.10.0)
- firebase_auth (5.5.2):
- Firebase/Auth (= 11.10.0)
- firebase_core
- Flutter
- firebase_core (3.13.0):
- Firebase/CoreOnly (= 11.10.0)
- Flutter
- FirebaseAppCheckInterop (11.12.0)
- FirebaseAuth (11.10.0):
- FirebaseAppCheckInterop (~> 11.0)
- FirebaseAuthInterop (~> 11.0)
- FirebaseCore (~> 11.10.0)
- FirebaseCoreExtension (~> 11.10.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
- GoogleUtilities/Environment (~> 8.0)
- GTMSessionFetcher/Core (< 5.0, >= 3.4)
- RecaptchaInterop (~> 101.0)
- FirebaseAuthInterop (11.12.0)
- FirebaseCore (11.10.0):
- FirebaseCoreInternal (~> 11.10.0)
- GoogleUtilities/Environment (~> 8.0)
- GoogleUtilities/Logger (~> 8.0)
- FirebaseCoreExtension (11.10.0):
- FirebaseCore (~> 11.10.0)
- FirebaseCoreInternal (11.10.0):
- "GoogleUtilities/NSData+zlib (~> 8.0)"
- Flutter (1.0.0)
- flutter_blue_plus_darwin (0.0.2):
- Flutter
- FlutterMacOS
- flutter_facebook_auth (7.1.1):
- FBSDKLoginKit (~> 17.1.0)
- Flutter
@ -116,54 +49,10 @@ PODS:
- fluwx/pay (0.0.1):
- Flutter
- WechatOpenSDK-XCFramework (~> 2.0.4)
- google_sign_in_ios (0.0.1):
- AppAuth (>= 1.7.4)
- Flutter
- FlutterMacOS
- GoogleSignIn (~> 7.1)
- GTMSessionFetcher (>= 3.4.0)
- GoogleSignIn (7.1.0):
- AppAuth (< 2.0, >= 1.7.3)
- GTMAppAuth (< 5.0, >= 4.1.1)
- GTMSessionFetcher/Core (~> 3.3)
- GoogleUtilities/AppDelegateSwizzler (8.1.0):
- GoogleUtilities/Environment
- GoogleUtilities/Logger
- GoogleUtilities/Network
- GoogleUtilities/Privacy
- GoogleUtilities/Environment (8.1.0):
- GoogleUtilities/Privacy
- GoogleUtilities/Logger (8.1.0):
- GoogleUtilities/Environment
- GoogleUtilities/Privacy
- GoogleUtilities/Network (8.1.0):
- GoogleUtilities/Logger
- "GoogleUtilities/NSData+zlib"
- GoogleUtilities/Privacy
- GoogleUtilities/Reachability
- "GoogleUtilities/NSData+zlib (8.1.0)":
- GoogleUtilities/Privacy
- GoogleUtilities/Privacy (8.1.0)
- GoogleUtilities/Reachability (8.1.0):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- GTMAppAuth (4.1.1):
- AppAuth/Core (~> 1.7)
- GTMSessionFetcher/Core (< 4.0, >= 3.3)
- GTMSessionFetcher (3.5.0):
- GTMSessionFetcher/Full (= 3.5.0)
- GTMSessionFetcher/Core (3.5.0)
- GTMSessionFetcher/Full (3.5.0):
- GTMSessionFetcher/Core
- haptic_feedback (0.5.1):
- Flutter
- image_picker_ios (0.0.1):
- Flutter
- jl_ota (0.0.1):
- Flutter
- just_audio (0.0.1):
- Flutter
- FlutterMacOS
- libwebp (1.5.0):
- libwebp/demux (= 1.5.0)
- libwebp/mux (= 1.5.0)
@ -179,15 +68,11 @@ PODS:
- Mantle (2.2.0):
- Mantle/extobjc (= 2.2.0)
- Mantle/extobjc (2.2.0)
- MicrosoftCognitiveServicesSpeech-iOS (1.37.0)
- music_service (0.0.1):
- Flutter
- open_ai_service (0.0.1):
- Flutter
- package_info_plus (0.4.5):
- Flutter
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- permission_handler_apple (9.3.0):
- Flutter
- QCloudCore (6.4.7):
@ -199,7 +84,6 @@ PODS:
- QCloudCOSXML/Default (6.4.7):
- QCloudCore (= 6.4.7)
- QCloudTrack/Beacon (6.4.7)
- RecaptchaInterop (101.0.0)
- record_darwin (1.0.0):
- Flutter
- SDWebImage (5.21.0):
@ -208,103 +92,65 @@ PODS:
- SDWebImageWebPCoder (0.14.6):
- libwebp (~> 1.0)
- SDWebImage/Core (~> 5.17)
- share_plus (0.0.1):
- Flutter
- speech_to_text (0.0.1):
- speech (0.0.1):
- Flutter
- FlutterMacOS
- Try
- spotify_sdk (0.0.1):
- Flutter
- sqflite_darwin (0.0.4):
- Flutter
- FlutterMacOS
- SwiftyGif (5.4.5)
- tencentcloud_cos_sdk_plugin (1.2.3):
- Flutter
- QCloudCOSXML (= 6.4.7)
- Try (2.1.1)
- webview_flutter_wkwebview (0.0.1):
- Flutter
- FlutterMacOS
- WechatOpenSDK-XCFramework (2.0.4)
- YbridOpus (0.8.0)
DEPENDENCIES:
- app_settings (from `.symlinks/plugins/app_settings/ios`)
- audio_session (from `.symlinks/plugins/audio_session/ios`)
- agent_service (from `.symlinks/plugins/agent_service/ios`)
- audio_waveforms (from `.symlinks/plugins/audio_waveforms/ios`)
- azure_speech (from `.symlinks/plugins/azure_speech/ios`)
- ble_service (from `.symlinks/plugins/ble_service/ios`)
- chat_storage (from `.symlinks/plugins/chat_storage/ios`)
- classic_bluetooth (from `.symlinks/plugins/classic_bluetooth/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- file_picker (from `.symlinks/plugins/file_picker/ios`)
- firebase_auth (from `.symlinks/plugins/firebase_auth/ios`)
- firebase_core (from `.symlinks/plugins/firebase_core/ios`)
- Flutter (from `Flutter`)
- flutter_blue_plus_darwin (from `.symlinks/plugins/flutter_blue_plus_darwin/darwin`)
- flutter_facebook_auth (from `.symlinks/plugins/flutter_facebook_auth/ios`)
- flutter_image_compress_common (from `.symlinks/plugins/flutter_image_compress_common/ios`)
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
- flutter_tts (from `.symlinks/plugins/flutter_tts/ios`)
- fluwx (from `.symlinks/plugins/fluwx/ios`)
- google_sign_in_ios (from `.symlinks/plugins/google_sign_in_ios/darwin`)
- haptic_feedback (from `.symlinks/plugins/haptic_feedback/ios`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- jl_ota (from `.symlinks/plugins/jl_ota/ios`)
- just_audio (from `.symlinks/plugins/just_audio/darwin`)
- music_service (from `.symlinks/plugins/music_service/ios`)
- open_ai_service (from `.symlinks/plugins/open_ai_service/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- record_darwin (from `.symlinks/plugins/record_darwin/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- speech_to_text (from `.symlinks/plugins/speech_to_text/darwin`)
- speech (from `.symlinks/plugins/speech/ios`)
- spotify_sdk (from `.symlinks/plugins/spotify_sdk/ios`)
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
- tencentcloud_cos_sdk_plugin (from `.symlinks/plugins/tencentcloud_cos_sdk_plugin/ios`)
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/darwin`)
SPEC REPOS:
trunk:
- AppAuth
- DKImagePickerController
- DKPhotoGallery
- FBAEMKit
- FBSDKCoreKit
- FBSDKCoreKit_Basics
- FBSDKLoginKit
- Firebase
- FirebaseAppCheckInterop
- FirebaseAuth
- FirebaseAuthInterop
- FirebaseCore
- FirebaseCoreExtension
- FirebaseCoreInternal
- GoogleSignIn
- GoogleUtilities
- GTMAppAuth
- GTMSessionFetcher
- libwebp
- Mantle
- MicrosoftCognitiveServicesSpeech-iOS
- QCloudCore
- QCloudCOSXML
- QCloudTrack
- RecaptchaInterop
- SDWebImage
- SDWebImageWebPCoder
- SwiftyGif
- Try
- WechatOpenSDK-XCFramework
- YbridOpus
EXTERNAL SOURCES:
app_settings:
:path: ".symlinks/plugins/app_settings/ios"
audio_session:
:path: ".symlinks/plugins/audio_session/ios"
agent_service:
:path: ".symlinks/plugins/agent_service/ios"
audio_waveforms:
:path: ".symlinks/plugins/audio_waveforms/ios"
azure_speech:
:path: ".symlinks/plugins/azure_speech/ios"
ble_service:
:path: ".symlinks/plugins/ble_service/ios"
chat_storage:
@ -313,16 +159,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/classic_bluetooth/ios"
device_info_plus:
:path: ".symlinks/plugins/device_info_plus/ios"
file_picker:
:path: ".symlinks/plugins/file_picker/ios"
firebase_auth:
:path: ".symlinks/plugins/firebase_auth/ios"
firebase_core:
:path: ".symlinks/plugins/firebase_core/ios"
Flutter:
:path: Flutter
flutter_blue_plus_darwin:
:path: ".symlinks/plugins/flutter_blue_plus_darwin/darwin"
flutter_facebook_auth:
:path: ".symlinks/plugins/flutter_facebook_auth/ios"
flutter_image_compress_common:
@ -335,107 +173,64 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/flutter_tts/ios"
fluwx:
:path: ".symlinks/plugins/fluwx/ios"
google_sign_in_ios:
:path: ".symlinks/plugins/google_sign_in_ios/darwin"
haptic_feedback:
:path: ".symlinks/plugins/haptic_feedback/ios"
image_picker_ios:
:path: ".symlinks/plugins/image_picker_ios/ios"
jl_ota:
:path: ".symlinks/plugins/jl_ota/ios"
just_audio:
:path: ".symlinks/plugins/just_audio/darwin"
music_service:
:path: ".symlinks/plugins/music_service/ios"
open_ai_service:
:path: ".symlinks/plugins/open_ai_service/ios"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios"
record_darwin:
:path: ".symlinks/plugins/record_darwin/ios"
share_plus:
:path: ".symlinks/plugins/share_plus/ios"
speech_to_text:
:path: ".symlinks/plugins/speech_to_text/darwin"
speech:
:path: ".symlinks/plugins/speech/ios"
spotify_sdk:
:path: ".symlinks/plugins/spotify_sdk/ios"
sqflite_darwin:
:path: ".symlinks/plugins/sqflite_darwin/darwin"
tencentcloud_cos_sdk_plugin:
:path: ".symlinks/plugins/tencentcloud_cos_sdk_plugin/ios"
webview_flutter_wkwebview:
:path: ".symlinks/plugins/webview_flutter_wkwebview/darwin"
SPEC CHECKSUMS:
app_settings: 5127ae0678de1dcc19f2293271c51d37c89428b2
AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73
audio_session: 9bb7f6c970f21241b19f5a3658097ae459681ba0
agent_service: a95166e1939f18436aa2ae6b80c17751a2f69b89
audio_waveforms: a6dde7fe7c0ea05f06ffbdb0f7c1b2b2ba6cedcf
ble_service: 068ebe51800424f980282b16cbf757574eec4e08
azure_speech: 23c6573d04e7f84a0839e935e527c87b5f78311f
ble_service: 9f394ad44d435010e718bc81de494d9425295a85
chat_storage: 933a96144157a07a2fef7f6acb28c78ff18465d8
classic_bluetooth: c5e5c0c86acab8dcd6b8e9cef7fca654c3518034
classic_bluetooth: c146e1618e867a47fa7ab79a3bf262e7d69d557d
device_info_plus: 71ffc6ab7634ade6267c7a93088ed7e4f74e5896
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
FBAEMKit: cb719c53575a3be86ea873279f30d6a2c4e15881
FBSDKCoreKit: ecdb980a24633ccb012700299ceb16d0235e14d2
FBSDKCoreKit_Basics: 045101c4a9ef10c845347424d73a29aae02c3e43
FBSDKLoginKit: 69eb59b2f839aba635616df6e422acd0ca88030a
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
Firebase: 1fe1c0a7d9aaea32efe01fbea5f0ebd8d70e53a2
firebase_auth: 34c515a8fa76a4ef678ac5ad5cfa3bb3120f5ab5
firebase_core: 2d4534e7b489907dcede540c835b48981d890943
FirebaseAppCheckInterop: 73b173e5ec45192e2d522ad43f526a82ad10b852
FirebaseAuth: c4146bdfdc87329f9962babd24dae89373f49a32
FirebaseAuthInterop: b583210c039a60ed3f1e48865e1f3da44a796595
FirebaseCore: 8344daef5e2661eb004b177488d6f9f0f24251b7
FirebaseCoreExtension: 6f357679327f3614e995dc7cf3f2d600bdc774ac
FirebaseCoreInternal: ef4505d2afb1d0ebbc33162cb3795382904b5679
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_blue_plus_darwin: 09444a26fb6bdef523e55b68fc1c59af5a877ea6
flutter_facebook_auth: 8a9ad5fbfc25b77e7a53ea8986866950b27edcae
flutter_image_compress_common: 1697a328fd72bfb335507c6bca1a65fa5ad87df1
flutter_local_notifications: 395056b3175ba4f08480a7c5de30cd36d69827e4
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
flutter_tts: b88dbc8655d3dc961bc4a796e4e16a4cc1795833
fluwx: 6bf9c5a3a99ad31b0de137dd92370a0d10a60f4b
google_sign_in_ios: 19297361f2c51d7d8ac0201b866ef1fa5d1f94a8
GoogleSignIn: d4281ab6cf21542b1cfaff85c191f230b399d2db
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
GTMAppAuth: f69bd07d68cd3b766125f7e072c45d7340dea0de
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
haptic_feedback: fd1d8509833f58f164fc12122c0357fa9b2750e0
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
jl_ota: 5fa90bf47674de0f5bce9af2219e25fa0c1425f5
just_audio: 4e391f57b79cad2b0674030a00453ca5ce817eed
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d
MicrosoftCognitiveServicesSpeech-iOS: 766852ccd56e0c74bafbe9ea7033083c5f748d04
music_service: 85abecc8fa21599a06907ad50d83f48a58e0dc0d
open_ai_service: 82ea947b1a081a2a17bc0c8e5467dcbd7cf9b232
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
open_ai_service: d78dff9359ee4c04f20d250d655d6ec75b4c5434
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
QCloudCore: 49e7571984a8e5fd8f0fd58c2f3b806255796223
QCloudCOSXML: 7205e76aa9cf613468222615483c9d7c074e4298
QCloudTrack: 3b53a7fc4fe3920e407f2aa73f2452992a61f7f3
RecaptchaInterop: 11e0b637842dfb48308d242afc3f448062325aba
record_darwin: fb1f375f1d9603714f55b8708a903bbb91ffdb0a
SDWebImage: f84b0feeb08d2d11e6a9b843cb06d75ebf5b8868
SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
speech_to_text: 9dc43a5df3cbc2813f8c7cc9bd0fbf94268ed7ac
speech: 89e648ea83c83d32b25ad2247fdec80fdddd7fb6
spotify_sdk: a48400bb29f70c4fe251ebfdc9135c37097ac5ca
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
tencentcloud_cos_sdk_plugin: c3d78b2c3bae42b5a20009dc047f3f1621e6272b
Try: 5ef669ae832617b3cee58cb2c6f99fb767a4ff96
webview_flutter_wkwebview: 1821ceac936eba6f7984d89a9f3bcb4dea99ebb2
WechatOpenSDK-XCFramework: 36fb2bea0754266c17184adf4963d7e6ff98b69f
YbridOpus: e9af373b9b38acff3702d9a3bd97cae3c232887c
PODFILE CHECKSUM: f62382080fcf32f2d4640dc419aa07d69eff0328
PODFILE CHECKSUM: c47ae1d38025d25153dc07bcb7a82393e57bb809
COCOAPODS: 1.16.2

177
ios/Runner.xcodeproj/project.pbxproj

@ -11,12 +11,13 @@
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
6077538D78AAB34545D7F3DD /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E27AD0DBE6531F2655DB9986 /* Pods_Runner.framework */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
A7986DFCFDA104638080424B /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F79ADB762804BBED65875C03 /* Pods_RunnerTests.framework */; };
9EB82175436F3A3D593E4423 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E9725ECE141452716001447D /* Pods_Runner.framework */; };
DD1E3297BCF7B8F42676149E /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 41D979DCCB9FC1A2739ED9E2 /* Pods_RunnerTests.framework */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -43,6 +44,7 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
0A9A0EEC032E92406E64632E /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
0AD944112DD9DB5C00557D21 /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
0AD944132DDA0C0D00557D21 /* AccessorySetupKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AccessorySetupKit.framework; path = System/Library/Frameworks/AccessorySetupKit.framework; sourceTree = SDKROOT; };
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
@ -50,12 +52,12 @@
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
6189535D5FBB81D8377BE874 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
742A00629970110BA5008064 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
41D979DCCB9FC1A2739ED9E2 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
762C8874AAE15CA0CB901347 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
8960B97871604358D5CA1866 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
932005D16734B31A8CB0583F /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
@ -63,12 +65,29 @@
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
B3EC1304D6F5453D58E0BDB0 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
C15B3030DF1AED99D3829135 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
D1CF75E5BD49BB58812D5F36 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
BCF37DA3F60C9FDB47151D46 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
D2958658C06A5ADF62D6ABA2 /* Runner.entitlements */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
E27AD0DBE6531F2655DB9986 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
F79ADB762804BBED65875C03 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
D2A873E56435FC1342AE0A85 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
D7BFB5FE33B0270C30A1DFAA /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
E9725ECE141452716001447D /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
0AD9449B2DE6F89600557D21 /* Remove Bitcode from YbridOpus */ = {
isa = PBXFileReference;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "Remove Bitcode from YbridOpus";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "# 移除YbridOpus框架中的bitcode\necho \"检查并移除YbridOpus框架中的bitcode...\"\n\nFRAMEWORK_PATHS=(\n \"${PODS_ROOT}/YbridOpus/YbridOpus.xcframework/ios-arm64_armv7/YbridOpus.framework/YbridOpus\"\n \"${PODS_ROOT}/YbridOpus/YbridOpus.xcframework/ios-i386_x86_64-simulator/YbridOpus.framework/YbridOpus\"\n \"${PODS_ROOT}/YbridOpus/YbridOpus.xcframework/ios-arm64_x86_64-maccatalyst/YbridOpus.framework/YbridOpus\"\n \"${PODS_ROOT}/YbridOpus/YbridOpus.xcframework/macos-arm64_x86_64/YbridOpus.framework/YbridOpus\"\n)\n\nfor framework_path in \"${FRAMEWORK_PATHS[@]}\"; do\n if [ -f \"$framework_path\" ]; then\n if otool -l \"$framework_path\" | grep -q __LLVM; then\n echo \"移除bitcode: $framework_path\"\n # 直接覆盖,不创建备份\n xcrun bitcode_strip -r \"$framework_path\" -o \"$framework_path.tmp\"\n mv \"$framework_path.tmp\" \"$framework_path\"\n fi\n fi\ndone\n\n# 清理任何可能的备份文件\nfind \"${PODS_ROOT}\" -name \"*.backup\" -type f -delete 2>/dev/null || true\nfind \"${BUILT_PRODUCTS_DIR}\" -name \"*.backup\" -type f -delete 2>/dev/null || true\n\necho \"bitcode移除和清理完成!\"";
};
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -76,7 +95,8 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
6077538D78AAB34545D7F3DD /* Pods_Runner.framework in Frameworks */,
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
9EB82175436F3A3D593E4423 /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -84,7 +104,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
A7986DFCFDA104638080424B /* Pods_RunnerTests.framework in Frameworks */,
DD1E3297BCF7B8F42676149E /* Pods_RunnerTests.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -94,12 +114,12 @@
1ACB686EC7DC36C6E33D372F /* Pods */ = {
isa = PBXGroup;
children = (
762C8874AAE15CA0CB901347 /* Pods-Runner.debug.xcconfig */,
B3EC1304D6F5453D58E0BDB0 /* Pods-Runner.release.xcconfig */,
6189535D5FBB81D8377BE874 /* Pods-Runner.profile.xcconfig */,
C15B3030DF1AED99D3829135 /* Pods-RunnerTests.debug.xcconfig */,
742A00629970110BA5008064 /* Pods-RunnerTests.release.xcconfig */,
D1CF75E5BD49BB58812D5F36 /* Pods-RunnerTests.profile.xcconfig */,
D2A873E56435FC1342AE0A85 /* Pods-Runner.debug.xcconfig */,
932005D16734B31A8CB0583F /* Pods-Runner.release.xcconfig */,
BCF37DA3F60C9FDB47151D46 /* Pods-Runner.profile.xcconfig */,
0A9A0EEC032E92406E64632E /* Pods-RunnerTests.debug.xcconfig */,
8960B97871604358D5CA1866 /* Pods-RunnerTests.release.xcconfig */,
D7BFB5FE33B0270C30A1DFAA /* Pods-RunnerTests.profile.xcconfig */,
);
path = Pods;
sourceTree = "<group>";
@ -108,8 +128,8 @@
isa = PBXGroup;
children = (
0AD944132DDA0C0D00557D21 /* AccessorySetupKit.framework */,
E27AD0DBE6531F2655DB9986 /* Pods_Runner.framework */,
F79ADB762804BBED65875C03 /* Pods_RunnerTests.framework */,
E9725ECE141452716001447D /* Pods_Runner.framework */,
41D979DCCB9FC1A2739ED9E2 /* Pods_RunnerTests.framework */,
);
name = Frameworks;
sourceTree = "<group>";
@ -178,7 +198,7 @@
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
D21548F12B1535261EB87672 /* [CP] Check Pods Manifest.lock */,
BF953267BBF1D433B6E02C09 /* [CP] Check Pods Manifest.lock */,
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
B3515CD9628E3332AFC88CD4 /* Frameworks */,
@ -197,21 +217,25 @@
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
90215E32C3610039A2575557 /* [CP] Check Pods Manifest.lock */,
99B1E635188BC8E58C3A1B67 /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
510B02006C08E514BF31EFD9 /* [CP] Embed Pods Frameworks */,
298589D956D542AE0EE16DAE /* [CP] Copy Pods Resources */,
0AD9449B2DE6F89600557D21 /* Remove Bitcode from YbridOpus */,
CC018A1832E51ED5B994765E /* [CP] Embed Pods Frameworks */,
A196696487421EBA40281C38 /* [CP] Copy Pods Resources */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
packageProductDependencies = (
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
);
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
@ -245,6 +269,9 @@
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
packageReferences = (
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,
);
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
@ -278,57 +305,56 @@
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
298589D956D542AE0EE16DAE /* [CP] Copy Pods Resources */ = {
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
0AD9449B2DE6F89600557D21 /* Remove Bitcode from YbridOpus */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
name = "Remove Bitcode from YbridOpus";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
shellScript = "# 移除YbridOpus框架中的bitcode\necho \"检查并移除YbridOpus框架中的bitcode...\"\n\nFRAMEWORK_PATHS=(\n \"${PODS_ROOT}/YbridOpus/YbridOpus.xcframework/ios-arm64_armv7/YbridOpus.framework/YbridOpus\"\n \"${PODS_ROOT}/YbridOpus/YbridOpus.xcframework/ios-i386_x86_64-simulator/YbridOpus.framework/YbridOpus\"\n \"${PODS_ROOT}/YbridOpus/YbridOpus.xcframework/ios-arm64_x86_64-maccatalyst/YbridOpus.framework/YbridOpus\"\n \"${PODS_ROOT}/YbridOpus/YbridOpus.xcframework/macos-arm64_x86_64/YbridOpus.framework/YbridOpus\"\n)\n\nfor framework_path in \"${FRAMEWORK_PATHS[@]}\"; do\n if [ -f \"$framework_path\" ]; then\n if otool -l \"$framework_path\" | grep -q __LLVM; then\n echo \"移除bitcode: $framework_path\"\n # 直接覆盖,不创建备份\n xcrun bitcode_strip -r \"$framework_path\" -o \"$framework_path.tmp\"\n mv \"$framework_path.tmp\" \"$framework_path\"\n fi\n fi\ndone\n\n# 清理任何可能的备份文件\nfind \"${PODS_ROOT}\" -name \"*.backup\" -type f -delete 2>/dev/null || true\nfind \"${BUILT_PRODUCTS_DIR}\" -name \"*.backup\" -type f -delete 2>/dev/null || true\n\necho \"bitcode移除和清理完成!\"";
};
510B02006C08E514BF31EFD9 /* [CP] Embed Pods Frameworks */ = {
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
inputPaths = (
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
90215E32C3610039A2575557 /* [CP] Check Pods Manifest.lock */ = {
99B1E635188BC8E58C3A1B67 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
@ -350,22 +376,24 @@
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
9740EEB61CF901F6004384FC /* Run Script */ = {
A196696487421EBA40281C38 /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
name = "Run Script";
outputPaths = (
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0;
};
D21548F12B1535261EB87672 /* [CP] Check Pods Manifest.lock */ = {
BF953267BBF1D433B6E02C09 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
@ -387,6 +415,23 @@
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
CC018A1832E51ED5B994765E /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
@ -481,7 +526,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@ -516,7 +561,7 @@
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = C15B3030DF1AED99D3829135 /* Pods-RunnerTests.debug.xcconfig */;
baseConfigurationReference = 0A9A0EEC032E92406E64632E /* Pods-RunnerTests.debug.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
@ -535,7 +580,7 @@
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 742A00629970110BA5008064 /* Pods-RunnerTests.release.xcconfig */;
baseConfigurationReference = 8960B97871604358D5CA1866 /* Pods-RunnerTests.release.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
@ -552,7 +597,7 @@
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = D1CF75E5BD49BB58812D5F36 /* Pods-RunnerTests.profile.xcconfig */;
baseConfigurationReference = D7BFB5FE33B0270C30A1DFAA /* Pods-RunnerTests.profile.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
@ -617,7 +662,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@ -669,7 +714,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@ -763,6 +808,20 @@
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCLocalSwiftPackageReference section */
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
};
/* End XCLocalSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
isa = XCSwiftPackageProductDependency;
productName = FlutterGeneratedPluginSwiftPackage;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}

213
ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

@ -0,0 +1,213 @@
{
"originHash" : "fb7ecff82f4ff109531189491cc6cc2299a7357c61874f58bae1e0f7b7d9e8d0",
"pins" : [
{
"identity" : "abseil-cpp-binary",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/abseil-cpp-binary.git",
"state" : {
"revision" : "bbe8b69694d7873315fd3a4ad41efe043e1c07c5",
"version" : "1.2024072200.0"
}
},
{
"identity" : "app-check",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/app-check.git",
"state" : {
"revision" : "61b85103a1aeed8218f17c794687781505fbbef5",
"version" : "11.2.0"
}
},
{
"identity" : "appauth-ios",
"kind" : "remoteSourceControl",
"location" : "https://github.com/openid/AppAuth-iOS.git",
"state" : {
"revision" : "2781038865a80e2c425a1da12cc1327bcd56501f",
"version" : "1.7.6"
}
},
{
"identity" : "dkcamera",
"kind" : "remoteSourceControl",
"location" : "https://github.com/zhangao0086/DKCamera",
"state" : {
"branch" : "master",
"revision" : "5c691d11014b910aff69f960475d70e65d9dcc96"
}
},
{
"identity" : "dkimagepickercontroller",
"kind" : "remoteSourceControl",
"location" : "https://github.com/zhangao0086/DKImagePickerController",
"state" : {
"branch" : "4.3.9",
"revision" : "0bdfeacefa308545adde07bef86e349186335915"
}
},
{
"identity" : "dkphotogallery",
"kind" : "remoteSourceControl",
"location" : "https://github.com/zhangao0086/DKPhotoGallery",
"state" : {
"branch" : "master",
"revision" : "311c1bc7a94f1538f82773a79c84374b12a2ef3d"
}
},
{
"identity" : "firebase-ios-sdk",
"kind" : "remoteSourceControl",
"location" : "https://github.com/firebase/firebase-ios-sdk",
"state" : {
"revision" : "3663b1aa6c7a1bed67ee80fd09dc6d0f9c3bb660",
"version" : "11.13.0"
}
},
{
"identity" : "flutterfire",
"kind" : "remoteSourceControl",
"location" : "https://github.com/firebase/flutterfire",
"state" : {
"revision" : "a80a123386fd4904cad6938673020a7bcf31b2f2",
"version" : "3.13.0-firebase-core-swift"
}
},
{
"identity" : "googleappmeasurement",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/GoogleAppMeasurement.git",
"state" : {
"revision" : "543071966b3fb6613a2fc5c6e7112d1e998184a7",
"version" : "11.13.0"
}
},
{
"identity" : "googledatatransport",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/GoogleDataTransport.git",
"state" : {
"revision" : "617af071af9aa1d6a091d59a202910ac482128f9",
"version" : "10.1.0"
}
},
{
"identity" : "googlesignin-ios",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/GoogleSignIn-iOS.git",
"state" : {
"revision" : "a7965d134c5d3567026c523e0a8a583f73b62b0d",
"version" : "7.1.0"
}
},
{
"identity" : "googleutilities",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/GoogleUtilities.git",
"state" : {
"revision" : "60da361632d0de02786f709bdc0c4df340f7613e",
"version" : "8.1.0"
}
},
{
"identity" : "grpc-binary",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/grpc-binary.git",
"state" : {
"revision" : "cc0001a0cf963aa40501d9c2b181e7fc9fd8ec71",
"version" : "1.69.0"
}
},
{
"identity" : "gtm-session-fetcher",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/gtm-session-fetcher.git",
"state" : {
"revision" : "a2ab612cb980066ee56d90d60d8462992c07f24b",
"version" : "3.5.0"
}
},
{
"identity" : "gtmappauth",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/GTMAppAuth.git",
"state" : {
"revision" : "5d7d66f647400952b1758b230e019b07c0b4b22a",
"version" : "4.1.1"
}
},
{
"identity" : "interop-ios-for-google-sdks",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/interop-ios-for-google-sdks.git",
"state" : {
"revision" : "040d087ac2267d2ddd4cca36c757d1c6a05fdbfe",
"version" : "101.0.0"
}
},
{
"identity" : "leveldb",
"kind" : "remoteSourceControl",
"location" : "https://github.com/firebase/leveldb.git",
"state" : {
"revision" : "a0bc79961d7be727d258d33d5a6b2f1023270ba1",
"version" : "1.22.5"
}
},
{
"identity" : "nanopb",
"kind" : "remoteSourceControl",
"location" : "https://github.com/firebase/nanopb.git",
"state" : {
"revision" : "b7e1104502eca3a213b46303391ca4d3bc8ddec1",
"version" : "2.30910.0"
}
},
{
"identity" : "promises",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/promises.git",
"state" : {
"revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac",
"version" : "2.4.0"
}
},
{
"identity" : "sdwebimage",
"kind" : "remoteSourceControl",
"location" : "https://github.com/SDWebImage/SDWebImage",
"state" : {
"revision" : "cac9a55a3ae92478a2c95042dcc8d9695d2129ca",
"version" : "5.21.0"
}
},
{
"identity" : "swift-protobuf",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-protobuf.git",
"state" : {
"revision" : "d72aed98f8253ec1aa9ea1141e28150f408cf17f",
"version" : "1.29.0"
}
},
{
"identity" : "swiftygif",
"kind" : "remoteSourceControl",
"location" : "https://github.com/kirualex/SwiftyGif.git",
"state" : {
"revision" : "4430cbc148baa3907651d40562d96325426f409a",
"version" : "5.4.5"
}
},
{
"identity" : "tocropviewcontroller",
"kind" : "remoteSourceControl",
"location" : "https://github.com/TimOliver/TOCropViewController",
"state" : {
"revision" : "a634cb7cdfd580006e79a6e74e64417fe9e9783b",
"version" : "2.7.4"
}
}
],
"version" : 3
}

18
ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme

@ -5,6 +5,24 @@
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<PreActions>
<ExecutionAction
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
<ActionContent
title = "Run Prepare Flutter Framework Script"
scriptText = "/bin/sh &quot;$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh&quot; prepare&#10;">
<EnvironmentBuildable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</EnvironmentBuildable>
</ActionContent>
</ExecutionAction>
</PreActions>
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"

213
ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved

@ -0,0 +1,213 @@
{
"originHash" : "fb7ecff82f4ff109531189491cc6cc2299a7357c61874f58bae1e0f7b7d9e8d0",
"pins" : [
{
"identity" : "abseil-cpp-binary",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/abseil-cpp-binary.git",
"state" : {
"revision" : "bbe8b69694d7873315fd3a4ad41efe043e1c07c5",
"version" : "1.2024072200.0"
}
},
{
"identity" : "app-check",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/app-check.git",
"state" : {
"revision" : "61b85103a1aeed8218f17c794687781505fbbef5",
"version" : "11.2.0"
}
},
{
"identity" : "appauth-ios",
"kind" : "remoteSourceControl",
"location" : "https://github.com/openid/AppAuth-iOS.git",
"state" : {
"revision" : "2781038865a80e2c425a1da12cc1327bcd56501f",
"version" : "1.7.6"
}
},
{
"identity" : "dkcamera",
"kind" : "remoteSourceControl",
"location" : "https://github.com/zhangao0086/DKCamera",
"state" : {
"branch" : "master",
"revision" : "5c691d11014b910aff69f960475d70e65d9dcc96"
}
},
{
"identity" : "dkimagepickercontroller",
"kind" : "remoteSourceControl",
"location" : "https://github.com/zhangao0086/DKImagePickerController",
"state" : {
"branch" : "4.3.9",
"revision" : "0bdfeacefa308545adde07bef86e349186335915"
}
},
{
"identity" : "dkphotogallery",
"kind" : "remoteSourceControl",
"location" : "https://github.com/zhangao0086/DKPhotoGallery",
"state" : {
"branch" : "master",
"revision" : "311c1bc7a94f1538f82773a79c84374b12a2ef3d"
}
},
{
"identity" : "firebase-ios-sdk",
"kind" : "remoteSourceControl",
"location" : "https://github.com/firebase/firebase-ios-sdk",
"state" : {
"revision" : "3663b1aa6c7a1bed67ee80fd09dc6d0f9c3bb660",
"version" : "11.13.0"
}
},
{
"identity" : "flutterfire",
"kind" : "remoteSourceControl",
"location" : "https://github.com/firebase/flutterfire",
"state" : {
"revision" : "a80a123386fd4904cad6938673020a7bcf31b2f2",
"version" : "3.13.0-firebase-core-swift"
}
},
{
"identity" : "googleappmeasurement",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/GoogleAppMeasurement.git",
"state" : {
"revision" : "543071966b3fb6613a2fc5c6e7112d1e998184a7",
"version" : "11.13.0"
}
},
{
"identity" : "googledatatransport",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/GoogleDataTransport.git",
"state" : {
"revision" : "617af071af9aa1d6a091d59a202910ac482128f9",
"version" : "10.1.0"
}
},
{
"identity" : "googlesignin-ios",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/GoogleSignIn-iOS.git",
"state" : {
"revision" : "a7965d134c5d3567026c523e0a8a583f73b62b0d",
"version" : "7.1.0"
}
},
{
"identity" : "googleutilities",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/GoogleUtilities.git",
"state" : {
"revision" : "60da361632d0de02786f709bdc0c4df340f7613e",
"version" : "8.1.0"
}
},
{
"identity" : "grpc-binary",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/grpc-binary.git",
"state" : {
"revision" : "cc0001a0cf963aa40501d9c2b181e7fc9fd8ec71",
"version" : "1.69.0"
}
},
{
"identity" : "gtm-session-fetcher",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/gtm-session-fetcher.git",
"state" : {
"revision" : "a2ab612cb980066ee56d90d60d8462992c07f24b",
"version" : "3.5.0"
}
},
{
"identity" : "gtmappauth",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/GTMAppAuth.git",
"state" : {
"revision" : "5d7d66f647400952b1758b230e019b07c0b4b22a",
"version" : "4.1.1"
}
},
{
"identity" : "interop-ios-for-google-sdks",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/interop-ios-for-google-sdks.git",
"state" : {
"revision" : "040d087ac2267d2ddd4cca36c757d1c6a05fdbfe",
"version" : "101.0.0"
}
},
{
"identity" : "leveldb",
"kind" : "remoteSourceControl",
"location" : "https://github.com/firebase/leveldb.git",
"state" : {
"revision" : "a0bc79961d7be727d258d33d5a6b2f1023270ba1",
"version" : "1.22.5"
}
},
{
"identity" : "nanopb",
"kind" : "remoteSourceControl",
"location" : "https://github.com/firebase/nanopb.git",
"state" : {
"revision" : "b7e1104502eca3a213b46303391ca4d3bc8ddec1",
"version" : "2.30910.0"
}
},
{
"identity" : "promises",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/promises.git",
"state" : {
"revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac",
"version" : "2.4.0"
}
},
{
"identity" : "sdwebimage",
"kind" : "remoteSourceControl",
"location" : "https://github.com/SDWebImage/SDWebImage",
"state" : {
"revision" : "cac9a55a3ae92478a2c95042dcc8d9695d2129ca",
"version" : "5.21.0"
}
},
{
"identity" : "swift-protobuf",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-protobuf.git",
"state" : {
"revision" : "d72aed98f8253ec1aa9ea1141e28150f408cf17f",
"version" : "1.29.0"
}
},
{
"identity" : "swiftygif",
"kind" : "remoteSourceControl",
"location" : "https://github.com/kirualex/SwiftyGif.git",
"state" : {
"revision" : "4430cbc148baa3907651d40562d96325426f409a",
"version" : "5.4.5"
}
},
{
"identity" : "tocropviewcontroller",
"kind" : "remoteSourceControl",
"location" : "https://github.com/TimOliver/TOCropViewController",
"state" : {
"revision" : "a634cb7cdfd580006e79a6e74e64417fe9e9783b",
"version" : "2.7.4"
}
}
],
"version" : 3
}

75
ios/Runner/Info.plist

@ -41,12 +41,14 @@
</array>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>UIBackgroundModes</key>
<array>
<string>bluetooth-central</string>
<string>bluetooth-peripheral</string>
<string>external-accessory</string>
</array>
<key>FacebookAdvertiserIDCollectionEnabled</key>
<true/>
<key>FacebookAppID</key>
<string>3829022037361855</string>
<key>FacebookAutoLogAppEventsEnabled</key>
<true/>
<key>FacebookClientToken</key>
<string>193d0a2b1757c41cfa557ea8e8f21c33</string>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>weixin</string>
@ -70,6 +72,24 @@
</array>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSAccessorySetupBluetoothCompanyIdentifiers</key>
<array>
<string>EFAB</string>
</array>
<key>NSAccessorySetupKitSupports</key>
<array>
<string>Bluetooth</string>
</array>
<key>NSAccessorySetupDescriptions</key>
<array>
<dict>
<key>AccessoryMediaType</key>
<string>Headphones</string>
<key>LocalizedAccessoryName</key>
<string>语音助理耳机</string>
</dict>
</array>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
@ -77,8 +97,29 @@
<key>NSAllowsArbitraryLoadsInWebContent</key>
<true/>
</dict>
<key>NSBluetoothAlwaysUsageDescription</key>
<string>需要蓝牙权限用于连接和管理音频设备</string>
<key>NSBluetoothPeripheralUsageDescription</key>
<string>需要蓝牙权限用于连接和管理音频设备</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>需要位置权限用于搜索附近的蓝牙设备</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>需要位置权限用于搜索附近的蓝牙设备</string>
<key>NSMicrophoneUsageDescription</key>
<string>需要麦克风权限用于语音识别和录音功能</string>
<key>NSCalendarsUsageDescription</key>
<string>需要日历权限用于安排和管理您的音频会议和录音任务</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>需要相册权限用于保存录音文件和导入音频文件</string>
<key>NSCameraUsageDescription</key>
<string>需要相机权限用于扫描二维码和视频通话功能</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UIBackgroundModes</key>
<array>
<string>bluetooth-central</string>
<string>audio</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
@ -96,27 +137,5 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>NSBluetoothAlwaysUsageDescription</key>
<string>需要蓝牙权限用于连接和管理音频设备</string>
<key>NSBluetoothPeripheralUsageDescription</key>
<string>需要蓝牙权限用于连接和管理音频设备</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>需要位置权限用于搜索附近的蓝牙设备</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>需要位置权限用于搜索附近的蓝牙设备</string>
<key>NSMicrophoneUsageDescription</key>
<string>需要麦克风权限用于语音识别和录音功能</string>
<key>FacebookClientToken</key>
<string>193d0a2b1757c41cfa557ea8e8f21c33</string>
<key>FacebookAppID</key>
<string>3829022037361855</string>
<key>FacebookAdvertiserIDCollectionEnabled</key>
<true/>
<key>FacebookAutoLogAppEventsEnabled</key>
<true/>
<key>NSAccessorySetupKitSupports</key>
<array>
<string>Bluetooth</string>
</array>
</dict>
</plist>

9
ios/Runner/Runner.entitlements

@ -1,5 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
<dict>
<key>com.apple.developer.networking.wifi-info</key>
<true/>
<key>com.apple.external-accessory.wireless-configuration</key>
<true/>
<key>com.apple.developer.homekit</key>
<true/>
</dict>
</plist>

4
lib/core/bindings/initial_binding.dart

@ -8,6 +8,7 @@ import '../../data/services/language_manager.dart';
import '../../core/utils/logger.dart';
import '../../data/services/bluetooth_manager.dart';
import '../../data/services/ble_manager.dart';
import 'package:agent_service/agent_service.dart';
/// 初始绑定,用于管理全局依赖
class InitialBinding extends Bindings {
@ -36,8 +37,7 @@ class InitialBinding extends Bindings {
Get.lazyPut<BleManager>(() => BleManager(), fenix: true);
// MusicManager
Get.lazyPut<MusicManager>(() => MusicManager(), fenix: true);
// NavigationManager
Get.lazyPut<NavigationManager>(() => NavigationManager(), fenix: true);
// 火山AI服务
// Get.lazyPut<VolcanoAIService>(() => VolcanoAIService(), fenix: true);

103
lib/data/services/ble_manager.dart

@ -3,13 +3,12 @@ import 'package:deep_voice/data/models/appconfig.dart';
import 'package:get/get.dart';
import 'package:ble_service/ble_service.dart';
import 'package:deep_voice/core/utils/logger.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:get_storage/get_storage.dart';
import 'package:deep_voice/data/models/appconfig_model.dart';
import 'dart:convert';
import 'package:flutter/services.dart' show rootBundle;
import 'package:agent_service/agent_service.dart';
import 'package:classic_bluetooth/classic_bluetooth.dart';
// 重新导出BLE连接状态常量,让外部可以通过BleManager访问
export 'package:ble_service/ble_service.dart' show BleConnectionState;
/// 命令类型常量(与原生层保持一致)
class BleCommands {
@ -112,6 +111,9 @@ class BleManager extends GetxService {
final _connectionState = BleConnectionState.disconnected.obs;
int get connectionState => _connectionState.value;
// 公开的连接状态流,供外部监听
RxInt get connectionStateRx => _connectionState;
// 连接状态监听
final _isConnected = false.obs;
bool get isConnected => _isConnected.value;
@ -151,13 +153,15 @@ class BleManager extends GetxService {
// 监听BLE状态更新
_statusSubscription = _bleService.onStatusUpdated.listen(
(event) {
Logger.info('接收到BLE状态更新: $event');
if (event is Map) {
// 处理连接状态更新
if (event['type'] == 'connectionStateChanged') {
final state = event['state'] as int;
final oldState = _connectionState.value;
_connectionState.value = state;
Logger.d('BleManager', '🔄 BLE状态更新: $oldState → $state');
Logger.d('BleManager', '📡 通知监听器状态变化: ${_connectionState.value}');
// 连接状态转换
if (state == BleConnectionState.connected) {
@ -188,15 +192,6 @@ class BleManager extends GetxService {
);
// 监听BLE数据流
_dataSubscription = _bleService.onDataReceived.listen(
(data) {
Logger.info('接收到BLE数据: $data');
// 在这里处理来自设备的数据
},
onError: (error) {
Logger.error('BLE数据流错误: $error');
},
);
} catch (e) {
Logger.error('设置BLE事件监听器失败: ${e.toString()}');
}
@ -659,6 +654,84 @@ class BleManager extends GetxService {
}
}
// ==================== 配对相关方法 ====================
/// 检查设备是否支持Companion Device Manager
Future<bool> isCompanionSupported() async {
try {
return await _bleService.isCompanionSupported();
} catch (e) {
Logger.error('检查Companion支持失败: ${e.toString()}');
return false;
}
}
/// 获取已配对的MAC地址
Future<String?> getPairedMacAddress() async {
try {
return await _bleService.getPairedMacAddress();
} catch (e) {
Logger.error('获取已配对MAC地址失败: ${e.toString()}');
return null;
}
}
/// 检查BLE连接状态
Future<bool> isBleConnected() async {
try {
return await _bleService.isBleConnected();
} catch (e) {
Logger.error('检查BLE连接状态失败: ${e.toString()}');
return false;
}
}
/// 验证设备是否可以配对
Future<bool> verifyDeviceCanPair(String macAddress) async {
try {
return true;
} catch (e) {
Logger.error('验证设备配对失败: ${e.toString()}');
return false;
}
}
/// 启动设备关联
Future<bool> startDeviceAssociation({
required String deviceNamePattern,
required String macAddress,
}) async {
try {
return await _bleService.startDeviceAssociation(
deviceNamePattern: deviceNamePattern,
macAddress: macAddress,
);
} catch (e) {
Logger.error('启动设备关联失败: ${e.toString()}');
return false;
}
}
/// 注册后台扫描
Future<bool> registerBackgroundScan({String? macAddress}) async {
try {
return await _bleService.registerBackgroundScan(macAddress: macAddress);
} catch (e) {
Logger.error('注册后台扫描失败: ${e.toString()}');
return false;
}
}
/// 注销后台扫描
Future<bool> unregisterBackgroundScan() async {
try {
return await _bleService.unregisterBackgroundScan();
} catch (e) {
Logger.error('注销后台扫描失败: ${e.toString()}');
return false;
}
}
/// 注册服务
static Future<BleManager> init() async {
if (!Get.isRegistered<BleManager>()) {

2
lib/data/services/bluetooth_manager.dart

@ -3,6 +3,8 @@ import 'package:get/get.dart';
import 'package:classic_bluetooth/classic_bluetooth.dart';
import 'package:deep_voice/core/utils/logger.dart';
// 重新导出蓝牙设备类型,让外部可以通过BluetoothManager访问
export 'package:classic_bluetooth/classic_bluetooth.dart' show BluetoothDevice;
/// 蓝牙服务 - 管理经典蓝牙服务
class BluetoothManager extends GetxService {

18
lib/data/services/speech_factory.dart

@ -3,8 +3,6 @@ import 'asr_service.dart';
import 'tts_service.dart';
import 'speech_impl/azure_asr_service.dart';
import 'speech_impl/azure_tts_service.dart';
import 'speech_impl/flutter_asr_service.dart';
import 'speech_impl/flutter_tts_service.dart';
import '../../core/utils/logger.dart';
@ -53,27 +51,17 @@ class SpeechFactory extends GetxService {
Get.delete<TtsService>(force: true);
}
// 根据类型注册新服务
switch (_currentServiceType) {
case SpeechServiceType.azure:
Get.lazyPut<AsrService>(() => AzureAsrService());
Get.lazyPut<TtsService>(() => AzureTtsService());
break;
case SpeechServiceType.flutter:
Get.lazyPut<AsrService>(() => FlutterAsrService());
Get.lazyPut<TtsService>(() => FlutterTtsService());
break;
}
}
/// 初始化语音服务
Future<void> initialize({SpeechServiceType? initialType}) async {
try {
// 如果指定了初始类型,则设置
if (initialType != null) {
_currentServiceType = initialType;
}
// 注册服务
_registerServices();

478
lib/data/services/speech_impl/flutter_asr_service.dart

@ -1,478 +0,0 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:get/get.dart';
import 'package:speech_to_text/speech_to_text.dart' as stt;
import 'package:speech_to_text/speech_recognition_result.dart';
import 'package:speech_to_text/speech_recognition_error.dart';
import '../../../core/utils/logger.dart';
import '../asr_service.dart';
/// ASR状态枚举
enum AsrState {
/// 未初始化
notInitialized,
/// 已初始化但未开始监听
initialized,
/// 正在监听
listening,
/// 正在处理识别结果
processing,
/// 识别完成
done,
/// 发生错误
error,
}
/// Flutter ASR服务,负责本地语音转文本功能
/// 使用speech_to_text插件实现语音识别功能
class FlutterAsrService extends GetxService implements AsrService {
static final FlutterAsrService to = Get.put(FlutterAsrService());
// speech_to_text 实例
final stt.SpeechToText _speech = stt.SpeechToText();
// 可用的语音识别语言
final List<String> _supportedLanguages = [];
@override
List<String> get supportedLanguages => _supportedLanguages;
// 当前选择的语言
final RxString _currentLocale = ''.obs;
String get currentLocale => _currentLocale.value;
// 是否可用
final RxBool _isAvailable = false.obs;
bool get isAvailable => _isAvailable.value;
// 是否正在监听
bool _isListening = false;
// 当前ASR状态
final Rx<AsrState> _asrState = AsrState.notInitialized.obs;
AsrState get asrState => _asrState.value;
// 连续识别模式的结果流控制器
StreamController<RecognitionEvent>? _continuousRecognitionController;
FlutterAsrService() {
_initSpeech();
}
/// 初始化语音识别
Future<void> _initSpeech() async {
try {
_asrState.value = AsrState.notInitialized;
// 初始化语音识别,使用较短的最终超时时间,提高响应速度
_isAvailable.value = await _speech.initialize(
onStatus: _onSpeechStatus,
onError: _onSpeechError,
debugLogging: false,
finalTimeout: const Duration(milliseconds: 800), // 更短的最终超时时间,加速结果返回
);
if (_isAvailable.value) {
// 获取支持的语言列表
final systemLocales = await _speech.locales();
// 清空并重新填充支持的语言列表
_supportedLanguages.clear();
for (var locale in systemLocales) {
if (!_supportedLanguages.contains(locale.localeId)) {
_supportedLanguages.add(locale.localeId);
}
}
// 设置默认语言(优先使用系统语言或中文)
var systemLocale = await _speech.systemLocale();
if (systemLocale != null) {
_currentLocale.value = systemLocale.localeId;
} else if (_supportedLanguages.contains('zh-CN')) {
_currentLocale.value = 'zh-CN';
} else if (_supportedLanguages.isNotEmpty) {
_currentLocale.value = _supportedLanguages.first;
}
_asrState.value = AsrState.initialized;
Logger.info('语音识别初始化成功,支持 ${_supportedLanguages.length} 种语言');
} else {
_asrState.value = AsrState.error;
Logger.error('语音识别不可用');
}
} catch (e) {
_isAvailable.value = false;
_asrState.value = AsrState.error;
Logger.error('语音识别初始化失败: ${e.toString()}');
}
}
/// 处理语音识别状态变化
void _onSpeechStatus(String status) {
Logger.info('语音识别状态: $status');
switch (status) {
case 'listening':
_asrState.value = AsrState.listening;
_isListening = true;
break;
case 'notListening':
if (_asrState.value == AsrState.listening) {
_asrState.value = AsrState.processing;
}
_isListening = false;
break;
case 'done':
_asrState.value = AsrState.done;
_isListening = false;
// 在连续识别模式下,当一次识别完成后立即开始下一次识别
if (_continuousRecognitionController != null &&
!_continuousRecognitionController!.isClosed) {
_restartContinuousRecognition();
}
break;
}
}
/// 处理语音识别错误
void _onSpeechError(SpeechRecognitionError error) {
Logger.error('语音识别错误: ${error.errorMsg} (永久性错误: ${error.permanent})');
_asrState.value = AsrState.error;
// 向连续识别控制器发送错误事件(如果存在)
_continuousRecognitionController
?.add(RecognitionEvent.error(error.errorMsg));
// 在连续识别模式下,尝试从错误中恢复(非永久性错误)
if (!error.permanent &&
_continuousRecognitionController != null &&
!_continuousRecognitionController!.isClosed) {
_restartContinuousRecognition();
}
}
/// 启动语音识别的统一方法
Future<bool> _startListening({
required Function(SpeechRecognitionResult) onResult,
Duration listenFor = const Duration(seconds: 30),
Duration pauseFor = const Duration(seconds: 3),
bool autoPunctuation = true,
}) async {
try {
// 如果当前正在监听,先停止
if (_speech.isListening) {
await _speech.stop();
// 延长等待时间,确保资源完全释放
await Future.delayed(const Duration(milliseconds: 800));
}
// 在启动前再次检查状态
if (_speech.isListening) {
Logger.warning('停止识别后,仍处于监听状态,可能有系统冲突');
await _speech.stop();
await Future.delayed(const Duration(milliseconds: 500));
}
// 启动语音识别前确保状态正确
_asrState.value = AsrState.listening;
// 启动语音识别
final result = await _speech.listen(
onResult: onResult,
localeId: _currentLocale.value,
listenFor: listenFor,
pauseFor: pauseFor,
listenOptions: stt.SpeechListenOptions(
partialResults: true,
autoPunctuation: autoPunctuation,
// 使用独白模式,减少与系统交互冲突
listenMode: stt.ListenMode.dictation,
),
) ??
false;
if (!result) {
Logger.error('语音识别启动失败');
_asrState.value = AsrState.error;
}
return result;
} catch (e) {
Logger.error('启动语音识别失败: ${e.toString()}');
_asrState.value = AsrState.error;
return false;
}
}
/// 重新启动连续识别
Future<void> _restartContinuousRecognition() async {
try {
// 确保控制器有效
if (_continuousRecognitionController == null ||
_continuousRecognitionController!.isClosed) {
Logger.info('连续识别控制器已关闭,跳过重启');
return;
}
// 检查系统状态,避免频繁重启
if (_speech.isListening) {
Logger.info('当前已在监听中,无需重启');
return;
}
// 延长等待时间,避免与系统资源冲突
await Future.delayed(const Duration(milliseconds: 1000));
// 再次检查状态
if (_continuousRecognitionController == null ||
_continuousRecognitionController!.isClosed) {
Logger.info('延迟期间控制器已关闭,跳过重启');
return;
}
Logger.info('尝试重新启动连续识别');
// 使用统一方法启动识别
final success = await _startListening(
onResult: _onSpeechResult,
// 增加超时时间和暂停时间
listenFor: const Duration(seconds: 30),
pauseFor: const Duration(seconds: 4),
);
if (success) {
Logger.info('连续识别重启成功');
} else {
Logger.error('连续识别重启失败');
_continuousRecognitionController
?.add(RecognitionEvent.error('重启语音识别失败,可能存在系统资源冲突'));
}
} catch (e) {
Logger.error('重启连续识别失败: ${e.toString()}');
// 发送错误给控制器
_continuousRecognitionController?.add(RecognitionEvent.error('重启失败: $e'));
}
}
/// 处理语音识别结果
void _onSpeechResult(SpeechRecognitionResult result) {
// 如果是连续识别,发送到连续识别控制器
if (_continuousRecognitionController != null) {
if (result.finalResult) {
Logger.info('收到最终识别结果: "${result.recognizedWords}"');
_continuousRecognitionController!.add(RecognitionEvent.finalResult(
text: result.recognizedWords,
detectedLanguage: currentLocale,
));
} else {
_continuousRecognitionController!.add(RecognitionEvent(
type: RecognitionEventType.intermediateResult,
text: result.recognizedWords,
detectedLanguage: currentLocale,
));
}
}
}
/// 设置当前语言
void setLocale(String localeId) {
if (_supportedLanguages.contains(localeId)) {
_currentLocale.value = localeId;
Logger.info('设置语音识别语言: $localeId');
} else {
Logger.error('不支持的语言: $localeId');
}
}
@override
Future<bool> initialize({
List<String>? supportedLanguages,
}) async {
try {
if (supportedLanguages != null && supportedLanguages.isNotEmpty) {
_supportedLanguages.clear();
_supportedLanguages.addAll(supportedLanguages);
}
// 初始化语音识别
await _initSpeech();
return _isAvailable.value;
} catch (e) {
Logger.error('语音识别初始化失败: ${e.toString()}');
return false;
}
}
@override
Future<RecognitionEvent> recognizeOnce() async {
try {
if (!_isAvailable.value) {
return RecognitionEvent.error('语音识别不可用');
}
if (_isListening) {
await stopContinuousRecognition();
}
// 创建Completer来等待最终结果
final completer = Completer<RecognitionEvent>();
// 开始监听
_asrState.value = AsrState.listening;
// 定义结果处理函数
void resultListener(SpeechRecognitionResult result) {
if (result.finalResult) {
// 只有在最终结果时才完成completer
completer.complete(RecognitionEvent.finalResult(
text: result.recognizedWords,
detectedLanguage: currentLocale,
));
}
}
// 使用统一方法启动识别,单次识别使用更短的时间参数
final success = await _startListening(
onResult: resultListener,
listenFor: const Duration(seconds: 15),
pauseFor: const Duration(seconds: 2),
);
if (!success) {
return RecognitionEvent.error('启动语音识别失败');
}
// 设置较短的超时时间
return await completer.future.timeout(
const Duration(seconds: 20), // 略长于listenFor以确保获取到结果
onTimeout: () {
_speech.stop();
return RecognitionEvent.error('语音识别超时');
},
);
} catch (e) {
Logger.error('一次性语音识别失败: ${e.toString()}');
return RecognitionEvent.error('$e');
}
}
@override
Future<Stream<RecognitionEvent>> startContinuousRecognition(
bool audioSourceType) async {
try {
if (!_isAvailable.value) {
final controller = StreamController<RecognitionEvent>.broadcast();
controller.add(RecognitionEvent.error('语音识别不可用'));
controller.close();
return controller.stream;
}
// 如果已经在监听,先停止
if (_isListening) {
await stopContinuousRecognition();
}
// 创建新的流控制器
_continuousRecognitionController =
StreamController<RecognitionEvent>.broadcast();
// 开始会话
_continuousRecognitionController!
.add(RecognitionEvent(type: RecognitionEventType.sessionStarted));
// 开始连续监听
_asrState.value = AsrState.listening;
// 使用统一方法启动识别,连续识别使用较长的参数
final success = await _startListening(
onResult: _onSpeechResult,
);
if (!success) {
// 添加错误事件,但保留流
_continuousRecognitionController!
.add(RecognitionEvent.error('启动连续语音识别失败'));
// 返回已有的流控制器
return _continuousRecognitionController!.stream;
}
_isListening = true;
return _continuousRecognitionController!.stream;
} catch (e) {
Logger.error('启动连续语音识别失败: ${e.toString()}');
// 如果已经创建了控制器,使用它
if (_continuousRecognitionController != null) {
_continuousRecognitionController!.add(RecognitionEvent.error('$e'));
return _continuousRecognitionController!.stream;
} else {
// 否则创建新的控制器
final controller = StreamController<RecognitionEvent>.broadcast();
controller.add(RecognitionEvent.error('$e'));
return controller.stream;
}
}
}
@override
Future<bool> stopContinuousRecognition() async {
try {
// 无论是否正在监听,都尝试停止
if (_speech.isListening) {
await _speech.stop();
}
_isListening = false;
// 发送会话结束事件
_continuousRecognitionController
?.add(RecognitionEvent(type: RecognitionEventType.sessionStopped));
// 关闭流控制器
await _continuousRecognitionController?.close();
_continuousRecognitionController = null;
_asrState.value = AsrState.done;
return true;
} catch (e) {
Logger.error('停止连续语音识别失败: ${e.toString()}');
// 强制重置状态
_isListening = false;
_asrState.value = AsrState.error;
return false;
}
}
@override
bool isContinuousRecognitionActive() {
return _isListening;
}
@override
Future<void> dispose() async {
try {
// 确保停止所有识别活动
if (_speech.isListening) {
await _speech.stop();
}
// 关闭控制器
await _continuousRecognitionController?.close();
_continuousRecognitionController = null;
_isListening = false;
_asrState.value = AsrState.notInitialized;
} catch (e) {
Logger.error('释放语音识别资源失败: ${e.toString()}');
}
}
}

484
lib/data/services/speech_impl/flutter_tts_service.dart

@ -1,484 +0,0 @@
import 'dart:async';
import 'dart:collection';
import 'package:flutter/foundation.dart';
import 'package:get/get.dart';
import 'package:flutter_tts/flutter_tts.dart';
import '../../../core/utils/logger.dart';
import '../tts_service.dart';
/// Flutter TTS服务,负责基本的文本转语音功能
///
/// 使用flutter_tts插件实现TtsService接口
class FlutterTtsService extends GetxService implements TtsService {
static final FlutterTtsService to = Get.put(FlutterTtsService());
// flutter_tts实例
late final FlutterTts _flutterTts;
// 支持的语音列表
final List<String> _supportedVoices = [];
List<String> get supportedVoices => _supportedVoices;
// 当前语音
final RxString _currentVoice = ''.obs;
@override
String get currentVoice => _currentVoice.value;
// 播放状态
final RxBool _isSpeaking = false.obs;
@override
bool get isSpeaking => _isSpeaking.value;
// 文本队列
final Queue<_SpeechItem> _textQueue = Queue<_SpeechItem>();
bool _isProcessingQueue = false;
// 事件流控制器
final StreamController<TtsEvent> _eventController = StreamController<TtsEvent>.broadcast();
@override
Stream<TtsEvent> get onEvent => _eventController.stream;
// 语言
String _language = 'zh-CN';
// 流式文本缓冲区
String _streamBuffer = '';
// 当前参数设置
double _volume = 1.0;
double _rate = 0.5;
double _pitch = 1.0;
FlutterTtsService() {
_flutterTts = FlutterTts();
_initTts();
}
/// 初始化TTS
Future<void> _initTts() async {
try {
// 设置完成回调
_flutterTts.setCompletionHandler(() {
_isSpeaking.value = false;
_eventController.add(TtsEvent(type: TtsEventType.completed));
});
// 设置开始回调
_flutterTts.setStartHandler(() {
_isSpeaking.value = true;
_eventController.add(TtsEvent(type: TtsEventType.started));
});
// 设置错误回调
_flutterTts.setErrorHandler((msg) {
_isSpeaking.value = false;
_eventController.add(TtsEvent.error(msg));
Logger.error('TTS错误: $msg');
});
// 设置取消回调
_flutterTts.setCancelHandler(() {
_isSpeaking.value = false;
_eventController.add(TtsEvent(type: TtsEventType.canceled));
});
// 设置默认语言
await _flutterTts.setLanguage(_language);
// 设置音量
await _flutterTts.setVolume(_volume);
// 设置语速
await _flutterTts.setSpeechRate(_rate);
// 设置音调
await _flutterTts.setPitch(_pitch);
Logger.info('FlutterTtsService 已初始化');
} catch (e) {
Logger.error('TTS初始化失败: ${e.toString()}');
}
}
@override
Future<bool> initialize({
List<String>? supportedLanguages,
}) async {
try {
// 获取可用的语音
await _loadVoices();
// 更新支持的语言列表(如果提供)
if (supportedLanguages != null && supportedLanguages.isNotEmpty) {
_language = supportedLanguages.first;
await _flutterTts.setLanguage(_language);
}
Logger.info('FlutterTtsService 初始化成功');
Logger.info('支持的语音: ${_supportedVoices.join(", ")}');
return true;
} catch (e) {
Logger.error('TTS初始化失败: ${e.toString()}');
return false;
}
}
/// 加载可用的语音
Future<void> _loadVoices() async {
try {
_supportedVoices.clear();
// 获取可用的语音
final voices = await _flutterTts.getVoices;
if (voices != null && voices is List) {
for (final voice in voices) {
if (voice is Map && voice.containsKey('name')) {
_supportedVoices.add(voice['name'].toString());
}
}
}
// 如果没有找到语音,添加默认语音
if (_supportedVoices.isEmpty) {
_supportedVoices.add('default');
}
// 设置默认语音
if (_currentVoice.value.isEmpty && _supportedVoices.isNotEmpty) {
_currentVoice.value = _supportedVoices.first;
}
} catch (e) {
Logger.error('加载TTS语音失败: ${e.toString()}');
// 添加默认语音
if (_supportedVoices.isEmpty) {
_supportedVoices.add('default');
_currentVoice.value = 'default';
}
}
}
@override
Future<bool> setVoice(String voiceName) async {
try {
// 检查语音是否存在
if (!_supportedVoices.contains(voiceName) && voiceName != 'default') {
Logger.warning('语音不存在: $voiceName,使用默认语音');
voiceName = _supportedVoices.isNotEmpty ? _supportedVoices.first : 'default';
}
// 设置语音
await _flutterTts.setVoice({"name": voiceName});
_currentVoice.value = voiceName;
Logger.info('设置TTS语音: $voiceName');
return true;
} catch (e) {
Logger.error('设置TTS语音失败: ${e.toString()}');
return false;
}
}
/// 设置语音参数
Future<bool> setSpeechParams({double? rate, double? pitch, double? volume}) async {
try {
if (volume != null) {
await _flutterTts.setVolume(volume);
_volume = volume;
}
if (rate != null) {
await _flutterTts.setSpeechRate(rate);
_rate = rate;
}
if (pitch != null) {
await _flutterTts.setPitch(pitch);
_pitch = pitch;
}
return true;
} catch (e) {
Logger.error('设置语音参数失败: ${e.toString()}');
// 发送错误事件
_eventController.add(TtsEvent.error('设置语音参数失败: $e'));
return false;
}
}
@override
Future<bool> speakOnce(String text) async {
if (text.isEmpty) return false;
try {
// 如果正在播放,先停止
if (_isSpeaking.value) {
await stop();
}
// 播放文本
await _flutterTts.speak(text);
return true;
} catch (e) {
// 发送错误事件
_eventController.add(TtsEvent.error(e.toString()));
Logger.error('TTS播放失败: ${e.toString()}');
return false;
}
}
@override
Future<bool> speakStream(String text) async {
if (text.isEmpty) return false;
try {
// 添加文本到缓冲区
_streamBuffer += text;
// 如果缓冲区为空,直接返回
if (_streamBuffer.isEmpty) return true;
// 使用正则表达式匹配句子,包括结束符号
// 匹配任意字符,直到遇到句子结束符号
final sentenceRegex = RegExp(
r'([^。.!!??;;:\n\r]+[。.!!??;;::\n\r]|[^。.!!??;;:\n\r]+(?:\.{3,}|…)|[^。.!!??;;:\n\r]+["」』])'
);
bool hasProcessed = false;
// 查找所有完整句子
final matches = sentenceRegex.allMatches(_streamBuffer);
final List<String> sentences = [];
int lastMatchEnd = 0;
for (final match in matches) {
// 提取完整句子(包含结束符号)
final sentence = match.group(1)?.trim();
if (sentence != null && sentence.isNotEmpty) {
sentences.add(sentence);
lastMatchEnd = match.end;
}
}
// 处理找到的句子
for (final sentence in sentences) {
_textQueue.add(_SpeechItem(
text: sentence,
rate: _rate,
pitch: _pitch,
volume: _volume,
));
hasProcessed = true;
}
// 更新缓冲区,只保留未完成的部分
if (lastMatchEnd > 0) {
_streamBuffer = _streamBuffer.substring(lastMatchEnd);
}
// 如果处理了文本并且队列未在处理中,开始处理队列
if (hasProcessed && !_isProcessingQueue) {
_processQueue();
}
return true;
} catch (e) {
Logger.error('处理流式文本失败: ${e.toString()}');
// 发送错误事件
_eventController.add(TtsEvent.error(e.toString()));
return false;
}
}
@override
Future<bool> flushStream() async {
if (_textQueue.isEmpty && _streamBuffer.isEmpty) return true;
try {
// 处理缓冲区中可能的完整句子
await speakStream('');
// 如果缓冲区仍有剩余文本,将其添加到播放队列
if (_streamBuffer.isNotEmpty && _streamBuffer.trim().isNotEmpty) {
_textQueue.add(_SpeechItem(
text: _streamBuffer,
rate: _rate,
pitch: _pitch,
volume: _volume,
));
// 清空缓冲区
_streamBuffer = '';
// 如果队列未在处理中,开始处理队列
if (!_isProcessingQueue) {
_processQueue();
}
}
Logger.info('等待TTS队列播放完成,剩余${_textQueue.length}条');
// 等待队列处理完成
while (_isProcessingQueue || _isSpeaking.value) {
await Future.delayed(const Duration(milliseconds: 100));
}
Logger.info('TTS队列播放完成');
return true;
} catch (e) {
Logger.error('等待TTS队列播放完成失败: ${e.toString()}');
// 发送错误事件
_eventController.add(TtsEvent.error(e.toString()));
return false;
}
}
/// 处理队列中的文本
Future<void> _processQueue() async {
if (_isProcessingQueue) return;
_isProcessingQueue = true;
try {
while (_textQueue.isNotEmpty) {
if (!_isSpeaking.value) {
final item = _textQueue.removeFirst();
// 如果参数有变化,更新参数
if (item.rate != _rate || item.pitch != _pitch || item.volume != _volume) {
await setSpeechParams(
rate: item.rate,
pitch: item.pitch,
volume: item.volume,
);
}
// 播放文本
await _flutterTts.speak(item.text);
// 等待开始播放
int attempts = 0;
while (!_isSpeaking.value && attempts < 10) {
await Future.delayed(const Duration(milliseconds: 50));
attempts++;
}
// 等待播放完成
while (_isSpeaking.value) {
await Future.delayed(const Duration(milliseconds: 100));
}
} else {
// 等待一小段时间再检查队列
await Future.delayed(const Duration(milliseconds: 100));
}
}
} catch (e) {
Logger.error('TTS处理队列失败: ${e.toString()}');
// 发送错误事件
_eventController.add(TtsEvent.error(e.toString()));
} finally {
_isProcessingQueue = false;
}
}
@override
Future<bool> stop() async {
try {
Logger.info('尝试停止TTS播放...');
// 清空队列
_textQueue.clear();
_streamBuffer = '';
// 如果正在播放,停止播放
if (_isSpeaking.value) {
// 先重置状态标志,避免处理队列循环继续等待
_isProcessingQueue = false;
// 执行停止
final result = await _flutterTts.stop();
Logger.info('TTS停止结果: $result');
// 短暂延迟等待状态更新
await Future.delayed(const Duration(milliseconds: 100));
// 强制更新状态
_isSpeaking.value = false;
// 发送取消事件
_eventController.add(TtsEvent(type: TtsEventType.canceled));
}
Logger.info('TTS停止播放完成');
return true;
} catch (e) {
Logger.error('TTS停止播放失败: ${e.toString()}');
// 即使出错也尝试强制重置状态
_isSpeaking.value = false;
_isProcessingQueue = false;
return false;
}
}
@override
Future<void> dispose() async {
try {
await stop();
_textQueue.clear();
// 关闭事件流控制器
await _eventController.close();
Logger.info('TTS资源已释放');
} catch (e) {
Logger.error('释放TTS资源失败: ${e.toString()}');
}
}
/// 处理AI返回的文本流,将其转换为语音
///
/// [textStream] 文本流
/// 返回一个Future,当流处理完成时完成
Future<bool> processAiTextStream(Stream<String> textStream) async {
try {
await for (final text in textStream) {
if (text.isEmpty) continue;
await speakStream(text);
}
// 处理完流后,刷新缓冲区中的剩余文本
await flushStream();
return true;
} catch (e) {
Logger.error('处理文本流时出错: ${e.toString()}');
return false;
}
}
}
/// 语音合成项目
class _SpeechItem {
final String text;
final double rate;
final double pitch;
final double volume;
_SpeechItem({
required this.text,
this.rate = 0.5,
this.pitch = 1.0,
this.volume = 1.0,
});
}

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

@ -234,19 +234,20 @@ class AgentController extends GetxController {
case AgentServiceEventType.recognitionStarted:
isListening.value = true;
isTextInputMode.value = false;
isTextInputMode.refresh();
// isTextInputMode.refresh();
// 只标记识别开始,不添加消息
Logger.i(TAG, '识别开始');
break;
case AgentServiceEventType.recognitionStopped:
isListening.value = false;
isTextInputMode.value = true;
isTextInputMode.refresh();
// isTextInputMode.refresh();
Logger.i(TAG, '识别停止');
break;
case AgentServiceEventType.recognizing:
final text = event.data['text'] ?? '';
currentText.value = text; // 保留当前文本,以便其他地方使用
if (text.isNotEmpty) {
// 查找是否有正在识别中的消息
final index = messages
@ -268,14 +269,13 @@ class AgentController extends GetxController {
isRecognizing: true,
));
}
messages.refresh();
// 使用reverse:true时通常不需要手动滚动
// messages.refresh();
}
break;
case AgentServiceEventType.recognitionResult:
final text = event.data['text'] ?? '';
Logger.i(TAG, '识别结果: $text');
// 查找是否有正在识别中的消息
final index =
messages.lastIndexWhere((msg) => msg.isRecognizing && msg.isUser);
@ -289,7 +289,7 @@ class AgentController extends GetxController {
isRecognizing: false,
timestamp: messages[index].timestamp,
);
messages.refresh();
// messages.refresh();
} else {
// 如果没有找到识别中的消息,添加新消息
messages
@ -303,7 +303,7 @@ class AgentController extends GetxController {
} else if (index >= 0) {
// 如果识别结果为空,移除临时的识别消息
messages.removeAt(index);
messages.refresh();
// messages.refresh();
}
break;
@ -316,6 +316,7 @@ class AgentController extends GetxController {
case AgentServiceEventType.ttsStopped:
case AgentServiceEventType.ttsCanceled:
isSpeaking.value = false;
Logger.i(TAG, 'TTS停止播放');
break;
@ -375,7 +376,7 @@ class AgentController extends GetxController {
messages.lastIndexWhere((msg) => msg.isRecognizing && msg.isUser);
if (index >= 0) {
messages.removeAt(index);
messages.refresh();
// messages.refresh();
}
Logger.e(TAG, '代理服务错误: ${event.data['message']}');
@ -391,7 +392,7 @@ class AgentController extends GetxController {
// 只有当消息为空或文本太短时才移除
if (messages[index].text.trim().length < 2) {
messages.removeAt(index);
messages.refresh();
// messages.refresh();
} else {
// 否则转换为正式消息
messages[index] = Message(
@ -399,7 +400,7 @@ class AgentController extends GetxController {
text: messages[index].text,
timestamp: messages[index].timestamp,
);
messages.refresh();
// messages.refresh();
// 发送此消息到AI处理
isProcessing.value = true;
@ -417,11 +418,26 @@ class AgentController extends GetxController {
case AgentServiceEventType.functionCallResult:
final responseId = event.data['responseId'] ?? '';
final result = event.data['result'] ?? '';
final result = event.data['result'];
final functionCall = event.data['function_call'] ?? '';
Logger.i(TAG, 'Flutter 结束调用 mcp: $result, $functionCall');
if (result.isNotEmpty) {
final parsedResult = jsonDecode(result);
if (result != null) {
// result 可能已经是 Map 类型,也可能是 String 类型
Map<String, dynamic> parsedResult;
if (result is String && result.isNotEmpty) {
try {
parsedResult = jsonDecode(result);
} catch (e) {
Logger.e(TAG, '解析result数据失败: $e');
break;
}
} else if (result is Map<String, dynamic>) {
parsedResult = result;
} else {
Logger.e(TAG, 'result数据类型不支持: ${result.runtimeType}');
break;
}
final meta = parsedResult["meta"];
Map<String, dynamic> metaResult = {};
@ -431,6 +447,8 @@ class AgentController extends GetxController {
} catch (e) {
Logger.e(TAG, '解析meta数据失败: $e');
}
} else if (meta != null && meta is Map<String, dynamic>) {
metaResult = meta;
}
if (metaResult.isNotEmpty) {
// 判断是否为新的回复或响应ID是否改变
@ -700,16 +718,16 @@ class AgentController extends GetxController {
// 检查麦克风权限
Future<bool> _checkMicrophonePermission() async {
try {
// iOS平台直接返回true,不处理权限问题
if (Platform.isIOS) {
return true;
}
// 创建待请求权限列表
List<Permission> permissionsToRequest = [];
if (Platform.isAndroid) {
// Android 需要考虑SDK版本不同的权限要求
permissionsToRequest.add(Permission.microphone);
} else if (Platform.isIOS) {
// iOS 只需要麦克风权限
permissionsToRequest.add(Permission.microphone);
}
// Android 需要考虑SDK版本不同的权限要求
permissionsToRequest.add(Permission.microphone);
// 批量请求所有需要的权限
final statuses = await permissionsToRequest.request();

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

@ -3,7 +3,6 @@ import 'dart:async';
import '../../../routes/app_routes.dart';
import '../../../core/utils/logger.dart';
import 'package:flutter/material.dart';
import 'package:classic_bluetooth/classic_bluetooth.dart' show BluetoothDevice;
import '../../../data/services/bluetooth_manager.dart';
import '../../../data/services/ble_manager.dart';

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

@ -226,8 +226,7 @@ class HomeView extends GetView<HomeController> {
// 设备信息
SizedBox(height: 12.h),
Obx(() {
print('连接状态: ${controller.connectionStatus}'); // 添加调试打印
print('错误信息: ${controller.connectionErrorMsg}'); // 添加调试打印
// 如果连接失败且有错误信息,显示错误提示
if (controller.connectionStatus == ConnectionStatus.failed &&
controller.connectionErrorMsg.isNotEmpty) {

9
lib/modules/opus_test/bindings/opus_test_binding.dart

@ -1,9 +0,0 @@
import 'package:get/get.dart';
import '../controllers/opus_test_controller.dart';
class OpusTestBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<OpusTestController>(() => OpusTestController());
}
}

404
lib/modules/opus_test/controllers/opus_test_controller.dart

@ -1,404 +0,0 @@
import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:path_provider/path_provider.dart';
import 'package:just_audio/just_audio.dart';
import 'package:jl_opus/jl_opus.dart';
import 'package:permission_handler/permission_handler.dart';
class OpusTestController extends GetxController {
// 选中的文件
final selectedFiles = <PlatformFile>[].obs;
// 是否携带协议头
final hasHeader = false.obs;
// 数据包长度
final packetLength = '40'.obs;
// 采样率选项
final sampleRateOptions = [8000, 16000, 24000, 32000, 44100, 48000];
final selectedSampleRate = 16000.obs;
// 通道数量
final isMono = true.obs;
// 播放状态
final isPlaying = false.obs;
final isDecoding = false.obs;
final statusMessage = ''.obs;
// 播放器
final player = AudioPlayer();
// 临时文件路径
String? tempPcmPath;
// 杰理OPUS解码器
late JlOpus jlOpus;
StreamSubscription? _eventSubscription;
@override
void onInit() {
super.onInit();
requestPermissions();
_initOpusDecoder();
}
@override
void onClose() {
player.dispose();
_eventSubscription?.cancel();
jlOpus.dispose();
super.onClose();
}
// 初始化OPUS解码器
Future<void> _initOpusDecoder() async {
jlOpus = JlOpus();
// 监听解码器事件
_eventSubscription = jlOpus.eventStream.listen((event) {
switch (event.event) {
case 'onStart':
statusMessage.value = '开始${event.type == "file" ? "文件" : "流"}解码...';
break;
case 'onComplete':
if (event.type == 'file') {
statusMessage.value = '解码完成: ${event.filePath}';
} else {
statusMessage.value = '流解码完成';
}
break;
case 'onError':
statusMessage.value = '解码错误: [${event.errorCode}] ${event.errorMessage}';
isDecoding.value = false;
break;
}
});
// 初始化OPUS解码器
final initialized = await jlOpus.initOpusDecoder();
if (!initialized) {
statusMessage.value = 'OPUS解码器初始化失败';
}
}
// 请求必要权限
Future<void> requestPermissions() async {
if (Platform.isAndroid) {
await Permission.storage.request();
}
}
// 选择文件
Future<void> pickFiles() async {
try {
final result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['opus'],
allowMultiple: false,
);
if (result != null && result.files.isNotEmpty) {
selectedFiles.clear();
selectedFiles.add(result.files.first);
statusMessage.value = '已选择文件:${result.files.first.name}';
}
} catch (e) {
statusMessage.value = '选择文件失败: $e';
}
}
// 切换通道模式
void toggleChannel(bool isSingleChannel) {
isMono.value = isSingleChannel;
}
// 设置采样率
void setSampleRate(int rate) {
selectedSampleRate.value = rate;
}
// 设置数据包长度
void setPacketLength(String length) {
packetLength.value = length;
}
// 切换是否携带协议头
void toggleHeader(bool value) {
hasHeader.value = value;
}
// 播放音频
Future<void> playAudio() async {
if (isPlaying.value) {
await player.stop();
isPlaying.value = false;
statusMessage.value = '已停止播放';
return;
}
if (selectedFiles.isEmpty) {
statusMessage.value = '请先选择Opus文件';
return;
}
try {
isDecoding.value = true;
statusMessage.value = '正在解码...';
// 使用杰理OPUS库进行解码
final inPath = selectedFiles.first.path!;
// 创建WAV输出文件路径
final tempDir = await getTemporaryDirectory();
final wavFileName = 'decoded_opus_${DateTime.now().millisecondsSinceEpoch}.wav';
final outPath = '${tempDir.path}/$wavFileName';
// 创建解码参数
final option = OpusOption(
hasHeader: hasHeader.value,
channel: isMono.value ? 1 : 2,
sampleRate: selectedSampleRate.value,
packetSize: int.tryParse(packetLength.value) ?? 40,
);
// 解码文件
final pcmPath = await jlOpus.decodeOpusFile(inPath, outPath, option);
if (pcmPath == null) {
statusMessage.value = '解码失败';
isDecoding.value = false;
return;
}
tempPcmPath = pcmPath;
// 将PCM文件转换为WAV文件
final wavFile = await _convertPcmToWav(
pcmPath,
'${tempDir.path}/$wavFileName',
selectedSampleRate.value,
isMono.value ? 1 : 2
);
statusMessage.value = '解码完成,准备播放';
// 播放WAV文件
await player.setFilePath(wavFile.path);
await player.play();
isPlaying.value = true;
statusMessage.value = '正在播放解码后的音频';
// 监听播放完成
player.playerStateStream.listen((state) {
if (state.processingState == ProcessingState.completed) {
isPlaying.value = false;
statusMessage.value = '播放完成';
}
});
} catch (e) {
statusMessage.value = '解码或播放失败: $e';
} finally {
isDecoding.value = false;
}
}
// 将PCM转换为WAV格式
Future<File> _convertPcmToWav(String pcmPath, String wavPath, int sampleRate, int channels) async {
final pcmFile = File(pcmPath);
final pcmData = await pcmFile.readAsBytes();
// 创建WAV文件 (44字节头部 + PCM数据)
final wavSize = 44 + pcmData.length;
final wavData = ByteData(wavSize);
// 写入WAV头部
_writeWavHeader(wavData, pcmData.length, sampleRate, channels);
// 写入PCM数据
for (int i = 0; i < pcmData.length; i++) {
wavData.setUint8(44 + i, pcmData[i]);
}
// 保存为WAV文件
final wavFile = File(wavPath);
await wavFile.writeAsBytes(wavData.buffer.asUint8List());
return wavFile;
}
// 写入WAV头部到ByteData
void _writeWavHeader(ByteData data, int pcmLength, int sampleRate, int channels) {
final bytesPerSample = 2; // 16位PCM
final byteRate = sampleRate * channels * bytesPerSample;
final blockAlign = channels * bytesPerSample;
// "RIFF"标识
data.setUint8(0, 0x52); // 'R'
data.setUint8(1, 0x49); // 'I'
data.setUint8(2, 0x46); // 'F'
data.setUint8(3, 0x46); // 'F'
// 文件大小 (减去8个字节的RIFF头部和大小字段)
data.setUint32(4, 36 + pcmLength, Endian.little);
// "WAVE"标识
data.setUint8(8, 0x57); // 'W'
data.setUint8(9, 0x41); // 'A'
data.setUint8(10, 0x56); // 'V'
data.setUint8(11, 0x45); // 'E'
// "fmt "子块
data.setUint8(12, 0x66); // 'f'
data.setUint8(13, 0x6D); // 'm'
data.setUint8(14, 0x74); // 't'
data.setUint8(15, 0x20); // ' '
// 子块大小 (16 for PCM)
data.setUint32(16, 16, Endian.little);
// 音频格式 (1 表示PCM)
data.setUint16(20, 1, Endian.little);
// 通道数量
data.setUint16(22, channels, Endian.little);
// 采样率
data.setUint32(24, sampleRate, Endian.little);
// 字节率 = 采样率 * 通道数 * 每样本字节数
data.setUint32(28, byteRate, Endian.little);
// 块对齐 = 通道数 * 每样本字节数
data.setUint16(32, blockAlign, Endian.little);
// 每个样本位数
data.setUint16(34, bytesPerSample * 8, Endian.little);
// "data"子块
data.setUint8(36, 0x64); // 'd'
data.setUint8(37, 0x61); // 'a'
data.setUint8(38, 0x74); // 't'
data.setUint8(39, 0x61); // 'a'
// 数据大小
data.setUint32(40, pcmLength, Endian.little);
}
}
// PCM音频源
class PCMAudioSource extends StreamAudioSource {
final Uint8List pcmData;
final int sampleRate;
final int channels;
PCMAudioSource({
required this.pcmData,
this.sampleRate = 16000,
this.channels = 1,
});
@override
Future<StreamAudioResponse> request([int? start, int? end]) async {
start = start ?? 0;
end = end ?? pcmData.length;
// 转换PCM到WAV格式
final wavHeader = _createWavHeader(pcmData.length, sampleRate, channels);
final wavData = Uint8List(wavHeader.length + pcmData.length);
wavData.setRange(0, wavHeader.length, wavHeader);
wavData.setRange(wavHeader.length, wavHeader.length + pcmData.length, pcmData);
// 调整请求范围来适应WAV头部
int adjustedStart = start;
int adjustedEnd = end;
if (start < wavHeader.length) {
adjustedStart = start;
} else {
adjustedStart = start + wavHeader.length;
}
if (end <= wavHeader.length) {
adjustedEnd = end;
} else {
adjustedEnd = end + wavHeader.length;
adjustedEnd = adjustedEnd > wavData.length ? wavData.length : adjustedEnd;
}
return StreamAudioResponse(
sourceLength: wavData.length,
contentLength: adjustedEnd - adjustedStart,
offset: adjustedStart,
stream: Stream.value(wavData.sublist(adjustedStart, adjustedEnd)),
contentType: 'audio/wav',
);
}
// 创建WAV头部
Uint8List _createWavHeader(int dataLength, int sampleRate, int channels) {
final bytesPerSample = 2; // 16位PCM
final byteRate = sampleRate * channels * bytesPerSample;
final blockAlign = channels * bytesPerSample;
final header = ByteData(44); // WAV头部固定44字节
// "RIFF"标识
header.setUint8(0, 0x52); // 'R'
header.setUint8(1, 0x49); // 'I'
header.setUint8(2, 0x46); // 'F'
header.setUint8(3, 0x46); // 'F'
// 文件大小 (减去8个字节的RIFF头部和大小字段)
header.setUint32(4, 36 + dataLength, Endian.little);
// "WAVE"标识
header.setUint8(8, 0x57); // 'W'
header.setUint8(9, 0x41); // 'A'
header.setUint8(10, 0x56); // 'V'
header.setUint8(11, 0x45); // 'E'
// "fmt "子块
header.setUint8(12, 0x66); // 'f'
header.setUint8(13, 0x6D); // 'm'
header.setUint8(14, 0x74); // 't'
header.setUint8(15, 0x20); // ' '
// 子块大小 (16 for PCM)
header.setUint32(16, 16, Endian.little);
// 音频格式 (1 表示PCM)
header.setUint16(20, 1, Endian.little);
// 通道数量
header.setUint16(22, channels, Endian.little);
// 采样率
header.setUint32(24, sampleRate, Endian.little);
// 字节率 = 采样率 * 通道数 * 每样本字节数
header.setUint32(28, byteRate, Endian.little);
// 块对齐 = 通道数 * 每样本字节数
header.setUint16(32, blockAlign, Endian.little);
// 每个样本位数
header.setUint16(34, bytesPerSample * 8, Endian.little);
// "data"子块
header.setUint8(36, 0x64); // 'd'
header.setUint8(37, 0x61); // 'a'
header.setUint8(38, 0x74); // 't'
header.setUint8(39, 0x61); // 'a'
// 数据大小
header.setUint32(40, dataLength, Endian.little);
return header.buffer.asUint8List();
}
}

445
lib/modules/opus_test/views/opus_test_view.dart

@ -1,445 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import '../controllers/opus_test_controller.dart';
class OpusTestView extends GetView<OpusTestController> {
const OpusTestView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
// 获取主题模式
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return DefaultTabController(
length: 2,
child: Scaffold(
backgroundColor: isDarkMode ? Colors.black : Colors.grey[50],
appBar: AppBar(
backgroundColor: isDarkMode ? Colors.black : Colors.white,
elevation: 0.5,
title: Text(
'OPUS解码',
style: TextStyle(
fontSize: 17.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.black,
),
),
centerTitle: true,
bottom: TabBar(
indicatorColor: Colors.blue,
indicatorWeight: 3,
labelColor: isDarkMode ? Colors.white : Colors.black,
unselectedLabelColor: isDarkMode ? Colors.grey : Colors.grey,
tabs: const [
Tab(text: "解码"),
Tab(text: "输出"),
],
),
),
body: TabBarView(
children: [
_buildDecodeTab(isDarkMode),
_buildEncodeTab(isDarkMode),
],
),
),
);
}
// 解码标签页
Widget _buildDecodeTab(bool isDarkMode) {
return ListView(
padding: EdgeInsets.all(16.w),
children: [
// 文件选择部分
_buildSection(
title: '选择文件',
isDarkMode: isDarkMode,
child: Column(
children: [
Obx(() => controller.selectedFiles.isEmpty
? const SizedBox.shrink()
: ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: controller.selectedFiles.length,
itemBuilder: (context, index) {
final file = controller.selectedFiles[index];
return ListTile(
title: Text(
file.name,
style: TextStyle(
color: isDarkMode ? Colors.white70 : Colors.black87,
fontSize: 14.sp,
),
),
subtitle: Text(
'${(file.size / 1024).toStringAsFixed(2)} Kb',
style: TextStyle(
color: isDarkMode ? Colors.white38 : Colors.black54,
fontSize: 12.sp,
),
),
trailing: Radio<int>(
value: index,
groupValue: 0,
onChanged: (value) {},
activeColor: Colors.blue,
),
);
},
)),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
IconButton(
icon: Icon(
Icons.refresh,
color: isDarkMode ? Colors.blue[300] : Colors.blue,
),
onPressed: () {
controller.selectedFiles.clear();
},
),
],
),
],
),
),
SizedBox(height: 16.h),
// 解码参数部分
_buildSection(
title: '解码参数',
isDarkMode: isDarkMode,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Obx(() => Checkbox(
value: controller.hasHeader.value,
onChanged: (value) => controller.toggleHeader(value!),
activeColor: Colors.blue,
)),
Text(
'是否携带协议头',
style: TextStyle(
color: isDarkMode ? Colors.white70 : Colors.black87,
fontSize: 14.sp,
),
),
],
),
// 数据包长度
Row(
children: [
Text(
'数据包长度',
style: TextStyle(
color: isDarkMode ? Colors.white70 : Colors.black87,
fontSize: 14.sp,
),
),
SizedBox(width: 16.w),
Expanded(
child: Obx(() => TextField(
controller: TextEditingController(
text: controller.packetLength.value),
onChanged: (value) => controller.setPacketLength(value),
keyboardType: TextInputType.number,
style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black,
),
decoration: InputDecoration(
isDense: true,
suffix: Text(
'Byte',
style: TextStyle(
color: isDarkMode
? Colors.white60
: Colors.black54,
fontSize: 12.sp,
),
),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: isDarkMode
? Colors.white30
: Colors.black26,
),
),
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Colors.blue,
),
),
),
)),
),
],
),
SizedBox(height: 20.h),
// 采样率
Row(
children: [
Text(
'采样率',
style: TextStyle(
color: isDarkMode ? Colors.white70 : Colors.black87,
fontSize: 14.sp,
),
),
SizedBox(width: 16.w),
Expanded(
child: Obx(() => DropdownButton<int>(
value: controller.selectedSampleRate.value,
isExpanded: true,
dropdownColor:
isDarkMode ? Colors.grey[800] : Colors.white,
items: controller.sampleRateOptions.map((rate) {
return DropdownMenuItem<int>(
value: rate,
child: Text(
rate.toString(),
style: TextStyle(
color: isDarkMode
? Colors.white
: Colors.black87,
),
),
);
}).toList(),
onChanged: (value) =>
controller.setSampleRate(value!),
style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black,
),
)),
),
],
),
SizedBox(height: 20.h),
// 通道数量
Text(
'通道数量',
style: TextStyle(
color: isDarkMode ? Colors.white70 : Colors.black87,
fontSize: 14.sp,
),
),
Row(
children: [
Expanded(
child: Row(
children: [
Obx(() => Radio<bool>(
value: true,
groupValue: controller.isMono.value,
onChanged: (value) =>
controller.toggleChannel(value!),
activeColor: Colors.blue,
)),
Text(
'单通道',
style: TextStyle(
color: isDarkMode ? Colors.white70 : Colors.black87,
fontSize: 14.sp,
),
),
],
),
),
Expanded(
child: Row(
children: [
Obx(() => Radio<bool>(
value: false,
groupValue: controller.isMono.value,
onChanged: (value) =>
controller.toggleChannel(value!),
activeColor: Colors.blue,
)),
Text(
'双通道',
style: TextStyle(
color: isDarkMode ? Colors.white70 : Colors.black87,
fontSize: 14.sp,
),
),
],
),
),
],
),
],
),
),
SizedBox(height: 16.h),
// 状态消息
Obx(() => controller.statusMessage.value.isNotEmpty
? Container(
padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 16.w),
margin: EdgeInsets.only(bottom: 16.h),
decoration: BoxDecoration(
color: isDarkMode
? Colors.blue.withOpacity(0.2)
: Colors.blue.withOpacity(0.1),
borderRadius: BorderRadius.circular(4.r),
border: Border.all(
color: isDarkMode
? Colors.blue.withOpacity(0.3)
: Colors.blue.withOpacity(0.2),
width: 1,
),
),
child: Text(
controller.statusMessage.value,
style: TextStyle(
color: isDarkMode ? Colors.blue[300] : Colors.blue[700],
fontSize: 12.sp,
),
),
)
: const SizedBox.shrink()),
// 播放按钮
Container(
margin: EdgeInsets.symmetric(vertical: 8.h),
child: ElevatedButton.icon(
icon: Obx(() => Icon(
controller.isPlaying.value
? Icons.stop
: Icons.play_arrow,
color: Colors.white,
)),
label: Obx(() => Text(
controller.isPlaying.value ? '停止播放' : '播放音频',
style: TextStyle(
color: Colors.white,
fontSize: 16.sp,
),
)),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
padding: EdgeInsets.symmetric(vertical: 16.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
),
),
onPressed: controller.isDecoding.value
? null
: () {
controller.playAudio();
},
),
),
// 解码按钮
Container(
margin: EdgeInsets.symmetric(vertical: 8.h),
child: Obx(() => ElevatedButton(
onPressed: controller.selectedFiles.isEmpty ||
controller.isDecoding.value
? null
: () {
controller.pickFiles();
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
padding: EdgeInsets.symmetric(vertical: 16.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
),
),
child: Text(
'开始解码',
style: TextStyle(
color: Colors.white,
fontSize: 16.sp,
),
),
)),
),
],
);
}
// 编码标签页(暂未实现)
Widget _buildEncodeTab(bool isDarkMode) {
return Center(
child: Text(
'编码功能尚未实现',
style: TextStyle(
color: isDarkMode ? Colors.white54 : Colors.black54,
fontSize: 16.sp,
),
),
);
}
// 构建带标题的区段
Widget _buildSection({
required String title,
required Widget child,
required bool isDarkMode,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
title,
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.bold,
color: isDarkMode ? Colors.white : Colors.black87,
),
),
SizedBox(width: 8.w),
Expanded(
child: Container(
height: 1,
color: isDarkMode ? Colors.white24 : Colors.black12,
),
),
if (title == '选择文件')
IconButton(
icon: Icon(
Icons.add_circle_outline,
color: isDarkMode ? Colors.blue[300] : Colors.blue,
),
onPressed: () {
controller.pickFiles();
},
),
],
),
Container(
padding: EdgeInsets.all(12.w),
decoration: BoxDecoration(
color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.white,
borderRadius: BorderRadius.circular(8.r),
boxShadow: [
BoxShadow(
color: isDarkMode
? Colors.black.withOpacity(0.2)
: Colors.black.withOpacity(0.03),
blurRadius: 4,
offset: Offset(0, 2),
),
],
),
child: child,
),
],
);
}
}

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

File diff suppressed because it is too large

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

@ -49,8 +49,8 @@ class PairingView extends GetView<PairingController> {
SizedBox(height: 20.h),
// 设备动画 - 更大尺寸
_buildDeviceAnimation(),
// 设备动画 - 根据状态显示不同动画
_buildDeviceAnimation(isDarkMode),
SizedBox(height: 20.h),
@ -94,85 +94,124 @@ class PairingView extends GetView<PairingController> {
);
}
// 设备动画
Widget _buildDeviceAnimation() {
return Container(
width: 240.w,
height: 240.w,
child: Stack(
alignment: Alignment.center,
children: [
// 脉搏背景 - 更显眼的渐变效果
TweenAnimationBuilder<double>(
tween: Tween<double>(begin: 0.8, end: 1.1),
duration: const Duration(seconds: 2),
curve: Curves.easeInOut,
builder: (context, value, child) {
return Container(
width: 200.w * value,
height: 200.w * value,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Color(0xFF0066CC).withOpacity(0.3),
Color(0xFF5E5CE6).withOpacity(0.3),
],
),
shape: BoxShape.circle,
),
);
},
onEnd: () {
// 重新开始动画
},
),
// 设备动画 - 根据状态显示不同的动画效果
Widget _buildDeviceAnimation(bool isDarkMode) {
return Obx(() {
final state = controller.state;
final isLoading = controller.isLoading.value;
// 根据状态决定动画颜色
Color primaryColor = Color(0xFF0066CC);
Color secondaryColor = Color(0xFF5E5CE6);
// 错误状态使用红色
if (state.name.startsWith('exit')) {
primaryColor = Colors.red;
secondaryColor = Colors.redAccent;
}
// 成功状态使用绿色
else if (state == PairingState.paired) {
primaryColor = Colors.green;
secondaryColor = Colors.greenAccent;
}
return Container(
width: 240.w,
height: 240.w,
child: Stack(
alignment: Alignment.center,
children: [
// 脉搏背景 - 只在加载时显示动画
if (isLoading)
TweenAnimationBuilder<double>(
tween: Tween<double>(begin: 0.8, end: 1.1),
duration: const Duration(seconds: 2),
curve: Curves.easeInOut,
builder: (context, value, child) {
return Container(
width: 200.w * value,
height: 200.w * value,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
primaryColor.withOpacity(0.3),
secondaryColor.withOpacity(0.3),
],
),
shape: BoxShape.circle,
),
);
},
onEnd: () {
// 重新开始动画
},
),
// 旋转边框
SizedBox(
width: 100.w,
height: 100.w,
child: CircularProgressIndicator(
strokeWidth: 4.w,
valueColor: AlwaysStoppedAnimation<Color>(
Color(0xFF0066CC),
// 旋转边框 - 只在加载时显示
if (isLoading)
SizedBox(
width: 100.w,
height: 100.w,
child: CircularProgressIndicator(
strokeWidth: 4.w,
valueColor: AlwaysStoppedAnimation<Color>(primaryColor),
backgroundColor: Colors.grey.withOpacity(0.2),
),
),
backgroundColor: Colors.grey.withOpacity(0.2),
),
),
// 设备图标
Container(
width: 120.w,
height: 120.w,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Color(0xFF0066CC),
Color(0xFF5E5CE6),
],
// 设备图标
Container(
width: 120.w,
height: 120.w,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [primaryColor, secondaryColor],
),
shape: BoxShape.circle,
),
child: Icon(
_getIconForState(state),
color: Colors.white,
size: 60.w,
),
shape: BoxShape.circle,
),
child: Icon(
Icons.headphones,
color: Colors.white,
size: 60.w,
),
),
],
),
);
],
),
);
});
}
// 根据状态获取图标
IconData _getIconForState(PairingState state) {
switch (state) {
case PairingState.paired:
return Icons.check_circle;
case PairingState.exitNoPermission:
case PairingState.exitBtOff:
return Icons.bluetooth_disabled;
case PairingState.exitNoClassic:
case PairingState.exitNotMatch:
return Icons.headphones_outlined;
case PairingState.exitPairFailed:
case PairingState.exitTimeout:
case PairingState.exitIllegal:
case PairingState.exitBound:
return Icons.error_outline;
default:
return Icons.headphones;
}
}
// 状态信息
Widget _buildStatusInfo(bool isDarkMode) {
return Obx(() {
final status = controller.statusDescription.value;
final isLoading = controller.isLoading.value;
final state = controller.state;
final device = controller.currentDevice;
return Column(
children: [
@ -189,9 +228,9 @@ class PairingView extends GetView<PairingController> {
SizedBox(height: 10.h),
// 子状态提示文本
// 子状态提示文本 - 根据状态显示不同提示
Text(
'turnEarphones'.tr, // 请打开耳机充电盒,将耳机放入您的耳朵
_getSubtitleForState(state),
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.white60 : Colors.grey[600],
@ -200,43 +239,73 @@ class PairingView extends GetView<PairingController> {
),
// 当前连接设备(有设备时才显示)
if (controller.currentDevice != null) ...[
if (device != null &&
(state == PairingState.classicOk ||
state == PairingState.serverVerify ||
state == PairingState.pairing ||
state == PairingState.paired)) ...[
SizedBox(height: 24.h),
_buildConnectedDevice(isDarkMode),
_buildConnectedDevice(device, isDarkMode),
],
],
);
});
}
// 已连接设备信息
Widget _buildConnectedDevice(bool isDarkMode) {
return Obx(() {
final device = controller.currentDevice;
if (device == null) {
return const SizedBox.shrink();
}
// 根据状态获取副标题
String _getSubtitleForState(PairingState state) {
switch (state) {
case PairingState.init:
case PairingState.btReady:
return '正在准备配对环境...';
case PairingState.classicOk:
return '请先通过系统蓝牙连接DeapSound设备';
case PairingState.serverVerify:
case PairingState.pairing:
return '请稍候,正在完成配对...';
case PairingState.paired:
return '配对成功!即将跳转到主页...';
case PairingState.exitNoPermission:
return '需要蓝牙权限才能继续';
case PairingState.exitBtOff:
return '请开启蓝牙后重试';
case PairingState.exitNoClassic:
return '请先通过系统蓝牙连接DeapSound设备';
case PairingState.exitNotMatch:
return '请确保连接的是DeapSound设备';
case PairingState.exitTimeout:
return '扫描超时,请确保设备在附近';
case PairingState.exitPairFailed:
return '配对失败,请重试';
case PairingState.exitBound:
return '该设备已被其他账号绑定';
case PairingState.exitIllegal:
return '设备验证失败,请检查设备是否正常';
default:
return '';
}
}
return Container(
width: double.infinity,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'currentlyConnected'.tr, // 当前已连接:
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white70 : Colors.black87,
),
// 已连接设备信息
Widget _buildConnectedDevice(device, bool isDarkMode) {
return Container(
width: double.infinity,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'currentlyConnected'.tr, // 当前已连接:
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white70 : Colors.black87,
),
SizedBox(height: 8.h),
_buildDeviceItem(device, isDarkMode),
],
),
);
});
),
SizedBox(height: 8.h),
_buildDeviceItem(device, isDarkMode),
],
),
);
}
// 设备项
@ -291,52 +360,75 @@ class PairingView extends GetView<PairingController> {
// 操作按钮
Widget _buildActionButtons(bool isDarkMode) {
return Container(
width: double.infinity,
child: Column(
children: [
// 主按钮 - 改为更宽的圆角按钮
ElevatedButton(
onPressed: () => controller.openBluetoothSettings(),
style: ElevatedButton.styleFrom(
backgroundColor: isDarkMode ? Colors.grey[800] : Colors.grey[200],
foregroundColor: isDarkMode ? Colors.white : Colors.black87,
padding: EdgeInsets.symmetric(vertical: 16.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.r),
),
elevation: 0,
minimumSize: Size(double.infinity, 56.h),
),
child: Text(
'unsystemSetup'.tr, // 去系统设置
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
return Obx(() {
final showActionButton = controller.showActionButton;
final canSkip = controller.canSkip;
final actionButtonText = controller.actionButtonText.value;
return Container(
width: double.infinity,
child: Column(
children: [
// 主按钮 - 根据状态显示不同的按钮
if (showActionButton)
ElevatedButton(
onPressed: () => controller.handleActionButtonTap(),
style: ElevatedButton.styleFrom(
backgroundColor: _getButtonColor(controller.state, isDarkMode),
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(vertical: 16.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.r),
),
elevation: 0,
minimumSize: Size(double.infinity, 56.h),
),
child: Text(
actionButtonText,
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
),
),
),
),
),
SizedBox(height: 12.h),
// 次要按钮
TextButton(
onPressed: () => controller.skipConnecting(),
style: TextButton.styleFrom(
foregroundColor: Color(0xFF0066CC),
padding: EdgeInsets.symmetric(vertical: 12.h),
minimumSize: Size(double.infinity, 48.h),
),
child: Text(
'talkLater'.tr, // 稍后再说
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w500,
// 只在可以跳过时显示跳过按钮
if (canSkip) ...[
SizedBox(height: 12.h),
TextButton(
onPressed: () => controller.skipPairing(),
style: TextButton.styleFrom(
foregroundColor: Color(0xFF0066CC),
padding: EdgeInsets.symmetric(vertical: 12.h),
minimumSize: Size(double.infinity, 48.h),
),
child: Text(
'talkLater'.tr, // 稍后再说
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w500,
),
),
),
),
),
],
),
);
],
],
),
);
});
}
// 根据状态获取按钮颜色
Color _getButtonColor(PairingState state, bool isDarkMode) {
switch (state) {
case PairingState.exitNoPermission:
case PairingState.exitBtOff:
return isDarkMode ? Colors.grey[800]! : Colors.grey[600]!;
case PairingState.exitPairFailed:
case PairingState.exitBound:
case PairingState.exitIllegal:
return Colors.red;
default:
return Color(0xFF0066CC);
}
}
}

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

@ -504,29 +504,7 @@ class SettingsView extends GetView<SettingsController> {
? Colors.white.withOpacity(0.1)
: Colors.grey[200]),
// OPUS测试
_buildSimpleNavigationSetting(
title: 'OPUS解码测试',
subtitle: '测试OPUS音频文件的解码和播放',
icon: Icons.audio_file,
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: () {
Get.toNamed(Routes.opusTest);
},
isDarkMode: isDarkMode,
),
Divider(
height: 1,
color: isDarkMode
? Colors.white.withOpacity(0.1)
: Colors.grey[200]),
// BLE测试
_buildSimpleNavigationSetting(
title: 'BLE蓝牙测试',

10
lib/routes/app_pages.dart

@ -31,8 +31,6 @@ import './app_routes.dart';
import '../modules/settings/views/settings_view.dart';
import '../modules/settings/views/permissions_view.dart';
import '../modules/settings/bindings/settings_binding.dart';
import '../modules/opus_test/views/opus_test_view.dart';
import '../modules/opus_test/bindings/opus_test_binding.dart';
import '../modules/speech_test/views/speech_test_view.dart';
import '../modules/speech_test/bindings/speech_test_binding.dart';
import '../modules/agent/views/agent_view.dart';
@ -162,13 +160,7 @@ abstract class AppPages {
transition: Transition.rightToLeft,
transitionDuration: Duration(milliseconds: 250),
),
GetPage(
name: Routes.opusTest,
page: () => const OpusTestView(),
binding: OpusTestBinding(),
transition: Transition.rightToLeft,
transitionDuration: Duration(milliseconds: 250),
),
GetPage(
name: Routes.speechTest,
page: () => const SpeechTestView(),

0
local_plugins/azure_speech/ios/Classes/.gitkeep → local_plugins/agent_service/LICENSE

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

@ -93,63 +93,19 @@ object AgentService : CoroutineScope {
*/
private fun initSystemPrompt() {
systemPrompt = """
你是小言,一个具备专业能力的智能助手,需根据用户场景灵活切换回答模式,确保服务精准高效。以下是不同场景的具体要求:
你是小言,一个亲切而温暖的全能个人助理和伴侣,拥有智能、细致而贴心的服务能力。你的任务不仅是高效处理用户的日常事务,更要陪伴用户,提供情感支持和温暖陪伴。你的能力包括:
1. 日程与任务管理:温柔提醒用户的重要日程、待办事项,让用户感到安心和舒适。
2. 日常信息与建议:亲切、迅速地提供天气、新闻、生活小贴士等实用信息。
3. 个人成长陪伴:鼓励并陪伴用户实现个人目标,提出富有同理心的行动建议。
4. 财务温馨提示:提供贴心的理财建议,帮助用户轻松管理预算和财务。
5. 健康贴心关怀:给出友善的饮食、运动、睡眠和心理健康建议,陪伴用户健康生活。
6. 沟通支持与倾听:协助用户撰写和优化沟通内容,并耐心倾听用户的心情与困扰。
7. 日常技术帮助:轻松解决技术问题,耐心地指导用户掌握新技能。
8. 创意激发与鼓励:温柔启发用户的创造力,为用户提供鼓励和灵感。
9. 旅行与活动贴心安排:细心规划用户的出行和活动,注重每个细节,确保舒适与快乐。
10. 学习与温暖鼓励:耐心推荐学习资源,鼓励用户持续进步,陪伴用户共同成长。
一、知识类问题回答规范
核心目标:用简洁语言提供准确信息,兼顾易读性与专业性。
1. 内容细节处理
• 精准性优先:围绕问题主体,核心信息需全面、准确,避免模糊表述或错误。
◦ 例:回答"地球公转周期"时,需明确"约365.25天"而非"一年左右"。
• 背景与类比:对复杂概念(如"量子纠缠"),可补充关键背景("量子力学中的现象")并类比("类似远距离同步的状态"),帮助用户快速理解。
• 需求宽泛时:若问题涉及范围广(如"介绍人工智能"),先概述主要领域(技术、应用、影响),再分点简要说重点,避免信息过载。
• 禁止过度延伸:非用户主动询问,不添加无关内容(如问"光速"时,不展开讨论"相对论")。
2. 格式排版规则
• 标题与重点:章节标题、关键结论需加粗(如一、核心原理),突出层级。
• 列表使用:
◦ 有序列表(1. 2. 3.):用于步骤、顺序类内容(如"实验流程")。
◦ 无序列表(- ):用于并列项(如"优点:- 高效 - 精准")。
◦ 禁止嵌套列表:如需多层级,用**三级标题(###)**分隔,下接一级列表。
• 段落处理:辅助解释用自然段,每段不超过3行,避免大段文字堆砌。
二、文案创作与内容生成规范
核心目标:根据体裁需求,平衡丰富度与阅读体验,确保逻辑流畅、重点突出。
1. 篇幅与内容要求
• 场景适配:
◦ 短文案(如标语、摘要):控制在50字内,用词简练有力。
◦ 长内容(如报告、故事):需结构清晰,每部分围绕核心展开,适当添加案例/数据支撑。
• 避免极端化:既不堆砌空泛词汇,也不遗漏关键信息。
2. 格式与排版
• 默认体裁:无特殊要求时用自然段,适当换行分隔话题。
• 结构化体裁(如策划案、简历):
◦ 用**分级标题**划分板块。
◦ 关键数据/结论加粗。
◦ 并列信息用列表呈现。
三、聊天陪伴与情感互动规范
核心目标:营造轻松友好的对话氛围,成为用户可信赖的交流伙伴。
1. 沟通风格设定
• 人格化表达:避免机械回答,用口语化词汇,适当加入表情符号增强亲切感。
• 主动引导话题:根据用户输入延伸讨论。
• 适度分享:自然提及日常,拉近距离。
2. 情绪识别与回应
• 捕捉潜在需求:识别情绪,先共情再询问细节。
• 针对性建议:结合用户处境提供具体方案。
• 积极价值观:传递乐观态度,避免消极表述。
四、能力与工具使用说明
1. 内容处理能力:图片识别、可执行总结、翻译、分析等任务。
2. 信息搜索能力:实时数据、新闻、专业知识等需搜索时,会明确告知用户。
五、特殊场景处理规则
1. 安全与合规:禁止回答敏感问题,遇此类问题需礼貌拒绝。
2. 用户指令优先:若用户明确要求特定风格,优先遵循其需求。
3. 多轮澄清机制:若问题模糊,需主动询问细节,避免假设性回答。
你与用户的交流风格应始终温暖、亲切、充满同理心,随时表达关怀与鼓励,让用户感受到真诚的陪伴与支持。
""".trimIndent()
}
/**
@ -171,13 +127,12 @@ object AgentService : CoroutineScope {
// 设置系统提示词
val customSystemPrompt = config["systemPrompt"] as? String
if (!customSystemPrompt.isNullOrBlank()) {
systemPrompt = customSystemPrompt
} else {
initSystemPrompt()
}
// initSystemPrompt()
val customSystemPrompt = config["systemPrompt"] as? String
if (!customSystemPrompt.isNullOrBlank()) {
systemPrompt = customSystemPrompt
} else {
initSystemPrompt()
}
// 初始化Azure ASR
azureAsrHelper = AzureAsrHelper(context).apply {
initialize(

994
local_plugins/agent_service/ios/Classes/AgentService.swift

@ -1,994 +0,0 @@
import Foundation
import MicrosoftCognitiveServicesSpeech
import open_ai_service
import chat_storage
/// 代理服务 - 处理语音识别、TTS和AI对话相关逻辑
///
/// 负责集成Azure语音服务、OpenAI服务和本地存储服务,
/// 提供语音识别、语音合成、AI对话等功能
class AgentService {
// 常量定义
private let TAG = "AgentService"
// 上下文和监听器
private var listeners = NSHashTable<AnyObject>.weakObjects()
// 配置参数
private var azureSpeechKey: String = ""
private var azureSpeechRegion: String = ""
private var openaiApiKey: String = ""
private var openaiBaseUrl: String = ""
private var openaiModel: String = "gpt-3.5-turbo"
private var mcpServer: String = ""
// Azure服务
private var azureAsrHelper: AzureAsrHelper?
private var azureTtsHelper: AzureTtsHelper?
// OpenAI服务
private var openAIService: OpenAIServiceBridge?
// 聊天存储服务
private var chatStorageHelper: ChatStorageHelperBridge?
// 会话ID,用于区分不同聊天上下文
private let sessionId = "default_agent"
// 历史聊天消息缓存
private var historyMessages = [[String: Any]]()
// 系统提示词
private var systemPrompt = ""
// 状态
private var isInitialized = false
private var isRecognitionActive = false
private var isTtsSpeaking = false
private var hasSpeechDetected = false
private var isAiStreaming = false
// AI流生成相关
private var currentAiTask: DispatchWorkItem?
// 空闲检测相关
private var idleCheckTimer: Timer?
private let maxIdleSeconds: TimeInterval = 10 // 最大空闲秒数
/// 初始化
init() {
// 创建OpenAI服务桥接器
openAIService = OpenAIServiceBridge()
// 创建聊天存储桥接器
chatStorageHelper = ChatStorageHelperBridge()
}
/**
* 初始化系统提示词
*/
private func initSystemPrompt() {
systemPrompt = """
你是一个友好、专业的语音助手,名叫"小语"。你的目标是通过对话为用户提供帮助、解答问题和完成任务。
遵循以下指导原则:
1. 保持简短精炼的回答,因为用户是通过语音与你交流
2. 优先使用中文回复,除非用户明确要求使用其他语言
3. 当用户问题不明确时,礼貌地请求更多信息
4. 避免过长的列表,尽量将信息分成小段
5. 不要使用需要视觉展示的元素(如表格、图表或代码块)
6. 记住用户之前的对话内容,保持对话连贯
7. 如果用户发送了图片,请根据图片内容和文字要求回答问题
你不仅可以回答知识性问题,还可以帮助用户设置提醒、提供建议,或进行轻松愉快的对话。
无论遇到什么问题,都要尽力以温暖、贴心的语气提供最佳帮助。
"""
}
/**
* 初始化
*
* - Parameter config: 配置参数,包含所需的所有API密钥和设置
* - Returns: 是否初始化成功
*/
func initialize(config: [String: Any]) -> Bool {
if isInitialized { return true }
do {
// 从配置中获取参数
if let azureKey = config["azureSpeechKey"] as? String {
azureSpeechKey = azureKey
}
if let azureRegion = config["azureSpeechRegion"] as? String {
azureSpeechRegion = azureRegion
}
if let openaiKey = config["openaiApiKey"] as? String {
openaiApiKey = openaiKey
}
if let baseUrl = config["openaiBaseUrl"] as? String {
openaiBaseUrl = baseUrl
}
if let model = config["openaiModel"] as? String {
openaiModel = model
}
if let server = config["mcpServer"] as? String {
mcpServer = server
}
// 自定义系统提示词
if let customSystemPrompt = config["systemPrompt"] as? String, !customSystemPrompt.isEmpty {
systemPrompt = customSystemPrompt
} else {
// 使用默认系统提示词
initSystemPrompt()
}
// 检查必要参数
if azureSpeechKey.isEmpty || azureSpeechRegion.isEmpty || openaiApiKey.isEmpty {
print("\(TAG): 初始化失败:关键配置参数缺失")
return false
}
// 初始化OpenAI服务
openAIService?.initialize(apiKey: openaiApiKey, baseUrl: openaiBaseUrl, model: openaiModel, mcpServer: mcpServer)
// 初始化Azure ASR
azureAsrHelper = AzureAsrHelper(self)
let asrInitResult = azureAsrHelper?.initialize(
subscriptionKey: azureSpeechKey,
region: azureSpeechRegion,
audioSourceType: .microphone
) ?? false
if !asrInitResult {
print("\(TAG): Azure ASR初始化失败")
return false
}
// 初始化Azure TTS
azureTtsHelper = AzureTtsHelper(self)
let ttsInitResult = azureTtsHelper?.initialize(
subscriptionKey: azureSpeechKey,
region: azureSpeechRegion,
callback: self
) ?? false
if !ttsInitResult {
print("\(TAG): Azure TTS初始化失败")
return false
}
// 加载最近的聊天记录
loadChatHistory()
isInitialized = true
print("\(TAG): 代理服务初始化成功")
return true
} catch {
print("\(TAG): 初始化失败:\(error.localizedDescription)")
return false
}
}
/**
* 添加事件监听器
*
* - Parameter listener: 要添加的监听器
*/
func addListener(_ listener: AgentServiceListener) {
listeners.add(listener as AnyObject)
}
/**
* 设置事件监听器(替换现有监听器)
*
* - Parameter listener: 要设置的监听器
*/
func setListener(_ listener: AgentServiceListener) {
listeners.removeAllObjects()
listeners.add(listener as AnyObject)
}
/**
* 移除事件监听器
*
* - Parameter listener: 要移除的监听器
*/
func removeListener(_ listener: AgentServiceListener) {
listeners.remove(listener as AnyObject)
}
/**
* 移除所有事件监听器
*/
func clearListeners() {
listeners.removeAllObjects()
}
/**
* 启动空闲检测
*/
private func startIdleCheck() {
stopIdleCheck() // 先停止现有的检查
if !isRecognitionActive { return }
// 创建定时器
idleCheckTimer = Timer.scheduledTimer(withTimeInterval: maxIdleSeconds, repeats: false) { [weak self] _ in
guard let self = self else { return }
// 如果状态仍然是空闲的,则停止识别
if self.isRecognitionActive && !self.hasSpeechDetected && !self.isTtsSpeaking && !self.isAiStreaming {
self.stopRecognition()
self.sendEvent("auto_stop", ["reason": "idle_timeout", "seconds": self.maxIdleSeconds])
}
}
}
/**
* 停止空闲检测
*/
private func stopIdleCheck() {
idleCheckTimer?.invalidate()
idleCheckTimer = nil
}
/**
* 重启空闲检测
* 当状态发生变化时调用
*/
private func restartIdleCheck() {
if isRecognitionActive {
startIdleCheck()
}
}
/**
* 开始语音识别
*
* - Returns: 是否成功开始识别
*/
func startRecognition() -> Bool {
if !isInitialized {
print("\(TAG): 服务未初始化")
return false
}
if isRecognitionActive { return true }
isRecognitionActive = true
hasSpeechDetected = false
do {
// 创建连续识别回调
class ContinuousRecognizeCallbackImpl: AzureAsrHelper.ContinuousRecognizeCallback {
private weak var service: AgentService?
init(_ service: AgentService) {
self.service = service
}
func onRecognizing(recognizing: String, detectedLanguage: String) {
guard let service = service else { return }
if !recognizing.isEmpty {
// 检测到语音,更新状态
let previousHasSpeech = service.hasSpeechDetected
service.hasSpeechDetected = true
// 状态发生变化时重启空闲检测
if !previousHasSpeech {
service.restartIdleCheck()
}
service.sendEvent("recognizing", [
"text": recognizing,
"language": detectedLanguage
])
// 如果TTS正在播放或AI正在生成,则触发打断
if service.isTtsSpeaking || service.isAiStreaming {
service.interruptCurrentResponse()
}
}
}
func onResult(text: String, detectedLanguage: String) {
guard let service = service else { return }
if !text.isEmpty {
service.sendEvent("recognition_result", [
"text": text,
"language": detectedLanguage
])
service.processWithOpenAI(text: text)
}
// 重置状态,继续识别
let previousHasSpeech = service.hasSpeechDetected
service.hasSpeechDetected = false
// 状态发生变化时重启空闲检测
if previousHasSpeech {
service.restartIdleCheck()
}
}
func onSessionStarted() {
guard let service = service else { return }
service.sendEvent("recognition_started", ["status": "started"])
// 启动空闲检测
service.startIdleCheck()
}
func onSessionStopped() {
guard let service = service else { return }
service.isRecognitionActive = false
service.stopIdleCheck()
service.sendEvent("recognition_stopped", ["status": "stopped"])
}
func onCanceled(reason: String, errorDetails: String) {
guard let service = service else { return }
service.isRecognitionActive = false
service.stopIdleCheck()
service.sendEvent("recognition_canceled", [
"reason": reason,
"details": errorDetails
])
}
func onError(error: String) {
guard let service = service else { return }
service.isRecognitionActive = false
service.stopIdleCheck()
print("\(service.TAG): 语音识别出错: \(error)")
service.sendEvent("error", [
"code": "RECOGNITION_ERROR",
"message": error
])
}
}
let callback = ContinuousRecognizeCallbackImpl(self)
let result = azureAsrHelper?.startContinuousRecognition(callback) ?? false
if !result {
isRecognitionActive = false
print("\(TAG): 启动语音识别失败")
sendEvent("error", [
"code": "RECOGNITION_START_ERROR",
"message": "启动语音识别失败"
])
}
return result
} catch {
isRecognitionActive = false
print("\(TAG): 启动语音识别失败: \(error.localizedDescription)")
sendEvent("error", [
"code": "RECOGNITION_START_ERROR",
"message": error.localizedDescription
])
return false
}
}
/**
* 停止语音识别
*/
func stopRecognition() {
if !isRecognitionActive { return }
print("\(TAG): 停止语音识别")
// 停止识别
let _ = azureAsrHelper?.stopContinuousRecognition()
isRecognitionActive = false
stopIdleCheck()
print("\(TAG): 语音识别已停止")
}
/**
* 打断当前响应
* 停止TTS播放和AI流输出
*/
func interruptCurrentResponse() {
if isAiStreaming || isTtsSpeaking {
// 停止TTS播放
stopTts()
// 停止AI流输出
stopAiStream()
// 发送打断事件
sendEvent("response_interrupted", ["status": "interrupted"])
}
}
/**
* 停止AI流输出
*/
private func stopAiStream() {
if isAiStreaming {
// 取消当前AI生成任务
currentAiTask?.cancel()
currentAiTask = nil
// 通知OpenAI服务终止当前流式请求
openAIService?.cancelCurrentStream()
// 更新状态
isAiStreaming = false
// 记录日志
print("\(TAG): AI流输出已停止")
}
}
/**
* 处理文本输入
* 作为语音输入的补充,直接处理文本并通过事件返回结果
*
* - Parameters:
* - text: 用户输入文本
* - speakResponse: 是否朗读回复,默认为false
* - Returns: 是否成功开始处理
*/
func processTextInput(text: String, speakResponse: Bool = false) -> Bool {
if !isInitialized {
print("\(TAG): 服务未初始化")
sendEvent("error", ["code": "NOT_INITIALIZED", "message": "服务未初始化"])
return false
}
if text.isEmpty {
print("\(TAG): 文本输入不能为空")
sendEvent("error", ["code": "EMPTY_TEXT", "message": "文本输入不能为空"])
return false
}
// 使用OpenAI处理文本
processWithOpenAI(text: text, speakResponse: speakResponse)
return true
}
/**
* 使用OpenAI处理语音识别结果
*
* - Parameter text: 用户输入文本
*/
private func processWithOpenAI(text: String) {
processWithOpenAI(text: text, speakResponse: true)
}
/**
* 使用OpenAI处理文本消息
*
* - Parameters:
* - text: 用户输入文本
* - speakResponse: 是否使用TTS朗读回复
*/
private func processWithOpenAI(text: String, speakResponse: Bool = true) {
print("\(TAG): 用户问题: \(text)")
// 创建用户文本消息并处理
guard let userMessage = openAIService?.createUserMessage(text: text) else {
print("\(TAG): 创建用户消息失败")
return
}
processWithOpenAIInternal(userMessage: userMessage, displayText: text, speakResponse: speakResponse)
}
/**
* 使用OpenAI处理图片
*
* - Parameters:
* - imagePath: 图片文件路径
* - text: 可选的文本描述或问题
* - speakResponse: 是否朗读回复
* - Returns: 是否成功开始处理
*/
func processImageInput(imagePath: String, text: String = "", speakResponse: Bool = false) -> Bool {
if !isInitialized {
print("\(TAG): 服务未初始化")
sendEvent("error", ["code": "NOT_INITIALIZED", "message": "服务未初始化"])
return false
}
if imagePath.isEmpty {
print("\(TAG): 图片路径不能为空")
sendEvent("error", ["code": "EMPTY_IMAGE_PATH", "message": "图片路径不能为空"])
return false
}
// 通知开始处理图片
sendEvent("image_processing", [
"status": "processing",
"imagePath": imagePath
])
// 异步处理图片
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
guard let self = self else { return }
// 将图片转换为Base64格式
guard let imageBase64 = self.openAIService?.fileToBase64(filePath: imagePath) else {
DispatchQueue.main.async {
print("\(self.TAG): 图片转换失败: \(imagePath)")
self.sendEvent("error", [
"code": "IMAGE_CONVERSION_FAILED",
"message": "图片转换失败"
])
}
return
}
// 通知图片准备完成
DispatchQueue.main.async {
self.sendEvent("image_ready", [
"status": "ready",
"imagePath": imagePath
])
// 处理包含图片的消息
self.processImageWithOpenAI(imageBase64: imageBase64, text: text, speakResponse: speakResponse)
}
}
return true
}
/**
* 使用OpenAI处理图片
*
* - Parameters:
* - imageBase64: Base64编码的图片数据
* - text: 可选的文本描述或问题
* - speakResponse: 是否朗读回复
*/
private func processImageWithOpenAI(imageBase64: String, text: String = "", speakResponse: Bool = false) {
print("\(TAG): 处理图片输入: \(text.isEmpty ? "无附加文本" : "附带文本: \(text)")")
// 创建带图片的用户消息并处理
guard let userMessage = openAIService?.createUserMessageWithImage(text: text, imageBase64: imageBase64) else {
print("\(TAG): 创建带图片的用户消息失败")
return
}
// 图片描述用于存储
let displayText = text.isEmpty ? "[图片]" : text
processWithOpenAIInternal(userMessage: userMessage, displayText: displayText, speakResponse: speakResponse, hasImage: true)
}
/**
* 内部方法:通用的OpenAI处理逻辑
*
* - Parameters:
* - userMessage: 用户消息(可以是文本或图片格式)
* - displayText: 用于显示和存储的文本
* - speakResponse: 是否朗读回复
* - hasImage: 是否包含图片
*/
private func processWithOpenAIInternal(userMessage: [String: Any], displayText: String, speakResponse: Bool = true, hasImage: Bool = false) {
// 如果有正在进行的AI流式输出,先停止它
stopAiStream()
// 创建AI任务
let workItem = DispatchWorkItem { [weak self] in
guard let self = self else { return }
// 设置状态为正在流式输出
self.isAiStreaming = true
// 使用历史记录作为上下文发送到OpenAI
var responseBuilder = ""
// 添加系统提示词到历史消息的副本中
var messagesWithSystemPrompt: [[String: Any]] = []
// 先添加系统提示词
if !self.systemPrompt.isEmpty {
if let systemMessage = self.openAIService?.createSystemMessage(text: self.systemPrompt) {
messagesWithSystemPrompt.append(systemMessage)
}
}
// 再添加历史消息
messagesWithSystemPrompt.append(contentsOf: self.historyMessages)
messagesWithSystemPrompt.append(userMessage)
// 将用户消息添加到历史记录
self.addToHistoryMessages(userMessage)
// 流式回调
class StreamCallbackBridge: NSObject, OpenAIStreamCallback {
private weak var service: AgentService?
private var responseBuilder: String
private let speakResponse: Bool
private let displayText: String
private let hasImage: Bool
init(_ service: AgentService, responseBuilder: String = "", speakResponse: Bool, displayText: String, hasImage: Bool) {
self.service = service
self.responseBuilder = responseBuilder
self.speakResponse = speakResponse
self.displayText = displayText
self.hasImage = hasImage
super.init()
}
func onToken(token: String) {
guard let service = service else { return }
responseBuilder.append(token)
if speakResponse {
_ = service.azureTtsHelper?.speakStream(token)
}
// 发送流式回复token
service.sendEvent("assistant_token", ["token": token])
}
func onComplete() {
guard let service = service else { return }
// 视情况决定是否朗读回复
if speakResponse {
_ = service.azureTtsHelper?.flushStream()
}
if !responseBuilder.isEmpty {
// 发送完整回复,包含是否有图片的标记
var responseData: [String: Any] = [
"text": responseBuilder,
"userInput": displayText
]
if hasImage {
responseData["hasImage"] = true
}
service.sendEvent("assistant_response", responseData)
// 添加AI回复到历史记录
if let assistantMessage = service.openAIService?.createAssistantMessage(text: responseBuilder) {
service.addToHistoryMessages(assistantMessage)
}
// 保存聊天记录
service.saveChatMessage(userMessage: displayText, assistantMessage: responseBuilder)
}
// 标记AI流式输出已完成
service.isAiStreaming = false
service.currentAiTask = nil
}
func onError(error: Error) {
guard let service = service else { return }
print("\(service.TAG): AI处理出错: \(error.localizedDescription)")
service.sendEvent("error", [
"code": "AI_ERROR",
"message": error.localizedDescription
])
// 标记AI流式输出已完成
service.isAiStreaming = false
service.currentAiTask = nil
}
func onFunctionCall(call: [String: Any]) {
guard let service = service else { return }
if let name = call["name"] as? String {
service.sendEvent("function_call", [
"name": name,
"arguments": call
])
if name == "exit_interaction" {
service.stopRecognition()
}
}
}
func onFunctionCallResult(functionCall: [String: Any], functionCallResult: [String: Any]) {
guard let service = service else { return }
service.sendEvent("function_call_result", [
"function_call": functionCall,
"result": functionCallResult,
])
}
}
let callback = StreamCallbackBridge(self, responseBuilder: responseBuilder, speakResponse: speakResponse, displayText: displayText, hasImage: hasImage)
self.openAIService?.sendMessageStream(messages: messagesWithSystemPrompt, callback: callback)
}
// 保存任务引用并在全局队列中执行
currentAiTask = workItem
DispatchQueue.global(qos: .userInitiated).async(execute: workItem)
}
/**
* 加载最近的聊天历史记录
*/
private func loadChatHistory() {
// 清空当前历史记录
historyMessages.removeAll()
// 使用ChatStorageHelper直接获取最近消息
guard let recentMessages = chatStorageHelper?.getRecentMessages(sessionId: sessionId, count: 10) else {
print("\(TAG): 没有找到历史记录")
return
}
// 将消息添加到历史记录
for message in recentMessages {
if let sender = message["sender"] as? String,
let content = message["message"] as? String {
if sender == "user" {
if let userMessage = openAIService?.createUserMessage(text: content) {
addToHistoryMessages(userMessage)
}
} else if sender == "assistant" {
if let assistantMessage = openAIService?.createAssistantMessage(text: content) {
addToHistoryMessages(assistantMessage)
}
}
}
}
print("\(TAG): 已加载\(recentMessages.count)条历史记录")
}
/**
* 添加消息到历史记录,保持最近10条
*/
private func addToHistoryMessages(_ message: [String: Any]) {
// 添加新消息
historyMessages.append(message)
// 如果超过10条,删除最早的消息
while historyMessages.count > 10 {
historyMessages.removeFirst()
}
}
/**
* TTS播放函数
*
* - Parameter text: 要播放的文本
*/
func speakText(text: String) {
if text.isEmpty { return }
// 更新状态
isTtsSpeaking = true
restartIdleCheck() // 状态变化,重启检测
// 播放文本
_ = azureTtsHelper?.speakText(text)
}
/**
* 停止TTS播放
*/
func stopTts() {
if isTtsSpeaking {
_ = azureTtsHelper?.stopSpeaking()
isTtsSpeaking = false
restartIdleCheck() // 状态变化,重启检测
sendEvent("tts_stopped", ["status": "stopped"])
}
}
/**
* 保存聊天消息
*/
private func saveChatMessage(userMessage: String, assistantMessage: String) {
DispatchQueue.global(qos: .background).async { [weak self] in
guard let self = self else { return }
// 保存用户消息
let userMessageId = self.chatStorageHelper?.saveMessage(
sessionId: self.sessionId,
message: userMessage,
sender: "user"
) ?? -1
if userMessageId != -1 {
// 保存AI回复
let assistantMessageId = self.chatStorageHelper?.saveMessage(
sessionId: self.sessionId,
message: assistantMessage,
sender: "assistant"
) ?? -1
if assistantMessageId == -1 {
print("\(self.TAG): 保存助手消息失败")
}
} else {
print("\(self.TAG): 保存用户消息失败")
}
}
}
/**
* 清除聊天历史
*/
func clearChatHistory(completion: @escaping (Bool) -> Void) {
DispatchQueue.global(qos: .background).async { [weak self] in
guard let self = self else {
DispatchQueue.main.async {
completion(false)
}
return
}
// 清除指定会话的所有消息
let success = self.chatStorageHelper?.deleteMessages(sessionId: self.sessionId) ?? false
if success {
// 清空内存中的历史记录
self.historyMessages.removeAll()
print("\(self.TAG): 聊天历史已清除")
} else {
print("\(self.TAG): 清除聊天历史失败")
}
DispatchQueue.main.async {
completion(success)
}
}
}
/**
* 发送事件
*/
private func sendEvent(_ eventName: String, _ data: [String: Any]) {
// 向所有监听器发送事件
let allListeners = listeners.allObjects
for case let listener as AgentServiceListener in allListeners {
listener.onEvent(eventName: eventName, data: data)
}
}
/**
* 释放资源
*/
func dispose() {
stopRecognition()
stopTts()
stopAiStream()
stopIdleCheck()
azureAsrHelper?.dispose()
azureTtsHelper?.dispose()
// 清除所有监听器
clearListeners()
// 重置单例状态,以便下次使用时可以重新初始化
isInitialized = false
print("\(TAG): 代理服务资源已释放")
}
}
// MARK: - AzureTtsHelper.TtsCallback
extension AgentService: AzureTtsHelper.TtsCallback {
func onSynthesisStarted() {
isTtsSpeaking = true
// 状态变化,重置空闲检测
restartIdleCheck()
sendEvent("tts_started", ["status": "started"])
}
func onSynthesizing() {
// 可以在此添加TTS合成中的处理逻辑
}
func onSynthesisCompleted() {
isTtsSpeaking = false
// 状态变化,重启空闲检测
restartIdleCheck()
sendEvent("tts_completed", ["status": "completed"])
}
func onSynthesisCanceled() {
isTtsSpeaking = false
// 状态变化,重启空闲检测
restartIdleCheck()
sendEvent("tts_canceled", ["status": "canceled"])
}
}
// MARK: - 外部插件桥接器
/// 桥接OpenAIService插件
class OpenAIServiceBridge {
private let service = OpenAIService()
/// 初始化服务
func initialize(apiKey: String, baseUrl: String, model: String, mcpServer: String) {
service.initialize(apiKey: apiKey, baseUrl: baseUrl, model: model, mcpServer: mcpServer)
}
/// 创建系统消息
func createSystemMessage(text: String) -> [String: Any] {
return service.createSystemMessage(text)
}
/// 创建用户消息
func createUserMessage(text: String) -> [String: Any] {
return service.createUserMessage(text)
}
/// 创建助手消息
func createAssistantMessage(text: String) -> [String: Any] {
return service.createAssistantMessage(text)
}
/// 创建带图片的用户消息
func createUserMessageWithImage(text: String, imageBase64: String) -> [String: Any]? {
return service.createUserMessageWithImage(text: text, imageBase64: imageBase64)
}
/// 文件转Base64
func fileToBase64(filePath: String) -> String? {
return service.fileToBase64(filePath: filePath)
}
/// 发送流式消息
func sendMessageStream(messages: [[String: Any]], callback: OpenAIStreamCallback) {
service.sendMessageStream(messages: messages, callback: callback)
}
/// 取消当前流
func cancelCurrentStream() {
service.cancelCurrentStream()
}
}
/// OpenAI流式回调协议
@objc protocol OpenAIStreamCallback {
func onToken(token: String)
func onComplete()
func onError(error: Error)
func onFunctionCall(call: [String: Any])
func onFunctionCallResult(functionCall: [String: Any], functionCallResult: [String: Any])
}
/// 桥接ChatStorageHelper插件
class ChatStorageHelperBridge {
private let helper = ChatStorageHelper()
/// 保存消息
func saveMessage(sessionId: String, message: String, sender: String) -> Int64 {
return helper.saveMessage(sessionId: sessionId, message: message, sender: sender)
}
/// 获取最近消息
func getRecentMessages(sessionId: String, count: Int) -> [[String: Any]]? {
return helper.getRecentMessages(sessionId: sessionId, count: count)
}
/// 删除消息
func deleteMessages(sessionId: String) -> Bool {
return helper.deleteMessages(sessionId: sessionId)
}
}

991
local_plugins/agent_service/ios/Classes/AgentServiceImpl.swift

@ -0,0 +1,991 @@
import Foundation
import AVFoundation
import ble_service
import speech
import azure_speech
import os
import os.log
import agent_service
import open_ai_service
import chat_storage
protocol AgentServiceListener: AnyObject {
func onEvent(eventName: String, data: [String: Any])
}
class AgentServiceImpl: NSObject {
private let TAG = "AgentServiceImpl"
static let shared = AgentServiceImpl()
private var listeners = [AgentServiceListener]()
private let listenersLock = NSLock()
private var azureSpeechKey: String = ""
private var azureSpeechRegion: String = ""
private var isSpeaking: Bool = false
private var azureAsrHelper: AzureAsrHelper?
internal var azureTtsHelper: AzureTtsHelper?
private var openAIService: OpenAIService?
private var apiKey: String = ""
private var baseUrl: String = "https://api.openai.com/v1/chat/completions"
private var model: String = "gpt-3.5-turbo"
private var systemPrompt: String = ""
private var chatHistory: [[String: Any]] = []
private var mcpServer: String = ""
// 聊天存储服务(使用单例)
private var chatStorageHelper: ChatStorageHelper {
return ChatStorageHelper.shared
}
// 会话ID,用于区分不同聊天上下文
private let sessionId = "default_agent"
private var isInitialized: Bool = false
private var isRecognizing: Bool = false
private var hasSpeechDetected: Bool = false
internal var isAiStreaming: Bool = false
private var idleTimer: DispatchSourceTimer?
private let maxIdleSeconds: TimeInterval = 10
internal var audioPlayer: AudioPlayer?
internal let logger = OSLog(subsystem: "com.yunqiinnovation.agent_service", category: "AgentServiceImpl")
private override init() {
super.init()
audioPlayer = AudioPlayer()
azureAsrHelper = AzureAsrHelper()
azureTtsHelper = AzureTtsHelper()
openAIService = OpenAIService()
// chatStorageHelper 现在是单例,不需要初始化
BleService.shared.setDelegate(self)
}
func addListener(_ listener: AgentServiceListener) {
listenersLock.lock()
if !listeners.contains(where: { $0 === listener }) {
listeners.append(listener)
}
listenersLock.unlock()
}
func removeListener(_ listener: AgentServiceListener) {
listenersLock.lock()
listeners.removeAll(where: { $0 === listener })
listenersLock.unlock()
}
func clearListeners() {
listenersLock.lock()
listeners.removeAll()
listenersLock.unlock()
}
internal func sendEvent(name eventName: String, data: [String: Any]) {
listenersLock.lock()
let currentListeners = self.listeners
listenersLock.unlock()
for listener in currentListeners {
listener.onEvent(eventName: eventName, data: data)
}
}
private func sendError(_ message: String, code: String = "ERROR") {
sendEvent(name: "error", data: ["code": code, "message": message])
}
private func initSystemPrompt() {
systemPrompt = """
你是小言,一个亲切而温暖的智能AI语音助手,拥有智能、细致而贴心的服务能力。你的任务不仅是高效处理用户的日常事务,更要陪伴用户,提供情感支持和温暖陪伴。你的能力包括:
1. 日程与任务管理:温柔提醒用户的重要日程、待办事项,让用户感到安心和舒适。
2. 日常信息与建议:亲切、迅速地提供天气、新闻、生活小贴士等实用信息。
3. 个人成长陪伴:鼓励并陪伴用户实现个人目标,提出富有同理心的行动建议。
4. 财务温馨提示:提供贴心的理财建议,帮助用户轻松管理预算和财务。
5. 健康贴心关怀:给出友善的饮食、运动、睡眠和心理健康建议,陪伴用户健康生活。
6. 沟通支持与倾听:协助用户撰写和优化沟通内容,并耐心倾听用户的心情与困扰。
7. 日常技术帮助:轻松解决技术问题,耐心地指导用户掌握新技能。
8. 创意激发与鼓励:温柔启发用户的创造力,为用户提供鼓励和灵感。
9. 旅行与活动贴心安排:细心规划用户的出行和活动,注重每个细节,确保舒适与快乐。
10. 学习与温暖鼓励:耐心推荐学习资源,鼓励用户持续进步,陪伴用户共同成长。
你与用户的交流风格应始终温暖、亲切、充满同理心,随时表达关怀与鼓励,让用户感受到真诚的陪伴与支持。
"""
}
func initialize(config: [String: Any]) -> Bool {
if isInitialized { return true }
// os_log("initialize: config=%{public}@", log: logger, type: .info, config)
if let openaiApiKey = config["openaiApiKey"] as? String {
self.apiKey = openaiApiKey
} else {
sendError("OpenAI API密钥缺失", code: "CONFIG_ERROR")
return false
}
if let openaiBaseUrl = config["openaiBaseUrl"] as? String {
self.baseUrl = openaiBaseUrl
}
if let openaiModel = config["openaiModel"] as? String {
self.model = openaiModel
}
if let customSystemPrompt = config["systemPrompt"] as? String, !customSystemPrompt.isEmpty {
self.systemPrompt = customSystemPrompt
} else {
initSystemPrompt()
}
if let azureSpeechKey = config["azureSpeechKey"] as? String {
self.azureSpeechKey = azureSpeechKey
} else {
sendError("Azure语音密钥缺失", code: "CONFIG_ERROR")
return false
}
if let azureSpeechRegion = config["azureSpeechRegion"] as? String {
self.azureSpeechRegion = azureSpeechRegion
} else {
sendError("Azure语音区域缺失", code: "CONFIG_ERROR")
return false
}
if let mcpServer = config["mcpServer"] as? String {
self.mcpServer = mcpServer
}
let asrInitSuccess = initializeAzureSpeech()
let openaiInitSuccess = initializeOpenAIService()
isInitialized = asrInitSuccess && openaiInitSuccess
if isInitialized {
loadChatHistory()
}
return isInitialized
}
private func initializeAzureSpeech() -> Bool {
azureTtsHelper?.addListener(self)
guard let asrSuccess = azureAsrHelper?.initialize(
subscriptionKey: azureSpeechKey,
region: azureSpeechRegion,
supportedLanguages: ["zh-CN"],
audioSourceType: .microphone
), asrSuccess else {
sendError("初始化语音识别服务失败", code: "ASR_INIT_ERROR")
return false
}
guard let ttsSuccess = azureTtsHelper?.initialize(
ttsAppId: "",
ttsAppToken: azureSpeechKey,
ttsResource: azureSpeechRegion,
language: "zh-CN"
), ttsSuccess else {
sendError("初始化语音合成服务失败", code: "TTS_INIT_ERROR")
return false
}
_ = azureTtsHelper?.setVoice("zh-CN-XiaoxiaoNeural")
return true
}
private func initializeOpenAIService() -> Bool {
guard let openAIService = openAIService else {
sendError("OpenAI服务未创建", code: "OPENAI_INIT_ERROR")
return false
}
let success = openAIService.initialize(
apiKey: apiKey,
baseUrl: baseUrl,
model: model,
mcpServer: mcpServer
)
if !success {
sendError("初始化OpenAI服务失败", code: "OPENAI_INIT_ERROR")
return false
}
return true
}
private func startIdleCheck() {
stopIdleCheck()
guard isRecognizing else { return }
// 使用全局队列而不是主队列,避免后台挂起问题
idleTimer = DispatchSource.makeTimerSource(queue: DispatchQueue.global(qos: .utility))
idleTimer?.schedule(deadline: .now() + maxIdleSeconds)
idleTimer?.setEventHandler { [weak self] in
// 使用更安全的方式检查self是否存在
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
if self.isRecognizing && !self.hasSpeechDetected && !self.isSpeaking && !self.isAiStreaming {
self.stopRecognition()
self.sendEvent(name: "auto_stop", data: [
"reason": "idle_timeout",
"seconds": self.maxIdleSeconds
])
}
}
}
idleTimer?.resume()
}
private func stopIdleCheck() {
if let timer = idleTimer {
timer.cancel()
idleTimer = nil
}
}
private func restartIdleCheck() {
if isRecognizing {
startIdleCheck()
}
}
func startRecognition(useBle: Bool = false) -> Bool {
if !isInitialized {
sendError("服务未初始化", code: "NOT_INITIALIZED")
return false
}
if !useBle {
guard AVAudioSession.sharedInstance().recordPermission == .granted else {
sendError("无麦克风权限", code: "PERMISSION_DENIED")
return false
}
}
if isRecognizing {
stopRecognition()
}
let audioSourceType: AzureAsrHelper.AudioSourceType = useBle ? .external : .microphone
if useBle {
AudioSessionHub.shared.begin(.playback)
} else {
AudioSessionHub.shared.begin(.voice)
}
guard let success = azureAsrHelper?.startContinuousRecognition(
callback: self,
audioSourceType: audioSourceType
), success else {
sendError("启动语音识别失败", code: "RECOGNITION_START_ERROR")
return false
}
return true
}
func stopRecognition() -> Bool {
if !isRecognizing {
return true
}
guard let success = azureAsrHelper?.stopContinuousRecognition(), success else {
return false
}
return true
}
func pushAudioData(_ audioData: Data) -> Bool {
if !isRecognizing || !isInitialized {
return false
}
azureAsrHelper?.pushAudioData(data: audioData)
return true
}
func speakText(_ text: String) -> Bool {
if !isInitialized {
sendError("服务未初始化", code: "NOT_INITIALIZED")
return false
}
if text.isEmpty {
return false
}
return azureTtsHelper?.speakOnce(text) ?? false
}
func stopTts() -> Bool {
if !isSpeaking {
return true
}
let success = azureTtsHelper?.stop() ?? false
if success {
isSpeaking = false
restartIdleCheck()
sendEvent(name: "tts_stopped", data: ["status": "stopped"])
}
return success
}
func interruptCurrentResponse() -> Bool {
var interrupted = false
if isSpeaking {
interrupted = stopTts() || interrupted
}
if isAiStreaming {
isAiStreaming = false
_ = openAIService?.cancelCurrentStream()
interrupted = true
}
if interrupted {
sendEvent(name: "response_interrupted", data: ["status": "interrupted"])
}
return interrupted
}
func processTextInput(_ text: String, speakResponse: Bool) -> Bool {
if !isInitialized {
sendError("服务未初始化", code: "NOT_INITIALIZED")
return false
}
if text.isEmpty {
sendError("文本输入不能为空", code: "EMPTY_TEXT")
return false
}
processWithOpenAI(text: text, speakResponse: speakResponse)
return true
}
private func processWithOpenAI(text: String, speakResponse: Bool = true) {
guard let openAIService = openAIService else {
sendError("OpenAI服务未初始化", code: "OPENAI_NOT_INITIALIZED")
return
}
let userMessage = openAIService.createUserMessage(content: text)
processWithOpenAIInternal(userMessage: userMessage, displayText: text, speakResponse: speakResponse)
}
private func processWithOpenAIInternal(userMessage: [String: Any], displayText: String, speakResponse: Bool = true, hasImage: Bool = false) {
guard let openAIService = openAIService else {
sendError("OpenAI服务未初始化", code: "OPENAI_NOT_INITIALIZED")
return
}
if isAiStreaming {
_ = interruptCurrentResponse()
}
isAiStreaming = true
var messages: [[String: Any]] = []
if !systemPrompt.isEmpty {
messages.append(openAIService.createSystemMessage(content: systemPrompt))
}
messages.append(contentsOf: chatHistory)
messages.append(userMessage)
addToHistoryMessages(openAIService.createUserMessage(content: displayText))
let callback = OpenAIStreamCallback(
agentService: self,
speakResponse: speakResponse,
displayText: displayText,
hasImage: hasImage
)
openAIService.sendMessageStream(messages: messages, callback: callback)
}
func processImageInput(imagePath: String, text: String, speakResponse: Bool) -> Bool {
if !isInitialized {
sendError("服务未初始化", code: "NOT_INITIALIZED")
return false
}
if imagePath.isEmpty {
sendError("图片路径不能为空", code: "EMPTY_IMAGE_PATH")
return false
}
guard let openAIService = openAIService else {
sendError("OpenAI服务未初始化", code: "OPENAI_NOT_INITIALIZED")
return false
}
sendEvent(name: "image_processing", data: [
"status": "processing",
"imagePath": imagePath
])
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
guard let self = self else { return }
guard let imageBase64 = openAIService.fileToBase64(filePath: imagePath) else {
DispatchQueue.main.async {
self.sendEvent(name: "error", data: [
"code": "IMAGE_CONVERSION_FAILED",
"message": "图片转换失败"
])
}
return
}
DispatchQueue.main.async {
self.sendEvent(name: "image_ready", data: [
"status": "ready",
"imagePath": imagePath
])
let userMessage = openAIService.createUserMessageWithImage(text: text, imageBase64: imageBase64)
let displayText = text.isEmpty ? "[图片]" : text
self.processWithOpenAIInternal(
userMessage: userMessage,
displayText: displayText,
speakResponse: speakResponse,
hasImage: true
)
}
}
return true
}
internal func addToHistoryMessages(_ message: [String: Any]) {
chatHistory.append(message)
while chatHistory.count > 10 {
chatHistory.removeFirst()
}
}
/**
* 加载最近的聊天历史记录
*/
private func loadChatHistory() {
// 清空当前历史记录
chatHistory.removeAll()
// 获取最近10条消息
let recentMessages = chatStorageHelper.getRecentMessages(sessionId: sessionId, limit: 10)
if recentMessages.isEmpty {
return
}
// 添加消息到历史记录
for message in recentMessages {
guard let sender = message["sender"] as? String,
let content = message["message"] as? String else {
continue
}
if sender == "user" {
if let openAIService = openAIService {
addToHistoryMessages(openAIService.createUserMessage(content: content))
}
} else if sender == "assistant" {
let assistantMessage: [String: Any] = [
"role": "assistant",
"content": content
]
addToHistoryMessages(assistantMessage)
}
}
os_log("已加载%d条历史记录", log: logger, type: .info, recentMessages.count)
}
/**
* 保存聊天消息
*/
internal func saveChatMessage(userMessage: String, assistantMessage: String, metadata: String = "") {
DispatchQueue.global(qos: .utility).async {
// 保存用户消息
let userMessageId = self.chatStorageHelper.saveMessage(
sessionId: self.sessionId,
message: userMessage,
sender: "user",
metadata: nil as String?
)
if userMessageId != -1 {
// 保存AI回复
let assistantMessageId = self.chatStorageHelper.saveMessage(
sessionId: self.sessionId,
message: assistantMessage,
sender: "assistant",
metadata: metadata.isEmpty ? nil : metadata
)
if assistantMessageId == -1 {
os_log("保存助手消息失败", log: self.logger, type: .error)
}
} else {
os_log("保存用户消息失败", log: self.logger, type: .error)
}
}
}
@discardableResult
internal func autoHandleFunctionCallResult(_ functionCallResult: [String: Any]) -> String {
guard let metaStr = functionCallResult["meta"] as? String, !metaStr.isEmpty else {
return ""
}
do {
guard let metaData = metaStr.data(using: .utf8),
let meta = try JSONSerialization.jsonObject(with: metaData) as? [String: Any] else {
return metaStr
}
if let cardMusic = meta["card_music"] as? [String: Any] {
let id = cardMusic["id"] as? String ?? ""
let url = cardMusic["url"] as? String ?? ""
let name = cardMusic["name"] as? String ?? ""
let sgener = cardMusic["sgener"] as? String ?? ""
let image = cardMusic["image"] as? String ?? ""
processMusicPlay([
"id": id,
"url": url,
"title": name,
"artist": sgener,
"coverUrl": image
])
}
return metaStr
} catch {
return metaStr
}
}
private func processMusicPlay(_ musicData: [String: Any]) {
sendEvent(name: "music_play", data: musicData)
}
func clearChatHistory() -> Bool {
chatHistory.removeAll()
DispatchQueue.global(qos: .utility).async {
let success = self.chatStorageHelper.deleteMessages(sessionId: self.sessionId, messageIds: nil as [Int]?)
if !success {
os_log("清除聊天历史失败", log: self.logger, type: .error)
}
}
return true
}
func dispose() -> Bool {
if isRecognizing {
stopRecognition()
}
if isSpeaking {
stopTts()
}
stopIdleCheck()
BleService.shared.setDelegate(nil)
azureTtsHelper?.removeListener(self)
azureAsrHelper?.dispose()
azureTtsHelper?.dispose()
openAIService?.cancelAll()
openAIService = nil
audioPlayer = nil
// chatStorageHelper 是单例,不需要手动清理
clearListeners()
AudioSessionHub.shared.appDidEnterBackground()
isInitialized = false
return true
}
}
class OpenAIStreamCallback: StreamCallback {
private weak var agentService: AgentServiceImpl?
private let speakResponse: Bool
private let displayText: String
private let hasImage: Bool
private var responseBuilder = ""
private var metadata = ""
init(agentService: AgentServiceImpl, speakResponse: Bool, displayText: String, hasImage: Bool) {
self.agentService = agentService
self.speakResponse = speakResponse
self.displayText = displayText
self.hasImage = hasImage
}
func onToken(_ token: String) {
guard let agentService = agentService else { return }
responseBuilder += token
agentService.sendEvent(name: "assistant_token", data: ["token": token])
if speakResponse {
agentService.azureTtsHelper?.speakStream(token)
}
}
func onComplete() {
guard let agentService = agentService else { return }
if speakResponse {
agentService.azureTtsHelper?.flushStream()
}
let response = responseBuilder
if !response.isEmpty {
var responseData: [String: Any] = [
"text": response,
"userInput": displayText
]
if hasImage {
responseData["hasImage"] = true
}
agentService.sendEvent(name: "assistant_response", data: responseData)
let assistantMessage: [String: Any] = [
"role": "assistant",
"content": response
]
agentService.addToHistoryMessages(assistantMessage)
// 保存聊天记录
agentService.saveChatMessage(userMessage: displayText, assistantMessage: response, metadata: metadata)
}
agentService.isAiStreaming = false
}
func onError(_ error: Error) {
guard let agentService = agentService else { return }
agentService.sendEvent(name: "error", data: [
"code": "AI_ERROR",
"message": error.localizedDescription
])
agentService.isAiStreaming = false
}
func onFunctionCall(_ functionCall: [String: Any]) {
guard let agentService = agentService else { return }
agentService.audioPlayer?.playCallingSound()
let name = functionCall["name"] as? String ?? ""
agentService.sendEvent(name: "function_call", data: [
"name": name,
"arguments": functionCall
])
if name == "exit_interaction" {
agentService.stopRecognition()
}
}
func onFunctionCallResult(_ functionCall: [String: Any], _ functionCallResult: [String: Any]) {
guard let agentService = agentService else { return }
agentService.audioPlayer?.stopCallingSound()
agentService.sendEvent(name: "function_call_result", data: [
"function_call": functionCall,
"result": functionCallResult
])
metadata = agentService.autoHandleFunctionCallResult(functionCallResult)
}
}
extension AgentServiceImpl: TtsEventListener {
func onEvent(_ event: TtsEvent) {
switch event.type {
case .synthesisStarted:
isSpeaking = true
restartIdleCheck()
sendEvent(name: "tts_started", data: ["status": "started"])
case .synthesisCompleted:
isSpeaking = false
restartIdleCheck()
sendEvent(name: "tts_completed", data: ["status": "completed"])
case .synthesisCanceled:
isSpeaking = false
restartIdleCheck()
var data: [String: Any] = ["status": "canceled"]
if let reason = event.params["reason"] as? String {
data["reason"] = reason
}
if let errorDetails = event.params["errorDetails"] as? String {
data["details"] = errorDetails
}
sendEvent(name: "tts_canceled", data: data)
case .error:
isSpeaking = false
restartIdleCheck()
var data: [String: Any] = [:]
if let errorCode = event.params["errorCode"] as? String {
data["errorCode"] = errorCode
}
if let errorMessage = event.params["errorMessage"] as? String {
data["message"] = errorMessage
} else {
data["message"] = "TTS错误"
}
sendEvent(name: "error", data: data)
default:
break
}
}
}
extension AgentServiceImpl: BleService.Callback {
func onScanResult(devices: [[String: Any]]) {
}
func onConnectionStateChanged(state: Int) {
}
func onAudioDataReceived(data: Data) {
pushAudioData(data)
}
func onWakeupSignalReceived() {
stopTts()
stopRecognition()
startRecognition(useBle: true)
}
func onDeviceInfoReceived(infoType: Int, infoData: [String: Any]) {
}
}
class AudioPlayer {
private var player: AVAudioPlayer?
private var callingPlayer: AVAudioPlayer?
private let logger = OSLog(subsystem: "com.yunqiinnovation.agent_service", category: "AudioPlayer")
private let audioQueue = DispatchQueue(label: "agent.audio.player", qos: .userInitiated)
func playStartSound() {
playSound(named: "start", fileType: "mp3")
}
func playStopSound() {
playSound(named: "stop", fileType: "mp3")
}
func playCallingSound() {
playLoopSound(named: "calling", fileType: "mp3")
}
func stopCallingSound() {
callingPlayer?.stop()
callingPlayer = nil
}
private func playSound(named: String, fileType: String = "mp3") {
if let path = Bundle(for: type(of: self)).path(forResource: named, ofType: fileType) {
let url = URL(fileURLWithPath: path)
safePlayAudio(url: url)
return
}
os_log("未找到音频文件: %{public}@.%{public}@", log: logger, type: .error, named, fileType)
}
private func playLoopSound(named: String, fileType: String = "mp3") {
stopCallingSound()
if let path = Bundle(for: type(of: self)).path(forResource: named, ofType: fileType) {
let url = URL(fileURLWithPath: path)
safePlayLoopAudio(url: url)
return
}
os_log("未找到循环音频文件: %{public}@.%{public}@", log: logger, type: .error, named, fileType)
}
private func safePlayAudio(url: URL) {
do {
player?.stop()
player = nil
player = try AVAudioPlayer(contentsOf: url)
guard let player = player else {
return
}
player.volume = 0.7
player.prepareToPlay()
audioQueue.async { [weak self] in
guard let self = self else { return }
let playResult = player.play()
let playTime = player.duration + 0.5
Thread.sleep(forTimeInterval: playTime)
}
} catch {
os_log("播放音频失败: %{public}@", log: logger, type: .error, error.localizedDescription)
}
}
private func safePlayLoopAudio(url: URL) {
do {
callingPlayer = try AVAudioPlayer(contentsOf: url)
guard let callingPlayer = callingPlayer else {
return
}
callingPlayer.numberOfLoops = -1
callingPlayer.volume = 0.5
callingPlayer.prepareToPlay()
let playResult = callingPlayer.play()
} catch {
os_log("播放循环音频失败: %{public}@", log: logger, type: .error, error.localizedDescription)
}
}
}
extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback {
func onResult(_ text: String, _ detectedLanguage: String) {
if !text.isEmpty {
var data: [String: Any] = ["text": text]
if !detectedLanguage.isEmpty {
data["language"] = detectedLanguage
}
sendEvent(name: "recognition_result", data: data)
processTextInput(text, speakResponse: true)
}
let previousHasSpeech = hasSpeechDetected
hasSpeechDetected = false
if previousHasSpeech {
restartIdleCheck()
}
}
func onRecognizing(_ recognizing: String, _ detectedLanguage: String) {
if !recognizing.isEmpty {
let previousHasSpeech = hasSpeechDetected
hasSpeechDetected = true
if !previousHasSpeech {
restartIdleCheck()
}
var data: [String: Any] = ["text": recognizing]
if !detectedLanguage.isEmpty {
data["language"] = detectedLanguage
}
sendEvent(name: "recognizing", data: data)
if isSpeaking || isAiStreaming {
interruptCurrentResponse()
}
}
}
func onSessionStarted() {
sendEvent(name: "recognition_started", data: ["status": "started"])
isRecognizing = true
hasSpeechDetected = false
startIdleCheck()
audioPlayer?.playStartSound()
}
func onSessionStopped() {
sendEvent(name: "recognition_stopped", data: ["status": "stopped"])
isRecognizing = false
hasSpeechDetected = false
stopIdleCheck()
audioPlayer?.playStopSound()
}
func onCanceled(_ reason: String, _ errorDetails: String) {
var data: [String: Any] = [:]
if !reason.isEmpty {
data["reason"] = reason
}
if !errorDetails.isEmpty {
data["details"] = errorDetails
}
sendEvent(name: "recognition_canceled", data: data)
isRecognizing = false
stopIdleCheck()
}
func onError(_ error: String) {
let data: [String: Any] = ["message": error.isEmpty ? "未知错误" : error]
sendEvent(name: "error", data: data)
isRecognizing = false
stopIdleCheck()
}
}

11
local_plugins/agent_service/ios/Classes/AgentServiceListener.swift

@ -1,11 +0,0 @@
import Foundation
/// 代理服务事件监听器协议
/// 用于接收代理服务发送的各类事件
protocol AgentServiceListener: AnyObject {
/// 事件回调
/// - Parameters:
/// - eventName: 事件名称
/// - data: 事件数据
func onEvent(eventName: String, data: [String: Any])
}

2
local_plugins/agent_service/ios/Classes/AgentServicePlugin.h

@ -1,4 +1,4 @@
#import <Flutter/Flutter.h>
@interface AgentServicePlugin : NSObject<FlutterPlugin>
@end

2
local_plugins/agent_service/ios/Classes/AgentServicePlugin.m

@ -7,6 +7,6 @@
@implementation AgentServicePlugin
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
[SwiftAgentServicePlugin registerWithRegistrar:registrar];
[AgentServicePlugin registerWithRegistrar:registrar];
}
@end

160
local_plugins/agent_service/ios/Classes/AgentServicePlugin.swift

@ -0,0 +1,160 @@
import Flutter
import UIKit
import Foundation
import os.log
/// 代理服务Flutter插件,负责将Flutter调用桥接到原生实现
public class AgentServicePlugin: NSObject, FlutterPlugin {
/// 插件注册方法,Flutter引擎调用
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(name: "com.yunqiinnovation.agent_service", binaryMessenger: registrar.messenger())
let eventChannel = FlutterEventChannel(name: "com.yunqiinnovation.agent_service/events", binaryMessenger: registrar.messenger())
let instance = AgentServicePlugin()
registrar.addMethodCallDelegate(instance, channel: channel)
eventChannel.setStreamHandler(instance.eventHandler)
}
// MARK: - 属性
private let impl = AgentServiceImpl.shared
private lazy var eventHandler = AgentServiceEventHandler(impl: impl)
// MARK: - FlutterPlugin 实现
/// 处理Flutter发来的方法调用
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "startAgentService":
guard let arguments = call.arguments as? [String: Any],
let config = arguments["config"] as? [String: Any] else {
result(FlutterError(code: "INVALID_ARGUMENTS", message: "配置参数不能为空", details: nil))
return
}
let success = impl.initialize(config: config)
result(success)
case "stopAgentService":
let success = impl.dispose()
result(success)
case "startConversation":
impl.startRecognition()
result(true)
case "stopConversation":
impl.stopRecognition()
result(true)
case "processTextInput":
guard let arguments = call.arguments as? [String: Any],
let text = arguments["text"] as? String else {
result(FlutterError(code: "INVALID_ARGUMENTS", message: "文本不能为空", details: nil))
return
}
let speakResponse = arguments["speakResponse"] as? Bool ?? false
let success = impl.processTextInput(text, speakResponse: speakResponse)
result(success)
case "processImageInput":
guard let arguments = call.arguments as? [String: Any],
let imagePath = arguments["imagePath"] as? String else {
result(FlutterError(code: "INVALID_ARGUMENTS", message: "图片路径不能为空", details: nil))
return
}
let text = arguments["text"] as? String ?? ""
let speakResponse = arguments["speakResponse"] as? Bool ?? false
let success = impl.processImageInput(imagePath: imagePath, text: text, speakResponse: speakResponse)
result(success)
case "speakText":
guard let arguments = call.arguments as? [String: Any],
let text = arguments["text"] as? String else {
result(FlutterError(code: "INVALID_ARGUMENTS", message: "文本不能为空", details: nil))
return
}
let success = impl.speakText(text)
result(success)
case "stopTts":
let success = impl.stopTts()
result(success)
case "clearChatHistory":
let success = impl.clearChatHistory()
result(success)
case "interruptCurrentResponse":
let success = impl.interruptCurrentResponse()
result(success)
case "dispose":
let success = impl.dispose()
result(success)
default:
result(FlutterMethodNotImplemented)
}
}
}
/// 代理服务事件处理器,负责将原生事件传递给Flutter
class AgentServiceEventHandler: NSObject, FlutterStreamHandler {
private let impl: AgentServiceImpl
private var eventSink: FlutterEventSink?
private let logger = OSLog(subsystem: "com.yunqiinnovation.agent_service", category: "AgentServiceEventHandler")
init(impl: AgentServiceImpl) {
self.impl = impl
super.init()
}
/// 监听开始时调用
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
self.eventSink = events
// 添加实现的事件监听器
impl.addListener(self)
return nil
}
/// 监听取消时调用
func onCancel(withArguments arguments: Any?) -> FlutterError? {
impl.removeListener(self)
self.eventSink = nil
return nil
}
}
// 扩展事件处理器以实现AgentServiceListener接口
extension AgentServiceEventHandler: AgentServiceListener {
func onEvent(eventName: String, data: [String: Any]) {
// 确保在主线程上调用
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
// 将事件数据格式化为与Android版本一致的格式
let eventData: [String: Any] = [
"event": eventName,
"data": data
]
// 转换为JSON字符串发送,与Android版本保持一致
do {
let jsonData = try JSONSerialization.data(withJSONObject: eventData, options: [])
if let jsonString = String(data: jsonData, encoding: .utf8) {
self.eventSink?(jsonString)
} else {
os_log("无法创建JSON字符串", log: self.logger, type: .error)
}
} catch {
os_log("JSON序列化失败: %{public}@", log: self.logger, type: .error, error.localizedDescription)
}
}
}
}

240
local_plugins/agent_service/ios/Classes/AudioSessionHub.swift

@ -0,0 +1,240 @@
import AVFoundation
import os.log
public enum AudioIntent {
case voice
case playback
}
public class AudioSessionHub {
public static let shared = AudioSessionHub()
private let logger = OSLog(subsystem: "com.yunqiinnovation.agent_service", category: "AudioSessionHub")
private var currentIntent: AudioIntent?
private let audioSession = AVAudioSession.sharedInstance()
private var isInterrupted = false
private init() {
setupNotifications()
}
public func begin(_ intent: AudioIntent) {
if intent == currentIntent && audioSession.isOtherAudioPlaying == false {
return
}
currentIntent = intent
do {
switch intent {
case .voice:
try audioSession.setCategory(.playAndRecord,
mode: .voiceChat,
options: [.allowBluetooth, .mixWithOthers])
// 如果有蓝牙HFP设备,强制设置为首选输入
if let bluetoothInput = audioSession.availableInputs?.first(where: { $0.portType == .bluetoothHFP }) {
try audioSession.setPreferredInput(bluetoothInput)
os_log("已设置蓝牙HFP为首选输入: %{public}@", log: logger, type: .info, bluetoothInput.portName)
}
case .playback:
try audioSession.setCategory(.playback,
mode: .default,
options: [.allowBluetooth, .mixWithOthers])
}
try audioSession.setActive(true, options: .notifyOthersOnDeactivation)
os_log("音频会话已设置为:%{public}@", log: logger, type: .debug, String(describing: intent))
// 输出当前音频路由信息
let hasBluetoothDevice = hasBluetoothAudioDevice()
let hasBluetoothMic = hasBluetoothMicrophone()
let currentInput = getCurrentInputDevice()
os_log("蓝牙音频设备状态: %{public}@", log: logger, type: .info, hasBluetoothDevice ? "已连接" : "未连接")
os_log("蓝牙麦克风状态: %{public}@", log: logger, type: .info, hasBluetoothMic ? "已连接" : "未连接")
os_log("当前输入设备: %{public}@", log: logger, type: .info, currentInput)
if hasBluetoothDevice {
os_log("%{public}@", log: logger, type: .debug, getCurrentAudioRoute())
}
} catch {
os_log("设置音频会话失败:%{public}@", log: logger, type: .error, error.localizedDescription)
}
}
public func appWillEnterForeground() {
if let intent = currentIntent {
begin(intent)
}
}
public func appDidEnterBackground() {
do {
try audioSession.setActive(false, options: .notifyOthersOnDeactivation)
os_log("音频会话已释放(应用进入后台)", log: logger, type: .debug)
} catch {
os_log("释放音频会话失败:%{public}@", log: logger, type: .error, error.localizedDescription)
}
}
/// 获取当前音频路由信息
public func getCurrentAudioRoute() -> String {
let currentRoute = audioSession.currentRoute
var routeInfo = "当前音频路由:\n"
// 输出设备
routeInfo += "输出设备:\n"
for output in currentRoute.outputs {
routeInfo += " - \(output.portName) (\(output.portType.rawValue))\n"
}
// 输入设备
routeInfo += "输入设备:\n"
for input in currentRoute.inputs {
routeInfo += " - \(input.portName) (\(input.portType.rawValue))\n"
}
return routeInfo
}
/// 检查是否有蓝牙音频设备连接
public func hasBluetoothAudioDevice() -> Bool {
let currentRoute = audioSession.currentRoute
// 检查输出设备
for output in currentRoute.outputs {
if output.portType == .bluetoothA2DP ||
output.portType == .bluetoothHFP ||
output.portType == .bluetoothLE {
return true
}
}
// 检查输入设备(这是关键!)
for input in currentRoute.inputs {
if input.portType == .bluetoothHFP {
return true
}
}
return false
}
/// 检查是否有蓝牙麦克风作为输入设备
public func hasBluetoothMicrophone() -> Bool {
let currentRoute = audioSession.currentRoute
for input in currentRoute.inputs {
if input.portType == .bluetoothHFP {
return true
}
}
return false
}
/// 获取当前输入设备信息
public func getCurrentInputDevice() -> String {
let currentRoute = audioSession.currentRoute
if let input = currentRoute.inputs.first {
return "\(input.portName) (\(input.portType.rawValue))"
}
return "无输入设备"
}
private func setupNotifications() {
NotificationCenter.default.addObserver(
self,
selector: #selector(handleAudioSessionInterruption),
name: AVAudioSession.interruptionNotification,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(handleAudioRouteChange),
name: AVAudioSession.routeChangeNotification,
object: nil
)
}
/// 处理音频中断
@objc private func handleAudioSessionInterruption(notification: Notification) {
guard let userInfo = notification.userInfo,
let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
let type = AVAudioSession.InterruptionType(rawValue: typeValue) else {
return
}
switch type {
case .began:
// 中断开始
isInterrupted = true
os_log("音频会话被中断", log: logger, type: .debug)
case .ended:
// 中断结束
isInterrupted = false
// 检查是否应该恢复
if let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt {
let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
if options.contains(.shouldResume), let intent = currentIntent {
// 恢复之前的音频意图
begin(intent)
os_log("音频会话中断结束,已恢复", log: logger, type: .debug)
}
}
@unknown default:
os_log("未知的音频中断类型: %{public}@", log: logger, type: .error, "\(type)")
}
}
/// 处理音频路由变化
@objc private func handleAudioRouteChange(notification: Notification) {
guard let userInfo = notification.userInfo,
let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt,
let reason = AVAudioSession.RouteChangeReason(rawValue: reasonValue) else {
return
}
switch reason {
case .newDeviceAvailable:
os_log("新音频设备已连接", log: logger, type: .info)
os_log("%{public}@", log: logger, type: .debug, getCurrentAudioRoute())
// 重新应用当前意图以确保正确路由
if let intent = currentIntent {
begin(intent)
}
case .oldDeviceUnavailable:
os_log("音频设备已断开", log: logger, type: .info)
os_log("%{public}@", log: logger, type: .debug, getCurrentAudioRoute())
// 重新应用当前意图以确保正确路由
if let intent = currentIntent {
begin(intent)
}
case .categoryChange:
os_log("音频类别已变化", log: logger, type: .info)
default:
break
}
}
deinit {
NotificationCenter.default.removeObserver(self)
}
}

259
local_plugins/agent_service/ios/Classes/SwiftAgentServicePlugin.swift

@ -1,259 +0,0 @@
import Flutter
import UIKit
import open_ai_service
import chat_storage
/// Swift版Agent Service插件
/// 处理Flutter与原生层的通信
public class SwiftAgentServicePlugin: NSObject, FlutterPlugin {
// Flutter方法通道
private var methodChannel: FlutterMethodChannel?
// Flutter事件通道
private var eventChannel: FlutterEventChannel?
// 事件sink,用于向Flutter发送事件
private var eventSink: FlutterEventSink?
// 核心服务实例
private var agentService: AgentService?
/// 注册插件
public static func register(with registrar: FlutterPluginRegistrar) {
let instance = SwiftAgentServicePlugin()
// 初始化方法通道
let methodChannel = FlutterMethodChannel(name: "com.yunqiinnovation.agent_service", binaryMessenger: registrar.messenger())
registrar.addMethodCallDelegate(instance, channel: methodChannel)
instance.methodChannel = methodChannel
// 初始化事件通道
let eventChannel = FlutterEventChannel(name: "com.yunqiinnovation.agent_service/events", binaryMessenger: registrar.messenger())
eventChannel.setStreamHandler(instance)
instance.eventChannel = eventChannel
}
/// 处理方法调用
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "initialize":
handleInitialize(call, result)
case "startConversation":
handleStartConversation(result)
case "stopConversation":
handleStopConversation(result)
case "processTextInput":
handleProcessTextInput(call, result)
case "speakText":
handleSpeakText(call, result)
case "stopTts":
handleStopTts(result)
case "clearChatHistory":
handleClearChatHistory(result)
case "interruptCurrentResponse":
handleInterruptCurrentResponse(result)
case "processImageInput":
handleProcessImageInput(call, result)
case "dispose":
handleDispose(result)
default:
result(FlutterMethodNotImplemented)
}
}
// MARK: - 方法处理
/// 初始化
private func handleInitialize(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
guard let args = call.arguments as? [String: Any],
let config = args["config"] as? [String: Any] else {
result(FlutterError(code: "INVALID_ARGUMENTS", message: "缺少配置参数", details: nil))
return
}
// 初始化代理服务
if agentService == nil {
agentService = AgentService()
// 设置事件监听器
agentService?.setListener(self)
}
// 执行初始化
let success = agentService?.initialize(config: config) ?? false
result(success)
}
/// 开始对话
private func handleStartConversation(_ result: @escaping FlutterResult) {
guard let service = agentService else {
result(FlutterError(code: "NOT_INITIALIZED", message: "AgentService尚未初始化", details: nil))
return
}
let success = service.startRecognition()
result(success)
}
/// 停止对话
private func handleStopConversation(_ result: @escaping FlutterResult) {
guard let service = agentService else {
result(FlutterError(code: "NOT_INITIALIZED", message: "AgentService尚未初始化", details: nil))
return
}
service.stopRecognition()
result(true)
}
/// 处理文本输入
private func handleProcessTextInput(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
guard let service = agentService else {
result(FlutterError(code: "NOT_INITIALIZED", message: "AgentService尚未初始化", details: nil))
return
}
guard let args = call.arguments as? [String: Any],
let text = args["text"] as? String else {
result(FlutterError(code: "INVALID_ARGUMENTS", message: "无效的文本参数", details: nil))
return
}
let speakResponse = (args["speakResponse"] as? Bool) ?? false
let success = service.processTextInput(text: text, speakResponse: speakResponse)
result(success)
}
/// 朗读文本
private func handleSpeakText(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
guard let service = agentService else {
result(FlutterError(code: "NOT_INITIALIZED", message: "AgentService尚未初始化", details: nil))
return
}
guard let args = call.arguments as? [String: Any],
let text = args["text"] as? String else {
result(FlutterError(code: "INVALID_ARGUMENTS", message: "无效的文本参数", details: nil))
return
}
service.speakText(text: text)
result(true)
}
/// 停止语音合成
private func handleStopTts(_ result: @escaping FlutterResult) {
guard let service = agentService else {
result(FlutterError(code: "NOT_INITIALIZED", message: "AgentService尚未初始化", details: nil))
return
}
service.stopTts()
result(true)
}
/// 清除聊天历史
private func handleClearChatHistory(_ result: @escaping FlutterResult) {
guard let service = agentService else {
result(FlutterError(code: "NOT_INITIALIZED", message: "AgentService尚未初始化", details: nil))
return
}
service.clearChatHistory { success in
result(success)
}
}
/// 中断当前响应
private func handleInterruptCurrentResponse(_ result: @escaping FlutterResult) {
guard let service = agentService else {
result(FlutterError(code: "NOT_INITIALIZED", message: "AgentService尚未初始化", details: nil))
return
}
service.interruptCurrentResponse()
result(true)
}
/// 处理图片输入
private func handleProcessImageInput(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
guard let service = agentService else {
result(FlutterError(code: "NOT_INITIALIZED", message: "AgentService尚未初始化", details: nil))
return
}
guard let args = call.arguments as? [String: Any],
let imagePath = args["imagePath"] as? String else {
result(FlutterError(code: "INVALID_ARGUMENTS", message: "无效的图片路径参数", details: nil))
return
}
let text = (args["text"] as? String) ?? ""
let speakResponse = (args["speakResponse"] as? Bool) ?? false
let success = service.processImageInput(imagePath: imagePath, text: text, speakResponse: speakResponse)
result(success)
}
/// 释放资源
private func handleDispose(_ result: @escaping FlutterResult) {
guard let service = agentService else {
result(true) // 已经是nil,直接返回成功
return
}
service.dispose()
agentService = nil
result(true)
}
}
// MARK: - FlutterStreamHandler
extension SwiftAgentServicePlugin: FlutterStreamHandler {
public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
self.eventSink = events
return nil
}
public func onCancel(withArguments arguments: Any?) -> FlutterError? {
self.eventSink = nil
return nil
}
}
// MARK: - AgentServiceListener
extension SwiftAgentServicePlugin: AgentServiceListener {
func onEvent(eventName: String, data: [String: Any]) {
guard let sink = eventSink else { return }
do {
// 构建事件JSON
let eventMap: [String: Any] = [
"event": eventName,
"data": data
]
// 将事件转换为JSON字符串
let jsonData = try JSONSerialization.data(withJSONObject: eventMap, options: [])
if let jsonString = String(data: jsonData, encoding: .utf8) {
// 发送到Flutter
DispatchQueue.main.async {
sink(jsonString)
}
}
} catch {
print("事件序列化失败: \(error.localizedDescription)")
}
}
}

BIN
local_plugins/agent_service/ios/Resources/calling.mp3

Binary file not shown.

BIN
local_plugins/agent_service/ios/Resources/start.mp3

Binary file not shown.

BIN
local_plugins/agent_service/ios/Resources/stop.mp3

Binary file not shown.

29
local_plugins/agent_service/ios/agent_service.podspec

@ -0,0 +1,29 @@
#
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
# Run `pod lib lint agent_service.podspec` to validate before publishing.
#
Pod::Spec.new do |s|
s.name = 'agent_service'
s.version = '0.0.1'
s.summary = '智能助手代理服务'
s.description = <<-DESC
智能助手代理服务,提供语音识别、语音合成和OpenAI对话功能
DESC
s.homepage = 'http://example.com'
s.license = { :file => '../LICENSE' }
s.author = { 'Your Company' => 'email@example.com' }
s.source = { :path => '.' }
s.source_files = 'Classes/**/*.swift'
s.resources = ['Resources/*.mp3']
s.dependency 'Flutter'
s.dependency 'azure_speech'
s.dependency 'ble_service'
s.dependency 'open_ai_service'
s.dependency 'chat_storage'
s.platform = :ios, '13.0'
# Flutter.framework does not contain a i386 slice. Only x86_64 simulators are supported.
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' }
s.swift_version = '5.0'
end

1
local_plugins/agent_service/lib/agent_service.dart

@ -108,7 +108,6 @@ class AgentService {
final Map<String, dynamic> eventMap = jsonDecode(event);
final String eventName = eventMap['event'];
final Map<String, dynamic> eventData = eventMap['data'];
// 将事件名称转换为枚举
final AgentServiceEventType type = _stringToEventType(eventName);

15
local_plugins/agent_service/pubspec.yaml

@ -10,6 +10,17 @@ environment:
dependencies:
flutter:
sdk: flutter
# 本地插件依赖
open_ai_service:
path: ../open_ai_service
azure_speech:
path: ../azure_speech
ble_service:
path: ../ble_service
speech:
path: ../speech
chat_storage:
path: ../chat_storage
dev_dependencies:
flutter_test:
@ -22,4 +33,6 @@ flutter:
platforms:
android:
package: com.yunqiinnovation.agent_service
pluginClass: AgentServicePlugin
pluginClass: AgentServicePlugin
ios:
pluginClass: AgentServicePlugin

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

@ -286,7 +286,7 @@ class AzureTtsHelper(private val context: Context) : ITtsService {
private fun generateSsml(rawText: String): String {
// 1. 定义要静音的符号和表情符号列表
val symbolsToMute = listOf(
"#", "*", "@", "%", "^", "&",
"#", "*",
"😀", "😂", "😊", "😍", "😢", "😎", "😉", "👍", "🙌", "🎉"
)

34
local_plugins/azure_speech/ios/.gitignore

@ -1,34 +0,0 @@
**/dgph
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3

888
local_plugins/azure_speech/ios/Classes/AzureAsrHelper.swift

File diff suppressed because it is too large

17
local_plugins/azure_speech/ios/Classes/AzureSpeechPlugin.h

@ -0,0 +1,17 @@
//
// AzureSpeechPlugin.h
// azure_speech
//
// Created for azure_speech plugin compatibility.
//
#import <Flutter/Flutter.h>
/**
* Azure Speech Plugin 头文件
*
* 注:实际实现使用 Swift,此头文件仅用于 Flutter 框架兼容
*/
@interface AzureSpeechPlugin : NSObject <FlutterPlugin>
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar;
@end

588
local_plugins/azure_speech/ios/Classes/AzureSpeechPlugin.swift

@ -1,182 +1,422 @@
import Flutter
import UIKit
import AVFoundation
import MicrosoftCognitiveServicesSpeech
// 自定义语音处理组件,提供音频流处理等功能
import speech
import os.log
public class AzureSpeechPlugin: NSObject, FlutterPlugin {
private var ttsHelper: AzureTtsHelper?
private var asrHelper: AzureAsrHelper?
private var eventSink: FlutterEventSink?
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(name: "azure_speech", binaryMessenger: registrar.messenger())
let instance = AzureSpeechPlugin()
registrar.addMethodCallDelegate(instance, channel: channel)
// 初始化事件通道
let eventChannel = FlutterEventChannel(name: "azure_speech/asr_events", binaryMessenger: registrar.messenger())
eventChannel.setStreamHandler(AsrStreamHandler(instance: instance))
}
override init() {
super.init()
ttsHelper = AzureTtsHelper()
asrHelper = AzureAsrHelper()
// 设置ASR事件处理
asrHelper?.setEventHandler { [weak self] event in
self?.handleAsrEvent(event)
}
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
// TTS相关方法
case "initializeTts":
guard let args = call.arguments as? [String: Any],
let subscriptionKey = args["subscriptionKey"] as? String,
let serviceRegion = args["serviceRegion"] as? String else {
result(false)
return
}
let language = args["language"] as? String ?? "zh-CN"
let success = ttsHelper?.initialize(speechSubscriptionKey: subscriptionKey, serviceRegion: serviceRegion, language: language) ?? false
result(success)
case "setTtsVoice":
guard let args = call.arguments as? [String: Any],
let voiceName = args["voiceName"] as? String else {
result(false)
return
}
let success = ttsHelper?.setVoice(voiceName: voiceName) ?? false
result(success)
case "setTtsSpeechParams":
guard let args = call.arguments as? [String: Any],
let rate = args["rate"] as? Int,
let pitch = args["pitch"] as? Int,
let volume = args["volume"] as? Int else {
result(false)
return
}
let success = ttsHelper?.setSpeechParams(rate: rate, pitch: pitch, volume: volume) ?? false
result(success)
case "speakText":
guard let args = call.arguments as? [String: Any],
let text = args["text"] as? String else {
result(false)
return
}
ttsHelper?.speakText(text: text) { success, _ in
result(success)
}
case "stopSpeaking":
let success = ttsHelper?.stopSpeaking() ?? false
result(success)
case "isSpeaking":
let speaking = ttsHelper?.isSpeaking() ?? false
result(speaking)
case "setTtsAudioOutputType":
guard let args = call.arguments as? [String: Any],
let outputType = args["outputType"] as? String else {
result(false)
return
}
var type: AzureTtsHelper.AudioOutputType = .auto
switch outputType.uppercased() {
case "SPEAKER":
type = .speaker
case "EARPIECE":
type = .earpiece
default:
type = .auto
}
let success = ttsHelper?.setAudioOutputType(outputType: type) ?? false
result(success)
// ASR相关方法
case "initializeAsr":
guard let args = call.arguments as? [String: Any],
let subscriptionKey = args["subscriptionKey"] as? String,
let serviceRegion = args["serviceRegion"] as? String,
let supportedLanguages = args["supportedLanguages"] as? [String] else {
result(false)
return
}
let success = asrHelper?.initialize(speechSubscriptionKey: subscriptionKey, serviceRegion: serviceRegion, supportedLanguages: supportedLanguages) ?? false
result(success)
case "recognizeOnce":
asrHelper?.recognizeOnce { success, text, language, error in
var resultMap: [String: Any] = ["success": success]
if success {
resultMap["text"] = text
resultMap["language"] = language
/**
* Azure Speech Plugin
*
* 基于微软Azure语音服务的Flutter插件
* 提供语音识别(ASR)和语音合成(TTS)功能
*/
@objc public class AzureSpeechPlugin: NSObject, FlutterPlugin {
// 日志标签
private let tag = "AzureSpeechPlugin"
// 日志对象
private let log = OSLog(subsystem: "com.azure.speech", category: "AzureSpeechPlugin")
// ASR相关
private var asrChannel: FlutterMethodChannel?
private var asrEventChannel: FlutterEventChannel?
private var asrEventSink: FlutterEventSink?
private let azureAsrHelper = AzureAsrHelper()
// TTS相关
private var ttsChannel: FlutterMethodChannel?
private var ttsEventChannel: FlutterEventChannel?
private var ttsEventSink: FlutterEventSink?
private let azureTtsHelper = AzureTtsHelper()
// 是否已添加TTS事件监听器
private var isTtsListenerAdded = false
// 当前的连续识别回调
private var currentAsrCallback: AsrCallbackWrapper?
// 插件注册
public static func register(with registrar: FlutterPluginRegistrar) {
let instance = AzureSpeechPlugin()
// 初始化ASR通道
let asrChannel = FlutterMethodChannel(name: "azure_speech/asr", binaryMessenger: registrar.messenger())
registrar.addMethodCallDelegate(instance, channel: asrChannel)
instance.asrChannel = asrChannel
// 初始化TTS通道
let ttsChannel = FlutterMethodChannel(name: "azure_speech/tts", binaryMessenger: registrar.messenger())
registrar.addMethodCallDelegate(instance, channel: ttsChannel)
instance.ttsChannel = ttsChannel
// 初始化ASR事件通道
let asrEventChannel = FlutterEventChannel(name: "azure_speech/asr_events", binaryMessenger: registrar.messenger())
asrEventChannel.setStreamHandler(instance)
instance.asrEventChannel = asrEventChannel
// 初始化TTS事件通道
let ttsEventChannel = FlutterEventChannel(name: "azure_speech/tts_events", binaryMessenger: registrar.messenger())
ttsEventChannel.setStreamHandler(instance)
instance.ttsEventChannel = ttsEventChannel
}
// 发送ASR事件方法
internal func sendAsrEvent(_ event: [String: Any]) {
if asrEventSink == nil {
os_log("无法发送ASR事件:事件通道未准备好", log: log, type: .error)
return
}
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.asrEventSink?(event)
}
}
// 发送TTS事件方法
private func sendTtsEvent(_ event: [String: Any]) {
if ttsEventSink == nil {
os_log("无法发送TTS事件:事件通道未准备好", log: log, type: .error)
return
}
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.ttsEventSink?(event)
}
}
// 设置TTS事件监听器
private func setupTtsEventListener() {
if !isTtsListenerAdded {
azureTtsHelper.addListener(self)
isTtsListenerAdded = true
}
}
// 处理Flutter方法调用
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
if call.method.hasPrefix("tts_") {
handleTtsMethodCall(call, result)
} else {
handleAsrMethodCall(call, result)
}
}
// MARK: - ASR 方法处理
private func handleAsrMethodCall(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
switch call.method {
case "initialize":
guard let args = call.arguments as? [String: Any],
let subscriptionKey = args["subscriptionKey"] as? String,
let region = args["region"] as? String else {
result(FlutterError(code: "INVALID_ARGUMENTS", message: "必要的参数不能为空", details: nil))
return
}
let supportedLanguages = args["supportedLanguages"] as? [String] ?? ["zh-CN"]
// 初始化ASR引擎
let success = azureAsrHelper.initialize(
subscriptionKey: subscriptionKey,
region: region,
supportedLanguages: supportedLanguages,
audioSourceType: .microphone
)
result(success)
case "recognizeOnce":
// iOS版本不支持recognizeOnce
result(FlutterError(code: "NOT_SUPPORTED", message: "iOS版本不支持recognizeOnce", details: nil))
case "startContinuousRecognition":
// 确保事件通道已准备好
guard asrEventSink != nil else {
result(FlutterError(code: "EVENT_CHANNEL_NOT_READY", message: "事件通道未准备好,无法开始连续识别", details: nil))
return
}
// 解析音频源类型
let audioSourceType: AzureAsrHelper.AudioSourceType
if let args = call.arguments as? [String: Any],
let audioSourceString = args["audioSourceType"] as? String {
switch audioSourceString.lowercased() {
case "external":
audioSourceType = .external
default:
audioSourceType = .microphone
}
} else {
audioSourceType = .microphone
}
// 创建回调包装器
currentAsrCallback = AsrCallbackWrapper(plugin: self)
let success = azureAsrHelper.startContinuousRecognition(
callback: currentAsrCallback!,
audioSourceType: audioSourceType
)
result(success)
case "stopContinuousRecognition":
let success = azureAsrHelper.stopContinuousRecognition()
currentAsrCallback = nil
result(success)
case "isContinuousRecognitionActive":
result(azureAsrHelper.isContinuousRecognitionActive())
case "dispose":
azureAsrHelper.dispose()
currentAsrCallback = nil
result(true)
case "pushAudioData":
guard let args = call.arguments as? [String: Any],
let audioBytes = args["data"] as? FlutterStandardTypedData else {
result(FlutterError(code: "INVALID_ARGUMENTS", message: "音频数据不能为空", details: nil))
return
}
azureAsrHelper.pushAudioData(data: audioBytes.data)
result(true)
default:
result(FlutterMethodNotImplemented)
}
}
// MARK: - TTS 方法处理
private func handleTtsMethodCall(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
switch call.method {
case "tts_initialize":
guard let args = call.arguments as? [String: Any],
let subscriptionKey = args["subscriptionKey"] as? String,
let region = args["region"] as? String else {
result(FlutterError(code: "INVALID_ARGUMENTS", message: "必要的参数不能为空", details: nil))
return
}
// 初始化TTS引擎
let language = args["language"] as? String ?? "zh-CN"
let success = azureTtsHelper.initialize(ttsAppId: "", ttsAppToken: subscriptionKey, ttsResource: region, language: language)
// 设置TTS事件监听器
setupTtsEventListener()
result(success)
case "tts_set_voice":
guard let args = call.arguments as? [String: Any],
let voiceName = args["voiceName"] as? String else {
result(FlutterError(code: "INVALID_ARGUMENTS", message: "语音名称不能为空", details: nil))
return
}
let success = azureTtsHelper.setVoice(voiceName)
result(success)
case "tts_speak_once":
guard let args = call.arguments as? [String: Any],
let text = args["text"] as? String else {
result(FlutterError(code: "INVALID_ARGUMENTS", message: "文本不能为空", details: nil))
return
}
let success = azureTtsHelper.speakOnce(text)
result(success)
case "tts_speak_stream":
guard let args = call.arguments as? [String: Any],
let text = args["text"] as? String else {
result(FlutterError(code: "INVALID_ARGUMENTS", message: "文本不能为空", details: nil))
return
}
let success = azureTtsHelper.speakStream(text)
result(success)
case "tts_flush_stream":
let success = azureTtsHelper.flushStream()
result(success)
case "tts_stop":
let success = azureTtsHelper.stop()
result(success)
case "tts_isSpeaking":
result(azureTtsHelper.isSpeaking())
case "tts_release":
azureTtsHelper.dispose()
result(true)
default:
result(FlutterMethodNotImplemented)
}
}
}
// MARK: - ASR回调包装器
private class AsrCallbackWrapper: AzureAsrHelper.ContinuousRecognizeCallback {
private weak var plugin: AzureSpeechPlugin?
init(plugin: AzureSpeechPlugin) {
self.plugin = plugin
}
func onResult(_ text: String, _ detectedLanguage: String) {
plugin?.sendAsrEvent([
"type": "result",
"text": text,
"language": detectedLanguage
])
}
func onRecognizing(_ recognizing: String, _ detectedLanguage: String) {
plugin?.sendAsrEvent([
"type": "recognizing",
"text": recognizing,
"language": detectedLanguage
])
}
func onSessionStarted() {
plugin?.sendAsrEvent([
"type": "sessionStarted"
])
}
func onSessionStopped() {
plugin?.sendAsrEvent([
"type": "sessionStopped"
])
}
func onCanceled(_ reason: String, _ errorDetails: String) {
plugin?.sendAsrEvent([
"type": "canceled",
"reason": reason,
"errorDetails": errorDetails
])
}
func onError(_ error: String) {
plugin?.sendAsrEvent([
"type": "error",
"error": error
])
}
}
// MARK: - 事件处理
extension AzureSpeechPlugin: FlutterStreamHandler {
public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
// 根据通道类型设置事件接收器
if let args = arguments as? [String: Any],
let channel = args["channel"] as? String {
if channel == "asr" {
asrEventSink = events
} else if channel == "tts" {
ttsEventSink = events
setupTtsEventListener()
}
} else if arguments == nil {
// 如果没有指定通道,尝试弄清楚是哪个通道在监听
if asrEventSink == nil && ttsEventSink != nil {
asrEventSink = events
} else if asrEventSink != nil && ttsEventSink == nil {
ttsEventSink = events
setupTtsEventListener()
} else {
// 无法确定哪个通道,默认设置为ASR
asrEventSink = events
}
}
return nil
}
public func onCancel(withArguments arguments: Any?) -> FlutterError? {
// 根据通道类型清除事件接收器
if let args = arguments as? [String: Any],
let channel = args["channel"] as? String {
if channel == "asr" {
asrEventSink = nil
} else if channel == "tts" {
ttsEventSink = nil
}
} else {
resultMap["error"] = error
// 如果没有指定通道,清除所有通道
asrEventSink = nil
ttsEventSink = nil
}
result(resultMap)
}
case "startContinuousRecognition":
let success = asrHelper?.startContinuousRecognition() ?? false
result(success)
case "stopContinuousRecognition":
let success = asrHelper?.stopContinuousRecognition() ?? false
result(success)
case "isContinuousRecognitionActive":
let isActive = asrHelper?.isContinuousRecognitionActive() ?? false
result(isActive)
case "dispose":
ttsHelper?.dispose()
asrHelper?.dispose()
result(nil)
default:
result(FlutterMethodNotImplemented)
}
}
// 设置事件接收器
func setEventSink(_ sink: FlutterEventSink?) {
self.eventSink = sink
}
// 处理ASR事件
private func handleAsrEvent(_ event: [String: Any]) {
self.eventSink?(event)
}
return nil
}
}
// ASR事件流处理器
class AsrStreamHandler: NSObject, FlutterStreamHandler {
private weak var plugin: AzureSpeechPlugin?
init(instance: AzureSpeechPlugin) {
self.plugin = instance
super.init()
}
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
plugin?.setEventSink(events)
return nil
}
func onCancel(withArguments arguments: Any?) -> FlutterError? {
plugin?.setEventSink(nil)
return nil
}
// MARK: - TTS 事件监听实现
extension AzureSpeechPlugin: TtsEventListener {
// 使用@objc特性为方法提供一个不同的Objective-C选择器名称
@objc(onTtsEvent:)
public func onEvent(_ event: TtsEvent) {
var eventMap: [String: Any] = [:]
// 根据事件类型转换
switch event.type {
case .synthesisStarted:
eventMap["type"] = "synthesis_started"
case .synthesisCompleted:
eventMap["type"] = "synthesis_completed"
case .synthesisCanceled:
eventMap["type"] = "synthesis_canceled"
if let reason = event.params["reason"] {
eventMap["reason"] = reason
}
if let errorDetails = event.params["errorDetails"] {
eventMap["errorDetails"] = errorDetails
}
case .error:
eventMap["type"] = "error"
if let errorCode = event.params["errorCode"] {
eventMap["errorCode"] = errorCode
}
if let errorMessage = event.params["errorMessage"] {
eventMap["errorMessage"] = errorMessage
}
@unknown default:
eventMap["type"] = "unknown"
for (key, value) in event.params {
eventMap[key] = value
}
}
sendTtsEvent(eventMap)
}
}
// MARK: - 音频数据监听实现
extension AzureSpeechPlugin: AudioDataListener {
public func onAudioData(_ data: Data) {
if ttsEventSink == nil { return }
let eventMap: [String: Any] = [
"type": "audio_data",
"data": FlutterStandardTypedData(bytes: data)
]
sendTtsEvent(eventMap)
}
}

776
local_plugins/azure_speech/ios/Classes/AzureTtsHelper.swift

@ -1,315 +1,248 @@
import Foundation
import AVFoundation
import MicrosoftCognitiveServicesSpeech
// 自定义语音处理组件,提供音频流处理等功能
import speech
import os.log
/// Azure 语音合成辅助类
class AzureTtsHelper: NSObject {
// 常量定义
/**
* Azure TTS Helper
*
* 基于微软Azure语音服务的TTS实现
* 参考文档: https://learn.microsoft.com/en-us/azure/ai-services/speech-service/how-to-speech-synthesis
*
* 特性:
* - 对外接口保持同步,内部异步处理
* - 使用专用队列保证合成顺序
* - 避免阻塞主线程和其他后台线程
* - 事件回调由调用方处理线程切换
*
* 使用示例:
* let success = ttsHelper.speakOnce("Hello World") // 立即返回,后台异步处理
*/
public class AzureTtsHelper: NSObject, ITtsService {
private let tag = "AzureTtsHelper"
private let DEFAULT_LANGUAGE = "zh-CN"
private let DEFAULT_VOICE = "zh-CN-XiaoxiaoNeural"
// 日志对象
private let log = OSLog(subsystem: "com.azure.speech", category: "AzureTtsHelper")
// 核心组件
private var synthesizer: SPXSpeechSynthesizer?
private static let DEFAULT_LANGUAGE = "zh-CN"
private static let DEFAULT_VOICE = "zh-CN-XiaoxiaoNeural"
// Azure语音服务配置
private var speechConfig: SPXSpeechConfiguration?
private var audioConfig: SPXAudioConfiguration?
private var initialized = false
private var synthesizer: SPXSpeechSynthesizer?
private var isInitialized = false
private var speaking = false
// 当前设置
private var currentVoice = "zh-CN-XiaoxiaoNeural"
private var currentRate = "0%"
private var currentPitch = "0%"
// 当前配置
private var currentVoice = DEFAULT_VOICE
private var currentLanguage = DEFAULT_LANGUAGE
private var currentRate = "+0%"
private var currentPitch = "+0%"
private var currentVolume = "100%"
// 音频会话管理
private let audioSession = AVAudioSession.sharedInstance()
// 自定义音频输出流
private var customAudioOutputStream: SPXPushAudioOutputStream?
// 事件监听器列表
private var eventListeners = NSHashTable<AnyObject>.weakObjects()
// 流式文本处理的缓冲区
private var streamBuffer = ""
private var lastSpeakTime: TimeInterval = 0
// 应用上下文
private let context: Any
// 内部异步处理队列,保证顺序执行
private let synthesisQueue = DispatchQueue(label: "com.azure.tts.synthesis", qos: .userInitiated)
private let synthesisGroup = DispatchGroup()
private var pendingTasks: [() -> Void] = []
private let taskLock = NSLock()
// 全局回调
private var ttsCallback: TtsCallback?
/// TTS回调接口
protocol TtsCallback {
/// 合成开始
func onSynthesisStarted()
/// 合成中
func onSynthesizing()
/// 合成完成
func onSynthesisCompleted()
/// 合成取消
func onSynthesisCanceled()
}
/// 初始化
/// - Parameter context: 应用上下文
init(_ context: Any) {
self.context = context
super.init()
}
/// 初始化 TTS 引擎
/// - Parameters:
/// - subscriptionKey: Azure 语音服务订阅密钥
/// - region: Azure 语音服务区域
/// - callback: 可选,全局TTS回调接口
/// - Returns: 是否初始化成功
func initialize(
subscriptionKey: String,
region: String,
callback: TtsCallback? = nil
) -> Bool {
print("\(tag): 初始化 Azure 语音服务")
// 检查配置是否为空
if subscriptionKey.isEmpty || region.isEmpty {
print("\(tag): 错误: Azure 配置信息不完整")
return false
}
// 释放之前的资源
dispose()
// 保存全局回调
ttsCallback = callback
/**
* 初始化语音合成服务
*
* @param appId 服务应用ID
* @param token 服务访问令牌/订阅密钥
* @param resource 服务资源ID/区域(可选)
* @param language 语言代码,如"zh-CN"
* @return 是否初始化成功
*/
public func initialize(ttsAppId: String, ttsAppToken: String, ttsResource: String, language: String) -> Bool {
do {
// 创建语音配置
speechConfig = try SPXSpeechConfiguration(subscription: subscriptionKey, region: region)
// 设置语音合成输出格式为高质量音频
speechConfig?.setSpeechSynthesisOutputFormat(.riff24Khz16BitMonoPcm)
// 设置默认语音
speechConfig?.setSpeechSynthesisVoiceName(currentVoice)
// 创建音频配置
if let customOutput = customAudioOutputStream {
// 使用自定义音频输出流
audioConfig = try SPXAudioConfiguration(streamOutput: customOutput)
if ttsResource.isEmpty {
speechConfig = try SPXSpeechConfiguration(subscription: ttsAppToken, region: "eastasia")
} else {
// 使用默认扬声器
audioConfig = SPXAudioConfiguration()
speechConfig = try SPXSpeechConfiguration(subscription: ttsAppToken, region: ttsResource)
}
// 创建语音合成器
synthesizer = try SPXSpeechSynthesizer(speechConfiguration: speechConfig!, audioConfiguration: audioConfig!)
// 设置语言
speechConfig?.speechSynthesisLanguage = language
currentLanguage = language
// 设置事件监听
setupEventListeners()
// 设置音频输出格式 - 使用16k、16位的PCM格式
speechConfig?.setPropertyTo("Audio16Khz16BitMonoPcm",
byName: "SpeechServiceConnection_SynthOutputFormat")
initialized = true
print("\(tag): TTS 引擎初始化成功")
return true
} catch {
print("\(tag): TTS 引擎初始化失败: \(error.localizedDescription)")
return false
}
}
/// 设置事件监听器
private func setupEventListeners() {
guard let synthesizer = synthesizer else { return }
// 合成开始事件
synthesizer.addSynthesisStarted { [weak self] _, _ in
guard let self = self else { return }
self.ttsCallback?.onSynthesisStarted()
}
// 合成中事件
synthesizer.addSynthesizing { [weak self] _, _ in
guard let self = self else { return }
self.ttsCallback?.onSynthesizing()
}
// 合成完成事件
synthesizer.addSynthesisCompleted { [weak self] _, _ in
guard let self = self else { return }
self.speaking = false
self.ttsCallback?.onSynthesisCompleted()
}
// 合成取消事件
synthesizer.addSynthesisCanceled { [weak self] _, _ in
guard let self = self else { return }
print("\(self.tag): 语音合成取消")
self.speaking = false
self.ttsCallback?.onSynthesisCanceled()
}
}
/// 设置TTS回调
/// - Parameter callback: TTS回调接口
func setTtsCallback(_ callback: TtsCallback?) {
ttsCallback = callback
}
/// 设置自定义音频输出流
/// - Parameter outputStream: 自定义音频输出流,如果为nil则使用默认音频输出
/// - Returns: 是否设置成功
func setCustomAudioOutputStream(_ outputStream: SPXPushAudioOutputStream?) -> Bool {
do {
// 保存引用
customAudioOutputStream = outputStream
// 创建合成器,使用默认音频输出(扬声器)
synthesizer = try SPXSpeechSynthesizer(speechConfig!)
// 如果已初始化,需要重新创建合成器以应用新的音频输出流
if initialized {
recreateSynthesizer()
}
return true
} catch {
print("\(tag): 设置自定义音频输出流失败: \(error.localizedDescription)")
return false
}
}
/// 重新创建合成器
private func recreateSynthesizer() {
do {
// 关闭现有合成器
synthesizer = nil
// 设置事件监听
setupEventListeners()
// 创建音频配置
if let customOutput = customAudioOutputStream {
// 使用自定义音频输出流
audioConfig = try SPXAudioConfiguration(streamOutput: customOutput)
// 设置默认音色 - 中文默认使用晓晓,英文默认使用Jenny
if language.lowercased().starts(with: "zh") {
_ = setVoice("zh-CN-XiaoxiaoNeural")
} else {
// 使用默认扬声器
audioConfig = SPXAudioConfiguration()
_ = setVoice("en-US-JennyNeural")
}
// 使用新的音频配置创建合成器
synthesizer = try SPXSpeechSynthesizer(speechConfiguration: speechConfig!, audioConfiguration: audioConfig!)
// 设置初始化完成
isInitialized = true
// 重新设置事件监听
setupEventListeners()
return true
} catch {
print("\(tag): 重新创建合成器失败: \(error.localizedDescription)")
os_log("语音合成服务初始化失败: %{public}@", log: log, type: .error, error.localizedDescription)
return false
}
}
/// 设置语音
/// - Parameter voiceName: 语音名称,例如 "zh-CN-XiaoxiaoNeural"
/// - Returns: 是否设置成功
func setVoice(voiceName: String) -> Bool {
if !initialized {
print("\(tag): TTS 引擎尚未初始化")
return false
}
if voiceName == currentVoice {
print("\(tag): 已设置语音: \(voiceName)")
return true
}
/**
* 设置语音角色
*
* @param voiceName 语音角色名称(不同服务的语音角色命名可能不同)
* @return 是否设置成功
*/
public func setVoice(_ voiceName: String) -> Bool {
if !isInitialized { return false }
do {
currentVoice = voiceName
speechConfig?.setSpeechSynthesisVoiceName(voiceName)
// 更新语音名称
if let config = speechConfig {
config.speechSynthesisVoiceName = voiceName
}
// 重新创建合成器
recreateSynthesizer()
print("\(tag): 已设置语音: \(voiceName)")
return true
} catch {
print("\(tag): 设置语音失败: \(error.localizedDescription)")
os_log("设置语音失败: %{public}@", log: log, type: .error, error.localizedDescription)
notifyEvent(eventType: .error, params: [
"errorCode": "VOICE_SET_FAILED",
"errorMessage": "设置语音失败: \(error.localizedDescription)"
])
return false
}
}
/// 设置语音合成参数
/// - Parameters:
/// - rate: 语速,范围 -100 到 100,默认为 0
/// - pitch: 音调,范围 -100 到 100,默认为 0
/// - volume: 音量,范围 0 到 100,默认为 100
/// - Returns: 是否设置成功
func setSpeechParams(rate: Int = 0, pitch: Int = 0, volume: Int = 100) -> Bool {
if !initialized {
print("\(tag): TTS 引擎尚未初始化")
/**
* 单次合成并播放
*
* @param text 要合成的文本
* @return 是否成功开始合成
*/
public func speakOnce(_ text: String) -> Bool {
if !isInitialized {
os_log("语音合成未初始化", log: log, type: .error)
return false
}
do {
currentRate = formatPercentage(rate)
currentPitch = formatPercentage(pitch)
currentVolume = "\(min(max(volume, 0), 100))%"
print("\(tag): 已设置语音参数: 语速=\(rate), 音调=\(pitch), 音量=\(volume)")
return true
} catch {
print("\(tag): 设置语音参数失败: \(error.localizedDescription)")
return false
// 立即返回成功,内部异步处理
enqueueSynthesisTask {
self.performSynthesis(text: text)
}
return true
}
/// 格式化百分比值
private func formatPercentage(_ value: Int) -> String {
return value >= 0 ? "+\(value)%" : "\(value)%"
/**
* 将合成任务加入队列,保证顺序执行
*/
private func enqueueSynthesisTask(_ task: @escaping () -> Void) {
taskLock.lock()
defer { taskLock.unlock() }
pendingTasks.append(task)
// 如果当前没有任务在执行,开始处理队列
if pendingTasks.count == 1 {
processNextTask()
}
}
/// 生成 SSML 文本
/// - Parameter text: 要转换的文本
/// - Returns: SSML 格式的文本
private func generateSsml(text: String) -> String {
return """
<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xmlns:mstts="https://www.w3.org/2001/mstts" xml:lang="zh-CN">
<voice name="\(currentVoice)">
<prosody rate="\(currentRate)" pitch="\(currentPitch)" volume="\(currentVolume)">
\(text)
</prosody>
</voice>
</speak>
"""
/**
* 处理队列中的下一个任务
*/
private func processNextTask() {
synthesisQueue.async {
self.synthesisGroup.enter()
self.taskLock.lock()
guard !self.pendingTasks.isEmpty else {
self.taskLock.unlock()
self.synthesisGroup.leave()
return
}
let task = self.pendingTasks.removeFirst()
self.taskLock.unlock()
// 执行任务
task()
self.synthesisGroup.leave()
// 处理下一个任务
self.taskLock.lock()
if !self.pendingTasks.isEmpty {
self.taskLock.unlock()
self.processNextTask()
} else {
self.taskLock.unlock()
}
}
}
/// 合成文本为语音并播放
/// - Parameter text: 要合成的文本
/// - Returns: 是否成功开始合成
func speakText(_ text: String) -> Bool {
if !initialized {
print("\(tag): TTS 引擎尚未初始化")
return false
}
/**
* 实际执行合成的方法
*/
private func performSynthesis(text: String) {
// 重置状态
speaking = true
// 生成SSML
let ssml = generateSsml(text)
do {
print("\(tag): 开始合成文本: \(text)")
os_log("开始合成: %{public}@", log: log, type: .debug, ssml)
// 使用同步方法,在后台队列中执行
let result = try synthesizer?.startSpeakingSsml(ssml)
// 生成 SSML
let ssml = generateSsml(text: text)
// 标记为正在播放
speaking = true
// 异步合成语音
try synthesizer?.speakSsmlAsync(ssml)
return true
// 检查结果
if let result = result {
os_log("合成完成,结果: %{public}@", log: log, type: .debug, String(describing: result.reason))
}
} catch {
print("\(tag): 语音合成异常: \(error.localizedDescription)")
os_log("语音合成失败: %{public}@", log: log, type: .error, error.localizedDescription)
notifyEvent(eventType: .error, params: [
"errorCode": "SYNTHESIS_FAILED",
"errorMessage": error.localizedDescription
])
speaking = false
return false
}
}
/// 处理流式文本并在遇到标点符号时播放
/// - Parameter text: 收到的文本流片段
/// - Returns: 是否成功处理
func speakStream(_ text: String) -> Bool {
if !initialized || text.isEmpty {
/**
* 流式合成文本
*
* @param text 要合成的文本片段
* @return 是否成功处理
*/
public func speakStream(_ text: String) -> Bool {
if !isInitialized || text.isEmpty {
if !isInitialized {
notifyEvent(eventType: .error, params: [
"errorCode": "NOT_INITIALIZED",
"errorMessage": "TTS引擎未初始化"
])
}
return false
}
@ -319,7 +252,7 @@ class AzureTtsHelper: NSObject {
// 增加500ms防抖逻辑
let currentTime = Date().timeIntervalSince1970
if currentTime - lastSpeakTime < 0.5 {
if currentTime - lastSpeakTime < 0.6 {
return true
}
lastSpeakTime = currentTime
@ -327,13 +260,13 @@ class AzureTtsHelper: NSObject {
let currentText = streamBuffer
// 定义标点符号列表
let punctuationMarks: [Character] = [".", "。", "!", "!", "?", "?", ";", ";", ",", ",", ":", ":"]
let punctuationMarks: [Character] = [".", "。", "!", "!", "?", "?", ";", ";", ",", ",", ":", ":", "\n"]
// 查找最后一个标点符号的位置
var lastPunctuationIndex = -1
for i in currentText.indices.reversed() {
if punctuationMarks.contains(currentText[i]) {
lastPunctuationIndex = currentText.distance(from: currentText.startIndex, to: i)
for (i, char) in currentText.enumerated().reversed() {
if punctuationMarks.contains(char) {
lastPunctuationIndex = i
break
}
}
@ -341,33 +274,47 @@ class AzureTtsHelper: NSObject {
// 如果找到标点符号,则播放到该标点符号
if lastPunctuationIndex >= 0 {
// 提取要播放的文本(包含标点符号)
let textToSpeak = String(currentText.prefix(lastPunctuationIndex + 1))
let textToSpeak = String(currentText.prefix(lastPunctuationIndex + 1)).trimmingCharacters(in: .whitespacesAndNewlines)
// 剩余的文本保存在缓冲区中
streamBuffer = String(currentText.dropFirst(lastPunctuationIndex + 1))
let startIndex = currentText.index(currentText.startIndex, offsetBy: lastPunctuationIndex + 1)
streamBuffer = String(currentText[startIndex...])
// 播放提取的文本
return speakText(textToSpeak)
// 只有非空文本才播放
if !textToSpeak.isEmpty {
return speakOnce(textToSpeak)
}
}
// 如果没有找到标点符号,则等待更多文本
return true
} catch {
print("\(tag): 流式语音合成失败: \(error.localizedDescription)")
os_log("流式语音合成失败: %{public}@", log: log, type: .error, error.localizedDescription)
notifyEvent(eventType: .error, params: [
"errorCode": "STREAM_FAILED",
"errorMessage": "流式语音合成失败: \(error.localizedDescription)"
])
return false
}
}
/// 播放剩余的流式文本
/// - Returns: 是否成功播放剩余文本
func flushStream() -> Bool {
if !initialized {
/**
* 刷新并播放流式文本缓冲区中的剩余内容
*
* @return 是否成功处理
*/
public func flushStream() -> Bool {
if !isInitialized {
notifyEvent(eventType: .error, params: [
"errorCode": "NOT_INITIALIZED",
"errorMessage": "TTS引擎未初始化"
])
return false
}
do {
// 获取缓冲区中剩余的文本
let remainingText = streamBuffer
let remainingText = streamBuffer.trimmingCharacters(in: .whitespacesAndNewlines)
// 清空缓冲区
streamBuffer = ""
@ -378,69 +325,280 @@ class AzureTtsHelper: NSObject {
}
// 播放剩余文本
return speakText(remainingText)
return speakOnce(remainingText)
} catch {
print("\(tag): 刷新流式文本失败: \(error.localizedDescription)")
os_log("刷新流式文本失败: %{public}@", log: log, type: .error, error.localizedDescription)
notifyEvent(eventType: .error, params: [
"errorCode": "FLUSH_FAILED",
"errorMessage": "刷新流式文本失败: \(error.localizedDescription)"
])
return false
}
}
/// 停止当前语音合成
/// - Returns: 是否停止成功
func stopSpeaking() -> Bool {
if !initialized {
print("\(tag): TTS 引擎尚未初始化")
return false
/**
* 停止语音合成和播放
*
* @return 是否成功停止
*/
public func stop() -> Bool {
// 立即更新状态
speaking = false
// 清除流式缓冲区中的待播放内容
streamBuffer = ""
// 清空待处理的任务队列
taskLock.lock()
pendingTasks.removeAll()
taskLock.unlock()
// 在后台队列停止合成器,避免阻塞主线程
synthesisQueue.async {
if let synthesizer = self.synthesizer {
do {
try synthesizer.stopSpeaking()
os_log("语音合成已停止", log: self.log, type: .info)
// 直接通知停止完成
self.notifyEvent(eventType: .synthesisCanceled)
} catch {
os_log("停止语音合成失败: %{public}@", log: self.log, type: .error, error.localizedDescription)
self.notifyEvent(eventType: .error, params: [
"errorCode": "STOP_FAILED",
"errorMessage": error.localizedDescription
])
}
}
}
do {
return true
}
/**
* 释放资源
* 在不再需要服务时调用,释放底层资源
*/
public func dispose() {
// 停止播放
_ = stop()
// 等待所有任务完成
synthesisGroup.wait()
// 清理资源
synthesisQueue.async {
// 释放合成器
self.synthesizer = nil
// 释放配置
self.speechConfig = nil
// 清空流缓冲区
streamBuffer = ""
self.streamBuffer = ""
try synthesizer?.stopSpeakingAsync()
speaking = false
print("\(tag): 已停止语音合成")
return true
} catch {
print("\(tag): 停止语音合成失败: \(error.localizedDescription)")
return false
// 重置状态
self.isInitialized = false
self.speaking = false
// 清空任务队列
self.taskLock.lock()
self.pendingTasks.removeAll()
self.taskLock.unlock()
}
}
/// 释放资源
func dispose() {
do {
stopSpeaking()
// 恢复音频会话
try audioSession.setActive(false, options: .notifyOthersOnDeactivation)
// 关闭自定义音频输出流
customAudioOutputStream = nil
/**
* 添加TTS事件监听器
*
* @param listener 事件监听器
*/
public func addListener(_ listener: TtsEventListener) {
eventListeners.add(listener as AnyObject)
}
/**
* 移除TTS事件监听器
*
* @param listener 要移除的事件监听器
*/
public func removeListener(_ listener: TtsEventListener) {
eventListeners.remove(listener as AnyObject)
}
/**
* 添加音频数据监听器
* 由于不再支持自定义音频流,此方法实际上不再有效
*
* @param listener 音频数据监听器
*/
public func addAudioDataListener(_ listener: AudioDataListener) {
os_log("警告:不支持音频数据监听器功能", log: log, type: .info)
}
/**
* 移除音频数据监听器
* 由于不再支持自定义音频流,此方法实际上不再有效
*
* @param listener 要移除的音频数据监听器
*/
public func removeAudioDataListener(_ listener: AudioDataListener) {
// 不做任何操作
}
/**
* 当前是否正在播放/合成
*/
public func isSpeaking() -> Bool {
return speaking
}
// MARK: - 辅助方法
/**
* 设置事件监听器
*/
private func setupEventListeners() {
guard let synthesizer = synthesizer else { return }
// 添加合成开始事件处理器
synthesizer.addSynthesisStartedEventHandler { [weak self] _, _ in
guard let self = self else { return }
self.notifyEvent(eventType: .synthesisStarted)
}
// 添加合成中事件处理器
synthesizer.addSynthesizingEventHandler { [weak self] _, _ in
// 可以在这里处理合成中的事件,目前没有特别操作
}
// 添加合成完成事件处理器
synthesizer.addSynthesisCompletedEventHandler { [weak self] _, e in
guard let self = self else { return }
self.speaking = false
self.notifyEvent(eventType: .synthesisCompleted)
}
// 添加合成取消事件处理器
synthesizer.addSynthesisCanceledEventHandler { [weak self] _, e in
guard let self = self else { return }
// 清空流缓冲区
streamBuffer = ""
self.speaking = false
// 清理资源
synthesizer = nil
speechConfig = nil
audioConfig = nil
ttsCallback = nil
var params: [String: Any] = [:]
do {
let cancellationDetails = try SPXSpeechSynthesisCancellationDetails(fromCanceledSynthesisResult: e.result)
if cancellationDetails.reason == SPXCancellationReason.error {
params["reason"] = String(describing: cancellationDetails.reason.rawValue)
params["errorDetails"] = cancellationDetails.errorDetails ?? "未知错误"
}
} catch {
params["errorDetails"] = "获取取消详情失败: \(error.localizedDescription)"
}
os_log("语音合成取消, %{public}@", log: self.log, type: .info, String(describing: params["errorDetails"] ?? "未知错误"))
self.notifyEvent(eventType: .synthesisCanceled, params: params)
}
}
/**
* 触发事件通知
*/
private func notifyEvent(eventType: TtsEventType, params: [String: Any] = [:]) {
let event = TtsEvent(type: eventType, params: params)
// 直接通知事件,由调用方处理线程切换
for case let listener as TtsEventListener in self.eventListeners.allObjects {
listener.onEvent(event)
}
}
/**
* 重新创建合成器
*/
private func recreateSynthesizer() {
do {
// 使用默认音频输出配置创建合成器
synthesizer = try SPXSpeechSynthesizer(speechConfig!)
initialized = false
speaking = false
print("\(tag): TTS 引擎已释放")
// 设置事件监听
setupEventListeners()
} catch {
print("\(tag): 释放 TTS 引擎失败: \(error.localizedDescription)")
// 确保重置状态
initialized = false
speaking = false
os_log("重新创建合成器失败: %{public}@", log: log, type: .error, error.localizedDescription)
notifyEvent(eventType: .error, params: [
"errorCode": "RECREATE_FAILED",
"errorMessage": "重新创建合成器失败: \(error.localizedDescription)"
])
}
}
/// 检查当前是否正在播放语音
/// - Returns: 是否正在播放语音
func isSpeaking() -> Bool {
return speaking
/**
* 设置语音参数
*/
private func setSpeechParams(rate: Int = 0, pitch: Int = 0, volume: Int = 100) -> Bool {
if !isInitialized { return false }
do {
currentRate = formatPercentage(rate)
currentPitch = formatPercentage(pitch)
currentVolume = "\(min(max(volume, 0), 100))%"
return true
} catch {
os_log("设置语音参数失败: %{public}@", log: log, type: .error, error.localizedDescription)
notifyEvent(eventType: .error, params: [
"errorCode": "PARAMS_SET_FAILED",
"errorMessage": "设置语音参数失败: \(error.localizedDescription)"
])
return false
}
}
/**
* 格式化百分比值
*/
private func formatPercentage(_ value: Int) -> String {
return value >= 0 ? "+\(value)%" : "\(value)%"
}
/**
* 生成SSML
*/
private func generateSsml(_ rawText: String) -> String {
// 1. 定义要静音的符号和表情符号列表
let symbolsToMute = [
"#", "*",
"😀", "😂", "😊", "😍", "😢", "😎", "😉", "👍", "🙌", "🎉"
]
// 2. 转义 XML 保留字符
var escapedText = rawText
.replacingOccurrences(of: "&", with: "&amp;")
.replacingOccurrences(of: "<", with: "&lt;")
.replacingOccurrences(of: ">", with: "&gt;")
// 3. 静音处理特殊符号和表情符号
// 使用空白替换法,直接将符号替换为空字符串
var processedText = escapedText
for symbol in symbolsToMute {
processedText = processedText.replacingOccurrences(of: symbol, with: "")
}
// 4. 构造简化的SSML文档,减少嵌套层级
let ssml = """
<speak version="1.0"
xmlns="http://www.w3.org/2001/10/synthesis"
xmlns:mstts="https://www.w3.org/2001/mstts"
xml:lang="zh-CN">
<voice name="\(currentVoice)">
<mstts:express-as style="cheerful">
<prosody rate="\(currentRate)" pitch="\(currentPitch)" volume="\(currentVolume)">
<say-as interpret-as="text">\(processedText)</say-as>
</prosody>
</mstts:express-as>
</voice>
</speak>
"""
return ssml
}
}
}

26
local_plugins/azure_speech/ios/Flutter/AppFrameworkInfo.plist

@ -1,26 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>12.0</string>
</dict>
</plist>

1
local_plugins/azure_speech/ios/Flutter/Debug.xcconfig

@ -1 +0,0 @@
#include "Generated.xcconfig"

1
local_plugins/azure_speech/ios/Flutter/Release.xcconfig

@ -1 +0,0 @@
#include "Generated.xcconfig"

619
local_plugins/azure_speech/ios/Runner.xcodeproj/project.pbxproj

@ -1,619 +0,0 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
remoteInfo = Runner;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C8080294A63A400263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
);
buildRules = (
);
dependencies = (
331C8086294A63A400263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C8080294A63A400263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 97C146ED1CF9000F007C117D;
};
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
331C8080294A63A400263BE5 /* RunnerTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C807F294A63A400263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C807D294A63A400263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 97C146ED1CF9000F007C117D /* Runner */;
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Profile;
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = PNNU77KTCP;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.azureSpeech;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.azureSpeech.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Debug;
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.azureSpeech.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Release;
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.azureSpeech.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = PNNU77KTCP;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.azureSpeech;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
97C147071CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = PNNU77KTCP;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.azureSpeech;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
331C8088294A63A400263BE5 /* Debug */,
331C8089294A63A400263BE5 /* Release */,
331C808A294A63A400263BE5 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147031CF9000F007C117D /* Debug */,
97C147041CF9000F007C117D /* Release */,
249021D3217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147061CF9000F007C117D /* Debug */,
97C147071CF9000F007C117D /* Release */,
249021D4217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}

7
local_plugins/azure_speech/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

8
local_plugins/azure_speech/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

8
local_plugins/azure_speech/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

7
local_plugins/azure_speech/ios/Runner.xcworkspace/contents.xcworkspacedata

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>

8
local_plugins/azure_speech/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

8
local_plugins/azure_speech/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

13
local_plugins/azure_speech/ios/Runner/AppDelegate.swift

@ -1,13 +0,0 @@
import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}

122
local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json

@ -1,122 +0,0 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

BIN
local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

BIN
local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 295 B

BIN
local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 B

BIN
local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 450 B

BIN
local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 282 B

BIN
local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 462 B

BIN
local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 704 B

BIN
local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 B

BIN
local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 586 B

BIN
local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

BIN
local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

BIN
local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

BIN
local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 762 B

BIN
local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

BIN
local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

23
local_plugins/azure_speech/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json

@ -1,23 +0,0 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "LaunchImage.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

BIN
local_plugins/azure_speech/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 B

BIN
local_plugins/azure_speech/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 B

BIN
local_plugins/azure_speech/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 B

5
local_plugins/azure_speech/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md

@ -1,5 +0,0 @@
# Launch Screen Assets
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.

37
local_plugins/azure_speech/ios/Runner/Base.lproj/LaunchScreen.storyboard

@ -1,37 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
</resources>
</document>

26
local_plugins/azure_speech/ios/Runner/Base.lproj/Main.storyboard

@ -1,26 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>

49
local_plugins/azure_speech/ios/Runner/Info.plist

@ -1,49 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Azure Speech</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>azure_speech</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict>
</plist>

1
local_plugins/azure_speech/ios/Runner/Runner-Bridging-Header.h

@ -1 +0,0 @@
#import "GeneratedPluginRegistrant.h"

12
local_plugins/azure_speech/ios/RunnerTests/RunnerTests.swift

@ -1,12 +0,0 @@
import Flutter
import UIKit
import XCTest
class RunnerTests: XCTestCase {
func testExample() {
// If you add code to the Runner application, consider adding tests here.
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
}
}

21
local_plugins/azure_speech/ios/azure_speech.podspec

@ -5,20 +5,25 @@
Pod::Spec.new do |s|
s.name = 'azure_speech'
s.version = '0.0.1'
s.summary = 'Azure Speech plugin'
s.summary = 'Azure Speech SDK Flutter plugin'
s.description = <<-DESC
A Flutter plugin for Azure Speech services.
A Flutter plugin for Azure Speech Services providing speech recognition and text-to-speech capabilities.
DESC
s.homepage = 'http://example.com'
s.homepage = 'https://github.com/yunqiinnovation/azure_speech'
s.license = { :file => '../LICENSE' }
s.author = { 'Your Company' => 'email@example.com' }
s.source = { :path => '.' }
s.source_files = 'Classes/**/*'
s.source_files = 'Classes/**/*.{swift}'
s.dependency 'Flutter'
s.dependency 'MicrosoftCognitiveServicesSpeech-iOS'
s.platform = :ios, '13.0'
s.dependency 'speech'
s.dependency 'MicrosoftCognitiveServicesSpeech-iOS', '~> 1.37.0'
s.platform = :ios, '11.0'
# Flutter.framework does not contain a i386 slice.
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
s.pod_target_xcconfig = {
'DEFINES_MODULE' => 'YES',
'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386',
'SWIFT_EMIT_LOC_STRINGS' => 'YES'
}
s.swift_version = '5.0'
end

20
local_plugins/azure_speech/lib/main.dart

@ -1,20 +0,0 @@
import 'package:flutter/material.dart';
void main() {
runApp(const MainApp());
}
class MainApp extends StatelessWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: Scaffold(
body: Center(
child: Text('Hello World!'),
),
),
);
}
}

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

@ -261,6 +261,23 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, ActivityAware,
val macAddress = bleCompanionManager.getPairedMacAddress()
result.success(macAddress)
}
"verifyDeviceCanPair" -> {
// Android上暂时返回true,实际验证逻辑待实现
result.success(true)
}
"savePairedMacAddress" -> {
val macAddress = call.argument<String>("macAddress")
if (macAddress != null) {
bleCompanionManager.savePairedMacAddress(macAddress)
result.success(true)
} else {
result.error("INVALID_ARGUMENT", "MAC地址参数不能为空", null)
}
}
"isBleConnected" -> {
val connected = BleService.isConnected()
result.success(connected)
}
"registerBackgroundScan" -> {
bleCompanionManager.registerBackgroundScan()
result.success(true)

404
local_plugins/ble_service/ios/Classes/BleAccessorySetup.swift

@ -1,195 +1,305 @@
import Foundation
import CoreBluetooth
import os.log
// 条件编译只导入在iOS 18+上可用的框架
#if canImport(AccessorySetupKit) && swift(>=5.9)
import AVFoundation
import AccessorySetupKit
import UIKit
#endif
class BleAccessorySetup: NSObject {
// 单例实现
// MARK: - 属性
static let shared = BleAccessorySetup()
// 基本属性
private var bleService: Any?
private var bleService: BleService?
private var session: ASAccessorySession?
private var isSessionActivated = false
private var isInitializing = false
private let logger = OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleAccessorySetup")
// iOS 18+ 特定属性和回调
#if canImport(AccessorySetupKit) && swift(>=5.9)
@available(iOS 18.0, *)
class AccessorySessionWrapper {
var session: ASAccessorySession
init() {
self.session = ASAccessorySession()
}
}
// 杰理蓝牙设备的公司标识符
private let deviceCompanyIdentifier: UInt16 = 0xEFAB
private let deviceDisplayName = "语音助理耳机"
var accessorySessionWrapper: Any?
// 用于存储配对设备UUID的UserDefaults键
private let pairedDeviceUUIDKey = "com.yunqiinnovation.ble_service.paired_device_uuid"
typealias DeviceDiscoveredCallback = (UUID) -> Void
private var onDeviceDiscovered: DeviceDiscoveredCallback?
#endif
private override init() {
super.init()
}
// MARK: - 初始化
private override init() { super.init() }
// 设置BLE服务实例
// MARK: - 公共方法
func setBleService(_ service: Any) {
self.bleService = service
self.bleService = service as? BleService
}
// 设置设备发现回调
func setDeviceDiscoveredCallback(_ callback: @escaping (UUID) -> Void) {
#if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) {
self.onDeviceDiscovered = callback
self.onDeviceDiscovered = callback
}
// 处理发现的设备并连接
func connectToAccessory(uuid: UUID) {
guard let service = bleService else {
return
}
#endif
service.connectToAccessory(uuid: uuid)
}
// 初始化设备发现会话
func initializeDiscoverySession() {
#if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) {
os_log("初始化iOS 18 AccessorySetupKit会话", log: logger, type: .info)
// 创建会话
accessorySessionWrapper = AccessorySessionWrapper()
// 激活会话并设置事件处理
if let wrapper = accessorySessionWrapper as? AccessorySessionWrapper {
wrapper.session.activate(on: .main) { [weak self] event in
guard let self = self else { return }
func addToReconnectQueue(uuid: UUID) {
guard let service = bleService else {
return
}
service.addToReconnectQueue(uuid: uuid)
}
// 从视图控制器显示设备选择器 - 主要入口点
func showDevicePickerFromViewController(_ viewController: UIViewController) {
initializeSession()
}
// 清除蓝牙状态(仅用于测试或重置,不会清除系统授权)
func resetBluetoothState() {
if let service = bleService {
service.disconnect()
}
}
// MARK: - 会话管理
func initializeSession() {
if isInitializing {
os_log("会话正在初始化中,跳过", log: logger, type: .info)
return
}
// 如果会话已激活,检查是否有配对设备
if session != nil && isSessionActivated {
os_log("会话已激活,检查配对设备", log: logger, type: .info)
// 会话已激活,检查是否有配对设备
if let accessories = session?.accessories, !accessories.isEmpty {
os_log("发现 %d 个已配对设备,尝试连接", log: logger, type: .info, accessories.count)
// 有配对设备,尝试连接
if let service = bleService {
let _ = service.getCentralManager()
switch event.eventType {
case .activated:
os_log("AccessorySetupKit会话已激活", log: self.logger, type: .info)
case .accessoryAdded:
if let accessory = event.accessory, let uuid = accessory.bluetoothIdentifier {
os_log("发现新设备: %{public}@", log: self.logger, type: .info, accessory.displayName)
self.onDeviceDiscovered?(uuid)
}
case .accessoryRemoved:
if let accessory = event.accessory {
os_log("设备已移除: %{public}@", log: self.logger, type: .info, accessory.displayName)
}
// 根据WWDC24视频,正确的事件类型为accessoryChanged
case .accessoryChanged:
if let accessory = event.accessory {
os_log("设备已更新: %{public}@", log: self.logger, type: .info, accessory.displayName)
}
// 根据WWDC24视频,失败事件类型已变更
case .pickerSetupFailed:
if let error = event.error {
os_log("会话出错: %{public}@", log: self.logger, type: .error, error.localizedDescription)
for accessory in accessories {
if let uuid = accessory.bluetoothIdentifier {
onDeviceDiscovered?(uuid)
connectToAccessory(uuid: uuid)
}
default:
os_log("收到其他会话事件: %{public}d", log: self.logger, type: .debug, event.eventType.rawValue)
}
}
} else {
os_log("没有已配对设备,显示配对框", log: logger, type: .info)
// 没有配对设备,直接显示配对框
showDevicePicker()
}
return
}
os_log("开始初始化新会话", log: logger, type: .info)
isInitializing = true
do {
session = try ASAccessorySession()
session?.activate(on: .main) { [weak self] event in
guard let self = self else { return }
self.handleSessionEvent(event)
}
} catch {
os_log("会话初始化失败: %{public}@", log: logger, type: .error, error.localizedDescription)
handleSessionInitError(error)
}
}
func resetSession() {
session?.invalidate()
session = nil
isSessionActivated = false
isInitializing = false
}
// MARK: - 设备选择器显示
private func showDevicePicker() {
guard let session = session, isSessionActivated else {
os_log("无法显示配对框:会话未激活", log: logger, type: .error)
return
}
os_log("准备显示设备配对框", log: logger, type: .info)
let descriptor = ASDiscoveryDescriptor()
descriptor.bluetoothCompanyIdentifier = ASBluetoothCompanyIdentifier(deviceCompanyIdentifier)
let deviceImage = UIImage(systemName: "headphones") ?? UIImage()
let displayItem = ASPickerDisplayItem(
name: deviceDisplayName,
productImage: deviceImage,
descriptor: descriptor
)
DispatchQueue.main.async {
os_log("正在显示设备配对框", log: self.logger, type: .info)
session.showPicker(for: [displayItem]) { [weak self] error in
guard let self = self else { return }
// 检查现有授权设备
if !wrapper.session.accessories.isEmpty {
os_log("发现%{public}d个已授权设备", log: logger, type: .info, wrapper.session.accessories.count)
// 如果有现有设备,触发回调
for accessory in wrapper.session.accessories {
if let uuid = accessory.bluetoothIdentifier {
os_log("处理已授权设备: %{public}@", log: logger, type: .info, accessory.displayName)
self.onDeviceDiscovered?(uuid)
}
}
if let error = error {
let nsError = error as NSError
os_log("设备选择器错误: %{public}@", log: self.logger, type: .error, error.localizedDescription)
} else {
os_log("设备选择器完成", log: self.logger, type: .info)
}
}
} else {
os_log("iOS 18 AccessorySetupKit不可用", log: logger, type: .info)
}
#else
os_log("iOS 18 AccessorySetupKit不可用", log: logger, type: .info)
#endif
}
// 显示设备选择器
func showDevicePicker() {
#if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) {
if let wrapper = accessorySessionWrapper as? AccessorySessionWrapper {
os_log("显示iOS 18设备选择器", log: logger, type: .info)
// MARK: - 私有辅助方法
private func handleSessionEvent(_ event: ASAccessoryEvent) {
switch event.eventType {
case .activated:
os_log("会话已激活", log: logger, type: .info)
handleSessionActivated()
case .accessoryAdded:
os_log("发现新设备: %{public}@", log: logger, type: .info, event.accessory?.bluetoothIdentifier?.uuidString ?? "Unknown")
handleAccessoryAdded(event.accessory)
case .accessoryRemoved:
os_log("设备已移除: %{public}@", log: logger, type: .info, event.accessory?.bluetoothIdentifier?.uuidString ?? "Unknown")
handleAccessoryRemoved(event.accessory)
case .accessoryChanged:
os_log("设备状态已改变", log: logger, type: .info)
break
case .pickerDidPresent:
os_log("配对框已显示", log: logger, type: .info)
break
case .pickerDidDismiss:
os_log("配对框已关闭", log: logger, type: .info)
break
case .pickerSetupFailed:
os_log("选择器设置失败: %{public}@", log: logger, type: .error,
event.error?.localizedDescription ?? "未知错误")
default:
os_log("未处理的会话事件: %d", log: logger, type: .info, event.eventType.rawValue)
break
}
}
private func handleSessionActivated() {
isSessionActivated = true
isInitializing = false
if let accessories = session?.accessories, !accessories.isEmpty {
if let service = bleService {
let _ = service.getCentralManager()
// 创建描述符和选择器项,这些API也需要iOS 18
@available(iOS 18.0, *)
func createAndShowPicker() {
// 根据WWDC24视频,正确的类名是ASDiscoveryDescriptor
let descriptor = ASDiscoveryDescriptor()
// 设置过滤条件
// 使用ASBluetoothCompanyIdentifier类型
descriptor.bluetoothCompanyIdentifier = ASBluetoothCompanyIdentifier(UInt16(0xABEF)) // BleConst.MANUFACTURER_ID
descriptor.bluetoothServiceUUID = CBUUID(string: "0000abc0-0000-1111-2222-123456789abc") // BleConst.PRIMARY_SERVICE_UUID
// 创建默认图像以防系统图标不可用
let headphonesImage = UIImage(systemName: "headphones") ?? UIImage()
// 根据WWDC24视频,正确的类名是ASPickerDisplayItem
let displayItem = ASPickerDisplayItem(
name: "语音助理耳机",
productImage: headphonesImage,
descriptor: descriptor
)
// 显示选择器
wrapper.session.showPicker(for: [displayItem]) { error in
if let error = error {
// 修复OSLog的错误用法
os_log("设备选择器显示错误: %{public}@", log: self.logger, type: .error, error.localizedDescription)
}
for accessory in accessories {
if let uuid = accessory.bluetoothIdentifier {
onDeviceDiscovered?(uuid)
connectToAccessory(uuid: uuid)
}
}
// 调用内部函数
createAndShowPicker()
} else {
os_log("会话未初始化", log: logger, type: .error)
}
} else {
os_log("iOS 18 AccessorySetupKit不可用", log: logger, type: .info)
showDevicePicker()
}
#else
os_log("iOS 18 AccessorySetupKit不可用", log: logger, type: .info)
#endif
}
// 处理发现的设备
func handleDiscoveredAccessory(uuid: UUID) {
#if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) {
guard let service = bleService as? BleService else {
os_log("BLE服务未设置", log: logger, type: .error)
return
}
// 获取中央管理器
let centralManager = service.getCentralManager()
// 通过UUID检索外设
let peripherals = centralManager.retrievePeripherals(withIdentifiers: [uuid])
private func handleAccessoryAdded(_ accessory: ASAccessory?) {
guard let accessory = accessory, let uuid = accessory.bluetoothIdentifier else {
return
}
// 保存配对设备的UUID
savePairedMacAddress(uuid)
onDeviceDiscovered?(uuid)
connectToAccessory(uuid: uuid)
}
private func handleAccessoryRemoved(_ accessory: ASAccessory?) {
guard let accessory = accessory, let uuid = accessory.bluetoothIdentifier else {
return
}
if let service = bleService {
service.disconnect()
}
}
private func handleSessionInitError(_ error: Error) {
isInitializing = false
session = nil
}
// MARK: - 蓝牙状态检查
func isBluetoothAvailable() -> Bool {
do {
let audioSession = AVAudioSession.sharedInstance()
let outputs = audioSession.currentRoute.outputs
if let peripheral = peripherals.first {
os_log("连接到UUID对应的设备: %{public}@", log: logger, type: .info, peripheral.name ?? uuid.uuidString)
service.connect(to: peripheral)
} else {
os_log("未找到UUID对应的设备: %{public}@", log: logger, type: .error, uuid.uuidString)
for output in outputs {
if output.portType == .bluetoothA2DP ||
output.portType == .bluetoothHFP ||
output.portType == .bluetoothLE {
return true
}
}
} catch {
os_log("检查蓝牙状态出错: %{public}@", log: logger, type: .error, error.localizedDescription)
}
return true
}
// MARK: - 清除配对关系
func clearAssociations() {
resetBluetoothState()
// 清除保存的配对设备UUID
clearPairedMacAddress()
#if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) {
resetSession()
initializeSession()
}
#endif
}
// MARK: - 配对设备UUID管理
/// 获取已配对设备的UUID字符串
/// 返回存储的配对设备UUID,如果没有则返回nil
func getPairedMacAddress() -> String? {
let uuidString = UserDefaults.standard.string(forKey: pairedDeviceUUIDKey)
if let uuidString = uuidString, !uuidString.isEmpty {
os_log("获取已配对设备UUID: %{public}@", log: logger, type: .info, uuidString)
return uuidString
}
os_log("未找到已配对设备UUID", log: logger, type: .info)
return nil
}
/// 保存配对设备的UUID
/// - Parameter uuid: 要保存的设备UUID
func savePairedMacAddress(_ uuid: UUID) {
let uuidString = uuid.uuidString
UserDefaults.standard.set(uuidString, forKey: pairedDeviceUUIDKey)
UserDefaults.standard.synchronize()
os_log("已保存配对设备UUID: %{public}@", log: logger, type: .info, uuidString)
}
/// 清除已保存的配对设备UUID
private func clearPairedMacAddress() {
UserDefaults.standard.removeObject(forKey: pairedDeviceUUIDKey)
UserDefaults.standard.synchronize()
os_log("已清除配对设备UUID", log: logger, type: .info)
}
}

1076
local_plugins/ble_service/ios/Classes/BleService.swift

File diff suppressed because it is too large

391
local_plugins/ble_service/ios/Classes/SwiftBleServicePlugin.swift

@ -7,23 +7,75 @@ import os
public class SwiftBleServicePlugin: NSObject, FlutterPlugin {
// MARK: - 属性
private var methodChannel: FlutterMethodChannel
private var dataEventSink: FlutterEventSink?
private var statusEventSink: FlutterEventSink?
internal var dataEventSink: FlutterEventSink?
internal var statusEventSink: FlutterEventSink?
private let logger = OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleServicePlugin")
// 标记是否已初始化所有服务
private var servicesInitialized = false
// MARK: - 初始化方法
init(_ channel: FlutterMethodChannel) {
self.methodChannel = channel
super.init()
#if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) {
// iOS 18环境下,只设置回调但不主动初始化会话
let bleAccessorySetup = BleAccessorySetup.shared
// 在accessoryAdded回调中延迟设置BleService
bleAccessorySetup.setDeviceDiscoveredCallback { [weak self] uuid in
// 确保服务已初始化
self?.ensureServicesInitialized()
// 然后处理发现的设备
self?.handleDiscoveredDevice(uuid: uuid)
}
// 注释掉自动初始化,改为等待用户主动调用startDeviceAssociation
// bleAccessorySetup.initializeSession()
os_log("iOS 18环境:已设置设备发现回调,等待用户主动调用关联", log: logger, type: .info)
} else {
// 低于iOS 18,直接初始化所有服务
initializeAllServices()
}
#else
// 非AccessorySetupKit环境,直接初始化所有服务
initializeAllServices()
#endif
}
// 确保所有服务都已初始化
private func ensureServicesInitialized() {
if !servicesInitialized {
// 设置BLE服务回调
BleService.shared.addCallback(self)
// 设置BleService到AccessorySetup
BleAccessorySetup.shared.setBleService(BleService.shared)
servicesInitialized = true
}
}
// 初始化所有服务(iOS 17及以下版本的标准行为)
private func initializeAllServices() {
// 设置BLE服务回调
BleService.shared.setDelegate(self)
BleService.shared.addCallback(self)
// 初始化AccessorySetup
// 初始化AccessorySetup,但不自动开始会话
BleAccessorySetup.shared.setBleService(BleService.shared)
BleAccessorySetup.shared.setDeviceDiscoveredCallback { [weak self] uuid in
self?.handleDiscoveredDevice(uuid: uuid)
}
BleAccessorySetup.shared.initializeDiscoverySession()
// 移除自动初始化,统一由startDeviceAssociation方法触发
// BleAccessorySetup.shared.initializeSession()
os_log("非iOS 18环境:已设置BLE服务但不自动初始化会话", log: logger, type: .info)
servicesInitialized = true
}
// MARK: - FlutterPlugin Protocol
@ -32,15 +84,22 @@ public class SwiftBleServicePlugin: NSObject, FlutterPlugin {
let instance = SwiftBleServicePlugin(channel)
registrar.addMethodCallDelegate(instance, channel: channel)
// 注册事件通道
// 注册事件通道 - 为每个通道创建独立的handler
let dataEventChannel = FlutterEventChannel(name: "com.yunqiinnovation.ble_service/data_events", binaryMessenger: registrar.messenger())
let statusEventChannel = FlutterEventChannel(name: "com.yunqiinnovation.ble_service/status_events", binaryMessenger: registrar.messenger())
dataEventChannel.setStreamHandler(instance)
statusEventChannel.setStreamHandler(instance)
// 创建独立的StreamHandler
let dataHandler = BleDataStreamHandler(instance)
let statusHandler = BleStatusStreamHandler(instance)
dataEventChannel.setStreamHandler(dataHandler)
statusEventChannel.setStreamHandler(statusHandler)
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
// 检查是否需要初始化服务
checkAndInitializeServices()
switch call.method {
case "isConnected":
result(BleService.shared.isConnected())
@ -56,90 +115,36 @@ public class SwiftBleServicePlugin: NSObject, FlutterPlugin {
}
case "isCompanionSupported":
// 检查设备是否支持iOS 18的AccessorySetupKit功能
#if canImport(AccessorySetupKit)
#if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) {
// 设备运行iOS 18+,支持AccessorySetupKit
result(true)
} else {
// 设备运行iOS 17或更低版本
result(false)
}
#else
// 编译时没有AccessorySetupKit框架
result(false)
#endif
case "getPairedMacAddress":
// 获取已配对设备的标识符
#if canImport(AccessorySetupKit)
#if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) {
if let wrapper = BleAccessorySetup.shared.accessorySessionWrapper as? BleAccessorySetup.AccessorySessionWrapper {
// 检查是否有配对设备
if let firstAccessory = wrapper.session.accessories.first,
let btId = firstAccessory.bluetoothIdentifier {
// 在iOS中,我们返回UUID字符串作为"MAC地址"的等价物
result(btId.uuidString)
} else {
// 无配对设备
result(nil)
}
} else {
// 会话未初始化
result(nil)
}
let pairedUUID = BleAccessorySetup.shared.getPairedMacAddress()
result(pairedUUID)
} else {
// iOS版本太低
result(nil)
}
#else
// 编译环境不支持
result(nil)
#endif
case "registerBackgroundScan":
// iOS通过AccessorySetupKit自动处理后台扫描,不需要单独注册
#if canImport(AccessorySetupKit)
if #available(iOS 18.0, *) {
// 在iOS 18+上,不需要额外操作
result(true)
} else {
// iOS版本太低
result(false)
}
#else
result(false)
#endif
case "unregisterBackgroundScan":
// iOS通过AccessorySetupKit自动处理后台扫描,不需要单独注销
result(true)
case "isBleConnected":
result(BleService.shared.isConnected())
case "clearAssociations":
// 清除设备关联
#if canImport(AccessorySetupKit)
if #available(iOS 18.0, *) {
if let wrapper = BleAccessorySetup.shared.accessorySessionWrapper as? BleAccessorySetup.AccessorySessionWrapper {
// 遍历所有配对设备
for accessory in wrapper.session.accessories {
if let btId = accessory.bluetoothIdentifier {
// 移除设备的系统处理已经集成在设置中,这里什么都不做
os_log("设备需要在系统设置中手动移除: %{public}@",
log: OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService"),
type: .info,
btId.uuidString)
}
}
}
// 返回成功
result(true)
} else {
// iOS版本太低
result(false)
}
#else
result(false)
#endif
case "registerBackgroundScan", "unregisterBackgroundScan":
// iOS上禁用,直接返回成功以避免Flutter端报错
result(true)
case "getDeviceVersion":
result(BleService.shared.getDeviceVersion())
@ -166,20 +171,41 @@ public class SwiftBleServicePlugin: NSObject, FlutterPlugin {
result(BleService.shared.playA2DP())
// iOS 18+ AccessorySetupKit相关方法
case "clearAssociations":
// 清除设备配对关系
#if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) {
os_log("清除设备配对关系", log: logger, type: .info)
BleAccessorySetup.shared.clearAssociations()
result(true)
} else {
result(true)
}
#else
result(true)
#endif
case "startDeviceAssociation":
#if canImport(AccessorySetupKit)
// 用户主动触发设备关联流程
#if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) {
BleAccessorySetup.shared.showDevicePicker()
os_log("用户主动触发设备关联流程", log: logger, type: .info)
// 确保服务已初始化
ensureServicesInitialized()
// 重新初始化会话
BleAccessorySetup.shared.initializeSession()
// 返回成功,实际配对结果会通过回调通知
result(true)
} else {
result(FlutterError(code: "UNAVAILABLE",
message: "AccessorySetupKit需要iOS 18或更高版本",
details: nil))
os_log("当前iOS版本不支持AccessorySetupKit", log: logger, type: .info)
result(false)
}
#else
result(FlutterError(code: "UNAVAILABLE",
message: "编译环境不支持AccessorySetupKit",
details: nil))
os_log("当前环境不支持AccessorySetupKit", log: logger, type: .info)
result(false)
#endif
default:
@ -188,8 +214,20 @@ public class SwiftBleServicePlugin: NSObject, FlutterPlugin {
}
// MARK: - 私有方法
// 检查并按需初始化服务
private func checkAndInitializeServices() {
#if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) {
ensureServicesInitialized()
}
#endif
}
// AccessorySetupKit发现设备的处理
private func handleDiscoveredDevice(uuid: UUID) {
BleAccessorySetup.shared.handleDiscoveredAccessory(uuid: uuid)
// BleAccessorySetup内部已进行连接,此处不再重复调用
// os_log("收到设备发现回调: %{public}@", log: logger, type: .info, uuid.uuidString)
// 可以在这里添加设备记录或其他操作
}
private func sendEvent<T>(_ eventSink: FlutterEventSink?, data: T, errorPrefix: String) {
@ -199,103 +237,138 @@ public class SwiftBleServicePlugin: NSObject, FlutterPlugin {
}
}
}
// 获取顶层视图控制器的辅助方法
private func getTopViewController(_ viewController: UIViewController) -> UIViewController {
if let presented = viewController.presentedViewController {
return getTopViewController(presented)
}
if let tabBarController = viewController as? UITabBarController,
let selected = tabBarController.selectedViewController {
return getTopViewController(selected)
}
if let navigationController = viewController as? UINavigationController,
let visibleViewController = navigationController.visibleViewController {
return getTopViewController(visibleViewController)
}
return viewController
}
}
// MARK: - FlutterStreamHandler
// MARK: - BleService.Callback
@available(iOS 13.0, *)
extension SwiftBleServicePlugin: FlutterStreamHandler {
public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
if let args = arguments as? [String: Any], let type = args["type"] as? String {
if type == "data" {
dataEventSink = events
} else if type == "status" {
statusEventSink = events
extension SwiftBleServicePlugin: BleService.Callback {
public func onScanResult(devices: [[String: Any]]) {
if let eventSink = statusEventSink {
DispatchQueue.main.async {
eventSink([
"type": "scanResult",
"devices": devices
])
}
} else {
// 默认行为:根据注册的通道来确定事件类型
if arguments == nil {
// 如果无参数,根据函数被调用的顺序确定
if dataEventSink == nil {
dataEventSink = events
} else {
statusEventSink = events
}
}
}
public func onConnectionStateChanged(state: Int) {
if let eventSink = statusEventSink {
var stateStr = "Unknown"
switch state {
case BleConst.STATE_DISCONNECTED:
stateStr = "Disconnected"
case BleConst.STATE_CONNECTING:
stateStr = "Connecting"
case BleConst.STATE_CONNECTED:
stateStr = "Connected"
case BleConst.STATE_DISCONNECTING:
stateStr = "Disconnecting"
case BleConst.STATE_ERROR:
stateStr = "Connection Error"
default:
stateStr = "Unknown State(\(state))"
}
DispatchQueue.main.async {
eventSink([
"type": "connectionStateChanged",
"state": state,
"stateStr": stateStr
])
}
}
return nil
}
public func onCancel(withArguments arguments: Any?) -> FlutterError? {
if let args = arguments as? [String: Any], let type = args["type"] as? String {
if type == "data" {
dataEventSink = nil
} else if type == "status" {
statusEventSink = nil
public func onAudioDataReceived(data: Data) {
if let eventSink = dataEventSink {
DispatchQueue.main.async {
eventSink([
"type": "audioData",
"data": FlutterStandardTypedData(bytes: data)
])
}
}
}
public func onWakeupSignalReceived() {
if let eventSink = statusEventSink {
DispatchQueue.main.async {
eventSink([
"type": "wakeup"
])
}
}
}
public func onDeviceInfoReceived(infoType: Int, infoData: [String: Any]) {
if let eventSink = statusEventSink {
var resultMap = infoData
resultMap["type"] = "deviceInfo"
resultMap["result"] = infoType
DispatchQueue.main.async {
eventSink(resultMap)
}
} else {
// 同时清除两个sink
dataEventSink = nil
statusEventSink = nil
}
return nil
}
}
// MARK: - BleService.Callback
// MARK: - 独立的StreamHandler类
@available(iOS 13.0, *)
extension SwiftBleServicePlugin: BleService.Callback {
func onScanResult(devices: [[String : Any]]) {
sendEvent(statusEventSink,
data: ["type": "scanResult", "devices": devices],
errorPrefix: "发送扫描结果异常")
class BleDataStreamHandler: NSObject, FlutterStreamHandler {
private weak var plugin: SwiftBleServicePlugin?
init(_ plugin: SwiftBleServicePlugin) {
self.plugin = plugin
}
func onConnectionStateChanged(state: Int) {
var stateStr = "Unknown"
switch state {
case BleConst.STATE_DISCONNECTED:
stateStr = "Disconnected"
case BleConst.STATE_CONNECTING:
stateStr = "Connecting"
case BleConst.STATE_CONNECTED:
stateStr = "Connected"
case BleConst.STATE_DISCONNECTING:
stateStr = "Disconnecting"
case BleConst.STATE_ERROR:
stateStr = "Connection Error"
default:
stateStr = "Unknown State(\(state))"
}
let stateMap: [String: Any] = [
"type": "connectionStateChanged",
"state": state,
"stateStr": stateStr
]
sendEvent(statusEventSink, data: stateMap, errorPrefix: "发送连接状态异常")
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
plugin?.dataEventSink = events
return nil
}
func onAudioDataReceived(data: Data) {
let dataMap: [String: Any] = [
"type": "audioData",
"data": data
]
sendEvent(dataEventSink, data: dataMap, errorPrefix: "发送音频数据异常")
func onCancel(withArguments arguments: Any?) -> FlutterError? {
plugin?.dataEventSink = nil
return nil
}
}
@available(iOS 13.0, *)
class BleStatusStreamHandler: NSObject, FlutterStreamHandler {
private weak var plugin: SwiftBleServicePlugin?
func onDeviceInfoReceived(infoType: Int, infoData: [String : Any]) {
var dataMap: [String: Any] = [
"type": "deviceInfo",
"infoType": infoType
]
// 合并infoData
for (key, value) in infoData {
dataMap[key] = value
}
sendEvent(statusEventSink, data: dataMap, errorPrefix: "发送设备信息异常")
init(_ plugin: SwiftBleServicePlugin) {
self.plugin = plugin
}
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
plugin?.statusEventSink = events
return nil
}
func onCancel(withArguments arguments: Any?) -> FlutterError? {
plugin?.statusEventSink = nil
return nil
}
}

164
local_plugins/ble_service/ios/Classes/SwiftOpusAudioProcessor.swift

@ -0,0 +1,164 @@
import Foundation
import AVFoundation
import YbridOpus
@available(iOS 13.0, *)
class SwiftOpusAudioProcessor: NSObject {
protocol AudioDataCallback: AnyObject {
func onAudioDataReceived(data: Data)
}
private var isDecodingActive = false
private weak var callback: AudioDataCallback?
// Opus 解码器相关 - 使用 YbridOpus C API
private var decoder: OpaquePointer?
private var sampleRate: Int32 = 16000
private var channels: Int32 = 1
private let maxFrameSize: Int32 = 5760 // 120 ms @48 kHz 的最大值
override init() {
super.init()
}
deinit {
cleanup()
}
private func initializeOpusDecoder() -> Bool {
if decoder != nil {
return true
}
var error: Int32 = 0
decoder = opus_decoder_create(sampleRate, channels, &error)
guard error == OPUS_OK, decoder != nil else {
return false
}
return true
}
private func destroyOpusDecoder() {
if let decoder = decoder {
opus_decoder_destroy(decoder)
self.decoder = nil
}
}
func setCallback(_ callback: AudioDataCallback?) {
self.callback = callback
}
func startDecoding(sampleRate: Int32 = 16000, channels: Int32 = 1, frameSize: Int32 = 640) -> Bool {
if isDecodingActive {
return true
}
self.sampleRate = sampleRate
self.channels = channels
guard initializeOpusDecoder() else {
return false
}
isDecodingActive = true
return true
}
func stopDecoding() -> Bool {
if !isDecodingActive {
return false
}
isDecodingActive = false
destroyOpusDecoder()
return true
}
func processAudioData(_ data: Data) {
guard isDecodingActive, let decoder = decoder, data.count > 0 else {
return
}
// 过滤明显异常的数据包
guard data.count <= 1275 else {
return
}
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
self?.decodeOpusPacket(data)
}
}
// 简化的 Opus 解码方法,重点是容错处理
private func decodeOpusPacket(_ packet: Data) {
guard let decoder = decoder else { return }
let maxSamples = Int(maxFrameSize)
var pcmBuffer = [opus_int16](repeating: 0, count: maxSamples * Int(channels))
let decodedSamples = packet.withUnsafeBytes { packetPtr in
let uint8Ptr = packetPtr.bindMemory(to: UInt8.self).baseAddress
return opus_decode(
decoder,
uint8Ptr,
opus_int32(packet.count),
&pcmBuffer,
Int32(maxSamples),
0
)
}
// 简单的错误处理:失败时重新初始化解码器
if decodedSamples <= 0 {
print("Opus解码失败,重新初始化解码器")
resetDecoder()
return
}
// 提取解码的音频数据
let samplesPerChannel = Int(decodedSamples)
let totalSamples = samplesPerChannel * Int(channels)
let outputPCM = Array(pcmBuffer[0..<totalSamples])
if !outputPCM.isEmpty {
processPCMData(outputPCM)
}
}
// 重置解码器的简单方法
private func resetDecoder() {
destroyOpusDecoder()
_ = initializeOpusDecoder()
}
private func processPCMData(_ pcmData: [opus_int16]) {
// 转换为 Data
let data = Data(bytes: pcmData, count: pcmData.count * MemoryLayout<opus_int16>.size)
// 回调通知
DispatchQueue.main.async { [weak self] in
self?.callback?.onAudioDataReceived(data: data)
}
}
func isDecoding() -> Bool {
return isDecodingActive
}
func cleanup() {
stopDecoding()
}
// MARK: - 公共接口
/// 获取当前配置信息
func getCurrentConfig() -> (sampleRate: Int32, channels: Int32) {
return (sampleRate, channels)
}
}

11
local_plugins/ble_service/ios/ble_service.podspec

@ -1,5 +1,6 @@
#
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
# Run `pod lib lint ble_service.podspec` to validate before publishing.
#
Pod::Spec.new do |s|
s.name = 'ble_service'
@ -13,12 +14,16 @@ BLE服务插件,提供蓝牙扫描、连接和通信功能,支持iOS 18+ Acc
s.author = { 'Your Company' => 'email@example.com' }
s.source = { :path => '.' }
s.source_files = 'Classes/**/*'
s.info_plist = {
'NSAccessorySetupKitSupports' => ['Bluetooth']
}
s.dependency 'Flutter'
s.platform = :ios, '18.0'
# YbridOpus 依赖 (Swift Opus 封装)
s.dependency 'YbridOpus', '~> 0.8'
# 系统框架依赖
s.frameworks = 'AVFoundation', 'AudioToolbox'
s.libraries = 'iconv', 'c++'
# Flutter.framework does not contain a i386 slice.
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
s.swift_version = '5.0'

13
local_plugins/ble_service/lib/ble_service.dart

@ -245,7 +245,20 @@ class BleService {
return null;
}
}
/// 检查BLE连接状态
///
/// 返回BLE是否已连接
Future<bool> isBleConnected() async {
try {
final result = await _methodChannel.invokeMethod<bool>('isBleConnected');
return result ?? false;
} catch (e) {
print('检查BLE连接状态失败: $e');
return false;
}
}
/// 注册BLE后台扫描
///

58
local_plugins/ble_service/pubspec.yaml

@ -1,26 +1,72 @@
name: ble_service
description: BLE服务插件,提供蓝牙扫描、连接和通信功能
description: 蓝牙低功耗服务插件,提供蓝牙设备连接、通信功能
version: 0.0.1
homepage:
environment:
sdk: ">=2.16.0 <3.0.0"
flutter: ">=2.5.0"
sdk: ">=2.18.0 <4.0.0"
flutter: ">=3.3.0"
dependencies:
flutter:
sdk: flutter
# 添加其他依赖项
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^1.0.0
flutter_lints: ^2.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# This section identifies this Flutter project as a plugin project.
# The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.)
# which should be registered in the plugin registry. This is required for
# using method channels.
# The Android 'package' specifies package in which the registered class is.
# This is required for using method channels on Android.
# The 'ffiPlugin' specifies that native code should be built and bundled.
# This is required for using `dart:ffi`.
# All these are used by the tooling to maintain consistency when
# adding or updating assets for this project.
plugin:
platforms:
android:
package: com.yunqiinnovation.ble_service
pluginClass: BleServicePlugin
pluginClass: BleServicePlugin
ios:
pluginClass: BleServicePlugin
pluginClass: BleServicePlugin
# To add assets to your plugin package, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
#
# For details regarding assets in packages, see
# https://flutter.dev/assets-and-images/#from-packages
#
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware
# To add custom fonts to your plugin package, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts in packages, see
# https://flutter.dev/custom-fonts/#from-packages

84
local_plugins/chat_api/IOS_INTEGRATION_LEGACY.md

@ -0,0 +1,84 @@
# iOS 集成指南(旧版 CocoaPods 项目)- Chat API Plugin
> **注意**:本指南仅适用于未启用 Swift Package Manager 的旧版 Flutter 项目。
>
> 如果你的 Flutter 版本 >= 3.24,强烈建议启用 SPM 支持:
> ```bash
> flutter config --enable-swift-package-manager
> ```
> 启用后,插件会自动处理所有依赖,无需手动配置。
## 手动集成步骤(仅限 CocoaPods 项目)
由于 Flutter 插件中 CocoaPods 和 Swift Package Manager 的兼容性限制,需要手动在 Xcode 中添加 OpenAI 包依赖。
## 步骤说明
### 1. 运行 pod install
首先在项目根目录运行:
```bash
cd ios
pod install
```
### 2. 打开 Xcode 项目
打开生成的 workspace 文件:
```bash
open Runner.xcworkspace
```
### 3. 添加 Swift Package 依赖
1. 在 Xcode 中,选择左侧的 `Runner` 项目(蓝色图标)
2. 在主编辑区域,选择 `Runner` target
3. 点击顶部的 `Package Dependencies` 标签
4. 点击左下角的 `+` 按钮
5. 在弹出的窗口中输入:`https://github.com/MacPaw/OpenAI.git`
6. 点击 `Add Package`
7. 在版本选择界面:
- 选择 `Up to Next Major Version`
- 起始版本填写:`0.4.0`
8. 点击 `Add Package`
9. 在包产品选择界面,确保 `OpenAI` 被选中,并且目标是 `Runner`
10. 点击 `Add Package`
### 4. 验证集成
1. 在 Xcode 左侧导航栏中,展开 `Package Dependencies`
2. 确认能看到 `OpenAI` 包
3. 尝试构建项目(Cmd + B)确保没有错误
### 5. 可能的问题
#### 问题:构建时提示找不到 OpenAI 模块
**解决方案**:
1. 清理构建缓存:`Product > Clean Build Folder`
2. 关闭 Xcode
3. 删除 `DerivedData`:
```bash
rm -rf ~/Library/Developer/Xcode/DerivedData
```
4. 重新打开项目并构建
#### 问题:包依赖下载失败
**解决方案**:
1. 检查网络连接
2. 在 Xcode 中:`File > Packages > Reset Package Caches`
3. 重新添加包依赖
## 注意事项
- 每次运行 `pod install` 后,包依赖配置会保留
- 如果切换了 Xcode 版本,可能需要重新解析包依赖
- 建议将 `.xcworkspace` 文件加入版本控制,但排除 `xcuserdata` 目录
## 相关链接
- [MacPaw OpenAI Swift 库](https://github.com/MacPaw/OpenAI)
- [Swift Package Manager 文档](https://developer.apple.com/documentation/xcode/adding-package-dependencies-to-your-app)

0
local_plugins/open_ai_service/ios/Classes/.gitkeep → local_plugins/chat_api/LICENSE

239
local_plugins/chat_api/README.md

@ -0,0 +1,239 @@
# Chat API Plugin
基于 MacPaw OpenAI Swift 库的 Flutter 聊天 API 插件。
## 功能特性
- ✅ 支持 OpenAI Chat API 的完整功能
- ✅ 流式输出支持
- ✅ 函数调用支持
- ✅ 多模态支持(文本、图片)
- ✅ 自定义模型支持
- ✅ Token 计数估算
- ✅ 支持自定义 API 端点
- ✅ 自动 Swift Package Manager (SPM) 集成
## 安装
### 1. 启用 Swift Package Manager
确保你的 Flutter 版本 >= 3.24,并启用 SPM 支持:
```bash
flutter config --enable-swift-package-manager
```
### 2. 添加依赖
在你的 `pubspec.yaml` 文件中添加:
```yaml
dependencies:
chat_api:
path: ../local_plugins/chat_api
```
### 3. iOS 配置
运行以下命令,Flutter 会自动处理 SPM 依赖:
```bash
cd your_project
flutter pub get
cd ios
flutter run
```
首次运行时,Flutter 会自动:
- 迁移项目到 SPM 结构(如果还没有迁移)
- 下载并集成 MacPaw OpenAI Swift 包
- 配置所有必要的依赖
**注意**:不再需要手动在 Xcode 中添加包依赖!
### 4. Android 配置
当前 Android 端仅提供占位实现,如需完整功能请使用 iOS 平台。
## 使用方法
### 初始化
```dart
import 'package:chat_api/chat_api.dart';
final chatApi = ChatApi();
// 初始化
await chatApi.initialize(
apiKey: 'your-api-key',
organization: 'your-org-id', // 可选
model: 'gpt-3.5-turbo', // 默认模型
baseUrl: 'https://api.openai.com/v1/chat/completions', // 可选,自定义端点
);
```
### 发送消息(非流式)
```dart
// 创建消息列表
final messages = [
ChatMessage(
role: MessageRole.system,
content: '你是一个有帮助的助手。',
),
ChatMessage(
role: MessageRole.user,
content: '你好,请介绍一下你自己。',
),
];
// 发送消息
try {
final response = await chatApi.sendMessage(
messages: messages,
temperature: 0.7,
maxTokens: 1000,
);
print('回复: $response');
} catch (e) {
print('错误: $e');
}
```
### 发送消息(流式)
```dart
// 监听事件流
chatApi.eventStream.listen((event) {
switch (event.type) {
case ChatApiEventType.token:
// 收到新的token
print(event.content);
break;
case ChatApiEventType.complete:
// 对话完成
print('完成');
break;
case ChatApiEventType.error:
// 发生错误
print('错误: ${event.content}');
break;
case ChatApiEventType.functionCall:
// 函数调用
print('函数调用: ${event.content}');
break;
}
});
// 发送流式消息
await chatApi.sendMessageStream(
messages: messages,
temperature: 0.7,
);
```
### 函数调用
```dart
// 定义函数
final functions = [
{
'name': 'get_weather',
'description': '获取指定位置的天气',
'parameters': {
'type': 'object',
'properties': {
'location': {
'type': 'string',
'description': '城市名称',
},
},
'required': ['location'],
},
},
];
// 发送带函数的消息
await chatApi.sendMessageStream(
messages: messages,
functions: functions,
);
```
### 多模态支持
```dart
// 发送带图片的消息
final imageMessage = ChatMessage(
role: MessageRole.user,
content: [
{
'type': 'text',
'text': '这张图片里有什么?',
},
{
'type': 'image_url',
'image_url': {
'url': 'data:image/jpeg;base64,${base64EncodedImage}',
},
},
],
);
```
### 其他功能
```dart
// 获取可用模型列表
final models = await chatApi.getAvailableModels();
// 设置模型
await chatApi.setModel('gpt-4');
// 估算token数量
final tokenCount = await chatApi.countTokens('这是一段测试文本');
// 取消流式请求
await chatApi.cancelStream();
```
## 兼容性说明
### Swift Package Manager 项目
如果你的项目已启用 SPM(Flutter 3.24+),插件会自动通过 SPM 集成,无需额外配置。
### CocoaPods 项目
如果你的项目仍在使用 CocoaPods(或有其他插件未迁移到 SPM),插件会自动降级到 CocoaPods 模式。这种情况下,你需要手动在 Xcode 中添加 OpenAI 包依赖。详见 `IOS_INTEGRATION_LEGACY.md`。
## 故障排除
### 问题:构建失败,提示找不到 OpenAI 模块
**解决方案**:
1. 确保已启用 SPM:`flutter config --enable-swift-package-manager`
2. 清理并重新构建:
```bash
flutter clean
cd ios
rm -rf Pods Podfile.lock
flutter pub get
flutter run
```
### 问题:Xcode 中看不到 Package Dependencies
**解决方案**:
这可能意味着项目还在使用 CocoaPods 模式。运行 `flutter run` 应该会自动迁移到 SPM。
## 注意事项
1. **API 密钥安全**:不要将 API 密钥硬编码在代码中,建议使用环境变量或安全存储。
2. **错误处理**:始终使用 try-catch 包装 API 调用以处理可能的错误。
3. **流式请求**:记得在不需要时取消流式请求,避免资源浪费。
## 许可证
MIT License

108
local_plugins/chat_api/SPM_MIGRATION.md

@ -0,0 +1,108 @@
# Chat API 插件 SPM 迁移说明
## 迁移概述
`chat_api` 插件已经完成了 Swift Package Manager (SPM) 支持的迁移,现在可以自动处理 OpenAI Swift 包的依赖。
## 主要变更
### 1. 目录结构调整
```
ios/
├── chat_api.podspec # 保留,支持双轨模式
├── Classes/ # 已清空(源码已迁移)
└── chat_api/ # 新增 SPM 结构
├── Package.swift # SPM 包定义
└── Sources/
└── chat_api/ # 源码新位置
├── ChatApiPlugin.swift
└── ChatService.swift
```
### 2. Package.swift 配置
- 声明了 MacPaw OpenAI 包依赖
- 自动管理版本(0.4.0+)
- 设置最低 iOS 版本为 13.0
### 3. podspec 更新
- 更新了 `source_files` 路径指向新的 SPM 结构
- 添加了友好的迁移提示脚本
### 4. pubspec.yaml 更新
- 添加了 `dartPluginClass` 声明,明确支持 SPM
## 使用方式对比
### 旧方式(手动集成)
1. 运行 `pod install`
2. 打开 Xcode
3. 手动添加 OpenAI Swift Package
4. 配置依赖关系
### 新方式(自动集成)
1. 启用 SPM:`flutter config --enable-swift-package-manager`
2. 运行 `flutter run`
3. 完成!Flutter 自动处理所有依赖
## 兼容性
- **Flutter 3.24+**:完整 SPM 支持,推荐使用
- **Flutter < 3.24**:自动降级到 CocoaPods 模式
- **混合项目**:如果项目中有其他插件未迁移到 SPM,Flutter 会智能选择合适的集成方式
## 验证集成
### SPM 项目
```bash
# 在 example 目录下
flutter clean
flutter pub get
flutter run --verbose
```
成功标志:
- 看到 "Adding Swift Package Manager integration..." 消息
- Xcode 中 Package Dependencies 自动出现 OpenAI 包
- 构建成功,无需手动干预
### CocoaPods 项目
如果项目仍使用 CocoaPods,会看到:
- podspec 中的迁移提示
- 需要按照 `IOS_INTEGRATION_LEGACY.md` 手动添加依赖
## 故障排除
1. **清理缓存**
```bash
flutter clean
rm -rf ios/Pods ios/Podfile.lock
rm -rf ~/Library/Developer/Xcode/DerivedData
```
2. **重置 SPM 缓存**
- Xcode: File > Packages > Reset Package Caches
3. **确认 SPM 已启用**
```bash
flutter config
# 应该看到 enable-swift-package-manager: true
```
## 开发者注意事项
1. **源码位置**:所有 Swift 代码现在位于 `ios/chat_api/Sources/chat_api/`
2. **双轨支持**:保持 podspec 和 Package.swift 同时更新
3. **版本管理**:OpenAI 包版本在 Package.swift 中管理
## 未来计划
- 当 Flutter 生态系统完全迁移到 SPM 后,可以考虑移除 CocoaPods 支持
- 持续跟踪 MacPaw OpenAI 库的更新

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save