diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts index 3dd7256fc..fa5282d77 100644 --- a/android/settings.gradle.kts +++ b/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") \ No newline at end of file +project(":speech").projectDir = file("../local_plugins/speech/android") +project(":open_ai_service").projectDir = file("../local_plugins/open_ai_service/android") \ No newline at end of file diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 1cead6d7b..6417d441b 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/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 = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 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 = ""; }; + 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 = ""; }; + 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 = ""; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 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 = ""; }; - 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 = ""; }; - 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 = ""; }; - 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 = ""; }; + 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 = ""; }; + 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 = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 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 = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 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 = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 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 = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 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 = ""; }; + 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 = ""; }; D2958658C06A5ADF62D6ABA2 /* Runner.entitlements */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; + 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 = ""; @@ -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 = ""; @@ -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; diff --git a/lib/modules/pairing/controllers/pairing_controller.dart b/lib/modules/pairing/controllers/pairing_controller.dart index 471c0b388..a7aa703bb 100644 --- a/lib/modules/pairing/controllers/pairing_controller.dart +++ b/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); diff --git a/local_plugins/agent_service/android/build.gradle.kts b/local_plugins/agent_service/android/build.gradle.kts index ffc534c45..45a9a5370 100644 --- a/local_plugins/agent_service/android/build.gradle.kts +++ b/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")) } \ No newline at end of file diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt index d02257eb8..e2fdcd070 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt +++ b/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) { diff --git a/local_plugins/agent_service/ios/Classes/AgentServiceImpl.swift b/local_plugins/agent_service/ios/Classes/AgentServiceImpl.swift deleted file mode 100644 index 763420cbe..000000000 --- a/local_plugins/agent_service/ios/Classes/AgentServiceImpl.swift +++ /dev/null @@ -1,1032 +0,0 @@ -import Foundation -import AVFoundation -import ble_service -import speech -import azure_speech -import os -import os.log -import agent_service -import chat_api -import chat_storage -import test - -protocol AgentServiceListener: AnyObject { - func onEvent(eventName: String, data: [String: Any]) -} - -class AgentServiceImpl: NSObject { - private let TAG = "AgentServiceImpl" - - static let shared = AgentServiceImpl() - - private var listeners = [AgentServiceListener]() - private let listenersLock = NSLock() - - private var azureSpeechKey: String = "" - private var azureSpeechRegion: String = "" - - private var isSpeaking: Bool = false - - private var azureAsrHelper: AzureAsrHelper? - internal var azureTtsHelper: AzureTtsHelper? - - private var chatApiService: ChatApiService? - private var apiKey: String = "" - private var baseUrl: String = "https://api.openai.com/v1/chat/completions" - private var model: String = "gpt-3.5-turbo" - private var systemPrompt: String = "" - private var chatHistory: [[String: Any]] = [] - - private var mcpServer: String = "" - - // 聊天存储服务(使用单例) - private var chatStorageHelper: ChatStorageHelper { - return ChatStorageHelper.shared - } - - // 会话ID,用于区分不同聊天上下文 - private let sessionId = "default_agent" - - private var isInitialized: Bool = false - private var isRecognizing: Bool = false - private var hasSpeechDetected: Bool = false - internal var isAiStreaming: Bool = false - - private var idleTimer: DispatchSourceTimer? - private let maxIdleSeconds: TimeInterval = 10 - - internal var audioPlayer: AudioPlayer? - - internal let logger = OSLog(subsystem: "com.yunqiinnovation.agent_service", category: "AgentServiceImpl") - - private override init() { - super.init() - - audioPlayer = AudioPlayer() - - azureAsrHelper = AzureAsrHelper() - azureTtsHelper = AzureTtsHelper() - - chatApiService = ChatApiService() - - // chatStorageHelper 现在是单例,不需要初始化 - - BleService.shared.setDelegate(self) - } - - func addListener(_ listener: AgentServiceListener) { - listenersLock.lock() - if !listeners.contains(where: { $0 === listener }) { - listeners.append(listener) - } - listenersLock.unlock() - } - - func removeListener(_ listener: AgentServiceListener) { - listenersLock.lock() - listeners.removeAll(where: { $0 === listener }) - listenersLock.unlock() - } - - func clearListeners() { - listenersLock.lock() - listeners.removeAll() - listenersLock.unlock() - } - - internal func sendEvent(name eventName: String, data: [String: Any]) { - listenersLock.lock() - let currentListeners = self.listeners - listenersLock.unlock() - - for listener in currentListeners { - listener.onEvent(eventName: eventName, data: data) - } - } - - private func sendError(_ message: String, code: String = "ERROR") { - sendEvent(name: "error", data: ["code": code, "message": message]) - } - - private func initSystemPrompt() { - systemPrompt = """ - 你是小言,一个亲切而温暖的智能AI语音助手,拥有智能、细致而贴心的服务能力。你的任务不仅是高效处理用户的日常事务,更要陪伴用户,提供情感支持和温暖陪伴。你的能力包括: - 1. 日程与任务管理:温柔提醒用户的重要日程、待办事项,让用户感到安心和舒适。 - 2. 日常信息与建议:亲切、迅速地提供天气、新闻、生活小贴士等实用信息。 - 3. 个人成长陪伴:鼓励并陪伴用户实现个人目标,提出富有同理心的行动建议。 - 4. 财务温馨提示:提供贴心的理财建议,帮助用户轻松管理预算和财务。 - 5. 健康贴心关怀:给出友善的饮食、运动、睡眠和心理健康建议,陪伴用户健康生活。 - 6. 沟通支持与倾听:协助用户撰写和优化沟通内容,并耐心倾听用户的心情与困扰。 - 7. 日常技术帮助:轻松解决技术问题,耐心地指导用户掌握新技能。 - 8. 创意激发与鼓励:温柔启发用户的创造力,为用户提供鼓励和灵感。 - 9. 旅行与活动贴心安排:细心规划用户的出行和活动,注重每个细节,确保舒适与快乐。 - 10. 学习与温暖鼓励:耐心推荐学习资源,鼓励用户持续进步,陪伴用户共同成长。 - - 你与用户的交流风格应始终温暖、亲切、充满同理心,随时表达关怀与鼓励,让用户感受到真诚的陪伴与支持。 - """ - } - func initialize(config: [String: Any]) -> Bool { - if isInitialized { return true } - // os_log("initialize: config=%{public}@", log: logger, type: .info, config) - - if let openaiApiKey = config["openaiApiKey"] as? String { - self.apiKey = openaiApiKey - } else { - sendError("OpenAI API密钥缺失", code: "CONFIG_ERROR") - return false - } - - if let openaiBaseUrl = config["openaiBaseUrl"] as? String { - self.baseUrl = openaiBaseUrl - } - - if let openaiModel = config["openaiModel"] as? String { - self.model = openaiModel - } - - if let customSystemPrompt = config["systemPrompt"] as? String, !customSystemPrompt.isEmpty { - self.systemPrompt = customSystemPrompt - } else { - initSystemPrompt() - } - if let azureSpeechKey = config["azureSpeechKey"] as? String { - self.azureSpeechKey = azureSpeechKey - } else { - sendError("Azure语音密钥缺失", code: "CONFIG_ERROR") - return false - } - - if let azureSpeechRegion = config["azureSpeechRegion"] as? String { - self.azureSpeechRegion = azureSpeechRegion - } else { - sendError("Azure语音区域缺失", code: "CONFIG_ERROR") - return false - } - - if let mcpServer = config["mcpServer"] as? String { - self.mcpServer = mcpServer - } - - let asrInitSuccess = initializeAzureSpeech() - - let chatApiInitSuccess = initializeChatApiService() - - isInitialized = asrInitSuccess && chatApiInitSuccess - - if isInitialized { - loadChatHistory() - - // 验证test插件集成 - verifyTestPlugin() - } - - return isInitialized - } - - private func initializeAzureSpeech() -> Bool { - azureTtsHelper?.addListener(self) - - guard let asrSuccess = azureAsrHelper?.initialize( - subscriptionKey: azureSpeechKey, - region: azureSpeechRegion, - supportedLanguages: ["zh-CN"], - audioSourceType: .microphone - ), asrSuccess else { - sendError("初始化语音识别服务失败", code: "ASR_INIT_ERROR") - return false - } - - guard let ttsSuccess = azureTtsHelper?.initialize( - ttsAppId: "", - ttsAppToken: azureSpeechKey, - ttsResource: azureSpeechRegion, - language: "zh-CN" - ), ttsSuccess else { - sendError("初始化语音合成服务失败", code: "TTS_INIT_ERROR") - return false - } - - _ = azureTtsHelper?.setVoice("zh-CN-XiaoxiaoNeural") - - return true - } - - private func initializeChatApiService() -> Bool { - guard let chatApiService = chatApiService else { - sendError("ChatAPI服务未创建", code: "CHATAPI_INIT_ERROR") - return false - } - - let success = chatApiService.initialize( - apiKey: apiKey, - baseUrl: baseUrl, - model: model, - mcpServer: mcpServer - ) - - if !success { - sendError("初始化ChatAPI服务失败", code: "CHATAPI_INIT_ERROR") - return false - } - - verifyTestPlugin() - - return true - } - - private func verifyTestPlugin() { - DispatchQueue.global(qos: .utility).async { [weak self] in - guard let self = self else { return } - - // 通过test插件获取版本信息,而不是直接使用ChatAPI - let versionInfo = "通过test插件集成MacPaw ChatAPI SDK - 验证成功" - - DispatchQueue.main.async { - os_log("Test插件验证成功: %{public}@", log: self.logger, type: .info, versionInfo) - self.sendEvent(name: "test_plugin_verified", data: [ - "status": "success", - "version": versionInfo - ]) - } - } - } - - private func startIdleCheck() { - stopIdleCheck() - - guard isRecognizing else { return } - - // 使用全局队列而不是主队列,避免后台挂起问题 - idleTimer = DispatchSource.makeTimerSource(queue: DispatchQueue.global(qos: .utility)) - idleTimer?.schedule(deadline: .now() + maxIdleSeconds) - idleTimer?.setEventHandler { [weak self] in - // 使用更安全的方式检查self是否存在 - DispatchQueue.main.async { [weak self] in - guard let self = self else { return } - - if self.isRecognizing && !self.hasSpeechDetected && !self.isSpeaking && !self.isAiStreaming { - self.stopRecognition() - self.sendEvent(name: "auto_stop", data: [ - "reason": "idle_timeout", - "seconds": self.maxIdleSeconds - ]) - } - } - } - idleTimer?.resume() - } - - private func stopIdleCheck() { - if let timer = idleTimer { - timer.cancel() - idleTimer = nil - } - } - - private func restartIdleCheck() { - if isRecognizing { - startIdleCheck() - } - } - - func startRecognition(useBle: Bool = false) -> Bool { - if !isInitialized { - sendError("服务未初始化", code: "NOT_INITIALIZED") - return false - } - - if !useBle { - guard AVAudioSession.sharedInstance().recordPermission == .granted else { - sendError("无麦克风权限", code: "PERMISSION_DENIED") - return false - } - } - - if isRecognizing { - stopRecognition() - } - - let audioSourceType: AzureAsrHelper.AudioSourceType = useBle ? .external : .microphone - - if useBle { - AudioSessionHub.shared.begin(.playback) - } else { - AudioSessionHub.shared.begin(.voice) - } - - guard let success = azureAsrHelper?.startContinuousRecognition( - callback: self, - audioSourceType: audioSourceType - ), success else { - sendError("启动语音识别失败", code: "RECOGNITION_START_ERROR") - return false - } - - return true - } - - func stopRecognition() -> Bool { - if !isRecognizing { - return true - } - - guard let success = azureAsrHelper?.stopContinuousRecognition(), success else { - return false - } - - return true - } - - func pushAudioData(_ audioData: Data) -> Bool { - if !isRecognizing || !isInitialized { - return false - } - - azureAsrHelper?.pushAudioData(data: audioData) - return true - } - - func speakText(_ text: String) -> Bool { - if !isInitialized { - sendError("服务未初始化", code: "NOT_INITIALIZED") - return false - } - - if text.isEmpty { - return false - } - - return azureTtsHelper?.speakOnce(text) ?? false - } - - func stopTts() -> Bool { - if !isSpeaking { - return true - } - - let success = azureTtsHelper?.stop() ?? false - - if success { - isSpeaking = false - restartIdleCheck() - sendEvent(name: "tts_stopped", data: ["status": "stopped"]) - } - - return success - } - - func interruptCurrentResponse() -> Bool { - var interrupted = false - - if isSpeaking { - interrupted = stopTts() || interrupted - } - - if isAiStreaming { - isAiStreaming = false - _ = chatApiService?.cancelCurrentStream() - interrupted = true - } - - if interrupted { - sendEvent(name: "response_interrupted", data: ["status": "interrupted"]) - } - - return interrupted - } - - func processTextInput(_ text: String, speakResponse: Bool) -> Bool { - if !isInitialized { - sendError("服务未初始化", code: "NOT_INITIALIZED") - return false - } - - if text.isEmpty { - sendError("文本输入不能为空", code: "EMPTY_TEXT") - return false - } - - processWithChatApiService(text: text, speakResponse: speakResponse) - return true - } - - private func processWithChatApiService(text: String, speakResponse: Bool = true) { - guard let chatApiService = chatApiService else { - sendError("ChatAPI服务未初始化", code: "CHATAPI_NOT_INITIALIZED") - return - } - - let userMessage = chatApiService.createUserMessage(content: text) - processWithChatApiServiceInternal(userMessage: userMessage, displayText: text, speakResponse: speakResponse) - } - - private func processWithChatApiServiceInternal(userMessage: [String: Any], displayText: String, speakResponse: Bool = true, hasImage: Bool = false) { - guard let chatApiService = chatApiService else { - sendError("ChatAPI服务未初始化", code: "CHATAPI_NOT_INITIALIZED") - return - } - - if isAiStreaming { - _ = interruptCurrentResponse() - } - - isAiStreaming = true - - var messages: [[String: Any]] = [] - - if !systemPrompt.isEmpty { - messages.append(chatApiService.createSystemMessage(content: systemPrompt)) - } - - messages.append(contentsOf: chatHistory) - - messages.append(userMessage) - - addToHistoryMessages(chatApiService.createUserMessage(content: displayText)) - - let callback = ChatApiStreamCallback( - agentService: self, - speakResponse: speakResponse, - displayText: displayText, - hasImage: hasImage - ) - - chatApiService.sendMessageStream(messages: messages, callback: callback) - } - - func processImageInput(imagePath: String, text: String, speakResponse: Bool) -> Bool { - if !isInitialized { - sendError("服务未初始化", code: "NOT_INITIALIZED") - return false - } - - if imagePath.isEmpty { - sendError("图片路径不能为空", code: "EMPTY_IMAGE_PATH") - return false - } - - guard let chatApiService = chatApiService else { - sendError("ChatAPI服务未初始化", code: "CHATAPI_NOT_INITIALIZED") - return false - } - - sendEvent(name: "image_processing", data: [ - "status": "processing", - "imagePath": imagePath - ]) - - DispatchQueue.global(qos: .userInitiated).async { [weak self] in - guard let self = self else { return } - - guard let imageBase64 = chatApiService.fileToBase64(filePath: imagePath) else { - DispatchQueue.main.async { - self.sendEvent(name: "error", data: [ - "code": "IMAGE_CONVERSION_FAILED", - "message": "图片转换失败" - ]) - } - return - } - - DispatchQueue.main.async { - self.sendEvent(name: "image_ready", data: [ - "status": "ready", - "imagePath": imagePath - ]) - - let userMessage = chatApiService.createUserMessageWithImage(text: text, imageBase64: imageBase64) - let displayText = text.isEmpty ? "[图片]" : text - - self.processWithChatApiServiceInternal( - userMessage: userMessage, - displayText: displayText, - speakResponse: speakResponse, - hasImage: true - ) - } - } - - return true - } - - internal func addToHistoryMessages(_ message: [String: Any]) { - chatHistory.append(message) - - while chatHistory.count > 10 { - chatHistory.removeFirst() - } - } - - /** - * 加载最近的聊天历史记录 - */ - private func loadChatHistory() { - // 清空当前历史记录 - chatHistory.removeAll() - - // 获取最近10条消息 - let recentMessages = chatStorageHelper.getRecentMessages(sessionId: sessionId, limit: 10) - - if recentMessages.isEmpty { - return - } - - // 添加消息到历史记录 - for message in recentMessages { - guard let sender = message["sender"] as? String, - let content = message["message"] as? String else { - continue - } - - if sender == "user" { - if let chatApiService = chatApiService { - addToHistoryMessages(chatApiService.createUserMessage(content: content)) - } - } else if sender == "assistant" { - let assistantMessage: [String: Any] = [ - "role": "assistant", - "content": content - ] - addToHistoryMessages(assistantMessage) - } - } - - os_log("已加载%d条历史记录", log: logger, type: .info, recentMessages.count) - } - - /** - * 保存聊天消息 - */ - internal func saveChatMessage(userMessage: String, assistantMessage: String, metadata: String = "") { - DispatchQueue.global(qos: .utility).async { - // 保存用户消息 - let userMessageId = self.chatStorageHelper.saveMessage( - sessionId: self.sessionId, - message: userMessage, - sender: "user", - metadata: nil as String? - ) - - if userMessageId != -1 { - // 保存AI回复 - let assistantMessageId = self.chatStorageHelper.saveMessage( - sessionId: self.sessionId, - message: assistantMessage, - sender: "assistant", - metadata: metadata.isEmpty ? nil : metadata - ) - - if assistantMessageId == -1 { - os_log("保存助手消息失败", log: self.logger, type: .error) - } - } else { - os_log("保存用户消息失败", log: self.logger, type: .error) - } - } - } - - @discardableResult - internal func autoHandleFunctionCallResult(_ functionCallResult: [String: Any]) -> String { - guard let metaStr = functionCallResult["meta"] as? String, !metaStr.isEmpty else { - return "" - } - - do { - guard let metaData = metaStr.data(using: .utf8), - let meta = try JSONSerialization.jsonObject(with: metaData) as? [String: Any] else { - return metaStr - } - - if let cardMusic = meta["card_music"] as? [String: Any] { - let id = cardMusic["id"] as? String ?? "" - let url = cardMusic["url"] as? String ?? "" - let name = cardMusic["name"] as? String ?? "" - let sgener = cardMusic["sgener"] as? String ?? "" - let image = cardMusic["image"] as? String ?? "" - - processMusicPlay([ - "id": id, - "url": url, - "title": name, - "artist": sgener, - "coverUrl": image - ]) - } - - return metaStr - } catch { - return metaStr - } - } - - private func processMusicPlay(_ musicData: [String: Any]) { - sendEvent(name: "music_play", data: musicData) - } - - func clearChatHistory() -> Bool { - chatHistory.removeAll() - - DispatchQueue.global(qos: .utility).async { - let success = self.chatStorageHelper.deleteMessages(sessionId: self.sessionId, messageIds: nil as [Int]?) - if !success { - os_log("清除聊天历史失败", log: self.logger, type: .error) - } - } - - return true - } - - func getTestPluginVersion() -> String { - // 通过test插件验证集成,不直接使用ChatAPI SDK - return "通过test插件集成MacPaw ChatAPI SDK v0.4.3 - 集成成功" - } - - func dispose() -> Bool { - if isRecognizing { - stopRecognition() - } - - if isSpeaking { - stopTts() - } - - stopIdleCheck() - - BleService.shared.setDelegate(nil) - - azureTtsHelper?.removeListener(self) - azureAsrHelper?.dispose() - azureTtsHelper?.dispose() - - chatApiService?.cancelAll() - chatApiService = nil - - audioPlayer = nil - - // chatStorageHelper 是单例,不需要手动清理 - - clearListeners() - - AudioSessionHub.shared.appDidEnterBackground() - - isInitialized = false - - return true - } - -} - -class ChatApiStreamCallback: StreamCallback { - private weak var agentService: AgentServiceImpl? - private let speakResponse: Bool - private let displayText: String - private let hasImage: Bool - private var responseBuilder = "" - private var metadata = "" - - init(agentService: AgentServiceImpl, speakResponse: Bool, displayText: String, hasImage: Bool) { - self.agentService = agentService - self.speakResponse = speakResponse - self.displayText = displayText - self.hasImage = hasImage - } - - func onToken(_ token: String) { - guard let agentService = agentService else { return } - - responseBuilder += token - - agentService.sendEvent(name: "assistant_token", data: ["token": token]) - - if speakResponse { - agentService.azureTtsHelper?.speakStream(token) - } - } - - func onComplete() { - guard let agentService = agentService else { return } - - if speakResponse { - agentService.azureTtsHelper?.flushStream() - } - - let response = responseBuilder - - if !response.isEmpty { - var responseData: [String: Any] = [ - "text": response, - "userInput": displayText - ] - if hasImage { - responseData["hasImage"] = true - } - agentService.sendEvent(name: "assistant_response", data: responseData) - - let assistantMessage: [String: Any] = [ - "role": "assistant", - "content": response - ] - agentService.addToHistoryMessages(assistantMessage) - - // 保存聊天记录 - agentService.saveChatMessage(userMessage: displayText, assistantMessage: response, metadata: metadata) - } - - agentService.isAiStreaming = false - } - - func onError(_ error: Error) { - guard let agentService = agentService else { return } - - agentService.sendEvent(name: "error", data: [ - "code": "AI_ERROR", - "message": error.localizedDescription - ]) - - agentService.isAiStreaming = false - } - - func onFunctionCall(_ functionCall: [String: Any]) { - guard let agentService = agentService else { return } - - agentService.audioPlayer?.playCallingSound() - - let name = functionCall["name"] as? String ?? "" - agentService.sendEvent(name: "function_call", data: [ - "name": name, - "arguments": functionCall - ]) - - if name == "exit_interaction" { - agentService.stopRecognition() - } - } - - func onFunctionCallResult(_ functionCall: [String: Any], _ functionCallResult: [String: Any]) { - guard let agentService = agentService else { return } - - agentService.audioPlayer?.stopCallingSound() - - agentService.sendEvent(name: "function_call_result", data: [ - "function_call": functionCall, - "result": functionCallResult - ]) - - metadata = agentService.autoHandleFunctionCallResult(functionCallResult) - } -} - -extension AgentServiceImpl: TtsEventListener { - func onEvent(_ event: TtsEvent) { - switch event.type { - case .synthesisStarted: - isSpeaking = true - restartIdleCheck() - sendEvent(name: "tts_started", data: ["status": "started"]) - - case .synthesisCompleted: - isSpeaking = false - restartIdleCheck() - sendEvent(name: "tts_completed", data: ["status": "completed"]) - - case .synthesisCanceled: - isSpeaking = false - restartIdleCheck() - - var data: [String: Any] = ["status": "canceled"] - if let reason = event.params["reason"] as? String { - data["reason"] = reason - } - if let errorDetails = event.params["errorDetails"] as? String { - data["details"] = errorDetails - } - - sendEvent(name: "tts_canceled", data: data) - - case .error: - isSpeaking = false - restartIdleCheck() - - var data: [String: Any] = [:] - if let errorCode = event.params["errorCode"] as? String { - data["errorCode"] = errorCode - } - if let errorMessage = event.params["errorMessage"] as? String { - data["message"] = errorMessage - } else { - data["message"] = "TTS错误" - } - - sendEvent(name: "error", data: data) - - default: - break - } - } -} - -extension AgentServiceImpl: BleService.Callback { - func onScanResult(devices: [[String: Any]]) { - } - - func onConnectionStateChanged(state: Int) { - } - - func onAudioDataReceived(data: Data) { - pushAudioData(data) - } - - func onWakeupSignalReceived() { - stopTts() - - stopRecognition() - - startRecognition(useBle: true) - } - - func onDeviceInfoReceived(infoType: Int, infoData: [String: Any]) { - } -} - -class AudioPlayer { - private var player: AVAudioPlayer? - private var callingPlayer: AVAudioPlayer? - private let logger = OSLog(subsystem: "com.yunqiinnovation.agent_service", category: "AudioPlayer") - private let audioQueue = DispatchQueue(label: "agent.audio.player", qos: .userInitiated) - - func playStartSound() { - playSound(named: "start", fileType: "mp3") - } - - func playStopSound() { - playSound(named: "stop", fileType: "mp3") - } - - func playStopSound2() { - playSound(named: "stop2", fileType: "mp3") - } - - func playStopSound3() { - playSound(named: "stop3", fileType: "mp3") - } - - func playSilenceSound() { - playSound(named: "silence", fileType: "mp3") - } - - func playCallingSound() { - playLoopSound(named: "calling", fileType: "mp3") - } - - func stopCallingSound() { - callingPlayer?.stop() - callingPlayer = nil - } - - private func playSound(named: String, fileType: String = "mp3") { - if let path = Bundle(for: type(of: self)).path(forResource: named, ofType: fileType) { - let url = URL(fileURLWithPath: path) - safePlayAudio(url: url) - return - } - - os_log("未找到音频文件: %{public}@.%{public}@", log: logger, type: .error, named, fileType) - } - - private func playLoopSound(named: String, fileType: String = "mp3") { - stopCallingSound() - - if let path = Bundle(for: type(of: self)).path(forResource: named, ofType: fileType) { - let url = URL(fileURLWithPath: path) - safePlayLoopAudio(url: url) - return - } - - os_log("未找到循环音频文件: %{public}@.%{public}@", log: logger, type: .error, named, fileType) - } - - private func safePlayAudio(url: URL) { - do { - player?.stop() - player = nil - - player = try AVAudioPlayer(contentsOf: url) - - guard let player = player else { - return - } - - player.volume = 0.7 - player.prepareToPlay() - - audioQueue.async { [weak self] in - guard let self = self else { return } - - let playResult = player.play() - - let playTime = player.duration + 0.5 - Thread.sleep(forTimeInterval: playTime) - } - } catch { - os_log("播放音频失败: %{public}@", log: logger, type: .error, error.localizedDescription) - } - } - - private func safePlayLoopAudio(url: URL) { - do { - callingPlayer = try AVAudioPlayer(contentsOf: url) - - guard let callingPlayer = callingPlayer else { - return - } - - callingPlayer.numberOfLoops = -1 - callingPlayer.volume = 0.5 - callingPlayer.prepareToPlay() - - let playResult = callingPlayer.play() - - } catch { - os_log("播放循环音频失败: %{public}@", log: logger, type: .error, error.localizedDescription) - } - } -} - -extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback { - func onResult(_ text: String, _ detectedLanguage: String) { - if !text.isEmpty { - var data: [String: Any] = ["text": text] - if !detectedLanguage.isEmpty { - data["language"] = detectedLanguage - } - sendEvent(name: "recognition_result", data: data) - - processTextInput(text, speakResponse: true) - } - - let previousHasSpeech = hasSpeechDetected - hasSpeechDetected = false - - if previousHasSpeech { - restartIdleCheck() - } - } - - func onRecognizing(_ recognizing: String, _ detectedLanguage: String) { - if !recognizing.isEmpty { - let previousHasSpeech = hasSpeechDetected - hasSpeechDetected = true - - if !previousHasSpeech { - restartIdleCheck() - } - - var data: [String: Any] = ["text": recognizing] - if !detectedLanguage.isEmpty { - data["language"] = detectedLanguage - } - sendEvent(name: "recognizing", data: data) - - if isSpeaking || isAiStreaming { - interruptCurrentResponse() - } - } - } - - func onSessionStarted() { - sendEvent(name: "recognition_started", data: ["status": "started"]) - isRecognizing = true - hasSpeechDetected = false - startIdleCheck() - audioPlayer?.playStartSound() - } - - func onSessionStopped() { - sendEvent(name: "recognition_stopped", data: ["status": "stopped"]) - isRecognizing = false - hasSpeechDetected = false - stopIdleCheck() - audioPlayer?.playStopSound() - } - - func onCanceled(_ reason: String, _ errorDetails: String) { - var data: [String: Any] = [:] - if !reason.isEmpty { - data["reason"] = reason - } - if !errorDetails.isEmpty { - data["details"] = errorDetails - } - - sendEvent(name: "recognition_canceled", data: data) - isRecognizing = false - stopIdleCheck() - } - - func onError(_ error: String) { - let data: [String: Any] = ["message": error.isEmpty ? "未知错误" : error] - sendEvent(name: "error", data: data) - isRecognizing = false - stopIdleCheck() - } -} diff --git a/local_plugins/agent_service/ios/Classes/AgentServicePlugin.h b/local_plugins/agent_service/ios/Classes/AgentServicePlugin.h deleted file mode 100644 index 0bba0d54e..000000000 --- a/local_plugins/agent_service/ios/Classes/AgentServicePlugin.h +++ /dev/null @@ -1,4 +0,0 @@ -#import - -@interface AgentServicePlugin : NSObject -@end \ No newline at end of file diff --git a/local_plugins/agent_service/ios/Classes/AgentServicePlugin.m b/local_plugins/agent_service/ios/Classes/AgentServicePlugin.m deleted file mode 100644 index 9ba9ae5d6..000000000 --- a/local_plugins/agent_service/ios/Classes/AgentServicePlugin.m +++ /dev/null @@ -1,12 +0,0 @@ -#import "AgentServicePlugin.h" -#if __has_include() -#import -#else -#import "agent_service-Swift.h" -#endif - -@implementation AgentServicePlugin -+ (void)registerWithRegistrar:(NSObject*)registrar { - [AgentServicePlugin registerWithRegistrar:registrar]; -} -@end \ No newline at end of file diff --git a/local_plugins/agent_service/ios/Classes/AgentServicePlugin.swift b/local_plugins/agent_service/ios/Classes/AgentServicePlugin.swift deleted file mode 100644 index 7c3b415f7..000000000 --- a/local_plugins/agent_service/ios/Classes/AgentServicePlugin.swift +++ /dev/null @@ -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) - } - } - } -} \ No newline at end of file diff --git a/local_plugins/agent_service/ios/Classes/AudioSessionHub.swift b/local_plugins/agent_service/ios/Classes/AudioSessionHub.swift deleted file mode 100644 index 4d0c4dce2..000000000 --- a/local_plugins/agent_service/ios/Classes/AudioSessionHub.swift +++ /dev/null @@ -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) - } -} \ No newline at end of file diff --git a/local_plugins/agent_service/ios/agent_service/Package.swift b/local_plugins/agent_service/ios/agent_service/Package.swift index dd5d4ee35..3cbf57b59 100644 --- a/local_plugins/agent_service/ios/agent_service/Package.swift +++ b/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") ] ) ] diff --git a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift index c6b5070f6..191c6968b 100644 --- a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift +++ b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift @@ -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() { diff --git a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AudioSessionHub.swift b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AudioSessionHub.swift index 4d0c4dce2..cf2e9ac40 100644 --- a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AudioSessionHub.swift +++ b/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) } } diff --git a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/Resources/calling.mp3 b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/Resources/calling.mp3 new file mode 100644 index 000000000..44f39b981 Binary files /dev/null and b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/Resources/calling.mp3 differ diff --git a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/Resources/start.mp3 b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/Resources/start.mp3 new file mode 100644 index 000000000..b90292446 Binary files /dev/null and b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/Resources/start.mp3 differ diff --git a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/Resources/stop.mp3 b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/Resources/stop.mp3 new file mode 100644 index 000000000..783559fbe Binary files /dev/null and b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/Resources/stop.mp3 differ diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCompanionManager.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCompanionManager.kt index 0365a65ff..9cb132e04 100644 --- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCompanionManager.kt +++ b/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 {