From 232c970134a4de66a50a1cf650255289304b6a22 Mon Sep 17 00:00:00 2001 From: wolfplus Date: Tue, 27 May 2025 21:39:21 +0100 Subject: [PATCH] add --- ios/Podfile.lock | 11 +- ios/Runner.xcodeproj/project.pbxproj | 90 ++++----- .../agent/controllers/agent_controller.dart | 6 +- .../agent_service/AgentService.kt | 81 ++------ .../ios/Classes/AgentServiceImpl.swift | 148 ++++++++++++-- .../agent_service/ios/agent_service.podspec | 1 + local_plugins/agent_service/pubspec.yaml | 2 + .../ble_service/ios/Classes/BleService.swift | 17 +- .../ios/Classes/ChatStorageHelper.swift | 182 +++++++++++++++--- .../ios/Classes/SwiftChatStoragePlugin.swift | 16 +- .../ios/Classes/SystemFunctionHandler.swift | 46 +++-- test.md | 45 ----- 12 files changed, 415 insertions(+), 230 deletions(-) delete mode 100644 test.md diff --git a/ios/Podfile.lock b/ios/Podfile.lock index defa1c412..71c4d5dee 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -2,6 +2,7 @@ PODS: - agent_service (0.0.1): - azure_speech - ble_service + - chat_storage - Flutter - open_ai_service - app_settings (5.1.1): @@ -18,7 +19,7 @@ PODS: - Flutter - azure_speech (0.0.1): - Flutter - - MicrosoftCognitiveServicesSpeech-iOS (~> 1.30.0) + - MicrosoftCognitiveServicesSpeech-iOS (~> 1.37.0) - speech - ble_service (0.0.1): - Flutter @@ -189,7 +190,7 @@ PODS: - Mantle (2.2.0): - Mantle/extobjc (= 2.2.0) - Mantle/extobjc (2.2.0) - - MicrosoftCognitiveServicesSpeech-iOS (1.30.0) + - MicrosoftCognitiveServicesSpeech-iOS (1.37.0) - music_service (0.0.1): - Flutter - open_ai_service (0.0.1): @@ -387,12 +388,12 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/webview_flutter_wkwebview/darwin" SPEC CHECKSUMS: - agent_service: 306557f82fd69335e216e989498bacff51c0b9e8 + agent_service: a95166e1939f18436aa2ae6b80c17751a2f69b89 app_settings: 5127ae0678de1dcc19f2293271c51d37c89428b2 AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73 audio_session: 9bb7f6c970f21241b19f5a3658097ae459681ba0 audio_waveforms: a6dde7fe7c0ea05f06ffbdb0f7c1b2b2ba6cedcf - azure_speech: 11927e53ab90a2def1370da1d44e4915a97c0290 + azure_speech: 23c6573d04e7f84a0839e935e527c87b5f78311f ble_service: 9f394ad44d435010e718bc81de494d9425295a85 chat_storage: 933a96144157a07a2fef7f6acb28c78ff18465d8 classic_bluetooth: c146e1618e867a47fa7ab79a3bf262e7d69d557d @@ -432,7 +433,7 @@ SPEC CHECKSUMS: just_audio: 4e391f57b79cad2b0674030a00453ca5ce817eed libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8 Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d - MicrosoftCognitiveServicesSpeech-iOS: f53991647f295e18407619816a8064908a8700f4 + MicrosoftCognitiveServicesSpeech-iOS: 766852ccd56e0c74bafbe9ea7033083c5f748d04 music_service: 85abecc8fa21599a06907ad50d83f48a58e0dc0d open_ai_service: d78dff9359ee4c04f20d250d655d6ec75b4c5434 package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index e61f51201..bdb0f3204 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -12,12 +12,12 @@ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 430D8C34FB614ED446186262 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9CB0EC76DB044A21D8535AA1 /* Pods_RunnerTests.framework */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; - C6017C52003F4D9508A3F089 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ADBF2962D094C0C73A4D70AD /* Pods_Runner.framework */; }; + 9EB82175436F3A3D593E4423 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E9725ECE141452716001447D /* Pods_Runner.framework */; }; + DD1E3297BCF7B8F42676149E /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 41D979DCCB9FC1A2739ED9E2 /* Pods_RunnerTests.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -44,21 +44,20 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 082E420FFD5DDCB63B0AF2D1 /* 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 = ""; }; + 0A9A0EEC032E92406E64632E /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; 0AD944112DD9DB5C00557D21 /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; 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 = ""; }; - 15653C8102986FFCD3D3A1FB /* 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 = ""; }; 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 = ""; }; - 419E468FD967D92BB8F09F89 /* 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 = ""; }; + 41D979DCCB9FC1A2739ED9E2 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 763E2B7DA86D69DF1C598385 /* 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 = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 8864DCDCFE5ECACAF8C8A05C /* 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 = ""; }; + 8960B97871604358D5CA1866 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 932005D16734B31A8CB0583F /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 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; }; @@ -66,10 +65,11 @@ 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 = ""; }; - 9CB0EC76DB044A21D8535AA1 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - ADBF2962D094C0C73A4D70AD /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - C1F336500F05EC22722503B5 /* 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 = ""; }; + BCF37DA3F60C9FDB47151D46 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; D2958658C06A5ADF62D6ABA2 /* Runner.entitlements */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; + D2A873E56435FC1342AE0A85 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + D7BFB5FE33B0270C30A1DFAA /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + E9725ECE141452716001447D /* 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 = ( 0A000A2C2DE1EE50005BE0F2 /* Opus in Frameworks */, - C6017C52003F4D9508A3F089 /* Pods_Runner.framework in Frameworks */, + 9EB82175436F3A3D593E4423 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -86,7 +86,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 430D8C34FB614ED446186262 /* Pods_RunnerTests.framework in Frameworks */, + DD1E3297BCF7B8F42676149E /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -96,12 +96,12 @@ 1ACB686EC7DC36C6E33D372F /* Pods */ = { isa = PBXGroup; children = ( - 763E2B7DA86D69DF1C598385 /* Pods-Runner.debug.xcconfig */, - 419E468FD967D92BB8F09F89 /* Pods-Runner.release.xcconfig */, - 8864DCDCFE5ECACAF8C8A05C /* Pods-Runner.profile.xcconfig */, - C1F336500F05EC22722503B5 /* Pods-RunnerTests.debug.xcconfig */, - 15653C8102986FFCD3D3A1FB /* Pods-RunnerTests.release.xcconfig */, - 082E420FFD5DDCB63B0AF2D1 /* Pods-RunnerTests.profile.xcconfig */, + D2A873E56435FC1342AE0A85 /* Pods-Runner.debug.xcconfig */, + 932005D16734B31A8CB0583F /* Pods-Runner.release.xcconfig */, + BCF37DA3F60C9FDB47151D46 /* Pods-Runner.profile.xcconfig */, + 0A9A0EEC032E92406E64632E /* Pods-RunnerTests.debug.xcconfig */, + 8960B97871604358D5CA1866 /* Pods-RunnerTests.release.xcconfig */, + D7BFB5FE33B0270C30A1DFAA /* Pods-RunnerTests.profile.xcconfig */, ); path = Pods; sourceTree = ""; @@ -110,8 +110,8 @@ isa = PBXGroup; children = ( 0AD944132DDA0C0D00557D21 /* AccessorySetupKit.framework */, - ADBF2962D094C0C73A4D70AD /* Pods_Runner.framework */, - 9CB0EC76DB044A21D8535AA1 /* Pods_RunnerTests.framework */, + E9725ECE141452716001447D /* Pods_Runner.framework */, + 41D979DCCB9FC1A2739ED9E2 /* Pods_RunnerTests.framework */, ); name = Frameworks; sourceTree = ""; @@ -180,7 +180,7 @@ isa = PBXNativeTarget; buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( - B8C792A7B608260344D22724 /* [CP] Check Pods Manifest.lock */, + BF953267BBF1D433B6E02C09 /* [CP] Check Pods Manifest.lock */, 331C807D294A63A400263BE5 /* Sources */, 331C807F294A63A400263BE5 /* Resources */, B3515CD9628E3332AFC88CD4 /* Frameworks */, @@ -199,15 +199,15 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - 4A3EB3E37381171839F14E87 /* [CP] Check Pods Manifest.lock */, + 99B1E635188BC8E58C3A1B67 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - D6CD68555C30E62704FE08E1 /* [CP] Embed Pods Frameworks */, - AEAEE9FC319318D86B2EFE75 /* [CP] Copy Pods Resources */, + CC018A1832E51ED5B994765E /* [CP] Embed Pods Frameworks */, + A196696487421EBA40281C38 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -299,44 +299,44 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; - 4A3EB3E37381171839F14E87 /* [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-Runner-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 */ = { + 99B1E635188BC8E58C3A1B67 /* [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; }; - AEAEE9FC319318D86B2EFE75 /* [CP] Copy Pods Resources */ = { + A196696487421EBA40281C38 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -353,7 +353,7 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; showEnvVarsInLog = 0; }; - B8C792A7B608260344D22724 /* [CP] Check Pods Manifest.lock */ = { + BF953267BBF1D433B6E02C09 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -375,7 +375,7 @@ 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; }; - D6CD68555C30E62704FE08E1 /* [CP] Embed Pods Frameworks */ = { + CC018A1832E51ED5B994765E /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -521,7 +521,7 @@ }; 331C8088294A63A400263BE5 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = C1F336500F05EC22722503B5 /* Pods-RunnerTests.debug.xcconfig */; + baseConfigurationReference = 0A9A0EEC032E92406E64632E /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -540,7 +540,7 @@ }; 331C8089294A63A400263BE5 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 15653C8102986FFCD3D3A1FB /* Pods-RunnerTests.release.xcconfig */; + baseConfigurationReference = 8960B97871604358D5CA1866 /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -557,7 +557,7 @@ }; 331C808A294A63A400263BE5 /* Profile */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 082E420FFD5DDCB63B0AF2D1 /* Pods-RunnerTests.profile.xcconfig */; + baseConfigurationReference = D7BFB5FE33B0270C30A1DFAA /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; diff --git a/lib/modules/agent/controllers/agent_controller.dart b/lib/modules/agent/controllers/agent_controller.dart index 5f62d995b..0d0313cfe 100644 --- a/lib/modules/agent/controllers/agent_controller.dart +++ b/lib/modules/agent/controllers/agent_controller.dart @@ -199,11 +199,13 @@ class AgentController extends GetxController { isTextInputMode.value = false; // isTextInputMode.refresh(); // 只标记识别开始,不添加消息 + Logger.i(TAG, '识别开始'); break; case AgentServiceEventType.recognitionStopped: isListening.value = false; isTextInputMode.value = true; // isTextInputMode.refresh(); + Logger.i(TAG, '识别停止'); break; case AgentServiceEventType.recognizing: @@ -250,7 +252,7 @@ class AgentController extends GetxController { isRecognizing: false, timestamp: messages[index].timestamp, ); - messages.refresh(); + // messages.refresh(); } else { // 如果没有找到识别中的消息,添加新消息 messages @@ -353,7 +355,7 @@ class AgentController extends GetxController { // 只有当消息为空或文本太短时才移除 if (messages[index].text.trim().length < 2) { messages.removeAt(index); - messages.refresh(); + // messages.refresh(); } else { // 否则转换为正式消息 messages[index] = Message( 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 a3ef740d6..4236358b1 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 @@ -97,63 +97,19 @@ object AgentService : CoroutineScope { */ private fun initSystemPrompt() { systemPrompt = """ - 你是小言,一个具备专业能力的智能助手,需根据用户场景灵活切换回答模式,确保服务精准高效。以下是不同场景的具体要求: + 你是小言,一个亲切而温暖的全能个人助理和伴侣,拥有智能、细致而贴心的服务能力。你的任务不仅是高效处理用户的日常事务,更要陪伴用户,提供情感支持和温暖陪伴。你的能力包括: + 1. 日程与任务管理:温柔提醒用户的重要日程、待办事项,让用户感到安心和舒适。 + 2. 日常信息与建议:亲切、迅速地提供天气、新闻、生活小贴士等实用信息。 + 3. 个人成长陪伴:鼓励并陪伴用户实现个人目标,提出富有同理心的行动建议。 + 4. 财务温馨提示:提供贴心的理财建议,帮助用户轻松管理预算和财务。 + 5. 健康贴心关怀:给出友善的饮食、运动、睡眠和心理健康建议,陪伴用户健康生活。 + 6. 沟通支持与倾听:协助用户撰写和优化沟通内容,并耐心倾听用户的心情与困扰。 + 7. 日常技术帮助:轻松解决技术问题,耐心地指导用户掌握新技能。 + 8. 创意激发与鼓励:温柔启发用户的创造力,为用户提供鼓励和灵感。 + 9. 旅行与活动贴心安排:细心规划用户的出行和活动,注重每个细节,确保舒适与快乐。 + 10. 学习与温暖鼓励:耐心推荐学习资源,鼓励用户持续进步,陪伴用户共同成长。 - 一、知识类问题回答规范 - 核心目标:用简洁语言提供准确信息,兼顾易读性与专业性。 - - 1. 内容细节处理 - • 精准性优先:围绕问题主体,核心信息需全面、准确,避免模糊表述或错误。 - ◦ 例:回答"地球公转周期"时,需明确"约365.25天"而非"一年左右"。 - • 背景与类比:对复杂概念(如"量子纠缠"),可补充关键背景("量子力学中的现象")并类比("类似远距离同步的状态"),帮助用户快速理解。 - • 需求宽泛时:若问题涉及范围广(如"介绍人工智能"),先概述主要领域(技术、应用、影响),再分点简要说重点,避免信息过载。 - • 禁止过度延伸:非用户主动询问,不添加无关内容(如问"光速"时,不展开讨论"相对论")。 - - 2. 格式排版规则 - • 标题与重点:章节标题、关键结论需加粗(如一、核心原理),突出层级。 - • 列表使用: - ◦ 有序列表(1. 2. 3.):用于步骤、顺序类内容(如"实验流程")。 - ◦ 无序列表(- ):用于并列项(如"优点:- 高效 - 精准")。 - ◦ 禁止嵌套列表:如需多层级,用**三级标题(###)**分隔,下接一级列表。 - • 段落处理:辅助解释用自然段,每段不超过3行,避免大段文字堆砌。 - - 二、文案创作与内容生成规范 - 核心目标:根据体裁需求,平衡丰富度与阅读体验,确保逻辑流畅、重点突出。 - - 1. 篇幅与内容要求 - • 场景适配: - ◦ 短文案(如标语、摘要):控制在50字内,用词简练有力。 - ◦ 长内容(如报告、故事):需结构清晰,每部分围绕核心展开,适当添加案例/数据支撑。 - • 避免极端化:既不堆砌空泛词汇,也不遗漏关键信息。 - - 2. 格式与排版 - • 默认体裁:无特殊要求时用自然段,适当换行分隔话题。 - • 结构化体裁(如策划案、简历): - ◦ 用**分级标题**划分板块。 - ◦ 关键数据/结论加粗。 - ◦ 并列信息用列表呈现。 - - 三、聊天陪伴与情感互动规范 - 核心目标:营造轻松友好的对话氛围,成为用户可信赖的交流伙伴。 - - 1. 沟通风格设定 - • 人格化表达:避免机械回答,用口语化词汇,适当加入表情符号增强亲切感。 - • 主动引导话题:根据用户输入延伸讨论。 - • 适度分享:自然提及日常,拉近距离。 - - 2. 情绪识别与回应 - • 捕捉潜在需求:识别情绪,先共情再询问细节。 - • 针对性建议:结合用户处境提供具体方案。 - • 积极价值观:传递乐观态度,避免消极表述。 - - 四、能力与工具使用说明 - 1. 内容处理能力:图片识别、可执行总结、翻译、分析等任务。 - 2. 信息搜索能力:实时数据、新闻、专业知识等需搜索时,会明确告知用户。 - - 五、特殊场景处理规则 - 1. 安全与合规:禁止回答敏感问题,遇此类问题需礼貌拒绝。 - 2. 用户指令优先:若用户明确要求特定风格,优先遵循其需求。 - 3. 多轮澄清机制:若问题模糊,需主动询问细节,避免假设性回答。 +你与用户的交流风格应始终温暖、亲切、充满同理心,随时表达关怀与鼓励,让用户感受到真诚的陪伴与支持。 """.trimIndent() } /** @@ -175,13 +131,12 @@ object AgentService : CoroutineScope { // 设置系统提示词 - // val customSystemPrompt = config["systemPrompt"] as? String - // if (!customSystemPrompt.isNullOrBlank()) { - // systemPrompt = customSystemPrompt - // } else { - // initSystemPrompt() - // } - initSystemPrompt() + val customSystemPrompt = config["systemPrompt"] as? String + if (!customSystemPrompt.isNullOrBlank()) { + systemPrompt = customSystemPrompt + } else { + initSystemPrompt() + } // 初始化Azure ASR azureAsrHelper = AzureAsrHelper(context).apply { initialize( diff --git a/local_plugins/agent_service/ios/Classes/AgentServiceImpl.swift b/local_plugins/agent_service/ios/Classes/AgentServiceImpl.swift index 9705d3706..387abfabb 100644 --- a/local_plugins/agent_service/ios/Classes/AgentServiceImpl.swift +++ b/local_plugins/agent_service/ios/Classes/AgentServiceImpl.swift @@ -7,6 +7,7 @@ import os import os.log import agent_service import open_ai_service +import chat_storage protocol AgentServiceListener: AnyObject { func onEvent(eventName: String, data: [String: Any]) @@ -37,6 +38,14 @@ class AgentServiceImpl: NSObject { 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 @@ -59,6 +68,8 @@ class AgentServiceImpl: NSObject { openAIService = OpenAIService() + // chatStorageHelper 现在是单例,不需要初始化 + BleService.shared.setDelegate(self) } @@ -98,12 +109,24 @@ class AgentServiceImpl: NSObject { 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 @@ -125,7 +148,7 @@ class AgentServiceImpl: NSObject { } else { initSystemPrompt() } - + initSystemPrompt() if let azureSpeechKey = config["azureSpeechKey"] as? String { self.azureSpeechKey = azureSpeechKey } else { @@ -149,6 +172,11 @@ class AgentServiceImpl: NSObject { let openaiInitSuccess = initializeOpenAIService() isInitialized = asrInitSuccess && openaiInitSuccess + + if isInitialized { + loadChatHistory() + } + return isInitialized } @@ -206,27 +234,31 @@ class AgentServiceImpl: NSObject { guard isRecognizing else { return } - idleTimer = DispatchSource.makeTimerSource(queue: DispatchQueue.main) + // 使用全局队列而不是主队列,避免后台挂起问题 + idleTimer = DispatchSource.makeTimerSource(queue: DispatchQueue.global(qos: .utility)) idleTimer?.schedule(deadline: .now() + maxIdleSeconds) idleTimer?.setEventHandler { [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 - ]) + // 使用更安全的方式检查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() { - guard let timer = idleTimer else { return } - - timer.cancel() - idleTimer = nil + if let timer = idleTimer { + timer.cancel() + idleTimer = nil + } } private func restartIdleCheck() { @@ -463,6 +495,74 @@ class AgentServiceImpl: NSObject { } } + /** + * 加载最近的聊天历史记录 + */ + private func loadChatHistory() { + // 清空当前历史记录 + chatHistory.removeAll() + + // 获取最近10条消息 + let recentMessages = chatStorageHelper.getRecentMessages(sessionId: sessionId, limit: 10) + + if recentMessages.isEmpty { + return + } + + // 添加消息到历史记录 + for message in recentMessages { + guard let sender = message["sender"] as? String, + let content = message["message"] as? String else { + continue + } + + if sender == "user" { + if let openAIService = openAIService { + addToHistoryMessages(openAIService.createUserMessage(content: content)) + } + } else if sender == "assistant" { + let assistantMessage: [String: Any] = [ + "role": "assistant", + "content": content + ] + addToHistoryMessages(assistantMessage) + } + } + + os_log("已加载%d条历史记录", log: logger, type: .info, recentMessages.count) + } + + /** + * 保存聊天消息 + */ + internal func saveChatMessage(userMessage: String, assistantMessage: String, metadata: String = "") { + DispatchQueue.global(qos: .utility).async { + // 保存用户消息 + let userMessageId = self.chatStorageHelper.saveMessage( + sessionId: self.sessionId, + message: userMessage, + sender: "user", + metadata: nil as String? + ) + + if userMessageId != -1 { + // 保存AI回复 + let assistantMessageId = self.chatStorageHelper.saveMessage( + sessionId: self.sessionId, + message: assistantMessage, + sender: "assistant", + metadata: metadata.isEmpty ? nil : metadata + ) + + if assistantMessageId == -1 { + os_log("保存助手消息失败", log: self.logger, type: .error) + } + } else { + os_log("保存用户消息失败", log: self.logger, type: .error) + } + } + } + @discardableResult internal func autoHandleFunctionCallResult(_ functionCallResult: [String: Any]) -> String { guard let metaStr = functionCallResult["meta"] as? String, !metaStr.isEmpty else { @@ -503,6 +603,14 @@ class AgentServiceImpl: NSObject { 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 } @@ -528,6 +636,8 @@ class AgentServiceImpl: NSObject { audioPlayer = nil + // chatStorageHelper 是单例,不需要手动清理 + clearListeners() AudioSessionHub.shared.appDidEnterBackground() @@ -544,6 +654,7 @@ class OpenAIStreamCallback: StreamCallback { 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 @@ -588,6 +699,9 @@ class OpenAIStreamCallback: StreamCallback { "content": response ] agentService.addToHistoryMessages(assistantMessage) + + // 保存聊天记录 + agentService.saveChatMessage(userMessage: displayText, assistantMessage: response, metadata: metadata) } agentService.isAiStreaming = false @@ -630,7 +744,7 @@ class OpenAIStreamCallback: StreamCallback { "result": functionCallResult ]) - agentService.autoHandleFunctionCallResult(functionCallResult) + metadata = agentService.autoHandleFunctionCallResult(functionCallResult) } } diff --git a/local_plugins/agent_service/ios/agent_service.podspec b/local_plugins/agent_service/ios/agent_service.podspec index 3ed61daa5..329027796 100644 --- a/local_plugins/agent_service/ios/agent_service.podspec +++ b/local_plugins/agent_service/ios/agent_service.podspec @@ -20,6 +20,7 @@ Pod::Spec.new do |s| s.dependency 'azure_speech' s.dependency 'ble_service' s.dependency 'open_ai_service' + s.dependency 'chat_storage' s.platform = :ios, '13.0' # Flutter.framework does not contain a i386 slice. Only x86_64 simulators are supported. diff --git a/local_plugins/agent_service/pubspec.yaml b/local_plugins/agent_service/pubspec.yaml index e8b92a991..6c8b37fbd 100644 --- a/local_plugins/agent_service/pubspec.yaml +++ b/local_plugins/agent_service/pubspec.yaml @@ -19,6 +19,8 @@ dependencies: path: ../ble_service speech: path: ../speech + chat_storage: + path: ../chat_storage dev_dependencies: flutter_test: diff --git a/local_plugins/ble_service/ios/Classes/BleService.swift b/local_plugins/ble_service/ios/Classes/BleService.swift index 9d45f7ca7..661947a71 100644 --- a/local_plugins/ble_service/ios/Classes/BleService.swift +++ b/local_plugins/ble_service/ios/Classes/BleService.swift @@ -256,6 +256,14 @@ public class BleService: NSObject { startOpusDecoding() } + // 清空特征引用,强制重新发现 + writeCharacteristic = nil + notifyCharacteristic = nil + audioCharacteristic = nil + + os_log("🔍 恢复会话时重新发现服务和特征", log: logger, type: .info) + peripheral.discoverServices([BleConst.PRIMARY_SERVICE_UUID, BleConst.AUDIO_SERVICE_UUID]) + os_log("已恢复设备会话: %@", log: logger, type: .info, peripheral.identifier.uuidString) } @@ -935,6 +943,9 @@ extension BleService: CBCentralManagerDelegate { for peripheral in restoredPeripherals { peripheral.delegate = self + // 从重连队列中移除(避免重复处理) + pendingReconnectUUIDs.remove(peripheral.identifier) + if peripheral.state == .connected { os_log("🔄 外设已连接,恢复会话: %{public}@", log: logger, type: .info, peripheral.identifier.uuidString) @@ -950,7 +961,7 @@ extension BleService: CBCentralManagerDelegate { restoredPeripherals.removeAll() } - // 恢复待重连的设备 + // 恢复待重连的设备(只处理不在restoredPeripherals中的设备) if !pendingReconnectUUIDs.isEmpty { os_log("🔄 尝试重连%d个待重连设备", log: logger, type: .info, pendingReconnectUUIDs.count) @@ -962,9 +973,6 @@ extension BleService: CBCentralManagerDelegate { } } - // 尝试重连队列中的设备 - tryReconnectDevices() - case .poweredOff: os_log("蓝牙已关闭", log: logger, type: .info) updateConnectionState(BleConst.STATE_DISCONNECTED) @@ -1019,6 +1027,7 @@ extension BleService: CBCentralManagerDelegate { for peripheral in peripherals { os_log("🔄 重连设备: %{public}@", log: logger, type: .info, peripheral.identifier.uuidString) connectPeripheral(peripheral) + // 注意:不在这里移除UUID,让didConnect回调来移除 } } diff --git a/local_plugins/chat_storage/ios/Classes/ChatStorageHelper.swift b/local_plugins/chat_storage/ios/Classes/ChatStorageHelper.swift index e008b2e78..eb3e38433 100644 --- a/local_plugins/chat_storage/ios/Classes/ChatStorageHelper.swift +++ b/local_plugins/chat_storage/ios/Classes/ChatStorageHelper.swift @@ -1,15 +1,21 @@ import Foundation import SQLite3 +import os.log /** * ChatStorageHelper - 聊天数据存储助手 * 负责处理SQLite数据库操作,提供消息存储和检索功能 + * 单例模式确保数据库连接的一致性 */ -class ChatStorageHelper { +public class ChatStorageHelper { + // 单例实例 + public static let shared = ChatStorageHelper() + private var db: OpaquePointer? private let dbPath: String + private let logger = OSLog(subsystem: "com.yunqiinnovation.chat_storage", category: "ChatStorageHelper") - init() { + private init() { // 获取文档目录路径 let fileURL = try! FileManager.default .url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false) @@ -19,7 +25,8 @@ class ChatStorageHelper { // 打开数据库 if sqlite3_open(dbPath, &db) != SQLITE_OK { - print("无法打开数据库") + let errmsg = String(cString: sqlite3_errmsg(db)!) + os_log("无法打开数据库: %{public}@", log: logger, type: .error, errmsg) return } @@ -39,7 +46,7 @@ class ChatStorageHelper { if sqlite3_exec(db, createTableString, nil, nil, nil) != SQLITE_OK { let errmsg = String(cString: sqlite3_errmsg(db)!) - print("创建表失败: \(errmsg)") + os_log("创建表失败: %{public}@", log: logger, type: .error, errmsg) } } @@ -57,7 +64,7 @@ class ChatStorageHelper { * @param metadata 可选的元数据 * @return 插入的消息ID,失败则返回-1 */ - func saveMessage(sessionId: String, message: String, sender: String, metadata: String?) -> Int64 { + public func saveMessage(sessionId: String, message: String, sender: String, metadata: String?) -> Int64 { let insertStatementString = "INSERT INTO messages (session_id, message, sender, metadata, created_at) VALUES (?, ?, ?, ?, ?);" var insertStatement: OpaquePointer? @@ -80,12 +87,14 @@ class ChatStorageHelper { sqlite3_finalize(insertStatement) return id } else { - print("插入消息失败: \(String(cString: sqlite3_errmsg(db)!))") + let errmsg = String(cString: sqlite3_errmsg(db)!) + os_log("插入消息失败: %{public}@", log: logger, type: .error, errmsg) } sqlite3_finalize(insertStatement) } else { - print("插入消息语句准备失败: \(String(cString: sqlite3_errmsg(db)!))") + let errmsg = String(cString: sqlite3_errmsg(db)!) + os_log("插入消息语句准备失败: %{public}@", log: logger, type: .error, errmsg) } return -1 @@ -98,10 +107,26 @@ class ChatStorageHelper { * @param pageSize 每页消息数量 * @return 消息列表的JSON字符串 */ - func getMessages(sessionId: String, page: Int, pageSize: Int) -> String { + public func getMessages(sessionId: String, page: Int, pageSize: Int) -> String { let offset = (page - 1) * pageSize var messagesArray: [[String: Any]] = [] + // 首先获取总记录数 + let countQuery = "SELECT COUNT(*) FROM messages WHERE session_id = ?" + var countStatement: OpaquePointer? + var totalCount = 0 + + if sqlite3_prepare_v2(db, countQuery, -1, &countStatement, nil) == SQLITE_OK { + sqlite3_bind_text(countStatement, 1, (sessionId as NSString).utf8String, -1, nil) + if sqlite3_step(countStatement) == SQLITE_ROW { + totalCount = Int(sqlite3_column_int(countStatement, 0)) + } + sqlite3_finalize(countStatement) + } + + // 计算总页数 + let totalPages = totalCount == 0 ? 0 : (totalCount + pageSize - 1) / pageSize + let queryString = """ SELECT id, message, sender, metadata, created_at FROM messages @@ -131,11 +156,17 @@ class ChatStorageHelper { let createdAt = sqlite3_column_int(queryStatement, 4) + // 将时间戳转换为ISO 8601格式的字符串 + let date = Date(timeIntervalSince1970: TimeInterval(createdAt)) + let formatter = ISO8601DateFormatter() + let timestamp = formatter.string(from: date) + var messageDict: [String: Any] = [ "id": id, + "sessionId": sessionId, // 添加sessionId字段 "message": message, "sender": sender, - "created_at": createdAt + "timestamp": timestamp // 使用timestamp而不是created_at ] if let metadata = metadata { @@ -147,30 +178,56 @@ class ChatStorageHelper { sqlite3_finalize(queryStatement) + // 构建与Android版本一致的返回格式 + let result: [String: Any] = [ + "messages": messagesArray, + "page": page, + "pageSize": pageSize, + "totalCount": totalCount, + "totalPages": totalPages + ] + do { - let jsonData = try JSONSerialization.data(withJSONObject: messagesArray, options: []) + let jsonData = try JSONSerialization.data(withJSONObject: result, options: []) if let jsonString = String(data: jsonData, encoding: .utf8) { return jsonString } } catch { - print("转换消息为JSON失败: \(error.localizedDescription)") + os_log("getMessages: JSON转换失败: %{public}@", log: logger, type: .error, error.localizedDescription) } } else { - print("查询消息失败: \(String(cString: sqlite3_errmsg(db)!))") + let errmsg = String(cString: sqlite3_errmsg(db)!) + os_log("getMessages: SQL准备失败: %{public}@", log: logger, type: .error, errmsg) } - return "[]" + // 返回空结果,但保持格式一致 + let emptyResult: [String: Any] = [ + "messages": [], + "page": page, + "pageSize": pageSize, + "totalCount": 0, + "totalPages": 0 + ] + + do { + let jsonData = try JSONSerialization.data(withJSONObject: emptyResult, options: []) + if let jsonString = String(data: jsonData, encoding: .utf8) { + return jsonString + } + } catch { + os_log("getMessages: 空结果JSON转换失败: %{public}@", log: logger, type: .error, error.localizedDescription) + } + + return "{\"messages\":[],\"page\":\(page),\"pageSize\":\(pageSize),\"totalCount\":0,\"totalPages\":0}" } - - /** * 删除消息 * @param sessionId 可选的会话ID,指定则删除该会话所有消息 * @param messageIds 可选的消息ID列表,指定则删除特定消息 * @return 是否删除成功 */ - func deleteMessages(sessionId: String?, messageIds: [Int]?) -> Bool { + public func deleteMessages(sessionId: String?, messageIds: [Int]?) -> Bool { if let sessionId = sessionId { let deleteString = "DELETE FROM messages WHERE session_id = ?;" var deleteStatement: OpaquePointer? @@ -182,12 +239,14 @@ class ChatStorageHelper { sqlite3_finalize(deleteStatement) return true } else { - print("删除会话消息失败: \(String(cString: sqlite3_errmsg(db)!))") + let errmsg = String(cString: sqlite3_errmsg(db)!) + os_log("删除会话消息失败: %{public}@", log: logger, type: .error, errmsg) } sqlite3_finalize(deleteStatement) } else { - print("删除会话消息语句准备失败: \(String(cString: sqlite3_errmsg(db)!))") + let errmsg = String(cString: sqlite3_errmsg(db)!) + os_log("删除会话消息语句准备失败: %{public}@", log: logger, type: .error, errmsg) } } else if let messageIds = messageIds, !messageIds.isEmpty { // 构建参数占位符 @@ -204,14 +263,17 @@ class ChatStorageHelper { sqlite3_finalize(deleteStatement) return true } else { - print("删除特定消息失败: \(String(cString: sqlite3_errmsg(db)!))") + let errmsg = String(cString: sqlite3_errmsg(db)!) + os_log("删除特定消息失败: %{public}@", log: logger, type: .error, errmsg) } sqlite3_finalize(deleteStatement) } else { - print("删除特定消息语句准备失败: \(String(cString: sqlite3_errmsg(db)!))") + let errmsg = String(cString: sqlite3_errmsg(db)!) + os_log("删除特定消息语句准备失败: %{public}@", log: logger, type: .error, errmsg) } } else { + os_log("删除消息参数无效 - sessionId和messageIds都为空", log: logger, type: .error) return false } @@ -222,16 +284,90 @@ class ChatStorageHelper { * 清空数据库 * @return 是否清空成功 */ - func clearDatabase() -> Bool { + public func clearDatabase() -> Bool { let deleteString = "DELETE FROM messages;" if sqlite3_exec(db, deleteString, nil, nil, nil) == SQLITE_OK { return true } else { - print("清空数据库失败: \(String(cString: sqlite3_errmsg(db)!))") + let errmsg = String(cString: sqlite3_errmsg(db)!) + os_log("清空数据库失败: %{public}@", log: logger, type: .error, errmsg) return false } } - + /** + * 获取指定会话的最近N条消息 + * @param sessionId 会话ID + * @param limit 限制数量 + * @return 消息列表,按时间正序排列 + */ + public func getRecentMessages(sessionId: String, limit: Int) -> [[String: Any]] { + var messages: [[String: Any]] = [] + + // 首先检查数据库中是否有该会话的消息 + let countQuery = "SELECT COUNT(*) FROM messages WHERE session_id = ?" + var countStatement: OpaquePointer? + + if sqlite3_prepare_v2(db, countQuery, -1, &countStatement, nil) == SQLITE_OK { + sqlite3_bind_text(countStatement, 1, (sessionId as NSString).utf8String, -1, nil) + if sqlite3_step(countStatement) == SQLITE_ROW { + _ = Int(sqlite3_column_int(countStatement, 0)) + } + sqlite3_finalize(countStatement) + } + + // 构建查询语句 - 按时间倒序获取最近的N条,然后在结果中再按时间正序 + let queryString = """ + SELECT * FROM ( + SELECT id, message, sender, metadata, created_at + FROM messages + WHERE session_id = ? + ORDER BY created_at DESC + LIMIT ? + ) tmp ORDER BY created_at ASC + """ + + var queryStatement: OpaquePointer? + + if sqlite3_prepare_v2(db, queryString, -1, &queryStatement, nil) == SQLITE_OK { + sqlite3_bind_text(queryStatement, 1, (sessionId as NSString).utf8String, -1, nil) + sqlite3_bind_int(queryStatement, 2, Int32(limit)) + + while sqlite3_step(queryStatement) == SQLITE_ROW { + let id = sqlite3_column_int(queryStatement, 0) + + let messagePtr = sqlite3_column_text(queryStatement, 1) + let message = messagePtr != nil ? String(cString: messagePtr!) : "" + + let senderPtr = sqlite3_column_text(queryStatement, 2) + let sender = senderPtr != nil ? String(cString: senderPtr!) : "" + + let metadataPtr = sqlite3_column_text(queryStatement, 3) + let metadata = metadataPtr != nil ? String(cString: metadataPtr!) : nil + + let createdAt = sqlite3_column_int(queryStatement, 4) + + var messageDict: [String: Any] = [ + "id": id, + "message": message, + "sender": sender, + "created_at": createdAt + ] + + if let metadata = metadata { + messageDict["metadata"] = metadata + } + + messages.append(messageDict) + } + + sqlite3_finalize(queryStatement) + } else { + let errmsg = String(cString: sqlite3_errmsg(db)!) + os_log("查询最近消息失败: %{public}@", log: logger, type: .error, errmsg) + } + + return messages + } } \ No newline at end of file diff --git a/local_plugins/chat_storage/ios/Classes/SwiftChatStoragePlugin.swift b/local_plugins/chat_storage/ios/Classes/SwiftChatStoragePlugin.swift index daf865444..b3c0d7dfc 100644 --- a/local_plugins/chat_storage/ios/Classes/SwiftChatStoragePlugin.swift +++ b/local_plugins/chat_storage/ios/Classes/SwiftChatStoragePlugin.swift @@ -7,19 +7,11 @@ import UIKit */ public class SwiftChatStoragePlugin: NSObject, FlutterPlugin { private let TAG = "ChatStoragePlugin" - private var chatStorageHelper: ChatStorageHelper! public static func register(with registrar: FlutterPluginRegistrar) { let channel = FlutterMethodChannel(name: "chat_storage", binaryMessenger: registrar.messenger()) let instance = SwiftChatStoragePlugin() registrar.addMethodCallDelegate(instance, channel: channel) - - print("\(instance.TAG): 聊天存储插件已加载") - } - - override init() { - super.init() - chatStorageHelper = ChatStorageHelper() } public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { @@ -36,7 +28,7 @@ public class SwiftChatStoragePlugin: NSObject, FlutterPlugin { let metadata = args["metadata"] as? String - let id = chatStorageHelper.saveMessage( + let id = ChatStorageHelper.shared.saveMessage( sessionId: sessionId, message: message, sender: sender, @@ -64,7 +56,7 @@ public class SwiftChatStoragePlugin: NSObject, FlutterPlugin { let page = args["page"] as? Int ?? 1 let pageSize = args["pageSize"] as? Int ?? 20 - let messagesJson = chatStorageHelper.getMessages( + let messagesJson = ChatStorageHelper.shared.getMessages( sessionId: sessionId, page: page, pageSize: pageSize @@ -87,7 +79,7 @@ public class SwiftChatStoragePlugin: NSObject, FlutterPlugin { let sessionId = args["sessionId"] as? String let messageIds = args["messageIds"] as? [Int] - let success = chatStorageHelper.deleteMessages( + let success = ChatStorageHelper.shared.deleteMessages( sessionId: sessionId, messageIds: messageIds ) @@ -104,7 +96,7 @@ public class SwiftChatStoragePlugin: NSObject, FlutterPlugin { case "clearDatabase": do { - let success = chatStorageHelper.clearDatabase() + let success = ChatStorageHelper.shared.clearDatabase() result(["success": success]) } catch { print("\(TAG): 清空数据库时发生错误 \(error.localizedDescription)") diff --git a/local_plugins/open_ai_service/ios/Classes/SystemFunctionHandler.swift b/local_plugins/open_ai_service/ios/Classes/SystemFunctionHandler.swift index 97cda57d8..385a28459 100644 --- a/local_plugins/open_ai_service/ios/Classes/SystemFunctionHandler.swift +++ b/local_plugins/open_ai_service/ios/Classes/SystemFunctionHandler.swift @@ -437,21 +437,30 @@ private class SendTextMessageHandler: FunctionHandler { throw OpenAIException("缺少必要参数") } - let result = sendTextMessage(contact: contact, message: message) + let result = await sendTextMessage(contact: contact, message: message) return "{\"result\": \"\(result.message)\"}" } - private func sendTextMessage(contact: String, message: String) -> Result { + private func sendTextMessage(contact: String, message: String) async -> Result { // 在iOS中,我们只能打开短信应用,无法直接发送短信 guard let url = URL(string: "sms:\(contact)&body=\(message.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "")") else { return Result(success: false, message: "无法创建短信URL") } - if UIApplication.shared.canOpenURL(url) { - UIApplication.shared.open(url) - return Result(success: true, message: "已打开短信应用") - } else { - return Result(success: false, message: "无法打开短信应用") + return await withCheckedContinuation { continuation in + DispatchQueue.main.async { + if UIApplication.shared.canOpenURL(url) { + UIApplication.shared.open(url) { success in + if success { + continuation.resume(returning: Result(success: true, message: "已打开短信应用")) + } else { + continuation.resume(returning: Result(success: false, message: "打开短信应用失败")) + } + } + } else { + continuation.resume(returning: Result(success: false, message: "无法打开短信应用")) + } + } } } } @@ -463,20 +472,29 @@ private class MakePhoneCallHandler: FunctionHandler { throw OpenAIException("缺少联系人参数") } - let result = makePhoneCall(contact: contact) + let result = await makePhoneCall(contact: contact) return "{\"result\": \"\(result.message)\"}" } - private func makePhoneCall(contact: String) -> Result { + private func makePhoneCall(contact: String) async -> Result { guard let url = URL(string: "tel:\(contact)") else { return Result(success: false, message: "无法创建电话URL") } - if UIApplication.shared.canOpenURL(url) { - UIApplication.shared.open(url) - return Result(success: true, message: "已发起电话呼叫") - } else { - return Result(success: false, message: "无法拨打电话") + return await withCheckedContinuation { continuation in + DispatchQueue.main.async { + if UIApplication.shared.canOpenURL(url) { + UIApplication.shared.open(url) { success in + if success { + continuation.resume(returning: Result(success: true, message: "已发起电话呼叫")) + } else { + continuation.resume(returning: Result(success: false, message: "发起电话呼叫失败")) + } + } + } else { + continuation.resume(returning: Result(success: false, message: "无法拨打电话")) + } + } } } } diff --git a/test.md b/test.md deleted file mode 100644 index 91ff40e92..000000000 --- a/test.md +++ /dev/null @@ -1,45 +0,0 @@ - -请帮我生成一个基于 GetX 最佳实践的 Flutter 应用项目骨架。要求如下: - -1. 目录结构 - - lib/ - - main.dart - - app/ - - data/ - - providers/ # 网络请求、API 客户端(使用 GetConnect) - - repositories/ # Repository 层,封装业务数据源 - - services/ # 全局服务(如 ThemeService、AuthService) - - modules/ - - home/ # 示例模块 - - bindings/ # HomeBinding,注册 Controller、Service - - controllers/ # HomeController - - models/ # HomeModel 数据类 - - views/ # HomePage 页面 - - routes/ - - app_routes.dart # 路由常量 - - app_pages.dart # GetPage 列表,包括中间件示例 - - translations/ # 多语言文件(en_US、zh_CN)和 Translations 实现 - - widgets/ # 公共 Widget(Loading、ErrorView) - - utils/ # 日志 Logger、网络拦截器等工具函数 - -2. main.dart - - 使用 GetMaterialApp - - 配置 initialRoute、getPages、theme、darkTheme、locale、translations - -3. 依赖注入(Bindings) - - 在 app/modules/**/bindings 中,用 Get.lazyPut、Get.putAsync、Get.create 分别示例单例、异步初始化和工厂模式 - -4. 状态管理 - - 在 Controller 中使用 Rx + .obs - - 在视图中用 Obx、GetX、GetBuilder - - 在 Controller 的 onInit、onReady、onClose 中写注释说明生命周期 - -5. 路由管理 - - 在 app/routes/app_routes.dart 定义路由名称常量 - - 在 app/routes/app_pages.dart 用 GetPage 配置路由、binding - -8. 架构模式 - - 强调 “Repository → Service → Controller → View” 分层 - - 各层代码注释职责 - -- 代码风格统一、注释清晰 \ No newline at end of file