Browse Source

add

newdev_shunjiawei
wolfplus 1 year ago
parent
commit
725e638538
  1. 7
      android/settings.gradle.kts
  2. 140
      ios/Runner.xcodeproj/project.pbxproj
  3. 2
      lib/modules/pairing/controllers/pairing_controller.dart
  4. 2
      local_plugins/agent_service/android/build.gradle.kts
  5. 17
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
  6. 1032
      local_plugins/agent_service/ios/Classes/AgentServiceImpl.swift
  7. 4
      local_plugins/agent_service/ios/Classes/AgentServicePlugin.h
  8. 12
      local_plugins/agent_service/ios/Classes/AgentServicePlugin.m
  9. 160
      local_plugins/agent_service/ios/Classes/AgentServicePlugin.swift
  10. 240
      local_plugins/agent_service/ios/Classes/AudioSessionHub.swift
  11. 4
      local_plugins/agent_service/ios/agent_service/Package.swift
  12. 6
      local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift
  13. 253
      local_plugins/agent_service/ios/agent_service/Sources/agent_service/AudioSessionHub.swift
  14. BIN
      local_plugins/agent_service/ios/agent_service/Sources/agent_service/Resources/calling.mp3
  15. BIN
      local_plugins/agent_service/ios/agent_service/Sources/agent_service/Resources/start.mp3
  16. BIN
      local_plugins/agent_service/ios/agent_service/Sources/agent_service/Resources/stop.mp3
  17. 7
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCompanionManager.kt

7
android/settings.gradle.kts

@ -32,7 +32,6 @@ plugins {
include(":app")
include(":azure_speech")
include(":open_ai_service")
include(":chat_storage")
include(":jieli")
include(":spotify_sdk")
@ -40,13 +39,15 @@ include(":ble_service")
include(":classic_bluetooth")
include(":agent_service")
include(":speech")
include(":open_ai_service")
// 设置azure_speech项目的路径
project(":azure_speech").projectDir = file("../local_plugins/azure_speech/android")
project(":open_ai_service").projectDir = file("../local_plugins/open_ai_service/android")
project(":chat_storage").projectDir = file("../local_plugins/chat_storage/android")
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(":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")
project(":speech").projectDir = file("../local_plugins/speech/android")
project(":open_ai_service").projectDir = file("../local_plugins/open_ai_service/android")

140
ios/Runner.xcodeproj/project.pbxproj

@ -13,11 +13,11 @@
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
93B17271C888656B4B397BC2 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 78CE6F02322D8DF27DF90403 /* Pods_Runner.framework */; };
93D73EC48D95512F5F541AD7 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 162D67C21ED243BA396CEF5C /* Pods_RunnerTests.framework */; };
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 */; };
E0A3F16A6E4E5EE4E2CFA52A /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C7149989675CB5D71A7427D0 /* Pods_RunnerTests.framework */; };
CBE786ED7A0DB415954B1BA4 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EFDA2C0DD7A7D82CA03799B2 /* Pods_Runner.framework */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -48,18 +48,18 @@
0AD944132DDA0C0D00557D21 /* AccessorySetupKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AccessorySetupKit.framework; path = System/Library/Frameworks/AccessorySetupKit.framework; sourceTree = SDKROOT; };
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
295FC65584EC3019A679107E /* 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>"; };
162D67C21ED243BA396CEF5C /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
280769B5B4FB941FA7D35C06 /* 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>"; };
2C91016A31D6647548E59DC8 /* 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>"; };
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>"; };
49AB99AA281C6CC7FD875C0B /* 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>"; };
4EC8693222B4131484101683 /* 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>"; };
58F8C8AD87B4FAC8D686BB67 /* 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>"; };
46859C536D3D0051CC82E6C5 /* 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>"; };
4852C04805E85DB32D31EEC2 /* 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>"; };
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>"; };
78CE6F02322D8DF27DF90403 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
76274BCD123CDD5AF172875F /* 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>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
8A7A4179E9EBF4D8C8ACC841 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
@ -67,9 +67,9 @@
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>"; };
C7149989675CB5D71A7427D0 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
CC1FCC6CF0A639ACBDAD3D96 /* 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>"; };
CD9846946CA680DD2E0721FA /* 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>"; };
D2958658C06A5ADF62D6ABA2 /* Runner.entitlements */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
EFDA2C0DD7A7D82CA03799B2 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -78,7 +78,7 @@
buildActionMask = 2147483647;
files = (
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
93B17271C888656B4B397BC2 /* Pods_Runner.framework in Frameworks */,
CBE786ED7A0DB415954B1BA4 /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -86,7 +86,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
E0A3F16A6E4E5EE4E2CFA52A /* Pods_RunnerTests.framework in Frameworks */,
93D73EC48D95512F5F541AD7 /* Pods_RunnerTests.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -96,12 +96,12 @@
1ACB686EC7DC36C6E33D372F /* Pods */ = {
isa = PBXGroup;
children = (
58F8C8AD87B4FAC8D686BB67 /* Pods-Runner.debug.xcconfig */,
8A7A4179E9EBF4D8C8ACC841 /* Pods-Runner.release.xcconfig */,
295FC65584EC3019A679107E /* Pods-Runner.profile.xcconfig */,
49AB99AA281C6CC7FD875C0B /* Pods-RunnerTests.debug.xcconfig */,
4EC8693222B4131484101683 /* Pods-RunnerTests.release.xcconfig */,
CC1FCC6CF0A639ACBDAD3D96 /* Pods-RunnerTests.profile.xcconfig */,
CD9846946CA680DD2E0721FA /* Pods-Runner.debug.xcconfig */,
4852C04805E85DB32D31EEC2 /* Pods-Runner.release.xcconfig */,
280769B5B4FB941FA7D35C06 /* Pods-Runner.profile.xcconfig */,
46859C536D3D0051CC82E6C5 /* Pods-RunnerTests.debug.xcconfig */,
76274BCD123CDD5AF172875F /* Pods-RunnerTests.release.xcconfig */,
2C91016A31D6647548E59DC8 /* Pods-RunnerTests.profile.xcconfig */,
);
path = Pods;
sourceTree = "<group>";
@ -110,8 +110,8 @@
isa = PBXGroup;
children = (
0AD944132DDA0C0D00557D21 /* AccessorySetupKit.framework */,
78CE6F02322D8DF27DF90403 /* Pods_Runner.framework */,
C7149989675CB5D71A7427D0 /* Pods_RunnerTests.framework */,
EFDA2C0DD7A7D82CA03799B2 /* Pods_Runner.framework */,
162D67C21ED243BA396CEF5C /* Pods_RunnerTests.framework */,
);
name = Frameworks;
sourceTree = "<group>";
@ -180,7 +180,7 @@
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
6A1C670EB5A3ADA19461D677 /* [CP] Check Pods Manifest.lock */,
C9BCBB2ED2D63153791BA331 /* [CP] Check Pods Manifest.lock */,
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
B3515CD9628E3332AFC88CD4 /* Frameworks */,
@ -199,7 +199,7 @@
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
45B9D3C28EC20A3F8CDE1E87 /* [CP] Check Pods Manifest.lock */,
C8A928FCC4E7B91E60992CB3 /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
@ -207,8 +207,8 @@
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
0AD9449B2DE6F89600557D21 /* Remove Bitcode from YbridOpus */,
02A0CB1B0C100BBBA8592529 /* [CP] Embed Pods Frameworks */,
CE2FAAF80D6BD14A146F00CC /* [CP] Copy Pods Resources */,
7983395BAD15A5F148B66817 /* [CP] Embed Pods Frameworks */,
2F4CC5CCDA87D20DF15BA3FF /* [CP] Copy Pods Resources */,
);
buildRules = (
);
@ -287,40 +287,40 @@
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
02A0CB1B0C100BBBA8592529 /* [CP] Embed Pods Frameworks */ = {
0AD9449B2DE6F89600557D21 /* Remove Bitcode from YbridOpus */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
inputPaths = (
);
name = "Remove Bitcode from YbridOpus";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
shellScript = "# 移除YbridOpus框架中的bitcode\necho \"检查并移除YbridOpus框架中的bitcode...\"\n\nFRAMEWORK_PATHS=(\n \"${PODS_ROOT}/YbridOpus/YbridOpus.xcframework/ios-arm64_armv7/YbridOpus.framework/YbridOpus\"\n \"${PODS_ROOT}/YbridOpus/YbridOpus.xcframework/ios-i386_x86_64-simulator/YbridOpus.framework/YbridOpus\"\n \"${PODS_ROOT}/YbridOpus/YbridOpus.xcframework/ios-arm64_x86_64-maccatalyst/YbridOpus.framework/YbridOpus\"\n \"${PODS_ROOT}/YbridOpus/YbridOpus.xcframework/macos-arm64_x86_64/YbridOpus.framework/YbridOpus\"\n)\n\nfor framework_path in \"${FRAMEWORK_PATHS[@]}\"; do\n if [ -f \"$framework_path\" ]; then\n if otool -l \"$framework_path\" | grep -q __LLVM; then\n echo \"移除bitcode: $framework_path\"\n # 直接覆盖,不创建备份\n xcrun bitcode_strip -r \"$framework_path\" -o \"$framework_path.tmp\"\n mv \"$framework_path.tmp\" \"$framework_path\"\n fi\n fi\ndone\n\n# 清理任何可能的备份文件\nfind \"${PODS_ROOT}\" -name \"*.backup\" -type f -delete 2>/dev/null || true\nfind \"${BUILT_PRODUCTS_DIR}\" -name \"*.backup\" -type f -delete 2>/dev/null || true\n\necho \"bitcode移除和清理完成!\"";
};
0AD9449B2DE6F89600557D21 /* Remove Bitcode from YbridOpus */ = {
2F4CC5CCDA87D20DF15BA3FF /* [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 = (
);
name = "Remove Bitcode from YbridOpus";
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
);
outputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "# 移除YbridOpus框架中的bitcode\necho \"检查并移除YbridOpus框架中的bitcode...\"\n\nFRAMEWORK_PATHS=(\n \"${PODS_ROOT}/YbridOpus/YbridOpus.xcframework/ios-arm64_armv7/YbridOpus.framework/YbridOpus\"\n \"${PODS_ROOT}/YbridOpus/YbridOpus.xcframework/ios-i386_x86_64-simulator/YbridOpus.framework/YbridOpus\"\n \"${PODS_ROOT}/YbridOpus/YbridOpus.xcframework/ios-arm64_x86_64-maccatalyst/YbridOpus.framework/YbridOpus\"\n \"${PODS_ROOT}/YbridOpus/YbridOpus.xcframework/macos-arm64_x86_64/YbridOpus.framework/YbridOpus\"\n)\n\nfor framework_path in \"${FRAMEWORK_PATHS[@]}\"; do\n if [ -f \"$framework_path\" ]; then\n if otool -l \"$framework_path\" | grep -q __LLVM; then\n echo \"移除bitcode: $framework_path\"\n # 直接覆盖,不创建备份\n xcrun bitcode_strip -r \"$framework_path\" -o \"$framework_path.tmp\"\n mv \"$framework_path.tmp\" \"$framework_path\"\n fi\n fi\ndone\n\n# 清理任何可能的备份文件\nfind \"${PODS_ROOT}\" -name \"*.backup\" -type f -delete 2>/dev/null || true\nfind \"${BUILT_PRODUCTS_DIR}\" -name \"*.backup\" -type f -delete 2>/dev/null || true\n\necho \"bitcode移除和清理完成!\"";
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0;
};
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
@ -338,80 +338,80 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
45B9D3C28EC20A3F8CDE1E87 /* [CP] Check Pods Manifest.lock */ = {
7983395BAD15A5F148B66817 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${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-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
6A1C670EB5A3ADA19461D677 /* [CP] Check Pods Manifest.lock */ = {
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
name = "Run Script";
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
);
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";
showEnvVarsInLog = 0;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
C8A928FCC4E7B91E60992CB3 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
name = "Run Script";
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
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;
};
CE2FAAF80D6BD14A146F00CC /* [CP] Copy Pods Resources */ = {
C9BCBB2ED2D63153791BA331 /* [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 */
@ -543,7 +543,7 @@
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 49AB99AA281C6CC7FD875C0B /* Pods-RunnerTests.debug.xcconfig */;
baseConfigurationReference = 46859C536D3D0051CC82E6C5 /* Pods-RunnerTests.debug.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
@ -562,7 +562,7 @@
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 4EC8693222B4131484101683 /* Pods-RunnerTests.release.xcconfig */;
baseConfigurationReference = 76274BCD123CDD5AF172875F /* Pods-RunnerTests.release.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
@ -579,7 +579,7 @@
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = CC1FCC6CF0A639ACBDAD3D96 /* Pods-RunnerTests.profile.xcconfig */;
baseConfigurationReference = 2C91016A31D6647548E59DC8 /* Pods-RunnerTests.profile.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;

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

@ -340,7 +340,7 @@ class PairingController extends GetxController {
}
// 设置配对超时保护
_stateTimer = Timer(const Duration(seconds: 10), () {
_stateTimer = Timer(const Duration(seconds: 30), () {
if (state == PairingState.pairing) {
Logger.e(_tag, 'BLE配对超时');
_transition(PairingState.exitTimeout);

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

@ -43,10 +43,10 @@ dependencies {
implementation("androidx.security:security-crypto:1.1.0-alpha03")
// 依赖本地其他插件
add("compileOnly", project(":azure_speech"))
add("compileOnly", project(":chat_api"))
add("compileOnly", project(":chat_storage"))
add("compileOnly", project(":ble_service"))
add("compileOnly", project(":music_service"))
// add("compileOnly", project(":amap_service"))
add("compileOnly", project(":speech"))
add("compileOnly", project(":open_ai_service"))
}

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

@ -4,7 +4,7 @@ import android.content.Context
import org.json.JSONArray
import org.json.JSONObject
import com.yunqiinnovation.azure_speech.AzureAsrHelper
import com.yunqiinnovation.chat_api.ChatApiService
import com.yunqiinnovation.open_ai_service.OpenAIService
import com.yunqiinnovation.chat_storage.ChatStorageHelper
import com.yunqiinnovation.music_service.MusicServiceStarter
import android.util.Log
@ -56,8 +56,8 @@ object AgentService : CoroutineScope {
// 使用AzureTtsHelper作为唯一的TTS实现
private var ttsService: AzureTtsHelper? = null
// OpenAI服务
private lateinit var openAIService: ChatApiService
// OpenAI服务 - 修改为使用OpenAIService
private lateinit var openAIService: OpenAIService
// 聊天存储服务
private lateinit var chatStorageHelper: ChatStorageHelper
@ -125,7 +125,7 @@ object AgentService : CoroutineScope {
try {
// 初始化上下文和服务
this.context = appContext.applicationContext
this.openAIService = ChatApiService(context)
this.openAIService = OpenAIService(context)
this.chatStorageHelper = ChatStorageHelper(context)
this.audioPlayer = AudioPlayer(context)
@ -185,6 +185,11 @@ object AgentService : CoroutineScope {
azureAsrHelper?.dispose()
// 释放OpenAI服务
if (::openAIService.isInitialized) {
openAIService.cancelAll()
}
// 释放TTS服务
ttsService?.release()
ttsService = null
@ -662,8 +667,8 @@ object AgentService : CoroutineScope {
}
openAIService.sendMessageStream(
messages = messagesWithSystemPrompt,
callback = object : ChatApiService.StreamCallback {
messagesWithSystemPrompt,
object : OpenAIService.StreamCallback {
override fun onToken(token: String) {
responseBuilder.append(token)
if (speakResponse) {

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

File diff suppressed because it is too large

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

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

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

@ -1,12 +0,0 @@
#import "AgentServicePlugin.h"
#if __has_include(<agent_service/agent_service-Swift.h>)
#import <agent_service/agent_service-Swift.h>
#else
#import "agent_service-Swift.h"
#endif
@implementation AgentServicePlugin
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
[AgentServicePlugin registerWithRegistrar:registrar];
}
@end

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

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

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

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

4
local_plugins/agent_service/ios/agent_service/Package.swift

@ -28,7 +28,9 @@ let package = Package(
],
path: "Sources/agent_service",
resources: [
.process("../../../Resources")
.copy("Resources/calling.mp3"),
.copy("Resources/start.mp3"),
.copy("Resources/stop.mp3")
]
)
]

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

@ -885,6 +885,11 @@ class AudioPlayer {
private let audioQueue = DispatchQueue(label: "agent.audio.player", qos: .userInitiated)
private var resourcesBundle: Bundle {
#if SWIFT_PACKAGE
// 在 Swift Package 中使用 Bundle.module
return Bundle.module
#else
// 在其他情况下使用传统方式
let mainBundle = Bundle(for: AudioPlayer.self)
// 查找 agent_service_agent_service.bundle
@ -895,6 +900,7 @@ class AudioPlayer {
// 如果找不到特定bundle,返回主bundle(用于调试)
return mainBundle
#endif
}
func playStartSound() {

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

@ -21,65 +21,213 @@ public class AudioSessionHub {
public func begin(_ intent: AudioIntent) {
if intent == currentIntent && audioSession.isOtherAudioPlaying == false {
os_log("音频意图未变化且无其他音频播放,跳过设置: %{public}@", log: logger, type: .debug, String(describing: intent))
return
}
os_log("开始设置音频会话,意图: %{public}@", log: logger, type: .info, String(describing: intent))
currentIntent = intent
do {
switch intent {
case .voice:
os_log("设置语音模式: category=playAndRecord, mode=voiceChat", log: logger, type: .info)
// 第一步:设置音频类别和模式
try audioSession.setCategory(.playAndRecord,
mode: .voiceChat,
options: [.allowBluetooth, .mixWithOthers])
options: [.allowBluetooth, .defaultToSpeaker, .mixWithOthers])
os_log("✓ 音频类别设置成功: playAndRecord + voiceChat", log: logger, type: .info)
// 如果有蓝牙HFP设备,强制设置为首选输入
if let bluetoothInput = audioSession.availableInputs?.first(where: { $0.portType == .bluetoothHFP }) {
try audioSession.setPreferredInput(bluetoothInput)
os_log("已设置蓝牙HFP为首选输入: %{public}@", log: logger, type: .info, bluetoothInput.portName)
// 第二步:检查并设置蓝牙输入
os_log("检查可用输入设备...", log: logger, type: .info)
if let availableInputs = audioSession.availableInputs {
os_log("可用输入设备数量: %d", log: logger, type: .info, availableInputs.count)
for (index, input) in availableInputs.enumerated() {
os_log("输入设备[%d]: %{public}@ (%{public}@)", log: logger, type: .info,
index, input.portName, input.portType.rawValue)
}
// 查找蓝牙HFP设备
if let bluetoothInput = availableInputs.first(where: { $0.portType == .bluetoothHFP }) {
os_log("找到蓝牙HFP设备,尝试设置为首选输入: %{public}@", log: logger, type: .info, bluetoothInput.portName)
try audioSession.setPreferredInput(bluetoothInput)
os_log("✓ 蓝牙HFP输入设置成功: %{public}@", log: logger, type: .info, bluetoothInput.portName)
} else {
os_log("未找到蓝牙HFP设备", log: logger, type: .info)
}
} else {
os_log("⚠️ availableInputs 为 nil", log: logger, type: .default)
}
case .playback:
try audioSession.setCategory(.playback,
mode: .default,
options: [.allowBluetooth, .mixWithOthers])
os_log("设置播放模式: category=playback, mode=default", log: logger, type: .info)
// 检查当前音频会话状态
os_log("设置前状态检查:", log: logger, type: .info)
os_log(" - 当前类别: %{public}@", log: logger, type: .info, audioSession.category.rawValue)
os_log(" - 当前模式: %{public}@", log: logger, type: .info, audioSession.mode.rawValue)
os_log(" - 当前是否激活: %{public}@", log: logger, type: .info, audioSession.isOtherAudioPlaying ? "是" : "否")
os_log(" - 其他音频播放: %{public}@", log: logger, type: .info, audioSession.isOtherAudioPlaying ? "是" : "否")
// 如果当前已经激活,先尝试停用
do {
if audioSession.isOtherAudioPlaying {
os_log("检测到其他音频正在播放,先尝试停用当前会话", log: logger, type: .info)
try audioSession.setActive(false, options: .notifyOthersOnDeactivation)
os_log("✓ 成功停用当前音频会话", log: logger, type: .info)
}
} catch let deactivateError as NSError {
os_log("⚠️ 停用当前音频会话失败: %{public}@ (继续尝试设置)", log: logger, type: .default, deactivateError.localizedDescription)
}
// 尝试渐进式设置:先尝试最简单的设置
var settingSuccess = false
// 策略1: 只设置类别,不设置选项
if !settingSuccess {
do {
os_log("尝试策略1: 仅设置类别 (playback)", log: logger, type: .info)
try audioSession.setCategory(.playback)
os_log("✓ 策略1成功: 音频类别设置为 playback", log: logger, type: .info)
settingSuccess = true
} catch let error as NSError {
os_log("策略1失败: %{public}@ (OSStatus: %d)", log: logger, type: .info, error.localizedDescription, error.code)
}
}
// 策略2: 设置类别和模式,不设置选项
if !settingSuccess {
do {
os_log("尝试策略2: 设置类别和模式 (playback + default)", log: logger, type: .info)
try audioSession.setCategory(.playback, mode: .default)
os_log("✓ 策略2成功: 音频类别和模式设置完成", log: logger, type: .info)
settingSuccess = true
} catch let error as NSError {
os_log("策略2失败: %{public}@ (OSStatus: %d)", log: logger, type: .info, error.localizedDescription, error.code)
}
}
// 策略3: 设置类别、模式和单个选项
if !settingSuccess {
do {
os_log("尝试策略3: 设置类别、模式和混音选项", log: logger, type: .info)
try audioSession.setCategory(.playback, mode: .default, options: [.mixWithOthers])
os_log("✓ 策略3成功: 音频类别、模式和混音选项设置完成", log: logger, type: .info)
settingSuccess = true
} catch let error as NSError {
os_log("策略3失败: %{public}@ (OSStatus: %d)", log: logger, type: .info, error.localizedDescription, error.code)
}
}
// 策略4: 设置类别、模式和蓝牙选项
if !settingSuccess {
do {
os_log("尝试策略4: 设置类别、模式和蓝牙选项", log: logger, type: .info)
try audioSession.setCategory(.playback, mode: .default, options: [.allowBluetoothA2DP])
os_log("✓ 策略4成功: 音频类别、模式和蓝牙选项设置完成", log: logger, type: .info)
settingSuccess = true
} catch let error as NSError {
os_log("策略4失败: %{public}@ (OSStatus: %d)", log: logger, type: .info, error.localizedDescription, error.code)
}
}
// 策略5: 完整设置(原始方案)
if !settingSuccess {
os_log("尝试策略5: 完整设置 (playback + default + 所有选项)", log: logger, type: .info)
try audioSession.setCategory(.playback,
mode: .default,
options: [.allowBluetoothA2DP, .mixWithOthers])
os_log("✓ 策略5成功: 完整音频设置完成", log: logger, type: .info)
settingSuccess = true
}
if !settingSuccess {
os_log("❌ 所有设置策略都失败了", log: logger, type: .error)
throw NSError(domain: "AudioSessionHub", code: -1, userInfo: [NSLocalizedDescriptionKey: "所有音频类别设置策略都失败"])
}
}
// 第三步:激活音频会话
os_log("尝试激活音频会话...", log: logger, type: .info)
try audioSession.setActive(true, options: .notifyOthersOnDeactivation)
os_log("✓ 音频会话激活成功", log: logger, type: .info)
os_log("音频会话已设置为:%{public}@", log: logger, type: .debug, String(describing: intent))
os_log("✅ 音频会话完整设置成功:%{public}@", log: logger, type: .info, String(describing: intent))
// 输出当前音频路由信息
let hasBluetoothDevice = hasBluetoothAudioDevice()
let hasBluetoothMic = hasBluetoothMicrophone()
let currentInput = getCurrentInputDevice()
os_log("蓝牙音频设备状态: %{public}@", log: logger, type: .info, hasBluetoothDevice ? "已连接" : "未连接")
os_log("蓝牙麦克风状态: %{public}@", log: logger, type: .info, hasBluetoothMic ? "已连接" : "未连接")
os_log("当前输入设备: %{public}@", log: logger, type: .info, currentInput)
os_log("📊 音频状态总览:", log: logger, type: .info)
os_log(" - 蓝牙音频设备: %{public}@", log: logger, type: .info, hasBluetoothDevice ? "已连接" : "未连接")
os_log(" - 蓝牙麦克风: %{public}@", log: logger, type: .info, hasBluetoothMic ? "已连接" : "未连接")
os_log(" - 当前输入设备: %{public}@", log: logger, type: .info, currentInput)
if hasBluetoothDevice {
os_log("%{public}@", log: logger, type: .debug, getCurrentAudioRoute())
os_log("📋 详细路由信息:\n%{public}@", log: logger, type: .debug, getCurrentAudioRoute())
}
} catch let error as NSError {
let osStatusCode = error.code
let domain = error.domain
let description = error.localizedDescription
os_log("❌ 设置音频会话失败", log: logger, type: .error)
os_log(" - 错误域: %{public}@", log: logger, type: .error, domain)
os_log(" - OSStatus代码: %d (0x%X)", log: logger, type: .error, osStatusCode, osStatusCode)
os_log(" - 错误描述: %{public}@", log: logger, type: .error, description)
os_log(" - 当前意图: %{public}@", log: logger, type: .error, String(describing: intent))
// 解析具体的 OSStatus 错误
switch osStatusCode {
case -50:
os_log(" ➤ 错误分析: OSStatus -50 通常表示参数错误 (kAudioSessionInvalidPropertyError)", log: logger, type: .error)
case -560557673: // kAudioSessionNotInitialized
os_log(" ➤ 错误分析: 音频会话未初始化", log: logger, type: .error)
case -560030580: // kAudioSessionBadPropertySizeError
os_log(" ➤ 错误分析: 属性大小错误", log: logger, type: .error)
case -560161140: // kAudioSessionIncompatibleCategory
os_log(" ➤ 错误分析: 音频类别不兼容", log: logger, type: .error)
default:
os_log(" ➤ 错误分析: 未知的OSStatus错误代码", log: logger, type: .error)
}
} catch {
os_log("设置音频会话失败:%{public}@", log: logger, type: .error, error.localizedDescription)
// 输出当前音频会话状态用于调试
os_log("🔍 当前音频会话状态:", log: logger, type: .error)
os_log(" - 类别: %{public}@", log: logger, type: .error, audioSession.category.rawValue)
os_log(" - 模式: %{public}@", log: logger, type: .error, audioSession.mode.rawValue)
os_log(" - 是否激活: %{public}@", log: logger, type: .error, audioSession.isOtherAudioPlaying ? "是" : "否")
os_log(" - 其他音频播放: %{public}@", log: logger, type: .error, audioSession.isOtherAudioPlaying ? "是" : "否")
}
}
public func appWillEnterForeground() {
os_log("📱 应用即将进入前台", log: logger, type: .info)
if let intent = currentIntent {
os_log("恢复之前的音频意图: %{public}@", log: logger, type: .info, String(describing: intent))
begin(intent)
} else {
os_log("无之前的音频意图需要恢复", log: logger, type: .info)
}
}
public func appDidEnterBackground() {
os_log("📱 应用进入后台,准备释放音频会话", log: logger, type: .info)
do {
try audioSession.setActive(false, options: .notifyOthersOnDeactivation)
os_log("音频会话已释放(应用进入后台)", log: logger, type: .debug)
} catch {
os_log("释放音频会话失败:%{public}@", log: logger, type: .error, error.localizedDescription)
os_log("✓ 音频会话已释放(应用进入后台)", log: logger, type: .info)
} catch let error as NSError {
let osStatusCode = error.code
let domain = error.domain
let description = error.localizedDescription
os_log("❌ 释放音频会话失败", log: logger, type: .error)
os_log(" - 错误域: %{public}@", log: logger, type: .error, domain)
os_log(" - OSStatus代码: %d (0x%X)", log: logger, type: .error, osStatusCode, osStatusCode)
os_log(" - 错误描述: %{public}@", log: logger, type: .error, description)
}
}
@ -168,9 +316,12 @@ public class AudioSessionHub {
/// 处理音频中断
@objc private func handleAudioSessionInterruption(notification: Notification) {
os_log("🔊 收到音频会话中断通知", log: logger, type: .info)
guard let userInfo = notification.userInfo,
let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
let type = AVAudioSession.InterruptionType(rawValue: typeValue) else {
os_log("⚠️ 无法解析音频中断通知", log: logger, type: .default)
return
}
@ -178,59 +329,105 @@ public class AudioSessionHub {
case .began:
// 中断开始
isInterrupted = true
os_log("音频会话被中断", log: logger, type: .debug)
os_log("🚫 音频会话中断开始", log: logger, type: .info)
os_log(" - 中断状态: %{public}@", log: logger, type: .info, isInterrupted ? "是" : "否")
case .ended:
// 中断结束
isInterrupted = false
os_log("✅ 音频会话中断结束", log: logger, type: .info)
// 检查是否应该恢复
if let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt {
let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
os_log("中断选项: %d, 应该恢复: %{public}@", log: logger, type: .info, optionsValue, options.contains(.shouldResume) ? "是" : "否")
if options.contains(.shouldResume), let intent = currentIntent {
os_log("恢复之前的音频意图: %{public}@", log: logger, type: .info, String(describing: intent))
// 恢复之前的音频意图
begin(intent)
os_log("音频会话中断结束,已恢复", log: logger, type: .debug)
os_log("✓ 音频会话中断结束,已恢复", log: logger, type: .info)
} else {
os_log("不需要恢复音频会话", log: logger, type: .info)
}
} else {
os_log("⚠️ 中断通知中无选项信息", log: logger, type: .default)
}
@unknown default:
os_log("未知的音频中断类型: %{public}@", log: logger, type: .error, "\(type)")
os_log("❓ 未知的音频中断类型: %{public}@", log: logger, type: .error, "\(type)")
}
}
/// 处理音频路由变化
@objc private func handleAudioRouteChange(notification: Notification) {
os_log("🔄 收到音频路由变化通知", log: logger, type: .info)
guard let userInfo = notification.userInfo,
let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt,
let reason = AVAudioSession.RouteChangeReason(rawValue: reasonValue) else {
os_log("⚠️ 无法解析音频路由变化通知", log: logger, type: .default)
return
}
os_log("路由变化原因: %d (%{public}@)", log: logger, type: .info, reasonValue, "\(reason)")
switch reason {
case .newDeviceAvailable:
os_log("新音频设备已连接", log: logger, type: .info)
os_log("%{public}@", log: logger, type: .debug, getCurrentAudioRoute())
os_log("📱 新音频设备已连接", log: logger, type: .info)
os_log("📋 当前音频路由:\n%{public}@", log: logger, type: .info, getCurrentAudioRoute())
// 重新应用当前意图以确保正确路由
if let intent = currentIntent {
os_log("重新应用当前音频意图: %{public}@", log: logger, type: .info, String(describing: intent))
begin(intent)
} else {
os_log("无当前音频意图需要重新应用", log: logger, type: .info)
}
case .oldDeviceUnavailable:
os_log("音频设备已断开", log: logger, type: .info)
os_log("%{public}@", log: logger, type: .debug, getCurrentAudioRoute())
os_log("📱 音频设备已断开", log: logger, type: .info)
// 输出之前的路由信息(如果可用)
if let previousRoute = userInfo[AVAudioSessionRouteChangePreviousRouteKey] as? AVAudioSessionRouteDescription {
os_log("之前的音频路由:", log: logger, type: .info)
for output in previousRoute.outputs {
os_log(" - 输出: %{public}@ (%{public}@)", log: logger, type: .info, output.portName, output.portType.rawValue)
}
for input in previousRoute.inputs {
os_log(" - 输入: %{public}@ (%{public}@)", log: logger, type: .info, input.portName, input.portType.rawValue)
}
}
os_log("📋 当前音频路由:\n%{public}@", log: logger, type: .info, getCurrentAudioRoute())
// 重新应用当前意图以确保正确路由
if let intent = currentIntent {
os_log("重新应用当前音频意图: %{public}@", log: logger, type: .info, String(describing: intent))
begin(intent)
} else {
os_log("无当前音频意图需要重新应用", log: logger, type: .info)
}
case .categoryChange:
os_log("音频类别已变化", log: logger, type: .info)
os_log("🔧 音频类别已变化", log: logger, type: .info)
os_log(" - 当前类别: %{public}@", log: logger, type: .info, audioSession.category.rawValue)
os_log(" - 当前模式: %{public}@", log: logger, type: .info, audioSession.mode.rawValue)
case .override:
os_log("🔀 音频路由被覆盖", log: logger, type: .info)
default:
break
case .wakeFromSleep:
os_log("😴 从睡眠中唤醒", log: logger, type: .info)
case .noSuitableRouteForCategory:
os_log("❌ 当前类别没有合适的路由", log: logger, type: .default)
case .routeConfigurationChange:
os_log("⚙️ 路由配置发生变化", log: logger, type: .info)
@unknown default:
os_log("❓ 未知的路由变化原因: %d", log: logger, type: .default)
}
}

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

7
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCompanionManager.kt

@ -327,6 +327,13 @@ class BleCompanionManager(private val context: Context) {
// 清除临时MAC地址
pendingMacAddress = null
// 🎯 立即发起BLE连接
try {
Log.i(TAG, "配对成功,立即发起BLE连接: $deviceMac")
BleService.connect(deviceMac)
} catch (e: Exception) {
Log.e(TAG, "配对成功后发起BLE连接失败: ${e.message}", e)
}
return true
} else {

Loading…
Cancel
Save