tanlongsheng 1 year ago
parent
commit
309cf47318
  1. 6
      .gitignore
  2. 10
      .mcp copy.json
  3. 10
      .metadata
  4. 1
      amap_flutter/android/settings.gradle
  5. 3
      amap_flutter/android/src/main/AndroidManifest.xml
  6. 169
      amap_flutter/android/src/main/java/com/example/amap_flutter/AmapFlutterPlugin.java
  7. 36
      amap_flutter/lib/amap_flutter.dart
  8. 45
      amap_flutter/lib/amap_flutter_method_channel.dart
  9. 49
      amap_flutter/lib/amap_flutter_platform_interface.dart
  10. 20
      android/app/build.gradle.kts
  11. 2
      android/app/src/main/kotlin/com/yunqiinnovation/deepsound/MainActivity.kt
  12. 2
      android/build.gradle.kts
  13. 5
      android/settings.gradle.kts
  14. 36
      assets/config/GoogleService-Info.plist
  15. 6
      ios/Podfile
  16. 204
      ios/Podfile.lock
  17. 224
      ios/Runner.xcodeproj/project.pbxproj
  18. 123
      ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
  19. 123
      ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved
  20. 730
      ios/Runner/AppDelegate.swift
  21. 589
      ios/Runner/AudioSessionManager.swift
  22. 813
      ios/Runner/AzureAsrHelper.swift
  23. 454
      ios/Runner/AzureTtsHelper.swift
  24. 13
      ios/Runner/Base.lproj/Main.storyboard
  25. 464
      ios/Runner/BluetoothMediaButtonHelper.swift
  26. 243
      ios/Runner/ClassicBluetoothHelper.swift
  27. 108
      ios/Runner/Info.plist
  28. 0
      ios/Runner/Resources/.gitkeep
  29. BIN
      ios/Runner/Resources/sample.mp3
  30. BIN
      ios/Runner/Resources/silence.mp3
  31. 6
      ios/Runner/Runner-Bridging-Header.h
  32. 5
      ios/Runner/Runner.entitlements
  33. 565
      ios/Runner/VoiceInteractionService.swift
  34. 391
      ios/Runner/VolcanoAIService.swift
  35. BIN
      ios/Runner/sample.mp3
  36. BIN
      ios/Runner/silence.mp3
  37. 4
      lib/core/themes/theme_service.dart
  38. 85
      lib/core/utils/logger.dart
  39. 49
      lib/data/services/ble_manager.dart
  40. 8
      lib/data/services/bluetooth_manager.dart
  41. 2
      lib/data/services/chat_history_service.dart
  42. 6
      lib/data/services/deapsound_ai_service.dart
  43. 6
      lib/data/services/kouzi_ai_service.dart
  44. 4
      lib/data/services/login_service.dart
  45. 29
      lib/data/services/navigation_manager.dart
  46. 10
      lib/data/services/network/api.dart
  47. 2
      lib/data/services/network/dio_manager.dart
  48. 4
      lib/data/services/speech_factory.dart
  49. 21
      lib/data/services/speech_impl/azure_asr_service.dart
  50. 22
      lib/data/services/speech_impl/azure_tts_service.dart
  51. 18
      lib/data/services/speech_impl/flutter_asr_service.dart
  52. 24
      lib/data/services/speech_impl/flutter_tts_service.dart
  53. 22
      lib/data/services/speech_impl/voice_clone_tts_service.dart
  54. 38
      lib/data/services/speech_impl/volcano_asr_api_service.dart
  55. 16
      lib/data/services/speech_impl/volcano_tts_api_service.dart
  56. 4
      lib/data/services/speech_impl/volcano_tts_service.dart
  57. 20
      lib/data/services/speech_impl/xunfei_asr_service.dart
  58. 18
      lib/data/services/voice_interaction_impl/android_voice_interaction_service.dart
  59. 12
      lib/data/services/voice_interaction_impl/ios_voice_interaction_service.dart
  60. 4
      lib/firebase_options.dart
  61. 4
      lib/main.dart
  62. 123
      lib/modules/agent/controllers/agent_controller.dart
  63. 84
      lib/modules/agent/views/agent_view.dart
  64. 577
      lib/modules/agent/views/message_bubble.dart
  65. 96
      lib/modules/agent/views/message_bubble_navigation.dart
  66. 24
      lib/modules/chat/controllers/chat_controller.dart
  67. 3
      lib/modules/home/controllers/home_controller.dart
  68. 11
      lib/modules/jieli_test/bindings/jieli_test_binding.dart
  69. 323
      lib/modules/jieli_test/controllers/jieli_test_controller.dart
  70. 568
      lib/modules/jieli_test/views/jieli_test_view.dart
  71. 8
      lib/modules/meeting/controllers/meeting_details_controller.dart
  72. 124
      lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart
  73. 130
      lib/modules/pairing/controllers/pairing_controller.dart
  74. 43
      lib/modules/profile/controllers/profile_controller.dart
  75. 18
      lib/modules/speech_test/controllers/speech_test_controller.dart
  76. 27
      lib/modules/splash/controllers/splash_controller.dart
  77. 26
      lib/modules/translation/controllers/translation_controller.dart
  78. 8
      lib/modules/translation/controllers/translation_detail_controller.dart
  79. 9
      lib/routes/app_pages.dart
  80. 4
      local_plugins/agent_service/android/build.gradle.kts
  81. 13
      local_plugins/agent_service/android/settings.gradle.kts
  82. 464
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
  83. 30
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt
  84. 28
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt
  85. 57
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt
  86. 14
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/utils.kt
  87. 12
      local_plugins/agent_service/pubspec.yaml
  88. 0
      local_plugins/amap_service/.gitignore
  89. 14
      local_plugins/amap_service/.metadata
  90. 0
      local_plugins/amap_service/CHANGELOG.md
  91. 0
      local_plugins/amap_service/LICENSE
  92. 2
      local_plugins/amap_service/README.md
  93. 0
      local_plugins/amap_service/analysis_options.yaml
  94. 0
      local_plugins/amap_service/android/.gitignore
  95. 27
      local_plugins/amap_service/android/build.gradle
  96. 1
      local_plugins/amap_service/android/settings.gradle
  97. 48
      local_plugins/amap_service/android/src/main/AndroidManifest.xml
  98. 54
      local_plugins/amap_service/android/src/main/kotlin/com/yunqiinnovation/amap_service/AmapServicePlugin.kt
  99. 314
      local_plugins/amap_service/android/src/main/kotlin/com/yunqiinnovation/amap_service/NavigationService.kt
  100. 34
      local_plugins/amap_service/android/src/main/kotlin/com/yunqiinnovation/amap_service/NavigationServiceStarter.kt

6
.gitignore

@ -136,3 +136,9 @@ coverage/
*.js_
*.js.deps
*.js.map
linux/flutter/generated_plugin_registrant.cc
linux/flutter/generated_plugin_registrant.h
linux/flutter/generated_plugins.cmake
windows/flutter/generated_plugin_registrant.cc
windows/flutter/generated_plugin_registrant.h
windows/flutter/generated_plugins.cmake

10
.mcp copy.json

@ -1,10 +0,0 @@
{
"mcpServers": {
"web-search": {
"url": "http://mcp.ideapsound.com:8000/sse"
},
"amap-amap-sse": {
"url": "https://mcp.amap.com/sse?key=df7b0b6747c632fc38ddad22e7176646"
}
}
}

10
.metadata

@ -4,7 +4,7 @@
# This file should be version controlled and should not be manually edited.
version:
revision: "35c388afb57ef061d06a39b537336c87e0e3d1b1"
revision: "ea121f8859e4b13e47a8f845e4586164519588bc"
channel: "stable"
project_type: app
@ -13,11 +13,11 @@ project_type: app
migration:
platforms:
- platform: root
create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
- platform: ios
create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
# User provided section

1
amap_flutter/android/settings.gradle

@ -1 +0,0 @@
rootProject.name = 'amap_flutter'

3
amap_flutter/android/src/main/AndroidManifest.xml

@ -1,3 +0,0 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.amap_flutter">
</manifest>

169
amap_flutter/android/src/main/java/com/example/amap_flutter/AmapFlutterPlugin.java

@ -1,169 +0,0 @@
package com.example.amap_flutter;
import android.content.Context;
import androidx.annotation.NonNull;
import android.util.Log;
import com.amap.api.location.AMapLocation;
import com.amap.api.location.AMapLocationClient;
import com.amap.api.location.AMapLocationClientOption;
import com.amap.api.location.AMapLocationListener;
import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.plugin.common.EventChannel;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugin.common.MethodChannel.Result;
import java.util.HashMap;
import java.util.Map;
/** AmapFlutterPlugin */
public class AmapFlutterPlugin implements FlutterPlugin, MethodCallHandler {
/// The MethodChannel that will the communication between Flutter and native Android
///
/// This local reference serves to register the plugin with the Flutter Engine and unregister it
/// when the Flutter Engine is detached from the Activity
private MethodChannel methodChannel;
private EventChannel eventChannel;
private Context applicationContext;
private AMapLocationClient mLocationClient;
private EventChannel.EventSink eventSink;
@Override
public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
applicationContext = flutterPluginBinding.getApplicationContext();
methodChannel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), "amap_flutter");
methodChannel.setMethodCallHandler(this);
// 初始化EventChannel
eventChannel = new EventChannel(flutterPluginBinding.getBinaryMessenger(), "amap_flutter/events");
eventChannel.setStreamHandler(new EventChannel.StreamHandler() {
@Override
public void onListen(Object arguments, EventChannel.EventSink events) {
eventSink = events;
}
@Override
public void onCancel(Object arguments) {
eventSink = null;
}
});
}
@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
switch (call.method) {
case "setApiKey":
setApiKey(call, result);
break;
case "initLocation":
initLocation(result);
break;
case "startLocation":
startLocation(result);
break;
case "stopLocation":
stopLocation(result);
break;
case "dispose":
dispose(result);
break;
default:
result.notImplemented();
}
}
private void setApiKey(MethodCall call, Result result) {
try {
String apiKey = call.argument("androidKey");
AMapLocationClient.updatePrivacyShow(applicationContext, true, true);
AMapLocationClient.updatePrivacyAgree(applicationContext, true);
result.success(null);
} catch (Exception e) {
result.error("INIT_ERROR", "初始化失败", e.getMessage());
}
}
private void initLocation(Result result) {
try {
mLocationClient = new AMapLocationClient(applicationContext);
// 配置定位参数
AMapLocationClientOption option = new AMapLocationClientOption();
option.setOnceLocation(true);
option.setNeedAddress(true);
mLocationClient.setLocationOption(option);
mLocationClient.setLocationListener(mLocationListener);
result.success(null);
} catch (Exception e) {
result.error("INIT_ERROR", "初始化失败", e.getMessage());
}
}
private void startLocation(Result result) {
mLocationClient.startLocation();
result.success(null);
}
private void stopLocation(Result result) {
destroyLocationClient();
result.success(null);
}
private void dispose(Result result) {
try {
destroyLocationClient();
result.success(null);
} catch (Exception e) {
result.error("DISPOSE_ERROR", "资源释放失败", e.getMessage());
}
}
private void destroyLocationClient() {
if (mLocationClient != null) {
try {
// 停止定位
mLocationClient.stopLocation();
// 销毁定位客户端
mLocationClient.onDestroy();
mLocationClient = null;
// 关闭事件通道
if (eventSink != null) {
eventSink.endOfStream();
eventSink = null;
}
} catch (Exception e) {
Log.e("AmapFlutter", "销毁定位客户端失败", e);
}
}
}
AMapLocationListener mLocationListener = new AMapLocationListener() {
@Override
public void onLocationChanged(AMapLocation amapLocation) {
if (eventSink == null) return;
if (amapLocation != null) {
if (amapLocation.getErrorCode() == 0) {
Map<String, Object> locationData = new HashMap<>();
locationData.put("latitude", amapLocation.getLatitude());
locationData.put("longitude", amapLocation.getLongitude());
locationData.put("city", amapLocation.getCity());
locationData.put("address", amapLocation.getAddress());
eventSink.success(locationData);
}
}
}
};
@Override
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
methodChannel.setMethodCallHandler(null);
eventChannel.setStreamHandler(null);
destroyLocationClient();
}
}

36
amap_flutter/lib/amap_flutter.dart

@ -1,36 +0,0 @@
import 'amap_flutter_platform_interface.dart';
class AmapFlutter {
// 初始化SDK
Future<void> initSdk({String? iosKey, String? androidKey}) async {
await AmapFlutterPlatform.instance.setApiKey(
iosKey: iosKey,
androidKey: androidKey,
);
}
// 定位初始化
Future<void> initLocation() async {
return AmapFlutterPlatform.instance.initLocation();
}
// 开始定位
Future<void> startLocation() async {
return AmapFlutterPlatform.instance.startLocation();
}
// 监听定位返回位置
Stream<dynamic> locationStream() {
return AmapFlutterPlatform.instance.locationStream();
}
// 停止定位
Future<void> stopLocation() async {
await AmapFlutterPlatform.instance.stopLocation();
}
// 销毁
Future<void> dispose() async {
await AmapFlutterPlatform.instance.dispose();
}
}

45
amap_flutter/lib/amap_flutter_method_channel.dart

@ -1,45 +0,0 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'amap_flutter_platform_interface.dart';
/// An implementation of [AmapFlutterPlatform] that uses method channels.
class MethodChannelAmapFlutter extends AmapFlutterPlatform {
/// The method channel used to interact with the native platform.
@visibleForTesting
final methodChannel = const MethodChannel('amap_flutter');
final eventChannel = const EventChannel('amap_flutter/events');
@override
Future<void> setApiKey({String? iosKey, String? androidKey}) async {
await methodChannel.invokeMethod('setApiKey', {
"iosKey": iosKey,
"androidKey": androidKey,
});
}
@override
Future<void> initLocation() async {
return await methodChannel.invokeMethod('initLocation');
}
@override
Future<void> startLocation() async {
return await methodChannel.invokeMethod('startLocation');
}
@override
Stream<dynamic> locationStream() {
return eventChannel.receiveBroadcastStream();
}
@override
Future<void> stopLocation() async {
await methodChannel.invokeMethod('stopLocation');
}
@override
Future<void> dispose() async {
await methodChannel.invokeMethod('dispose');
}
}

49
amap_flutter/lib/amap_flutter_platform_interface.dart

@ -1,49 +0,0 @@
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
import 'amap_flutter_method_channel.dart';
abstract class AmapFlutterPlatform extends PlatformInterface {
/// Constructs a AmapFlutterPlatform.
AmapFlutterPlatform() : super(token: _token);
static final Object _token = Object();
static AmapFlutterPlatform _instance = MethodChannelAmapFlutter();
/// The default instance of [AmapFlutterPlatform] to use.
///
/// Defaults to [MethodChannelAmapFlutter].
static AmapFlutterPlatform get instance => _instance;
/// Platform-specific implementations should set this with their own
/// platform-specific class that extends [AmapFlutterPlatform] when
/// they register themselves.
static set instance(AmapFlutterPlatform instance) {
PlatformInterface.verifyToken(instance, _token);
_instance = instance;
}
Future<void> setApiKey({String? iosKey, String? androidKey}) {
throw UnimplementedError('init() has not been implemented.');
}
Future<void> initLocation() {
throw UnimplementedError('initLocation() has not been implemented.');
}
Future<void> startLocation() {
throw UnimplementedError('startLocation() has not been implemented.');
}
Stream<dynamic> locationStream() {
throw UnimplementedError('locationStream() has not been implemented.');
}
Future<void> stopLocation() {
throw UnimplementedError('stopLocation() has not been implemented.');
}
Future<void> dispose() {
throw UnimplementedError('dispose() has not been implemented.');
}
}

20
android/app/build.gradle.kts

@ -120,28 +120,8 @@ android {
}
dependencies {
implementation("io.modelcontextprotocol:kotlin-sdk:0.4.0")
// 添加本地插件模块依赖
implementation(project(":spotify_sdk"))
implementation(project(":deepvoice_utils"))
implementation(project(":ble_service"))
// 添加OkHttp依赖
implementation("com.squareup.okhttp3:okhttp:4.9.3")
// 添加核心库反糖化
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.3")
// 添加androidx.media依赖
implementation("androidx.media:media:1.6.0")
// 添加legacy-support-v4依赖
implementation("androidx.legacy:legacy-support-v4:1.0.0")
}
flutter {

2
android/app/src/main/kotlin/com/yunqiinnovation/deepsound/MainActivity.kt

@ -12,7 +12,7 @@ import io.flutter.embedding.android.FlutterActivity
import android.content.Context
import android.os.Handler
import android.os.Looper
import com.yunqiinnovation.deepvoice_utils.FileLogger
import android.util.Log
class MainActivity: FlutterActivity() {
private val TAG = "MainActivity"

2
android/build.gradle.kts

@ -35,7 +35,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.10")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.20")
// 其他依赖...
}
}

5
android/settings.gradle.kts

@ -37,9 +37,9 @@ include(":chat_storage")
include(":jieli")
include(":spotify_sdk")
include(":ble_service")
include(":deepvoice_utils")
include(":classic_bluetooth")
include(":agent_service")
include(":speech")
// 设置azure_speech项目的路径
project(":azure_speech").projectDir = file("../local_plugins/azure_speech/android")
project(":open_ai_service").projectDir = file("../local_plugins/open_ai_service/android")
@ -47,7 +47,6 @@ project(":chat_storage").projectDir = file("../local_plugins/chat_storage/androi
project(":jieli").projectDir = file("../local_plugins/jieli/android")
project(":spotify_sdk").projectDir = file("../local_plugins/spotify_sdk/android")
project(":ble_service").projectDir = file("../local_plugins/ble_service/android")
project(":deepvoice_utils").projectDir = file("../local_plugins/deepvoice_utils/android")
project(":classic_bluetooth").projectDir = file("../local_plugins/classic_bluetooth/android")
project(":agent_service").projectDir = file("../local_plugins/agent_service/android")
project(":speech").projectDir = file("../local_plugins/speech/android")

36
assets/config/GoogleService-Info.plist

@ -0,0 +1,36 @@
<?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>CLIENT_ID</key>
<string>907754492821-oc9e77svgmmlgouo0op7vv00e1hb8278.apps.googleusercontent.com</string>
<key>REVERSED_CLIENT_ID</key>
<string>com.googleusercontent.apps.907754492821-oc9e77svgmmlgouo0op7vv00e1hb8278</string>
<key>ANDROID_CLIENT_ID</key>
<string>907754492821-9jakqvathu61e6b8g95i41ssi7o4dnka.apps.googleusercontent.com</string>
<key>API_KEY</key>
<string>AIzaSyAvyBQcOXuO_q851vevqZIufsPRTMCe808</string>
<key>GCM_SENDER_ID</key>
<string>907754492821</string>
<key>PLIST_VERSION</key>
<string>1</string>
<key>BUNDLE_ID</key>
<string>com.yunqiinnovation.deepsound</string>
<key>PROJECT_ID</key>
<string>deepsound-e0e3d</string>
<key>STORAGE_BUCKET</key>
<string>deepsound-e0e3d.firebasestorage.app</string>
<key>IS_ADS_ENABLED</key>
<false></false>
<key>IS_ANALYTICS_ENABLED</key>
<false></false>
<key>IS_APPINVITE_ENABLED</key>
<true></true>
<key>IS_GCM_ENABLED</key>
<true></true>
<key>IS_SIGNIN_ENABLED</key>
<true></true>
<key>GOOGLE_APP_ID</key>
<string>1:907754492821:ios:246794fc44b23174145c9c</string>
</dict>
</plist>

6
ios/Podfile

@ -1,4 +1,4 @@
# Uncomment this line to define a global platform for your project
# Define a global platform for your project
platform :ios, '13.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
@ -31,10 +31,6 @@ target 'Runner' do
use_frameworks!
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
# 添加 Microsoft Cognitive Services Speech SDK
pod 'MicrosoftCognitiveServicesSpeech-iOS', '~> 1.34.0'
target 'RunnerTests' do
inherit! :search_paths
end

204
ios/Podfile.lock

@ -11,8 +11,43 @@ PODS:
- Flutter
- audio_waveforms (0.0.1):
- Flutter
- 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):
@ -21,36 +56,39 @@ PODS:
- FBSDKCoreKit_Basics (17.1.0)
- FBSDKLoginKit (17.1.0):
- FBSDKCoreKit (= 17.1.0)
- Firebase/Auth (11.8.0):
- file_picker (0.0.1):
- DKImagePickerController/PhotoGallery
- Flutter
- Firebase/Auth (11.10.0):
- Firebase/CoreOnly
- FirebaseAuth (~> 11.8.0)
- Firebase/CoreOnly (11.8.0):
- FirebaseCore (~> 11.8.0)
- firebase_auth (5.5.1):
- Firebase/Auth (= 11.8.0)
- 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.12.1):
- Firebase/CoreOnly (= 11.8.0)
- firebase_core (3.13.0):
- Firebase/CoreOnly (= 11.10.0)
- Flutter
- FirebaseAppCheckInterop (11.10.0)
- FirebaseAuth (11.8.1):
- FirebaseAppCheckInterop (11.12.0)
- FirebaseAuth (11.10.0):
- FirebaseAppCheckInterop (~> 11.0)
- FirebaseAuthInterop (~> 11.0)
- FirebaseCore (~> 11.8.0)
- FirebaseCoreExtension (~> 11.8.0)
- FirebaseCore (~> 11.10.0)
- FirebaseCoreExtension (~> 11.10.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
- GoogleUtilities/Environment (~> 8.0)
- GTMSessionFetcher/Core (< 5.0, >= 3.4)
- RecaptchaInterop (~> 100.0)
- FirebaseAuthInterop (11.10.0)
- FirebaseCore (11.8.1):
- FirebaseCoreInternal (~> 11.8.0)
- 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.8.0):
- FirebaseCore (~> 11.8.0)
- FirebaseCoreInternal (11.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):
@ -86,25 +124,25 @@ PODS:
- AppAuth (< 2.0, >= 1.7.3)
- GTMAppAuth (< 5.0, >= 4.1.1)
- GTMSessionFetcher/Core (~> 3.3)
- GoogleUtilities/AppDelegateSwizzler (8.0.2):
- GoogleUtilities/AppDelegateSwizzler (8.1.0):
- GoogleUtilities/Environment
- GoogleUtilities/Logger
- GoogleUtilities/Network
- GoogleUtilities/Privacy
- GoogleUtilities/Environment (8.0.2):
- GoogleUtilities/Environment (8.1.0):
- GoogleUtilities/Privacy
- GoogleUtilities/Logger (8.0.2):
- GoogleUtilities/Logger (8.1.0):
- GoogleUtilities/Environment
- GoogleUtilities/Privacy
- GoogleUtilities/Network (8.0.2):
- GoogleUtilities/Network (8.1.0):
- GoogleUtilities/Logger
- "GoogleUtilities/NSData+zlib"
- GoogleUtilities/Privacy
- GoogleUtilities/Reachability
- "GoogleUtilities/NSData+zlib (8.0.2)":
- "GoogleUtilities/NSData+zlib (8.1.0)":
- GoogleUtilities/Privacy
- GoogleUtilities/Privacy (8.0.2)
- GoogleUtilities/Reachability (8.0.2):
- GoogleUtilities/Privacy (8.1.0)
- GoogleUtilities/Reachability (8.1.0):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- GTMAppAuth (4.1.1):
@ -115,8 +153,12 @@ PODS:
- 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
@ -135,13 +177,27 @@ PODS:
- Mantle (2.2.0):
- Mantle/extobjc (= 2.2.0)
- Mantle/extobjc (2.2.0)
- MicrosoftCognitiveServicesSpeech-iOS (1.34.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
- RecaptchaInterop (100.0.0)
- QCloudCore (6.4.7):
- QCloudCore/Default (= 6.4.7)
- QCloudCore/Default (6.4.7):
- QCloudTrack/Beacon (= 6.4.7)
- QCloudCOSXML (6.4.7):
- QCloudCOSXML/Default (= 6.4.7)
- 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):
@ -156,17 +212,29 @@ PODS:
- 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)
DEPENDENCIES:
- app_settings (from `.symlinks/plugins/app_settings/ios`)
- audio_session (from `.symlinks/plugins/audio_session/ios`)
- audio_waveforms (from `.symlinks/plugins/audio_waveforms/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`)
@ -178,19 +246,28 @@ DEPENDENCIES:
- 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`)
- MicrosoftCognitiveServicesSpeech-iOS (~> 1.34.0)
- 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`)
- 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
@ -208,10 +285,13 @@ SPEC REPOS:
- GTMSessionFetcher
- libwebp
- Mantle
- MicrosoftCognitiveServicesSpeech-iOS
- QCloudCore
- QCloudCOSXML
- QCloudTrack
- RecaptchaInterop
- SDWebImage
- SDWebImageWebPCoder
- SwiftyGif
- Try
- WechatOpenSDK-XCFramework
@ -222,8 +302,14 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/audio_session/ios"
audio_waveforms:
:path: ".symlinks/plugins/audio_waveforms/ios"
chat_storage:
:path: ".symlinks/plugins/chat_storage/ios"
classic_bluetooth:
: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:
@ -246,10 +332,20 @@ EXTERNAL SOURCES:
: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:
@ -260,28 +356,39 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/share_plus/ios"
speech_to_text:
:path: ".symlinks/plugins/speech_to_text/darwin"
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
audio_waveforms: a6dde7fe7c0ea05f06ffbdb0f7c1b2b2ba6cedcf
chat_storage: 933a96144157a07a2fef7f6acb28c78ff18465d8
classic_bluetooth: c5e5c0c86acab8dcd6b8e9cef7fca654c3518034
device_info_plus: 71ffc6ab7634ade6267c7a93088ed7e4f74e5896
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
FBAEMKit: cb719c53575a3be86ea873279f30d6a2c4e15881
FBSDKCoreKit: ecdb980a24633ccb012700299ceb16d0235e14d2
FBSDKCoreKit_Basics: 045101c4a9ef10c845347424d73a29aae02c3e43
FBSDKLoginKit: 69eb59b2f839aba635616df6e422acd0ca88030a
Firebase: d80354ed7f6df5f9aca55e9eb47cc4b634735eaf
firebase_auth: 9ebbd83276bf977dea1c74dfc199acf9d2a2f42f
firebase_core: 8d552814f6c01ccde5d88939fced4ec26f2f5510
FirebaseAppCheckInterop: 9664c858489710f682766ef54e2b6741d3b62070
FirebaseAuth: ad59a1a7b161e75f74c39f70179d2482d40e2737
FirebaseAuthInterop: 01a804fb074424fd58b92dd50dd0272277199356
FirebaseCore: 99fe0c4b44a39f37d99e6404e02009d2db5d718d
FirebaseCoreExtension: 3d3f2017a00d06e09ab4ebe065391b0bb642565e
FirebaseCoreInternal: df24ce5af28864660ecbd13596fc8dd3a8c34629
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
@ -292,26 +399,37 @@ SPEC CHECKSUMS:
fluwx: 6bf9c5a3a99ad31b0de137dd92370a0d10a60f4b
google_sign_in_ios: 19297361f2c51d7d8ac0201b866ef1fa5d1f94a8
GoogleSignIn: d4281ab6cf21542b1cfaff85c191f230b399d2db
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
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: 723eccb3e459bbb530df3d9bfe84a291074b09eb
music_service: 85abecc8fa21599a06907ad50d83f48a58e0dc0d
open_ai_service: 82ea947b1a081a2a17bc0c8e5467dcbd7cf9b232
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
RecaptchaInterop: 7d1a4a01a6b2cb1610a47ef3f85f0c411434cb21
QCloudCore: 49e7571984a8e5fd8f0fd58c2f3b806255796223
QCloudCOSXML: 7205e76aa9cf613468222615483c9d7c074e4298
QCloudTrack: 3b53a7fc4fe3920e407f2aa73f2452992a61f7f3
RecaptchaInterop: 11e0b637842dfb48308d242afc3f448062325aba
record_darwin: fb1f375f1d9603714f55b8708a903bbb91ffdb0a
SDWebImage: f84b0feeb08d2d11e6a9b843cb06d75ebf5b8868
SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
speech_to_text: 9dc43a5df3cbc2813f8c7cc9bd0fbf94268ed7ac
spotify_sdk: a48400bb29f70c4fe251ebfdc9135c37097ac5ca
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
tencentcloud_cos_sdk_plugin: c3d78b2c3bae42b5a20009dc047f3f1621e6272b
Try: 5ef669ae832617b3cee58cb2c6f99fb767a4ff96
webview_flutter_wkwebview: 1821ceac936eba6f7984d89a9f3bcb4dea99ebb2
WechatOpenSDK-XCFramework: 36fb2bea0754266c17184adf4963d7e6ff98b69f
PODFILE CHECKSUM: 4f26889ae572e1722a139b651939749d40c71469
PODFILE CHECKSUM: 017ca79b199d8f37c4acb8d83ec912447003b920
COCOAPODS: 1.16.2

224
ios/Runner.xcodeproj/project.pbxproj

@ -7,25 +7,16 @@
objects = {
/* Begin PBXBuildFile section */
0A5EDBFC2D89C4FD00215E26 /* ClassicBluetoothHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A5EDBFB2D89C4FD00215E26 /* ClassicBluetoothHelper.swift */; };
0A5EDC042D8B544100215E26 /* BluetoothMediaButtonHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A5EDC032D8B544100215E26 /* BluetoothMediaButtonHelper.swift */; };
0A7A32E42D8DBA6D0047F746 /* VoiceInteractionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A7A32E22D8DBA6D0047F746 /* VoiceInteractionService.swift */; };
0A7A32E52D8DBA6D0047F746 /* VolcanoAIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A7A32E32D8DBA6D0047F746 /* VolcanoAIService.swift */; };
0ADE29692D8C25660032BFCB /* silence.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 0ADE29682D8C25660032BFCB /* silence.mp3 */; };
0ADE296A2D8C25660032BFCB /* sample.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 0ADE29672D8C25660032BFCB /* sample.mp3 */; };
0ADE296D2D8C66D60032BFCB /* AzureTtsHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ADE296C2D8C66D60032BFCB /* AzureTtsHelper.swift */; };
0ADE296E2D8C66D60032BFCB /* AzureAsrHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ADE296B2D8C66D60032BFCB /* AzureAsrHelper.swift */; };
0ADE29702D8C6C450032BFCB /* AudioSessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ADE296F2D8C6C450032BFCB /* AudioSessionManager.swift */; };
0AD944122DD9DB5C00557D21 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 0AD944112DD9DB5C00557D21 /* GoogleService-Info.plist */; };
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3406320CAE3314A73EF78043 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5BBE10AD7954527B6BB12CFB /* Pods_RunnerTests.framework */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
591293932D9524D1004A65BB /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 591293922D9524D1004A65BB /* GoogleService-Info.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 */; };
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 */; };
E1D2128FAB4726BEEED42F4C /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4A9190D0838960F143BDC158 /* Pods_Runner.framework */; };
A7986DFCFDA104638080424B /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F79ADB762804BBED65875C03 /* Pods_RunnerTests.framework */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -52,32 +43,18 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
02CF74744224D1C933681603 /* 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>"; };
0906EF7A73CBA5EAFC64E05D /* 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>"; };
0A5EDBFB2D89C4FD00215E26 /* ClassicBluetoothHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClassicBluetoothHelper.swift; sourceTree = "<group>"; };
0A5EDC032D8B544100215E26 /* BluetoothMediaButtonHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothMediaButtonHelper.swift; sourceTree = "<group>"; };
0A7A32E22D8DBA6D0047F746 /* VoiceInteractionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceInteractionService.swift; sourceTree = "<group>"; };
0A7A32E32D8DBA6D0047F746 /* VolcanoAIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VolcanoAIService.swift; sourceTree = "<group>"; };
0ADE29672D8C25660032BFCB /* sample.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = sample.mp3; sourceTree = "<group>"; };
0ADE29682D8C25660032BFCB /* silence.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = silence.mp3; sourceTree = "<group>"; };
0ADE296B2D8C66D60032BFCB /* AzureAsrHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AzureAsrHelper.swift; sourceTree = "<group>"; };
0ADE296C2D8C66D60032BFCB /* AzureTtsHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AzureTtsHelper.swift; sourceTree = "<group>"; };
0ADE296F2D8C6C450032BFCB /* AudioSessionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioSessionManager.swift; sourceTree = "<group>"; };
0AD944112DD9DB5C00557D21 /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
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>"; };
4A9190D0838960F143BDC158 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
5912938C2D9520CE004A65BB /* RunnerRelease.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerRelease.entitlements; sourceTree = "<group>"; };
591293922D9524D1004A65BB /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
5BBE10AD7954527B6BB12CFB /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
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>"; };
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>"; };
77D16D761891F29239C8DF02 /* 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>"; };
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>"; };
8C1130E20389847CBD7B1AAC /* 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>"; };
930FE6D58BDB20688CE0A7CF /* 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>"; };
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; };
@ -85,7 +62,12 @@
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>"; };
9B84B894288BF76332D3F407 /* 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>"; };
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>"; };
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; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -93,40 +75,49 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
E1D2128FAB4726BEEED42F4C /* Pods_Runner.framework in Frameworks */,
6077538D78AAB34545D7F3DD /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
B4459155A2535A0ED1D6C7D8 /* Frameworks */ = {
B3515CD9628E3332AFC88CD4 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
3406320CAE3314A73EF78043 /* Pods_RunnerTests.framework in Frameworks */,
A7986DFCFDA104638080424B /* Pods_RunnerTests.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
331C8082294A63A400263BE5 /* RunnerTests */ = {
1ACB686EC7DC36C6E33D372F /* Pods */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
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 */,
);
path = RunnerTests;
path = Pods;
sourceTree = "<group>";
};
776E5AB1CE0A62CBDBAB3996 /* Pods */ = {
1E5DDD8701F6FB27AA502080 /* Frameworks */ = {
isa = PBXGroup;
children = (
8C1130E20389847CBD7B1AAC /* Pods-Runner.debug.xcconfig */,
77D16D761891F29239C8DF02 /* Pods-Runner.release.xcconfig */,
0906EF7A73CBA5EAFC64E05D /* Pods-Runner.profile.xcconfig */,
930FE6D58BDB20688CE0A7CF /* Pods-RunnerTests.debug.xcconfig */,
02CF74744224D1C933681603 /* Pods-RunnerTests.release.xcconfig */,
9B84B894288BF76332D3F407 /* Pods-RunnerTests.profile.xcconfig */,
E27AD0DBE6531F2655DB9986 /* Pods_Runner.framework */,
F79ADB762804BBED65875C03 /* Pods_RunnerTests.framework */,
);
path = Pods;
name = Frameworks;
sourceTree = "<group>";
};
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
@ -147,8 +138,8 @@
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
776E5AB1CE0A62CBDBAB3996 /* Pods */,
A5A9FDE94E78257E9A48E2E8 /* Frameworks */,
1ACB686EC7DC36C6E33D372F /* Pods */,
1E5DDD8701F6FB27AA502080 /* Frameworks */,
);
sourceTree = "<group>";
};
@ -164,18 +155,8 @@
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
5912938C2D9520CE004A65BB /* RunnerRelease.entitlements */,
0A7A32E22D8DBA6D0047F746 /* VoiceInteractionService.swift */,
0A7A32E32D8DBA6D0047F746 /* VolcanoAIService.swift */,
0ADE296F2D8C6C450032BFCB /* AudioSessionManager.swift */,
0ADE296B2D8C66D60032BFCB /* AzureAsrHelper.swift */,
0ADE296C2D8C66D60032BFCB /* AzureTtsHelper.swift */,
0ADE29672D8C25660032BFCB /* sample.mp3 */,
0ADE29682D8C25660032BFCB /* silence.mp3 */,
0A5EDC032D8B544100215E26 /* BluetoothMediaButtonHelper.swift */,
0A5EDBFB2D89C4FD00215E26 /* ClassicBluetoothHelper.swift */,
0AD944112DD9DB5C00557D21 /* GoogleService-Info.plist */,
97C146FA1CF9000F007C117D /* Main.storyboard */,
591293922D9524D1004A65BB /* GoogleService-Info.plist */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
@ -183,19 +164,11 @@
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
D2958658C06A5ADF62D6ABA2 /* Runner.entitlements */,
);
path = Runner;
sourceTree = "<group>";
};
A5A9FDE94E78257E9A48E2E8 /* Frameworks */ = {
isa = PBXGroup;
children = (
4A9190D0838960F143BDC158 /* Pods_Runner.framework */,
5BBE10AD7954527B6BB12CFB /* Pods_RunnerTests.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@ -203,10 +176,10 @@
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
1BD7F444CF127CDFCB7F9F47 /* [CP] Check Pods Manifest.lock */,
D21548F12B1535261EB87672 /* [CP] Check Pods Manifest.lock */,
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
B4459155A2535A0ED1D6C7D8 /* Frameworks */,
B3515CD9628E3332AFC88CD4 /* Frameworks */,
);
buildRules = (
);
@ -222,15 +195,15 @@
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
8A6DB356BF41E96BFD7DF109 /* [CP] Check Pods Manifest.lock */,
90215E32C3610039A2575557 /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
4A5C19D622EF675DB4056F07 /* [CP] Embed Pods Frameworks */,
D49299AF1487DF3783287D24 /* [CP] Copy Pods Resources */,
510B02006C08E514BF31EFD9 /* [CP] Embed Pods Frameworks */,
298589D956D542AE0EE16DAE /* [CP] Copy Pods Resources */,
);
buildRules = (
);
@ -270,9 +243,6 @@
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
packageReferences = (
5912938D2D95215F004A65BB /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */,
);
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
@ -297,37 +267,30 @@
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
0AD944122DD9DB5C00557D21 /* GoogleService-Info.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
0ADE29692D8C25660032BFCB /* silence.mp3 in Resources */,
0ADE296A2D8C25660032BFCB /* sample.mp3 in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
591293932D9524D1004A65BB /* GoogleService-Info.plist in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
1BD7F444CF127CDFCB7F9F47 /* [CP] Check Pods Manifest.lock */ = {
298589D956D542AE0EE16DAE /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
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";
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0;
};
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
@ -346,7 +309,7 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
4A5C19D622EF675DB4056F07 /* [CP] Embed Pods Frameworks */ = {
510B02006C08E514BF31EFD9 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
@ -363,7 +326,7 @@
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
8A6DB356BF41E96BFD7DF109 /* [CP] Check Pods Manifest.lock */ = {
90215E32C3610039A2575557 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
@ -400,21 +363,26 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
D49299AF1487DF3783287D24 /* [CP] Copy Pods Resources */ = {
D21548F12B1535261EB87672 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Copy Pods Resources";
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
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;
};
/* End PBXShellScriptBuildPhase section */
@ -432,14 +400,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
0ADE296D2D8C66D60032BFCB /* AzureTtsHelper.swift in Sources */,
0ADE296E2D8C66D60032BFCB /* AzureAsrHelper.swift in Sources */,
0A5EDBFC2D89C4FD00215E26 /* ClassicBluetoothHelper.swift in Sources */,
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
0A5EDC042D8B544100215E26 /* BluetoothMediaButtonHelper.swift in Sources */,
0A7A32E42D8DBA6D0047F746 /* VoiceInteractionService.swift in Sources */,
0A7A32E52D8DBA6D0047F746 /* VolcanoAIService.swift in Sources */,
0ADE29702D8C6C450032BFCB /* AudioSessionManager.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -498,6 +459,7 @@
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
@ -532,16 +494,15 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 23;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 42C82SZUVX;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.entertainment";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.0;
PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@ -552,64 +513,50 @@
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 930FE6D58BDB20688CE0A7CF /* Pods-RunnerTests.debug.xcconfig */;
baseConfigurationReference = C15B3030DF1AED99D3829135 /* Pods-RunnerTests.debug.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 42C82SZUVX;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound;
PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Debug;
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 02CF74744224D1C933681603 /* Pods-RunnerTests.release.xcconfig */;
baseConfigurationReference = 742A00629970110BA5008064 /* Pods-RunnerTests.release.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 42C82SZUVX;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound;
PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Release;
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9B84B894288BF76332D3F407 /* Pods-RunnerTests.profile.xcconfig */;
baseConfigurationReference = D1CF75E5BD49BB58812D5F36 /* Pods-RunnerTests.profile.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 42C82SZUVX;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound;
PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Profile;
@ -638,6 +585,7 @@
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
@ -695,6 +643,7 @@
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
@ -731,21 +680,17 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 23;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 42C82SZUVX;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.entertainment";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.0;
PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
@ -759,17 +704,15 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/RunnerRelease.entitlements;
CURRENT_PROJECT_VERSION = 23;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 42C82SZUVX;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.entertainment";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.0;
PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@ -812,17 +755,6 @@
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
5912938D2D95215F004A65BB /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/firebase/firebase-ios-sdk";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 11.10.0;
};
};
/* End XCRemoteSwiftPackageReference section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}

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

@ -1,123 +0,0 @@
{
"originHash" : "c63c63846d9c539229e96de38d6af51417e28c0ee9a0bc48bd0f0f19d923c329",
"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" : "firebase-ios-sdk",
"kind" : "remoteSourceControl",
"location" : "https://github.com/firebase/firebase-ios-sdk",
"state" : {
"revision" : "2e4a4daac9da51f79c131e76cb47e785dd7708c4",
"version" : "11.10.0"
}
},
{
"identity" : "googleappmeasurement",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/GoogleAppMeasurement.git",
"state" : {
"revision" : "defe1970a9099d47765592ebd7ac5f5e9fdb57c6",
"version" : "11.10.0"
}
},
{
"identity" : "googledatatransport",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/GoogleDataTransport.git",
"state" : {
"revision" : "617af071af9aa1d6a091d59a202910ac482128f9",
"version" : "10.1.0"
}
},
{
"identity" : "googleutilities",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/GoogleUtilities.git",
"state" : {
"revision" : "53156c7ec267db846e6b64c9f4c4e31ba4cf75eb",
"version" : "8.0.2"
}
},
{
"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" : "4d70340d55d7d07cc2fdf8e8125c4c126c1d5f35",
"version" : "4.4.0"
}
},
{
"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" : "swift-protobuf",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-protobuf.git",
"state" : {
"revision" : "d72aed98f8253ec1aa9ea1141e28150f408cf17f",
"version" : "1.29.0"
}
}
],
"version" : 3
}

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

@ -1,123 +0,0 @@
{
"originHash" : "c63c63846d9c539229e96de38d6af51417e28c0ee9a0bc48bd0f0f19d923c329",
"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" : "firebase-ios-sdk",
"kind" : "remoteSourceControl",
"location" : "https://github.com/firebase/firebase-ios-sdk",
"state" : {
"revision" : "2e4a4daac9da51f79c131e76cb47e785dd7708c4",
"version" : "11.10.0"
}
},
{
"identity" : "googleappmeasurement",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/GoogleAppMeasurement.git",
"state" : {
"revision" : "defe1970a9099d47765592ebd7ac5f5e9fdb57c6",
"version" : "11.10.0"
}
},
{
"identity" : "googledatatransport",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/GoogleDataTransport.git",
"state" : {
"revision" : "617af071af9aa1d6a091d59a202910ac482128f9",
"version" : "10.1.0"
}
},
{
"identity" : "googleutilities",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/GoogleUtilities.git",
"state" : {
"revision" : "53156c7ec267db846e6b64c9f4c4e31ba4cf75eb",
"version" : "8.0.2"
}
},
{
"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" : "4d70340d55d7d07cc2fdf8e8125c4c126c1d5f35",
"version" : "4.4.0"
}
},
{
"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" : "swift-protobuf",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-protobuf.git",
"state" : {
"revision" : "d72aed98f8253ec1aa9ea1141e28150f408cf17f",
"version" : "1.29.0"
}
}
],
"version" : 3
}

730
ios/Runner/AppDelegate.swift

@ -1,743 +1,13 @@
import Flutter
import UIKit
import AVFoundation
@main
@objc class AppDelegate: FlutterAppDelegate {
private var classicBluetoothHelper: ClassicBluetoothHelper?
private var mediaButtonHelper: Any?
// 添加Azure语音服务辅助类
private var azureAsrHelper: Any?
private var azureTtsHelper: Any?
// 添加火山AI服务
private var volcanoAIService: VolcanoAIService?
// 语音交互服务
private var voiceInteractionService: VoiceInteractionService?
private var audioSessionManager: AudioSessionManager?
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// 获取flutter控制器
guard let controller = window?.rootViewController as? FlutterViewController else {
NSLog("AppDelegate: Flutter控制器不可用")
return false
}
// 初始化音频会话管理器
audioSessionManager = AudioSessionManager.shared
// 统一注册所有方法通道
registerMethodChannel()
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
private func registerMethodChannel() {
let controller = window?.rootViewController as? FlutterViewController
guard let controller = controller else {
NSLog("AppDelegate: FlutterViewController 不可用")
return
}
// 注册经典蓝牙方法通道
registerClassicBluetoothChannels(controller: controller)
// 仅在iOS 13.0及以上版本注册媒体按钮通道和Azure语音服务通道
if #available(iOS 13.0, *) {
// 注册媒体按钮方法通道
registerMediaButtonChannels(controller: controller)
// 注册Azure ASR方法通道
registerAzureAsrChannels(controller: controller)
// 注册Azure TTS方法通道
registerAzureTtsChannels(controller: controller)
// 注册火山AI服务方法通道
registerVolcanoAIServiceChannels(controller: controller)
// 注册语音交互服务方法通道
registerVoiceInteractionChannels(controller: controller)
}
}
// MARK: - 经典蓝牙通道
private func registerClassicBluetoothChannels(controller: FlutterViewController) {
// 初始化蓝牙辅助类
if classicBluetoothHelper == nil {
classicBluetoothHelper = ClassicBluetoothHelper()
}
guard let classicBluetoothHelper = self.classicBluetoothHelper else {
NSLog("AppDelegate: 经典蓝牙辅助实例不可用")
return
}
// 注册经典蓝牙方法通道
let classicBluetoothChannel = FlutterMethodChannel(
name: "com.deep_voice.classic_bluetooth",
binaryMessenger: controller.binaryMessenger
)
classicBluetoothChannel.setMethodCallHandler { [weak self, weak classicBluetoothHelper] (call, result) in
guard let _ = self, let classicBluetoothHelper = classicBluetoothHelper else {
result(FlutterError(code: "UNAVAILABLE", message: "经典蓝牙辅助实例不可用", details: nil))
return
}
switch call.method {
case "isBluetoothEnabled":
result(classicBluetoothHelper.isBluetoothEnabled())
case "getConnectedHeadsetDevices":
// 获取已连接的耳机设备
classicBluetoothHelper.getConnectedHeadsetDevices { devices, error in
if let error = error {
result(FlutterError(code: "GET_DEVICES_FAILED", message: error, details: nil))
} else {
result(devices)
}
}
case "initialize":
classicBluetoothHelper.initialize()
result(true)
default:
result(FlutterMethodNotImplemented)
}
}
// 注册蓝牙流通道
let classicBluetoothStreamChannel = FlutterEventChannel(
name: "com.deep_voice.classic_bluetooth_events",
binaryMessenger: controller.binaryMessenger
)
// 创建蓝牙流处理器(作为局部变量)
let bluetoothStreamHandler = BluetoothStreamHandler(bluetoothHelper: classicBluetoothHelper)
classicBluetoothStreamChannel.setStreamHandler(bluetoothStreamHandler)
}
// MARK: - 消息通道设置
// MARK: - 蓝牙媒体按键通道
@available(iOS 13.0, *)
private func registerMediaButtonChannels(controller: FlutterViewController) {
// 初始化媒体按钮辅助类
if mediaButtonHelper == nil {
mediaButtonHelper = BluetoothMediaButtonHelper()
}
// guard let mediaButtonHelper = self.mediaButtonHelper as? BluetoothMediaButtonHelper else {
// NSLog("AppDelegate: 媒体按钮辅助实例不可用")
// return
// }
// mediaButtonHelper.startButtonListening()
// 注册媒体按钮方法通道
let mediaButtonChannel = FlutterMethodChannel(
name: "com.deep_voice.bluetooth_media_button",
binaryMessenger: controller.binaryMessenger
)
mediaButtonChannel.setMethodCallHandler { [weak self] (call, result) in
guard let _ = self, let mediaButtonHelper = self?.mediaButtonHelper as? BluetoothMediaButtonHelper else {
result(FlutterError(code: "UNAVAILABLE", message: "媒体按钮辅助实例不可用", details: nil))
return
}
switch call.method {
case "startButtonListening":
let success = mediaButtonHelper.startButtonListening()
result(success)
case "stopButtonListening":
let success = mediaButtonHelper.stopButtonListening()
result(success)
default:
result(FlutterMethodNotImplemented)
}
}
// 注册媒体按钮流通道
let mediaButtonStreamChannel = FlutterEventChannel(
name: "com.deep_voice.bluetooth_media_button_events",
binaryMessenger: controller.binaryMessenger
)
// 创建媒体按钮流处理器
let mediaButtonStreamHandler = MediaButtonStreamHandler(mediaButtonHelper: mediaButtonHelper as! BluetoothMediaButtonHelper)
mediaButtonStreamChannel.setStreamHandler(mediaButtonStreamHandler)
}
// 注册Azure ASR通道
@available(iOS 13.0, *)
private func registerAzureAsrChannels(controller: FlutterViewController) {
// 注册Azure ASR方法通道
let azureAsrChannel = FlutterMethodChannel(
name: "com.deep_voice.azure_asr",
binaryMessenger: controller.binaryMessenger
)
// 初始化Azure ASR辅助类
azureAsrHelper = AzureAsrHelper()
// 设置ASR方法处理器
azureAsrChannel.setMethodCallHandler { [weak self] (call, result) in
guard let self = self,
let asrHelper = self.azureAsrHelper as? AzureAsrHelper else {
result(FlutterError(code: "UNAVAILABLE", message: "Azure ASR辅助实例不可用", details: nil))
return
}
switch call.method {
case "initialize":
guard let args = call.arguments as? [String: Any],
let key = args["subscriptionKey"] as? String,
let region = args["region"] as? String else {
result(FlutterError(code: "INVALID_ARGS", message: "初始化参数无效", details: nil))
return
}
let languages = args["supportedLanguages"] as? [String]
let success = asrHelper.initialize(speechSubscriptionKey: key, serviceRegion: region, supportedLanguages: languages)
result(success)
case "recognizeOnce":
let success = asrHelper.recognizeOnce()
result(success)
case "startContinuousRecognition":
let success = asrHelper.startContinuousRecognition()
result(success)
case "stopContinuousRecognition":
let success = asrHelper.stopContinuousRecognition()
result(success)
case "isContinuousRecognitionActive":
let isActive = asrHelper.isContinuousRecognitionActive()
result(isActive)
case "dispose":
asrHelper.dispose()
result(true)
default:
result(FlutterMethodNotImplemented)
}
}
// 注册Azure ASR流通道
let azureAsrStreamChannel = FlutterEventChannel(
name: "com.deep_voice.azure_asr_events",
binaryMessenger: controller.binaryMessenger
)
// 创建ASR流处理器
let asrStreamHandler = AsrStreamHandler(azureAsrHelper: azureAsrHelper as! AzureAsrHelper)
azureAsrStreamChannel.setStreamHandler(asrStreamHandler)
}
// 注册Azure TTS通道
@available(iOS 13.0, *)
private func registerAzureTtsChannels(controller: FlutterViewController) {
// 注册Azure TTS方法通道
let azureTtsChannel = FlutterMethodChannel(
name: "com.deep_voice.azure_tts",
binaryMessenger: controller.binaryMessenger
)
// 初始化Azure TTS辅助类
azureTtsHelper = AzureTtsHelper()
// 设置TTS方法处理器
azureTtsChannel.setMethodCallHandler { [weak self] (call, result) in
guard let self = self,
let ttsHelper = self.azureTtsHelper as? AzureTtsHelper else {
result(FlutterError(code: "UNAVAILABLE", message: "Azure TTS辅助实例不可用", details: nil))
return
}
switch call.method {
case "initialize":
guard let args = call.arguments as? [String: Any],
let key = args["subscriptionKey"] as? String,
let region = args["region"] as? String else {
result(FlutterError(code: "INVALID_ARGS", message: "初始化参数无效", details: nil))
return
}
let language = args["language"] as? String ?? "zh-CN"
let success = ttsHelper.initialize(speechSubscriptionKey: key, serviceRegion: region, language: language)
result(success)
case "setVoice":
guard let args = call.arguments as? [String: Any],
let voiceName = args["voiceName"] as? String else {
result(FlutterError(code: "INVALID_ARGS", message: "语音名称参数无效", details: nil))
return
}
let success = ttsHelper.setVoice(voiceName: voiceName)
result(success)
case "setSpeechParams":
guard let args = call.arguments as? [String: Any] else {
result(FlutterError(code: "INVALID_ARGS", message: "语音参数无效", details: nil))
return
}
let rate = args["rate"] as? Int ?? 0
let pitch = args["pitch"] as? Int ?? 0
let volume = args["volume"] as? Int ?? 100
let success = ttsHelper.setSpeechParams(rate: rate, pitch: pitch, volume: volume)
result(success)
case "speakText":
guard let args = call.arguments as? [String: Any],
let text = args["text"] as? String else {
result(FlutterError(code: "INVALID_ARGS", message: "文本参数无效", details: nil))
return
}
let success = ttsHelper.speakText(text: text)
result(success)
case "stopSpeaking":
let success = ttsHelper.stopSpeaking()
result(success)
case "isSpeaking":
let isSpeaking = ttsHelper.isSpeaking()
result(isSpeaking)
case "dispose":
ttsHelper.dispose()
result(true)
default:
result(FlutterMethodNotImplemented)
}
}
// 注册Azure TTS流通道
let azureTtsStreamChannel = FlutterEventChannel(
name: "com.deep_voice.azure_tts_events",
binaryMessenger: controller.binaryMessenger
)
// 创建TTS流处理器
let ttsStreamHandler = TtsStreamHandler(azureTtsHelper: azureTtsHelper as! AzureTtsHelper)
azureTtsStreamChannel.setStreamHandler(ttsStreamHandler)
}
// 注册火山AI服务方法通道
@available(iOS 13.0, *)
private func registerVolcanoAIServiceChannels(controller: FlutterViewController) {
guard let volcanoAIService = volcanoAIService else {
return
}
// 注册火山AI方法通道
let volcanoAIChannel = FlutterMethodChannel(
name: "com.deep_voice.volcano_ai",
binaryMessenger: controller.binaryMessenger
)
volcanoAIChannel.setMethodCallHandler { [weak self, weak volcanoAIService] (call, result) in
guard let _ = self, let volcanoAIService = volcanoAIService else {
result(FlutterError(code: "UNAVAILABLE", message: "火山AI服务实例不可用", details: nil))
return
}
switch call.method {
case "initialize":
let apiKey = call.arguments as? String ?? ""
let success = volcanoAIService.initialize(apiKey: apiKey)
result(success)
case "sendMessage":
guard let arguments = call.arguments as? [String: Any],
let message = arguments["message"] as? String,
let systemPrompt = arguments["systemPrompt"] as? String else {
result(FlutterError(code: "INVALID_ARGS", message: "参数无效", details: nil))
return
}
if message.isEmpty {
result(FlutterError(code: "EMPTY_MESSAGE", message: "消息不能为空", details: nil))
return
}
// 异步发送消息
Task {
do {
// 创建用户消息
let userMessage = volcanoAIService.createUserMessage(content: message)
// 发送消息并等待响应
let response = try await volcanoAIService.sendMessage(
messages: [userMessage],
systemPrompt: systemPrompt
)
// 返回响应
DispatchQueue.main.async {
result(response)
}
} catch {
// 处理错误
DispatchQueue.main.async {
result(FlutterError(
code: "AI_ERROR",
message: "AI请求失败: \(error.localizedDescription)",
details: nil
))
}
}
}
default:
result(FlutterMethodNotImplemented)
}
}
// 注册火山AI流通道
let volcanoAIStreamChannel = FlutterEventChannel(
name: "com.deep_voice.volcano_ai_stream",
binaryMessenger: controller.binaryMessenger
)
// 创建火山AI流处理器(作为局部变量)
let volcanoAIStreamHandler = VolcanoAIStreamHandler(volcanoAIService: volcanoAIService)
volcanoAIStreamChannel.setStreamHandler(volcanoAIStreamHandler)
}
// 注册语音交互服务方法通道
@available(iOS 13.0, *)
private func registerVoiceInteractionChannels(controller: FlutterViewController) {
// 初始化语音交互服务
voiceInteractionService = VoiceInteractionService.shared
// 注册语音交互服务方法通道
let voiceInteractionChannel = FlutterMethodChannel(
name: "com.deep_voice.voice_interaction",
binaryMessenger: controller.binaryMessenger
)
voiceInteractionChannel.setMethodCallHandler { [weak self, weak voiceInteractionService] (call, result) in
guard let _ = self, let voiceInteractionService = voiceInteractionService else {
result(FlutterError(code: "UNAVAILABLE", message: "语音交互服务实例不可用", details: nil))
return
}
switch call.method {
case "startService":
guard let args = call.arguments as? [String: Any],
let azureKey = args["azure_speech_key"] as? String,
let azureRegion = args["azure_speech_region"] as? String,
let volcanoKey = args["volcano_ai_api_key"] as? String else {
result(FlutterError(code: "INVALID_ARGS", message: "启动服务参数无效", details: nil))
return
}
let success = voiceInteractionService.start(azureKey: azureKey, azureRegion: azureRegion, volcanoKey: volcanoKey)
result(success)
case "stopService":
voiceInteractionService.stop()
result(true)
case "isServiceRunning":
let isRunning = voiceInteractionService.isServiceRunning()
result(isRunning)
case "pauseVoiceInteraction":
voiceInteractionService.pauseVoiceInteraction()
result(true)
default:
result(FlutterMethodNotImplemented)
}
}
// 注册语音交互流通道
let voiceInteractionStreamChannel = FlutterEventChannel(
name: "com.deep_voice.voice_interaction_events",
binaryMessenger: controller.binaryMessenger
)
// 创建语音交互流处理器(作为局部变量)
let voiceInteractionStreamHandler = VoiceInteractionStreamHandler(voiceInteractionService: voiceInteractionService!)
voiceInteractionStreamChannel.setStreamHandler(voiceInteractionStreamHandler)
}
}
// 蓝牙事件流处理器
class BluetoothStreamHandler: NSObject, FlutterStreamHandler, BluetoothEventHandler {
private let bluetoothHelper: ClassicBluetoothHelper
private var eventSink: FlutterEventSink?
init(bluetoothHelper: ClassicBluetoothHelper) {
self.bluetoothHelper = bluetoothHelper
super.init()
// 设置事件处理器
bluetoothHelper.setEventHandler(self)
}
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
// NSLog("BluetoothStreamHandler: 开始监听蓝牙事件")
self.eventSink = events
return nil
}
func onCancel(withArguments arguments: Any?) -> FlutterError? {
// NSLog("BluetoothStreamHandler: 取消监听蓝牙事件")
eventSink = nil
return nil
}
// 实现BluetoothEventHandler协议
func sendEvent(_ event: [String: Any]) {
NSLog("BluetoothStreamHandler: 接收到蓝牙事件: \(event)")
DispatchQueue.main.async { [weak self] in
self?.eventSink?(event)
}
}
}
// 媒体按钮事件流处理器
@available(iOS 13.0, *)
class MediaButtonStreamHandler: NSObject, FlutterStreamHandler, MediaButtonEventHandler {
private let mediaButtonHelper: BluetoothMediaButtonHelper
private var eventSink: FlutterEventSink?
init(mediaButtonHelper: BluetoothMediaButtonHelper) {
self.mediaButtonHelper = mediaButtonHelper
super.init()
// 设置事件处理器
mediaButtonHelper.setEventHandler(self)
}
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
NSLog("MediaButtonStreamHandler: 开始监听媒体按钮事件")
self.eventSink = events
return nil
}
func onCancel(withArguments arguments: Any?) -> FlutterError? {
NSLog("MediaButtonStreamHandler: 取消监听媒体按钮事件")
eventSink = nil
return nil
}
// 实现MediaButtonEventHandler协议
func sendEvent(_ event: [String: Any]) {
NSLog("MediaButtonStreamHandler: 收到按钮事件 \(event)")
DispatchQueue.main.async { [weak self] in
self?.eventSink?(event)
}
}
}
// ASR事件处理器协议
@available(iOS 13.0, *)
protocol AsrEventHandler: AnyObject {
func sendEvent(_ event: [String: Any])
}
// ASR事件流处理器
@available(iOS 13.0, *)
class AsrStreamHandler: NSObject, FlutterStreamHandler, AsrEventHandler {
private let azureAsrHelper: AzureAsrHelper
private var eventSink: FlutterEventSink?
init(azureAsrHelper: AzureAsrHelper) {
self.azureAsrHelper = azureAsrHelper
super.init()
// 设置事件处理器
azureAsrHelper.setEventHandler { [weak self] event in
self?.sendEvent(event)
}
}
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
NSLog("AsrStreamHandler: 开始监听ASR事件")
self.eventSink = events
return nil
}
func onCancel(withArguments arguments: Any?) -> FlutterError? {
NSLog("AsrStreamHandler: 取消监听ASR事件")
eventSink = nil
return nil
}
// 实现AsrEventHandler协议
func sendEvent(_ event: [String: Any]) {
// NSLog("AsrStreamHandler: 收到事件 \(event)")
DispatchQueue.main.async { [weak self] in
self?.eventSink?(event)
}
}
}
// TTS事件处理器协议
@available(iOS 13.0, *)
protocol TtsEventHandler: AnyObject {
func sendEvent(_ event: [String: Any])
}
// TTS事件流处理器
@available(iOS 13.0, *)
class TtsStreamHandler: NSObject, FlutterStreamHandler, TtsEventHandler {
private let azureTtsHelper: AzureTtsHelper
private var eventSink: FlutterEventSink?
init(azureTtsHelper: AzureTtsHelper) {
self.azureTtsHelper = azureTtsHelper
super.init()
// 设置事件处理器
azureTtsHelper.setEventHandler { [weak self] event in
self?.sendEvent(event)
}
}
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
NSLog("TtsStreamHandler: 开始监听TTS事件")
self.eventSink = events
return nil
}
func onCancel(withArguments arguments: Any?) -> FlutterError? {
NSLog("TtsStreamHandler: 取消监听TTS事件")
eventSink = nil
return nil
}
// 实现TtsEventHandler协议
func sendEvent(_ event: [String: Any]) {
NSLog("TtsStreamHandler: 收到事件 \(event)")
DispatchQueue.main.async { [weak self] in
self?.eventSink?(event)
}
}
}
// 火山AI事件流处理器
@available(iOS 13.0, *)
class VolcanoAIStreamHandler: NSObject, FlutterStreamHandler {
private let volcanoAIService: VolcanoAIService
private var eventSink: FlutterEventSink?
init(volcanoAIService: VolcanoAIService) {
self.volcanoAIService = volcanoAIService
super.init()
}
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
self.eventSink = events
return nil
}
func onCancel(withArguments arguments: Any?) -> FlutterError? {
eventSink = nil
return nil
}
// 处理来自Flutter的流式请求
func handleStreamRequest(messages: [[String: Any]], systemPrompt: String) {
volcanoAIService.sendMessageStream(messages: messages, systemPrompt: systemPrompt, streamCallback: VolcanoAIService.StreamCallback(
onToken: { [weak self] token in
guard let self = self, let eventSink = self.eventSink else { return }
let event: [String: Any] = [
"type": "token",
"token": token
]
DispatchQueue.main.async {
eventSink(event)
}
},
onComplete: { [weak self] in
guard let self = self, let eventSink = self.eventSink else { return }
let event: [String: Any] = [
"type": "complete"
]
DispatchQueue.main.async {
eventSink(event)
}
},
onError: { [weak self] error in
guard let self = self, let eventSink = self.eventSink else { return }
let event: [String: Any] = [
"type": "error",
"error": error.localizedDescription
]
DispatchQueue.main.async {
eventSink(event)
}
}
))
}
}
// 语音交互事件处理器协议
@available(iOS 13.0, *)
protocol VoiceInteractionEventHandler: AnyObject {
func sendEvent(_ event: [String: Any])
}
// 语音交互流处理器
@available(iOS 13.0, *)
class VoiceInteractionStreamHandler: NSObject, FlutterStreamHandler, VoiceInteractionEventHandler {
private let voiceInteractionService: VoiceInteractionService
private var eventSink: FlutterEventSink?
init(voiceInteractionService: VoiceInteractionService) {
self.voiceInteractionService = voiceInteractionService
super.init()
// 设置事件处理器
voiceInteractionService.setEventHandler(self)
}
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
NSLog("VoiceInteractionStreamHandler: 开始监听语音交互事件")
self.eventSink = events
return nil
}
func onCancel(withArguments arguments: Any?) -> FlutterError? {
NSLog("VoiceInteractionStreamHandler: 取消监听语音交互事件")
eventSink = nil
return nil
}
// 实现VoiceInteractionEventHandler协议
func sendEvent(_ event: [String: Any]) {
NSLog("VoiceInteractionStreamHandler: 收到事件 \(event)")
DispatchQueue.main.async { [weak self] in
self?.eventSink?(event)
}
}
}

589
ios/Runner/AudioSessionManager.swift

@ -1,589 +0,0 @@
import Foundation
import AVFoundation
import UIKit
/// 统一音频会话管理类
/// 用于统一管理所有组件的音频会话配置
@objc class AudioSessionManager: NSObject {
// MARK: - 单例实现
@objc static let shared = AudioSessionManager()
// 音频会话
private let audioSession = AVAudioSession.sharedInstance()
// 音频会话状态
private var isConfigured = false
private var isActive = false
// 当前配置类型
private var currentCategory: AVAudioSession.Category?
private var currentMode: AVAudioSession.Mode?
private var currentOptions: AVAudioSession.CategoryOptions?
// 中断监听器
private var interruptionObserver: NSObjectProtocol?
// 路由变化监听器
private var routeChangeObserver: NSObjectProtocol?
// 私有初始化方法,确保只能通过shared访问
private override init() {
super.init()
NSLog("[AudioSessionManager] 音频会话管理器已初始化")
setupNotifications()
}
deinit {
removeNotifications()
}
// MARK: - 通知设置
private func setupNotifications() {
// 监听中断事件
interruptionObserver = NotificationCenter.default.addObserver(
forName: AVAudioSession.interruptionNotification,
object: nil,
queue: .main) { [weak self] notification in
self?.handleInterruption(notification)
}
// 监听路由变化事件
routeChangeObserver = NotificationCenter.default.addObserver(
forName: AVAudioSession.routeChangeNotification,
object: nil,
queue: .main) { [weak self] notification in
self?.handleRouteChange(notification)
}
// 监听应用进入后台/前台
NotificationCenter.default.addObserver(
self,
selector: #selector(handleAppDidEnterBackground),
name: UIApplication.didEnterBackgroundNotification,
object: nil)
NotificationCenter.default.addObserver(
self,
selector: #selector(handleAppWillEnterForeground),
name: UIApplication.willEnterForegroundNotification,
object: nil)
}
private func removeNotifications() {
if let interruptionObserver = interruptionObserver {
NotificationCenter.default.removeObserver(interruptionObserver)
}
if let routeChangeObserver = routeChangeObserver {
NotificationCenter.default.removeObserver(routeChangeObserver)
}
NotificationCenter.default.removeObserver(self)
}
// MARK: - 通知处理
private func handleInterruption(_ 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:
// 中断开始
NSLog("[AudioSessionManager] 音频会话被中断")
isActive = false
case .ended:
// 中断结束,尝试恢复
if let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt {
let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
if options.contains(.shouldResume) {
NSLog("[AudioSessionManager] 尝试恢复被中断的音频会话")
try? activateSession()
}
}
@unknown default:
break
}
}
private func handleRouteChange(_ 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:
// 新设备连接
let currentRoute = audioSession.currentRoute
for output in currentRoute.outputs where output.portType == .bluetoothA2DP || output.portType == .bluetoothHFP {
NSLog("[AudioSessionManager] 检测到蓝牙音频设备连接: \(output.portName)")
// 蓝牙设备连接,可能需要更新配置
if isActive && currentOptions?.contains(.allowBluetooth) == true {
try? forceReconfigure()
}
break
}
case .oldDeviceUnavailable:
// 设备断开连接
if let previousRoute = userInfo[AVAudioSessionRouteChangePreviousRouteKey] as? AVAudioSessionRouteDescription {
for output in previousRoute.outputs where output.portType == .bluetoothA2DP || output.portType == .bluetoothHFP {
NSLog("[AudioSessionManager] 蓝牙音频设备断开连接: \(output.portName)")
// 蓝牙设备断开,重新配置到内置扬声器
if isActive {
try? forceReconfigure()
}
break
}
}
default:
break
}
}
@objc private func handleAppDidEnterBackground() {
NSLog("[AudioSessionManager] 应用进入后台")
// 应用进入后台时可能需要调整音频会话
}
@objc private func handleAppWillEnterForeground() {
NSLog("[AudioSessionManager] 应用即将进入前台")
// 应用返回前台,检查并恢复音频会话
if isConfigured && !isActive {
try? activateSession()
}
}
// MARK: - 音频会话配置
/// 统一配置方法 - 所有音频配置都通过此方法进行
@objc func configureSession(category: AVAudioSession.Category,
mode: AVAudioSession.Mode,
options: AVAudioSession.CategoryOptions,
force: Bool = false) throws {
NSLog("[AudioSessionManager] 配置音频会话: 类别=\(category), 模式=\(mode), 选项=\(options), 强制=\(force)")
// 如果已配置且不强制重新配置,则直接返回
if isConfigured && !force &&
currentCategory == category &&
currentMode == mode &&
currentOptions == options {
NSLog("[AudioSessionManager] 音频会话已配置,跳过配置")
return
}
do {
// NSLog("[AudioSessionManager] 配置音频会话: 类别=\(category), 模式=\(mode)")
// 暂时停用当前会话(如果正在活动)
if isActive {
try audioSession.setActive(false, options: .notifyOthersOnDeactivation)
}
// 设置新的分类、模式和选项
try audioSession.setCategory(category, mode: mode, options: options)
// 设置采样率和缓冲区大小(对于语音识别优化)
// if mode == .spokenAudio {
// try audioSession.setPreferredSampleRate(16000)
// try audioSession.setPreferredIOBufferDuration(0.01) // 10ms
// }
// 重新激活会话
try audioSession.setActive(true, options: .notifyOthersOnDeactivation)
isActive = true
// 切换到扬声器
// 记录当前配置
currentCategory = category
currentMode = mode
currentOptions = options
isConfigured = true
// 检查并设置首选的输入设备(如果需要)
if category == .playAndRecord || category == .record {
try configurePreferredInput()
}
return
} catch {
NSLog("[AudioSessionManager] 配置音频会话失败: \(error.localizedDescription)")
throw error
}
}
/// 配置首选输入设备
private func configurePreferredInput() throws {
// 获取可用输入设备
guard let availableInputs = audioSession.availableInputs else {
NSLog("[AudioSessionManager] 无法获取可用输入设备")
return
}
// 首选顺序:有线耳机 > 蓝牙耳机 > 内置麦克风
var preferredInput: AVAudioSessionPortDescription? = nil
// 先尝试找到有线耳机
if let wiredHeadset = availableInputs.first(where: { $0.portType == .headsetMic }) {
preferredInput = wiredHeadset
NSLog("[AudioSessionManager] 设置首选输入设备: 有线耳机")
}
// 如果没有有线耳机,尝试使用蓝牙
else if let bluetooth = availableInputs.first(where: { $0.portType == .bluetoothHFP }) {
preferredInput = bluetooth
NSLog("[AudioSessionManager] 设置首选输入设备: 蓝牙耳机")
}
// 如果没有外部设备,使用内置麦克风
else if let builtIn = availableInputs.first(where: { $0.portType == .builtInMic }) {
preferredInput = builtIn
NSLog("[AudioSessionManager] 设置首选输入设备: 内置麦克风")
}
// 设置首选输入设备
if let input = preferredInput {
try audioSession.setPreferredInput(input)
}
}
/// 激活音频会话(如果已配置)
@objc func activateSession() throws {
if !isConfigured {
NSLog("[AudioSessionManager] 警告: 尝试激活未配置的音频会话,应用默认配置")
// 未配置,应用默认配置
// try configureSession(
// category: .playAndRecord,
// mode: .spokenAudio,
// options: [.allowBluetooth, .allowBluetoothA2DP, .duckOthers, .defaultToSpeaker],
// force: true
// )
try configureSession(
category: .playAndRecord, // 允许同时录音和播放
mode: .voiceChat, // 使用voiceChat模式获得最佳回音消除效果
options: [
.allowBluetooth, // 允许蓝牙设备
.defaultToSpeaker, // 默认使用扬声器
.mixWithOthers // 允许与其他应用混音
],
force: false // 是否强制重新配置
)
} else if !isActive {
NSLog("[AudioSessionManager] 激活已配置的音频会话")
try audioSession.setActive(true, options: .notifyOthersOnDeactivation)
isActive = true
}
}
/// 停用音频会话(保留配置)
@objc func deactivateSession() throws {
if isActive {
NSLog("[AudioSessionManager] 停用音频会话")
try audioSession.setActive(false, options: .notifyOthersOnDeactivation)
isActive = false
}
}
/// 重新配置当前会话
@objc func forceReconfigure() throws {
NSLog("[AudioSessionManager] 强制重新配置音频会话")
// try configureSession(
// category: currentCategory ?? .playAndRecord,
// mode: currentMode ?? .default,
// options: currentOptions ?? [.allowBluetooth, .allowBluetoothA2DP, .defaultToSpeaker],
// force: true
// )
try configureSession(
category: .playAndRecord, // 允许同时录音和播放
mode: .voiceChat, // 使用voiceChat模式获得最佳回音消除效果
options: [
.allowBluetooth, // 允许蓝牙设备
.defaultToSpeaker, // 默认使用扬声器
.mixWithOthers // 允许与其他应用混音
],
force: false // 是否强制重新配置
)
try audioSession.overrideOutputAudioPort(.speaker)
}
// MARK: - 状态检查
/// 检查麦克风权限
@objc func checkMicrophonePermission(completion: @escaping (Bool) -> Void) {
switch AVAudioSession.sharedInstance().recordPermission {
case .granted:
completion(true)
case .denied:
NSLog("[AudioSessionManager] 麦克风权限被拒绝")
completion(false)
case .undetermined:
NSLog("[AudioSessionManager] 请求麦克风权限")
AVAudioSession.sharedInstance().requestRecordPermission { granted in
DispatchQueue.main.async {
completion(granted)
}
}
@unknown default:
completion(false)
}
}
/// 获取当前音频会话的配置信息
@objc func getCurrentConfiguration() -> [String: Any] {
return [
"isConfigured": isConfigured,
"isActive": isActive,
"category": String(describing: currentCategory),
"mode": String(describing: currentMode),
"bluetoothEnabled": currentOptions?.contains(.allowBluetooth) == true,
"currentInputs": audioSession.currentRoute.inputs.map { [$0.portType.rawValue: $0.portName] },
"currentOutputs": audioSession.currentRoute.outputs.map { [$0.portType.rawValue: $0.portName] }
]
}
/// 检查当前是否有蓝牙耳机连接
@objc func hasBluetoothHeadsetConnected() -> Bool {
let outputs = audioSession.currentRoute.outputs
return outputs.contains { $0.portType == .bluetoothA2DP || $0.portType == .bluetoothHFP || $0.portType == .bluetoothLE }
}
/// 检查蓝牙是否启用
@objc func isBluetoothEnabled() -> Bool {
// 在iOS中无法直接检查蓝牙是否启用,我们可以通过检查音频会话配置来推断
// 检查音频会话是否配置了允许蓝牙
if let options = currentOptions {
return options.contains(.allowBluetooth) || options.contains(.allowBluetoothA2DP)
}
// 如果尚未配置音频会话,尝试检查当前路由中是否有蓝牙设备
let outputs = audioSession.currentRoute.outputs
let hasBluetooth = outputs.contains {
$0.portType == .bluetoothA2DP || $0.portType == .bluetoothHFP || $0.portType == .bluetoothLE
}
return hasBluetooth
}
/// 获取当前音频路由
@objc func getCurrentAudioRoute() -> String {
let outputs = audioSession.currentRoute.outputs
if let output = outputs.first {
return "\(output.portType.rawValue) (\(output.portName))"
}
return "unknown"
}
/// 获取当前输入设备
@objc func getCurrentInputDevice() -> String {
let inputs = audioSession.currentRoute.inputs
if let input = inputs.first {
return "\(input.portType.rawValue) (\(input.portName))"
}
return "unknown"
}
// MARK: - 路由管理和监听
/// 获取当前路由信息
@objc func getCurrentRoute() -> AVAudioSessionRouteDescription {
return audioSession.currentRoute
}
/// 添加路由变更监听器
/// - Parameters:
/// - observer: 监听对象
/// - selector: 监听方法选择器
@objc func addRouteChangeListener(_ observer: Any, selector: Selector) {
NotificationCenter.default.addObserver(
observer,
selector: selector,
name: AVAudioSession.routeChangeNotification,
object: nil
)
}
/// 移除路由变更监听器
/// - Parameter observer: 要移除的监听对象
@objc func removeRouteChangeListener(_ observer: Any) {
NotificationCenter.default.removeObserver(
observer,
name: AVAudioSession.routeChangeNotification,
object: nil
)
}
/// 获取设备类型描述
private func getDeviceType(portType: AVAudioSession.Port) -> String {
switch portType {
case .bluetoothA2DP, .bluetoothHFP, .bluetoothLE:
return "bluetooth"
case .headphones, .headsetMic:
return "wired"
case .airPlay:
return "airplay"
default:
return "unknown"
}
}
// /// 为Azure语音识别专门配置的音频会话
// /// 此函数优化Azure语音识别的音频处理配置,提供更强的回音消除能力
// /// - Parameters:
// /// - force: 是否强制重新配置
// /// - configureAdditionalSettings: 配置完成后进行额外的音频设置
// /// - Returns: 配置是否成功
// @objc func configureForAzureSpeechRecognition(force: Bool = false, configureAdditionalSettings: Bool = true) -> Bool {
// NSLog("[AudioSessionManager] 配置Azure ASR语音识别的音频会话")
// do {
// // 配置音频会话以优化语音识别
// try configureSession(
// category: .playAndRecord, // 允许同时录音和播放
// mode: .voiceChat, // 使用voiceChat模式获得最佳回音消除效果
// options: [
// .allowBluetooth, // 允许蓝牙设备
// .defaultToSpeaker, // 默认使用扬声器
// .mixWithOthers // 允许与其他应用混音
// ],
// force: force // 是否强制重新配置
// )
// // 额外的优化配置
// if configureAdditionalSettings {
// // 获取当前是否使用耳机
// let currentRoute = audioSession.currentRoute
// let hasHeadphones = currentRoute.outputs.contains {
// $0.portType == .headphones || $0.portType == .bluetoothA2DP || $0.portType == .bluetoothHFP
// }
// // 设置采样率为16kHz(Azure Speech API推荐)
// try audioSession.setPreferredSampleRate(16000.0)
// // 设置较小的缓冲区大小以减少延迟
// try audioSession.setPreferredIOBufferDuration(0.01)
// // 根据是否有耳机连接调整输入增益
// if !hasHeadphones {
// // 无耳机时降低输入增益以减少回音
// try audioSession.setInputGain(0.8)
// NSLog("[AudioSessionManager] 启用扬声器回音消除优化")
// } else {
// // 使用耳机时可以使用较高增益
// try audioSession.setInputGain(1.0)
// NSLog("[AudioSessionManager] 检测到耳机连接,应用耳机模式")
// }
// }
// NSLog("[AudioSessionManager] 已成功配置Azure语音识别的音频会话")
// return true
// } catch {
// NSLog("[AudioSessionManager] 配置Azure语音识别的音频会话失败: \(error.localizedDescription)")
// return false
// }
// }
@objc func configureForVoiceInteraction(force: Bool = false, configureAdditionalSettings: Bool = true) -> Bool {
NSLog("[AudioSessionManager] 配置Azure语音识别的音频会话")
do {
// 配置音频会话以优化语音识别
try configureSession(
category: .playAndRecord, // 允许同时录音和播放
mode: .voiceChat, // 使用voiceChat模式获得最佳回音消除效果
options: [
.allowBluetooth, // 允许蓝牙设备
.defaultToSpeaker, // 默认使用扬声器
.mixWithOthers // 允许与其他应用混音
],
force: force // 是否强制重新配置
)
try audioSession.overrideOutputAudioPort(.speaker)
// 额外的优化配置
if configureAdditionalSettings {
// 获取当前是否使用耳机
let currentRoute = audioSession.currentRoute
let hasHeadphones = currentRoute.outputs.contains {
$0.portType == .headphones || $0.portType == .bluetoothA2DP || $0.portType == .bluetoothHFP
}
// 设置采样率为16kHz(Azure Speech API推荐)
try audioSession.setPreferredSampleRate(16000.0)
// 设置较小的缓冲区大小以减少延迟
try audioSession.setPreferredIOBufferDuration(0.01)
// 根据是否有耳机连接调整输入增益
if !hasHeadphones {
// 无耳机时降低输入增益以减少回音
try audioSession.setInputGain(0.8)
NSLog("[AudioSessionManager] 启用扬声器回音消除优化")
} else {
// 使用耳机时可以使用较高增益
try audioSession.setInputGain(1.0)
NSLog("[AudioSessionManager] 检测到耳机连接,应用耳机模式")
}
}
NSLog("[AudioSessionManager] 已成功配置Azure语音识别的音频会话")
return true
} catch {
NSLog("[AudioSessionManager] 配置Azure语音识别的音频会话失败: \(error.localizedDescription)")
return false
}
}
/// 配置用于语音交互(同时支持ASR和TTS)的音频会话
/// 此函数综合优化语音识别和语音合成,适用于需要双向交互的场景
/// - Returns: 配置是否成功
// @objc func configureForVoiceInteraction(force: Bool = false) -> Bool {
// do {
// // 配置音频会话以支持语音交互
// // 使用playAndRecord类别允许同时录音和播放
// // 使用spokenAudio模式优化语音交互
// try configureSession(
// category: .playAndRecord,
// mode: .voiceChat,
// options: [
// // .allowBluetoothA2DP, // 允许通过蓝牙A2DP连接进行高质量音频输出
// .allowBluetooth, // 允许通过蓝牙SCO连接进行输入和输出
// .defaultToSpeaker, // 默认使用扬声器输出
// .mixWithOthers
// ],
// force: force // 强制重新配置,确保设置生效
// )
// // 设置首选输入设备
// try configurePreferredInput()
// NSLog("[AudioSessionManager] 已配置语音交互(ASR+TTS)的音频会话")
// return true
// } catch {
// NSLog("[AudioSessionManager] 配置语音交互(ASR+TTS)的音频会话失败: \(error.localizedDescription)")
// return false
// }
// }
}

813
ios/Runner/AzureAsrHelper.swift

@ -1,813 +0,0 @@
import Foundation
import MicrosoftCognitiveServicesSpeech
import AVFoundation
import AudioToolbox
/// Azure ASR工具类,负责实现语音识别服务接口
@available(iOS 13.0, *)
class AzureAsrHelper: NSObject {
// MARK: - 属性
/// 是否使用自定义音频处理器 - 设置为false时将使用系统默认麦克风输入
private let useCustomAudioProcessor = true
/// 事件处理回调
private var eventHandler: (([String: Any]) -> Void)?
/// 语音配置信息
private var speechSubscriptionKey: String = ""
private var serviceRegion: String = ""
/// 语音识别相关
private var speechConfig: SPXSpeechConfiguration?
private var recognizer: SPXSpeechRecognizer?
private var audioConfig: SPXAudioConfiguration?
/// 添加自定义音频处理相关
private var pushStream: SPXPushAudioInputStream?
private var audioProcessor: CustomAudioProcessor?
private var isProcessingAudio = false
private var audioProcessingTimer: Timer?
/// 状态标志
private var isInitialized = false
private var _isContinuousRecognitionActive = false
/// 当前语言和支持的语言
private var currentLanguage = "zh-CN"
private var supportedLanguages: [String] = ["zh-CN", "en-US"]
private var isAutoDetectLanguage = false
/// 音频会话管理器
private let audioSessionManager = AudioSessionManager.shared
// MARK: - 初始化
override init() {
super.init()
}
deinit {
dispose()
}
// MARK: - ASR Service 接口实现
/// 初始化语音识别服务
/// - Parameters:
/// - speechSubscriptionKey: Azure 语音服务订阅密钥
/// - serviceRegion: Azure 服务区域 (如 eastasia)
/// - supportedLanguages: 支持的语言代码数组 (可选)
/// - Returns: 初始化是否成功
func initialize(speechSubscriptionKey: String, serviceRegion: String, supportedLanguages: [String]? = nil) -> Bool {
print("[AzureAsrHelper] 初始化 Azure 语音服务")
// 检查配置是否为空
if speechSubscriptionKey.isEmpty || serviceRegion.isEmpty {
print("[AzureAsrHelper] 错误: Azure 配置信息不完整")
eventHandler?(["type": "error", "message": "Azure 配置信息不完整"])
return false
}
// 检查语言参数
if let languages = supportedLanguages, languages.isEmpty {
print("[AzureAsrHelper] 警告: 传入的支持语言列表为空,将使用默认语言")
}
// 释放之前的资源
dispose()
// 记录配置信息
self.speechSubscriptionKey = speechSubscriptionKey
self.serviceRegion = serviceRegion
// 设置语言
if let languages = supportedLanguages, !languages.isEmpty {
self.supportedLanguages = languages
}
// 根据支持的语言数量决定是否启用自动语言检测
isAutoDetectLanguage = self.supportedLanguages.count >= 2
// 如果只有一种语言,设置为当前语言
if !isAutoDetectLanguage && !self.supportedLanguages.isEmpty {
currentLanguage = self.supportedLanguages[0]
}
// 创建识别器和设置回调
if !createRecognizerAndSetupCallbacks() {
return false
}
print("[AzureAsrHelper] Azure 语音服务初始化成功")
isInitialized = true
return true
}
/// 创建识别器并设置回调
private func createRecognizerAndSetupCallbacks() -> Bool {
// 释放之前的 recognizer
recognizer = nil
audioConfig = nil
do {
// 创建语音配置
speechConfig = try SPXSpeechConfiguration(subscription: speechSubscriptionKey, region: serviceRegion)
// 设置音频输入参数
try setupAudioSession()
// 根据设置决定是否使用自定义音频处理器
if useCustomAudioProcessor {
// 创建自定义音频流和处理器,替代默认的麦克风输入
pushStream = try SPXPushAudioInputStream()
audioConfig = try SPXAudioConfiguration(streamInput: pushStream!)
// 初始化自定义音频处理器
audioProcessor = CustomAudioProcessor()
} else {
// 使用默认麦克风输入
audioConfig = try SPXAudioConfiguration()
}
// 设置语言配置
if isAutoDetectLanguage {
// 设置自动语言检测
speechConfig?.setPropertyTo("Continuous", by: SPXPropertyId.speechServiceConnectionLanguageIdMode)
// 创建自动语言检测配置
let autoDetectSourceLanguageConfig = try SPXAutoDetectSourceLanguageConfiguration(supportedLanguages)
// 创建识别器
recognizer = try SPXSpeechRecognizer(
speechConfiguration: speechConfig!,
autoDetectSourceLanguageConfiguration: autoDetectSourceLanguageConfig,
audioConfiguration: audioConfig!
)
} else {
// 设置指定的识别语言
speechConfig?.speechRecognitionLanguage = currentLanguage
// 创建识别器
recognizer = try SPXSpeechRecognizer(speechConfiguration: speechConfig!, audioConfiguration: audioConfig!)
}
// 设置所有回调
setupAllCallbacks()
return true
} catch {
print("[AzureAsrHelper] 错误: 创建识别器失败: \(error.localizedDescription)")
eventHandler?(["type": "error", "message": "创建识别器失败: \(error.localizedDescription)"])
return false
}
}
/// 设置音频会话
private func setupAudioSession() throws {
print("[AzureAsrHelper] 开始配置音频会话...")
// 使用AudioSessionManager配置音频会话,使用专门为Azure ASR优化的配置
let success = audioSessionManager.configureForVoiceInteraction()
if !success {
print("[AzureAsrHelper] 警告: 通过AudioSessionManager配置音频会话失败")
}
}
/// 设置所有回调
private func setupAllCallbacks() {
guard let recognizer = recognizer else { return }
// 最终识别结果
recognizer.addRecognizedEventHandler { [weak self] _, event in
guard let self = self else { return }
if event.result.reason == SPXResultReason.recognizedSpeech {
let detectedLanguage = self.getDetectedLanguage(from: event.result)
print("[AzureAsrHelper] 识别结果: \(event.result.text ?? ""), 语言: \(detectedLanguage)")
self.eventHandler?(["type": "result",
"text": event.result.text ?? "",
"detectedLanguage": detectedLanguage
])
}
}
// 识别中事件
recognizer.addRecognizingEventHandler { [weak self] _, event in
guard let self = self else { return }
if event.result.reason == SPXResultReason.recognizingSpeech {
let detectedLanguage = self.getDetectedLanguage(from: event.result)
// print("[AzureAsrHelper] 识别中: \(event.result.text ?? ""), 语言: \(detectedLanguage)")
self.eventHandler?(["type": "recognizing",
"text": event.result.text ?? "",
"detectedLanguage": detectedLanguage
])
}
}
// 会话事件
recognizer.addSessionStartedEventHandler { [weak self] _, _ in
guard let self = self else { return }
print("[AzureAsrHelper] 识别会话已开始")
self._isContinuousRecognitionActive = true
self.eventHandler?(["type": "sessionStarted"])
}
recognizer.addSessionStoppedEventHandler { [weak self] _, _ in
guard let self = self else { return }
print("[AzureAsrHelper] 识别会话已结束")
self._isContinuousRecognitionActive = false
self.eventHandler?(["type": "sessionStopped"])
}
// 取消事件
recognizer.addCanceledEventHandler { [weak self] _, event in
guard let self = self else { return }
let reason = event.reason.rawValue
let errorDetails = event.errorDetails ?? "未知错误"
print("[AzureAsrHelper] 识别取消: \(errorDetails)")
self.eventHandler?(["type": "canceled", "reason": reason, "errorDetails": errorDetails])
self._isContinuousRecognitionActive = false
}
}
/// 执行一次性语音识别
/// - Returns: 是否成功启动识别
func recognizeOnce() -> Bool {
// 检查是否初始化
if !isInitialized {
print("[AzureAsrHelper] 错误: 语音服务未初始化")
eventHandler?(["type": "error", "message": "语音服务未初始化"])
return false
}
// 检查参数有效性
if speechSubscriptionKey.isEmpty || serviceRegion.isEmpty {
print("[AzureAsrHelper] 错误: Azure 配置信息不完整")
eventHandler?(["type": "error", "message": "Azure 配置信息不完整"])
return false
}
// 如果正在连续识别,先停止
if _isContinuousRecognitionActive {
stopContinuousRecognition()
}
// 确保识别器已创建
if recognizer == nil && !createRecognizerAndSetupCallbacks() {
return false
}
do {
// 启动音频处理
startAudioProcessing()
// 通知会话开始
eventHandler?(["type": "sessionStarted"])
// 执行识别
try recognizer?.recognizeOnceAsync { [weak self] result in
guard let self = self else { return }
// 停止音频处理
self.stopAudioProcessing()
if result.reason == SPXResultReason.recognizedSpeech {
let detectedLanguage = self.getDetectedLanguage(from: result)
self.eventHandler?(["type": "result",
"text": result.text ?? "",
"detectedLanguage": detectedLanguage
])
} else if result.reason == SPXResultReason.noMatch {
print("[AzureAsrHelper] 无匹配结果")
self.eventHandler?(["type": "noMatch"])
} else if result.reason == SPXResultReason.canceled {
do {
let details = try SPXCancellationDetails(fromCanceledRecognitionResult: result)
let errorDetails = details.errorDetails ?? "未知错误"
self.eventHandler?(["type": "error", "message": "识别取消: \(errorDetails)"])
} catch {
print("[AzureAsrHelper] 错误: 获取取消详情失败: \(error.localizedDescription)")
self.eventHandler?(["type": "error", "message": "识别取消,无法获取详细原因"])
}
}
}
return true
} catch {
print("[AzureAsrHelper] 错误: 识别异常: \(error.localizedDescription)")
eventHandler?(["type": "error", "message": "识别异常: \(error.localizedDescription)"])
stopAudioProcessing()
return false
}
}
/// 开始连续语音识别
/// - Returns: 是否成功启动识别
func startContinuousRecognition() -> Bool {
// 检查是否初始化
if !isInitialized {
print("[AzureAsrHelper] 错误: 语音服务未初始化")
eventHandler?(["type": "error", "message": "语音服务未初始化"])
return false
}
// 检查参数有效性
if speechSubscriptionKey.isEmpty || serviceRegion.isEmpty {
print("[AzureAsrHelper] 错误: Azure 配置信息不完整")
eventHandler?(["type": "error", "message": "Azure 配置信息不完整"])
return false
}
// 检查是否已经在识别
if _isContinuousRecognitionActive {
print("[AzureAsrHelper] 已经在进行连续识别中,忽略请求")
return true
}
// 确保识别器已创建
if recognizer == nil {
print("[AzureAsrHelper] 尝试重新创建识别器...")
if !createRecognizerAndSetupCallbacks() {
eventHandler?(["type": "error", "message": "无法创建语音识别器"])
return false
}
}
// 启动音频处理
startAudioProcessing()
// 尝试启动连续识别
do {
// 启动连续识别
try recognizer?.startContinuousRecognition()
_isContinuousRecognitionActive = true
print("[AzureAsrHelper] 连续识别已启动")
return true
} catch {
_isContinuousRecognitionActive = false
print("[AzureAsrHelper] 错误: 开始连续识别失败: \(error.localizedDescription)")
// 停止音频处理
stopAudioProcessing()
// 发送错误通知
eventHandler?(["type": "error", "message": "开始连续识别失败: \(error.localizedDescription)"])
return false
}
}
/// 停止连续语音识别
/// - Returns: 是否成功停止识别
func stopContinuousRecognition() -> Bool {
// 停止音频处理
stopAudioProcessing()
// 检查是否初始化
if !isInitialized {
print("[AzureAsrHelper] 错误: 语音服务未初始化")
eventHandler?(["type": "error", "message": "语音服务未初始化"])
return false
}
if !_isContinuousRecognitionActive {
print("[AzureAsrHelper] 未进行连续识别,忽略停止请求")
return true
}
print("[AzureAsrHelper] 停止连续语音识别")
// 防止空指针异常
guard let recognizer = recognizer else {
print("[AzureAsrHelper] 警告: 识别器为空,但状态显示活跃")
_isContinuousRecognitionActive = false
eventHandler?(["type": "sessionStopped"])
return true
}
// 先标记为非活跃状态,防止重复调用
_isContinuousRecognitionActive = false
// 异步执行停止操作,避免阻塞主线程
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
guard let self = self else { return }
do {
// 在后台线程执行停止操作
try recognizer.stopContinuousRecognition()
// 主线程回调通知结果
DispatchQueue.main.async {
print("[AzureAsrHelper] 连续识别已停止")
self.eventHandler?(["type": "sessionStopped"])
}
} catch {
// 主线程回调通知错误
DispatchQueue.main.async {
print("[AzureAsrHelper] 错误: 停止连续识别失败: \(error.localizedDescription)")
// 停止失败,恢复状态
self._isContinuousRecognitionActive = true
self.eventHandler?(["type": "error", "message": "停止识别失败: \(error.localizedDescription)"])
}
}
}
// 立即返回,不阻塞调用方
return true
}
/// 检查连续识别是否活跃
/// - Returns: 连续识别是否处于活跃状态
func isContinuousRecognitionActive() -> Bool {
return _isContinuousRecognitionActive
}
/// 释放资源
func dispose() {
// 停止音频处理
stopAudioProcessing()
// 尝试停止所有识别操作
if _isContinuousRecognitionActive {
do {
if let recognizer = recognizer {
try recognizer.stopContinuousRecognition()
print("[AzureAsrHelper] 连续识别已停止")
}
} catch {
print("[AzureAsrHelper] 警告: 停止连续识别失败: \(error.localizedDescription)")
}
_isContinuousRecognitionActive = false
}
// 释放识别器
recognizer = nil
audioConfig = nil
speechConfig = nil
pushStream = nil
audioProcessor = nil
// 重置状态
isInitialized = false
print("[AzureAsrHelper] 资源已释放")
}
/// 从结果中获取检测到的语言
private func getDetectedLanguage(from result: SPXSpeechRecognitionResult) -> String {
if isAutoDetectLanguage {
do {
let langResult = try SPXAutoDetectSourceLanguageResult(result)
return langResult.language ?? currentLanguage
} catch {
print("[AzureAsrHelper] 错误: 获取检测到的语言失败: \(error.localizedDescription)")
return currentLanguage
}
} else {
return currentLanguage
}
}
/// 设置事件处理器
@objc func setEventHandler(_ handler: @escaping ([String: Any]) -> Void) {
self.eventHandler = handler
}
// MARK: - 音频处理
/// 开始音频处理
private func startAudioProcessing() {
// 如果未使用自定义音频处理器,跳过
if !useCustomAudioProcessor {
return
}
guard !isProcessingAudio, let audioProcessor = audioProcessor else { return }
isProcessingAudio = true
// 启动音频处理器
if !audioProcessor.startRecord() {
print("[AzureAsrHelper] 错误: 启动音频处理器失败")
eventHandler?(["type": "error", "message": "启动音频处理器失败"])
return
}
// 启动音频处理定时器
audioProcessingTimer = Timer.scheduledTimer(withTimeInterval: 0.08, repeats: true) { [weak self] _ in
guard let self = self, self.isProcessingAudio, let processor = self.audioProcessor, let stream = self.pushStream else {
return
}
// 读取处理后的音频数据
var bytes = [UInt8](repeating: 0, count: 2560)
let bytesRead = processor.read(bytes: &bytes)
if bytesRead > 0 {
// 推送数据到Azure语音服务
let data = Data(bytes: bytes, count: bytesRead)
stream.write(data)
// 通知音频数据可用(可选)
// self.eventHandler?(["type": "audioData", "data": bytes])
}
}
print("[AzureAsrHelper] 音频处理已启动")
}
/// 停止音频处理
private func stopAudioProcessing() {
// 如果未使用自定义音频处理器,跳过
if !useCustomAudioProcessor {
return
}
// 停止定时器
audioProcessingTimer?.invalidate()
audioProcessingTimer = nil
// 停止音频处理器
audioProcessor?.stopRecord()
isProcessingAudio = false
print("[AzureAsrHelper] 音频处理已停止")
}
}
// MARK: - 自定义音频处理器
@available(iOS 13.0, *)
class CustomAudioProcessor: NSObject {
// 音频单元
private var ioUnit: AudioUnit?
// 音频格式
private var audioFormat: AudioStreamBasicDescription
// 音频缓冲
private var audioBufferList: AudioBufferList
private var audioList: [Float] = []
private let audioListQueue = DispatchQueue(label: "audioListQueue")
// 回音消除状态
private var isEchoCancellationEnabled = true
override init() {
// 设置音频格式 - 16kHz, 16位, 单声道
audioFormat = AudioStreamBasicDescription(
mSampleRate: 16000.0,
mFormatID: kAudioFormatLinearPCM,
mFormatFlags: kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked,
mBytesPerPacket: 2,
mFramesPerPacket: 1,
mBytesPerFrame: 2,
mChannelsPerFrame: 1,
mBitsPerChannel: 16,
mReserved: 0
)
// 初始化音频缓冲
audioBufferList = AudioBufferList(
mNumberBuffers: 1,
mBuffers: AudioBuffer(
mNumberChannels: 1,
mDataByteSize: 4096,
mData: malloc(4096)
)
)
super.init()
}
deinit {
stopRecord()
free(audioBufferList.mBuffers.mData)
}
/// 启动音频处理
/// - Returns: 是否成功启动
func startRecord() -> Bool {
print("[CustomAudioProcessor] 配置音频单元")
// 创建音频组件描述 - 使用VoiceProcessingIO类型获取回音消除
var ioUnitDescription = AudioComponentDescription(
componentType: kAudioUnitType_Output,
componentSubType: kAudioUnitSubType_VoiceProcessingIO,
componentManufacturer: kAudioUnitManufacturer_Apple,
componentFlags: 0,
componentFlagsMask: 0
)
// 查找音频组件
guard let ioUnitRef = AudioComponentFindNext(nil, &ioUnitDescription) else {
print("[CustomAudioProcessor] 错误: 未找到音频组件")
return false
}
// 创建音频单元实例
if checkError(AudioComponentInstanceNew(ioUnitRef, &ioUnit), "创建音频单元") {
ioUnit = nil
return false
}
// 启用输入端口
var enableInput: UInt32 = 1
let kInputBus: AudioUnitElement = 1
let kOutputBus: AudioUnitElement = 0
if checkError(AudioUnitSetProperty(ioUnit!, kAudioOutputUnitProperty_EnableIO,
kAudioUnitScope_Input, kInputBus, &enableInput,
UInt32(MemoryLayout<UInt32>.size)), "启用输入端口") {
return false
}
// 禁用输出端口 (我们只需要输入)
var enableOutput: UInt32 = 0
if checkError(AudioUnitSetProperty(ioUnit!, kAudioOutputUnitProperty_EnableIO,
kAudioUnitScope_Output, kOutputBus,
&enableOutput, UInt32(MemoryLayout<UInt32>.size)), "禁用输出端口") {
return false
}
// 设置缓冲区分配标志
var flag: UInt32 = 0
if checkError(AudioUnitSetProperty(ioUnit!, kAudioUnitProperty_ShouldAllocateBuffer,
kAudioUnitScope_Output, kInputBus, &flag, UInt32(MemoryLayout<UInt32>.size)), "设置缓冲区分配标志") {
return false
}
// 设置音频格式
let size = UInt32(MemoryLayout<AudioStreamBasicDescription>.size)
if checkError(AudioUnitSetProperty(ioUnit!, kAudioUnitProperty_StreamFormat,
kAudioUnitScope_Output, kInputBus, &audioFormat, size), "设置输入总线输出范围的流格式") {
return false
}
if checkError(AudioUnitSetProperty(ioUnit!, kAudioUnitProperty_StreamFormat,
kAudioUnitScope_Input, kOutputBus, &audioFormat, size), "设置输出总线输入范围的流格式") {
return false
}
// 启用回音消除 - 注意: kAUVoiceIOProperty_BypassVoiceProcessing值为1时表示绕过处理,值为0表示启用处理
if isEchoCancellationEnabled {
var echoCancellation: UInt32 = 0 // 0表示不绕过,即启用回音消除
AudioUnitSetProperty(ioUnit!, kAUVoiceIOProperty_BypassVoiceProcessing,
kAudioUnitScope_Global, 0, &echoCancellation, UInt32(MemoryLayout<UInt32>.size))
}
// 设置输入回调 - 当有新音频数据时调用
var inputCallback = AURenderCallbackStruct(
inputProc: CustomAudioProcessor.onAudioDataAvailable,
inputProcRefCon: UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())
)
if checkError(AudioUnitSetProperty(ioUnit!,
kAudioOutputUnitProperty_SetInputCallback,
kAudioUnitScope_Global, kInputBus,
&inputCallback, UInt32(MemoryLayout<AURenderCallbackStruct>.size)), "设置输入回调") {
return false
}
// 初始化音频单元
var hasError = checkError(AudioUnitInitialize(ioUnit!), "初始化音频单元")
while hasError {
Thread.sleep(forTimeInterval: 0.1)
hasError = checkError(AudioUnitInitialize(ioUnit!), "初始化音频单元")
}
// 启动音频单元
hasError = checkError(AudioOutputUnitStart(ioUnit!), "启动音频单元")
print("[CustomAudioProcessor] 音频处理器已启动,回音消除\(isEchoCancellationEnabled ? "已启用" : "已禁用")")
return !hasError
}
/// 停止音频处理
func stopRecord() {
print("[CustomAudioProcessor] 停止音频处理器")
if let ioUnit = ioUnit {
// 停止音频单元
_ = checkError(AudioOutputUnitStop(ioUnit), "停止音频单元")
// 关闭音频单元
_ = checkError(AudioUnitUninitialize(ioUnit), "反初始化音频单元")
_ = checkError(AudioComponentInstanceDispose(ioUnit), "释放音频单元")
self.ioUnit = nil
}
// 清空音频数据缓冲
audioListQueue.sync {
audioList.removeAll()
}
}
/// 音频数据回调 - 当有新的音频数据可用时调用
private static let onAudioDataAvailable: AURenderCallback = { inRefCon, ioActionFlags, inTimeStamp, inBusNumber, inNumberFrames, ioData in
// 获取实例
let processor = Unmanaged<CustomAudioProcessor>.fromOpaque(inRefCon).takeUnretainedValue()
// 计算预期数据大小
let expectedDataByteSize = inNumberFrames * processor.audioFormat.mBytesPerFrame
// 确保缓冲区足够大
if processor.audioBufferList.mBuffers.mDataByteSize < expectedDataByteSize {
processor.audioBufferList.mBuffers.mData = realloc(processor.audioBufferList.mBuffers.mData, Int(expectedDataByteSize))
processor.audioBufferList.mBuffers.mDataByteSize = expectedDataByteSize
}
// 渲染音频数据
let status = processor.checkOSStatus(AudioUnitRender(processor.ioUnit!, ioActionFlags, inTimeStamp,
inBusNumber, inNumberFrames, &processor.audioBufferList),
"渲染音频数据")
// 将Int16数据转换为浮点数据进行处理
var audioDataFloat = [Float](repeating: 0.0, count: Int(inNumberFrames))
let buffer = processor.audioBufferList.mBuffers
let bufferData = buffer.mData!.assumingMemoryBound(to: Int16.self)
for j in 0..<Int(buffer.mDataByteSize / UInt32(MemoryLayout<Int16>.size)) {
// 归一化到[-1.0, 1.0]范围
audioDataFloat[j] = Float(bufferData[j]) / 32768.0
}
// 保存处理后的数据
if status == noErr {
processor.audioListQueue.async {
processor.audioList.append(contentsOf: audioDataFloat)
}
}
return status
}
/// 读取处理后的音频数据
/// - Parameter bytes: 输出字节数组
/// - Returns: 读取的字节数
func read(bytes: inout [UInt8]) -> Int {
return audioListQueue.sync {
// 如果没有数据,返回0
if audioList.isEmpty {
return 0
}
// 确保有足够的数据 (至少1280个样本)
if audioList.count < 1280 {
return 0
}
// 读取一帧数据 (1280个样本)
let frameLength = 1280
let buffer = Array(audioList.prefix(frameLength))
audioList.removeFirst(frameLength)
// 将浮点数据转回Int16格式
var int16Data = buffer.map { Int16($0 * 32767) }
// 转换为字节数组
let data = Data(buffer: UnsafeBufferPointer(start: &int16Data, count: int16Data.count))
bytes = [UInt8](data)
// 每个样本2字节 (16位PCM)
return frameLength * 2
}
}
/// 检查错误并打印日志
/// - Parameters:
/// - status: 操作状态
/// - operation: 操作描述
/// - Returns: 是否发生错误
private func checkError(_ status: OSStatus, _ operation: String) -> Bool {
if status != noErr {
print("[CustomAudioProcessor] 错误: \(operation)失败: \(status)")
return true
}
return false
}
/// 检查OSStatus并返回状态
/// - Parameters:
/// - status: 操作状态
/// - operation: 操作描述
/// - Returns: 原始状态
private func checkOSStatus(_ status: OSStatus, _ operation: String) -> OSStatus {
if status != noErr {
print("[CustomAudioProcessor] 错误: \(operation)失败: \(status)")
}
return status
}
}

454
ios/Runner/AzureTtsHelper.swift

@ -1,454 +0,0 @@
import Foundation
import MicrosoftCognitiveServicesSpeech
import AVFoundation
/// Azure TTS工具类,负责实现TTS服务接口
@available(iOS 13.0, *)
class AzureTtsHelper: NSObject {
// MARK: - 属性
/// 事件处理回调
private var eventHandler: (([String: Any]) -> Void)?
/// 语音配置信息
private var speechSubscriptionKey: String = ""
private var serviceRegion: String = ""
/// 语音合成配置
private var speechConfig: SPXSpeechConfiguration?
/// 语音合成器
private var synthesizer: SPXSpeechSynthesizer?
/// 是否初始化成功
private var isInitialized = false
/// 当前是否正在播放
private var _isSpeaking = false
/// 音频会话配置
private var isAudioSessionConfigured = false
/// 音频会话管理器
private let audioSessionManager = AudioSessionManager.shared
// MARK: - 语音设置
/// 当前语音
private var currentVoice = "zh-CN-XiaoxiaoNeural"
/// 支持的语音映射
private var voiceMap: [String: String] = [
"zh-CN": "zh-CN-XiaoxiaoNeural",
"en-US": "en-US-JennyNeural",
"ja-JP": "ja-JP-NanamiNeural",
"ko-KR": "ko-KR-SunHiNeural",
"zh-TW": "zh-TW-HsiaoChenNeural",
"zh-HK": "zh-HK-HiuMaanNeural"
]
/// 当前语音合成参数
private var currentSpeechRate = "0%"
private var currentPitch = "0%"
private var currentVolume = "100%"
// MARK: - 初始化
override init() {
super.init()
}
deinit {
dispose()
}
/// 设置事件处理器
@objc func setEventHandler(_ handler: @escaping ([String: Any]) -> Void) {
self.eventHandler = handler
}
// MARK: - TTS 接口实现
/// 初始化语音合成服务
/// - Parameters:
/// - speechSubscriptionKey: Azure 语音服务订阅密钥
/// - serviceRegion: Azure 服务区域 (如 eastasia)
/// - language: 语言代码 (默认 zh-CN)
/// - Returns: 初始化是否成功
func initialize(speechSubscriptionKey: String, serviceRegion: String, language: String = "zh-CN") -> Bool {
print("[AzureTtsHelper] 初始化语音合成服务")
// 检查配置是否为空
if speechSubscriptionKey.isEmpty || serviceRegion.isEmpty {
print("[AzureTtsHelper] 错误: Azure 配置信息不完整")
eventHandler?(["type": "error", "message": "Azure 配置信息不完整"])
return false
}
// 释放之前的资源
dispose()
// 记录配置信息
self.speechSubscriptionKey = speechSubscriptionKey
self.serviceRegion = serviceRegion
// 配置音频会话
if !configureAudioSession() {
print("[AzureTtsHelper] 警告: 音频会话配置失败,将尝试继续初始化")
}
do {
// 创建语音配置
speechConfig = try SPXSpeechConfiguration(subscription: speechSubscriptionKey, region: serviceRegion)
// 设置默认语音
let defaultVoice = getDefaultVoiceForLanguage(language)
currentVoice = defaultVoice
speechConfig?.speechSynthesisVoiceName = defaultVoice
// 创建语音合成器
synthesizer = try SPXSpeechSynthesizer(speechConfig!)
// 设置事件处理器
setupSynthesizerEvents()
isInitialized = true
print("[AzureTtsHelper] TTS 引擎初始化成功")
return true
} catch {
print("[AzureTtsHelper] 错误: 初始化语音合成服务失败: \(error.localizedDescription)")
eventHandler?(["type": "error", "message": "初始化语音合成服务失败: \(error.localizedDescription)"])
return false
}
}
/// 配置音频会话
private func configureAudioSession() -> Bool {
do {
// 使用音频会话管理器配置媒体播放模式
try audioSessionManager.configureForVoiceInteraction()
isAudioSessionConfigured = true
print("[AzureTtsHelper] 音频会话配置成功")
return true
} catch {
print("[AzureTtsHelper] 警告: 音频会话配置失败: \(error.localizedDescription)")
isAudioSessionConfigured = false
return false
}
}
/// 设置语音
/// - Parameter voiceName: 语音名称 (如 "zh-CN-XiaoxiaoNeural")
/// - Returns: 设置是否成功
func setVoice(voiceName: String) -> Bool {
// 检查是否初始化
if !isInitialized {
print("[AzureTtsHelper] 错误: TTS 引擎尚未初始化")
eventHandler?(["type": "error", "message": "TTS 引擎尚未初始化"])
return false
}
// 检查参数有效性
if speechSubscriptionKey.isEmpty || serviceRegion.isEmpty {
print("[AzureTtsHelper] 错误: Azure 配置信息不完整")
eventHandler?(["type": "error", "message": "Azure 配置信息不完整"])
return false
}
if voiceName.isEmpty {
print("[AzureTtsHelper] 错误: 声音名称为空")
eventHandler?(["type": "error", "message": "声音名称不能为空"])
return false
}
if voiceName == currentVoice {
print("[AzureTtsHelper] 已设置语音: \(voiceName)")
return true
}
print("[AzureTtsHelper] 设置声音: \(voiceName)")
currentVoice = voiceName
// 更新语音配置
if let speechConfig = speechConfig {
speechConfig.speechSynthesisVoiceName = voiceName
return true
}
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 !isInitialized {
print("[AzureTtsHelper] 错误: TTS 引擎尚未初始化")
eventHandler?(["type": "error", "message": "TTS 引擎尚未初始化"])
return false
}
// 检查参数有效性
if speechSubscriptionKey.isEmpty || serviceRegion.isEmpty {
print("[AzureTtsHelper] 错误: Azure 配置信息不完整")
eventHandler?(["type": "error", "message": "Azure 配置信息不完整"])
return false
}
// 转换参数格式
currentSpeechRate = formatRateParam(rate)
currentPitch = formatPitchParam(pitch)
currentVolume = formatVolumeParam(volume)
print("[AzureTtsHelper] 已设置语音参数: 语速=\(currentSpeechRate), 音调=\(currentPitch), 音量=\(currentVolume)")
return true
}
/// 合成文本为语音并播放
/// - Parameter text: 要合成的文本
/// - Returns: 操作是否成功启动
func speakText(text: String) -> Bool {
// 检查是否初始化
if !isInitialized {
print("[AzureTtsHelper] 错误: TTS 引擎尚未初始化")
eventHandler?(["type": "error", "message": "TTS 引擎尚未初始化"])
return false
}
// 检查参数有效性
if speechSubscriptionKey.isEmpty || serviceRegion.isEmpty {
print("[AzureTtsHelper] 错误: Azure 配置信息不完整")
eventHandler?(["type": "error", "message": "Azure 配置信息不完整"])
return false
}
if text.isEmpty {
print("[AzureTtsHelper] 警告: 要播放的文本为空")
return true
}
// 确保音频会话已配置
if !isAudioSessionConfigured {
_ = configureAudioSession()
}
print("[AzureTtsHelper] 开始语音合成: \(text.prefix(50))...")
// 生成SSML
let ssml = generateSsml(text: text)
// 直接进行SSML合成
return speakSsmlInternal(text: ssml)
}
/// 内部SSML合成和播放
private func speakSsmlInternal(text: String) -> Bool {
guard let synthesizer = synthesizer else {
print("[AzureTtsHelper] 错误: 合成器未初始化")
eventHandler?(["type": "error", "message": "合成器未初始化"])
return false
}
_isSpeaking = true
eventHandler?(["type": "started"])
Task {
do {
// 使用异步方法进行合成并直接播放
_ = try synthesizer.startSpeakingSsml(text)
} catch {
print("[AzureTtsHelper] 错误: 语音合成失败: \(error.localizedDescription)")
DispatchQueue.main.async {
self._isSpeaking = false
self.eventHandler?(["type": "error", "message": "语音合成失败: \(error.localizedDescription)"])
}
}
}
return true
}
/// 停止当前语音合成
/// - Returns: 操作是否成功
func stopSpeaking() -> Bool {
// 检查是否初始化
if !isInitialized {
print("[AzureTtsHelper] 错误: TTS 引擎尚未初始化")
eventHandler?(["type": "error", "message": "TTS 引擎尚未初始化"])
return false
}
if !_isSpeaking {
print("[AzureTtsHelper] 未进行语音播放,忽略停止请求")
return true
}
// 停止合成
do {
try synthesizer?.stopSpeaking()
_isSpeaking = false
eventHandler?(["type": "canceled"])
print("[AzureTtsHelper] 已停止语音合成")
return true
} catch {
print("[AzureTtsHelper] 错误: 停止语音合成失败: \(error.localizedDescription)")
eventHandler?(["type": "error", "message": "停止语音合成失败: \(error.localizedDescription)"])
return false
}
}
/// 检查是否正在播放
/// - Returns: 当前是否正在播放语音
func isSpeaking() -> Bool {
return _isSpeaking
}
/// 释放资源
func dispose() {
// 尝试停止所有语音合成
if _isSpeaking {
do {
if let synthesizer = synthesizer {
try synthesizer.stopSpeaking()
print("[AzureTtsHelper] 语音合成已停止")
}
} catch {
print("[AzureTtsHelper] 警告: 停止语音合成失败: \(error.localizedDescription)")
}
_isSpeaking = false
}
// 释放合成器
synthesizer = nil
speechConfig = nil
// 重置状态
isInitialized = false
isAudioSessionConfigured = false
print("[AzureTtsHelper] 资源已释放")
}
// MARK: - 私有辅助方法
/// 设置合成器事件处理
private func setupSynthesizerEvents() {
guard let synthesizer = synthesizer else { return }
// 添加书签到达事件处理
synthesizer.addBookmarkReachedEventHandler { _, e in
print("[AzureTtsHelper] 书签事件: 音频偏移: \((e.audioOffset + 5000) / 10000)ms, 文本: \"\(e.text)\"")
}
// 合成完成事件
synthesizer.addSynthesisCompletedEventHandler { [weak self] _, e in
guard let self = self else { return }
print("[AzureTtsHelper] 语音合成完成: 音频持续时间: \(e.result.audioDuration)")
DispatchQueue.main.async {
self._isSpeaking = false
self.eventHandler?(["type": "completed"])
}
}
// 合成取消事件
synthesizer.addSynthesisCanceledEventHandler { [weak self] _, e in
guard let self = self else { return }
let result = e.result
do {
let cancellationDetails = try SPXSpeechSynthesisCancellationDetails(fromCanceledSynthesisResult: result)
print("[AzureTtsHelper] 语音合成取消: 原因: \(cancellationDetails.reason)")
if cancellationDetails.reason == SPXCancellationReason.error {
print("[AzureTtsHelper] 错误代码: \(cancellationDetails.errorCode)")
print("[AzureTtsHelper] 错误详情: \(cancellationDetails.errorDetails ?? "未知")")
}
DispatchQueue.main.async {
self._isSpeaking = false
self.eventHandler?(["type": "error", "message": "语音合成取消: \(cancellationDetails.errorDetails ?? "未知错误")"])
}
} catch {
print("[AzureTtsHelper] 获取取消详情时出错: \(error)")
DispatchQueue.main.async {
self._isSpeaking = false
self.eventHandler?(["type": "error", "message": "语音合成被取消"])
}
}
}
// 合成开始事件
synthesizer.addSynthesisStartedEventHandler { _, _ in
// print("[AzureTtsHelper] 语音合成开始")
}
// 合成中事件
synthesizer.addSynthesizingEventHandler { _, _ in
// print("[AzureTtsHelper] 语音合成中")
}
}
/// 生成 SSML 文本
private func generateSsml(text: String) -> String {
return """
<speak version='1.0' xmlns='http://www.w3.org/2001/10/synthesis' xml:lang='zh-CN'>
<voice name='\(currentVoice)'>
<prosody rate='\(currentSpeechRate)' pitch='\(currentPitch)' volume='\(currentVolume)'>
\(text)
</prosody>
</voice>
</speak>
"""
}
/// 格式化语速参数
private func formatRateParam(_ rate: Int) -> String {
let clampedRate = rate.clamp(min: -100, max: 100)
if clampedRate == 0 {
return "0%"
} else if clampedRate < 0 {
return "\(Int(Double(clampedRate) * 0.9))%"
} else {
return "+\(clampedRate)%"
}
}
/// 格式化音调参数
private func formatPitchParam(_ pitch: Int) -> String {
let clampedPitch = pitch.clamp(min: -100, max: 100)
if clampedPitch == 0 {
return "0%"
} else {
return "\(Int(Double(clampedPitch) * 0.5))%"
}
}
/// 格式化音量参数
private func formatVolumeParam(_ volume: Int) -> String {
let clampedVolume = volume.clamp(min: 0, max: 100)
return "\(clampedVolume)%"
}
/// 获取指定语言的默认语音
private func getDefaultVoiceForLanguage(_ language: String) -> String {
return voiceMap[language] ?? "zh-CN-XiaoxiaoNeural"
}
}
// MARK: - 扩展
extension Int {
func clamp(min: Int, max: Int) -> Int {
if self < min { return min }
if self > max { return max }
return self
}
}

13
ios/Runner/Base.lproj/Main.storyboard

@ -1,10 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="23504" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<device id="retina6_12" orientation="portrait" appearance="light"/>
<?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="23506"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
@ -16,14 +14,13 @@
<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="393" height="852"/>
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-26" y="-77"/>
</scene>
</scenes>
</document>

464
ios/Runner/BluetoothMediaButtonHelper.swift

@ -1,464 +0,0 @@
import Foundation
import MediaPlayer
import AVFoundation
import UIKit
/// 媒体按钮事件处理协议
@available(iOS 13.0, *)
@objc protocol MediaButtonEventHandler: AnyObject {
func sendEvent(_ event: [String: Any])
}
/// 蓝牙媒体按键事件处理类
/// 该类仅在iOS 13.0及以上版本可用
@available(iOS 13.0, *)
class BluetoothMediaButtonHelper: NSObject, AVAudioPlayerDelegate {
// MARK: - 属性
@objc static let shared = BluetoothMediaButtonHelper()
/// 日志标签
private let TAG = "BluetoothMediaButtonHelper"
/// 远程控制中心
private var remoteCommandCenter: MPRemoteCommandCenter
/// 事件处理器 - 用于发送事件到Flutter
private weak var eventHandler: MediaButtonEventHandler?
/// 是否已启用监听
private var isListening = false
/// 静音音频播放器 - 用于保持音频会话活跃
private var silentAudioPlayer: AVAudioPlayer?
/// 播放计时器 - 用于定期更新播放信息
private var playbackTimer: Timer?
/// 当前播放时间 - 模拟播放进度
private var currentPlaybackTime: TimeInterval = 0
/// 总时长 - 模拟
private let totalDuration: TimeInterval = 300.0
/// 音频会话管理器
private let audioSessionManager = AudioSessionManager.shared
// MARK: - 初始化
override init() {
// 获取远程控制中心单例
self.remoteCommandCenter = MPRemoteCommandCenter.shared()
super.init()
NSLog("\(TAG): 初始化媒体按钮处理器")
// 初始化静音音频播放器
initSilentAudioPlayer()
}
deinit {
stopButtonListening()
NSLog("\(TAG): 媒体按钮处理器已释放")
}
// MARK: - 公共方法
/// 设置事件处理器
@objc func setEventHandler(_ handler: MediaButtonEventHandler) {
eventHandler = handler
NSLog("\(TAG): 已设置媒体按钮事件处理器")
}
/// 开始监听蓝牙媒体按钮事件
@objc func startButtonListening() -> Bool {
if isListening {
NSLog("\(TAG): 已经在监听蓝牙媒体按钮事件")
return true
}
NSLog("\(TAG): 开始监听蓝牙媒体按钮事件")
// 立即注册远程命令 - 这不需要音频会话
registerRemoteCommands()
// 标记为已在监听
isListening = true
// 确保NowPlaying信息可用 - 这也不依赖于活跃的音频会话
setupNowPlaying(isPlaying: true)
// 尝试启用静音音频播放(但即使失败,远程命令仍然可用)
do {
// try audioSessionManager.configureForBluetoothMediaButtons()
startSilentAudio()
// 延时1秒
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
self.setupNowPlaying(isPlaying: false)
self.stopSilentAudio()
}
// startUpdatingPlaybackInfo()
} catch {
NSLog("\(TAG): 音频设置或播放失败: \(error.localizedDescription)")
// 继续执行,因为远程命令在某些设备上不需要播放音频也能工作
}
NSLog("\(TAG): 蓝牙媒体按钮监听已启动")
return true
}
/// 停止监听蓝牙媒体按钮事件
@objc func stopButtonListening() -> Bool {
if !isListening {
return true
}
NSLog("\(TAG): 停止监听蓝牙媒体按钮事件")
// 停止播放静音音频
stopSilentAudio()
// 停止更新播放信息
stopUpdatingPlaybackInfo()
// 取消注册远程命令
unregisterRemoteCommands()
// 清除NowPlaying信息
DispatchQueue.main.async {
MPNowPlayingInfoCenter.default().nowPlayingInfo = nil
}
isListening = false
NSLog("\(TAG): 蓝牙媒体按钮监听已停止")
return true
}
// MARK: - 私有方法
/// 初始化静音音频播放器
private func initSilentAudioPlayer() {
do {
// 尝试加载sample.mp3文件
if let audioPath = Bundle.main.path(forResource: "sample", ofType: "mp3") {
let audioUrl = URL(fileURLWithPath: audioPath)
silentAudioPlayer = try AVAudioPlayer(contentsOf: audioUrl)
silentAudioPlayer?.numberOfLoops = -1 // 无限循环播放
silentAudioPlayer?.volume = 0.0 // 设置为全音量
silentAudioPlayer?.prepareToPlay() // 预加载
silentAudioPlayer?.delegate = self
NSLog("\(TAG): 音频播放器使用sample.mp3初始化成功")
return
}
NSLog("\(TAG): 未找到sample.mp3文件,使用生成的静音数据")
} catch {
NSLog("\(TAG): 初始化静音音频播放器失败: \(error.localizedDescription)")
}
}
/// 开始播放静音音频
private func startSilentAudio() {
guard let player = silentAudioPlayer else {
NSLog("\(TAG): 无法开始播放,播放器未初始化")
return
}
if !player.isPlaying {
let success = player.play()
if success {
NSLog("\(TAG): 开始播放静音音频")
} else {
NSLog("\(TAG): 播放启动失败")
}
} else {
NSLog("\(TAG): 静音音频已在播放中")
}
}
/// 停止播放静音音频
private func stopSilentAudio() {
silentAudioPlayer?.stop()
NSLog("\(TAG): 停止播放静音音频")
}
/// 开始更新播放信息
private func startUpdatingPlaybackInfo() {
// 初始设置
setupNowPlaying(isPlaying: true)
// 定期更新播放信息 - 恢复定时器,确保持续更新播放状态
playbackTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
guard let self = self else { return }
// 更新播放时间(模拟)
self.currentPlaybackTime += 1.0
if self.currentPlaybackTime >= self.totalDuration {
self.currentPlaybackTime = 0.0
}
// 更新NowPlaying信息
self.setupNowPlaying(isPlaying: true)
}
NSLog("\(TAG): 开始更新播放信息")
}
/// 停止更新播放信息
private func stopUpdatingPlaybackInfo() {
playbackTimer?.invalidate()
playbackTimer = nil
// 清空NowPlaying信息
DispatchQueue.main.async {
MPNowPlayingInfoCenter.default().nowPlayingInfo = nil
}
NSLog("\(TAG): 停止更新播放信息")
}
/// 设置NowPlaying信息
private func setupNowPlaying(isPlaying: Bool) {
NSLog("\(TAG): 设置NowPlaying信息, isPlaying=\(isPlaying)")
// 创建音频专辑图片
let albumArt = UIImage(named: "AppIcon") // 使用应用图标作为专辑图片
var nowPlayingInfo: [String: Any] = [
MPMediaItemPropertyTitle: "DeepSound 语音助手",
MPMediaItemPropertyArtist: "DeepSound",
MPNowPlayingInfoPropertyPlaybackRate: isPlaying ? 1.0 : 0.0, // 1.0表示正在播放
MPNowPlayingInfoPropertyElapsedPlaybackTime: currentPlaybackTime,
MPMediaItemPropertyPlaybackDuration: totalDuration
]
// 如果有专辑图片,添加到NowPlaying信息中
if let albumArt = albumArt {
let artwork = MPMediaItemArtwork(boundsSize: albumArt.size) { size in
return albumArt
}
nowPlayingInfo[MPMediaItemPropertyArtwork] = artwork
}
// 设置NowPlaying信息 (在主线程执行)
DispatchQueue.main.async {
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
NSLog("\(self.TAG): NowPlaying信息设置完成")
}
}
/// 注册远程命令
private func registerRemoteCommands() {
let commandCenter = MPRemoteCommandCenter.shared()
// 播放/暂停命令
commandCenter.togglePlayPauseCommand.isEnabled = true
commandCenter.togglePlayPauseCommand.addTarget(self, action: #selector(handlePlayPauseCommand(_:)))
commandCenter.playCommand.isEnabled = true
commandCenter.playCommand.addTarget(self, action: #selector(handlePlayPauseCommand(_:)))
commandCenter.pauseCommand.isEnabled = true
commandCenter.pauseCommand.addTarget(self, action: #selector(handlePlayPauseCommand(_:)))
// 下一曲/上一曲命令
commandCenter.nextTrackCommand.isEnabled = true
commandCenter.nextTrackCommand.addTarget(self, action: #selector(handleNextTrackCommand(_:)))
commandCenter.previousTrackCommand.isEnabled = true
commandCenter.previousTrackCommand.addTarget(self, action: #selector(handlePreviousTrackCommand(_:)))
// 快进/快退命令
commandCenter.seekForwardCommand.isEnabled = true
commandCenter.seekForwardCommand.addTarget(self, action: #selector(handleSeekForwardCommand(_:)))
commandCenter.seekBackwardCommand.isEnabled = true
commandCenter.seekBackwardCommand.addTarget(self, action: #selector(handleSeekBackwardCommand(_:)))
// 停止命令
commandCenter.stopCommand.isEnabled = true
commandCenter.stopCommand.addTarget(self, action: #selector(handleStopCommand(_:)))
NSLog("\(TAG): 远程命令已注册")
}
/// 取消注册远程命令
private func unregisterRemoteCommands() {
NSLog("\(TAG): 取消注册远程命令")
// 基本控制命令
remoteCommandCenter.playCommand.isEnabled = false
remoteCommandCenter.playCommand.removeTarget(nil)
remoteCommandCenter.pauseCommand.isEnabled = false
remoteCommandCenter.pauseCommand.removeTarget(nil)
remoteCommandCenter.togglePlayPauseCommand.isEnabled = false
remoteCommandCenter.togglePlayPauseCommand.removeTarget(nil)
remoteCommandCenter.stopCommand.isEnabled = false
remoteCommandCenter.stopCommand.removeTarget(nil)
}
/// 发送按钮事件到Flutter
private func sendButtonEvent(type: String) {
// 创建事件数据
let eventData: [String: Any] = [
"type": "mediaButtonEvent",
"buttonType": type,
"timestamp": Int(Date().timeIntervalSince1970 * 1000)
]
// 发送事件到处理器
eventHandler?.sendEvent(eventData)
}
// MARK: - 命令处理方法
/// 处理播放/暂停命令
@objc private func handlePlayPauseCommand(_ event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
NSLog("\(TAG): 收到播放/暂停命令")
// 发送事件到Flutter
sendButtonEvent(type: "play_pause")
// 调用VoiceInteractionService处理媒体按钮事件
if VoiceInteractionService.shared.isServiceRunning() {
NSLog("\(TAG): 转发到VoiceInteractionService处理")
VoiceInteractionService.shared.handleMediaButtonAction()
}
return .success
}
/// 处理下一曲命令
@objc private func handleNextTrackCommand(_ event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
NSLog("\(TAG): 收到下一曲命令")
// 发送事件到Flutter
sendButtonEvent(type: "next_track")
// 也可以转发到VoiceInteractionService
if VoiceInteractionService.shared.isServiceRunning() {
VoiceInteractionService.shared.handleMediaButtonAction()
}
return .success
}
/// 处理上一曲命令
@objc private func handlePreviousTrackCommand(_ event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
NSLog("\(TAG): 收到上一曲命令")
// 发送事件到Flutter
sendButtonEvent(type: "previous_track")
// 也可以转发到VoiceInteractionService
if VoiceInteractionService.shared.isServiceRunning() {
VoiceInteractionService.shared.handleMediaButtonAction()
}
return .success
}
/// 处理快进命令
@objc private func handleSeekForwardCommand(_ event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
NSLog("\(TAG): 收到快进命令")
// 发送事件到Flutter
sendButtonEvent(type: "seek_forward")
return .success
}
/// 处理快退命令
@objc private func handleSeekBackwardCommand(_ event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
NSLog("\(TAG): 收到快退命令")
// 发送事件到Flutter
sendButtonEvent(type: "seek_backward")
return .success
}
/// 处理停止命令
@objc private func handleStopCommand(_ event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
NSLog("\(TAG): 收到停止命令")
// 发送事件到Flutter
sendButtonEvent(type: "stop")
// 调用VoiceInteractionService处理媒体按钮事件
if VoiceInteractionService.shared.isServiceRunning() {
VoiceInteractionService.shared.handleMediaButtonAction()
}
return .success
}
// MARK: - AVAudioPlayerDelegate方法
/// 音频播放结束回调
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
NSLog("\(TAG): 音频播放结束, 成功: \(flag)")
// 如果仍在监听状态,重新开始播放
if isListening && player == silentAudioPlayer {
player.play()
}
}
/// 音频播放错误回调
func audioPlayerDecodeErrorDidOccur(_ player: AVAudioPlayer, error: Error?) {
if let error = error {
NSLog("\(TAG): 音频解码错误: \(error.localizedDescription)")
} else {
NSLog("\(TAG): 音频解码发生未知错误")
}
// 重新初始化播放器
if player == silentAudioPlayer && isListening {
initSilentAudioPlayer()
startSilentAudio()
}
}
/// 清理和释放资源
func dispose() {
NSLog("\(TAG): 释放BluetoothMediaButtonHelper资源")
// 停止监听
stopButtonListening()
// 释放播放器和计时器资源
silentAudioPlayer = nil
if let timer = playbackTimer {
timer.invalidate()
playbackTimer = nil
}
// 清除引用
eventHandler = nil
NSLog("\(TAG): 资源已释放")
}
}
extension UInt32 {
var data: Data {
var int = self
return Data(bytes: &int, count: MemoryLayout<UInt32>.size)
}
}
extension UInt16 {
var data: Data {
var int = self
return Data(bytes: &int, count: MemoryLayout<UInt16>.size)
}
}

243
ios/Runner/ClassicBluetoothHelper.swift

@ -1,243 +0,0 @@
import Foundation
import CoreBluetooth
import AVFoundation
import UIKit
/// 蓝牙事件处理协议
@objc protocol BluetoothEventHandler: AnyObject {
func sendEvent(_ event: [String: Any])
}
@objc class ClassicBluetoothHelper: NSObject, CBCentralManagerDelegate {
// MARK: - 属性
// 音频会话管理器
private let audioSessionManager = AudioSessionManager.shared
// 蓝牙管理器
private var centralManager: CBCentralManager?
// 事件处理器
private weak var eventHandler: BluetoothEventHandler?
// MARK: - 初始化与设置
override init() {
super.init()
}
// 设置事件处理器
@objc func setEventHandler(_ handler: BluetoothEventHandler) {
eventHandler = handler
}
// MARK: - CBCentralManagerDelegate
func centralManagerDidUpdateState(_ central: CBCentralManager) {
let stateString = getBluetoothStateString(state: central.state)
sendBluetoothStateEvent(state: stateString)
}
// MARK: - 设备监控
// 获取蓝牙状态字符串
private func getBluetoothStateString(state: CBManagerState) -> String {
switch state {
case .poweredOn:
return "on"
case .poweredOff:
return "off"
default:
return "unknown"
}
}
// 发送蓝牙状态事件
private func sendBluetoothStateEvent(state: String) {
let event: [String: Any] = [
"type": "bluetoothStateChanged",
"state": state,
"timestamp": Int(Date().timeIntervalSince1970 * 1000)
]
eventHandler?.sendEvent(event)
}
// 启动蓝牙设备监控
@objc func initialize() {
if centralManager == nil {
centralManager = CBCentralManager(delegate: self, queue: nil)
}
// 发送当前蓝牙状态
if let manager = centralManager {
let stateString = getBluetoothStateString(state: manager.state)
sendBluetoothStateEvent(state: stateString)
}
// 移除之前的监听器
NotificationCenter.default.removeObserver(self)
// 添加新的监听器
audioSessionManager.addRouteChangeListener(self, selector: #selector(handleRouteChange(_:)))
// BluetoothMediaButtonHelper.shared.startButtonListening()
}
// 处理音频路由变化
@objc private func handleRouteChange(_ notification: Notification) {
guard let userInfo = notification.userInfo,
let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt,
let reason = AVAudioSession.RouteChangeReason(rawValue: reasonValue) else {
return
}
// 获取当前设备
let currentDevices = getConnectedAudioDevices()
// 检查原因
switch reason {
case .newDeviceAvailable:
// 新设备已连接
for device in currentDevices {
// 发送设备连接事件
NSLog("发送设备连接事件 - 设备: \(device["name"] ?? "未知设备") [\(device["address"] ?? "未知地址")]")
sendDeviceConnectedEvent(device: device)
}
case .oldDeviceUnavailable:
// 获取断开之前的路由
if let previousRoute = userInfo[AVAudioSessionRouteChangePreviousRouteKey] as? AVAudioSessionRouteDescription {
// 过滤蓝牙相关设备
for output in previousRoute.outputs where isBluetoothOutput(output.portType) {
let device: [String: String] = [
"name": output.portName,
"address": cleanDeviceAddress(output.uid)
]
// 发送设备断开事件
sendDeviceDisconnectedEvent(device: device)
}
}
default:
break
}
}
// 检查是否为蓝牙输出
private func isBluetoothOutput(_ portType: AVAudioSession.Port) -> Bool {
return portType == .bluetoothA2DP || portType == .bluetoothHFP || portType == .bluetoothLE
}
// MARK: - 设备事件
// 发送设备连接事件
private func sendDeviceConnectedEvent(device: [String: String]) {
guard let name = device["name"], let address = device["address"] else { return }
let deviceMap: [String: Any] = [
"name": name,
"address": address,
"type": "headset"
]
let event: [String: Any] = [
"type": "deviceConnected",
"device": deviceMap,
"timestamp": Int(Date().timeIntervalSince1970 * 1000)
]
eventHandler?.sendEvent(event)
}
// 发送设备断开事件
private func sendDeviceDisconnectedEvent(device: [String: String]) {
guard let name = device["name"], let address = device["address"] else { return }
let deviceMap: [String: Any] = [
"name": name,
"address": address,
"type": "headset"
]
let event: [String: Any] = [
"type": "deviceDisconnected",
"device": deviceMap,
"timestamp": Int(Date().timeIntervalSince1970 * 1000)
]
eventHandler?.sendEvent(event)
}
// MARK: - 公共API
// 获取当前连接的耳机设备
@objc func getConnectedHeadsetDevices(_ completion: @escaping ([[String: String]]?, String?) -> Void) {
// 检查蓝牙状态
if centralManager?.state != .poweredOn {
completion(nil, "蓝牙未启用")
return
}
do {
// 获取设备
let devices = getConnectedAudioDevices()
completion(devices, nil)
} catch {
completion(nil, "获取设备失败: \(error.localizedDescription)")
}
}
// 获取当前连接的音频设备
private func getConnectedAudioDevices() -> [[String: String]] {
var result: [[String: String]] = []
// 获取当前音频路由
let outputs = audioSessionManager.getCurrentRoute().outputs
// 过滤蓝牙相关设备
for output in outputs where isBluetoothOutput(output.portType) {
let device: [String: String] = [
"name": output.portName,
"address": cleanDeviceAddress(output.uid) // 处理设备地址,移除后缀
]
result.append(device)
}
return result
}
// 释放资源
@objc func dispose() {
audioSessionManager.removeRouteChangeListener(self)
eventHandler = nil
}
// 检查蓝牙是否启用
@objc func isBluetoothEnabled() -> Bool {
return centralManager?.state == .poweredOn
}
// 清理设备地址,移除后缀
private func cleanDeviceAddress(_ address: String) -> String {
// 检查是否包含常见分隔符
if let range = address.range(of: "-") ?? address.range(of: ":") ?? address.range(of: "_") {
// 只保留分隔符之前的部分
return String(address[..<range.lowerBound])
}
// iOS设备UID有时会包含额外信息,尝试只保留标准MAC地址格式部分
// MAC地址通常是12个十六进制字符,可能带有分隔符
let hexPattern = "[0-9A-Fa-f]{2}(:[0-9A-Fa-f]{2}){5}|[0-9A-Fa-f]{12}"
if let regex = try? NSRegularExpression(pattern: hexPattern, options: []),
let match = regex.firstMatch(in: address, options: [], range: NSRange(location: 0, length: address.utf16.count)) {
let matchRange = match.range
if let range = Range(matchRange, in: address) {
return String(address[range])
}
}
return address
}
}

108
ios/Runner/Info.plist

@ -2,46 +2,14 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>com.yunqiinnovation.deepsound</string>
<key>CFBundleURLSchemes</key>
<array>
<string>fb3829022037361855</string>
<string>com.googleusercontent.apps.907754492821-oc9e77svgmmlgouo0op7vv00e1hb8278</string>
</array>
</dict>
</array>
<key>FacebookAppID</key>
<string>3829022037361855</string>
<key>FacebookClientToken</key>
<string>193d0a2b1757c41cfa557ea8e8f21c33</string>
<key>FacebookDisplayName</key>
<string>Deepsound</string>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>fbapi</string>
<string>fb-messenger-api</string>
<string>fbauth2</string>
<string>fbshareextension</string>
<string>googlechrome</string>
<string>googlechromes</string>
</array>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>DeepSound</string>
<string>Deep Voice</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>NSUserTrackingUsageDescription</key>
<string>This application needs to access your tracking data in order to offer better personalized services and advertisements</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
@ -54,23 +22,57 @@
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>fb3829022037361855</string>
</array>
</dict>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>wx5bbb7be9324f3e2d</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>weixin</string>
<string>weixinULAPI</string>
<string>weixinURLParamsAPI</string>
<string>fbapi</string>
<string>fbapi20130214</string>
<string>fbapi20130410</string>
<string>fbapi20130702</string>
<string>fbapi20131010</string>
<string>fbapi20131219</string>
<string>fbapi20140410</string>
<string>fbapi20140116</string>
<string>fbapi20150313</string>
<string>fbapi20150629</string>
<string>fbapi20160328</string>
<string>fbauth</string>
<string>fb-messenger-share-api</string>
<string>fbauth2</string>
<string>fbshareextension</string>
</array>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSBluetoothAlwaysUsageDescription</key>
<string>Bluetooth permission is required to connect and manage audio devices</string>
<key>NSBluetoothPeripheralUsageDescription</key>
<string>Bluetooth permission is required to connect and manage audio devices</string>
<key>NSMicrophoneUsageDescription</key>
<string>Microphone permission is required for voice recognition</string>
<key>NSSpeechRecognitionUsageDescription</key>
<string>Speech recognition permission is required to provide voice interaction services</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
<key>NSAllowsArbitraryLoadsInWebContent</key>
<true/>
</dict>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UIBackgroundModes</key>
<array>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
@ -88,5 +90,23 @@
<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/>
</dict>
</plist>

0
ios/Runner/Resources/.gitkeep

BIN
ios/Runner/Resources/sample.mp3

Binary file not shown.

BIN
ios/Runner/Resources/silence.mp3

Binary file not shown.

6
ios/Runner/Runner-Bridging-Header.h

@ -1,7 +1 @@
#ifndef Runner_Bridging_Header_h
#define Runner_Bridging_Header_h
#import "GeneratedPluginRegistrant.h"
#import <MicrosoftCognitiveServicesSpeech/SPXSpeechApi.h>
#endif /* Runner_Bridging_Header_h */

5
ios/Runner/Runner.entitlements

@ -0,0 +1,5 @@
<?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/>
</plist>

565
ios/Runner/VoiceInteractionService.swift

@ -1,565 +0,0 @@
import Foundation
import UIKit
import AVFoundation
/**
* 语音交互服务,iOS原生实现
*
* 负责:
* 1) 监听蓝牙耳机按键
* 2) 处理语音识别
* 3) 与火山AI服务交互
* 4) 文本转语音播放
*/
@available(iOS 13.0, *)
class VoiceInteractionService: NSObject {
// 常量
private let TAG = "VoiceInteractionService"
private let RECOGNITION_TIMEOUT: TimeInterval = 8.0
// 单例
static let shared = VoiceInteractionService()
// 服务状态
private var isRunning = false
private var isActive = false
private var isRecognitionActive = false
private var isTimeoutPaused = false
private var hasSpeechDetected = false
private var isTtsSpeaking = false
// 事件处理器
private weak var eventHandler: VoiceInteractionEventHandler?
// 按键处理
private var lastKeyEventTime: TimeInterval = 0
private var keyEventCount = 0
// 活动时间
private var lastActivityTime: TimeInterval = 0
// 当前用户输入
private var currentUserInput = ""
private var lastUserMessage = ""
private var lastAssistantMessage = ""
// 服务组件
private var azureAsrHelper: AzureAsrHelper?
private var azureTtsHelper: AzureTtsHelper?
private var volcanoAIService: VolcanoAIService?
private var silenceTimer: Timer?
// 系统提示词
private let systemPrompt = """
你是一个智能语音助手,能够简洁明了地回答用户的问题。
请保持回答简短、准确,避免过长的解释。
如果用户的问题不清楚,请礼貌地请求澄清。
不要使用复杂的术语,除非用户明确要求。
用户用语音和你交互。
"""
// 初始化方法
private override init() {
super.init()
NSLog("%@: VoiceInteractionService 初始化中", TAG)
}
/**
* 启动服务
*
* @param apiKey Azure语音服务API密钥
* @param region Azure语音服务区域
* @param volcanoKey 火山AI服务API密钥
* @return 启动是否成功
*/
func start(azureKey: String, azureRegion: String, volcanoKey: String) -> Bool {
guard !isRunning else {
NSLog("%@: 服务已经在运行", TAG)
return true
}
NSLog("%@: 启动服务中", TAG)
// 重置状态
resetState()
// 初始化各个服务组件
initServices(azureKey: azureKey, azureRegion: azureRegion, volcanoKey: volcanoKey)
// 启动monitorService定时器
silenceTimer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(monitorService), userInfo: nil, repeats: true)
NSLog("%@: monitorService定时器已启动", TAG)
// 标记服务为运行状态
isRunning = true
NSLog("%@: 服务启动完成,等待蓝牙按键事件", TAG)
return true
}
/**
* 停止服务
*/
func stop() {
guard isRunning else {
NSLog("%@: 服务未运行", TAG)
return
}
NSLog("%@: 停止服务中", TAG)
// 停止monitorService定时器
silenceTimer?.invalidate()
silenceTimer = nil
// 停止语音识别
if isRecognitionActive {
stopVoiceRecognition()
}
// 停止TTS
stopCurrentTTS()
// 释放服务实例
azureAsrHelper?.dispose()
azureTtsHelper?.dispose()
// 更新状态
resetState()
isRunning = false
NSLog("%@: 服务已停止", TAG)
}
/**
* 获取服务运行状态
*/
func isServiceRunning() -> Bool {
return isRunning
}
/**
* 重置状态
*/
private func resetState() {
isActive = false
isRecognitionActive = false
isTimeoutPaused = false
hasSpeechDetected = false
isTtsSpeaking = false
lastUserMessage = ""
lastAssistantMessage = ""
}
/**
* 初始化服务
*/
private func initServices(azureKey: String, azureRegion: String, volcanoKey: String) {
NSLog("%@: 初始化服务组件", TAG)
// 初始化Azure ASR
azureAsrHelper = AzureAsrHelper()
if let helper = azureAsrHelper {
helper.setEventHandler({ [weak self] eventData in
// 处理ASR事件
self?.handleAsrEvent(eventData: eventData)
})
}
// 初始化Azure TTS
azureTtsHelper = AzureTtsHelper()
if let helper = azureTtsHelper {
helper.setEventHandler({ [weak self] eventData in
// 处理TTS事件
self?.handleTtsEvent(eventData: eventData)
})
}
// 初始化火山AI服务
volcanoAIService = VolcanoAIService()
// 配置Azure ASR
if !azureKey.isEmpty && !azureRegion.isEmpty {
azureAsrHelper?.initialize(speechSubscriptionKey: azureKey, serviceRegion: azureRegion, supportedLanguages: ["zh-CN"])
NSLog("%@: Azure ASR初始化完成", TAG)
} else {
NSLog("%@: Azure配置信息不完整,无法初始化Azure ASR", TAG)
}
// 配置Azure TTS
if !azureKey.isEmpty && !azureRegion.isEmpty {
azureTtsHelper?.initialize(speechSubscriptionKey: azureKey, serviceRegion: azureRegion, language: "zh-CN")
NSLog("%@: Azure TTS初始化完成", TAG)
} else {
NSLog("%@: Azure配置信息不完整,无法初始化Azure TTS", TAG)
}
// 配置火山AI
if !volcanoKey.isEmpty {
volcanoAIService?.initialize(apiKey: volcanoKey)
NSLog("%@: 火山AI服务初始化完成", TAG)
} else {
NSLog("%@: 火山AI配置信息不完整,无法初始化火山AI服务", TAG)
}
}
/**
* 监控服务状态
*/
@objc private func monitorService() {
// 确保服务保持活跃状态
if !isActive {
isActive = true
}
// 检查语音识别状态
if isRecognitionActive {
let currentTime = Date().timeIntervalSince1970
let elapsedTime = currentTime - lastActivityTime
// NSLog("%@: 已过去时间: %@, 是否检测到语音: %@, 是否TTS播放: %@", TAG, String(elapsedTime), String(hasSpeechDetected), String(isTtsSpeaking), String(elapsedTime))
// 如果超过超时时间没有检测到语音,且不在TTS播放中,暂停语音识别
if !hasSpeechDetected && !isTtsSpeaking && elapsedTime >= RECOGNITION_TIMEOUT {
// NSLog("%@: 超过%@秒未检测到语音,停止识别", TAG, String(RECOGNITION_TIMEOUT))
isTimeoutPaused = true
playNotification("没有听到您说话,已暂停对话。双击耳机按钮可重新开始。")
// 停止语音识别并确保资源完全释放
stopVoiceRecognition()
// 清理识别状态
isRecognitionActive = false
hasSpeechDetected = false
}
}
}
/**
* 处理ASR事件
*/
private func handleAsrEvent(eventData: [String: Any]) {
if let type = eventData["type"] as? String {
// NSLog("%@: 处理ASR事件: %@", TAG, type)
switch type {
case "recognizing":
if let text = eventData["text"] as? String, !text.isEmpty {
hasSpeechDetected = true
stopCurrentTTS()
updateLastActivityTime()
} else if let data = eventData["data"] as? [String: Any],
let text = data["text"] as? String,
!text.isEmpty {
hasSpeechDetected = true
stopCurrentTTS()
updateLastActivityTime()
}
case "result":
var text: String?
// 直接读取text或从data中读取
if let directText = eventData["text"] as? String, !directText.isEmpty {
text = directText
} else if let data = eventData["data"] as? [String: Any],
let dataText = data["text"] as? String,
!dataText.isEmpty {
text = dataText
}
if let finalText = text {
updateLastActivityTime()
// 保存用户消息
lastUserMessage = finalText
processWithVolcanoAI(finalText)
}
// 重置状态,继续识别
hasSpeechDetected = false
case "sessionStarted":
updateLastActivityTime()
case "sessionStopped":
isRecognitionActive = false
case "canceled":
isRecognitionActive = false
case "error":
isRecognitionActive = false
if let message = eventData["message"] as? String {
NSLog("%@: ASR错误: %@", TAG, message)
}
playNotification("语音识别出错")
default:
break
}
}
}
/**
* 处理TTS事件
*/
private func handleTtsEvent(eventData: [String: Any]) {
if let type = eventData["type"] as? String {
// NSLog("%@: 处理TTS事件: %@", TAG, type)
switch type {
case "started":
isTtsSpeaking = true
case "completed":
isTtsSpeaking = false
updateLastActivityTime()
case "canceled":
isTtsSpeaking = false
case "error":
isTtsSpeaking = false
if let message = eventData["message"] as? String {
NSLog("%@: TTS错误: %@", TAG, message)
}
default:
break
}
}
}
/**
* 处理双击事件
*/
func handleMediaButtonAction() {
NSLog("%@: 处理媒体按钮事件", TAG)
// 屏蔽短时间重复响应的问题
let currentTime = Date().timeIntervalSince1970
let timeDiff = currentTime - lastKeyEventTime
if timeDiff < 0.5 {
NSLog("%@: 短时间重复响应,忽略", TAG)
return
}
lastKeyEventTime = currentTime
// 停止当前TTS播放
stopCurrentTTS()
// 检查应用是否在后台
if UIApplication.shared.applicationState == .background {
NSLog("%@: 应用在后台,无法开始语音识别", TAG)
playNotification("请打开应用后重试!")
return
}
// 播放提示音
playPrompt("我在!")
// 重置超时暂停标志
isTimeoutPaused = false
// 启动或重置语音识别
if !isRecognitionActive {
NSLog("%@: 语音识别未激活,开始启动", TAG)
startVoiceRecognition()
} else {
NSLog("%@: 语音识别已激活,更新活动时间", TAG)
updateLastActivityTime()
hasSpeechDetected = false
}
}
/**
* 开始语音识别
*/
private func startVoiceRecognition() {
if isRecognitionActive { return }
// 通知Flutter语音识别已启动
notifyVoiceRecognitionStarted()
isActive = true
isRecognitionActive = true
hasSpeechDetected = false
updateLastActivityTime()
if let asrHelper = azureAsrHelper {
let success = asrHelper.startContinuousRecognition()
if !success {
isRecognitionActive = false
NSLog("%@: 启动语音识别失败", TAG)
playNotification("启动语音识别失败")
}
} else {
isRecognitionActive = false
NSLog("%@: Azure ASR服务未初始化", TAG)
playNotification("语音识别服务未初始化")
}
}
/**
* 停止语音识别
*/
private func stopVoiceRecognition() {
if !isRecognitionActive { return }
NSLog("%@: 停止语音识别", TAG)
if let asrHelper = azureAsrHelper {
let success = asrHelper.stopContinuousRecognition()
if !success {
NSLog("%@: 停止语音识别失败", TAG)
}
}
isRecognitionActive = false
hasSpeechDetected = false
}
/**
* 使用火山AI处理语音识别结果
*/
private func processWithVolcanoAI(_ text: String) {
// 保存当前用户输入
currentUserInput = text
// 在后台线程处理
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
guard let self = self, let volcanoAIService = self.volcanoAIService else {
return
}
do {
// 创建消息
let message = volcanoAIService.createUserMessage(content: text)
let messages = [message]
// 发送请求
let response = try volcanoAIService.sendMessage(messages: messages, systemPrompt: self.systemPrompt)
// 保存助手回复
self.lastAssistantMessage = response
// 播放AI回复
self.speakAIResponse(response)
// 同步聊天记录到Flutter端
self.notifyChatHistoryUpdated(userMessage: text, assistantMessage: response)
} catch {
NSLog("%@: AI处理出错: %@", self.TAG, error.localizedDescription)
self.playNotification("AI处理出错")
}
}
}
/**
* 播放AI回复
*/
private func speakAIResponse(_ text: String) {
isTtsSpeaking = true
azureTtsHelper?.speakText(text: text)
}
/**
* 播放提示音
*/
private func playPrompt(_ message: String) {
isTtsSpeaking = true
azureTtsHelper?.speakText(text: message)
}
/**
* 播放通知提示音
*/
private func playNotification(_ message: String) {
isTtsSpeaking = true
azureTtsHelper?.speakText(text: message)
}
/**
* 停止当前TTS播放
*/
private func stopCurrentTTS() {
if isTtsSpeaking {
azureTtsHelper?.stopSpeaking()
isTtsSpeaking = false
}
}
/**
* 更新最后活动时间
*/
private func updateLastActivityTime() {
lastActivityTime = Date().timeIntervalSince1970
}
/**
* 暂停语音交互
*/
func pauseVoiceInteraction() {
NSLog("%@: 暂停语音交互", TAG)
// 停止当前语音播放
stopCurrentTTS()
// 停止语音识别
if isRecognitionActive {
stopVoiceRecognition()
}
// 设置状态为暂停,但服务保持运行
isTimeoutPaused = true
}
/**
* 通知语音识别已开始
*/
private func notifyVoiceRecognitionStarted() {
// 发送识别开始事件
sendEvent(type: "recognition_started", data: [:])
}
/**
* 通知聊天历史更新
*/
private func notifyChatHistoryUpdated(userMessage: String, assistantMessage: String) {
// 发送聊天历史更新事件
sendEvent(type: "chat_history_updated", data: [
"agentId": "personal_assistant",
"userMessage": userMessage,
"assistantMessage": assistantMessage
])
}
/**
* 设置事件处理器
*/
func setEventHandler(_ handler: VoiceInteractionEventHandler) {
self.eventHandler = handler
}
/**
* 发送事件到Flutter端
*/
private func sendEvent(type: String, data: [String: Any] = [:]) {
var eventData = data
eventData["type"] = type
eventData["timestamp"] = Int(Date().timeIntervalSince1970 * 1000)
// 使用事件处理器发送事件
eventHandler?.sendEvent(eventData)
}
}

391
ios/Runner/VolcanoAIService.swift

@ -1,391 +0,0 @@
import Foundation
/**
* 火山AI服务的iOS原生实现
*
* 参考Android端的VolcanoAIService实现,提供同步和异步的API调用方式
*/
class VolcanoAIService {
private let TAG = "VolcanoAIService"
private let baseUrl = "https://ark.cn-beijing.volces.com/api/v3"
private let chatEndpoint = "/chat/completions"
private let session: URLSession
private var apiKey: String = ""
private var isInitialized = false
init() {
let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = 30.0
config.timeoutIntervalForResource = 30.0
self.session = URLSession(configuration: config)
}
/**
* 初始化火山AI服务
*
* @param apiKey 火山AI API密钥
* @return 初始化是否成功
*/
func initialize(apiKey: String) -> Bool {
self.apiKey = apiKey
isInitialized = !apiKey.isEmpty
if !isInitialized {
NSLog("%@: 初始化失败:API key 不能为空", TAG)
} else {
NSLog("%@: 火山AI服务初始化成功", TAG)
}
return isInitialized
}
/**
* 生成个性化问候语
*
* @param agentName 代理名称
* @param systemPrompt 系统提示词
* @param callback 回调函数,返回生成的问候语
*/
func generateGreeting(agentName: String, systemPrompt: String, callback: @escaping (String?, Error?) -> Void) {
let messages: [[String: Any]] = [
["role": "system", "content": systemPrompt],
["role": "user", "content": "请用一句简短的话向我打个招呼,要符合你的身份和性格特点,不要超过18个字。"]
]
let messagesData = try? JSONSerialization.data(withJSONObject: messages, options: [])
let messagesArray = try? JSONSerialization.jsonObject(with: messagesData!, options: []) as? [[String: Any]]
var result = ""
sendMessageStream(messages: messagesArray!, systemPrompt: systemPrompt, streamCallback: StreamCallback(
onToken: { token in
result.append(token)
},
onComplete: {
callback(result, nil)
},
onError: { error in
callback(nil, error)
}
))
}
/**
* 发送消息(非流式输出)
*
* @param messages 消息列表
* @param systemPrompt 系统提示词
* @return 返回AI的回复
* @throws VolcanoAIError 如果API调用失败
*/
func sendMessage(messages: [[String: Any]], systemPrompt: String) throws -> String {
// 检查是否已初始化
if !isInitialized || apiKey.isEmpty {
throw VolcanoAIError.serviceNotInitialized
}
var fullMessages: [[String: Any]] = [
["role": "system", "content": systemPrompt]
]
fullMessages.append(contentsOf: messages)
let requestBody: [String: Any] = [
"model": "doubao-1-5-lite-32k-250115",
"messages": fullMessages,
"temperature": 0.7,
"max_tokens": 2000,
"stream": false
]
guard let url = URL(string: "\(baseUrl)\(chatEndpoint)") else {
throw VolcanoAIError.invalidURL
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
do {
request.httpBody = try JSONSerialization.data(withJSONObject: requestBody, options: [])
} catch {
throw VolcanoAIError.invalidRequestBody
}
let semaphore = DispatchSemaphore(value: 0)
var responseData: Data?
var responseError: Error?
let task = session.dataTask(with: request) { data, response, error in
if let error = error {
responseError = VolcanoAIError.networkError(error.localizedDescription)
semaphore.signal()
return
}
guard let httpResponse = response as? HTTPURLResponse else {
responseError = VolcanoAIError.invalidResponse
semaphore.signal()
return
}
if !(200...299).contains(httpResponse.statusCode) {
var errorMessage = "Unknown error occurred"
if let data = data, let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let error = json["error"] as? [String: Any],
let message = error["message"] as? String {
errorMessage = message
}
responseError = VolcanoAIError.apiError(errorMessage)
semaphore.signal()
return
}
responseData = data
semaphore.signal()
}
task.resume()
_ = semaphore.wait(timeout: .distantFuture)
if let error = responseError {
throw error
}
guard let data = responseData else {
throw VolcanoAIError.emptyResponse
}
do {
guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
let choices = json["choices"] as? [[String: Any]],
let firstChoice = choices.first,
let message = firstChoice["message"] as? [String: Any],
let content = message["content"] as? String else {
throw VolcanoAIError.invalidResponseFormat
}
return content
} catch {
throw VolcanoAIError.invalidResponseFormat
}
}
/**
* 发送消息(流式输出)
*
* @param messages 消息列表
* @param systemPrompt 系统提示词
* @param streamCallback 回调函数,用于接收流式输出的结果
*/
func sendMessageStream(messages: [[String: Any]], systemPrompt: String, streamCallback: StreamCallback) {
// 检查是否已初始化
if !isInitialized || apiKey.isEmpty {
streamCallback.onError(VolcanoAIError.serviceNotInitialized)
return
}
var fullMessages: [[String: Any]] = [
["role": "system", "content": systemPrompt]
]
fullMessages.append(contentsOf: messages)
let requestBody: [String: Any] = [
"model": "doubao-1-5-lite-32k-250115",
"messages": fullMessages,
"temperature": 0.7,
"max_tokens": 2000,
"stream": true
]
guard let url = URL(string: "\(baseUrl)\(chatEndpoint)") else {
streamCallback.onError(VolcanoAIError.invalidURL)
return
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
request.addValue("text/event-stream", forHTTPHeaderField: "Accept")
do {
request.httpBody = try JSONSerialization.data(withJSONObject: requestBody, options: [])
} catch {
streamCallback.onError(VolcanoAIError.invalidRequestBody)
return
}
let task = session.dataTask(with: request) { data, response, error in
if let error = error {
streamCallback.onError(VolcanoAIError.networkError(error.localizedDescription))
return
}
guard let httpResponse = response as? HTTPURLResponse else {
streamCallback.onError(VolcanoAIError.invalidResponse)
return
}
if !(200...299).contains(httpResponse.statusCode) {
var errorMessage = "Unknown error occurred"
if let data = data, let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let error = json["error"] as? [String: Any],
let message = error["message"] as? String {
errorMessage = message
}
streamCallback.onError(VolcanoAIError.apiError(errorMessage))
return
}
guard let data = data else {
streamCallback.onError(VolcanoAIError.emptyResponse)
return
}
// 处理SSE流数据
let responseString = String(data: data, encoding: .utf8) ?? ""
let lines = responseString.components(separatedBy: "\n")
for line in lines {
if line.isEmpty { continue }
if line.hasPrefix("data: ") {
let data = String(line.dropFirst(6))
if data == "[DONE]" {
streamCallback.onComplete()
break
}
do {
if let jsonData = data.data(using: .utf8),
let json = try JSONSerialization.jsonObject(with: jsonData) as? [String: Any],
let choices = json["choices"] as? [[String: Any]],
let firstChoice = choices.first,
let delta = firstChoice["delta"] as? [String: Any],
let content = delta["content"] as? String {
streamCallback.onToken(content)
}
} catch {
// 忽略无效的JSON数据
continue
}
}
}
}
task.resume()
}
/**
* 同步方式发送消息(流式输出)
*
* 注意:此方法会阻塞当前线程,请在后台线程中调用
*
* @param messages 消息列表
* @param systemPrompt 系统提示词
* @return 返回完整的AI回复
* @throws VolcanoAIError 如果API调用失败
*/
func sendMessageStreamSync(messages: [[String: Any]], systemPrompt: String) throws -> String {
var result = ""
let semaphore = DispatchSemaphore(value: 0)
var responseError: Error?
sendMessageStream(messages: messages, systemPrompt: systemPrompt, streamCallback: StreamCallback(
onToken: { token in
result.append(token)
},
onComplete: {
semaphore.signal()
},
onError: { error in
responseError = error
semaphore.signal()
}
))
// 等待流式输出完成或出错
_ = semaphore.wait(timeout: .now() + 60)
if let error = responseError {
throw error
}
return result
}
/**
* 创建用户消息
*/
func createUserMessage(content: String) -> [String: Any] {
return ["role": "user", "content": content]
}
/**
* 创建系统消息
*/
func createSystemMessage(content: String) -> [String: Any] {
return ["role": "system", "content": content]
}
/**
* 创建助手消息
*/
func createAssistantMessage(content: String) -> [String: Any] {
return ["role": "assistant", "content": content]
}
/**
* 流式输出回调类
*/
class StreamCallback {
let onToken: (String) -> Void
let onComplete: () -> Void
let onError: (Error) -> Void
init(onToken: @escaping (String) -> Void, onComplete: @escaping () -> Void, onError: @escaping (Error) -> Void) {
self.onToken = onToken
self.onComplete = onComplete
self.onError = onError
}
}
}
/**
* 火山AI错误枚举
*/
enum VolcanoAIError: Error {
case serviceNotInitialized
case invalidURL
case invalidRequestBody
case networkError(String)
case invalidResponse
case emptyResponse
case invalidResponseFormat
case apiError(String)
var localizedDescription: String {
switch self {
case .serviceNotInitialized:
return "火山AI服务未初始化或API key为空,请先调用initialize方法"
case .invalidURL:
return "无效的URL"
case .invalidRequestBody:
return "无效的请求体"
case .networkError(let message):
return "网络错误: \(message)"
case .invalidResponse:
return "无效的响应"
case .emptyResponse:
return "空响应"
case .invalidResponseFormat:
return "无效的响应格式"
case .apiError(let message):
return "API错误: \(message)"
}
}
}

BIN
ios/Runner/sample.mp3

Binary file not shown.

BIN
ios/Runner/silence.mp3

Binary file not shown.

4
lib/core/themes/theme_service.dart

@ -21,7 +21,7 @@ class ThemeService {
Logger.info('从存储读取主题设置: $value');
return value ?? false;
} catch (e) {
Logger.error('读取主题设置失败: $e');
Logger.error('读取主题设置失败: ${e.toString()}');
return false;
}
}
@ -32,7 +32,7 @@ class ThemeService {
Logger.info('保存主题设置: ${isDarkMode ? "暗黑" : "浅色"}');
await _box.write(_key, isDarkMode);
} catch (e) {
Logger.error('保存主题设置失败: $e');
Logger.error('保存主题设置失败: ${e.toString()}');
}
}
}

85
lib/core/utils/logger.dart

@ -15,77 +15,84 @@ class Logger {
/// 当前日志级别,低于此级别的日志不会被输出
static LogLevel _currentLevel = LogLevel.debug;
/// 默认标签
static const String _defaultTag = 'App';
/// 设置日志级别
static void setLevel(LogLevel level) {
_currentLevel = level;
}
/// 输出调试日志 (不带标签版本)
static void debug(String message) {
if (_currentLevel.index <= LogLevel.debug.index) {
_log('DEBUG', _defaultTag, message);
}
}
/// 输出调试日志
static void debug(String message, {String? tag}) {
static void d(String tag, String message) {
if (_currentLevel.index <= LogLevel.debug.index) {
_log('DEBUG', message, tag: tag);
_log('DEBUG', tag, message);
}
}
/// 输出信息日志 (不带标签版本)
static void info(String message) {
if (_currentLevel.index <= LogLevel.info.index) {
_log('INFO', _defaultTag, message);
}
}
/// 输出信息日志
static void info(String message, {String? tag}) {
static void i(String tag, String message) {
if (_currentLevel.index <= LogLevel.info.index) {
_log('INFO', message, tag: tag);
_log('INFO', tag, message);
}
}
/// 输出警告日志 (不带标签版本)
static void warning(String message) {
if (_currentLevel.index <= LogLevel.warning.index) {
_log('WARNING', _defaultTag, message);
}
}
/// 输出警告日志
static void warning(String message, {String? tag}) {
static void w(String tag, String message) {
if (_currentLevel.index <= LogLevel.warning.index) {
_log('WARNING', message, tag: tag);
_log('WARNING', tag, message);
}
}
/// 输出错误日志
static void error(String message, [dynamic error, StackTrace? stackTrace, String? tag]) {
/// 输出错误日志 (不带标签版本)
static void error(String message, [dynamic error, StackTrace? stackTrace]) {
if (_currentLevel.index <= LogLevel.error.index) {
_log('ERROR', message, tag: tag);
_log('ERROR', _defaultTag, message);
if (error != null) {
_log('ERROR', 'Original error: $error', tag: tag);
_log('ERROR', _defaultTag, 'Original error: $error');
}
if (stackTrace != null) {
_log('ERROR', 'Stack trace: $stackTrace', tag: tag);
_log('ERROR', _defaultTag, 'Stack trace: $stackTrace');
}
}
}
/// 获取当前调用栈的类名
static String? _getCallerClassName() {
try {
final stackTrace = StackTrace.current.toString();
final frames = stackTrace.split('\n');
// 查找调用者帧 (跳过Logger框架内部的帧)
for (int i = 0; i < frames.length; i++) {
final frame = frames[i].trim();
if (frame.contains('Logger.') || frame.contains('_log')) {
continue;
}
// 简单的解析类名
final regex = RegExp(r'#\d+\s+([^\.]+)\.([^\.]+)\s');
final match = regex.firstMatch(frame);
if (match != null && match.groupCount >= 1) {
return match.group(1);
}
break;
/// 输出错误日志
static void e(String tag, String message, [dynamic error, StackTrace? stackTrace]) {
if (_currentLevel.index <= LogLevel.error.index) {
_log('ERROR', tag, message);
if (error != null) {
_log('ERROR', tag, 'Original error: $error');
}
if (stackTrace != null) {
_log('ERROR', tag, 'Stack trace: $stackTrace');
}
} catch (e) {
// 忽略解析错误
}
return null;
}
/// 内部日志输出方法
static void _log(String level, String message, {String? tag}) {
final timestamp = DateTime.now().toString();
final className = tag ?? _getCallerClassName() ?? '';
final tagInfo = className.isNotEmpty ? '[$className] ' : '';
developer.log('[$timestamp][$level][$tagInfo]: $message');
static void _log(String level, String tag, String message) {
developer.log('[$tag]: $message');
}
}

49
lib/data/services/ble_manager.dart

@ -54,7 +54,7 @@ class DeviceInfo {
this.rightEarCharging = false,
this.caseCharging = false,
this.firmwareVersion = '',
this.firmwareProductId = 's3',
this.firmwareProductId = '',
this.firmwareProductColor = '',
DateTime? lastUpdated,
}) : lastUpdated = lastUpdated ?? DateTime.now();
@ -168,7 +168,7 @@ class BleManager extends GetxService {
Future.delayed(const Duration(milliseconds: 300), () {
// 可选:检查是否仍处于连接状态
if (_isConnected.value) {
fetchDeviceInfo();
// fetchDeviceInfo();
}
});
} else if (state == BleConnectionState.disconnected ||
@ -198,7 +198,7 @@ class BleManager extends GetxService {
},
);
} catch (e) {
Logger.error('设置BLE事件监听器失败: $e');
Logger.error('设置BLE事件监听器失败: ${e.toString()}');
}
}
@ -225,7 +225,7 @@ class BleManager extends GetxService {
}
}
} catch (e) {
Logger.error('处理设备信息异常: $e');
Logger.error('处理设备信息异常: ${e.toString()}');
}
}
@ -303,10 +303,9 @@ class BleManager extends GetxService {
caseCharging: caseCharging,
);
Logger.info(
'设备电池信息已更新: 电量=$overallBattery%, 左耳=$leftBattery%, 右耳=$rightBattery%, 充电仓=$caseBattery%');
Logger.info('设备电池信息已更新: 电量=$overallBattery%, 左耳=$leftBattery%, 右耳=$rightBattery%, 充电仓=$caseBattery%');
} catch (e) {
Logger.error('更新电池信息失败: $e');
Logger.error('更新电池信息失败: ${e.toString()}');
}
}
@ -329,7 +328,7 @@ class BleManager extends GetxService {
Logger.info('设备版本信息已更新: 版本=$version');
} catch (e) {
Logger.error('更新版本信息失败: $e');
Logger.error('更新版本信息失败: ${e.toString()}');
}
}
@ -352,7 +351,7 @@ class BleManager extends GetxService {
Logger.info('设备设备id已更新: 设备id=$productId');
} catch (e) {
Logger.error('更新设备id失败: $e');
Logger.error('更新设备id失败: ${e.toString()}');
}
}
@ -440,7 +439,7 @@ class BleManager extends GetxService {
Logger.warning('产品id获取指令发送失败');
}
} catch (e) {
Logger.error('获取BLE设备信息失败: $e');
Logger.error('获取BLE设备信息失败: ${e.toString()}');
}
}
@ -455,7 +454,7 @@ class BleManager extends GetxService {
return _connectionState.value;
} catch (e) {
Logger.error('获取BLE连接状态失败: $e');
Logger.error('获取BLE连接状态失败: ${e.toString()}');
return BleConnectionState.error;
}
}
@ -497,6 +496,8 @@ class BleManager extends GetxService {
'openaiModel': openaiModel,
'systemPrompt': systemPrompt,
'mcpServer': jsonEncode(jsonData),
'volcanoAppId': '2316081933',
'volcanoToken': 'mcJTh6OelBqU_L78lgGd77Nyrc_-5-En',
};
// 记录日志(不记录敏感信息)
@ -531,7 +532,7 @@ class BleManager extends GetxService {
// 如果设备信息存在,则获取详细信息
if (deviceInfo.value != null) {
// 获取详细设备信息
fetchDeviceInfo();
// fetchDeviceInfo();
}
}
} else {
@ -540,7 +541,7 @@ class BleManager extends GetxService {
return result;
} catch (e) {
Logger.error('启动BLE服务失败: $e');
Logger.error('启动BLE服务失败: ${e.toString()}');
return false;
}
}
@ -560,7 +561,7 @@ class BleManager extends GetxService {
return result;
} catch (e) {
Logger.error('停止BLE服务失败: $e');
Logger.error('停止BLE服务失败: ${e.toString()}');
return false;
}
}
@ -570,7 +571,7 @@ class BleManager extends GetxService {
try {
return await _bleService.getBatteryLevel();
} catch (e) {
Logger.error('获取电池电量失败: $e');
Logger.error('获取电池电量失败: ${e.toString()}');
return 0;
}
}
@ -580,7 +581,7 @@ class BleManager extends GetxService {
try {
return await _bleService.getBatteryInfo();
} catch (e) {
Logger.error('获取电池信息失败: $e');
Logger.error('获取电池信息失败: ${e.toString()}');
return false;
}
}
@ -590,7 +591,7 @@ class BleManager extends GetxService {
try {
return await _bleService.getProductId();
} catch (e) {
Logger.error('获取设备版本失败: $e');
Logger.error('获取设备版本失败: ${e.toString()}');
return false;
}
}
@ -600,7 +601,7 @@ class BleManager extends GetxService {
try {
return await _bleService.getDeviceVersion();
} catch (e) {
Logger.error('获取设备版本失败: $e');
Logger.error('获取设备版本失败: ${e.toString()}');
return false;
}
}
@ -610,7 +611,7 @@ class BleManager extends GetxService {
try {
return await _bleService.openEncoder();
} catch (e) {
Logger.error('打开编码器失败: $e');
Logger.error('打开编码器失败: ${e.toString()}');
return false;
}
}
@ -620,7 +621,7 @@ class BleManager extends GetxService {
try {
return await _bleService.openDecoder();
} catch (e) {
Logger.error('打开解码器失败: $e');
Logger.error('打开解码器失败: ${e.toString()}');
return false;
}
}
@ -630,7 +631,7 @@ class BleManager extends GetxService {
try {
return await _bleService.openA2DPDecoder();
} catch (e) {
Logger.error('打开解码器失败: $e');
Logger.error('打开解码器失败: ${e.toString()}');
return false;
}
}
@ -640,7 +641,7 @@ class BleManager extends GetxService {
try {
return await _bleService.closeCodec();
} catch (e) {
Logger.error('关闭编解码器失败: $e');
Logger.error('关闭编解码器失败: ${e.toString()}');
return false;
}
}
@ -650,7 +651,7 @@ class BleManager extends GetxService {
try {
return await _bleService.playA2DP();
} catch (e) {
Logger.error('播放A2DP失败: $e');
Logger.error('播放A2DP失败: ${e.toString()}');
return false;
}
}
@ -662,4 +663,4 @@ class BleManager extends GetxService {
}
return Future.value(Get.find<BleManager>());
}
}
}

8
lib/data/services/bluetooth_manager.dart

@ -75,11 +75,11 @@ class BluetoothManager extends GetxService {
}
},
onError: (error) {
Logger.error('蓝牙事件流错误: $error');
Logger.error('蓝牙事件流错误: ${error.toString()}');
},
);
} catch (e) {
Logger.error('设置蓝牙事件监听器失败: $e');
Logger.error('设置蓝牙事件监听器失败: ${e.toString()}');
}
}
@ -106,7 +106,7 @@ class BluetoothManager extends GetxService {
Logger.info('未找到已连接的蓝牙设备');
} catch (e) {
Logger.error('刷新设备连接失败: $e');
Logger.error('刷新设备连接失败: ${e.toString()}');
}
}
@ -117,7 +117,7 @@ class BluetoothManager extends GetxService {
_bluetoothState.value = isEnabled ? 'on' : 'off';
return isEnabled;
} catch (e) {
Logger.error('检查蓝牙状态失败: $e');
Logger.error('检查蓝牙状态失败: ${e.toString()}');
return false;
}
}

2
lib/data/services/chat_history_service.dart

@ -39,7 +39,7 @@ class ChatHistoryService extends GetxService {
// 检查并清理过多的历史记录
await _cleanupOldMessages(agentId);
} catch (e) {
Logger.error('保存聊天记录失败', e);
Logger.error('保存聊天记录失败', e.toString());
}
}

6
lib/data/services/deapsound_ai_service.dart

@ -112,14 +112,14 @@ class DeapsoundAIService implements AiService {
}
}
} catch (e) {
Logger.error('JSON解析错误: $e');
Logger.error('JSON解析错误: ${e.toString()}');
}
}
}
} catch (e) {
Logger.error('请求异常: $e');
Logger.error('请求异常: ${e.toString()}');
} finally {
client.close();
}
}
}
}

6
lib/data/services/kouzi_ai_service.dart

@ -82,14 +82,14 @@ class KouziAIService implements AiService {
}
}
} catch (e) {
Logger.error('JSON解析错误: $e');
Logger.error('JSON解析错误: ${e.toString()}');
}
}
}
} catch (e) {
Logger.error('请求异常: $e');
Logger.error('请求异常: ${e.toString()}');
} finally {
client.close();
}
}
}
}

4
lib/data/services/login_service.dart

@ -8,6 +8,7 @@ class LoginService {
FlWX().register(
appSecret: '4cfde33bc22b6cc6b1f8b225dd97d255',
appId: 'wx5bbb7be9324f3e2d',
universalLink: 'https://yunqiinnovation.com/app/',
params: FlWXCallbackParams(onHttp: (url) async {
return '';
}));
@ -18,13 +19,12 @@ class LoginService {
Future<void> _initializeFirebase() async {
try {
// 尝试获取已存在的实例
Firebase.app('DeepSound');
Firebase.app();
print('已存在的 Firebase 实例被重用');
} catch (e) {
try {
// 如果不存在则初始化新实例
await Firebase.initializeApp(
name: 'DeepSound',
options: DefaultFirebaseOptions.currentPlatform,
);
print('Firebase 初始化成功');

29
lib/data/services/navigation_manager.dart

@ -0,0 +1,29 @@
// import 'package:amap_service/amap_service.dart';
import 'package:deep_voice/core/utils/logger.dart';
import 'package:get/get.dart';
//音乐播放管理
class NavigationManager extends GetxService {
static NavigationManager get to => Get.find<NavigationManager>();
// 音乐服务实例
// final AmapService _amapervice = AmapService();
/// 初始化
@override
void onInit() {
super.onInit();
Logger.info('导航服务初始化');
}
/// 释放资源
@override
void onClose() {
super.onClose();
}
//导航
Future<void> navigation(String org, String dis) async {
// await _amapervice.navigation(org, dis);
}
}

10
lib/data/services/network/api.dart

@ -38,12 +38,20 @@ class Api {
}
//更新用户设置
static updateUserSetting([params]) {
static updateUserSetting([dynamic params]) {
return DioManager().request(
NWMethod.post,
'/api/home/user_setting',
params: params,
);
// options: params is dio.FormData
// ? Options(
// headers: {
// 'Content-Type': 'multipart/form-data',
// },
// )
// : null,
// );
}
//注销账号

2
lib/data/services/network/dio_manager.dart

@ -31,7 +31,7 @@ class DioManager {
Future request<T>(
NWMethod method,
String path, {
Map? params,
Object? params,
Map<String, dynamic>? queryParameters,
}) async {
Response response = await dio.request(

4
lib/data/services/speech_factory.dart

@ -88,7 +88,7 @@ class SpeechFactory extends GetxService {
Logger.info(
'语音服务初始化完成,当前类型: ${_currentServiceType.toString().split('.').last}');
} catch (e) {
Logger.error('语音服务初始化失败: $e');
Logger.error('语音服务初始化失败: ${e.toString()}');
rethrow;
}
}
@ -108,4 +108,4 @@ class SpeechFactory extends GetxService {
}
return Get.find<TtsService>();
}
}
}

21
lib/data/services/speech_impl/azure_asr_service.dart

@ -106,7 +106,7 @@ class AzureAsrService extends GetxService implements AsrService {
Logger.info('Azure 语音识别服务初始化${result ? '成功' : '失败'}');
return result;
} catch (e) {
Logger.error('Azure 语音识别服务初始化失败: $e');
Logger.error('Azure 语音识别服务初始化失败: ${e.toString()}');
_isInitialized = false;
rethrow;
}
@ -144,7 +144,7 @@ class AzureAsrService extends GetxService implements AsrService {
throw Exception('无效的识别结果格式');
} catch (e) {
Logger.error('语音识别失败: $e');
Logger.error('语音识别失败: ${e.toString()}');
return RecognitionEvent.error(e.toString());
}
}
@ -177,7 +177,7 @@ class AzureAsrService extends GetxService implements AsrService {
_isContinuousRecognitionActive = true;
return _eventStreamController!.stream;
} catch (e) {
Logger.error('开始连续语音识别失败: $e');
Logger.error('开始连续语音识别失败: ${e.toString()}');
_cleanupEventStream();
rethrow;
}
@ -195,7 +195,7 @@ class AzureAsrService extends GetxService implements AsrService {
_cleanupEventStream();
return result;
} catch (e) {
Logger.error('停止连续语音识别失败: $e');
Logger.error('停止连续语音识别失败: ${e.toString()}');
_cleanupEventStream();
rethrow;
}
@ -214,7 +214,6 @@ class AzureAsrService extends GetxService implements AsrService {
final String eventType = eventMap['type'] as String? ?? '';
// 添加日志帮助调试
Logger.debug('收到语音识别事件: $eventType, 数据: $eventMap');
switch (eventType) {
case 'result':
@ -260,7 +259,7 @@ class AzureAsrService extends GetxService implements AsrService {
final String errorDetails = eventMap['errorDetails'] as String? ?? '';
if (reason.isNotEmpty || errorDetails.isNotEmpty) {
Logger.error('识别取消: $reason - $errorDetails');
Logger.error('识别取消: $reason - ${errorDetails.toString()}');
}
_eventStreamController?.add(RecognitionEvent(
@ -271,7 +270,7 @@ class AzureAsrService extends GetxService implements AsrService {
case 'error':
final String error = eventMap['message'] as String? ?? '';
Logger.error('识别错误: $error');
Logger.error('识别错误: ${error.toString()}');
_eventStreamController?.add(RecognitionEvent(
type: RecognitionEventType.error,
error: error,
@ -282,7 +281,7 @@ class AzureAsrService extends GetxService implements AsrService {
/// 处理识别事件流错误
void _handleRecognitionError(Object error) {
Logger.error('识别事件流错误: $error');
Logger.error('识别事件流错误: ${error.toString()}');
_eventStreamController?.addError(error);
_cleanupEventStream();
}
@ -311,7 +310,7 @@ class AzureAsrService extends GetxService implements AsrService {
Logger.info('Azure 语音识别资源已释放');
} catch (e) {
Logger.error('释放语音识别资源失败: $e');
Logger.error('释放语音识别资源失败: ${e.toString()}');
_cleanupEventStream();
_isInitialized = false;
}
@ -339,8 +338,8 @@ class AzureAsrService extends GetxService implements AsrService {
return true;
} catch (e) {
Logger.error('推送音频数据失败: $e');
Logger.error('推送音频数据失败: ${e.toString()}');
return false;
}
}
}
}

22
lib/data/services/speech_impl/azure_tts_service.dart

@ -69,7 +69,7 @@ class AzureTtsService extends GetxService implements TtsService {
_handleTtsEvent(event);
}
}, onError: (error) {
Logger.error('TTS事件流错误: $error');
Logger.error('TTS事件流错误: ${error.toString()}');
_eventStreamController.addError(error);
});
}
@ -78,7 +78,7 @@ class AzureTtsService extends GetxService implements TtsService {
void _handleTtsEvent(Map<dynamic, dynamic> event) {
final String eventType = event['type'] as String? ?? '';
// Logger.debug('收到TTS事件: $eventType, 数据: $event');
// Logger.debug('收到TTS事件: ${e.toString()}ventType, 数据: ${e.toString()}vent');
switch (eventType) {
case 'synthesis_started':
@ -159,7 +159,7 @@ class AzureTtsService extends GetxService implements TtsService {
_isInitialized = result;
return result;
} catch (e) {
Logger.error('初始化失败: $e');
Logger.error('初始化失败: ${e.toString()}');
return false;
}
}
@ -177,7 +177,7 @@ class AzureTtsService extends GetxService implements TtsService {
if (result) _currentVoice = voiceName;
return result;
} catch (e) {
Logger.error('设置语音失败: $e');
Logger.error('设置语音失败: ${e.toString()}');
return false;
}
}
@ -195,7 +195,7 @@ class AzureTtsService extends GetxService implements TtsService {
return result == true;
} catch (e) {
Logger.error('语音合成失败: $e');
Logger.error('语音合成失败: ${e.toString()}');
return false;
}
}
@ -250,7 +250,7 @@ class AzureTtsService extends GetxService implements TtsService {
return true;
} catch (e) {
Logger.error('处理流式文本失败: $e');
Logger.error('处理流式文本失败: ${e.toString()}');
return false;
}
}
@ -316,7 +316,7 @@ class AzureTtsService extends GetxService implements TtsService {
Logger.info('TTS队列播放完成');
return result;
} catch (e) {
Logger.error('刷新TTS流失败: $e');
Logger.error('刷新TTS流失败: ${e.toString()}');
return false;
}
}
@ -358,7 +358,7 @@ class AzureTtsService extends GetxService implements TtsService {
_textQueue.removeAt(0);
}
} catch (e) {
Logger.error('处理语音队列出错: $e');
Logger.error('处理语音队列出错: ${e.toString()}');
// 出错时移除当前项并尝试继续
if (_textQueue.isNotEmpty) {
@ -389,7 +389,7 @@ class AzureTtsService extends GetxService implements TtsService {
return true;
} catch (e) {
Logger.error('停止语音合成出错: $e');
Logger.error('停止语音合成出错: ${e.toString()}');
return false;
}
}
@ -410,7 +410,7 @@ class AzureTtsService extends GetxService implements TtsService {
await _channel.invokeMethod('dispose');
_isInitialized = false;
} catch (e) {
Logger.error('释放资源失败: $e');
Logger.error('释放资源失败: ${e.toString()}');
}
}
}
}

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

@ -107,7 +107,7 @@ class FlutterAsrService extends GetxService implements AsrService {
} catch (e) {
_isAvailable.value = false;
_asrState.value = AsrState.error;
Logger.error('语音识别初始化失败: $e');
Logger.error('语音识别初始化失败: ${e.toString()}');
}
}
@ -203,7 +203,7 @@ class FlutterAsrService extends GetxService implements AsrService {
return result;
} catch (e) {
Logger.error('启动语音识别失败: $e');
Logger.error('启动语音识别失败: ${e.toString()}');
_asrState.value = AsrState.error;
return false;
}
@ -253,7 +253,7 @@ class FlutterAsrService extends GetxService implements AsrService {
?.add(RecognitionEvent.error('重启语音识别失败,可能存在系统资源冲突'));
}
} catch (e) {
Logger.error('重启连续识别失败: $e');
Logger.error('重启连续识别失败: ${e.toString()}');
// 发送错误给控制器
_continuousRecognitionController?.add(RecognitionEvent.error('重启失败: $e'));
@ -304,7 +304,7 @@ class FlutterAsrService extends GetxService implements AsrService {
await _initSpeech();
return _isAvailable.value;
} catch (e) {
Logger.error('语音识别初始化失败: $e');
Logger.error('语音识别初始化失败: ${e.toString()}');
return false;
}
}
@ -357,7 +357,7 @@ class FlutterAsrService extends GetxService implements AsrService {
},
);
} catch (e) {
Logger.error('一次性语音识别失败: $e');
Logger.error('一次性语音识别失败: ${e.toString()}');
return RecognitionEvent.error('$e');
}
}
@ -406,7 +406,7 @@ class FlutterAsrService extends GetxService implements AsrService {
_isListening = true;
return _continuousRecognitionController!.stream;
} catch (e) {
Logger.error('启动连续语音识别失败: $e');
Logger.error('启动连续语音识别失败: ${e.toString()}');
// 如果已经创建了控制器,使用它
if (_continuousRecognitionController != null) {
@ -442,7 +442,7 @@ class FlutterAsrService extends GetxService implements AsrService {
_asrState.value = AsrState.done;
return true;
} catch (e) {
Logger.error('停止连续语音识别失败: $e');
Logger.error('停止连续语音识别失败: ${e.toString()}');
// 强制重置状态
_isListening = false;
@ -472,7 +472,7 @@ class FlutterAsrService extends GetxService implements AsrService {
_isListening = false;
_asrState.value = AsrState.notInitialized;
} catch (e) {
Logger.error('释放语音识别资源失败: $e');
Logger.error('释放语音识别资源失败: ${e.toString()}');
}
}
}
}

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

@ -96,7 +96,7 @@ class FlutterTtsService extends GetxService implements TtsService {
Logger.info('FlutterTtsService 已初始化');
} catch (e) {
Logger.error('TTS初始化失败: $e');
Logger.error('TTS初始化失败: ${e.toString()}');
}
}
@ -118,7 +118,7 @@ class FlutterTtsService extends GetxService implements TtsService {
Logger.info('支持的语音: ${_supportedVoices.join(", ")}');
return true;
} catch (e) {
Logger.error('TTS初始化失败: $e');
Logger.error('TTS初始化失败: ${e.toString()}');
return false;
}
}
@ -149,7 +149,7 @@ class FlutterTtsService extends GetxService implements TtsService {
_currentVoice.value = _supportedVoices.first;
}
} catch (e) {
Logger.error('加载TTS语音失败: $e');
Logger.error('加载TTS语音失败: ${e.toString()}');
// 添加默认语音
if (_supportedVoices.isEmpty) {
@ -175,7 +175,7 @@ class FlutterTtsService extends GetxService implements TtsService {
Logger.info('设置TTS语音: $voiceName');
return true;
} catch (e) {
Logger.error('设置TTS语音失败: $e');
Logger.error('设置TTS语音失败: ${e.toString()}');
return false;
}
}
@ -200,7 +200,7 @@ class FlutterTtsService extends GetxService implements TtsService {
return true;
} catch (e) {
Logger.error('设置语音参数失败: $e');
Logger.error('设置语音参数失败: ${e.toString()}');
// 发送错误事件
_eventController.add(TtsEvent.error('设置语音参数失败: $e'));
@ -227,7 +227,7 @@ class FlutterTtsService extends GetxService implements TtsService {
// 发送错误事件
_eventController.add(TtsEvent.error(e.toString()));
Logger.error('TTS播放失败: $e');
Logger.error('TTS播放失败: ${e.toString()}');
return false;
}
}
@ -288,7 +288,7 @@ class FlutterTtsService extends GetxService implements TtsService {
return true;
} catch (e) {
Logger.error('处理流式文本失败: $e');
Logger.error('处理流式文本失败: ${e.toString()}');
// 发送错误事件
_eventController.add(TtsEvent.error(e.toString()));
@ -333,7 +333,7 @@ class FlutterTtsService extends GetxService implements TtsService {
Logger.info('TTS队列播放完成');
return true;
} catch (e) {
Logger.error('等待TTS队列播放完成失败: $e');
Logger.error('等待TTS队列播放完成失败: ${e.toString()}');
// 发送错误事件
_eventController.add(TtsEvent.error(e.toString()));
@ -382,7 +382,7 @@ class FlutterTtsService extends GetxService implements TtsService {
}
}
} catch (e) {
Logger.error('TTS处理队列失败: $e');
Logger.error('TTS处理队列失败: ${e.toString()}');
// 发送错误事件
_eventController.add(TtsEvent.error(e.toString()));
@ -422,7 +422,7 @@ class FlutterTtsService extends GetxService implements TtsService {
Logger.info('TTS停止播放完成');
return true;
} catch (e) {
Logger.error('TTS停止播放失败: $e');
Logger.error('TTS停止播放失败: ${e.toString()}');
// 即使出错也尝试强制重置状态
_isSpeaking.value = false;
@ -443,7 +443,7 @@ class FlutterTtsService extends GetxService implements TtsService {
Logger.info('TTS资源已释放');
} catch (e) {
Logger.error('释放TTS资源失败: $e');
Logger.error('释放TTS资源失败: ${e.toString()}');
}
}
@ -462,7 +462,7 @@ class FlutterTtsService extends GetxService implements TtsService {
await flushStream();
return true;
} catch (e) {
Logger.error('处理文本流时出错: $e');
Logger.error('处理文本流时出错: ${e.toString()}');
return false;
}
}

22
lib/data/services/speech_impl/voice_clone_tts_service.dart

@ -76,7 +76,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService {
);
return true;
} catch (e) {
Logger.error('TTS初始化失败: $e');
Logger.error('TTS初始化失败: ${e.toString()}');
return false;
}
}
@ -88,7 +88,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService {
Logger.info('设置TTS语音: $voiceName');
return true;
} catch (e) {
Logger.error('设置TTS语音失败: $e');
Logger.error('设置TTS语音失败: ${e.toString()}');
return false;
}
}
@ -127,7 +127,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService {
_handle(message);
},
onError: (error) {
Logger.error('连接失败: $error');
Logger.error('连接失败: ${error.toString()}');
},
onDone: () {
Logger.error('WebSocket 连接已关闭!');
@ -135,7 +135,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService {
cancelOnError: true,
);
} catch (e) {
Logger.error('初始化异常: $e');
Logger.error('初始化异常: ${e.toString()}');
}
}
@ -226,7 +226,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService {
return true;
} catch (e) {
_isSpeaking.value = false;
Logger.error('语音合成失败: $e');
Logger.error('语音合成失败: ${e.toString()}');
// 发送错误事件
_eventController.add(TtsEvent.error('语音合成失败: $e'));
@ -285,7 +285,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService {
return true;
} catch (e) {
Logger.error('处理流式文本失败: $e');
Logger.error('处理流式文本失败: ${e.toString()}');
// 发送错误事件
_eventController.add(TtsEvent.error('处理流式文本失败: $e'));
@ -319,7 +319,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService {
return true;
} catch (e) {
Logger.error('刷新TTS流失败: $e');
Logger.error('刷新TTS流失败: ${e.toString()}');
// 发送错误事件
_eventController.add(TtsEvent.error('刷新TTS流失败: $e'));
@ -347,7 +347,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService {
await speakOnce(item);
}
} catch (e) {
Logger.error('处理语音队列出错: $e');
Logger.error('处理语音队列出错: ${e.toString()}');
// 发送错误事件
_eventController.add(TtsEvent.error('处理语音队列出错: $e'));
@ -370,7 +370,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService {
}
return true;
} catch (e) {
Logger.error('停止语音合成出错: $e');
Logger.error('停止语音合成出错: ${e.toString()}');
// 发送错误事件
_eventController.add(TtsEvent.error('停止语音合成出错: $e'));
return false;
@ -387,7 +387,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService {
await _eventController.close();
_isInitialized = false;
} catch (e) {
Logger.error('释放资源失败: $e');
Logger.error('释放资源失败: ${e.toString()}');
}
}
}
}

38
lib/data/services/speech_impl/volcano_asr_api_service.dart

@ -99,7 +99,7 @@ class VolcanoAsrApiService implements AsrService {
Logger.info('火山语音API已初始化');
return true;
} catch (e) {
Logger.error('初始化火山语音API失败: $e');
Logger.error('初始化火山语音API失败: ${e.toString()}');
return false;
}
}
@ -151,7 +151,7 @@ class VolcanoAsrApiService implements AsrService {
return resultCompleter.future;
} catch (e) {
Logger.error('单次语音识别失败: $e');
Logger.error('单次语音识别失败: ${e.toString()}');
return RecognitionEvent.error(e.toString());
}
}
@ -184,7 +184,7 @@ class VolcanoAsrApiService implements AsrService {
return _recognitionController!.stream;
} catch (e) {
Logger.error('开始连续语音识别失败: $e');
Logger.error('开始连续语音识别失败: ${e.toString()}');
await _cleanupResources();
// 返回一个包含错误的流
@ -222,7 +222,7 @@ class VolcanoAsrApiService implements AsrService {
return true;
} catch (e) {
Logger.error('停止连续语音识别失败: $e');
Logger.error('停止连续语音识别失败: ${e.toString()}');
// 确保资源被清理
await _cleanupResources();
@ -250,7 +250,7 @@ class VolcanoAsrApiService implements AsrService {
_isInitialized = false;
Logger.info('火山语音API资源已释放');
} catch (e) {
Logger.error('释放火山语音API资源失败: $e');
Logger.error('释放火山语音API资源失败: ${e.toString()}');
_isContinuousRecognitionActive = false;
_isInitialized = false;
}
@ -284,7 +284,7 @@ class VolcanoAsrApiService implements AsrService {
Logger.info('录音机初始化成功');
} catch (e) {
// _recorder = null;
Logger.error('初始化录音机失败: $e');
Logger.error('初始化录音机失败: ${e.toString()}');
rethrow;
}
}
@ -344,7 +344,7 @@ class VolcanoAsrApiService implements AsrService {
_connectionCompleter!.completeError(e);
}
Logger.error('连接WebSocket失败: $e');
Logger.error('连接WebSocket失败: ${e.toString()}');
rethrow;
}
@ -432,7 +432,7 @@ class VolcanoAsrApiService implements AsrService {
_channel!.sink.add(jsonEncode(heartbeatMsg));
} catch (e) {
Logger.error('发送心跳包失败: $e');
Logger.error('发送心跳包失败: ${e.toString()}');
}
}
}
@ -461,7 +461,7 @@ class VolcanoAsrApiService implements AsrService {
_channel!.sink.add(jsonEncode(startCommand));
Logger.info('已发送开始识别指令');
} catch (e) {
Logger.error('发送开始识别指令失败: $e');
Logger.error('发送开始识别指令失败: ${e.toString()}');
rethrow;
}
}
@ -496,7 +496,7 @@ class VolcanoAsrApiService implements AsrService {
Logger.info('录音已开始');
} catch (e) {
_isRecording = false;
Logger.error('开始录音失败: $e');
Logger.error('开始录音失败: ${e.toString()}');
rethrow;
}
*/
@ -544,7 +544,7 @@ class VolcanoAsrApiService implements AsrService {
await _sendAudioData(audioData);
}
} catch (e) {
Logger.error('捕获和发送音频失败: $e');
Logger.error('捕获和发送音频失败: ${e.toString()}');
}
*/
}
@ -562,7 +562,7 @@ class VolcanoAsrApiService implements AsrService {
_channel!.sink.add(jsonEncode(audioMessage));
} catch (e) {
Logger.error('发送音频数据失败: $e');
Logger.error('发送音频数据失败: ${e.toString()}');
}
}
@ -579,7 +579,7 @@ class VolcanoAsrApiService implements AsrService {
_channel!.sink.add(jsonEncode(endCommand));
Logger.info('已发送结束指令');
} catch (e) {
Logger.error('发送结束指令失败: $e');
Logger.error('发送结束指令失败: ${e.toString()}');
}
}
@ -596,7 +596,7 @@ class VolcanoAsrApiService implements AsrService {
_isRecording = false;
Logger.info('录音已停止');
} catch (e) {
Logger.error('停止录音失败: $e');
Logger.error('停止录音失败: ${e.toString()}');
}
}
*/
@ -641,7 +641,7 @@ class VolcanoAsrApiService implements AsrService {
}
}
} catch (e) {
Logger.error('处理WebSocket消息失败: $e');
Logger.error('处理WebSocket消息失败: ${e.toString()}');
if (_recognitionController != null && !_recognitionController!.isClosed) {
_recognitionController!.addError('处理消息失败: $e');
@ -675,7 +675,7 @@ class VolcanoAsrApiService implements AsrService {
}
}
} catch (e) {
Logger.error('处理结果消息失败: $e');
Logger.error('处理结果消息失败: ${e.toString()}');
}
}
@ -741,7 +741,7 @@ class VolcanoAsrApiService implements AsrService {
/// 处理WebSocket错误
void _handleWebSocketError(dynamic error) {
Logger.error('WebSocket错误: $error');
Logger.error('WebSocket错误: ${e.toString()}rror');
if (_connectionCompleter != null && !_connectionCompleter!.isCompleted) {
_connectionCompleter!.completeError(error);
@ -789,7 +789,7 @@ class VolcanoAsrApiService implements AsrService {
_channel!.sink.close();
_channel = null;
} catch (e) {
Logger.error('关闭WebSocket连接失败: $e');
Logger.error('关闭WebSocket连接失败: ${e.toString()}');
}
}
}
@ -811,4 +811,4 @@ class VolcanoAsrApiService implements AsrService {
_isContinuousRecognitionActive = false;
_isRecognitionEndRequested = false;
}
}
}

16
lib/data/services/speech_impl/volcano_tts_api_service.dart

@ -357,7 +357,7 @@ class VolcanoTtsApiService extends GetxController {
_handleMessage,
onError: (error) {
// 直接处理错误,不尝试重连
Logger.error('WebSocket错误: $error');
Logger.error('WebSocket错误: ${error.toString()}');
if (!_eventController.isClosed) {
_eventController.add({
'eventType': 'error',
@ -391,7 +391,7 @@ class VolcanoTtsApiService extends GetxController {
});
}
} catch (e) {
Logger.error('处理TTS连接超时事件失败: $e');
Logger.error('处理TTS连接超时事件失败: ${e.toString()}');
}
}
timeout = null;
@ -427,7 +427,7 @@ class VolcanoTtsApiService extends GetxController {
return result;
} catch (e) {
// 只记录错误,不尝试重连
Logger.error('连接过程中发生异常: $e');
Logger.error('连接过程中发生异常: ${e.toString()}');
return false;
} finally {
isConnecting.value = false;
@ -479,7 +479,7 @@ class VolcanoTtsApiService extends GetxController {
'error': '会话开始超时'
});
} catch (e) {
Logger.error('处理TTS会话超时事件失败: $e');
Logger.error('处理TTS会话超时事件失败: ${e.toString()}');
}
}
timeout = null;
@ -515,7 +515,7 @@ class VolcanoTtsApiService extends GetxController {
return result;
} catch (e) {
// 只记录错误,不尝试恢复
Logger.error('开始TTS会话失败: $e');
Logger.error('开始TTS会话失败: ${e.toString()}');
return false;
}
}
@ -902,10 +902,10 @@ class VolcanoTtsApiService extends GetxController {
'error': error.toString()
});
Logger.error('火山语音TTS错误: $error');
Logger.error('火山语音TTS错误: ${error.toString()}');
} catch (e) {
// 如果在处理错误时发生异常,记录到日志但不再抛出异常
Logger.error('处理TTS错误时发生异常: $e,原始错误: $error');
Logger.error('处理TTS错误时发生异常: ${e.toString()},原始错误: ${error.toString()}');
}
}
@ -1445,7 +1445,7 @@ class VolcanoTtsApiService extends GetxController {
return success;
} catch (e) {
Logger.error('TTS一次性合成失败: $e');
Logger.error('TTS一次性合成失败: ${e.toString()}');
// 确保会话结束
endSession();
return false;

4
lib/data/services/speech_impl/volcano_tts_service.dart

@ -87,11 +87,11 @@ class VolcanoTtsService extends GetxService implements TtsService {
case 'error':
_isSpeaking = false;
final errorCode = eventMap['errorCode'] as int? ?? 0;
final errorCode = eventMap['errorCode'] as String? ?? "-1";
final errorMessage = eventMap['errorMessage'] as String? ?? '未知错误';
_eventStreamController.add(TtsEvent(
type: TtsEventType.error,
error: errorMessage,
error: '$errorMessage (错误码: $errorCode)',
));
break;
}

20
lib/data/services/speech_impl/xunfei_asr_service.dart

@ -54,7 +54,7 @@ class XunfeiAsrService extends GetxService implements AsrService {
Logger.info('Azure 语音识别服务初始化成功');
return true;
} catch (e) {
Logger.error('Azure 语音识别服务初始化失败: $e');
Logger.error('Azure 语音识别服务初始化失败: ${e.toString()}');
_isInitialized = false;
rethrow;
}
@ -69,7 +69,7 @@ class XunfeiAsrService extends GetxService implements AsrService {
try {
throw Exception('无效的识别结果格式');
} catch (e) {
Logger.error('语音识别失败: $e');
Logger.error('语音识别失败: ${e.toString()}');
return RecognitionEvent.error(e.toString());
}
}
@ -112,7 +112,7 @@ class XunfeiAsrService extends GetxService implements AsrService {
_isContinuousRecognitionActive = true;
return _eventStreamController!.stream;
} catch (e) {
Logger.error('开始连续语音识别失败: $e');
Logger.error('开始连续语音识别失败: ${e.toString()}');
rethrow;
}
}
@ -152,7 +152,7 @@ class XunfeiAsrService extends GetxService implements AsrService {
},
onError: (error) {
isConnected = false;
Logger.error('连接失败: $error');
Logger.error('连接失败: ${error.toString()}');
},
onDone: () {
isConnected = false;
@ -161,7 +161,7 @@ class XunfeiAsrService extends GetxService implements AsrService {
cancelOnError: true,
);
} catch (e) {
Logger.error("初始化异常----------------$e");
Logger.error("初始化异常----------------${e.toString()}");
}
}
@ -185,7 +185,7 @@ class XunfeiAsrService extends GetxService implements AsrService {
}
}
}
Logger.info(
Logger.info(
"识别到的内容 type:${data['cn']['st']['type']} msg:$msg rl:$role");
if (msg.isNotEmpty) {
if (data['cn']['st']['type'] == "0") {
@ -206,7 +206,7 @@ class XunfeiAsrService extends GetxService implements AsrService {
}
}
} catch (e) {
Logger.error("接受消息异常 $e");
Logger.error("接受消息异常 ${e.toString()}");
}
}
@ -242,7 +242,7 @@ class XunfeiAsrService extends GetxService implements AsrService {
_isContinuousRecognitionActive = false;
return true;
} catch (e) {
Logger.error('停止连续语音识别失败: $e');
Logger.error('停止连续语音识别失败: ${e.toString()}');
rethrow;
}
}
@ -266,8 +266,8 @@ class XunfeiAsrService extends GetxService implements AsrService {
Logger.info('Azure 语音识别资源已释放');
} catch (e) {
Logger.error('释放语音识别资源失败: $e');
Logger.error('释放语音识别资源失败: ${e.toString()}');
_isInitialized = false;
}
}
}
}

18
lib/data/services/voice_interaction_impl/android_voice_interaction_service.dart

@ -76,7 +76,7 @@ class AndroidVoiceInteractionService extends GetxService {
_handleVoiceInteractionEvent(event);
}
}, onError: (error) {
Logger.error('语音交互事件通道错误: $error');
Logger.error('语音交互事件通道错误: ${error.toString()}');
});
}
@ -106,7 +106,7 @@ class AndroidVoiceInteractionService extends GetxService {
Logger.info('语音交互服务初始化完成');
return this;
} catch (e) {
Logger.error('语音交互服务初始化失败: $e');
Logger.error('语音交互服务初始化失败: ${e.toString()}');
return this;
}
}
@ -123,7 +123,7 @@ class AndroidVoiceInteractionService extends GetxService {
_isServiceRunning.value = isRunning;
Logger.info('语音交互服务状态: ${isRunning ? "运行中" : "未运行"}');
} catch (e) {
Logger.error('检查语音交互服务状态失败: $e');
Logger.error('检查语音交互服务状态失败: ${e.toString()}');
_isServiceRunning.value = false;
}
}
@ -151,7 +151,7 @@ class AndroidVoiceInteractionService extends GetxService {
return result;
} catch (e) {
Logger.error('启动语音交互服务失败: $e');
Logger.error('启动语音交互服务失败: ${e.toString()}');
return false;
}
}
@ -175,7 +175,7 @@ class AndroidVoiceInteractionService extends GetxService {
return result;
} catch (e) {
Logger.error('停止语音交互服务失败: $e');
Logger.error('停止语音交互服务失败: ${e.toString()}');
return false;
}
}
@ -199,7 +199,7 @@ class AndroidVoiceInteractionService extends GetxService {
return result;
} catch (e) {
Logger.error('暂停语音交互失败: $e');
Logger.error('暂停语音交互失败: ${e.toString()}');
return false;
}
}
@ -213,7 +213,7 @@ class AndroidVoiceInteractionService extends GetxService {
final int timestamp = eventMap['timestamp'] as int? ?? 0;
// 添加时间戳日志,帮助调试
Logger.info('收到原生端事件: $eventType, 时间戳: $timestamp, 当前时间: ${DateTime.now().millisecondsSinceEpoch}');
Logger.info('收到原生端事件: ${eventType.toString()}, 时间戳: $timestamp, 当前时间: ${DateTime.now().millisecondsSinceEpoch}');
switch (eventType) {
case 'chatHistory':
@ -252,7 +252,7 @@ class AndroidVoiceInteractionService extends GetxService {
break;
default:
Logger.warning('收到未知类型的语音交互事件: $eventType');
Logger.warning('收到未知类型的语音交互事件: ${eventType.toString()}');
break;
}
}
@ -297,7 +297,7 @@ class AndroidVoiceInteractionService extends GetxService {
// 保存更新后的历史记录
await chatHistoryService.saveHistory(agentId, existingMessages);
} catch (e) {
Logger.error('保存聊天记录失败: $e');
Logger.error('保存聊天记录失败: ${e.toString()}');
}
}

12
lib/data/services/voice_interaction_impl/ios_voice_interaction_service.dart

@ -66,7 +66,7 @@ class IosVoiceInteractionService extends GetxService {
// 保持idle状态,不自动启动语音识别
Logger.info('iOS语音交互服务初始化完成,处于idle状态');
} catch (e) {
Logger.error('iOS语音交互服务初始化失败: $e');
Logger.error('iOS语音交互服务初始化失败: ${e.toString()}');
}
return this;
@ -110,13 +110,13 @@ class IosVoiceInteractionService extends GetxService {
// 监听识别结果
_recognitionSubscription =
recognitionStream.listen(_handleRecognitionEvent, onError: (error) {
Logger.error('语音识别错误: $error');
Logger.error('语音识别错误: ${error.toString()}');
_startContinuousListening(); // 尝试重新启动
});
Logger.info('开始连续语音识别');
} catch (e) {
Logger.error('启动语音识别失败: $e');
Logger.error('启动语音识别失败: ${e.toString()}');
}
}
@ -263,7 +263,7 @@ class IosVoiceInteractionService extends GetxService {
// 处理完成
isProcessingAI.value = false;
} catch (e) {
Logger.error('AI响应处理失败: $e');
Logger.error('AI响应处理失败: ${e.toString()}');
// 错误恢复
isProcessingAI.value = false;
}
@ -437,7 +437,7 @@ class IosVoiceInteractionService extends GetxService {
Logger.info('保存聊天历史记录成功');
} catch (e) {
Logger.error('保存聊天历史记录失败: $e');
Logger.error('保存聊天历史记录失败: ${e.toString()}');
}
}
@ -463,4 +463,4 @@ class IosVoiceInteractionService extends GetxService {
_eventStreamController.add(event);
Logger.info('已发送识别开始事件');
}
}
}

4
lib/firebase_options.dart

@ -66,7 +66,7 @@ class DefaultFirebaseOptions {
storageBucket: 'deepsound-e0e3d.firebasestorage.app',
androidClientId: '907754492821-9jakqvathu61e6b8g95i41ssi7o4dnka.apps.googleusercontent.com',
iosClientId: '907754492821-ej6j6pfto66jfhpj47b9bjs9amvo9dde.apps.googleusercontent.com',
iosBundleId: 'com.example.client',
iosBundleId: 'com.yunqiinnovation.deepsound',
);
static const FirebaseOptions macos = FirebaseOptions(
@ -77,7 +77,7 @@ class DefaultFirebaseOptions {
storageBucket: 'deepsound-e0e3d.firebasestorage.app',
androidClientId: '907754492821-9jakqvathu61e6b8g95i41ssi7o4dnka.apps.googleusercontent.com',
iosClientId: '907754492821-ej6j6pfto66jfhpj47b9bjs9amvo9dde.apps.googleusercontent.com',
iosBundleId: 'com.example.client',
iosBundleId: 'com.yunqiinnovation.deepsound',
);
static const FirebaseOptions windows = FirebaseOptions(

4
lib/main.dart

@ -24,8 +24,10 @@ void main() async {
// 初始化环境变量
await dotenv.load();
// 初始化登录服务(包含Firebase初始化)
LoginService();
//LoginService.FirebaseInit();
// 设置窗口大小(仅在 macOS 下生效)
if (Platform.isMacOS) {
const double width = 375.0;

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

@ -5,12 +5,12 @@ import 'package:deep_voice/data/services/music_manager.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:agent_service/agent_service.dart';
import 'package:logger/logger.dart';
import 'package:chat_storage/chat_storage.dart';
import 'package:image_picker/image_picker.dart';
import 'package:get_storage/get_storage.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:app_settings/app_settings.dart';
import 'package:deep_voice/core/utils/logger.dart';
class Message {
final bool isUser; //标识信息是否来自用户还是AI
@ -36,8 +36,6 @@ class Message {
class AgentController extends GetxController {
static AgentController get to => Get.find(); //单例访问模式
final logger = Logger();
// 聊天消息列表
final messages = <Message>[].obs;
@ -78,6 +76,8 @@ class AgentController extends GetxController {
bool _isNewAssistantResponse = true;
String _currentResponseId = '';
final TAG = 'AgentController';
// 会话ID
final String sessionId = 'default_agent';
@ -109,7 +109,7 @@ class AgentController extends GetxController {
isTtsEnabled.value = storage.read('agent_tts_enabled') ?? true;
} catch (e) {
// 如果读取失败,使用默认值
logger.e('加载TTS偏好设置失败: $e');
Logger.e(TAG, '加载TTS偏好设置失败: $e');
isTtsEnabled.value = true;
}
}
@ -128,14 +128,14 @@ class AgentController extends GetxController {
final storage = GetStorage();
await storage.write('agent_tts_enabled', isTtsEnabled.value);
} catch (e) {
logger.e('保存TTS偏好设置失败: $e');
Logger.e(TAG, '保存TTS偏好设置失败: $e');
}
}
// 加载聊天历史记录
Future<void> _loadChatHistory() async {
try {
logger.i('正在加载聊天历史...');
Logger.i(TAG, '正在加载聊天历史...');
final result = await ChatStorage.getMessages(
sessionId: sessionId,
page: 1,
@ -160,14 +160,14 @@ class AgentController extends GetxController {
historyMessages.sort((a, b) => a.timestamp.compareTo(b.timestamp));
messages.assignAll(historyMessages);
logger.i('成功加载 ${historyMessages.length} 条聊天记录');
Logger.i(TAG, '成功加载 ${historyMessages.length} 条聊天记录');
} else {
// 没有历史记录时添加欢迎消息
_addWelcomeMessage();
logger.i('没有历史记录,添加欢迎消息');
Logger.i(TAG, '没有历史记录,添加欢迎消息');
}
} catch (e) {
logger.e('加载聊天历史失败: $e');
Logger.e(TAG, '加载聊天历史失败: $e');
_addWelcomeMessage();
} finally {
isHistoryLoaded.value = true;
@ -272,12 +272,14 @@ class AgentController extends GetxController {
case AgentServiceEventType.ttsStarted:
isSpeaking.value = true;
Logger.i(TAG, 'TTS开始播放');
break;
case AgentServiceEventType.ttsCompleted:
case AgentServiceEventType.ttsStopped:
case AgentServiceEventType.ttsCanceled:
isSpeaking.value = false;
Logger.i(TAG, 'TTS停止播放');
break;
case AgentServiceEventType.imageProcessing:
@ -339,7 +341,7 @@ class AgentController extends GetxController {
messages.refresh();
}
logger.e('代理服务错误: ${event.data['message']}');
Logger.e(TAG, '代理服务错误: ${event.data['message']}');
break;
case AgentServiceEventType.autoStop:
@ -373,14 +375,14 @@ class AgentController extends GetxController {
case AgentServiceEventType.functionCall:
final functionName = event.data['name'] ?? '';
final arguments = event.data['arguments'] ?? '';
// logger.i('Fultter 开始调用 mcp: $functionName, $arguments');
// debugPrint('Fultter 开始调用 mcp: $functionName, $arguments');
break;
case AgentServiceEventType.functionCallResult:
final responseId = event.data['responseId'] ?? '';
final responseId = event.data['responseId'] ?? '';
final result = event.data['result'] ?? '';
final functionCall = event.data['function_call'] ?? '';
logger.i('Flutter 结束调用 mcp: $result, $functionCall');
Logger.i(TAG, 'Flutter 结束调用 mcp: $result, $functionCall');
if (result.isNotEmpty) {
final parsedResult = jsonDecode(result);
final meta = parsedResult["meta"];
@ -390,7 +392,7 @@ final responseId = event.data['responseId'] ?? '';
try {
metaResult = jsonDecode(meta);
} catch (e) {
print('解析meta数据失败: $e');
Logger.e(TAG, '解析meta数据失败: $e');
}
}
if(metaResult.isNotEmpty){
@ -452,7 +454,7 @@ final responseId = event.data['responseId'] ?? '';
await AgentService.processTextInput(text,
speakResponse: isTtsEnabled.value);
} catch (e) {
logger.e('发送文本消息失败: $e');
Logger.e(TAG, '发送文本消息失败: $e');
isProcessing.value = false;
}
}
@ -460,7 +462,7 @@ final responseId = event.data['responseId'] ?? '';
// 发送图片消息
Future<void> sendImageMessage(String imagePath, {String? text}) async {
if (imagePath.isEmpty || !File(imagePath).existsSync()) {
logger.e('图片不存在: $imagePath');
Logger.e(TAG, '图片不存在: $imagePath');
return;
}
@ -494,7 +496,7 @@ final responseId = event.data['responseId'] ?? '';
speakResponse: isTtsEnabled.value,
);
} catch (e) {
logger.e('处理图片失败: $e');
Logger.e(TAG, '处理图片失败: $e');
isProcessing.value = false;
isImageProcessing.value = false;
}
@ -531,36 +533,73 @@ final responseId = event.data['responseId'] ?? '';
// 检查麦克风权限
Future<bool> _checkMicrophonePermission() async {
try {
PermissionStatus status = await Permission.microphone.status;
// 如果已授权,直接返回 true
if (status.isGranted) {
// 创建待请求权限列表
List<Permission> permissionsToRequest = [];
if (Platform.isAndroid) {
// Android 需要考虑SDK版本不同的权限要求
permissionsToRequest.add(Permission.microphone);
} else if (Platform.isIOS) {
// iOS 只需要麦克风权限
permissionsToRequest.add(Permission.microphone);
}
// 批量请求所有需要的权限
final statuses = await permissionsToRequest.request();
// 检查是否所有权限都已授予
bool allGranted = true;
Permission? deniedPermission;
statuses.forEach((permission, status) {
if (!status.isGranted) {
allGranted = false;
deniedPermission = permission;
Logger.e(TAG, '权限 ${permission.toString()} 未被授予: $status');
}
});
// 单独检查麦克风权限状态,因为这是主要需求
final micStatus = await Permission.microphone.status;
// 如果所有权限都已授予,或至少麦克风权限已授予,返回true
if (allGranted || micStatus.isGranted) {
return true;
}
// 如果被永久拒绝,提示用户前往设置页面
if (status.isPermanentlyDenied) {
// 处理特定权限被永久拒绝的情况
if (micStatus.isPermanentlyDenied) {
_showPermissionDialog(true);
return false;
}
// 如果之前拒绝但不是永久拒绝,或首次请求,尝试请求权限
if (status.isDenied) {
status = await Permission.microphone.request();
if (status.isGranted) {
return true;
} else {
_showPermissionDialog(false);
return false;
}
// 普通拒绝情况
if (deniedPermission != null) {
_showPermissionDialog(false);
}
return false;
} catch (e) {
logger.e('检查麦克风权限失败: $e');
Logger.e(TAG, '检查麦克风权限失败: $e');
return false;
}
}
// 获取Android SDK版本
Future<int> _getAndroidSdkVersion() async {
if (Platform.isAndroid) {
try {
// 这里我们简单返回一个较高的值,确保请求所有可能需要的权限
// 在实际应用中,你可能需要使用platform channel获取真实的SDK版本
return 31; // 对应Android 12
} catch (e) {
Logger.e(TAG, '获取Android SDK版本失败: $e');
}
}
return 0;
}
// 显示权限提示对话框
void _showPermissionDialog(bool isPermanent) {
@ -605,7 +644,7 @@ final responseId = event.data['responseId'] ?? '';
try {
await AgentService.startConversation();
} catch (e) {
logger.e('启动语音输入失败: $e');
Logger.e(TAG, '启动语音输入失败: $e');
}
}
@ -616,7 +655,7 @@ final responseId = event.data['responseId'] ?? '';
try {
await AgentService.stopConversation();
} catch (e) {
logger.e('停止语音输入失败: $e');
Logger.e(TAG, '停止语音输入失败: $e');
}
}
@ -634,7 +673,7 @@ final responseId = event.data['responseId'] ?? '';
// 重置回复状态
_isNewAssistantResponse = true;
} catch (e) {
logger.e('清除聊天历史失败: $e');
Logger.e(TAG, '清除聊天历史失败: $e');
}
}
@ -656,7 +695,7 @@ final responseId = event.data['responseId'] ?? '';
isImageInputActive.value = true;
}
} catch (e) {
logger.e('选择图片失败: $e');
Logger.e(TAG, '选择图片失败: $e');
}
}
@ -678,7 +717,7 @@ final responseId = event.data['responseId'] ?? '';
isImageInputActive.value = true;
}
} catch (e) {
logger.e('拍照失败: $e');
Logger.e(TAG, '拍照失败: $e');
}
}
@ -720,7 +759,7 @@ final responseId = event.data['responseId'] ?? '';
// 播放音乐
void musicStatus() async {
var status = await _musiceManager.getStatus();
// logger.i('获取当前音乐播放状态 $status');
// debugPrint('获取当前音乐播放状态 $status');
isPlaying.value = status['isPlaying'];
currentUrl.value = status['url'];
}

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

@ -149,28 +149,32 @@ class AgentView extends StatelessWidget {
// 聊天内容
Expanded(
child: Obx(
() => ListView.builder(
controller: controller.scrollController,
reverse: true,
padding:
const EdgeInsets.symmetric(vertical: 16, horizontal: 16),
itemCount: controller.messages.length,
itemBuilder: (context, index) {
final message = controller
.messages[controller.messages.length - 1 - index];
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: MessageBubble(
isUser: message.isUser,
message: message.text,
timestamp: message.timestamp,
isRecognizing: message.isRecognizing,
hasImage: message.hasImage,
imagePath: message.imagePath,
mate: message.meta,
),
);
},
() => Align(
alignment: Alignment.topCenter,
child: ListView.builder(
controller: controller.scrollController,
shrinkWrap: true,
reverse: true,
padding:
const EdgeInsets.symmetric(vertical: 16, horizontal: 16),
itemCount: controller.messages.length,
itemBuilder: (context, index) {
final message = controller
.messages[controller.messages.length - 1 - index];
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: MessageBubble(
isUser: message.isUser,
message: message.text,
timestamp: message.timestamp,
isRecognizing: message.isRecognizing,
hasImage: message.hasImage,
imagePath: message.imagePath,
meta: message.meta,
),
);
},
),
),
),
),
@ -286,12 +290,16 @@ class AgentView extends StatelessWidget {
width: 44,
height: 44,
decoration: BoxDecoration(
color: Colors.grey[200],
color: isDarkMode
? Colors.grey[800]
: Colors.grey[200],
borderRadius: BorderRadius.circular(22),
),
child: Icon(
isTextMode ? Icons.mic_none : Icons.keyboard,
color: Colors.grey[700],
color: isDarkMode
? Colors.grey[300]
: Colors.grey[700],
size: 22,
),
),
@ -308,13 +316,18 @@ class AgentView extends StatelessWidget {
if (isTextMode) {
// 文本输入模式 - 注意观察图片模式时的高度变化
return Container(
height: 44,
//height: 44,
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: Colors.grey[200],
color: isDarkMode
? Colors.grey[800]
: Colors.grey[200],
borderRadius: BorderRadius.circular(22),
border: Border.all(
color: Colors.grey.withOpacity(0.1),
color: isDarkMode
? Colors.grey[
800]! // 修改: 从 Colors.grey[700] 改为 Colors.grey[800]
: Colors.grey.withOpacity(0.1),
width: 1.0,
),
),
@ -334,7 +347,7 @@ class AgentView extends StatelessWidget {
fontSize: 15,
color: isDarkMode
? Colors.white
: Colors.black87,
: Colors.grey[800],
),
decoration: InputDecoration(
hintText: isImageActive
@ -378,14 +391,18 @@ class AgentView extends StatelessWidget {
}
},
child: Container(
height: 44,
height: 50,
padding:
const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: Colors.grey[200],
color: isDarkMode
? Colors.grey[800]
: Colors.grey[200],
borderRadius: BorderRadius.circular(22),
border: Border.all(
color: Colors.grey.withOpacity(0.1),
color: isDarkMode
? Colors.grey[800]!
: Colors.grey.withOpacity(0.1),
width: 1.0,
),
),
@ -446,13 +463,14 @@ class AgentView extends StatelessWidget {
width: 44,
height: 44,
decoration: BoxDecoration(
color: Colors.grey[200],
color: isDarkMode ? Colors.grey[800] : Colors.grey[200],
borderRadius: BorderRadius.circular(22),
),
child: PopupMenuButton<String>(
icon: Icon(
Icons.add_photo_alternate_outlined,
color: Colors.grey[700],
color:
isDarkMode ? Colors.grey[300] : Colors.grey[700],
size: 22,
),
padding: EdgeInsets.zero,

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

@ -1,7 +1,9 @@
import 'package:common_utils/common_utils.dart';
import 'package:deep_voice/modules/agent/views/message_bubble_navigation.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:get_storage/get_storage.dart';
import 'dart:io';
import '../controllers/agent_controller.dart';
@ -14,7 +16,7 @@ class MessageBubble extends GetView<AgentController> {
final bool isRecognizing;
final bool hasImage;
final String? imagePath;
final Map? mate;
final Map? meta;
const MessageBubble({
Key? key,
@ -24,7 +26,7 @@ class MessageBubble extends GetView<AgentController> {
this.isRecognizing = false,
this.hasImage = false,
this.imagePath,
this.mate,
this.meta,
}) : super(key: key);
@override
@ -38,8 +40,8 @@ class MessageBubble extends GetView<AgentController> {
isUser ? MainAxisAlignment.end : MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!isUser) _buildAvatar(isUser, context),
const SizedBox(width: 8),
//if (!isUser) _buildAvatar(isUser, context),
//const SizedBox(width: 8),
Flexible(
child: Column(
crossAxisAlignment:
@ -49,15 +51,23 @@ class MessageBubble extends GetView<AgentController> {
padding: hasImage
? (message == '[图片]'
? EdgeInsets.zero
: const EdgeInsets.all(12))
: const EdgeInsets.all(12),
: isUser
? const EdgeInsets.fromLTRB(
12, 12, 16, 12) // 用户消息右侧多一点内边距
: const EdgeInsets.fromLTRB(
16, 12, 12, 12)) // AI消息左侧多一点内边距
: isUser
? const EdgeInsets.fromLTRB(
12, 12, 16, 12) // 用户消息右侧多一点内边距
: const EdgeInsets.fromLTRB(
16, 12, 12, 12), // AI消息左侧多一点内边距
decoration: BoxDecoration(
color: _getBubbleColor(context),
borderRadius: BorderRadius.circular(18).copyWith(
bottomLeft: isUser
topLeft: isUser
? const Radius.circular(18)
: const Radius.circular(4),
bottomRight: !isUser
topRight: !isUser
? const Radius.circular(18)
: const Radius.circular(4),
),
@ -78,15 +88,17 @@ class MessageBubble extends GetView<AgentController> {
timeFormat.format(timestamp),
style: TextStyle(
fontSize: 10,
color: Colors.grey[500],
color: Theme.of(context).brightness == Brightness.dark
? Colors.grey[400]
: Colors.grey[500],
fontWeight: FontWeight.w400,
),
),
],
),
),
const SizedBox(width: 8),
if (isUser) _buildAvatar(isUser, context),
//const SizedBox(width: 8),
//if (isUser) _buildAvatar(isUser, context),
],
),
);
@ -94,37 +106,76 @@ class MessageBubble extends GetView<AgentController> {
/// 获取气泡颜色
Color _getBubbleColor(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
if (isUser) {
return isRecognizing
? Color(0xFF3B82F6).withOpacity(0.8) // 识别中的用户消息使用半透明的蓝色
: Color(0xFF3B82F6); // 使用更明亮的蓝色
} else {
return Color(0xFFF1F5F9); // 更淡的灰色背景
return isDarkMode
? Color(0xFF2A2A2A)
: Color(0xFFF1F5F9); // 根据主题调整AI消息背景色
}
}
/// 构建头像
Widget _buildAvatar(bool isUser, BuildContext context) {
return Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: isUser ? Colors.grey[300] : Theme.of(context).primaryColor,
borderRadius: BorderRadius.circular(18),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 2,
offset: const Offset(0, 1),
),
],
),
child: Icon(
isUser ? Icons.person : Icons.smart_toy_outlined,
color: isUser ? Colors.grey[700] : Colors.white,
size: 20,
),
);
if (isUser) {
final storage = GetStorage();
final localAvatarPath = storage.read<String>('local_avatar') ?? '';
return Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: localAvatarPath.isEmpty ? Colors.grey[300] : null,
borderRadius: BorderRadius.circular(18),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 2,
offset: const Offset(0, 1),
),
],
),
child: localAvatarPath.isNotEmpty
? ClipRRect(
borderRadius: BorderRadius.circular(18),
child: Image.file(
File(localAvatarPath),
fit: BoxFit.cover,
),
)
: Icon(
Icons.person,
color: Colors.grey[700],
size: 20,
),
);
} else {
// AI头像保持不变
return Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
borderRadius: BorderRadius.circular(18),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 2,
offset: const Offset(0, 1),
),
],
),
child: Icon(
Icons.smart_toy_outlined,
color: Colors.white,
size: 20,
),
);
}
}
/// 构建消息内容
@ -195,28 +246,29 @@ class MessageBubble extends GetView<AgentController> {
}
// 判断消息内容,显示特殊卡片
if (!isUser && mate != null) {
if (mate!.keys.contains('card_music')) {
final musicId = mate!['card_music']['id'].toString();
final musicName = mate!['card_music']['name'] as String;
final musicUrl = mate!['card_music']['url'] as String;
final musicSinger = mate!['card_music']['sgener'] as String;
final musicImage = mate!['card_music']['image'] as String;
if (!isUser && meta != null) {
// 判断消息类型,显示不同的卡片
if (meta!.keys.contains('card_music')) {
final musicId = meta!['card_music']['id'].toString();
final musicName = meta!['card_music']['name'] as String;
final musicUrl = meta!['card_music']['url'] as String;
final musicSinger = meta!['card_music']['sgener'] as String;
final musicImage = meta!['card_music']['image'] as String;
//音乐卡片
return _buildMusicItem(
context, musicId, musicName, musicUrl, musicSinger, musicImage);
} else if (mate!.keys.contains('card_musiclist')) {
final rawList = mate!['card_musiclist']['musics'] as List;
} else if (meta!.keys.contains('card_musiclist')) {
final rawList = meta!['card_musiclist']['musics'] as List;
final playlist =
rawList.map((e) => Map<String, dynamic>.from(e)).toList();
//音乐播放列表
return _buildMusicContent(context, playlist);
} else if (mate!.keys.contains('card_weather')) {
final city = mate!['card_weather']['city'] as String;
final rawList = mate!['card_weather']['dayweather'] as List;
} else if (meta!.keys.contains('card_weather')) {
final city = meta!['card_weather']['city'] as String;
final rawList = meta!['card_weather']['dayweather'] as List;
final daylist =
rawList.map((e) => Map<String, dynamic>.from(e)).toList();
final now = mate!['card_weather']['day'] as Map<String, dynamic>;
final now = meta!['card_weather']['day'] as Map<String, dynamic>;
//天气
return _buildWeatherContent(
context,
@ -224,12 +276,17 @@ class MessageBubble extends GetView<AgentController> {
now,
daylist,
);
} else if (mate!.keys.contains('card_travel')) {
final rawList = mate!['card_travel']['items'] as List;
} else if (meta!.keys.contains('card_travel')) {
final rawList = meta!['card_travel']['items'] as List;
final travellist =
rawList.map((e) => Map<String, dynamic>.from(e)).toList();
// 旅游
return _buildTravelContent(context, travellist);
} else if (meta!.keys.contains('card_navigation')) {
final rawList = meta!['card_navigation']['items'] as List;
final list = rawList.map((e) => Map<String, dynamic>.from(e)).toList();
// 导航
return navigationCard(context, modes: list);
}
// if (message.contains('播放列表') || message.contains('音乐')) {
@ -246,7 +303,11 @@ class MessageBubble extends GetView<AgentController> {
return Text(
message,
style: TextStyle(
color: isUser ? Colors.white : Colors.black87,
color: isUser
? Colors.white
: (Theme.of(context).brightness == Brightness.dark
? Colors.white.withOpacity(0.9)
: Colors.black87),
fontSize: 14,
height: 1.4,
fontWeight: FontWeight.w400,
@ -278,135 +339,146 @@ class MessageBubble extends GetView<AgentController> {
/// 构建音乐内容卡片
Widget _buildMusicContent(
BuildContext context, List<Map<String, dynamic>> playlist) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
Text(
'以下是一些推荐的音乐:',
style: TextStyle(
color: Colors.black87,
color: isDarkMode ? Colors.white.withOpacity(0.9) : Colors.black87,
fontSize: 14,
height: 1.4,
fontWeight: FontWeight.w400,
),
),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.03),
blurRadius: 3,
spreadRadius: 1,
offset: const Offset(0, 1),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.music_note,
size: 16,
color: Theme.of(context).primaryColor.withOpacity(0.7),
),
const SizedBox(width: 6),
Text(
'播放列表'.tr,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: Colors.grey[700],
),
),
],
),
const SizedBox(height: 10),
// 动态生成音乐条目
...playlist.asMap().entries.map((entry) {
final index = entry.key;
final item = entry.value;
final id = item["id"]?.toString() ?? "";
final name = item["name"] ?? "未知歌曲";
final url = item["url"] ?? "";
final singer = item["sgener"] ?? "未知歌手";
final image = item["image"] ?? "";
return Column(
children: [
_buildMusicItem(context, id, name, url, singer, image),
if (index != playlist.length - 1)
Divider(height: 1, thickness: 1, color: Colors.grey[100]),
],
);
}).toList(),
const SizedBox(height: 8),
Center(
child: TextButton(
onPressed: () {},
style: TextButton.styleFrom(
minimumSize: Size.zero,
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
child: Text(
'查看更多推荐',
style: TextStyle(
fontSize: 12,
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.w500,
),
),
),
),
],
),
),
const SizedBox(height: 10),
Row(
mainAxisSize: MainAxisSize.min,
// Container(
// padding: const EdgeInsets.all(12),
// decoration: BoxDecoration(
// color: Colors.white,
// borderRadius: BorderRadius.circular(12),
// boxShadow: [
// BoxShadow(
// color: Colors.black.withOpacity(0.03),
// blurRadius: 3,
// spreadRadius: 1,
// offset: const Offset(0, 1),
// ),
// ],
// ),
// child:
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: () {},
icon: Icon(Icons.play_arrow, size: 16),
label: Text('播放全部'),
style: OutlinedButton.styleFrom(
foregroundColor: Theme.of(context).primaryColor,
side: BorderSide(
color: Theme.of(context).primaryColor.withOpacity(0.5)),
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
Row(
children: [
Icon(
Icons.music_note,
size: 16,
color: Theme.of(context).primaryColor.withOpacity(0.7),
),
const SizedBox(width: 6),
Text(
'播放列表'.tr,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: Theme.of(context).brightness == Brightness.dark
? Colors.grey[300]
: Colors.grey[700],
),
textStyle: TextStyle(fontSize: 12),
),
),
],
),
SizedBox(width: 8),
Expanded(
child: OutlinedButton.icon(
const SizedBox(height: 10),
// 动态生成音乐条目
...playlist.asMap().entries.take(4).map((entry) {
final index = entry.key;
final item = entry.value;
final id = item["id"]?.toString() ?? "";
final name = item["name"] ?? "未知歌曲";
final url = item["url"] ?? "";
final singer = item["sgener"] ?? "未知歌手";
final image = item["image"] ?? "";
return Column(
children: [
_buildMusicItem(context, id, name, url, singer, image),
if (index != playlist.length - 1)
Divider(
height: 1,
thickness: 1,
color: Theme.of(context).brightness == Brightness.dark
? Colors.grey[800]
: Colors.grey[100],
),
],
);
}).toList(),
const SizedBox(height: 8),
Center(
child: TextButton(
onPressed: () {},
icon: Icon(Icons.shuffle, size: 16),
label: Text('随机播放'),
style: OutlinedButton.styleFrom(
foregroundColor: Colors.grey[700],
side: BorderSide(color: Colors.grey[400]!),
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
style: TextButton.styleFrom(
minimumSize: Size.zero,
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
child: Text(
'查看更多推荐',
style: TextStyle(
fontSize: 12,
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.w500,
),
textStyle: TextStyle(fontSize: 12),
),
),
),
],
),
//),
//const SizedBox(height: 10),
// Row(
// mainAxisSize: MainAxisSize.min,
// children: [
// Expanded(
// child: OutlinedButton.icon(
// onPressed: () {},
// icon: Icon(Icons.play_arrow, size: 16),
// label: Text('播放全部'),
// style: OutlinedButton.styleFrom(
// foregroundColor: Theme.of(context).primaryColor,
// side: BorderSide(
// color: Theme.of(context).primaryColor.withOpacity(0.5)),
// padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4),
// shape: RoundedRectangleBorder(
// borderRadius: BorderRadius.circular(16),
// ),
// textStyle: TextStyle(fontSize: 12),
// ),
// ),
// ),
// SizedBox(width: 8),
// Expanded(
// child: OutlinedButton.icon(
// onPressed: () {},
// icon: Icon(Icons.shuffle, size: 16),
// label: Text('随机播放'),
// style: OutlinedButton.styleFrom(
// foregroundColor: Colors.grey[700],
// side: BorderSide(color: Colors.grey[400]!),
// padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4),
// shape: RoundedRectangleBorder(
// borderRadius: BorderRadius.circular(16),
// ),
// textStyle: TextStyle(fontSize: 12),
// ),
// ),
// ),
// ],
// ),
],
);
}
@ -563,6 +635,8 @@ class MessageBubble extends GetView<AgentController> {
Widget _buildMusicItem(BuildContext context, String id, String name,
String url, String singer, String image) {
final controller = Get.find<AgentController>(); // 获取控制器
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
Map<String, String> song = {
'id': id,
'url': url,
@ -571,79 +645,102 @@ class MessageBubble extends GetView<AgentController> {
'coverUrl': image,
};
controller.musicStatus(); //刷新状态
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
children: [
Container(
width: 38,
height: 38,
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.music_note,
size: 18,
color: Theme.of(context).primaryColor,
),
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: isDarkMode ? Color(0xFF1E1E1E) : Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(isDarkMode ? 0.2 : 0.03),
blurRadius: 3,
spreadRadius: 1,
offset: const Offset(0, 1),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
name,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: Colors.grey[800],
],
),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
children: [
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(8),
image: image.isNotEmpty
? DecorationImage(
image: NetworkImage(image),
fit: BoxFit.cover,
)
: null,
),
child: image.isEmpty
? Icon(
Icons.music_note,
size: 18,
color: Theme.of(context).primaryColor,
)
: null,
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
name,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: Colors.grey[800],
),
overflow: TextOverflow.ellipsis,
),
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 2),
Text(
singer,
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
SizedBox(height: 2),
Text(
singer,
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
overflow: TextOverflow.ellipsis,
),
overflow: TextOverflow.ellipsis,
),
],
],
),
),
),
Obx(() {
final isThisPlaying = controller.isPlaying.value &&
controller.currentUrl.value == url;
Obx(() {
final isThisPlaying = controller.isPlaying.value &&
controller.currentUrl.value == url;
return IconButton(
onPressed: () {
if (controller.isPlaying.value) {
if (controller.currentUrl.value == url) {
controller.musicPause();
} else {
controller.musicPlay(song);
}
} else {
if (controller.currentUrl.value == url) {
controller.musicResume();
return IconButton(
onPressed: () {
if (controller.isPlaying.value) {
if (controller.currentUrl.value == url) {
controller.musicPause();
} else {
controller.musicPlay(song);
}
} else {
controller.musicPlay(song);
if (controller.currentUrl.value == url) {
controller.musicResume();
} else {
controller.musicPlay(song);
}
}
}
},
icon: Icon(
isThisPlaying ? Icons.pause_circle : Icons.play_circle,
color: Theme.of(context).primaryColor,
),
padding: EdgeInsets.zero,
constraints: BoxConstraints(),
iconSize: 24,
);
}),
],
},
icon: Icon(
isThisPlaying ? Icons.pause_circle : Icons.play_circle,
color: Theme.of(context).primaryColor,
),
padding: EdgeInsets.zero,
constraints: BoxConstraints(),
iconSize: 36,
);
}),
],
),
),
);
}
@ -651,6 +748,8 @@ class MessageBubble extends GetView<AgentController> {
/// 构建地点项目
Widget _buildLocationItem(
BuildContext context, String name, String description) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
@ -659,7 +758,7 @@ class MessageBubble extends GetView<AgentController> {
width: 38,
height: 38,
decoration: BoxDecoration(
color: Colors.blue[50],
color: isDarkMode ? Color(0xFF1A365D) : Colors.blue[50],
borderRadius: BorderRadius.circular(8),
),
child: Icon(
@ -678,7 +777,9 @@ class MessageBubble extends GetView<AgentController> {
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: Colors.grey[800],
color: isDarkMode
? Colors.white.withOpacity(0.9)
: Colors.grey[800],
),
overflow: TextOverflow.ellipsis,
),
@ -687,7 +788,7 @@ class MessageBubble extends GetView<AgentController> {
description,
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
),
overflow: TextOverflow.ellipsis,
),
@ -709,13 +810,15 @@ class MessageBubble extends GetView<AgentController> {
/// 构建天气内容卡片
Widget _buildWeatherContent(BuildContext context, String city,
Map<String, dynamic> now, List<Map<String, dynamic>> days) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'查询天气情况:',
style: TextStyle(
color: Colors.black87,
color: isDarkMode ? Colors.white.withOpacity(0.9) : Colors.black87,
fontSize: 14,
height: 1.4,
fontWeight: FontWeight.w400,
@ -725,15 +828,21 @@ class MessageBubble extends GetView<AgentController> {
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0xFF60A5FA), Color(0xFF3B82F6)],
),
gradient: isDarkMode
? LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0xFF1E40AF), Color(0xFF1E3A8A)],
)
: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0xFF60A5FA), Color(0xFF3B82F6)],
),
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
color: Colors.black.withOpacity(isDarkMode ? 0.3 : 0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
@ -810,7 +919,7 @@ class MessageBubble extends GetView<AgentController> {
SizedBox(height: 16),
Container(
height: 1,
color: Colors.white.withOpacity(0.2),
color: Colors.white.withOpacity(isDarkMode ? 0.15 : 0.2),
),
SizedBox(height: 16),
const Text(
@ -856,7 +965,9 @@ class MessageBubble extends GetView<AgentController> {
Container(
padding: EdgeInsets.symmetric(vertical: 8, horizontal: 12),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
color: isDarkMode
? Colors.white.withOpacity(0.15)
: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(8),
),
child: const Row(

96
lib/modules/agent/views/message_bubble_navigation.dart

@ -0,0 +1,96 @@
import 'package:flutter/material.dart';
/// A widget that displays a navigation card with different modes of transportation.
Widget navigationCard(
BuildContext context, {
required List<Map<String, dynamic>> modes,
double height = 260,
}) {
int currentIndex = 0;
return Container(
height: height,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
color: Colors.white,
boxShadow: const [
BoxShadow(
blurRadius: 8,
color: Colors.black12,
offset: Offset(0, 4),
),
],
),
child: StatefulBuilder(
builder: (context, setInnerState) {
return Column(
children: [
// 路线图片
ClipRRect(
borderRadius:
const BorderRadius.vertical(top: Radius.circular(16)),
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: Image.network(
modes[currentIndex]["image"] as String,
key: ValueKey<String>(modes[currentIndex]["ntype"] as String),
height: 160,
width: double.infinity,
fit: BoxFit.cover,
),
),
),
// 导航方式切换按钮
Padding(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: List.generate(modes.length, (index) {
final isSelected = index == currentIndex;
return GestureDetector(
onTap: () {
setInnerState(() => currentIndex = index);
},
child: SizedBox(
width: 50,
child: Text(
modes[index]["ntype"] as String,
style: TextStyle(
color: isSelected ? Colors.blue : Colors.black87,
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
),
);
}),
),
),
// 距离和耗时
Padding(
padding: const EdgeInsets.all(8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('距离:${modes[currentIndex]["distance"]}',
style: const TextStyle(fontSize: 12, color: Colors.blue)),
Container(
decoration: BoxDecoration(
shape: BoxShape.circle, // 👈 圆形背景
color: Colors.blue,
),
child: Icon(Icons.turn_left,
color: Colors.white, // 👈 圆形图标
size: 20),
)
],
),
),
],
);
},
),
);
}

24
lib/modules/chat/controllers/chat_controller.dart

@ -1,6 +1,5 @@
import 'dart:async';
import 'dart:convert';
import 'package:amap_flutter/amap_flutter.dart';
import '../../../data/services/spotify_service.dart';
import 'package:get/get.dart';
import 'package:flutter/widgets.dart';
@ -8,7 +7,6 @@ import 'package:get_storage/get_storage.dart';
import 'package:haptic_feedback/haptic_feedback.dart';
import 'package:just_audio/just_audio.dart';
import 'package:chat_storage/chat_storage.dart';
import 'package:path/path.dart';
import '../../../data/services/ai_service.dart';
import '../../../data/services/deapsound_ai_service.dart';
import '../../../data/services/kouzi_ai_service.dart';
@ -32,7 +30,6 @@ class ChatController extends GetxController {
// 添加ScrollController用于控制列表滚动
final ScrollController scrollController = ScrollController();
final _amapFlutter = AmapFlutter();
StreamSubscription? _locationStream;
String _latLng = '';
String _city = '深圳市';
@ -187,7 +184,6 @@ class ChatController extends GetxController {
// 自动停止计时器会在startVoiceRecognition中启动
}
_getLocation();
player.playerStateStream.listen((state) async {
if (state.processingState == ProcessingState.completed) {
@ -203,15 +199,6 @@ class ChatController extends GetxController {
});
}
void _getLocation() async {
await _amapFlutter.initLocation();
_locationStream = _amapFlutter.locationStream().listen((data) async {
_latLng = '${data['longitude']},${data['latitude']}}';
_city = data['city'];
_amapFlutter.stopLocation();
});
_amapFlutter.startLocation();
}
@override
void onClose() {
@ -228,7 +215,6 @@ class ChatController extends GetxController {
_talkDebounceTimer?.cancel();
_talkStartDebounceTimer?.cancel();
_amapFlutter.dispose();
_locationStream?.cancel();
// 停止TTS
@ -451,7 +437,7 @@ class ChatController extends GetxController {
}
isPlay.value = true;
} catch (e) {
Logger.error('加载异常: $e');
Logger.error('加载异常: ${e.toString()}');
}
}
@ -469,7 +455,7 @@ class ChatController extends GetxController {
isPlay.value = true;
audiourl.value = card['url'] as String;
} catch (e) {
Logger.error('加载异常: $e');
Logger.error('加载异常: ${e.toString()}');
}
}
@ -479,7 +465,7 @@ class ChatController extends GetxController {
_spotifService.pause();
isPlay.value = false;
} catch (e) {
Logger.error('加载异常: $e');
Logger.error('加载异常: ${e.toString()}');
}
}
@ -872,7 +858,7 @@ class ChatController extends GetxController {
}
// 删除数据库中的历史记录
ChatStorage.deleteMessages(sessionId: agent.id).catchError((e) {
Logger.error('清空数据库聊天记录失败: $e');
Logger.error('清空数据库聊天记录失败: ${e.toString()}');
});
}
@ -1003,4 +989,4 @@ class ChatController extends GetxController {
.abs() <
5000);
}
}
}

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

@ -103,6 +103,9 @@ class HomeController extends GetxController {
// 设置监听器监听蓝牙服务状态变化
_setupStateListeners();
// 在初始化时自动触发一次刷新设备连接状态
refreshDeviceConnection();
}
/// 设置状态监听器

11
lib/modules/jieli_test/bindings/jieli_test_binding.dart

@ -1,11 +0,0 @@
import 'package:get/get.dart';
import '../controllers/jieli_test_controller.dart';
class JieliTestBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<JieliTestController>(
() => JieliTestController(),
);
}
}

323
lib/modules/jieli_test/controllers/jieli_test_controller.dart

@ -1,323 +0,0 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/material.dart' hide ConnectionState;
import 'package:get/get.dart';
import 'package:jieli/jieli.dart';
/// 杰理测试控制器
class JieliTestController extends GetxController {
// Jieli插件实例
late Jieli jieli;
// 状态变量
var isScanning = false.obs;
var deviceInfo = '未获取'.obs;
var connectStatus = '未连接'.obs;
var currentDevice = Rx<BleDevice?>(null);
var scannedDevices = RxList<BleDevice>([]);
var wakeupReceived = false.obs;
var audioDataSize = 0.obs;
var receivedFrames = 0.obs;
var audioDataReceiving = false.obs;
var volumeCommandResult = ''.obs;
// 事件订阅
StreamSubscription? _scanSubscription;
StreamSubscription? _connectionSubscription;
StreamSubscription? _wakeupSubscription;
StreamSubscription? _audioSubscription;
StreamSubscription? _commandResultSubscription;
@override
void onInit() {
super.onInit();
_initializeJieli();
}
@override
void onClose() {
_cancelAllSubscriptions();
_disposeJieli();
super.onClose();
}
/// 取消所有订阅
void _cancelAllSubscriptions() {
_scanSubscription?.cancel();
_connectionSubscription?.cancel();
_wakeupSubscription?.cancel();
_audioSubscription?.cancel();
_commandResultSubscription?.cancel();
}
/// 初始化杰理SDK
void _initializeJieli() {
try {
jieli = Jieli();
_setupEventListeners();
} catch (e) {
// 初始化错误处理
}
}
/// 设置事件监听器
void _setupEventListeners() {
// 监听扫描结果
_scanSubscription = jieli.scanResults.listen((devices) {
scannedDevices.assignAll(devices);
});
// 监听连接状态
_connectionSubscription = jieli.connectionState.listen((state) {
switch (state) {
case ConnectionState.connected:
connectStatus.value = '已连接';
break;
case ConnectionState.disconnected:
connectStatus.value = '已断开';
currentDevice.value = null;
break;
case ConnectionState.connecting:
connectStatus.value = '连接中...';
break;
case ConnectionState.disconnecting:
connectStatus.value = '断开中...';
break;
case ConnectionState.error:
connectStatus.value = '连接错误';
break;
}
});
// 监听唤醒信号
_wakeupSubscription = jieli.wakeupStream.listen((wakeup) {
wakeupReceived.value = true;
Get.snackbar('收到唤醒信号', '耳机发送了唤醒信号');
});
// 监听音频数据
_audioSubscription = jieli.audioDataStream.listen((data) {
// 更新音频数据统计
audioDataSize.value += data.length;
receivedFrames.value++;
audioDataReceiving.value = true;
});
// 监听命令结果
_commandResultSubscription = jieli.commandResultStream.listen((result) {
// 处理音量调整命令结果
if (result.commandType == Jieli.commandTypeVolume) {
volumeCommandResult.value = result.success ? '音量调整成功' : '音量调整失败';
Get.snackbar(
result.success ? '音量调整成功' : '音量调整失败',
result.success ? '设备已更新音量' : '设备未能更新音量',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: result.success ? Colors.green.withOpacity(0.7) : Colors.red.withOpacity(0.7),
colorText: Colors.white,
duration: Duration(seconds: 2),
);
}
});
}
/// 检查蓝牙状态
Future<bool> checkBluetoothStatus() async {
try {
final isEnabled = await jieli.isBluetoothEnabled();
if (!isEnabled) {
return await jieli.requestBluetoothEnable();
}
return true;
} catch (e) {
return false;
}
}
/// 开始扫描设备
Future<void> startScan() async {
if (isScanning.value) return;
final bluetoothReady = await checkBluetoothStatus();
if (!bluetoothReady) {
Get.snackbar('蓝牙未启用', '请启用蓝牙后再试');
return;
}
try {
isScanning.value = true;
// 清空之前的设备列表
scannedDevices.clear();
// 开始扫描,10秒后自动停止
final success = await jieli.startScan(scanTimeout: 10000);
if (!success) {
isScanning.value = false;
Get.snackbar('扫描失败', '启动扫描设备失败,请检查蓝牙权限');
}
// 10秒后自动更新UI状态
Future.delayed(Duration(milliseconds: 10000), () {
isScanning.value = false;
});
} catch (e) {
isScanning.value = false;
Get.snackbar('扫描错误', e.toString());
}
}
/// 停止扫描
Future<void> stopScan() async {
if (!isScanning.value) return;
try {
await jieli.stopScan();
isScanning.value = false;
} catch (e) {
// 停止扫描错误处理
}
}
/// 连接到指定设备
Future<void> connectToDevice(BleDevice device) async {
try {
// 先断开当前连接
if (currentDevice.value != null) {
await disconnectDevice();
}
// 更新UI状态
connectStatus.value = '连接中...';
// 重置音频和唤醒相关的状态
wakeupReceived.value = false;
audioDataSize.value = 0;
receivedFrames.value = 0;
audioDataReceiving.value = false;
// 连接设备
final success = await jieli.connect(device.id);
if (success) {
// 连接成功,更新当前设备
currentDevice.value = device;
deviceInfo.value = '设备名称: ${device.name}\n设备ID: ${device.id}\nRSSI: ${device.rssi} dBm';
} else {
deviceInfo.value = '连接失败';
}
} catch (e) {
deviceInfo.value = '连接错误: ${e.toString()}';
}
}
/// 断开设备连接
Future<void> disconnectDevice() async {
if (currentDevice.value == null) return;
try {
connectStatus.value = '断开中...';
final success = await jieli.disconnect();
if (success) {
deviceInfo.value = '未连接设备';
}
} catch (e) {
// 断开连接错误处理
}
}
/// 发送开始录音命令
Future<void> sendStartRecordingCommand() async {
if (currentDevice.value == null) {
Get.snackbar('操作失败', '未连接设备');
return;
}
try {
final success = await jieli.sendStartRecordingCommand();
if (success) {
Get.snackbar('命令已发送', '已发送开始录音命令');
} else {
Get.snackbar('命令发送失败', '发送开始录音命令失败');
}
} catch (e) {
Get.snackbar('命令错误', e.toString());
}
}
/// 发送停止录音命令
Future<void> sendStopRecordingCommand() async {
if (currentDevice.value == null) {
Get.snackbar('操作失败', '未连接设备');
return;
}
try {
final success = await jieli.sendStopRecordingCommand();
if (success) {
Get.snackbar('命令已发送', '已发送停止录音命令');
} else {
Get.snackbar('命令发送失败', '发送停止录音命令失败');
}
} catch (e) {
Get.snackbar('命令错误', e.toString());
}
}
/// 增加音量
Future<void> increaseVolume() async {
if (currentDevice.value == null) {
Get.snackbar('操作失败', '未连接设备');
return;
}
try {
volumeCommandResult.value = '发送中...';
final success = await jieli.increaseVolume();
if (!success) {
Get.snackbar('命令发送失败', '发送增加音量命令失败');
volumeCommandResult.value = '发送失败';
}
} catch (e) {
Get.snackbar('命令错误', e.toString());
volumeCommandResult.value = '发送出错';
}
}
/// 减小音量
Future<void> decreaseVolume() async {
if (currentDevice.value == null) {
Get.snackbar('操作失败', '未连接设备');
return;
}
try {
volumeCommandResult.value = '发送中...';
final success = await jieli.decreaseVolume();
if (!success) {
Get.snackbar('命令发送失败', '发送减小音量命令失败');
volumeCommandResult.value = '发送失败';
}
} catch (e) {
Get.snackbar('命令错误', e.toString());
volumeCommandResult.value = '发送出错';
}
}
/// 清理资源
void _disposeJieli() {
try {
jieli.dispose();
} catch (e) {
// 释放资源错误处理
}
}
}

568
lib/modules/jieli_test/views/jieli_test_view.dart

@ -1,568 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../controllers/jieli_test_controller.dart';
import 'package:jieli/jieli.dart';
class JieliTestView extends GetView<JieliTestController> {
const JieliTestView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Scaffold(
backgroundColor: isDarkMode ? Colors.black : Colors.grey[50],
appBar: AppBar(
backgroundColor: isDarkMode ? Colors.black : Colors.white,
elevation: 0.5,
title: Text(
'杰理蓝牙测试',
style: TextStyle(
fontSize: 17.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.black,
),
),
centerTitle: true,
leading: IconButton(
icon: Icon(
Icons.arrow_back_ios,
color: isDarkMode ? Colors.white : Colors.black,
size: 20.sp,
),
onPressed: () => Get.back(),
),
),
body: Padding(
padding: EdgeInsets.all(16.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildStatusCard(isDarkMode),
SizedBox(height: 16.h),
_buildActionButtons(isDarkMode),
SizedBox(height: 16.h),
_buildDeviceInfo(isDarkMode),
SizedBox(height: 16.h),
_buildDevicesList(isDarkMode),
],
),
),
);
}
Widget _buildStatusCard(bool isDarkMode) {
return Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[900] : Colors.white,
borderRadius: BorderRadius.circular(12.r),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
'连接状态: ',
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.white : Colors.black87,
),
),
Obx(() => Text(
controller.connectStatus.value,
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.bold,
color: _getStatusColor(controller.connectStatus.value),
),
)),
],
),
SizedBox(height: 8.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Obx(() => _buildStatusChip(
label: '唤醒信号',
isActive: controller.wakeupReceived.value,
activeColor: Colors.orange,
isDarkMode: isDarkMode,
)),
Obx(() => _buildStatusChip(
label: '音频数据',
isActive: controller.audioDataReceiving.value,
activeColor: Colors.green,
isDarkMode: isDarkMode,
)),
],
),
SizedBox(height: 8.h),
Obx(() => controller.audioDataReceiving.value
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'音频数据统计:',
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.white70 : Colors.black54,
),
),
SizedBox(height: 4.h),
Text(
'已接收 ${(controller.audioDataSize.value / 1024).toStringAsFixed(2)} KB',
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.white70 : Colors.black54,
),
),
Text(
'共 ${controller.receivedFrames.value} 帧',
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.white70 : Colors.black54,
),
),
],
)
: SizedBox.shrink()
),
],
),
);
}
Widget _buildStatusChip({
required String label,
required bool isActive,
required Color activeColor,
required bool isDarkMode,
}) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h),
decoration: BoxDecoration(
color: isActive
? activeColor
: (isDarkMode ? Colors.grey[800] : Colors.grey[200]),
borderRadius: BorderRadius.circular(16.r),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 8.w,
height: 8.w,
decoration: BoxDecoration(
color: isActive ? Colors.white : Colors.grey,
shape: BoxShape.circle,
),
),
SizedBox(width: 6.w),
Text(
label,
style: TextStyle(
fontSize: 12.sp,
color: isActive
? Colors.white
: (isDarkMode ? Colors.white70 : Colors.black54),
fontWeight: isActive ? FontWeight.bold : FontWeight.normal,
),
),
],
),
);
}
Widget _buildActionButtons(bool isDarkMode) {
return Column(
children: [
Row(
children: [
Expanded(
child: Obx(() => ElevatedButton.icon(
onPressed: controller.isScanning.value
? controller.stopScan
: controller.startScan,
icon: Icon(
controller.isScanning.value ? Icons.stop : Icons.bluetooth_searching,
size: 18.sp,
),
label: Text(
controller.isScanning.value ? '停止扫描' : '扫描设备',
style: TextStyle(fontSize: 14.sp),
),
style: ElevatedButton.styleFrom(
backgroundColor: controller.isScanning.value
? Colors.red[600]
: Colors.blue[600],
padding: EdgeInsets.symmetric(vertical: 12.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
),
),
)),
),
],
),
SizedBox(height: 8.h),
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: controller.sendStartRecordingCommand,
icon: Icon(
Icons.mic,
size: 18.sp,
),
label: Text(
'开始录音',
style: TextStyle(fontSize: 14.sp),
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green[600],
padding: EdgeInsets.symmetric(vertical: 12.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
),
),
),
),
SizedBox(width: 8.w),
Expanded(
child: ElevatedButton.icon(
onPressed: controller.sendStopRecordingCommand,
icon: Icon(
Icons.stop_circle,
size: 18.sp,
),
label: Text(
'停止录音',
style: TextStyle(fontSize: 14.sp),
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red[600],
padding: EdgeInsets.symmetric(vertical: 12.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
),
),
),
),
],
),
SizedBox(height: 8.h),
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: controller.increaseVolume,
icon: Icon(
Icons.volume_up,
size: 18.sp,
),
label: Text(
'增加音量',
style: TextStyle(fontSize: 14.sp),
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue[600],
padding: EdgeInsets.symmetric(vertical: 12.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
),
),
),
),
SizedBox(width: 8.w),
Expanded(
child: ElevatedButton.icon(
onPressed: controller.decreaseVolume,
icon: Icon(
Icons.volume_down,
size: 18.sp,
),
label: Text(
'减小音量',
style: TextStyle(fontSize: 14.sp),
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange[600],
padding: EdgeInsets.symmetric(vertical: 12.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
),
),
),
),
],
),
SizedBox(height: 8.h),
Obx(() => controller.volumeCommandResult.value.isNotEmpty
? Container(
width: double.infinity,
padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 16.w),
decoration: BoxDecoration(
color: controller.volumeCommandResult.value.contains('成功')
? Colors.green[100]
: (controller.volumeCommandResult.value == '发送中...'
? Colors.blue[100]
: Colors.red[100]),
borderRadius: BorderRadius.circular(8.r),
),
child: Text(
'音量控制: ${controller.volumeCommandResult.value}',
style: TextStyle(
fontSize: 13.sp,
color: controller.volumeCommandResult.value.contains('成功')
? Colors.green[900]
: (controller.volumeCommandResult.value == '发送中...'
? Colors.blue[900]
: Colors.red[900]),
),
textAlign: TextAlign.center,
),
)
: SizedBox.shrink()
),
],
);
}
Widget _buildDeviceInfo(bool isDarkMode) {
return Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[900] : Colors.white,
borderRadius: BorderRadius.circular(12.r),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'设备信息:',
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.white : Colors.black87,
),
),
SizedBox(height: 8.h),
Container(
padding: EdgeInsets.all(8.w),
decoration: BoxDecoration(
color: isDarkMode ? Colors.black : Colors.grey[100],
borderRadius: BorderRadius.circular(8.r),
),
child: Obx(() => Text(
controller.deviceInfo.value,
style: TextStyle(
fontSize: 13.sp,
color: isDarkMode ? Colors.white70 : Colors.black54,
height: 1.4,
),
)),
),
SizedBox(height: 16.h),
Obx(() => controller.currentDevice.value != null
? ElevatedButton.icon(
onPressed: controller.disconnectDevice,
icon: Icon(
Icons.bluetooth_disabled,
size: 16.sp,
),
label: Text(
'断开连接',
style: TextStyle(fontSize: 13.sp),
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red[600],
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 10.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
),
),
)
: SizedBox.shrink()
),
],
),
);
}
Widget _buildDevicesList(bool isDarkMode) {
return Expanded(
child: Container(
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[900] : Colors.white,
borderRadius: BorderRadius.circular(12.r),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.all(16.w),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'扫描到的设备',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.black87,
),
),
Obx(() => Text(
'${controller.scannedDevices.length}个设备',
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.white70 : Colors.black54,
),
)),
],
),
),
Divider(height: 1, thickness: 1, color: isDarkMode ? Colors.grey[800] : Colors.grey[200]),
Expanded(
child: Obx(() => controller.scannedDevices.isEmpty
? Center(
child: controller.isScanning.value
? Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 16.h),
Text(
'正在扫描...',
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.white70 : Colors.black54,
),
),
],
)
: Text(
'未扫描到设备',
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.white70 : Colors.black54,
),
),
)
: ListView.separated(
padding: EdgeInsets.symmetric(vertical: 8.h),
itemCount: controller.scannedDevices.length,
separatorBuilder: (context, index) => Divider(
height: 1,
thickness: 1,
color: isDarkMode ? Colors.grey[800] : Colors.grey[200],
),
itemBuilder: (context, index) {
final device = controller.scannedDevices[index];
return ListTile(
dense: true,
title: Text(
device.name.isEmpty ? '未知设备' : device.name,
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w500,
color: isDarkMode ? Colors.white : Colors.black87,
),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'设备ID: ${device.id}',
style: TextStyle(
fontSize: 11.sp,
color: isDarkMode ? Colors.white70 : Colors.black54,
),
),
Text(
'RSSI: ${device.rssi} dBm',
style: TextStyle(
fontSize: 11.sp,
color: isDarkMode ? Colors.white70 : Colors.black54,
),
),
],
),
trailing: Obx(() => controller.currentDevice.value?.id == device.id
? Chip(
label: Text(
'已连接',
style: TextStyle(
fontSize: 12.sp,
color: Colors.white,
),
),
backgroundColor: Colors.green,
padding: EdgeInsets.symmetric(horizontal: 8.w),
)
: ElevatedButton(
onPressed: () => controller.connectToDevice(device),
child: Text(
'连接',
style: TextStyle(fontSize: 12.sp),
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green[600],
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(6.r),
),
),
),
),
contentPadding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
isThreeLine: true,
);
},
),
),
),
],
),
),
);
}
Color _getStatusColor(String status) {
switch (status) {
case '连接中...':
return Colors.orange;
case '已连接':
return Colors.green;
case '连接失败':
return Colors.red;
case '已断开':
return Colors.grey;
default:
return Colors.grey;
}
}
}

8
lib/modules/meeting/controllers/meeting_details_controller.dart

@ -258,7 +258,7 @@ class MeetingDetailsController extends GetxController
Logger.debug('taskId: $taskId');
return taskId;
} catch (e) {
Logger.error('Submit error: $e');
Logger.error('Submit error: ${e.toString()}');
rethrow;
}
}
@ -290,7 +290,7 @@ class MeetingDetailsController extends GetxController
final respDic = jsonDecode(response.body);
return respDic;
} catch (e) {
Logger.error('Query error: $e');
Logger.error('Query error: ${e.toString()}');
rethrow;
}
}
@ -322,7 +322,7 @@ class MeetingDetailsController extends GetxController
}
}
} catch (e) {
Logger.error('Error occurred: $e');
Logger.error('Error occurred: ${e.toString()}');
}
}
@ -357,4 +357,4 @@ class MeetingDetailsController extends GetxController
}
allText = text;
}
}
}

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

@ -22,7 +22,7 @@ class OtaUpgradeController extends GetxController {
final isUpdateAvailable = true.obs;
final RxDouble progress = 0.0.obs;
final JlOta jla = JlOta();
// 蓝牙服务
final _bleManager = Get.find<BleManager>();
@override
@ -53,68 +53,65 @@ class OtaUpgradeController extends GetxController {
}
Future<void> _getCurrentVersion() async {
JlOta().getPlatformVersion();
//try {
// // 添加获取设备信息的重试机制
// DeviceInfo? deviceInfo = _bleManager.deviceInfo.value;
// // 第一次检查设备信息
// if (deviceInfo == null) {
// // 显示加载提示
// isFetchingDeviceInfo.value = true;
// // 尝试重新获取设备信息
// await _bleManager.fetchDeviceInfo();
// // 等待后再次获取设备信息
// deviceInfo = _bleManager.deviceInfo.value;
// isFetchingDeviceInfo.value = false;
// // 仍然为空则抛出异常
// if (deviceInfo == null) {
// throw const FormatException('无法获取设备信息,请检查设备连接');
// }
// }
// // 检查固件信息有效性
// if (deviceInfo.firmwareVersion.isEmpty
// // ||
// // deviceInfo.firmwareProductId.isEmpty
// ) {
// throw const FormatException('设备固件信息不完整');
// }
// // 正常流程
// currentVersion.value = "0"; //deviceInfo.firmwareVersion;
// // API请求
// final response = await Api.getequipmentinfo({
// "id": "s3",
// });
// // 处理API响应
// if (response['info'] == null) {
// throw const FormatException('API 响应格式错误');
// }
// final resp = OtaConfigResp.fromJson(response);
// latestVersion.value = resp.systemversion!;
// updatepackageaddress = resp.updatepackageaddress!;
// // 版本检查
// isUpdateAvailable.value = _isNewVersionAvailable(resp.systemversion!);
// } catch (e) {
// // 统一错误处理
// errorMessage.value = e.toString();
// Get.snackbar('错误', e.toString(),
// snackPosition: SnackPosition.BOTTOM,
// backgroundColor: Colors.red,
// colorText: Colors.white);
// // 重要:重新抛出异常以便上层处理(可选)
// } finally {
// isFetchingDeviceInfo.value = false;
// }
try {
// 添加获取设备信息的重试机制
DeviceInfo? deviceInfo = _bleManager.deviceInfo.value;
// 第一次检查设备信息
if (deviceInfo == null) {
// 显示加载提示
isFetchingDeviceInfo.value = true;
// 尝试重新获取设备信息
await _bleManager.fetchDeviceInfo();
// 等待后再次获取设备信息
deviceInfo = _bleManager.deviceInfo.value;
isFetchingDeviceInfo.value = false;
// 仍然为空则抛出异常
if (deviceInfo == null) {
throw const FormatException('无法获取设备信息,请检查设备连接');
}
}
// 检查固件信息有效性
if (deviceInfo.firmwareVersion.isEmpty ||
deviceInfo.firmwareProductId.isEmpty) {
throw const FormatException('设备固件信息不完整');
}
// 正常流程
currentVersion.value = deviceInfo.firmwareVersion;
print('getequipmentinfo,id: ${deviceInfo.firmwareProductId}');
// API请求
final response = await Api.getequipmentinfo({
"id": deviceInfo.firmwareProductId,
});
// 处理API响应
if (response['info'] == null) {
throw const FormatException('API 响应格式错误');
}
final resp = OtaConfigResp.fromJson(response);
print('getequipmentinfo,resp.systemversion: ${resp.systemversion}');
latestVersion.value = "1";
updatepackageaddress = resp.updatepackageaddress!;
// 版本检查
isUpdateAvailable.value = _isNewVersionAvailable(resp.systemversion!);
} catch (e) {
// 统一错误处理
errorMessage.value = e.toString();
Get.snackbar('错误', e.toString(),
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white);
// 重要:重新抛出异常以便上层处理(可选)
} finally {
isFetchingDeviceInfo.value = false;
}
}
// 版本比较方法优化
@ -295,6 +292,7 @@ class OtaUpgradeController extends GetxController {
// 开始升级
void _installApp(String path) async {
jla.getPlatformVersion();
// JlOtaPlugin()
// final File file = File(path);
// if (await file.exists()) {

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

@ -9,6 +9,7 @@ import '../../../data/services/bluetooth_manager.dart';
import 'package:classic_bluetooth/classic_bluetooth.dart' show BluetoothDevice;
import '../../../data/services/ble_manager.dart';
import 'package:ble_service/ble_service.dart';
import 'package:flutter/material.dart';
/// 配对控制器,负责检查蓝牙权限、状态,并处理配对流程
class PairingController extends GetxController {
@ -83,6 +84,11 @@ class PairingController extends GetxController {
isLoading.value = true;
statusDescription.value = 'checkingPermissions'.tr;
// 在iOS上,延迟一下再请求权限,避免多权限冲突
if (Platform.isIOS) {
await Future.delayed(Duration(milliseconds: 800));
}
// 检查并请求权限
if (!await _checkAndRequestPermissions()) {
statusDescription.value = 'permissionDenied'.tr;
@ -96,7 +102,7 @@ class PairingController extends GetxController {
// 检查蓝牙状态并刷新设备
await _checkBluetoothAndRefresh();
} catch (e) {
Logger.error('初始化配对控制器失败: $e');
Logger.error('初始化配对控制器失败: ${e.toString()}');
statusDescription.value = 'initializationFailure'.tr;
} finally {
isLoading.value = false;
@ -170,7 +176,7 @@ class PairingController extends GetxController {
await _bleService.registerBackgroundScan();
_autoNavigateToHome();
} catch (e) {
Logger.error('检查设备配对状态失败: $e');
Logger.error('检查设备配对状态失败: ${e.toString()}');
}
}
@ -186,27 +192,72 @@ class PairingController extends GetxController {
if (Platform.isAndroid) {
permissionsToRequest.add(Permission.locationWhenInUse);
permissionsToRequest.add(Permission.microphone);
// permissionsToRequest.add(Permission.microphone);
if (await _getAndroidSdkVersion() >= 31) {
permissionsToRequest.add(Permission.bluetoothScan);
permissionsToRequest.add(Permission.bluetoothConnect);
}
} else if (Platform.isIOS) {
permissionsToRequest.add(Permission.bluetooth);
// iOS上单独处理蓝牙权限请求,避免冲突
PermissionStatus status = await Permission.bluetooth.status;
if (status.isPermanentlyDenied) {
Logger.warning('iOS蓝牙权限状态为permanentlyDenied,这可能是iOS系统误报');
// 在iOS上打开蓝牙设置对话框
Get.dialog(
AlertDialog(
title: Text('需要蓝牙权限'),
content: Text('请在系统设置中手动开启蓝牙权限,以便连接设备'),
actions: [
TextButton(
onPressed: () => Get.back(),
child: Text('稍后'),
),
TextButton(
onPressed: () {
Get.back();
openAppSettings();
},
child: Text('前往设置'),
),
],
),
barrierDismissible: false,
);
return false;
} else if (!status.isGranted) {
// 单独请求蓝牙权限
PermissionStatus result = await Permission.bluetooth.request();
if (!result.isGranted) {
Logger.warning('iOS蓝牙权限申请失败: $result');
return false;
}
}
// iOS不再添加到批量请求列表
// permissionsToRequest.add(Permission.bluetooth);
}
statuses = await permissionsToRequest.request();
bool allGranted = true;
statuses.forEach((permission, status) {
if (!status.isGranted) {
allGranted = false;
Logger.warning('权限 ${permission.toString()} 未被授予: $status');
// 如果还有其他权限需要请求
if (permissionsToRequest.isNotEmpty) {
statuses = await permissionsToRequest.request();
bool allGranted = true;
statuses.forEach((permission, status) {
if (!status.isGranted) {
allGranted = false;
Logger.warning('权限 ${permission.toString()} 未被授予: $status');
}
});
if (!allGranted) {
return false;
}
});
}
_hasPermissions.value = allGranted;
return allGranted;
_hasPermissions.value = true;
return true;
}
// 辅助方法获取Android SDK版本
@ -356,7 +407,7 @@ class PairingController extends GetxController {
_updateStatusDescription();
}
} catch (e) {
Logger.error('Companion设备关联过程发生错误: $e');
Logger.error('Companion设备关联过程发生错误: ${e.toString()}');
_isAssociating = false;
_userRejectedAssociation.value = true;
_updateStatusDescription();
@ -386,7 +437,7 @@ class PairingController extends GetxController {
final result = await BleManager.to.startAgentService();
Logger.info(result ? 'BLE后台服务启动成功' : 'BLE后台服务启动失败');
} catch (e) {
Logger.error('启动BLE后台服务发生错误: $e');
Logger.error('启动BLE后台服务发生错误: ${e.toString()}');
}
}
@ -396,7 +447,7 @@ class PairingController extends GetxController {
final result = await BleManager.to.stopAgentService();
Logger.info(result ? 'BLE后台服务停止成功' : 'BLE后台服务停止失败');
} catch (e) {
Logger.error('停止BLE后台服务发生错误: $e');
Logger.error('停止BLE后台服务发生错误: ${e.toString()}');
}
}
@ -424,13 +475,21 @@ class PairingController extends GetxController {
AppSettings.openAppSettings(type: AppSettingsType.bluetooth);
}
} catch (e) {
Logger.error('打开设置失败: $e');
Logger.error('打开设置失败: ${e.toString()}');
}
}
/// 打开应用设置
Future<void> openAppSettings() async {
await AppSettings.openAppSettings();
try {
await AppSettings.openAppSettings();
// 返回后重新检查权限状态
Future.delayed(Duration(milliseconds: 500), () {
reinitializeBluetooth();
});
} catch (e) {
Logger.error('打开应用设置失败: ${e.toString()}');
}
}
/// 重新开始关联
@ -447,4 +506,33 @@ class PairingController extends GetxController {
_isNavigatingToHome = true;
Get.offAllNamed(Routes.home);
}
}
/// 重新初始化蓝牙
Future<void> reinitializeBluetooth() async {
try {
isLoading.value = true;
statusDescription.value = '正在重新初始化蓝牙...';
// iOS上尝试重置权限状态
if (Platform.isIOS) {
await Future.delayed(Duration(milliseconds: 500));
if (await Permission.bluetooth.isPermanentlyDenied) {
Logger.info("iOS蓝牙权限仍处于永久拒绝状态,尝试直接使用蓝牙功能");
// 在永久拒绝状态下,尝试使用蓝牙功能,iOS可能会再次弹出系统权限请求
_bluetoothService.isBluetoothEnabled();
}
}
// 重新检查权限
if (await _checkAndRequestPermissions()) {
// 重新检查蓝牙状态
await _checkBluetoothAndRefresh();
}
} catch (e) {
Logger.error('重新初始化蓝牙失败: ${e.toString()}');
} finally {
isLoading.value = false;
_updateStatusDescription();
}
}
}

43
lib/modules/profile/controllers/profile_controller.dart

@ -1,3 +1,4 @@
import 'package:dio/dio.dart' as dio;
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:flutter/material.dart';
@ -14,6 +15,7 @@ import 'package:dio/dio.dart';
import '../../../routes/app_routes.dart';
import '../../../modules/login/controllers/googleLogin.dart';
import '../../../modules/login/controllers/facebookLogin.dart';
//import 'package:http_parser/http_parser.dart'; // 新增此导入
class ProfileController extends GetxController {
static ProfileController get to => Get.find();
@ -150,25 +152,29 @@ class ProfileController extends GetxController {
// 更新用户信息到后端
Future<void> _syncWithBackend() async {
try {
// 准备数据
Map<String, dynamic> data = {
"image": "", // 默认空字符串
"language": currentLanguage.value, //当前用户语言
"name": userInfo.value.name, //当前用户名
};
// 如果有头像,转换为base64
// 创建FormData对象用于上传
dio.FormData formData = dio.FormData();
// 添加基本用户信息
formData.fields.add(MapEntry('language', currentLanguage.value));
formData.fields.add(MapEntry('name', userInfo.value.name));
// 如果有头像,添加到FormData
if (localAvatar.value.isNotEmpty) {
//如果本地头像路径不为空,即代表有头像
try {
File imageFile = File(localAvatar.value);
if (await imageFile.exists()) {
List<int> imageBytes = await imageFile.readAsBytes();
String base64Image = base64Encode(imageBytes);
data["image"] = base64Image;
// // 验证base64数据 - 将base64转回图片并保存到本地
// await _verifyBase64Image(base64Image);
// 添加文件到FormData
formData.files.add(
MapEntry(
'file',
await dio.MultipartFile.fromFile(
imageFile.path,
filename: 'avatar.jpg',
),
),
);
Logger().d('已添加头像文件到请求');
} else {
Logger().e('头像文件不存在');
}
@ -177,9 +183,10 @@ class ProfileController extends GetxController {
}
}
Logger().d('准备更新用户信息: $data');
// 调用API
final response = await Api.updateUserSetting(data);
Logger().d('准备更新用户信息: ${formData.fields}');
// 调用API,使用FormData
final response = await Api.updateUserSetting(formData);
Logger().d('--更新用户信息响应: $response');
if (response != null) {
Logger().d('更新成功: $response');

18
lib/modules/speech_test/controllers/speech_test_controller.dart

@ -83,7 +83,7 @@ class SpeechTestController extends GetxController {
} catch (e) {
ttsStatus.value = '初始化失败';
recognitionStatus.value = '初始化失败';
Logger.error('语音测试初始化失败: $e');
Logger.error('语音测试初始化失败: ${e.toString()}');
} finally {
isProcessing.value = false;
}
@ -198,7 +198,7 @@ class SpeechTestController extends GetxController {
// 重新初始化服务
await _initServices();
} catch (e) {
Logger.error('切换语音服务类型失败: $e');
Logger.error('切换语音服务类型失败: ${e.toString()}');
} finally {
isProcessing.value = false;
}
@ -242,7 +242,7 @@ class SpeechTestController extends GetxController {
}
} catch (e) {
ttsStatus.value = '播放异常: $e';
Logger.error('单次播放异常: $e');
Logger.error('单次播放异常: ${e.toString()}');
} finally {
isProcessing.value = false;
}
@ -270,7 +270,7 @@ class SpeechTestController extends GetxController {
await _ttsService.flushStream();
} catch (e) {
ttsStatus.value = '连续播放异常: $e';
Logger.error('连续播放异常: $e');
Logger.error('连续播放异常: ${e.toString()}');
} finally {
isProcessing.value = false;
}
@ -284,7 +284,7 @@ class SpeechTestController extends GetxController {
await _ttsService.stop();
} catch (e) {
ttsStatus.value = '停止播放异常: $e';
Logger.error('停止播放异常: $e');
Logger.error('停止播放异常: ${e.toString()}');
}
}
@ -324,7 +324,7 @@ class SpeechTestController extends GetxController {
}
} catch (e) {
recognitionStatus.value = '识别异常: $e';
Logger.error('一次性识别异常: $e');
Logger.error('一次性识别异常: ${e.toString()}');
} finally {
isProcessing.value = false;
}
@ -351,7 +351,7 @@ class SpeechTestController extends GetxController {
_setupAsrEventListener(stream);
} catch (e) {
recognitionStatus.value = '启动连续识别异常: $e';
Logger.error('启动连续识别异常: $e');
Logger.error('启动连续识别异常: ${e.toString()}');
isListening.value = false;
intermediateResult.value = '';
finalResult.value = '';
@ -377,7 +377,7 @@ class SpeechTestController extends GetxController {
intermediateResult.value = '';
} catch (e) {
recognitionStatus.value = '停止连续识别异常: $e';
Logger.error('停止连续识别异常: $e');
Logger.error('停止连续识别异常: ${e.toString()}');
}
}
@ -413,4 +413,4 @@ class SpeechTestController extends GetxController {
super.onClose();
}
}
}

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

@ -1,5 +1,4 @@
import 'dart:async';
import 'package:amap_flutter/amap_flutter.dart';
import 'package:deep_voice/data/models/appconfig.dart';
import 'package:deep_voice/data/models/appconfig_model.dart';
import 'package:flutter/material.dart';
@ -9,6 +8,10 @@ import '../../../data/models/user_Info.dart';
import '../../../routes/app_routes.dart';
import '../../../core/themes/theme_service.dart';
import '../../../data/services/network/api.dart';
<<<<<<< HEAD
=======
import 'package:amap_service/amap_service.dart';
>>>>>>> e77ef1ff0198bb21316fa0e068b78d34f83b013c
class SplashController extends GetxController {
final GetStorage _storage = GetStorage();
@ -32,21 +35,21 @@ class SplashController extends GetxController {
WidgetsBinding.instance.addPostFrameCallback((_) {
_startLoadingAnimation();
_loadAppResources();
_initMapSDK();
// _initMapSDK();
});
}
// 初始化地图SDK
void _initMapSDK() {
try {
AmapFlutter().initSdk(
iosKey: "a4a1394fe817c2f86a424b897b4a9af4",
androidKey: "e5fdc9605eabdeb5626f18f5721f343d",
);
} catch (e) {
print('地图SDK初始化失败: $e');
}
}
// void _initMapSDK() {
// try {
// AmapFlutter().initSdk(
// iosKey: "a4a1394fe817c2f86a424b897b4a9af4",
// androidKey: "e5fdc9605eabdeb5626f18f5721f343d",
// );
// } catch (e) {
// print('地图SDK初始化失败: $e');
// }
// }
// 进度条动画(持续2秒)
void _startLoadingAnimation() {

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

@ -175,7 +175,7 @@ class TranslationController extends GetxController {
Logger.info('所有服务初始化完成');
} catch (e) {
Logger.error('初始化服务失败: $e');
Logger.error('初始化服务失败: ${e.toString()}');
}
}
@ -252,7 +252,7 @@ class TranslationController extends GetxController {
recognitionStream.listen(_handleRecognitionEvent);
} catch (e) {
isRecognizing.value = false;
Logger.error('启动语音识别失败: $e');
Logger.error('启动语音识别失败: ${e.toString()}');
}
}
@ -307,7 +307,7 @@ class TranslationController extends GetxController {
currentSessionId = null;
} catch (e) {
Logger.error('停止语音识别失败: $e');
Logger.error('停止语音识别失败: ${e.toString()}');
}
}
@ -445,7 +445,7 @@ class TranslationController extends GetxController {
}
}
} catch (e) {
Logger.error('翻译失败: $e');
Logger.error('翻译失败: ${e.toString()}');
} finally {
isTranslating.value = false;
}
@ -468,7 +468,7 @@ class TranslationController extends GetxController {
await _ttsService.setVoice(voiceName);
await _ttsService.speakOnce(text);
} catch (e) {
Logger.error('播放翻译失败: $e');
Logger.error('播放翻译失败: ${e.toString()}');
}
}
@ -482,7 +482,7 @@ class TranslationController extends GetxController {
_translationDebounceTimer?.cancel();
await _ttsService.stop();
} catch (e) {
Logger.error('停止操作失败: $e');
Logger.error('停止操作失败: ${e.toString()}');
}
}
@ -497,7 +497,7 @@ class TranslationController extends GetxController {
await _ttsService.setVoice(voiceName);
await _ttsService.speakOnce(item.translatedText);
} catch (e) {
Logger.error('播放翻译项失败: $e');
Logger.error('播放翻译项失败: ${e.toString()}');
}
}
@ -516,7 +516,7 @@ class TranslationController extends GetxController {
await _ttsService.setVoice(voiceName);
await _ttsService.speakOnce(text);
} catch (e) {
Logger.error('播放文本失败: $e');
Logger.error('播放文本失败: ${e.toString()}');
}
}
@ -531,7 +531,7 @@ class TranslationController extends GetxController {
curve: Curves.easeOut,
);
} catch (e) {
Logger.error('滚动到底部失败: $e');
Logger.error('滚动到底部失败: ${e.toString()}');
}
}
}
@ -629,7 +629,7 @@ class TranslationController extends GetxController {
Logger.info('ASR服务重新初始化成功');
} catch (e) {
Logger.error('重新初始化ASR服务失败: $e');
Logger.error('重新初始化ASR服务失败: ${e.toString()}');
}
}
@ -656,7 +656,7 @@ class TranslationController extends GetxController {
});
} catch (e) {
_storage.remove(_historyKey);
Logger.error('加载翻译历史失败: $e');
Logger.error('加载翻译历史失败: ${e.toString()}');
}
}
}
@ -673,7 +673,7 @@ class TranslationController extends GetxController {
translationHistory.map((item) => item.toJson()).toList();
_storage.write(_historyKey, jsonEncode(historyJson));
} catch (e) {
Logger.error('保存翻译历史失败: $e');
Logger.error('保存翻译历史失败: ${e.toString()}');
}
}
@ -764,4 +764,4 @@ class TranslationItem {
return languageManager.getChineseNameByAsrCode(targetLanguageCode) ??
'未知语言';
}
}
}

8
lib/modules/translation/controllers/translation_detail_controller.dart

@ -58,7 +58,7 @@ class TranslationDetailController extends GetxController {
loadSavedSummary();
});
} catch (e) {
Logger.error('初始化详情控制器失败: $e');
Logger.error('初始化详情控制器失败: ${e.toString()}');
} finally {
isLoading.value = false;
}
@ -72,7 +72,7 @@ class TranslationDetailController extends GetxController {
summaryResult.value = savedSummary;
}
} catch (e) {
Logger.error('加载摘要失败: $e');
Logger.error('加载摘要失败: ${e.toString()}');
}
}
@ -83,7 +83,7 @@ class TranslationDetailController extends GetxController {
try {
_storage.write(_getSummaryKey(), summary);
} catch (e) {
Logger.error('保存摘要失败: $e');
Logger.error('保存摘要失败: ${e.toString()}');
}
}
@ -174,7 +174,7 @@ class TranslationDetailController extends GetxController {
return result;
} catch (e) {
final errorMessage = '生成摘要失败: ${e.toString()}';
Logger.error(errorMessage);
Logger.error('TranslationDetailController', errorMessage);
summaryResult.value = errorMessage;
return errorMessage;
} finally {

9
lib/routes/app_pages.dart

@ -29,8 +29,6 @@ import '../modules/pairing/bindings/pairing_binding.dart';
import './app_routes.dart';
import '../modules/settings/views/settings_view.dart';
import '../modules/settings/bindings/settings_binding.dart';
import '../modules/jieli_test/views/jieli_test_view.dart';
import '../modules/jieli_test/bindings/jieli_test_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';
@ -162,13 +160,6 @@ abstract class AppPages {
transition: Transition.rightToLeft,
transitionDuration: Duration(milliseconds: 250),
),
GetPage(
name: Routes.jieliTest,
page: () => const JieliTestView(),
binding: JieliTestBinding(),
transition: Transition.rightToLeft,
transitionDuration: Duration(milliseconds: 250),
),
GetPage(
name: Routes.opusTest,
page: () => const OpusTestView(),

4
local_plugins/agent_service/android/build.gradle.kts

@ -36,7 +36,6 @@ android {
dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.6.10")
implementation("androidx.core:core-ktx:1.8.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.0")
implementation("androidx.work:work-runtime-ktx:2.8.1")
implementation("androidx.media:media:1.6.0")
@ -45,8 +44,9 @@ dependencies {
// 依赖本地其他插件
add("compileOnly", project(":azure_speech"))
add("compileOnly", project(":open_ai_service"))
add("compileOnly", project(":deepvoice_utils"))
add("compileOnly", project(":chat_storage"))
add("compileOnly", project(":ble_service"))
add("compileOnly", project(":music_service"))
add("compileOnly", project(":amap_service"))
add("compileOnly", project(":speech"))
}

13
local_plugins/agent_service/android/settings.gradle.kts

@ -1,15 +1,2 @@
rootProject.name = "agent_service"
// 包含项目依赖
include(":azure_speech")
include(":open_ai_service")
include(":deepvoice_utils")
include(":chat_storage")
include(":music_service")
// 设置项目路径
project(":azure_speech").projectDir = file("../../azure_speech/android")
project(":open_ai_service").projectDir = file("../../open_ai_service/android")
project(":deepvoice_utils").projectDir = file("../../deepvoice_utils/android")
project(":chat_storage").projectDir = file("../../chat_storage/android")
project(":music_service").projectDir = file("../../music_service/android")

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

@ -4,11 +4,11 @@ import android.content.Context
import org.json.JSONArray
import org.json.JSONObject
import com.yunqiinnovation.azure_speech.AzureAsrHelper
import com.yunqiinnovation.azure_speech.AzureTtsHelper
import com.yunqiinnovation.open_ai_service.OpenAIService
import com.yunqiinnovation.chat_storage.ChatStorageHelper
import com.yunqiinnovation.music_service.MusicServiceStarter
import com.yunqiinnovation.deepvoice_utils.FileLogger
import com.yunqiinnovation.amap_service.NavigationServiceStarter
import android.util.Log
import io.flutter.plugin.common.MethodChannel
import kotlinx.coroutines.*
import java.util.Collections
@ -18,6 +18,10 @@ import android.media.MediaPlayer
import com.yunqiinnovation.agent_service.R
import android.os.Handler
import android.os.Looper
import com.deep_voice.speech.tts.TtsEvent
import com.deep_voice.speech.tts.TtsEventListener
import com.deep_voice.speech.tts.TtsEventType
import com.yunqiinnovation.azure_speech.AzureTtsHelper
/**
* 代理服务事件监听接口
*/
@ -41,22 +45,17 @@ object AgentService : CoroutineScope {
// 协程相关
private val job = SupervisorJob()
override val coroutineContext: CoroutineContext
get() = Dispatchers.IO + job
get() = Dispatchers.Main + job
// 上下文和监听器
private lateinit var context: Context
private val listeners = Collections.synchronizedSet(mutableSetOf<AgentServiceListener>())
// 配置参数
private var azureSpeechKey: String = ""
private var azureSpeechRegion: String = ""
private var openaiApiKey: String = ""
private var openaiBaseUrl: String = ""
private var openaiModel: String = "gpt-3.5-turbo"
// Azure服务
private var azureAsrHelper: AzureAsrHelper? = null
private var azureTtsHelper: AzureTtsHelper? = null
// 使用AzureTtsHelper作为唯一的TTS实现
private var ttsService: AzureTtsHelper? = null
// OpenAI服务
private lateinit var openAIService: OpenAIService
@ -99,17 +98,63 @@ object AgentService : CoroutineScope {
*/
private fun initSystemPrompt() {
systemPrompt = """
你是一个友好、专业的语音助手,名叫"小语"。你的目标是通过对话为用户提供帮助、解答问题和完成任务。
遵循以下指导原则:
1. 保持简短精炼的回答,因为用户是通过语音与你交流
2. 优先使用中文回复,除非用户明确要求使用其他语言
3. 当用户问题不明确时,礼貌地请求更多信息
4. 避免过长的列表,尽量将信息分成小段
5. 不要使用需要视觉展示的元素(如表格、图表或代码块)
6. 记住用户之前的对话内容,保持对话连贯
7. 如果用户发送了图片,请根据图片内容和文字要求回答问题
你不仅可以回答知识性问题,还可以帮助用户设置提醒、提供建议,或进行轻松愉快的对话。
无论遇到什么问题,都要尽力以温暖、贴心的语气提供最佳帮助。
你是小言,一个具备专业能力的智能助手,需根据用户场景灵活切换回答模式,确保服务精准高效。以下是不同场景的具体要求:
一、知识类问题回答规范
核心目标:用简洁语言提供准确信息,兼顾易读性与专业性。
1. 内容细节处理
• 精准性优先:围绕问题主体,核心信息需全面、准确,避免模糊表述或错误。
◦ 例:回答"地球公转周期"时,需明确"约365.25天"而非"一年左右"。
• 背景与类比:对复杂概念(如"量子纠缠"),可补充关键背景("量子力学中的现象")并类比("类似远距离同步的状态"),帮助用户快速理解。
• 需求宽泛时:若问题涉及范围广(如"介绍人工智能"),先概述主要领域(技术、应用、影响),再分点简要说重点,避免信息过载。
• 禁止过度延伸:非用户主动询问,不添加无关内容(如问"光速"时,不展开讨论"相对论")。
2. 格式排版规则
• 标题与重点:章节标题、关键结论需加粗(如一、核心原理),突出层级。
• 列表使用:
◦ 有序列表(1. 2. 3.):用于步骤、顺序类内容(如"实验流程")。
◦ 无序列表(- ):用于并列项(如"优点:- 高效 - 精准")。
◦ 禁止嵌套列表:如需多层级,用**三级标题(###)**分隔,下接一级列表。
• 段落处理:辅助解释用自然段,每段不超过3行,避免大段文字堆砌。
二、文案创作与内容生成规范
核心目标:根据体裁需求,平衡丰富度与阅读体验,确保逻辑流畅、重点突出。
1. 篇幅与内容要求
• 场景适配:
◦ 短文案(如标语、摘要):控制在50字内,用词简练有力。
◦ 长内容(如报告、故事):需结构清晰,每部分围绕核心展开,适当添加案例/数据支撑。
• 避免极端化:既不堆砌空泛词汇,也不遗漏关键信息。
2. 格式与排版
• 默认体裁:无特殊要求时用自然段,适当换行分隔话题。
• 结构化体裁(如策划案、简历):
◦ 用**分级标题**划分板块。
◦ 关键数据/结论加粗。
◦ 并列信息用列表呈现。
三、聊天陪伴与情感互动规范
核心目标:营造轻松友好的对话氛围,成为用户可信赖的交流伙伴。
1. 沟通风格设定
• 人格化表达:避免机械回答,用口语化词汇,适当加入表情符号增强亲切感。
• 主动引导话题:根据用户输入延伸讨论。
• 适度分享:自然提及日常,拉近距离。
2. 情绪识别与回应
• 捕捉潜在需求:识别情绪,先共情再询问细节。
• 针对性建议:结合用户处境提供具体方案。
• 积极价值观:传递乐观态度,避免消极表述。
四、能力与工具使用说明
1. 内容处理能力:图片识别、可执行总结、翻译、分析等任务。
2. 信息搜索能力:实时数据、新闻、专业知识等需搜索时,会明确告知用户。
五、特殊场景处理规则
1. 安全与合规:禁止回答敏感问题,遇此类问题需礼貌拒绝。
2. 用户指令优先:若用户明确要求特定风格,优先遵循其需求。
3. 多轮澄清机制:若问题模糊,需主动询问细节,避免假设性回答。
""".trimIndent()
}
/**
@ -128,85 +173,156 @@ object AgentService : CoroutineScope {
this.openAIService = OpenAIService(context)
this.chatStorageHelper = ChatStorageHelper(context)
this.audioPlayer = AudioPlayer(context)
// 从配置中获取参数
azureSpeechKey = config["azureSpeechKey"] as? String ?: ""
azureSpeechRegion = config["azureSpeechRegion"] as? String ?: ""
openaiApiKey = config["openaiApiKey"] as? String ?: ""
openaiBaseUrl = config["openaiBaseUrl"] as? String ?: ""
openaiModel = config["openaiModel"] as? String ?: "gpt-3.5-turbo"
val mcpServer = config["mcpServer"] as? String ?: ""
// 设置系统提示词
val customSystemPrompt = config["systemPrompt"] as? String
if (!customSystemPrompt.isNullOrBlank()) {
systemPrompt = customSystemPrompt
} else {
initSystemPrompt()
}
// 检查必要参数
if (azureSpeechKey.isEmpty() || azureSpeechRegion.isEmpty() || openaiApiKey.isEmpty()) {
FileLogger.e(TAG, "初始化失败:关键配置参数缺失")
return false
}
// val customSystemPrompt = config["systemPrompt"] as? String
// if (!customSystemPrompt.isNullOrBlank()) {
// systemPrompt = customSystemPrompt
// } else {
// initSystemPrompt()
// }
initSystemPrompt()
// 初始化Azure ASR
azureAsrHelper = AzureAsrHelper(context).apply {
initialize(
subscriptionKey = azureSpeechKey,
region = azureSpeechRegion,
subscriptionKey = config["azureSpeechKey"]?.toString() ?: "",
region = config["azureSpeechRegion"]?.toString() ?: "",
audioSourceType = if (isExternalActive) AzureAsrHelper.AudioSourceType.EXTERNAL
else AzureAsrHelper.AudioSourceType.MICROPHONE
)
}
// 初始化Azure TTS
azureTtsHelper = AzureTtsHelper(context).apply {
initialize(
subscriptionKey = azureSpeechKey,
region = azureSpeechRegion
)
// 设置TTS回调
setTtsCallback(object : AzureTtsHelper.TtsCallback {
override fun onSynthesisStarted() {
isTtsSpeaking = true
restartIdleCheck()
sendEvent("tts_started", mapOf("status" to "started"))
}
override fun onSynthesizing() {}
override fun onSynthesisCompleted() {
isTtsSpeaking = false
restartIdleCheck()
sendEvent("tts_completed", mapOf("status" to "completed"))
}
override fun onSynthesisCanceled() {
isTtsSpeaking = false
restartIdleCheck()
sendEvent("tts_canceled", mapOf("status" to "canceled"))
}
})
}
// 初始化TTS服务
initializeTtsEngine(config)
// 初始化OpenAI服务
openAIService.initialize(openaiApiKey, openaiBaseUrl, openaiModel, mcpServer)
openAIService.initialize(
config["openaiApiKey"]?.toString() ?: "",
config["openaiBaseUrl"]?.toString() ?: "",
config["openaiModel"]?.toString() ?: "",
config["mcpServer"]?.toString() ?: ""
)
// 加载最近的聊天记录
loadChatHistory()
isInitialized = true
FileLogger.d(TAG, "代理服务初始化成功")
Log.d(TAG, "代理服务初始化成功")
return true
} catch (e: Exception) {
FileLogger.e(TAG, "初始化失败: ${e.message}")
Log.e(TAG, "初始化失败: ${e.message}")
return false
}
}
/**
* 释放所有资源
*/
fun dispose() {
try {
stopRecognition()
stopTts()
stopAiStream()
stopIdleCheck()
audioPlayer?.release()
audioPlayer = null
azureAsrHelper?.dispose()
// 释放TTS服务
ttsService?.release()
ttsService = null
job.cancel()
clearListeners()
isInitialized = false
Log.d(TAG, "代理服务资源已释放")
} catch (e: Exception) {
Log.e(TAG, "释放资源异常: ${e.message}")
}
}
/**
* 初始化TTS引擎,只使用火山语音引擎
*/
private fun initializeTtsEngine(config: Map<String, Any>) {
// 停止当前TTS服务(如果有)
if (ttsService != null) {
ttsService?.stop()
ttsService = null
}
try {
// 从配置中获取TTS参数
val ttsAppId = config["volcanoAppId"]?.toString() ?: ""
val ttsAppToken = config["volcanoToken"]?.toString() ?: ""
val ttsLanguage = config["ttsLanguage"]?.toString() ?: "zh-CN"
// 创建并初始化AzureTtsHelper
val azureTtsHelper = AzureTtsHelper(context)
ttsService = azureTtsHelper
// 初始化Azure TTS
val success = azureTtsHelper.initialize(
ttsAppId = "", // Azure TTS不需要appId
ttsAppToken = config["azureSpeechKey"]?.toString() ?: "", // Azure需要subscription key
ttsResource = config["azureSpeechRegion"]?.toString() ?: "", // Azure需要region信息
language = ttsLanguage
)
if (success) {
Log.d(TAG, "Azure TTS服务初始化成功")
} else {
Log.e(TAG, "Azure TTS服务初始化失败")
}
// 添加TTS事件监听
ttsService?.addListener(object : TtsEventListener {
override fun onEvent(event: TtsEvent) {
when (event.type) {
TtsEventType.SYNTHESIS_STARTED -> {
isTtsSpeaking = true
restartIdleCheck()
sendEvent("tts_started", mapOf("status" to "started"))
}
TtsEventType.SYNTHESIS_COMPLETED -> {
isTtsSpeaking = false
restartIdleCheck()
sendEvent("tts_completed", mapOf("status" to "completed"))
}
TtsEventType.SYNTHESIS_CANCELED -> {
isTtsSpeaking = false
restartIdleCheck()
sendEvent("tts_canceled", mapOf("status" to "canceled"))
}
TtsEventType.ERROR -> {
isTtsSpeaking = false
restartIdleCheck()
val params = event.params
val code = params["errorCode"] as? String ?: "UNKNOWN_ERROR"
val message = params["errorMessage"] as? String ?: "未知错误"
sendEvent("error", mapOf(
"code" to "TTS_ERROR",
"message" to "语音合成错误: $code, $message"
))
}
else -> {
// 处理其他类型的事件
Log.d(TAG, "TTS事件: ${event.type}")
}
}
}
})
Log.d(TAG, "TTS服务配置完成")
} catch (e: Exception) {
Log.e(TAG, "初始化TTS引擎失败: ${e.message}")
}
}
/**
* 添加事件监听器
* @param listener 要添加的监听器
@ -249,7 +365,7 @@ object AgentService : CoroutineScope {
} catch (e: CancellationException) {
// 协程被取消,正常行为
} catch (e: Exception) {
FileLogger.e(TAG, "空闲检测异常: ${e.message}")
Log.e(TAG, "空闲检测异常: ${e.message}")
}
}
}
@ -277,7 +393,7 @@ object AgentService : CoroutineScope {
*/
fun startRecognition(isExternalActive: Boolean): Boolean {
if (!isInitialized) {
FileLogger.e(TAG, "服务未初始化")
Log.e(TAG, "服务未初始化")
return false
}
@ -285,7 +401,7 @@ object AgentService : CoroutineScope {
// 检查录音权限
if (!isExternalActive && !checkRecordAudioPermission()) {
FileLogger.e(TAG, "需要录音权限")
Log.e(TAG, "需要录音权限")
sendEvent("error", mapOf("code" to "PERMISSION_DENIED", "message" to "需要录音权限"))
return false
}
@ -299,7 +415,7 @@ object AgentService : CoroutineScope {
} else {
AzureAsrHelper.AudioSourceType.MICROPHONE
}
FileLogger.d(TAG, "开始语音识别, 音频源类型: $audioSourceType")
Log.d(TAG, "开始语音识别, 音频源类型: $audioSourceType")
azureAsrHelper?.startContinuousRecognition(object : AzureAsrHelper.ContinuousRecognizeCallback {
override fun onRecognizing(recognizing: String, detectedLanguage: String) {
if (recognizing.isNotEmpty()) {
@ -371,7 +487,7 @@ object AgentService : CoroutineScope {
isRecognitionActive = false
stopIdleCheck()
BleService.closeCodec()
FileLogger.e(TAG, "语音识别错误: $error")
Log.e(TAG, "语音识别错误: $error")
sendEvent("error", mapOf(
"code" to "RECOGNITION_ERROR",
"message" to error
@ -381,7 +497,7 @@ object AgentService : CoroutineScope {
return true
} catch (e: Exception) {
isRecognitionActive = false
FileLogger.e(TAG, "启动语音识别失败: ${e.message}")
Log.e(TAG, "启动语音识别失败: ${e.message}")
sendEvent("error", mapOf(
"code" to "RECOGNITION_START_ERROR",
"message" to e.message.toString()
@ -404,9 +520,9 @@ object AgentService : CoroutineScope {
BleService.closeCodec()
isRecognitionActive = false
stopIdleCheck()
FileLogger.d(TAG, "语音识别已停止")
Log.d(TAG, "语音识别已停止")
} catch (e: Exception) {
FileLogger.e(TAG, "停止语音识别异常: ${e.message}")
Log.e(TAG, "停止语音识别异常: ${e.message}")
isRecognitionActive = false
stopIdleCheck()
}
@ -446,9 +562,9 @@ object AgentService : CoroutineScope {
openAIService.cancelCurrentStream()
// 记录日志
FileLogger.d(TAG, "AI流输出已停止")
Log.d(TAG, "AI流输出已停止")
} catch (e: Exception) {
FileLogger.e(TAG, "停止AI流输出异常", e)
Log.e(TAG, "停止AI流输出异常", e)
// 确保状态被重置,即使发生异常
isAiStreaming = false
currentAiJob = null
@ -461,13 +577,13 @@ object AgentService : CoroutineScope {
*/
fun processTextInput(text: String, speakResponse: Boolean = false): Boolean {
if (!isInitialized) {
FileLogger.e(TAG, "服务未初始化")
Log.e(TAG, "服务未初始化")
sendEvent("error", mapOf("code" to "NOT_INITIALIZED", "message" to "服务未初始化"))
return false
}
if (text.isEmpty()) {
FileLogger.e(TAG, "文本输入不能为空")
Log.e(TAG, "文本输入不能为空")
sendEvent("error", mapOf("code" to "EMPTY_TEXT", "message" to "文本输入不能为空"))
return false
}
@ -494,7 +610,7 @@ object AgentService : CoroutineScope {
text: String,
speakResponse: Boolean = true
) {
FileLogger.d(TAG, "用户问题: $text")
Log.d(TAG, "用户问题: $text")
// 创建用户文本消息并处理
val userMessage = openAIService.createUserMessage(text)
@ -513,7 +629,7 @@ object AgentService : CoroutineScope {
text: String = "",
speakResponse: Boolean = false
) {
FileLogger.d(TAG, "处理图片输入: ${if (text.isEmpty()) "无附加文本" else "附带文本: $text"}")
Log.d(TAG, "处理图片输入: ${if (text.isEmpty()) "无附加文本" else "附带文本: $text"}")
// 创建带图片的用户消息并处理
val userMessage = openAIService.createUserMessageWithImage(text, imageBase64)
@ -545,7 +661,6 @@ object AgentService : CoroutineScope {
// 设置状态为正在流式输出
isAiStreaming = true
// 使用历史记录作为上下文发送到OpenAI
val responseBuilder = StringBuilder()
var metadata:String = ""
@ -592,24 +707,23 @@ object AgentService : CoroutineScope {
addToHistoryMessages(userMessage)
}
openAIService.sendMessageStream(
messages = messagesWithSystemPrompt,
callback = object : OpenAIService.StreamCallback {
override fun onToken(token: String) {
responseBuilder.append(token)
if (speakResponse) {
azureTtsHelper?.speakStream(token)
ttsService?.speakStream(token)
}
// 发送流式回复token
sendEvent("assistant_token", mapOf("token" to token))
}
override fun onComplete() {
FileLogger.d(TAG, "AI完整回复: $responseBuilder")
Log.d(TAG, "AI完整回复: $responseBuilder")
// 视情况决定是否朗读回复
if (speakResponse) {
azureTtsHelper?.flushStream()
ttsService?.flushStream()
}
val response = responseBuilder.toString()
@ -637,7 +751,7 @@ object AgentService : CoroutineScope {
}
override fun onError(e: Exception) {
FileLogger.e(TAG, "AI处理出错", e)
Log.e(TAG, "AI处理出错", e)
sendEvent("error", mapOf(
"code" to "AI_ERROR",
"message" to e.message.toString()
@ -660,7 +774,7 @@ object AgentService : CoroutineScope {
stopRecognition()
}
} catch (e: Exception) {
FileLogger.e(TAG, "处理函数调用事件失败", e)
Log.e(TAG, "处理函数调用事件失败", e)
}
}
@ -670,15 +784,15 @@ object AgentService : CoroutineScope {
"function_call" to functionCall.toString(),
"result" to functionCallResult.toString(),
))
FileLogger.d(TAG, "mcp调用结果: $functionCallResult")
metadata = autoPlayMusic(functionCallResult);
Log.d(TAG, "mcp调用结果: $functionCallResult")
metadata = autoHandleFcunCallResult(functionCallResult);
}
}
)
} catch (e: Exception) {
val errorType = if (hasImage) "AI_IMAGE_PROCESS_ERROR" else "AI_PROCESS_ERROR"
FileLogger.e(TAG, "AI处理出错", e)
Log.e(TAG, "AI处理出错", e)
sendEvent("error", mapOf(
"code" to errorType,
"message" to e.message.toString()
@ -721,9 +835,9 @@ object AgentService : CoroutineScope {
}
}
FileLogger.d(TAG, "已加载${recentMessages.size}条历史记录")
Log.d(TAG, "已加载${recentMessages.size}条历史记录")
} catch (e: Exception) {
FileLogger.e(TAG, "加载聊天历史失败: ${e.message}")
Log.e(TAG, "加载聊天历史失败: ${e.message}")
}
}
@ -754,8 +868,8 @@ object AgentService : CoroutineScope {
isTtsSpeaking = true
restartIdleCheck() // 状态变化,重启检测
// 播放文本
azureTtsHelper?.speakText(text)
// 直接调用TTS,无需协程包装
ttsService?.speakOnce(text)
}
/**
@ -763,7 +877,8 @@ object AgentService : CoroutineScope {
*/
fun stopTts() {
if (isTtsSpeaking) {
azureTtsHelper?.stopSpeaking()
ttsService?.stop()
isTtsSpeaking = false
restartIdleCheck() // 状态变化,重启检测
sendEvent("tts_stopped", mapOf("status" to "stopped"))
@ -774,6 +889,7 @@ object AgentService : CoroutineScope {
* 保存聊天消息
*/
private fun saveChatMessage(userMessage: String, assistantMessage: String,metadata : String) {
// 显式指定IO调度器,数据库操作应在后台线程执行,即使父协程在Main上下文中
launch(Dispatchers.IO) {
try {
// 保存用户消息
@ -793,15 +909,15 @@ object AgentService : CoroutineScope {
)
// if (assistantMessageId != -1L) {
// FileLogger.d(TAG, "聊天记录已保存:用户消息ID=$userMessageId, 助手消息ID=$assistantMessageId")
// Log.d(TAG, "聊天记录已保存:用户消息ID=$userMessageId, 助手消息ID=$assistantMessageId")
// } else {
// FileLogger.e(TAG, "保存助手消息失败")
// Log.e(TAG, "保存助手消息失败")
// }
} else {
FileLogger.e(TAG, "保存用户消息失败")
Log.e(TAG, "保存用户消息失败")
}
} catch (e: Exception) {
FileLogger.e(TAG, "保存聊天记录异常", e)
Log.e(TAG, "保存聊天记录异常", e)
}
}
}
@ -820,19 +936,17 @@ object AgentService : CoroutineScope {
historyMessages.remove(0)
}
}
FileLogger.d(TAG, "聊天历史已清除")
Log.d(TAG, "聊天历史已清除")
} else {
FileLogger.e(TAG, "清除聊天历史失败")
Log.e(TAG, "清除聊天历史失败")
}
withContext(Dispatchers.Main) {
callback(success)
}
// 由于launch已在Main上下文中,可以直接调用回调
callback(success)
} catch (e: Exception) {
FileLogger.e(TAG, "清除聊天历史异常: ${e.message}")
withContext(Dispatchers.Main) {
callback(false)
}
Log.e(TAG, "清除聊天历史异常: ${e.message}")
// 由于launch已在Main上下文中,可以直接调用回调
callback(false)
}
}
}
@ -841,57 +955,19 @@ object AgentService : CoroutineScope {
* 发送事件
*/
private fun sendEvent(eventName: String, data: Map<String, Any>) {
// 检查当前是否在主线程
if (Looper.myLooper() == Looper.getMainLooper()) {
// 已在主线程,直接发送
listeners.forEach { listener ->
try {
listener.onEvent(eventName, data)
} catch (e: Exception) {
FileLogger.e(TAG, "向监听器发送事件失败", e)
}
// 使用协程确保在主线程上执行
launch {
// 我们已在主线程上下文中启动协程,无需再切换线程
listeners.forEach { listener ->
try {
listener.onEvent(eventName, data)
} catch (e: Exception) {
Log.e(TAG, "向监听器发送事件失败", e)
}
} else {
// 不在主线程,切换到主线程
Handler(Looper.getMainLooper()).post {
listeners.forEach { listener ->
try {
listener.onEvent(eventName, data)
} catch (e: Exception) {
FileLogger.e(TAG, "向监听器发送事件失败", e)
}
}
}
}
}
/**
* 释放所有资源
*/
fun dispose() {
try {
stopRecognition()
stopTts()
stopAiStream()
stopIdleCheck()
audioPlayer?.release()
audioPlayer = null
azureAsrHelper?.dispose()
azureTtsHelper?.dispose()
job.cancel()
clearListeners()
isInitialized = false
FileLogger.d(TAG, "代理服务资源已释放")
} catch (e: Exception) {
FileLogger.e(TAG, "释放资源异常: ${e.message}")
}
}
/**
* 检查录音权限
*/
@ -906,13 +982,13 @@ object AgentService : CoroutineScope {
*/
fun processImageInput(imagePath: String, text: String = "", speakResponse: Boolean = false): Boolean {
if (!isInitialized) {
FileLogger.e(TAG, "服务未初始化")
Log.e(TAG, "服务未初始化")
sendEvent("error", mapOf("code" to "NOT_INITIALIZED", "message" to "服务未初始化"))
return false
}
if (imagePath.isEmpty()) {
FileLogger.e(TAG, "图片路径不能为空")
Log.e(TAG, "图片路径不能为空")
sendEvent("error", mapOf("code" to "EMPTY_IMAGE_PATH", "message" to "图片路径不能为空"))
return false
}
@ -944,7 +1020,7 @@ object AgentService : CoroutineScope {
// 处理包含图片的消息
processImageWithOpenAI(imageBase64, text, speakResponse)
} catch (e: Exception) {
FileLogger.e(TAG, "处理图片失败: ${e.message}")
Log.e(TAG, "处理图片失败: ${e.message}")
sendEvent("error", mapOf(
"code" to "IMAGE_PROCESSING_ERROR",
"message" to e.message.toString()
@ -992,9 +1068,9 @@ object AgentService : CoroutineScope {
}
}
}
FileLogger.d(TAG, "开始播放音频资源")
Log.d(TAG, "开始播放音频资源")
} catch (e: Exception) {
FileLogger.e(TAG, "播放音频资源异常: ${e.message}", e)
Log.e(TAG, "播放音频资源异常: ${e.message}", e)
release()
}
}
@ -1016,17 +1092,17 @@ object AgentService : CoroutineScope {
}
}
}
/*
/*
* 自动播放音乐
* */
fun autoPlayMusic(functionCallResult: JSONObject): String{
fun autoHandleFcunCallResult(functionCallResult: JSONObject): String{
val metaStr = functionCallResult.optString("meta")
if (metaStr.isNotEmpty()) {
val meta = JSONObject(metaStr)
FileLogger.d(TAG, "mcp调用结果: $metaStr")
// Log.d(TAG, "mcp调用结果: $metaStr")
if (meta.has("card_music")) { //音乐卡片
val cardMusic = meta.getJSONObject("card_music")
FileLogger.d(TAG, "检查到音乐卡片: $cardMusic")
Log.d(TAG, "检查到音乐卡片: $cardMusic")
val id = cardMusic.optString("id", "")
val url = cardMusic.optString("url", "")
val name = cardMusic.optString("name", "")
@ -1048,14 +1124,19 @@ object AgentService : CoroutineScope {
playlist.add(mapOf("id" to id,"url" to url, "title" to name, "artist" to sgener,"coverUrl" to image))
}
if (playlist.isNotEmpty()) {
FileLogger.w(TAG, "自动播放音乐列表 ${playlist}")
Log.w(TAG, "自动播放音乐列表 ${playlist}")
processMusicPlayList(playlist)
} else {
FileLogger.w(TAG, "card_musiclist 中没有有效的音乐条目")
Log.w(TAG, "card_musiclist 中没有有效的音乐条目")
}
} else {
FileLogger.w(TAG, "card_musiclist.musics 不是一个数组")
Log.w(TAG, "card_musiclist.musics 不是一个数组")
}
}else if (meta.has("card_navigation")) { //导航服务
val cardNavigation = meta.getJSONObject("card_navigation")
val start = cardNavigation.optString("start", "")
val end = cardNavigation.optString("end", "")
processNavigation(start,end);
}
return metaStr;
}
@ -1074,7 +1155,7 @@ object AgentService : CoroutineScope {
*/
fun processMusicPlay(song: Map<String,String>){
// 在其他 Service、BroadcastReceiver 或 Application 中调用
FileLogger.i(TAG, "启动音乐服务 播放音乐 $song")
Log.i(TAG, "启动音乐服务 播放音乐 $song")
MusicServiceStarter.startServiceWithCommand(context, command = "play", song = song)
}
@ -1090,10 +1171,29 @@ object AgentService : CoroutineScope {
fun processMusicPlayList(songs:List<Map<String,String>>){
// 如果 playlist 中有有效的歌曲,开始播放
if (songs.isNotEmpty()) {
FileLogger.i(TAG, "启动音乐服务 播放音乐列表 ${songs}")
Log.i(TAG, "启动音乐服务 播放音乐列表 ${songs}")
MusicServiceStarter.startServiceWithPlaylist(context, command = "playlist", songs = songs)
} else {
FileLogger.i(TAG, "音乐列表为空,未启动播放服务")
Log.i(TAG, "音乐列表为空,未启动播放服务")
}
}
/**
* 处理导航
* 将ai返回的音乐连接 在后台启动播放
*
* @param musicUrl 图片文件路径
* @param name 歌曲名称,默认为空
* @param singer 歌手,默认为false
* @return 是否成功开始处理
*/
fun processNavigation(start:String,end:String){
// 如果 playlist 中有有效的歌曲,开始播放
if (!start.isNullOrEmpty() && !end.isNullOrEmpty()) {
Log.i(TAG, "启动导航服务 ${start} ${end}")
NavigationServiceStarter.startServiceWithCommand(context, command = "start", startpos = start, endpos = end)
} else {
Log.i(TAG, "启动导航服务失败")
}
}
}

30
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt

@ -8,7 +8,7 @@ import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
import io.flutter.plugin.common.EventChannel
import com.yunqiinnovation.deepvoice_utils.FileLogger
import android.util.Log
import org.json.JSONObject
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
@ -33,7 +33,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
// 添加监听器
AgentService.addListener(this)
FileLogger.d(TAG, "插件已加载")
Log.d(TAG, "插件已加载")
}
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
@ -46,7 +46,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
return
}
FileLogger.d(TAG, "启动BleMediaService,配置参数: $config")
Log.d(TAG, "启动BleMediaService,配置参数: $config")
// 创建Intent启动BleMediaService
val intent = Intent(context, BleMediaService::class.java)
@ -59,6 +59,8 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
config["openaiModel"]?.toString()?.let { intent.putExtra("openaiModel", it) }
config["systemPrompt"]?.toString()?.let { intent.putExtra("systemPrompt", it) }
config["mcpServer"]?.toString()?.let { intent.putExtra("mcpServer", it) }
config["volcanoAppId"]?.toString()?.let { intent.putExtra("volcanoAppId", it) }
config["volcanoToken"]?.toString()?.let { intent.putExtra("volcanoToken", it) }
// 启动前台服务
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@ -69,7 +71,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
result.success(true)
} catch (e: Exception) {
FileLogger.e(TAG, "启动AgentService失败", e)
Log.e(TAG, "启动AgentService失败", e)
result.error("SERVICE_START_ERROR", "启动AgentService失败: ${e.message}", null)
}
}
@ -79,7 +81,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
context.stopService(intent)
result.success(true)
} catch (e: Exception) {
FileLogger.e(TAG, "停止AgentService失败", e)
Log.e(TAG, "停止AgentService失败", e)
result.error("SERVICE_STOP_ERROR", "停止AgentService失败: ${e.message}", null)
}
}
@ -88,7 +90,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
val isStarted = AgentService.startRecognition(false)
result.success(isStarted)
} catch (e: Exception) {
FileLogger.e(TAG, "启动对话失败", e)
Log.e(TAG, "启动对话失败", e)
result.error("START_ERROR", "启动对话失败: ${e.message}", null)
}
}
@ -97,7 +99,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
AgentService.stopRecognition()
result.success(true)
} catch (e: Exception) {
FileLogger.e(TAG, "停止对话失败", e)
Log.e(TAG, "停止对话失败", e)
result.error("STOP_ERROR", "停止对话失败: ${e.message}", null)
}
}
@ -118,7 +120,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
result.success(isProcessing)
} catch (e: Exception) {
FileLogger.e(TAG, "处理文本输入失败", e)
Log.e(TAG, "处理文本输入失败", e)
result.error("TEXT_PROCESS_ERROR", "处理文本输入失败: ${e.message}", null)
}
}
@ -141,7 +143,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
result.success(isProcessing)
} catch (e: Exception) {
FileLogger.e(TAG, "处理图片输入失败", e)
Log.e(TAG, "处理图片输入失败", e)
result.error("IMAGE_PROCESS_ERROR", "处理图片输入失败: ${e.message}", null)
}
}
@ -156,7 +158,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
AgentService.speakText(text)
result.success(true)
} catch (e: Exception) {
FileLogger.e(TAG, "语音合成失败", e)
Log.e(TAG, "语音合成失败", e)
result.error("TTS_ERROR", "语音合成失败: ${e.message}", null)
}
}
@ -165,7 +167,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
AgentService.stopTts()
result.success(true)
} catch (e: Exception) {
FileLogger.e(TAG, "停止语音合成失败", e)
Log.e(TAG, "停止语音合成失败", e)
result.error("STOP_TTS_ERROR", "停止语音合成失败: ${e.message}", null)
}
}
@ -175,7 +177,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
result.success(success)
}
} catch (e: Exception) {
FileLogger.e(TAG, "清除聊天历史失败", e)
Log.e(TAG, "清除聊天历史失败", e)
result.error("CLEAR_HISTORY_ERROR", "清除聊天历史失败: ${e.message}", null)
}
}
@ -184,7 +186,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
AgentService.interruptCurrentResponse()
result.success(true)
} catch (e: Exception) {
FileLogger.e(TAG, "中断响应失败", e)
Log.e(TAG, "中断响应失败", e)
result.error("INTERRUPT_ERROR", "中断响应失败: ${e.message}", null)
}
}
@ -193,7 +195,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
AgentService.dispose()
result.success(true)
} catch (e: Exception) {
FileLogger.e(TAG, "释放资源失败", e)
Log.e(TAG, "释放资源失败", e)
result.error("DISPOSE_ERROR", "释放资源失败: ${e.message}", null)
}
}

28
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt

@ -3,7 +3,7 @@ package com.yunqiinnovation.agent_service
import android.content.Context
import com.yunqiinnovation.agent_service.AgentService
import com.yunqiinnovation.agent_service.AgentServiceListener
import com.yunqiinnovation.deepvoice_utils.FileLogger
import android.util.Log
import com.yunqiinnovation.ble_service.BleService
import com.yunqiinnovation.ble_service.BleConst
@ -29,11 +29,11 @@ object BleAgent : BleService.Callback, AgentServiceListener {
if (isInitialized) return true
try {
FileLogger.d(TAG, "初始化语音代理..., $config")
Log.d(TAG, "初始化语音代理..., $config")
// 1. 初始化AgentService
if (!AgentService.initialize(context, config, true)) {
FileLogger.e(TAG, "语音服务初始化失败")
Log.e(TAG, "语音服务初始化失败")
return false
}
@ -41,20 +41,20 @@ object BleAgent : BleService.Callback, AgentServiceListener {
if (BleService.initialize(context)) {
if (BleService.addCallback(this)) {
isRegisteredToBle = true
FileLogger.d(TAG, "已注册BLE回调")
Log.d(TAG, "已注册BLE回调")
} else {
FileLogger.w(TAG, "BLE回调注册失败")
Log.w(TAG, "BLE回调注册失败")
}
} else {
FileLogger.e(TAG, "BLE服务初始化失败")
Log.e(TAG, "BLE服务初始化失败")
return false
}
isInitialized = true
FileLogger.d(TAG, "语音代理初始化成功")
Log.d(TAG, "语音代理初始化成功")
return true
} catch (e: Exception) {
FileLogger.e(TAG, "初始化语音代理异常", e)
Log.e(TAG, "初始化语音代理异常", e)
return false
}
}
@ -64,7 +64,7 @@ object BleAgent : BleService.Callback, AgentServiceListener {
*/
fun startListening(): Boolean {
if (!isInitialized) {
FileLogger.e(TAG, "语音代理未初始化")
Log.e(TAG, "语音代理未初始化")
return false
}
@ -106,7 +106,7 @@ object BleAgent : BleService.Callback, AgentServiceListener {
AgentService.dispose()
isInitialized = false
FileLogger.d(TAG, "语音代理资源已释放")
Log.d(TAG, "语音代理资源已释放")
}
//============================================================================================
@ -119,16 +119,16 @@ object BleAgent : BleService.Callback, AgentServiceListener {
override fun onConnectionStateChanged(state: Int) {
// 可选:处理连接状态变化
if (state == BleConst.STATE_CONNECTED) {
FileLogger.d(TAG, "Ble设备已连接,准备好语音交互")
Log.d(TAG, "Ble设备已连接,准备好语音交互")
} else if (state == BleConst.STATE_DISCONNECTED) {
FileLogger.d(TAG, "Ble设备已断开,停止语音服务")
Log.d(TAG, "Ble设备已断开,停止语音服务")
// 设备断开连接,停止语音服务
// stopListening()
}
}
override fun onAudioDataReceived(data: ByteArray) {
// FileLogger.d(TAG, "onAudioDataReceived, data: ${data.size}")
// Log.d(TAG, "onAudioDataReceived, data: ${data.size}")
AgentService.pushAudioData(data)
// 可选:处理音频数据
}
@ -138,7 +138,7 @@ object BleAgent : BleService.Callback, AgentServiceListener {
* 在收到唤醒信号时启动语音识别
*/
override fun onWakeupSignalReceived() {
FileLogger.d(TAG, "收到唤醒信号,启动语音识别")
Log.d(TAG, "收到唤醒信号,启动语音识别")
// 停止当前TTS,避免冲突
AgentService.interruptCurrentResponse()

57
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt

@ -21,7 +21,7 @@ import android.support.v4.media.session.PlaybackStateCompat
import androidx.core.app.NotificationCompat
import androidx.media.session.MediaButtonReceiver
import com.yunqiinnovation.agent_service.R
import com.yunqiinnovation.deepvoice_utils.FileLogger
import android.util.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
@ -56,6 +56,8 @@ class BleMediaService : Service(), CoroutineScope {
private const val KEY_OPENAI_MODEL = "openaiModel"
private const val KEY_SYSTEM_PROMPT = "systemPrompt"
private const val KEY_MCP_SERVER = "mcpServer"
private const val KEY_VOLCANO_APP_ID = "volcanoAppId"
private const val KEY_VOLCANO_TOKEN = "volcanoToken"
// 用于跟踪服务是否正在运行
private val isRunning = AtomicBoolean(false)
@ -89,8 +91,7 @@ class BleMediaService : Service(), CoroutineScope {
override fun onCreate() {
super.onCreate()
FileLogger.init(this)
FileLogger.i(TAG, "BleMediaService正在创建...")
Log.i(TAG, "BleMediaService正在创建...")
// 设置服务状态
isRunning.set(true)
@ -111,11 +112,11 @@ class BleMediaService : Service(), CoroutineScope {
audioPlayer?.play(R.raw.silence, 0f)
setPlaybackState(PlaybackStateCompat.STATE_PAUSED)
FileLogger.i(TAG, "媒体服务已启动")
Log.i(TAG, "媒体服务已启动")
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
// FileLogger.i(TAG, "BleMediaService.onStartCommand: ${intent?.action}")
Log.i(TAG, "BleMediaService.onStartCommand: ${intent?.action}")
// 保存当前Intent,用于后续初始化
currentIntent = intent
@ -159,36 +160,36 @@ class BleMediaService : Service(), CoroutineScope {
// 如果Intent中有参数,则保存到SharedPreferences
if (configFromIntent.isNotEmpty()) {
FileLogger.d(TAG, "从Intent中获取到配置参数,保存到SharedPreferences")
Log.d(TAG, "从Intent中获取到配置参数,保存到SharedPreferences")
saveConfigToPreferences(configFromIntent, prefs)
}
// 合并Intent和SharedPreferences中的参数
val config = getConfigFromPreferences(prefs)
FileLogger.d(TAG, "当前配置参数: $config")
Log.d(TAG, "当前配置参数: $config")
// 检查必要的配置是否存在
if (config[KEY_AZURE_SPEECH_KEY].isNullOrEmpty() || config[KEY_OPENAI_API_KEY].isNullOrEmpty()) {
FileLogger.w(TAG, "语音代理初始化失败: API密钥未配置")
Log.w(TAG, "语音代理初始化失败: API密钥未配置")
return
}
if (isChatAgentInitialized) {
FileLogger.d(TAG, "ChatAgent已经初始化,跳过")
Log.d(TAG, "ChatAgent已经初始化,跳过")
return
}
if(config.isNotEmpty()) {
// 初始化ChatAgent
FileLogger.i(TAG, "开始初始化ChatAgent")
Log.i(TAG, "开始初始化ChatAgent")
val success = BleAgent.initialize(applicationContext, config)
if (success) {
isChatAgentInitialized = true
FileLogger.i(TAG, "语音交互代理初始化成功")
Log.i(TAG, "语音交互代理初始化成功")
} else {
FileLogger.e(TAG, "语音交互代理初始化失败")
Log.e(TAG, "语音交互代理初始化失败")
}
}
} catch (e: Exception) {
FileLogger.e(TAG, "初始化语音交互代理时发生异常", e)
Log.e(TAG, "初始化语音交互代理时发生异常", e)
}
}
@ -206,8 +207,10 @@ class BleMediaService : Service(), CoroutineScope {
it.getStringExtra(KEY_OPENAI_MODEL)?.let { value -> config[KEY_OPENAI_MODEL] = value }
it.getStringExtra(KEY_SYSTEM_PROMPT)?.let { value -> config[KEY_SYSTEM_PROMPT] = value }
it.getStringExtra(KEY_MCP_SERVER)?.let { value -> config[KEY_MCP_SERVER] = value }
it.getStringExtra(KEY_VOLCANO_APP_ID)?.let { value -> config[KEY_VOLCANO_APP_ID] = value }
it.getStringExtra(KEY_VOLCANO_TOKEN)?.let { value -> config[KEY_VOLCANO_TOKEN] = value }
}
FileLogger.i(TAG, "从Intent中提取到配置参数: $config")
Log.i(TAG, "从Intent中提取到配置参数: $config")
return config
}
@ -236,7 +239,9 @@ class BleMediaService : Service(), CoroutineScope {
KEY_OPENAI_BASE_URL to getConfigString(prefs, KEY_OPENAI_BASE_URL, "https://api.openai.com/v1"),
KEY_OPENAI_MODEL to getConfigString(prefs, KEY_OPENAI_MODEL, "gpt-3.5-turbo"),
KEY_SYSTEM_PROMPT to getConfigString(prefs, KEY_SYSTEM_PROMPT, "gpt-3.5-turbo"),
KEY_MCP_SERVER to getConfigString(prefs, KEY_MCP_SERVER, "")
KEY_MCP_SERVER to getConfigString(prefs, KEY_MCP_SERVER, ""),
KEY_VOLCANO_APP_ID to getConfigString(prefs, KEY_VOLCANO_APP_ID, ""),
KEY_VOLCANO_TOKEN to getConfigString(prefs, KEY_VOLCANO_TOKEN, "")
)
}
@ -250,15 +255,15 @@ class BleMediaService : Service(), CoroutineScope {
override fun onBind(intent: Intent?): IBinder? = null
override fun onDestroy() {
FileLogger.i(TAG, "BleMediaService正在销毁...")
Log.i(TAG, "BleMediaService正在销毁...")
// 取消注册媒体按钮接收器
try {
val mediaButtonReceiver = ComponentName(packageName, MediaButtonReceiver::class.java.name)
audioManager.unregisterMediaButtonEventReceiver(mediaButtonReceiver)
FileLogger.i(TAG, "媒体按钮接收器已取消注册")
Log.i(TAG, "媒体按钮接收器已取消注册")
} catch (e: Exception) {
FileLogger.e(TAG, "取消注册媒体按钮接收器失败: ${e.message}", e)
Log.e(TAG, "取消注册媒体按钮接收器失败: ${e.message}", e)
}
// 释放媒体会话
@ -319,16 +324,16 @@ class BleMediaService : Service(), CoroutineScope {
try {
val mediaButtonReceiver = ComponentName(packageName, MediaButtonReceiver::class.java.name)
audioManager.registerMediaButtonEventReceiver(mediaButtonReceiver)
FileLogger.i(TAG, "媒体按钮接收器注册成功: $mediaButtonReceiver")
Log.i(TAG, "媒体按钮接收器注册成功: $mediaButtonReceiver")
} catch (e: Exception) {
FileLogger.e(TAG, "媒体按钮接收器注册失败: ${e.message}", e)
Log.e(TAG, "媒体按钮接收器注册失败: ${e.message}", e)
}
}
private fun checkBleConnection() {
FileLogger.i(TAG, "checkBleConnection收到连接请求")
Log.i(TAG, "checkBleConnection收到连接请求")
try {
// 示例:使用协程异步处理连接
@ -338,7 +343,7 @@ class BleMediaService : Service(), CoroutineScope {
}
}
} catch (e: Exception) {
FileLogger.e(TAG, "连接设备失败: ${e.message}", e)
Log.e(TAG, "连接设备失败: ${e.message}", e)
}
}
@ -350,7 +355,7 @@ class BleMediaService : Service(), CoroutineScope {
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.notify(NOTIFICATION_ID, createNotification())
} catch (e: Exception) {
FileLogger.e(TAG, "更新通知异常: ${e.message}")
Log.e(TAG, "更新通知异常: ${e.message}")
}
}
@ -451,7 +456,7 @@ class BleMediaService : Service(), CoroutineScope {
// // 播放完成后自动循环播放,保持媒体会话活跃
// setOnCompletionListener {
// FileLogger.i(TAG, "静音资源播放完成,再次播放保持会话活跃")
// Log.i(TAG, "静音资源播放完成,再次播放保持会话活跃")
// play(resId, volume)
// }
@ -460,9 +465,9 @@ class BleMediaService : Service(), CoroutineScope {
start()
}
FileLogger.i(TAG, "开始播放静音资源")
Log.i(TAG, "开始播放静音资源")
} catch (e: Exception) {
FileLogger.e(TAG, "播放静音资源异常: ${e.message}", e)
Log.e(TAG, "播放静音资源异常: ${e.message}", e)
release()
}
}

14
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/utils.kt

@ -3,7 +3,7 @@ package com.yunqiinnovation.agent_service
import android.content.Context
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
import com.yunqiinnovation.deepvoice_utils.FileLogger
import android.util.Log
/**
* 通用工具类,包含安全存储等功能
@ -59,10 +59,10 @@ object Utils {
// 提交更改
editor.apply()
FileLogger.d(TAG, "数据已安全保存到加密存储中")
Log.d(TAG, "数据已安全保存到加密存储中")
true
} catch (e: Exception) {
FileLogger.e(TAG, "保存数据到加密存储失败", e)
Log.e(TAG, "保存数据到加密存储失败", e)
false
}
}
@ -97,10 +97,10 @@ object Utils {
}
}
FileLogger.d(TAG, "已从加密存储加载所有数据")
Log.d(TAG, "已从加密存储加载所有数据")
resultMap
} catch (e: Exception) {
FileLogger.e(TAG, "从加密存储加载所有数据失败", e)
Log.e(TAG, "从加密存储加载所有数据失败", e)
emptyMap()
}
}
@ -134,10 +134,10 @@ object Utils {
.putString(KEY_SESSION_ID, sessionId)
.apply()
FileLogger.d(TAG, "已清除所有安全存储的数据")
Log.d(TAG, "已清除所有安全存储的数据")
true
} catch (e: Exception) {
FileLogger.e(TAG, "清除安全存储数据失败", e)
Log.e(TAG, "清除安全存储数据失败", e)
false
}
}

12
local_plugins/agent_service/pubspec.yaml

@ -10,18 +10,6 @@ environment:
dependencies:
flutter:
sdk: flutter
# 依赖本地插件
open_ai_service:
path: ../open_ai_service
azure_speech:
path: ../azure_speech
deepvoice_utils:
path: ../deepvoice_utils
chat_storage:
path: ../chat_storage
music_service:
path: ../music_service
get: ^4.6.5
dev_dependencies:
flutter_test:

0
amap_flutter/.gitignore → local_plugins/amap_service/.gitignore

14
amap_flutter/.metadata → local_plugins/amap_service/.metadata

@ -4,7 +4,7 @@
# This file should be version controlled and should not be manually edited.
version:
revision: "35c388afb57ef061d06a39b537336c87e0e3d1b1"
revision: "c23637390482d4cf9598c3ce3f2be31aa7332daf"
channel: "stable"
project_type: plugin
@ -13,14 +13,14 @@ project_type: plugin
migration:
platforms:
- platform: root
create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
- platform: android
create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
- platform: ios
create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
# User provided section

0
amap_flutter/CHANGELOG.md → local_plugins/amap_service/CHANGELOG.md

0
amap_flutter/LICENSE → local_plugins/amap_service/LICENSE

2
amap_flutter/README.md → local_plugins/amap_service/README.md

@ -1,4 +1,4 @@
# amap_flutter
# amap_service
A new Flutter project.

0
amap_flutter/analysis_options.yaml → local_plugins/amap_service/analysis_options.yaml

0
amap_flutter/android/.gitignore → local_plugins/amap_service/android/.gitignore

27
amap_flutter/android/build.gradle → local_plugins/amap_service/android/build.gradle

@ -1,7 +1,8 @@
group = "com.example.amap_flutter"
version = "1.0"
group = "com.yunqiinnovation.amap_service"
version = "1.0-SNAPSHOT"
buildscript {
ext.kotlin_version = "1.8.22"
repositories {
google()
mavenCentral()
@ -9,10 +10,11 @@ buildscript {
dependencies {
classpath("com.android.tools.build:gradle:8.7.0")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version")
}
}
rootProject.allprojects {
allprojects {
repositories {
google()
mavenCentral()
@ -20,9 +22,10 @@ rootProject.allprojects {
}
apply plugin: "com.android.library"
apply plugin: "kotlin-android"
android {
namespace = "com.example.amap_flutter"
namespace = "com.yunqiinnovation.amap_service"
compileSdk = 35
@ -31,19 +34,29 @@ android {
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11
}
sourceSets {
main.java.srcDirs += "src/main/kotlin"
test.java.srcDirs += "src/test/kotlin"
}
defaultConfig {
minSdk = 21
}
dependencies {
implementation 'com.amap.api:3dmap-location-search:latest.integration'
testImplementation("junit:junit:4.13.2")
implementation 'com.amap.api:navi-3dmap:latest.integration'
testImplementation("org.jetbrains.kotlin:kotlin-test")
testImplementation("org.mockito:mockito-core:5.0.0")
}
testOptions {
unitTests.all {
useJUnitPlatform()
testLogging {
events "passed", "skipped", "failed", "standardOut", "standardError"
outputs.upToDateWhen {false}

1
local_plugins/amap_service/android/settings.gradle

@ -0,0 +1 @@
rootProject.name = 'amap_service'

48
local_plugins/amap_service/android/src/main/AndroidManifest.xml

@ -0,0 +1,48 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.yunqiinnovation.amap_service">
<!--允许访问网络,必选权限-->
<uses-permission android:name="android.permission.INTERNET" />
<!--允许获取精确位置,实时导航为必选-->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<!--允许获取粗略位置,实时导航为必选-->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<!--允许获取网络状态,用于网络定位(无gps情况下的定位),若需网络定位功能则必选-->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!--允许获取wifi网络信息,用于网络定位(无gps情况下的定位),若需网络定位功能则必选-->
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<!--允许获取wifi状态改变,用于网络定位(无gps情况下的定位),若需网络定位功能则必选-->
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<!--后台获取位置信息,若需后台定位或持续导航则必选-->
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<!--用于申请调用A-GPS模块,卫星定位加速-->
<uses-permission android:name="android.permission.ACCESS_LOCATION_EXTRA_COMMANDS" />
<!--用于用户链接蓝牙时,在导航组件页面的蓝牙连接提醒,建立链接后开发者可选用蓝牙通道进行tts播报-->
<uses-permission android:name="android.permission.BLUETOOTH" />
<!--用与导航状态中保持屏幕常亮-->
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<!--允许写设备缓存,用于问题排查-->
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
<application>
<activity
android:name="com.amap.api.navi.AmapRouteActivity"
android:theme="@android:style/Theme.NoTitleBar"
android:configChanges="orientation|keyboardHidden|screenSize|navigation" />
<service android:name="com.amap.api.location.APSService"/>
<service
android:name=".NavigationService"
android:exported="true"
android:enabled="true" android:foregroundServiceType="location|mediaPlayback">
</service>
</application>
</manifest>

54
local_plugins/amap_service/android/src/main/kotlin/com/yunqiinnovation/amap_service/AmapServicePlugin.kt

@ -0,0 +1,54 @@
package com.yunqiinnovation.amap_service
import android.content.Context
import android.content.Intent
import androidx.core.content.ContextCompat
// import com.yunqiinnovation.deepvoice_utils.FileLogger
import com.yunqiinnovation.amap_service.NavigationServiceStarter
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
/** MusicServicePlugin */
class AmapServicePlugin: FlutterPlugin, MethodCallHandler {
private val TAG = "MusicServicePlugin"
private lateinit var context: Context
private lateinit var channel : MethodChannel
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
context = flutterPluginBinding.applicationContext
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "com.yunqiinnovation.music_service")
channel.setMethodCallHandler(this)
}
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
// FileLogger.d(TAG, "Method call: ${call.method}")
when (call.method) {
"start" -> {
val startpos = call.argument<String>("startpos")
val endpos = call.argument<String>("endpos")
val ntype = call.argument<String>("ntype")
if (startpos != null && endpos != null && ntype != null) {
NavigationServiceStarter.startServiceWithCommand(context, "start", startpos, endpos, ntype)
result.success(null)
} else {
result.error("INVALID_ARGUMENT", "Missing 'song' map", null)
}
}
"stop" -> {
val songs = call.argument<List<Map<String, String>>>("songs")
if (songs != null && songs.isNotEmpty()) {
NavigationServiceStarter.startServiceWithCommand(context, "stop")
result.success(null)
} else {
result.error("INVALID_ARGUMENT", "Missing or empty 'songs' list", null)
}
}
else -> result.notImplemented()
}
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {}
}

314
local_plugins/amap_service/android/src/main/kotlin/com/yunqiinnovation/amap_service/NavigationService.kt

@ -0,0 +1,314 @@
package com.yunqiinnovation.amap_service
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.IBinder
import android.util.Log
import androidx.core.app.NotificationCompat
import com.amap.api.navi.AMapNavi
import com.amap.api.navi.AMapNaviListener
import com.amap.api.navi.enums.NaviType
import com.amap.api.navi.enums.PathPlanningStrategy
import com.amap.api.navi.model.AMapCalcRouteResult
import com.amap.api.navi.model.AMapLaneInfo
import com.amap.api.navi.model.AMapModelCross
import com.amap.api.navi.model.AMapNaviCameraInfo
import com.amap.api.navi.model.AMapNaviCross
import com.amap.api.navi.model.AMapNaviLocation
import com.amap.api.navi.model.AMapNaviRouteNotifyData
import com.amap.api.navi.model.AMapNaviTrafficFacilityInfo
import com.amap.api.navi.model.AMapServiceAreaInfo
import com.amap.api.navi.model.AimLessModeCongestionInfo
import com.amap.api.navi.model.AimLessModeStat
import com.amap.api.navi.model.NaviInfo
import com.amap.api.navi.model.NaviLatLng
import java.io.FileNotFoundException
import java.util.concurrent.atomic.AtomicBoolean
class NavigationService: Service() {
companion object {
private const val TAG = "NavigationService"
private const val CHANNEL_ID = "navigation_channel"
private const val CHANNEL_Name = "导航服务"
private const val NOTIFICATION_ID = 1003
private val isRunning = AtomicBoolean(false)
private var startpos: String = ""
private var endpos: String = ""
private var naviType: String? = null
private var playlist = mutableListOf<Map<String,String>>()
private var currentIndex = 0
fun isServiceRunning() = isRunning.get()
}
private lateinit var aMapNavi: AMapNavi
override fun onCreate() {
super.onCreate()
startForegroundNotification()
}
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
val command = intent.getStringExtra("command") ?: return START_NOT_STICKY
when (command) {
"start" -> {
try {
startpos = intent.getStringExtra("startpos").toString()
endpos = intent.getStringExtra("endpos").toString()
naviType = intent.getStringExtra("naviType");
Log.w(TAG, "开始导航 startpos:$startpos endpos:$endpos naviType:$naviType")
// 初始化导航
aMapNavi = AMapNavi.getInstance(applicationContext)
aMapNavi.setUseInnerVoice(true)
aMapNavi.setBroadcastMode(1)
aMapNavi.addAMapNaviListener(naviListener)
}catch (e: FileNotFoundException){
Log.w("NavigationService", "Custom voice bin not found: ${e.message}")
}
}
"stop" -> {
aMapNavi.stopNavi();
}
}
return START_STICKY
}
private fun startForegroundNotification() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
CHANNEL_ID,
CHANNEL_Name,
NotificationManager.IMPORTANCE_LOW
).apply {
description = "用于导航的前台服务"
setShowBadge(false)
}
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
manager.createNotificationChannel(channel)
}
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("导航进行中")
.setContentText("正在导航中,请保持应用运行")
.setSmallIcon(R.drawable.ic_navigation) // 使用你自己的图标资源
.setOngoing(true)
.build()
startForeground(NOTIFICATION_ID, notification)
}
// 实现监听器,处理导航播报等
private val naviListener = object : AMapNaviListener {
override fun onInitNaviFailure() {
Log.e(TAG, "onInitNaviFailure!")
// TODO("Not yet implemented")
}
override fun onInitNaviSuccess() {
Log.e(TAG, "onInitNaviSuccess!")
// 起点
val startParts: List<String> = startpos.split(",")
val startLng = startParts[0].toDouble()
val startLat = startParts[1].toDouble()
// 终点
val endParts: List<String> = endpos.split(",")
val endLng = endParts[0].toDouble()
val endLat = endParts[1].toDouble()
val startList: MutableList<NaviLatLng> = ArrayList()
val endList: MutableList<NaviLatLng> = ArrayList()
startList.add(NaviLatLng(startLat, startLng))
endList.add(NaviLatLng(endLat, endLng))
val success = aMapNavi.calculateDriveRoute(startList, endList, null, PathPlanningStrategy.DRIVING_MULTIPLE_ROUTES_DEFAULT);
if (!success) {
Log.e(TAG, "路径规划请求失败")
}
}
override fun onStartNavi(p0: Int) {
Log.e(TAG, "onStartNavi!")
// TODO("Not yet implemented")
}
override fun onTrafficStatusUpdate() {
Log.e(TAG, "onTrafficStatusUpdate!")
// TODO("Not yet implemented")
}
override fun onLocationChange(location: AMapNaviLocation) {
Log.e(TAG, "onLocationChange! ${location}")
// 可通过广播发送位置信息到 Flutter
}
override fun onGetNavigationText(p0: Int, p1: String?) {
Log.e(TAG, "onGetNavigationText! $p0 $p1")
// TODO("Not yet implemented")
}
override fun onGetNavigationText(p0: String?) {
Log.e(TAG, "onGetNavigationText! $p0")
// TODO("Not yet implemented")
}
override fun onEndEmulatorNavi() {
Log.e(TAG, "onEndEmulatorNavi! ")
// TODO("Not yet implemented")
}
override fun onArriveDestination() {
Log.e(TAG, "onArriveDestination! ")
// TODO("Not yet implemented")
}
override fun onCalculateRouteFailure(p0: Int) {
Log.e(TAG, "路径规划失败, 错误码: $p0")
// TODO: 可添加反馈机制给 Flutter 层或用户提示
}
override fun onCalculateRouteFailure(p0: AMapCalcRouteResult?) {
Log.e(TAG, "onCalculateRouteFailure! ${p0}")
// TODO("Not yet implemented")
}
override fun onReCalculateRouteForYaw() {
Log.e(TAG, "onReCalculateRouteForYaw!")
// TODO("Not yet implemented")
}
override fun onReCalculateRouteForTrafficJam() {
Log.e(TAG, "onReCalculateRouteForTrafficJam!")
// TODO("Not yet implemented")
}
override fun onArrivedWayPoint(p0: Int) {
Log.e(TAG, "onArrivedWayPoint!")
// TODO("Not yet implemented")
}
override fun onGpsOpenStatus(p0: Boolean) {
Log.e(TAG, "onGpsOpenStatus!")
// TODO("Not yet implemented")
}
override fun onNaviInfoUpdate(p0: NaviInfo?) {
Log.e(TAG, "onNaviInfoUpdate! $p0")
// TODO("Not yet implemented")
}
override fun updateCameraInfo(p0: Array<out AMapNaviCameraInfo>?) {
Log.e(TAG, "updateCameraInfo! $p0")
// TODO("Not yet implemented")
}
override fun updateIntervalCameraInfo(
p0: AMapNaviCameraInfo?,
p1: AMapNaviCameraInfo?,
p2: Int
) {
Log.e(TAG, "updateIntervalCameraInfo! $p0")
// TODO("Not yet implemented")
}
override fun onServiceAreaUpdate(p0: Array<out AMapServiceAreaInfo>?) {
Log.e(TAG, "onServiceAreaUpdate! $p0")
// TODO("Not yet implemented")
}
override fun showCross(p0: AMapNaviCross?) {
Log.e(TAG, "showCross! $p0")
// TODO("Not yet implemented")
}
override fun hideCross() {
Log.e(TAG, "hideCross!")
// TODO("Not yet implemented")
}
override fun showModeCross(p0: AMapModelCross?) {
Log.e(TAG, "showModeCross! $p0")
// TODO("Not yet implemented")
}
override fun hideModeCross() {
Log.e(TAG, "hideModeCross!")
// TODO("Not yet implemented")
}
override fun showLaneInfo(p0: Array<out AMapLaneInfo>?, p1: ByteArray?, p2: ByteArray?) {
Log.e(TAG, "showLaneInfo! $p0")
// TODO("Not yet implemented")
}
override fun showLaneInfo(p0: AMapLaneInfo?) {
Log.e(TAG, "showLaneInfo! $p0")
// TODO("Not yet implemented")
}
override fun hideLaneInfo() {
Log.e(TAG, "hideLaneInfo!")
// TODO("Not yet implemented")
}
override fun onCalculateRouteSuccess(ids: IntArray?) {
Log.e(TAG, "onCalculateRouteSuccess! $ids")
aMapNavi.startNavi(NaviType.EMULATOR)
}
override fun onCalculateRouteSuccess(p0: AMapCalcRouteResult?) {
Log.e(TAG, "onCalculateRouteSuccess! $p0")
// TODO("Not yet implemented")
}
override fun notifyParallelRoad(p0: Int) {
Log.d(TAG, "是否在拥挤平行路段: $p0")
// TODO("Not yet implemented")
}
override fun OnUpdateTrafficFacility(p0: Array<out AMapNaviTrafficFacilityInfo>?) {
Log.d(TAG, "OnUpdateTrafficFacility: $p0")
// TODO("Not yet implemented")
}
override fun OnUpdateTrafficFacility(p0: AMapNaviTrafficFacilityInfo?) {
Log.d(TAG, "OnUpdateTrafficFacility: $p0")
// TODO("Not yet implemented")
}
override fun updateAimlessModeStatistics(p0: AimLessModeStat?) {
// TODO("Not yet implemented")
Log.d(TAG, "updateAimlessModeStatistics: $p0")
}
override fun updateAimlessModeCongestionInfo(p0: AimLessModeCongestionInfo?) {
// TODO("Not yet implemented")
Log.d(TAG, "updateAimlessModeCongestionInfo: $p0")
}
override fun onPlayRing(p0: Int) {
Log.d(TAG, "onPlayRing: $p0")
// TODO("Not yet implemented")
}
override fun onNaviRouteNotify(p0: AMapNaviRouteNotifyData?) {
Log.w(TAG, "GPS信号弱")
// TODO("Not yet implemented")
}
override fun onGpsSignalWeak(p0: Boolean) {
Log.w(TAG, "GPS信号弱")
// TODO("Not yet implemented")
}
}
override fun onBind(intent: Intent?): IBinder? = null
}

34
local_plugins/amap_service/android/src/main/kotlin/com/yunqiinnovation/amap_service/NavigationServiceStarter.kt

@ -0,0 +1,34 @@
package com.yunqiinnovation.amap_service
import android.content.Context
import android.content.Intent
import android.util.Log
import androidx.core.content.ContextCompat
object NavigationServiceStarter {
private val TAG = "NavigationServiceStarter"
//播放单曲
fun startServiceWithCommand(context: Context, command: String, startpos: String? = null, endpos: String? = null, ntype: String? = null) {
Log.i(TAG, "liwei------------------- startServiceWithCommand: $command $startpos $endpos $ntype")
val intent = Intent(context, NavigationService::class.java).apply {
putExtra("command", command)
startpos?.let {
putExtra("startpos",it) // 只有非 null 时才 putExtra
}
endpos?.let {
putExtra("endpos", it) // 只有非 null 时才 putExtra
}
ntype?.let {
putExtra("ntype",it) // 只有非 null 时才 putExtra
}
}
val success = try {
ContextCompat.startForegroundService(context, intent)
true
} catch (e: Exception) {
Log.e(TAG, "liwei------------------- Failed to start service", e)
false
}
Log.d(TAG, "liwei------------------- Service start success: $success")
}
}

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

Loading…
Cancel
Save