From 1a42e347cf764b7467818f285e3afb0654fcad79 Mon Sep 17 00:00:00 2001 From: Developer Date: Fri, 22 May 2026 05:00:41 +0800 Subject: [PATCH] =?UTF-8?q?build:=20=E4=BF=AE=E5=A4=8DiOS/macOS=E6=9E=84?= =?UTF-8?q?=E5=BB=BA=E9=85=8D=E7=BD=AE=EF=BC=8C=E9=80=82=E9=85=8DmacOS=20K?= =?UTF-8?q?eychain=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 添加Pods依赖配置到Xcode工作区 2. 调整macOS权限配置,临时替换Keychain为shared_preferences 3. 重构secure_storage适配macOS兼容性问题 4. 整理iOS权限配置,移除重复声明 5. 更新插件依赖和Podfile配置 --- ios/Flutter/AppFrameworkInfo.plist | 2 - ios/Flutter/Debug.xcconfig | 1 + ios/Flutter/Release.xcconfig | 1 + ios/Podfile.lock | 339 +++++++ ios/Runner.xcodeproj/project.pbxproj | 141 +++ .../contents.xcworkspacedata | 3 + .../xcshareddata/WorkspaceSettings.xcsettings | 2 + ios/Runner/AppDelegate.swift | 7 +- ios/Runner/Info.plist | 127 ++- lib/core/storage/secure_storage.dart | 74 +- macos/Flutter/Flutter-Debug.xcconfig | 1 + macos/Flutter/Flutter-Release.xcconfig | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 4 +- macos/Podfile.lock | 7 +- macos/Runner.xcodeproj/project.pbxproj | 107 ++- .../contents.xcworkspacedata | 3 + macos/Runner/DebugProfile.entitlements | 2 +- macos/Runner/Release.entitlements | 6 + scripts/favorite_sync_test.dart | 542 ----------- scripts/file_transfer_full_test.dart | 900 ------------------ scripts/signaling_test.dart | 670 ------------- scripts/signin_api_test.dart | 356 ------- 22 files changed, 741 insertions(+), 2555 deletions(-) create mode 100644 ios/Podfile.lock delete mode 100644 scripts/favorite_sync_test.dart delete mode 100644 scripts/file_transfer_full_test.dart delete mode 100644 scripts/signaling_test.dart delete mode 100644 scripts/signin_api_test.dart diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist index 1dc6cf76..391a902b 100644 --- a/ios/Flutter/AppFrameworkInfo.plist +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -20,7 +20,5 @@ ???? CFBundleVersion 1.0 - MinimumOSVersion - 13.0 diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig index 592ceee8..ec97fc6f 100644 --- a/ios/Flutter/Debug.xcconfig +++ b/ios/Flutter/Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig index 592ceee8..c4855bfe 100644 --- a/ios/Flutter/Release.xcconfig +++ b/ios/Flutter/Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Podfile.lock b/ios/Podfile.lock new file mode 100644 index 00000000..ebe582b1 --- /dev/null +++ b/ios/Podfile.lock @@ -0,0 +1,339 @@ +PODS: + - app_links (7.0.0): + - Flutter + - audioplayers_darwin (0.0.1): + - Flutter + - FlutterMacOS + - battery_plus (1.0.0): + - Flutter + - connectivity_plus (0.0.1): + - Flutter + - device_info_plus (0.0.1): + - Flutter + - file_picker (0.0.1): + - Flutter + - FlutterMacOS + - Flutter (1.0.0) + - flutter_blue_plus_darwin (0.0.2): + - Flutter + - FlutterMacOS + - flutter_image_compress_common (1.0.0): + - Flutter + - Mantle + - SDWebImage + - SDWebImageWebPCoder + - flutter_inappwebview_ios (0.0.1): + - Flutter + - flutter_inappwebview_ios/Core (= 0.0.1) + - OrderedSet (~> 6.0.3) + - flutter_inappwebview_ios/Core (0.0.1): + - Flutter + - OrderedSet (~> 6.0.3) + - flutter_keyboard_visibility_temp_fork (0.0.1): + - Flutter + - flutter_local_notifications (0.0.1): + - Flutter + - flutter_mailer (0.0.1): + - Flutter + - flutter_nfc_kit (3.6.0): + - Flutter + - flutter_secure_storage_darwin (10.0.0): + - Flutter + - FlutterMacOS + - flutter_tts (0.0.1): + - Flutter + - flutter_webrtc (1.4.0): + - Flutter + - WebRTC-SDK (= 144.7559.01) + - fluttertoast (0.0.2): + - Flutter + - gal (1.0.0): + - Flutter + - FlutterMacOS + - home_widget (0.0.1): + - Flutter + - image_picker_ios (0.0.1): + - Flutter + - libwebp (1.5.0): + - libwebp/demux (= 1.5.0) + - libwebp/mux (= 1.5.0) + - libwebp/sharpyuv (= 1.5.0) + - libwebp/webp (= 1.5.0) + - libwebp/demux (1.5.0): + - libwebp/webp + - libwebp/mux (1.5.0): + - libwebp/demux + - libwebp/sharpyuv (1.5.0) + - libwebp/webp (1.5.0): + - libwebp/sharpyuv + - local_auth_darwin (0.0.1): + - Flutter + - FlutterMacOS + - Mantle (2.2.0): + - Mantle/extobjc (= 2.2.0) + - Mantle/extobjc (2.2.0) + - mobile_scanner (7.0.0): + - Flutter + - FlutterMacOS + - nearby_service (0.0.1): + - Flutter + - FlutterMacOS + - network_info_plus (0.0.1): + - Flutter + - OrderedSet (6.0.3) + - package_info_plus (0.4.5): + - Flutter + - permission_handler_apple (9.3.0): + - Flutter + - pro_image_editor (12.0.8): + - Flutter + - quill_native_bridge_ios (0.0.1): + - Flutter + - receive_sharing_intent (1.8.1): + - Flutter + - record_ios (1.2.0): + - Flutter + - SDWebImage (5.21.7): + - SDWebImage/Core (= 5.21.7) + - SDWebImage/Core (5.21.7) + - SDWebImageWebPCoder (0.15.0): + - libwebp (~> 1.0) + - SDWebImage/Core (~> 5.17) + - sensors_plus (0.0.1): + - Flutter + - share_plus (0.0.1): + - Flutter + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS + - sqflite_darwin (0.0.4): + - Flutter + - FlutterMacOS + - sqlite3 (3.52.0): + - sqlite3/common (= 3.52.0) + - sqlite3/common (3.52.0) + - sqlite3/dbstatvtab (3.52.0): + - sqlite3/common + - sqlite3/fts5 (3.52.0): + - sqlite3/common + - sqlite3/math (3.52.0): + - sqlite3/common + - sqlite3/perf-threadsafe (3.52.0): + - sqlite3/common + - sqlite3/rtree (3.52.0): + - sqlite3/common + - sqlite3/session (3.52.0): + - sqlite3/common + - sqlite3_flutter_libs (0.0.1): + - Flutter + - FlutterMacOS + - sqlite3 (~> 3.52.0) + - sqlite3/dbstatvtab + - sqlite3/fts5 + - sqlite3/math + - sqlite3/perf-threadsafe + - sqlite3/rtree + - sqlite3/session + - url_launcher_ios (0.0.1): + - Flutter + - video_compress (0.3.0): + - Flutter + - video_player_avfoundation (0.0.1): + - Flutter + - FlutterMacOS + - wakelock_plus (0.0.1): + - Flutter + - WebRTC-SDK (144.7559.01) + - wifi_iot (0.0.1): + - Flutter + +DEPENDENCIES: + - app_links (from `.symlinks/plugins/app_links/ios`) + - audioplayers_darwin (from `.symlinks/plugins/audioplayers_darwin/darwin`) + - battery_plus (from `.symlinks/plugins/battery_plus/ios`) + - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) + - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) + - file_picker (from `.symlinks/plugins/file_picker/darwin`) + - Flutter (from `Flutter`) + - flutter_blue_plus_darwin (from `.symlinks/plugins/flutter_blue_plus_darwin/darwin`) + - flutter_image_compress_common (from `.symlinks/plugins/flutter_image_compress_common/ios`) + - flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`) + - flutter_keyboard_visibility_temp_fork (from `.symlinks/plugins/flutter_keyboard_visibility_temp_fork/ios`) + - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) + - flutter_mailer (from `.symlinks/plugins/flutter_mailer/ios`) + - flutter_nfc_kit (from `.symlinks/plugins/flutter_nfc_kit/ios`) + - flutter_secure_storage_darwin (from `.symlinks/plugins/flutter_secure_storage_darwin/darwin`) + - flutter_tts (from `.symlinks/plugins/flutter_tts/ios`) + - flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`) + - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) + - gal (from `.symlinks/plugins/gal/darwin`) + - home_widget (from `.symlinks/plugins/home_widget/ios`) + - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) + - local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`) + - mobile_scanner (from `.symlinks/plugins/mobile_scanner/darwin`) + - nearby_service (from `.symlinks/plugins/nearby_service/darwin`) + - network_info_plus (from `.symlinks/plugins/network_info_plus/ios`) + - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) + - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) + - pro_image_editor (from `.symlinks/plugins/pro_image_editor/ios`) + - quill_native_bridge_ios (from `.symlinks/plugins/quill_native_bridge_ios/ios`) + - receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`) + - record_ios (from `.symlinks/plugins/record_ios/ios`) + - sensors_plus (from `.symlinks/plugins/sensors_plus/ios`) + - share_plus (from `.symlinks/plugins/share_plus/ios`) + - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) + - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`) + - sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`) + - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) + - video_compress (from `.symlinks/plugins/video_compress/ios`) + - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`) + - wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`) + - wifi_iot (from `.symlinks/plugins/wifi_iot/ios`) + +SPEC REPOS: + trunk: + - libwebp + - Mantle + - OrderedSet + - SDWebImage + - SDWebImageWebPCoder + - sqlite3 + - WebRTC-SDK + +EXTERNAL SOURCES: + app_links: + :path: ".symlinks/plugins/app_links/ios" + audioplayers_darwin: + :path: ".symlinks/plugins/audioplayers_darwin/darwin" + battery_plus: + :path: ".symlinks/plugins/battery_plus/ios" + connectivity_plus: + :path: ".symlinks/plugins/connectivity_plus/ios" + device_info_plus: + :path: ".symlinks/plugins/device_info_plus/ios" + file_picker: + :path: ".symlinks/plugins/file_picker/darwin" + Flutter: + :path: Flutter + flutter_blue_plus_darwin: + :path: ".symlinks/plugins/flutter_blue_plus_darwin/darwin" + flutter_image_compress_common: + :path: ".symlinks/plugins/flutter_image_compress_common/ios" + flutter_inappwebview_ios: + :path: ".symlinks/plugins/flutter_inappwebview_ios/ios" + flutter_keyboard_visibility_temp_fork: + :path: ".symlinks/plugins/flutter_keyboard_visibility_temp_fork/ios" + flutter_local_notifications: + :path: ".symlinks/plugins/flutter_local_notifications/ios" + flutter_mailer: + :path: ".symlinks/plugins/flutter_mailer/ios" + flutter_nfc_kit: + :path: ".symlinks/plugins/flutter_nfc_kit/ios" + flutter_secure_storage_darwin: + :path: ".symlinks/plugins/flutter_secure_storage_darwin/darwin" + flutter_tts: + :path: ".symlinks/plugins/flutter_tts/ios" + flutter_webrtc: + :path: ".symlinks/plugins/flutter_webrtc/ios" + fluttertoast: + :path: ".symlinks/plugins/fluttertoast/ios" + gal: + :path: ".symlinks/plugins/gal/darwin" + home_widget: + :path: ".symlinks/plugins/home_widget/ios" + image_picker_ios: + :path: ".symlinks/plugins/image_picker_ios/ios" + local_auth_darwin: + :path: ".symlinks/plugins/local_auth_darwin/darwin" + mobile_scanner: + :path: ".symlinks/plugins/mobile_scanner/darwin" + nearby_service: + :path: ".symlinks/plugins/nearby_service/darwin" + network_info_plus: + :path: ".symlinks/plugins/network_info_plus/ios" + package_info_plus: + :path: ".symlinks/plugins/package_info_plus/ios" + permission_handler_apple: + :path: ".symlinks/plugins/permission_handler_apple/ios" + pro_image_editor: + :path: ".symlinks/plugins/pro_image_editor/ios" + quill_native_bridge_ios: + :path: ".symlinks/plugins/quill_native_bridge_ios/ios" + receive_sharing_intent: + :path: ".symlinks/plugins/receive_sharing_intent/ios" + record_ios: + :path: ".symlinks/plugins/record_ios/ios" + sensors_plus: + :path: ".symlinks/plugins/sensors_plus/ios" + share_plus: + :path: ".symlinks/plugins/share_plus/ios" + shared_preferences_foundation: + :path: ".symlinks/plugins/shared_preferences_foundation/darwin" + sqflite_darwin: + :path: ".symlinks/plugins/sqflite_darwin/darwin" + sqlite3_flutter_libs: + :path: ".symlinks/plugins/sqlite3_flutter_libs/darwin" + url_launcher_ios: + :path: ".symlinks/plugins/url_launcher_ios/ios" + video_compress: + :path: ".symlinks/plugins/video_compress/ios" + video_player_avfoundation: + :path: ".symlinks/plugins/video_player_avfoundation/darwin" + wakelock_plus: + :path: ".symlinks/plugins/wakelock_plus/ios" + wifi_iot: + :path: ".symlinks/plugins/wifi_iot/ios" + +SPEC CHECKSUMS: + app_links: a754cbec3c255bd4bbb4d236ecc06f28cd9a7ce8 + audioplayers_darwin: 835ced6edd4c9fc8ebb0a7cc9e294a91d99917d5 + battery_plus: b42253f6d2dde71712f8c36fef456d99121c5977 + connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd + device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe + file_picker: 70164d9778c42c47218d6cd79ce435de0856b11a + Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 + flutter_blue_plus_darwin: 20a08bfeaa0f7804d524858d3d8744bcc1b6dbc3 + flutter_image_compress_common: 1697a328fd72bfb335507c6bca1a65fa5ad87df1 + flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99 + flutter_keyboard_visibility_temp_fork: 95b2d534bacf6ac62e7fcbe5c2a9e2c2a17ce06f + flutter_local_notifications: 643a3eda1ce1c0599413ca31672536d423dee214 + flutter_mailer: 3a8cd4f36c960fb04528d5471097270c19fec1c4 + flutter_nfc_kit: e1b71583eafd2c9650bc86844a7f2d185fb414f6 + flutter_secure_storage_darwin: acdb3f316ed05a3e68f856e0353b133eec373a23 + flutter_tts: 35ac3c7d42412733e795ea96ad2d7e05d0a75113 + flutter_webrtc: ec91d94b484ad49cf191ef93413f64a40ffd3b4c + fluttertoast: fe6790210fdba20801685be946e3a2124b72eef5 + gal: baecd024ebfd13c441269ca7404792a7152fde89 + home_widget: 54b4f6b36ed8d64cfee594a476225c35c3e45091 + image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326 + libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8 + local_auth_darwin: c3ee6cce0a8d56be34c8ccb66ba31f7f180aaebb + Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d + mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93 + nearby_service: 608702f35ef2b2f4d10b29b49c9a1bd24ae2ff03 + network_info_plus: cf61925ab5205dce05a4f0895989afdb6aade5fc + OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94 + package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 + permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d + pro_image_editor: 3dedac450f82a389877286fa9eb08852cefb04ea + quill_native_bridge_ios: f47af4b14e7757968486641656c5d23250cee521 + receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00 + record_ios: 412daca2350b228e698fffcd08f1f94ceb1e3844 + SDWebImage: e9fc87c1aab89a8ab1bbd74eba378c6f53be8abf + SDWebImageWebPCoder: 0e06e365080397465cc73a7a9b472d8a3bd0f377 + sensors_plus: 6a11ed0c2e1d0bd0b20b4029d3bad27d96e0c65b + share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a + shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb + sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 + sqlite3: a51c07cf16e023d6c48abd5e5791a61a47354921 + sqlite3_flutter_libs: b3e120efe9a82017e5552a620f696589ed4f62ab + url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b + video_compress: f2133a07762889d67f0711ac831faa26f956980e + video_player_avfoundation: 3453f792138786248960ca029747fcd9f318ef52 + wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556 + WebRTC-SDK: ab9b5319e458c2bfebdc92b3600740da35d5630d + wifi_iot: f645260a2be8608517b2a9bf4c39b98e97003acc + +PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e + +COCOAPODS: 1.16.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 5e48b0a4..b292fc0d 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -8,12 +8,14 @@ /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 261D109F22B957D6345FE8D4 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8516FA250E04F5DCA0BF879B /* Pods_Runner.framework */; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + DEE3CE70CC495E564BA02764 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6910A24D128D2AE7EC75E064 /* Pods_RunnerTests.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -45,9 +47,14 @@ 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 = ""; }; + 4E5D7D601A13EF4FC7FF09C9 /* 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 = ""; }; + 6910A24D128D2AE7EC75E064 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 6A4864A98473C1CB5799EC31 /* 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 = ""; }; + 6BACA6720B4314FD7805C6EA /* 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 = ""; }; 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 = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 8516FA250E04F5DCA0BF879B /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 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; }; @@ -55,13 +62,25 @@ 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 = ""; }; + AB11A1FFADECAB65336D2E91 /* 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 = ""; }; + DB4843BA3B8F591B55759752 /* 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 = ""; }; + FF5404F72C4CE124A8A15D0B /* 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 = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 11A350259BEACCDD73AF9502 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + DEE3CE70CC495E564BA02764 /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 97C146EB1CF9000F007C117D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 261D109F22B957D6345FE8D4 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -76,6 +95,15 @@ path = RunnerTests; sourceTree = ""; }; + 681D3589D0B414F14382F95F /* Frameworks */ = { + isa = PBXGroup; + children = ( + 8516FA250E04F5DCA0BF879B /* Pods_Runner.framework */, + 6910A24D128D2AE7EC75E064 /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( @@ -94,6 +122,8 @@ 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, 331C8082294A63A400263BE5 /* RunnerTests */, + F309E78E8D171786452C7A57 /* Pods */, + 681D3589D0B414F14382F95F /* Frameworks */, ); sourceTree = ""; }; @@ -121,6 +151,19 @@ path = Runner; sourceTree = ""; }; + F309E78E8D171786452C7A57 /* Pods */ = { + isa = PBXGroup; + children = ( + AB11A1FFADECAB65336D2E91 /* Pods-Runner.debug.xcconfig */, + 6A4864A98473C1CB5799EC31 /* Pods-Runner.release.xcconfig */, + DB4843BA3B8F591B55759752 /* Pods-Runner.profile.xcconfig */, + FF5404F72C4CE124A8A15D0B /* Pods-RunnerTests.debug.xcconfig */, + 6BACA6720B4314FD7805C6EA /* Pods-RunnerTests.release.xcconfig */, + 4E5D7D601A13EF4FC7FF09C9 /* Pods-RunnerTests.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -128,8 +171,10 @@ isa = PBXNativeTarget; buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( + C0F53C5A1FBC5EF72435D204 /* [CP] Check Pods Manifest.lock */, 331C807D294A63A400263BE5 /* Sources */, 331C807F294A63A400263BE5 /* Resources */, + 11A350259BEACCDD73AF9502 /* Frameworks */, ); buildRules = ( ); @@ -145,12 +190,15 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + D65CC93CFA8F5EF95009A8A4 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 08ABBB9E0B8EF5217E7AB276 /* [CP] Embed Pods Frameworks */, + FDD230797F38EC2827606A1E /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -222,6 +270,23 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 08ABBB9E0B8EF5217E7AB276 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -253,6 +318,67 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; + C0F53C5A1FBC5EF72435D204 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + 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; + }; + D65CC93CFA8F5EF95009A8A4 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + 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; + }; + FDD230797F38EC2827606A1E /* [CP] Copy Pods Resources */ = { + 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"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -327,6 +453,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -335,6 +462,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = 5V9NVUU6K5; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -349,6 +477,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 17.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; + STRING_CATALOG_GENERATE_SYMBOLS = YES; SUPPORTED_PLATFORMS = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; @@ -362,6 +491,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -379,6 +509,7 @@ }; 331C8088294A63A400263BE5 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = FF5404F72C4CE124A8A15D0B /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -396,6 +527,7 @@ }; 331C8089294A63A400263BE5 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 6BACA6720B4314FD7805C6EA /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -411,6 +543,7 @@ }; 331C808A294A63A400263BE5 /* Profile */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 4E5D7D601A13EF4FC7FF09C9 /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -448,6 +581,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -456,6 +590,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = 5V9NVUU6K5; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -477,6 +612,7 @@ MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; + STRING_CATALOG_GENERATE_SYMBOLS = YES; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -505,6 +641,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -513,6 +650,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = 5V9NVUU6K5; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -527,6 +665,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 17.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; + STRING_CATALOG_GENERATE_SYMBOLS = YES; SUPPORTED_PLATFORMS = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; @@ -542,6 +681,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -565,6 +705,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata index 1d526a16..21a3cc14 100644 --- a/ios/Runner.xcworkspace/contents.xcworkspacedata +++ b/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -4,4 +4,7 @@ + + diff --git a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings index f9b0d7c5..ff23ebc8 100644 --- a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +++ b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -2,6 +2,8 @@ + IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded + PreviewsEnabled diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 62666446..c30b367e 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -2,12 +2,15 @@ import Flutter import UIKit @main -@objc class AppDelegate: FlutterAppDelegate { +@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { - GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } + + func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { + GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) + } } diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 34d9bbd5..33fe2874 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -2,6 +2,8 @@ + CADisableMinimumFrameDurationOnPhone + CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName @@ -20,10 +22,67 @@ $(FLUTTER_BUILD_NAME) CFBundleSignature ???? + CFBundleURLTypes + + + CFBundleTypeRole + Viewer + CFBundleURLName + apps.xy.xianyan + CFBundleURLSchemes + + xianyan + + + CFBundleVersion $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS + NFCReaderUsageDescription + 闲言需要使用NFC以触碰配对设备进行文件传输 + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + NSBluetoothAlwaysUsageDescription + 闲言需要使用蓝牙以发现和配对附近设备进行文件传输 + NSCameraUsageDescription + 闲言需要使用相机以拍照制作壁纸、扫描二维码和文件传输扫码配对 + NSDocumentsFolderUsageDescription + 闲言需要访问文件以选择图片和导出内容 + NSLocalNetworkUsageDescription + 闲言需要本地网络权限以发现和连接局域网设备 + NSMicrophoneUsageDescription + 闲言需要使用麦克风录制语音消息 + NSPhotoLibraryAddUsageDescription + 闲言需要保存编辑好的卡片到您的相册 + NSPhotoLibraryUsageDescription + 闲言需要访问您的相册以选择图片作为卡片背景 + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneConfigurationName + flutter + UISceneDelegateClassName + FlutterSceneDelegate + UISceneStoryboardFile + Main + + + + + UIApplicationSupportsIndirectInputEvents + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile @@ -41,74 +100,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - CADisableMinimumFrameDurationOnPhone - - UIApplicationSupportsIndirectInputEvents - - - - - - NSCameraUsageDescription - 闲言需要使用相机以拍照制作壁纸、扫描二维码和文件传输扫码配对 - - - NSPhotoLibraryUsageDescription - 闲言需要访问您的相册以选择图片作为卡片背景 - - - NSPhotoLibraryAddUsageDescription - 闲言需要保存编辑好的卡片到您的相册 - - - NSDocumentsFolderUsageDescription - 闲言需要访问文件以选择图片和导出内容 - - - NSMicrophoneUsageDescription - 闲言需要使用麦克风录制语音消息 - - - NSBluetoothAlwaysUsageDescription - 闲言需要使用蓝牙以发现和配对附近设备进行文件传输 - - - NFCReaderUsageDescription - 闲言需要使用NFC以触碰配对设备进行文件传输 - - - NSLocalNetworkUsageDescription - 闲言需要本地网络权限以发现和连接局域网设备 - - - - - NSAppTransportSecurity - - NSAllowsArbitraryLoads - - - - - - - CFBundleURLTypes - - - CFBundleTypeRole - Viewer - CFBundleURLName - apps.xy.xianyan - CFBundleURLSchemes - - xianyan - - - - - - - com.apple.security.application-groups group.apps.xy.xianyan.share diff --git a/lib/core/storage/secure_storage.dart b/lib/core/storage/secure_storage.dart index dc827f0f..896f2dcc 100644 --- a/lib/core/storage/secure_storage.dart +++ b/lib/core/storage/secure_storage.dart @@ -1,12 +1,14 @@ /// ============================================================ /// 闲言APP — 安全存储 /// 创建时间: 2026-04-20 -/// 更新时间: 2026-04-20 +/// 更新时间: 2026-05-22 /// 作用: flutter_secure_storage 封装,用于敏感数据存储 -/// 上次更新: 初始创建 +/// 上次更新: macOS Keychain 兼容性修复,临时使用 shared_preferences 替代 /// ============================================================ +import 'package:flutter/foundation.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:shared_preferences/shared_preferences.dart'; /// 安全存储键名常量 class SecureKeys { @@ -26,6 +28,7 @@ class SecureKeys { /// /// 用于存储敏感数据如 Token、密码等, /// 底层使用 Keychain (iOS) / EncryptedSharedPreferences (Android)。 +/// macOS 临时使用 shared_preferences 避免 Keychain 配置问题 class SecureStorage { SecureStorage._(); @@ -33,29 +36,84 @@ class SecureStorage { accessibility: KeychainAccessibility.first_unlock_this_device, ); + static const _macosOptions = MacOsOptions( + usesDataProtectionKeychain: false, + accessibility: KeychainAccessibility.first_unlock_this_device, + groupId: null, + ); + static const FlutterSecureStorage _storage = FlutterSecureStorage( iOptions: _iosOptions, + mOptions: _macosOptions, ); + // macOS 临时使用 shared_preferences 替代 + static SharedPreferences? _prefs; + + static Future _ensurePrefs() async { + _prefs ??= await SharedPreferences.getInstance(); + } + + static bool get _isMacOS => defaultTargetPlatform == TargetPlatform.macOS; + // ============================================================ // 通用读写 // ============================================================ /// 读取 - static Future read(String key) => _storage.read(key: key); + static Future read(String key) async { + if (_isMacOS) { + await _ensurePrefs(); + return _prefs!.getString(key); + } else { + return _storage.read(key: key); + } + } /// 写入 - static Future write(String key, String value) => - _storage.write(key: key, value: value); + static Future write(String key, String value) async { + if (_isMacOS) { + await _ensurePrefs(); + await _prefs!.setString(key, value); + } else { + await _storage.write(key: key, value: value); + } + } /// 删除 - static Future delete(String key) => _storage.delete(key: key); + static Future delete(String key) async { + if (_isMacOS) { + await _ensurePrefs(); + await _prefs!.remove(key); + } else { + await _storage.delete(key: key); + } + } /// 是否包含 - static Future containsKey(String key) => _storage.containsKey(key: key); + static Future containsKey(String key) async { + if (_isMacOS) { + await _ensurePrefs(); + return _prefs!.containsKey(key); + } else { + return _storage.containsKey(key: key); + } + } /// 清空所有 - static Future deleteAll() => _storage.deleteAll(); + static Future deleteAll() async { + if (_isMacOS) { + await _ensurePrefs(); + final keys = _prefs!.getKeys(); + for (final key in keys) { + if (key.startsWith('auth_') || key == 'userId') { + await _prefs!.remove(key); + } + } + } else { + await _storage.deleteAll(); + } + } // ============================================================ // 便捷方法 diff --git a/macos/Flutter/Flutter-Debug.xcconfig b/macos/Flutter/Flutter-Debug.xcconfig index c2efd0b6..4b81f9b2 100644 --- a/macos/Flutter/Flutter-Debug.xcconfig +++ b/macos/Flutter/Flutter-Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/Flutter-Release.xcconfig b/macos/Flutter/Flutter-Release.xcconfig index c2efd0b6..5caa9d15 100644 --- a/macos/Flutter/Flutter-Release.xcconfig +++ b/macos/Flutter/Flutter-Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index e1e99aa4..c6f3e02a 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -16,7 +16,7 @@ import flutter_blue_plus_darwin import flutter_image_compress_macos import flutter_inappwebview_macos import flutter_local_notifications -import flutter_secure_storage_macos +import flutter_secure_storage_darwin import flutter_tts import flutter_webrtc import gal @@ -49,7 +49,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FlutterImageCompressMacosPlugin.register(with: registry.registrar(forPlugin: "FlutterImageCompressMacosPlugin")) InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) - FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) + FlutterSecureStorageDarwinPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageDarwinPlugin")) FlutterTtsPlugin.register(with: registry.registrar(forPlugin: "FlutterTtsPlugin")) FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin")) GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin")) diff --git a/macos/Podfile.lock b/macos/Podfile.lock index ac3b4a75..b91d6039 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -11,6 +11,7 @@ PODS: - device_info_plus (0.0.1): - FlutterMacOS - file_picker (0.0.1): + - Flutter - FlutterMacOS - file_selector_macos (0.0.1): - FlutterMacOS @@ -106,7 +107,7 @@ DEPENDENCIES: - battery_plus (from `Flutter/ephemeral/.symlinks/plugins/battery_plus/macos`) - connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos`) - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`) - - file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/macos`) + - file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/darwin`) - file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`) - flutter_blue_plus_darwin (from `Flutter/ephemeral/.symlinks/plugins/flutter_blue_plus_darwin/darwin`) - flutter_image_compress_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_image_compress_macos/macos`) @@ -152,7 +153,7 @@ EXTERNAL SOURCES: device_info_plus: :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos file_picker: - :path: Flutter/ephemeral/.symlinks/plugins/file_picker/macos + :path: Flutter/ephemeral/.symlinks/plugins/file_picker/darwin file_selector_macos: :path: Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos flutter_blue_plus_darwin: @@ -212,7 +213,7 @@ SPEC CHECKSUMS: battery_plus: f51ad29136e025b714b96f7d096f44f604615da7 connectivity_plus: 4adf20a405e25b42b9c9f87feff8f4b6fde18a4e device_info_plus: 4fb280989f669696856f8b129e4a5e3cd6c48f76 - file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a + file_picker: 70164d9778c42c47218d6cd79ce435de0856b11a file_selector_macos: 9e9e068e90ebee155097d00e89ae91edb2374db7 flutter_blue_plus_darwin: 20a08bfeaa0f7804d524858d3d8744bcc1b6dbc3 flutter_image_compress_macos: e68daf54bb4bf2144c580fd4d151c949cbf492f0 diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 478a8856..0d49bfad 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -27,6 +27,8 @@ 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 69AC2FD7F25C22464C6AA971 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5EAD42DE88C17A1D7496A378 /* Pods_RunnerTests.framework */; }; + 740419A3DA2BFE0F5E85D9AF /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9097D4D2EB98F14D8827394A /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -64,7 +66,7 @@ 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* xianyan.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "xianyan.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10ED2044A3C60003C045 /* xianyan.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = xianyan.app; sourceTree = BUILT_PRODUCTS_DIR; }; 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; @@ -76,8 +78,16 @@ 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 475082748095A981E9B2017D /* 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 = ""; }; + 5EAD42DE88C17A1D7496A378 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 9097D4D2EB98F14D8827394A /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + 9DF906408CB20CD31706ACF0 /* 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 = ""; }; + CC173CB9C4D834544F95E13C /* 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 = ""; }; + E124C85A506C3CD1E10E0BF4 /* 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 = ""; }; + F3F87547C10EAEA1CDC2EAC9 /* 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 = ""; }; + FFA53435E554D491C8B3505D /* 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 = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -85,6 +95,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 69AC2FD7F25C22464C6AA971 /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -92,6 +103,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 740419A3DA2BFE0F5E85D9AF /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -125,6 +137,7 @@ 331C80D6294CF71000263BE5 /* RunnerTests */, 33CC10EE2044A3C60003C045 /* Products */, D73912EC22F37F3D000D13A0 /* Frameworks */, + 59B25E09ABD2254CC7908B73 /* Pods */, ); sourceTree = ""; }; @@ -172,9 +185,25 @@ path = Runner; sourceTree = ""; }; + 59B25E09ABD2254CC7908B73 /* Pods */ = { + isa = PBXGroup; + children = ( + E124C85A506C3CD1E10E0BF4 /* Pods-Runner.debug.xcconfig */, + F3F87547C10EAEA1CDC2EAC9 /* Pods-Runner.release.xcconfig */, + FFA53435E554D491C8B3505D /* Pods-Runner.profile.xcconfig */, + CC173CB9C4D834544F95E13C /* Pods-RunnerTests.debug.xcconfig */, + 475082748095A981E9B2017D /* Pods-RunnerTests.release.xcconfig */, + 9DF906408CB20CD31706ACF0 /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; D73912EC22F37F3D000D13A0 /* Frameworks */ = { isa = PBXGroup; children = ( + 9097D4D2EB98F14D8827394A /* Pods_Runner.framework */, + 5EAD42DE88C17A1D7496A378 /* Pods_RunnerTests.framework */, ); name = Frameworks; sourceTree = ""; @@ -186,6 +215,7 @@ isa = PBXNativeTarget; buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( + A56E9379F374F7CCC33D60A7 /* [CP] Check Pods Manifest.lock */, 331C80D1294CF70F00263BE5 /* Sources */, 331C80D2294CF70F00263BE5 /* Frameworks */, 331C80D3294CF70F00263BE5 /* Resources */, @@ -204,11 +234,13 @@ isa = PBXNativeTarget; buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + D0015455C5ABF67198AEC3E4 /* [CP] Check Pods Manifest.lock */, 33CC10E92044A3C60003C045 /* Sources */, 33CC10EA2044A3C60003C045 /* Frameworks */, 33CC10EB2044A3C60003C045 /* Resources */, 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, + D06B9597A43A5822D591EB0B /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -329,6 +361,67 @@ shellPath = /bin/sh; shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; }; + A56E9379F374F7CCC33D60A7 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + 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; + }; + D0015455C5ABF67198AEC3E4 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + 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; + }; + D06B9597A43A5822D591EB0B /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -380,9 +473,12 @@ /* Begin XCBuildConfiguration section */ 331C80DB294CF71000263BE5 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = CC173CB9C4D834544F95E13C /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 5V9NVUU6K5; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = apps.xy.xianyan.RunnerTests; @@ -394,9 +490,12 @@ }; 331C80DC294CF71000263BE5 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 475082748095A981E9B2017D /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 5V9NVUU6K5; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = apps.xy.xianyan.RunnerTests; @@ -408,9 +507,12 @@ }; 331C80DD294CF71000263BE5 /* Profile */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 9DF906408CB20CD31706ACF0 /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 5V9NVUU6K5; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = apps.xy.xianyan.RunnerTests; @@ -478,6 +580,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = 5V9NVUU6K5; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -610,6 +713,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = 5V9NVUU6K5; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -630,6 +734,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = 5V9NVUU6K5; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", diff --git a/macos/Runner.xcworkspace/contents.xcworkspacedata b/macos/Runner.xcworkspace/contents.xcworkspacedata index 1d526a16..21a3cc14 100644 --- a/macos/Runner.xcworkspace/contents.xcworkspacedata +++ b/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -4,4 +4,7 @@ + + diff --git a/macos/Runner/DebugProfile.entitlements b/macos/Runner/DebugProfile.entitlements index 870aa62c..9cf31232 100644 --- a/macos/Runner/DebugProfile.entitlements +++ b/macos/Runner/DebugProfile.entitlements @@ -3,7 +3,7 @@ com.apple.security.app-sandbox - + com.apple.security.cs.allow-jit com.apple.security.network.client diff --git a/macos/Runner/Release.entitlements b/macos/Runner/Release.entitlements index 870aa62c..c3082a68 100644 --- a/macos/Runner/Release.entitlements +++ b/macos/Runner/Release.entitlements @@ -14,5 +14,11 @@ com.apple.security.files.user-selected.read-write + com.apple.security.keychain + + keychain-access-groups + + $(AppIdentifierPrefix)$(CFBundleIdentifier) + diff --git a/scripts/favorite_sync_test.dart b/scripts/favorite_sync_test.dart deleted file mode 100644 index 8dc5dba5..00000000 --- a/scripts/favorite_sync_test.dart +++ /dev/null @@ -1,542 +0,0 @@ -// ============================================================ -// 闲言APP — 收藏同步接口测试脚本 -// 创建时间: 2026-05-14 -// 更新时间: 2026-05-14 -// 作用: 测试收藏系统HTTP接口 -// - 收藏列表获取 (Feed API + UserCenter API) -// - 收藏添加/删除 -// - Feed API收藏操作 -// - 同步逻辑验证 -// - API基础URL: https://tools.wktyl.com -// 上次更新: 初始版本 -// 运行: dart run Scripts/favorite_sync_test.dart [token] -// ============================================================ - -import 'dart:convert'; -import 'dart:io'; - -const String kApiBase = 'https://tools.wktyl.com'; -const String kUserCenterBase = '/api/user_center'; -const String kFeedBase = '/api/feed'; -const Duration kTimeout = Duration(seconds: 15); - -int _passCount = 0; -int _failCount = 0; - -void _result(String name, bool pass, {String? detail}) { - final icon = pass ? '✅' : '❌'; - final status = pass ? 'PASS' : 'FAIL'; - _passCount += pass ? 1 : 0; - _failCount += pass ? 0 : 1; - print('$icon [$status] $name${detail != null ? ' — $detail' : ''}'); -} - -Future?> _httpGet( - String path, { - Map? queryParameters, - String? token, -}) async { - try { - final uri = Uri.parse('$kApiBase$path').replace( - queryParameters: queryParameters?.map( - (k, v) => MapEntry(k, v.toString()), - ), - ); - final client = HttpClient(); - client.connectionTimeout = kTimeout; - final request = await client.getUrl(uri); - request.headers.set('Accept', 'application/json'); - request.headers.set('Content-Type', 'application/x-www-form-urlencoded'); - if (token != null && token.isNotEmpty) { - request.headers.set('Authorization', 'Bearer $token'); - } - final response = await request.close(); - final body = await response.transform(utf8.decoder).join(); - client.close(); - return jsonDecode(body) as Map; - } catch (e) { - print(' ⚠️ HTTP GET $path 失败: $e'); - return null; - } -} - -Future?> _httpPost( - String path, { - Map? data, - String? token, -}) async { - try { - final uri = Uri.parse('$kApiBase$path'); - final client = HttpClient(); - client.connectionTimeout = kTimeout; - final request = await client.postUrl(uri); - request.headers.set('Accept', 'application/json'); - request.headers.set('Content-Type', 'application/x-www-form-urlencoded'); - if (token != null && token.isNotEmpty) { - request.headers.set('Authorization', 'Bearer $token'); - } - if (data != null && data.isNotEmpty) { - final formData = data.entries - .map((e) => - '${Uri.encodeComponent(e.key)}=${Uri.encodeComponent(e.value.toString())}') - .join('&'); - request.write(formData); - } - final response = await request.close(); - final body = await response.transform(utf8.decoder).join(); - client.close(); - return jsonDecode(body) as Map; - } catch (e) { - print(' ⚠️ HTTP POST $path 失败: $e'); - return null; - } -} - -String? _testToken; - -Future main(List args) async { - print('╔══════════════════════════════════════════════════════════╗'); - print('║ 闲言APP — 收藏同步接口测试 ║'); - print('║ API Base: $kApiBase ║'); - print('╚══════════════════════════════════════════════════════════╝\n'); - - if (args.isNotEmpty) { - _testToken = args[0]; - print('🔑 使用提供的Token: ${_testToken!.substring(0, 10)}...\n'); - } else { - print('⚠️ 未提供Token, 部分需要登录的接口可能返回401'); - print(' 用法: dart run Scripts/favorite_sync_test.dart \n'); - } - - await testFeedFavoriteList(); - await testUserCenterFavoriteList(); - await testFavoriteAddAndRemove(); - await testFeedActionFavorite(); - await testFavoriteSyncLogic(); - - print('\n╔══════════════════════════════════════════════════════════╗'); - print('║ 测试结果汇总 ║'); - print('╠══════════════════════════════════════════════════════════╣'); - print('║ ✅ 通过: $_passCount ║'); - print('║ ❌ 失败: $_failCount ║'); - print('║ 📊 总计: ${_passCount + _failCount} ║'); - print('╚══════════════════════════════════════════════════════════╝'); - - exit(_failCount > 0 ? 1 : 0); -} - -Future testFeedFavoriteList() async { - print('\n━━━ 1. Feed API 收藏列表获取 ━━━'); - - final result = await _httpGet( - '$kFeedBase/favorites', - queryParameters: {'page': '1', 'limit': '10'}, - token: _testToken, - ); - - _result( - 'Feed收藏列表接口响应', - result != null, - detail: result != null ? 'code=${result['code']}' : '请求失败', - ); - - if (result != null) { - final code = result['code'] as int? ?? 0; - _result( - '接口返回成功(code=1)', - code == 1, - detail: 'code=$code, msg=${result['msg']}', - ); - - if (code == 1) { - final data = result['data'] as Map? ?? {}; - final list = data['list'] as List? ?? []; - final total = data['total'] as int? ?? 0; - final page = data['page'] as int? ?? 1; - - _result( - '收藏列表非空', - list.isNotEmpty || total == 0, - detail: 'total=$total, page=$page, list.length=${list.length}', - ); - - if (list.isNotEmpty) { - final firstItem = list.first as Map; - final hasRequiredFields = firstItem.containsKey('id') && - firstItem.containsKey('feed_type') && - firstItem.containsKey('content'); - _result( - '收藏项包含必要字段(id/feed_type/content)', - hasRequiredFields, - detail: 'keys=${firstItem.keys.join(', ')}', - ); - - final isFavorited = firstItem['is_favorited'] as bool? ?? false; - _result( - '收藏项标记为已收藏', - isFavorited, - detail: 'is_favorited=$isFavorited', - ); - } - } - } -} - -Future testUserCenterFavoriteList() async { - print('\n━━━ 2. UserCenter API 收藏列表获取 ━━━'); - - final result = await _httpPost( - '$kUserCenterBase/favorite', - data: { - 'action': 'list', - 'target_type': 'article', - 'page': '1', - 'limit': '10', - }, - token: _testToken, - ); - - _result( - 'UserCenter收藏列表接口响应', - result != null, - detail: result != null ? 'code=${result['code']}' : '请求失败', - ); - - if (result != null) { - final code = result['code'] as int? ?? 0; - _result( - '接口返回成功(code=1)', - code == 1, - detail: 'code=$code, msg=${result['msg']}', - ); - - if (code == 1) { - final data = result['data'] as Map? ?? {}; - final list = data['list'] as List? ?? []; - final total = data['total'] as int? ?? 0; - - _result( - 'UserCenter收藏列表数据', - true, - detail: 'total=$total, list.length=${list.length}', - ); - - if (list.isNotEmpty) { - final firstItem = list.first as Map; - final hasRequiredFields = firstItem.containsKey('id') && - firstItem.containsKey('target_type') && - firstItem.containsKey('target_id'); - _result( - '收藏项包含必要字段(id/target_type/target_id)', - hasRequiredFields, - detail: 'keys=${firstItem.keys.join(', ')}', - ); - } - } - } -} - -Future testFavoriteAddAndRemove() async { - print('\n━━━ 3. 收藏添加/删除测试 ━━━'); - - if (_testToken == null) { - _result('收藏添加/删除(需登录)', false, detail: '未提供Token, 跳过'); - return; - } - - final feedListResult = await _httpGet( - '$kFeedBase/list', - queryParameters: {'channel': 'all', 'limit': '5'}, - ); - - int? testFeedId; - String? testFeedType; - - if (feedListResult != null && feedListResult['code'] == 1) { - final data = feedListResult['data'] as Map? ?? {}; - final list = data['list'] as List? ?? []; - if (list.isNotEmpty) { - final firstItem = list.first as Map; - testFeedId = firstItem['id'] as int?; - testFeedType = firstItem['feed_type'] as String? ?? 'feed'; - } - } - - if (testFeedId == null) { - _result('获取测试用Feed项', false, detail: '无法获取Feed列表中的项目'); - return; - } - - _result('获取测试用Feed项', true, detail: 'id=$testFeedId, type=$testFeedType'); - - final checkResult = await _httpPost( - '$kUserCenterBase/favorite', - data: { - 'action': 'check', - 'target_type': testFeedType, - 'target_id': '$testFeedId', - }, - token: _testToken, - ); - - bool wasFavorited = false; - if (checkResult != null && checkResult['code'] == 1) { - final data = checkResult['data'] as Map? ?? {}; - wasFavorited = data['is_favorited'] as bool? ?? false; - _result('收藏状态检查', true, detail: 'is_favorited=$wasFavorited'); - } else { - _result( - '收藏状态检查', - false, - detail: '接口返回code=${checkResult?['code']}', - ); - } - - final addAction = wasFavorited ? 'remove' : 'add'; - final addResult = await _httpPost( - '$kUserCenterBase/favorite', - data: { - 'action': addAction, - 'target_type': testFeedType, - 'target_id': '$testFeedId', - }, - token: _testToken, - ); - - _result( - '收藏${addAction == 'add' ? '添加' : '移除'}操作', - addResult != null && addResult['code'] == 1, - detail: addResult != null - ? 'code=${addResult['code']}, msg=${addResult['msg']}' - : '请求失败', - ); - - final verifyResult = await _httpPost( - '$kUserCenterBase/favorite', - data: { - 'action': 'check', - 'target_type': testFeedType, - 'target_id': '$testFeedId', - }, - token: _testToken, - ); - - if (verifyResult != null && verifyResult['code'] == 1) { - final data = verifyResult['data'] as Map? ?? {}; - final nowFavorited = data['is_favorited'] as bool? ?? false; - final expectedFavorited = !wasFavorited; - _result( - '收藏状态已变更', - nowFavorited == expectedFavorited, - detail: '操作前=$wasFavorited, 操作后=$nowFavorited, 期望=$expectedFavorited', - ); - } - - final restoreAction = wasFavorited ? 'add' : 'remove'; - final restoreResult = await _httpPost( - '$kUserCenterBase/favorite', - data: { - 'action': restoreAction, - 'target_type': testFeedType, - 'target_id': '$testFeedId', - }, - token: _testToken, - ); - - _result( - '恢复原始收藏状态', - restoreResult != null && restoreResult['code'] == 1, - detail: '已恢复为${wasFavorited ? "已收藏" : "未收藏"}', - ); -} - -Future testFeedActionFavorite() async { - print('\n━━━ 4. Feed API 收藏操作测试 ━━━'); - - if (_testToken == null) { - _result('Feed API收藏操作(需登录)', false, detail: '未提供Token, 跳过'); - return; - } - - final feedListResult = await _httpGet( - '$kFeedBase/list', - queryParameters: {'channel': 'all', 'limit': '3'}, - ); - - int? testFeedId; - String? testFeedType; - - if (feedListResult != null && feedListResult['code'] == 1) { - final data = feedListResult['data'] as Map? ?? {}; - final list = data['list'] as List? ?? []; - if (list.isNotEmpty) { - final item = list.last as Map; - testFeedId = item['id'] as int?; - testFeedType = item['feed_type'] as String? ?? 'feed'; - } - } - - if (testFeedId == null) { - _result('获取测试用Feed项', false, detail: '无法获取Feed列表中的项目'); - return; - } - - _result('获取测试用Feed项', true, detail: 'id=$testFeedId, type=$testFeedType'); - - final favoriteResult = await _httpPost( - '$kFeedBase/action', - data: { - 'action': 'favorite', - 'feed_type': testFeedType, - 'feed_id': '$testFeedId', - }, - token: _testToken, - ); - - _result( - 'Feed API收藏操作', - favoriteResult != null && favoriteResult['code'] == 1, - detail: favoriteResult != null - ? 'code=${favoriteResult['code']}, msg=${favoriteResult['msg']}' - : '请求失败', - ); - - await Future.delayed(const Duration(seconds: 1)); - - final unfavoriteResult = await _httpPost( - '$kFeedBase/action', - data: { - 'action': 'unfavorite', - 'feed_type': testFeedType, - 'feed_id': '$testFeedId', - }, - token: _testToken, - ); - - _result( - 'Feed API取消收藏操作', - unfavoriteResult != null && unfavoriteResult['code'] == 1, - detail: unfavoriteResult != null - ? 'code=${unfavoriteResult['code']}, msg=${unfavoriteResult['msg']}' - : '请求失败', - ); -} - -Future testFavoriteSyncLogic() async { - print('\n━━━ 5. 同步逻辑验证 ━━━'); - - if (_testToken == null) { - _result('同步逻辑验证(需登录)', false, detail: '未提供Token, 跳过'); - return; - } - - final feedFavResult = await _httpGet( - '$kFeedBase/favorites', - queryParameters: {'page': '1', 'limit': '20'}, - token: _testToken, - ); - - int feedFavCount = 0; - List feedFavIds = []; - - if (feedFavResult != null && feedFavResult['code'] == 1) { - final data = feedFavResult['data'] as Map? ?? {}; - final list = data['list'] as List? ?? []; - feedFavCount = data['total'] as int? ?? list.length; - feedFavIds = list - .map((e) => (e as Map)['id'] as int? ?? 0) - .where((id) => id > 0) - .toList(); - } - - _result( - 'Feed API收藏列表可获取', - feedFavResult != null, - detail: 'total=$feedFavCount, 本页${feedFavIds.length}条', - ); - - final ucFavResult = await _httpPost( - '$kUserCenterBase/favorite', - data: {'action': 'list', 'target_type': 'article', 'page': '1', 'limit': '20'}, - token: _testToken, - ); - - int ucFavCount = 0; - List ucFavIds = []; - - if (ucFavResult != null && ucFavResult['code'] == 1) { - final data = ucFavResult['data'] as Map? ?? {}; - final list = data['list'] as List? ?? []; - ucFavCount = data['total'] as int? ?? list.length; - ucFavIds = list - .map( - (e) => (e as Map)['target_id'] as int? ?? 0) - .where((id) => id > 0) - .toList(); - } - - _result( - 'UserCenter API收藏列表可获取', - ucFavResult != null, - detail: 'total=$ucFavCount, 本页${ucFavIds.length}条', - ); - - final countResult = await _httpPost( - '$kUserCenterBase/favorite', - data: {'action': 'count'}, - token: _testToken, - ); - - if (countResult != null && countResult['code'] == 1) { - final data = countResult['data'] as Map? ?? {}; - final counts = data['counts'] as Map? ?? {}; - _result( - '收藏统计接口可用', - true, - detail: 'counts=$counts', - ); - } else { - _result( - '收藏统计接口', - false, - detail: 'code=${countResult?['code']}, msg=${countResult?['msg']}', - ); - } - - final groupsResult = await _httpPost( - '$kUserCenterBase/favorite', - data: {'action': 'groups'}, - token: _testToken, - ); - - if (groupsResult != null && groupsResult['code'] == 1) { - final data = groupsResult['data'] as Map? ?? {}; - final groups = data['groups'] as List? ?? []; - _result( - '收藏分组接口可用', - true, - detail: 'groups=${groups.join(', ')}', - ); - } else { - _result( - '收藏分组接口', - false, - detail: 'code=${groupsResult?['code']}, msg=${groupsResult?['msg']}', - ); - } - - if (feedFavIds.isNotEmpty && ucFavIds.isNotEmpty) { - final overlapIds = feedFavIds.toSet().intersection(ucFavIds.toSet()); - _result( - '两个API收藏数据有交集(同步一致性)', - overlapIds.isNotEmpty, - detail: 'Feed API ${feedFavIds.length}条, UserCenter API ${ucFavIds.length}条, 交集${overlapIds.length}条', - ); - } else { - _result( - '同步一致性验证', - true, - detail: '至少一个API收藏列表为空, 无法比较交集', - ); - } -} diff --git a/scripts/file_transfer_full_test.dart b/scripts/file_transfer_full_test.dart deleted file mode 100644 index f74a6a1b..00000000 --- a/scripts/file_transfer_full_test.dart +++ /dev/null @@ -1,900 +0,0 @@ -// ============================================================ -// 闲言APP — 文件传输全流程验证脚本 v3 -// 创建时间: 2026-05-12 -// 更新时间: 2026-05-13 -// 作用: 模拟两个设备通过信令服务器互发消息/文件/数据 -// 验证配对、消息发送、文件传输、送达回执、在线状态等全流程 -// 上次更新: v3 新增deviceOnline/deviceOffline测试+text-message vs wsRelay对比 -// 运行: dart run Scripts/file_transfer_full_test.dart -// ============================================================ - -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; -import 'dart:math'; - -import 'package:web_socket_channel/web_socket_channel.dart'; - -const String kSignalingUrl = 'wss://tools.wktyl.com:9443'; -const String kApiBase = 'https://tools.wktyl.com/api/file_transfer'; -const Duration kTimeout = Duration(seconds: 10); -const Duration kHeartbeatInterval = Duration(seconds: 30); - -class TestDevice { - TestDevice({required this.localId, required this.alias, this.userId}); - - final String localId; - String? serverId; - final String alias; - final String? userId; - WebSocketChannel? _channel; - bool _isConnected = false; - bool get isConnected => _isConnected; - - String get effectiveId => serverId ?? localId; - - final StreamController> _messageController = - StreamController>.broadcast(); - Stream> get onMessage => _messageController.stream; - - final List> allMessages = []; - Timer? _heartbeatTimer; - - Future connect() async { - try { - print('[$alias] Connecting to $kSignalingUrl...'); - _channel = WebSocketChannel.connect(Uri.parse(kSignalingUrl)); - await _channel!.ready.timeout(kTimeout); - - _channel!.stream.listen( - (data) { - _handleMessage(data as String); - }, - onDone: () { - _isConnected = false; - print('[$alias] Connection closed'); - }, - onError: (error) { - _isConnected = false; - print('[$alias] Connection error: $error'); - }, - ); - - _isConnected = true; - - _sendRegister(); - _startHeartbeat(); - - print('[$alias] Connected, waiting for server ID...'); - return true; - } catch (e) { - print('[$alias] Connection failed: $e'); - return false; - } - } - - void _sendRegister() { - _send({ - 'type': 'register', - 'from': localId, - 'payload': { - 'alias': alias, - 'fingerprint': 'test-fp-${localId.substring(0, 8)}', - 'deviceType': 'headless', - 'deviceModel': 'TestDevice', - if (userId != null) 'userId': userId, - }, - 'ts': DateTime.now().millisecondsSinceEpoch, - }); - } - - void _startHeartbeat() { - _heartbeatTimer?.cancel(); - _heartbeatTimer = Timer.periodic(kHeartbeatInterval, (_) { - if (_isConnected) { - _send({ - 'type': 'heartbeat', - 'from': effectiveId, - 'ts': DateTime.now().millisecondsSinceEpoch, - }); - } - }); - } - - void _handleMessage(String data) { - try { - final json = jsonDecode(data) as Map; - allMessages.add(json); - _messageController.add(json); - - final type = json['type'] as String? ?? ''; - final from = json['from'] as String? ?? json['sender'] as String? ?? ''; - - if (type == 'registered') { - final assignedId = - json['id'] as String? ?? - json['payload']?['deviceId'] as String? ?? - json['payload']?['id'] as String? ?? - json['deviceId'] as String?; - if (assignedId != null && serverId == null) { - serverId = assignedId; - print('[$alias] Server assigned ID: $serverId'); - } - } - - if (type == 'display-name') { - final sid = - json['payload']?['id'] as String? ?? - json['id'] as String? ?? - json['payload']?['deviceId'] as String? ?? - json['sender'] as String?; - if (sid != null && serverId == null) { - serverId = sid; - print('[$alias] Got ID from display-name: $serverId'); - } - } - - print('[$alias] Received: type=$type from=$from'); - } catch (e) { - print('[$alias] Parse error: $e'); - } - } - - void _send(Map msg) { - if (!_isConnected || _channel == null) return; - _channel!.sink.add(jsonEncode(msg)); - } - - void sendTextMessage(String targetId, String text) { - _send({ - 'type': 'text-message', - 'from': effectiveId, - 'to': targetId, - 'payload': {'text': text}, - 'ts': DateTime.now().millisecondsSinceEpoch, - }); - print('[$alias] Sent text-message to $targetId: "$text"'); - } - - void sendWsRelayText(String targetId, String text) { - _send({ - 'type': 'wsRelay', - 'from': effectiveId, - 'to': targetId, - 'relayType': 'text', - 'payload': {'text': text}, - 'ts': DateTime.now().millisecondsSinceEpoch, - }); - print('[$alias] Sent wsRelay text to $targetId: "$text"'); - } - - void sendFileMeta( - String targetId, - String fileName, - int fileSize, - String fileId, - ) { - _send({ - 'type': 'wsRelay', - 'from': effectiveId, - 'to': targetId, - 'relayType': 'file-meta', - 'payload': { - 'fileId': fileId, - 'fileName': fileName, - 'fileSize': fileSize, - 'chunkSize': 65536, - 'checksum': 'test-checksum', - }, - 'ts': DateTime.now().millisecondsSinceEpoch, - }); - print('[$alias] Sent file meta to $targetId: $fileName ($fileSize bytes)'); - } - - void sendFileChunk( - String targetId, - String fileId, - int chunkIndex, - String base64Data, - ) { - _send({ - 'type': 'wsRelay', - 'from': effectiveId, - 'to': targetId, - 'relayType': 'file-chunk', - 'payload': { - 'fileId': fileId, - 'chunkIndex': chunkIndex, - 'data': base64Data, - }, - 'ts': DateTime.now().millisecondsSinceEpoch, - }); - } - - void sendFileComplete(String targetId, String fileId) { - _send({ - 'type': 'wsRelay', - 'from': effectiveId, - 'to': targetId, - 'relayType': 'file-complete', - 'payload': {'fileId': fileId}, - 'ts': DateTime.now().millisecondsSinceEpoch, - }); - print('[$alias] Sent file complete to $targetId: $fileId'); - } - - void sendDeliveryAck(String targetId, String messageId) { - _send({ - 'type': 'delivery-ack', - 'from': effectiveId, - 'to': targetId, - 'payload': {'messageId': messageId}, - 'ts': DateTime.now().millisecondsSinceEpoch, - }); - print('[$alias] Sent delivery ack to $targetId for $messageId'); - } - - void sendPairRequest(String targetId) { - _send({ - 'type': 'pair-request', - 'from': effectiveId, - 'to': targetId, - 'payload': { - 'alias': alias, - 'fingerprint': 'test-fp-${localId.substring(0, 8)}', - 'deviceType': 'headless', - }, - 'ts': DateTime.now().millisecondsSinceEpoch, - }); - print('[$alias] Sent pair request to $targetId'); - } - - void sendPairAccept(String targetId) { - _send({ - 'type': 'pair-accept', - 'from': effectiveId, - 'to': targetId, - 'payload': { - 'alias': alias, - 'fingerprint': 'test-fp-${localId.substring(0, 8)}', - }, - 'ts': DateTime.now().millisecondsSinceEpoch, - }); - print('[$alias] Sent pair accept to $targetId'); - } - - void sendPairReject(String targetId) { - _send({ - 'type': 'pair-reject', - 'from': effectiveId, - 'to': targetId, - 'ts': DateTime.now().millisecondsSinceEpoch, - }); - print('[$alias] Sent pair reject to $targetId'); - } - - void discover() { - _send({ - 'type': 'discover', - 'from': effectiveId, - 'ts': DateTime.now().millisecondsSinceEpoch, - }); - print('[$alias] Sent discover request'); - } - - void ping(String targetId) { - _send({ - 'type': 'ping', - 'from': effectiveId, - 'to': targetId, - 'ts': DateTime.now().millisecondsSinceEpoch, - }); - print('[$alias] Sent ping to $targetId'); - } - - Future disconnect() async { - _heartbeatTimer?.cancel(); - if (_isConnected && _channel != null) { - _send({ - 'type': 'leave', - 'from': effectiveId, - 'ts': DateTime.now().millisecondsSinceEpoch, - }); - await _channel?.sink.close(); - } - _isConnected = false; - print('[$alias] Disconnected'); - } - - Future?> waitForMessage( - String type, { - Duration timeout = kTimeout, - bool Function(Map)? filter, - }) async { - try { - return await onMessage - .where((msg) { - final msgType = msg['type'] as String? ?? ''; - if (msgType != type) return false; - if (filter != null) return filter(msg); - return true; - }) - .first - .timeout(timeout); - } on TimeoutException { - print('[$alias] Timeout waiting for message type: $type'); - return null; - } - } - - Future waitForRegistration({Duration timeout = kTimeout}) async { - if (serverId != null) return true; - - for (final msg in allMessages) { - final type = msg['type'] as String? ?? ''; - if (type == 'registered') { - final id = - msg['id'] as String? ?? - msg['payload']?['deviceId'] as String? ?? - msg['payload']?['id'] as String?; - if (id != null) { - serverId = id; - print('[$alias] Server assigned ID (from cache): $serverId'); - return true; - } - } - } - - try { - await onMessage - .where((msg) { - final type = msg['type'] as String? ?? ''; - return type == 'registered'; - }) - .first - .timeout(timeout); - return serverId != null; - } on TimeoutException { - print('[$alias] Timeout waiting for registration'); - return serverId != null; - } - } -} - -class TestResult { - TestResult(this.name, this.passed, [this.detail]); - final String name; - final bool passed; - final String? detail; - - @override - String toString() { - final icon = passed ? '✅' : '❌'; - return '$icon $name${detail != null ? ': $detail' : ''}'; - } -} - -Future main() async { - print('============================================================'); - print(' 闲言APP 文件传输全流程验证脚本 v3'); - print(' 时间: ${DateTime.now()}'); - print(' 新增: deviceOnline/deviceOffline + text-message vs wsRelay对比'); - print('============================================================\n'); - - final results = []; - - final deviceA = TestDevice( - localId: 'test-a-${Random().nextInt(9999)}', - alias: 'TestDeviceA', - userId: 'test-user-a', - ); - - final deviceB = TestDevice( - localId: 'test-b-${Random().nextInt(9999)}', - alias: 'TestDeviceB', - userId: 'test-user-b', - ); - - // ---- Test 1: REST API Health Check ---- - print('\n--- Test 1: REST API Health Check ---'); - try { - final client = HttpClient(); - final request = await client.getUrl(Uri.parse('$kApiBase/health')); - final response = await request.close().timeout(kTimeout); - final body = await response.transform(utf8.decoder).join(); - client.close(); - final healthy = response.statusCode == 200; - results.add( - TestResult('REST API Health', healthy, 'status=${response.statusCode} body=$body'), - ); - print(' Health check: ${healthy ? "OK" : "FAIL"}'); - } catch (e) { - results.add(TestResult('REST API Health', false, e.toString())); - } - - // ---- Test 2: Signaling Info ---- - print('\n--- Test 2: Signaling Info ---'); - try { - final client = HttpClient(); - final request = await client.getUrl(Uri.parse('$kApiBase/signaling_info')); - final response = await request.close().timeout(kTimeout); - final body = await response.transform(utf8.decoder).join(); - client.close(); - final json = jsonDecode(body) as Map; - results.add( - TestResult( - 'Signaling Info', - json['code'] == 1, - 'url=${json['data']?['signalingUrl']}', - ), - ); - } catch (e) { - results.add(TestResult('Signaling Info', false, e.toString())); - } - - // ---- Test 3: Device A Connect & Register ---- - print('\n--- Test 3: Device A Connect & Register ---'); - final connectedA = await deviceA.connect(); - results.add(TestResult('Device A Connect', connectedA)); - if (connectedA) { - await Future.delayed(const Duration(seconds: 3)); - final regA = await deviceA.waitForRegistration(); - results.add( - TestResult( - 'Device A Registered', - regA && deviceA.serverId != null, - 'serverId=${deviceA.serverId}', - ), - ); - } - - // ---- Test 4: Device B Connect & Register ---- - print('\n--- Test 4: Device B Connect & Register ---'); - final connectedB = await deviceB.connect(); - results.add(TestResult('Device B Connect', connectedB)); - if (connectedB) { - await Future.delayed(const Duration(seconds: 1)); - final regB = await deviceB.waitForRegistration(); - results.add( - TestResult( - 'Device B Registered', - regB && deviceB.serverId != null, - 'serverId=${deviceB.serverId}', - ), - ); - } - - if (!connectedA || - !connectedB || - deviceA.serverId == null || - deviceB.serverId == null) { - print('\n FATAL: Cannot proceed without both devices registered.'); - await deviceA.disconnect(); - await deviceB.disconnect(); - _printResults(results); - return; - } - - print('\n Device A server ID: ${deviceA.serverId}'); - print(' Device B server ID: ${deviceB.serverId}'); - - // ---- Test 5: Discover Devices ---- - print('\n--- Test 5: Discover Devices ---'); - deviceA.discover(); - final discoverResponse = await deviceA.waitForMessage( - 'peers', - timeout: const Duration(seconds: 5), - ); - results.add( - TestResult( - 'Discover Response (peers)', - discoverResponse != null, - discoverResponse != null - ? 'peers count=${(discoverResponse['payload']?['peers'] as List?)?.length ?? 0}' - : null, - ), - ); - - // ---- Test 6: deviceOnline Event ---- - print('\n--- Test 6: deviceOnline Event ---'); - final onlineEvents = deviceA.allMessages.where( - (m) => m['type'] == 'deviceOnline', - ).toList(); - results.add( - TestResult( - 'deviceOnline Event Received by A', - onlineEvents.isNotEmpty, - 'count=${onlineEvents.length}', - ), - ); - if (onlineEvents.isNotEmpty) { - final event = onlineEvents.first; - results.add( - TestResult( - 'deviceOnline Contains Device Info', - event['from'] != null || event['payload'] != null, - 'from=${event['from']} payload=${event['payload']?.toString().substring(0, 50)}', - ), - ); - } - - // ---- Test 7: Ping/Pong ---- - print('\n--- Test 7: Ping/Pong ---'); - deviceA.ping(deviceB.serverId!); - final pongReceived = await deviceA.waitForMessage( - 'pong', - filter: (msg) => msg['from'] == deviceB.serverId, - timeout: const Duration(seconds: 5), - ); - results.add(TestResult('Ping/Pong', pongReceived != null)); - - // ---- Test 8: Pairing Flow ---- - print('\n--- Test 8: Pairing Flow ---'); - deviceA.sendPairRequest(deviceB.serverId!); - - final pairReqAtB = await deviceB.waitForMessage( - 'pair-request', - filter: (msg) => msg['from'] == deviceA.serverId, - timeout: const Duration(seconds: 5), - ); - results.add( - TestResult( - 'Pair Request Received by B', - pairReqAtB != null, - pairReqAtB != null ? 'from=${pairReqAtB['from']}' : null, - ), - ); - - if (pairReqAtB != null) { - deviceB.sendPairAccept(deviceA.serverId!); - - final pairResponse = await deviceA.waitForMessage( - 'pair-response', - timeout: const Duration(seconds: 5), - ); - results.add( - TestResult( - 'Pair Accept/Response Received by A', - pairResponse != null, - pairResponse != null - ? 'from=${pairResponse['from']} status=${pairResponse['payload']?['status']}' - : null, - ), - ); - } else { - final pairResponseDirect = await deviceA.waitForMessage( - 'pair-response', - timeout: const Duration(seconds: 3), - ); - results.add( - TestResult( - 'Pair Response (server-mediated)', - pairResponseDirect != null, - pairResponseDirect != null - ? 'payload=${pairResponseDirect['payload']}' - : null, - ), - ); - } - - // ---- Test 9: text-message A → B (旧协议) ---- - print('\n--- Test 9: text-message A → B (旧协议) ---'); - final testText1 = 'OLD-PROTOCOL from A! ${DateTime.now().millisecondsSinceEpoch}'; - deviceA.sendTextMessage(deviceB.serverId!, testText1); - - final textReceived1 = await deviceB.waitForMessage( - 'text-message', - filter: (msg) => msg['from'] == deviceA.serverId, - timeout: const Duration(seconds: 5), - ); - results.add( - TestResult( - 'text-message A→B (旧协议)', - textReceived1 != null, - textReceived1 != null - ? 'text="${(textReceived1['payload']?['text'] as String?)?.substring(0, 30)}..."' - : '❌ 旧协议text-message未送达! 这就是bug根因', - ), - ); - - // ---- Test 10: wsRelay Text A → B (新协议) ---- - print('\n--- Test 10: wsRelay Text A → B (新协议) ---'); - final relayText1 = 'NEW-PROTOCOL from A! ${DateTime.now().millisecondsSinceEpoch}'; - deviceA.sendWsRelayText(deviceB.serverId!, relayText1); - - final relayReceived1 = await deviceB.waitForMessage( - 'wsRelay', - filter: (msg) => - msg['from'] == deviceA.serverId && - (msg['relayType'] == 'text' || msg['payload']?['relayType'] == 'text'), - timeout: const Duration(seconds: 5), - ); - results.add( - TestResult( - 'wsRelay Text A→B (新协议)', - relayReceived1 != null, - relayReceived1 != null - ? 'text="${(relayReceived1['payload']?['text'] as String?)?.substring(0, 30)}..."' - : null, - ), - ); - - // ---- Test 11: wsRelay Text B → A ---- - print('\n--- Test 11: wsRelay Text B → A ---'); - final relayText2 = 'NEW-PROTOCOL from B! ${DateTime.now().millisecondsSinceEpoch}'; - deviceB.sendWsRelayText(deviceA.serverId!, relayText2); - - final relayReceived2 = await deviceA.waitForMessage( - 'wsRelay', - filter: (msg) => - msg['from'] == deviceB.serverId && - (msg['relayType'] == 'text' || msg['payload']?['relayType'] == 'text'), - timeout: const Duration(seconds: 5), - ); - results.add( - TestResult( - 'wsRelay Text B→A', - relayReceived2 != null, - relayReceived2 != null - ? 'text="${(relayReceived2['payload']?['text'] as String?)?.substring(0, 30)}..."' - : null, - ), - ); - - // ---- Test 12: Protocol Comparison ---- - print('\n--- Test 12: text-message vs wsRelay 协议对比 ---'); - final oldProtocolWorks = textReceived1 != null; - final newProtocolWorks = relayReceived1 != null; - results.add( - TestResult( - '协议对比: wsRelay优于text-message', - newProtocolWorks, - 'text-message=${oldProtocolWorks ? "✅" : "❌"} wsRelay=${newProtocolWorks ? "✅" : "❌"}', - ), - ); - if (!oldProtocolWorks && newProtocolWorks) { - print(' 💡 确认: text-message协议不可靠,wsRelay协议可靠,修复正确!'); - } - - // ---- Test 13: File Transfer via WsRelay A → B ---- - print('\n--- Test 13: File Transfer via WsRelay A → B ---'); - final fileId = 'test-file-${Random().nextInt(9999)}'; - final fileName = 'test_file.txt'; - final fileContent = 'This is a test file from device A. ' * 10; - final fileBytes = utf8.encode(fileContent); - final fileSize = fileBytes.length; - final chunkSize = 65536; - - deviceA.sendFileMeta(deviceB.serverId!, fileName, fileSize, fileId); - - final fileMetaReceived = await deviceB.waitForMessage( - 'wsRelay', - filter: (msg) => - msg['from'] == deviceA.serverId && - (msg['relayType'] == 'file-meta' || - msg['payload']?['relayType'] == 'file-meta'), - timeout: const Duration(seconds: 5), - ); - results.add( - TestResult( - 'File Meta Received by B', - fileMetaReceived != null, - fileMetaReceived != null - ? 'fileName=${fileMetaReceived['payload']?['fileName']} fileSize=${fileMetaReceived['payload']?['fileSize']}' - : null, - ), - ); - - if (fileMetaReceived != null) { - int offset = 0; - int chunkIndex = 0; - while (offset < fileBytes.length) { - final end = (offset + chunkSize < fileBytes.length) - ? offset + chunkSize - : fileBytes.length; - final chunk = fileBytes.sublist(offset, end); - final base64Chunk = base64Encode(chunk); - deviceA.sendFileChunk(deviceB.serverId!, fileId, chunkIndex, base64Chunk); - offset = end; - chunkIndex++; - } - - deviceA.sendFileComplete(deviceB.serverId!, fileId); - - final fileCompleteReceived = await deviceB.waitForMessage( - 'wsRelay', - filter: (msg) => - msg['from'] == deviceA.serverId && - (msg['relayType'] == 'file-complete' || - msg['payload']?['relayType'] == 'file-complete'), - timeout: const Duration(seconds: 5), - ); - results.add( - TestResult( - 'File Complete Received by B', - fileCompleteReceived != null, - 'chunks=$chunkIndex totalSize=$fileSize', - ), - ); - } - - // ---- Test 14: Delivery Ack ---- - print('\n--- Test 14: Delivery Ack ---'); - if (relayReceived1 != null) { - deviceB.sendDeliveryAck( - deviceA.serverId!, - relayReceived1['ts']?.toString() ?? '', - ); - final ackReceived = await deviceA.waitForMessage( - 'delivery-ack', - filter: (msg) => msg['from'] == deviceB.serverId, - timeout: const Duration(seconds: 5), - ); - results.add(TestResult('Delivery Ack Received by A', ackReceived != null)); - } else { - results.add(TestResult('Delivery Ack', false, 'Skipped: no wsRelay message')); - } - - // ---- Test 15: deviceOffline Event ---- - print('\n--- Test 15: deviceOffline Event ---'); - final deviceC = TestDevice( - localId: 'test-c-${Random().nextInt(9999)}', - alias: 'TestDeviceC', - ); - final connectedC = await deviceC.connect(); - if (connectedC) { - final regC = await deviceC.waitForRegistration(); - await Future.delayed(const Duration(seconds: 1)); - if (regC && deviceC.serverId != null) { - print(' Device C registered: ${deviceC.serverId}'); - await deviceC.disconnect(); - print(' Device C disconnected, waiting for deviceOffline...'); - final offlineEvent = await deviceA.waitForMessage( - 'deviceOffline', - filter: (msg) => msg['from'] == deviceC.serverId, - timeout: const Duration(seconds: 8), - ); - results.add( - TestResult( - 'deviceOffline Event', - offlineEvent != null, - offlineEvent != null - ? 'from=${offlineEvent['from']}' - : '未收到deviceOffline广播', - ), - ); - } else { - results.add(TestResult('deviceOffline Event', false, 'Device C registration failed')); - } - } else { - results.add(TestResult('deviceOffline Event', false, 'Device C connect failed')); - } - - // ---- Test 16: Pair Reject Flow ---- - print('\n--- Test 16: Pair Reject Flow ---'); - final deviceD = TestDevice( - localId: 'test-d-${Random().nextInt(9999)}', - alias: 'TestDeviceD', - ); - final connectedD = await deviceD.connect(); - if (connectedD) { - final regD = await deviceD.waitForRegistration(); - if (regD && deviceD.serverId != null) { - await Future.delayed(const Duration(milliseconds: 500)); - deviceD.sendPairRequest(deviceB.serverId!); - final pairReqD = await deviceB.waitForMessage( - 'pair-request', - filter: (msg) => msg['from'] == deviceD.serverId, - timeout: const Duration(seconds: 5), - ); - if (pairReqD != null) { - deviceB.sendPairReject(deviceD.serverId!); - final pairRejected = await deviceD.waitForMessage( - 'pair-reject', - filter: (msg) => msg['from'] == deviceB.serverId, - timeout: const Duration(seconds: 5), - ); - results.add(TestResult('Pair Reject Flow', pairRejected != null)); - } else { - results.add(TestResult('Pair Reject Flow', false, 'Pair request not received')); - } - } else { - results.add(TestResult('Pair Reject Flow', false, 'Device D registration failed')); - } - await deviceD.disconnect(); - } else { - results.add(TestResult('Pair Reject Flow', false, 'Device D connect failed')); - } - - // ---- Test 17: REST API Pair Request ---- - print('\n--- Test 17: REST API Pair Request ---'); - try { - final client = HttpClient(); - final request = await client.postUrl(Uri.parse('$kApiBase/pair_request')); - request.headers.set('Content-Type', 'application/json; charset=utf-8'); - final body = jsonEncode({ - 'fromId': deviceA.serverId, - 'toId': deviceB.serverId, - 'alias': deviceA.alias, - 'fingerprint': 'test-fp-a', - }); - request.write(utf8.encode(body)); - final response = await request.close().timeout(kTimeout); - final respBody = await response.transform(utf8.decoder).join(); - client.close(); - final json = jsonDecode(respBody) as Map; - results.add( - TestResult( - 'REST Pair Request', - json['code'] == 1, - 'code=${json['code']} msg=${json['msg']}', - ), - ); - } catch (e) { - results.add(TestResult('REST Pair Request', false, e.toString())); - } - - // ---- Test 18: REST Paired Devices ---- - print('\n--- Test 18: REST Paired Devices ---'); - try { - final client = HttpClient(); - final request = await client.getUrl( - Uri.parse('$kApiBase/paired_devices?deviceId=${deviceA.serverId}'), - ); - final response = await request.close().timeout(kTimeout); - final respBody = await response.transform(utf8.decoder).join(); - client.close(); - final json = jsonDecode(respBody) as Map; - results.add( - TestResult( - 'REST Paired Devices', - json['code'] == 1, - 'code=${json['code']} devices=${json['data']?['devices']?.length ?? 0}', - ), - ); - } catch (e) { - results.add(TestResult('REST Paired Devices', false, e.toString())); - } - - // ---- Cleanup ---- - print('\n--- Cleanup ---'); - await deviceA.disconnect(); - await deviceB.disconnect(); - - // ---- Summary ---- - _printResults(results); -} - -void _printResults(List results) { - print('\n============================================================'); - print(' 验证结果汇总'); - print('============================================================'); - int passed = 0; - int failed = 0; - for (final r in results) { - print(' $r'); - if (r.passed) { - passed++; - } else { - failed++; - } - } - print('------------------------------------------------------------'); - print(' 总计: ${results.length} 通过: $passed 失败: $failed'); - if (results.isNotEmpty) { - print(' 通过率: ${(passed / results.length * 100).toStringAsFixed(1)}%'); - } - print('============================================================'); - - if (failed > 0) { - print('\n❌ 失败项:'); - for (final r in results.where((r) => !r.passed)) { - print(' - ${r.name}: ${r.detail ?? "无详情"}'); - } - print('\n💡 建议检查:'); - print(' 1. 信令服务器是否正确转发消息'); - print(' 2. 设备ID是否使用服务器分配的ID'); - print(' 3. wsRelay协议是否被服务器正确处理'); - print(' 4. deviceOnline/deviceOffline广播是否正常'); - } -} diff --git a/scripts/signaling_test.dart b/scripts/signaling_test.dart deleted file mode 100644 index 1e3d7670..00000000 --- a/scripts/signaling_test.dart +++ /dev/null @@ -1,670 +0,0 @@ -// ============================================================ -// 闲言APP — 信令服务器接口测试脚本 -// 创建时间: 2026-05-14 -// 更新时间: 2026-05-14 -// 作用: 测试WebSocket信令服务器核心接口 -// - 连接/注册/设备发现/消息转发/文件元数据/配对/心跳 -// - 验证discoverMyDevices去重逻辑 -// 上次更新: 初始版本 -// 运行: dart run Scripts/signaling_test.dart -// ============================================================ - -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; - -import 'package:web_socket_channel/web_socket_channel.dart'; - -const String kSignalingUrl = 'wss://tools.wktyl.com:9443'; -const Duration kTimeout = Duration(seconds: 10); -const Duration kHeartbeatInterval = Duration(seconds: 30); - -int _passCount = 0; -int _failCount = 0; - -void _result(String name, bool pass, {String? detail}) { - final icon = pass ? '✅' : '❌'; - final status = pass ? 'PASS' : 'FAIL'; - _passCount += pass ? 1 : 0; - _failCount += pass ? 0 : 1; - print('$icon [$status] $name${detail != null ? ' — $detail' : ''}'); -} - -class TestDevice { - TestDevice({required this.localId, required this.alias, this.userId}); - - final String localId; - String? serverId; - final String alias; - final String? userId; - WebSocketChannel? _channel; - bool _isConnected = false; - bool get isConnected => _isConnected; - String get effectiveId => serverId ?? localId; - - final StreamController> _messageController = - StreamController>.broadcast(); - Stream> get onMessage => _messageController.stream; - - final List> allMessages = []; - Timer? _heartbeatTimer; - - Future connect() async { - try { - print('\n🔗 [$alias] Connecting to $kSignalingUrl...'); - _channel = WebSocketChannel.connect(Uri.parse(kSignalingUrl)); - await _channel!.ready.timeout(kTimeout); - - _channel!.stream.listen( - (data) { - _handleMessage(data as String); - }, - onDone: () { - _isConnected = false; - print('[$alias] Connection closed'); - }, - onError: (Object error) { - _isConnected = false; - print('[$alias] Connection error: $error'); - }, - ); - - _isConnected = true; - _sendRegister(); - _startHeartbeat(); - - print('[$alias] Connected, waiting for server ID...'); - return true; - } catch (e) { - print('[$alias] Connection failed: $e'); - return false; - } - } - - void _sendRegister() { - _send({ - 'type': 'register', - 'from': localId, - 'payload': { - 'alias': alias, - 'fingerprint': 'test-fp-${localId.substring(0, 8)}', - 'deviceType': 'headless', - 'deviceModel': 'SignalingTestDevice', - if (userId != null) 'userId': userId, - }, - 'ts': DateTime.now().millisecondsSinceEpoch, - }); - } - - void _startHeartbeat() { - _heartbeatTimer?.cancel(); - _heartbeatTimer = Timer.periodic(kHeartbeatInterval, (_) { - if (_isConnected) { - _send({ - 'type': 'heartbeat', - 'from': effectiveId, - 'ts': DateTime.now().millisecondsSinceEpoch, - }); - } - }); - } - - void _handleMessage(String data) { - try { - final json = jsonDecode(data) as Map; - allMessages.add(json); - _messageController.add(json); - - final type = json['type'] as String? ?? ''; - - if (type == 'registered') { - final assignedId = - json['id'] as String? ?? - json['payload']?['deviceId'] as String? ?? - json['payload']?['id'] as String?; - if (assignedId != null && serverId == null) { - serverId = assignedId; - print('[$alias] Server assigned ID: $serverId'); - } - } - - if (type == 'display-name') { - final sid = - json['payload']?['id'] as String? ?? - json['id'] as String? ?? - json['sender'] as String?; - if (sid != null && serverId == null) { - serverId = sid; - print('[$alias] Got ID from display-name: $serverId'); - } - } - } catch (_) {} - } - - void _send(Map msg) { - if (!_isConnected || _channel == null) return; - _channel!.sink.add(jsonEncode(msg)); - } - - void sendDiscover() { - _send({ - 'type': 'discover', - 'from': effectiveId, - 'ts': DateTime.now().millisecondsSinceEpoch, - }); - print('[$alias] Sent discover'); - } - - void sendDiscoverMyDevices(String uid) { - _send({ - 'type': 'discoverMyDevices', - 'from': effectiveId, - 'payload': {'userId': uid}, - 'ts': DateTime.now().millisecondsSinceEpoch, - }); - print('[$alias] Sent discoverMyDevices for userId=$uid'); - } - - void sendTextMessage(String targetId, String text) { - _send({ - 'type': 'text-message', - 'from': effectiveId, - 'to': targetId, - 'payload': { - 'text': text, - 'timestamp': DateTime.now().millisecondsSinceEpoch, - }, - 'ts': DateTime.now().millisecondsSinceEpoch, - }); - print('[$alias] Sent text-message to $targetId'); - } - - void sendFileMeta( - String targetId, { - required String fileName, - required int fileSize, - required String mimeType, - required String taskId, - }) { - _send({ - 'type': 'file-meta', - 'from': effectiveId, - 'to': targetId, - 'payload': { - 'fileName': fileName, - 'fileSize': fileSize, - 'mimeType': mimeType, - 'taskId': taskId, - }, - 'ts': DateTime.now().millisecondsSinceEpoch, - }); - print('[$alias] Sent file-meta to $targetId: $fileName'); - } - - void sendPairRequest(String targetId) { - _send({ - 'type': 'pair-request', - 'from': effectiveId, - 'to': targetId, - 'payload': {'pin': '123456'}, - 'ts': DateTime.now().millisecondsSinceEpoch, - }); - print('[$alias] Sent pair-request to $targetId'); - } - - void sendPairResponse(String targetId, bool accepted) { - _send({ - 'type': 'pair-response', - 'from': effectiveId, - 'to': targetId, - 'payload': {'accepted': accepted}, - 'ts': DateTime.now().millisecondsSinceEpoch, - }); - print('[$alias] Sent pair-response to $targetId: accepted=$accepted'); - } - - void sendHeartbeat() { - _send({ - 'type': 'heartbeat', - 'from': effectiveId, - 'ts': DateTime.now().millisecondsSinceEpoch, - }); - print('[$alias] Sent heartbeat'); - } - - Future?> waitForMessage( - String type, { - Duration timeout = const Duration(seconds: 8), - }) async { - try { - return await onMessage - .firstWhere((m) => m['type'] == type) - .timeout(timeout); - } catch (_) { - return null; - } - } - - Future disconnect() async { - _heartbeatTimer?.cancel(); - if (_isConnected && _channel != null) { - _send({ - 'type': 'leave', - 'from': effectiveId, - 'ts': DateTime.now().millisecondsSinceEpoch, - }); - await _channel?.sink.close(); - } - _isConnected = false; - await _messageController.close(); - } -} - -Future main() async { - print('╔══════════════════════════════════════════════════════════╗'); - print('║ 闲言APP — 信令服务器接口测试 ║'); - print('║ Signaling Server: $kSignalingUrl ║'); - print('╚══════════════════════════════════════════════════════════╝\n'); - - await testWebSocketConnection(); - await testDeviceRegister(); - await testDiscoverMyDevicesDedup(); - await testTextMessageForwarding(); - await testFileMetaForwarding(); - await testPairRequestResponse(); - await testHeartbeat(); - - print('\n╔══════════════════════════════════════════════════════════╗'); - print('║ 测试结果汇总 ║'); - print('╠══════════════════════════════════════════════════════════╣'); - print('║ ✅ 通过: $_passCount ║'); - print('║ ❌ 失败: $_failCount ║'); - print('║ 📊 总计: ${_passCount + _failCount} ║'); - print('╚══════════════════════════════════════════════════════════╝'); - - exit(_failCount > 0 ? 1 : 0); -} - -Future testWebSocketConnection() async { - print('\n━━━ 1. WebSocket 连接测试 ━━━'); - - final device = TestDevice( - localId: 'conn-test-${DateTime.now().millisecondsSinceEpoch}', - alias: 'ConnTest', - ); - - final connected = await device.connect(); - _result('WebSocket连接建立', connected); - - if (connected) { - final registered = await device.waitForMessage('registered'); - _result( - '收到registered响应', - registered != null, - detail: registered != null - ? 'serverId=${registered['id'] ?? registered['payload']?['deviceId']}' - : '未收到registered消息', - ); - } - - await device.disconnect(); - await Future.delayed(const Duration(seconds: 1)); -} - -Future testDeviceRegister() async { - print('\n━━━ 2. 设备注册测试 ━━━'); - - final device = TestDevice( - localId: 'reg-test-${DateTime.now().millisecondsSinceEpoch}', - alias: 'RegisterTest', - userId: 'test_user_register', - ); - - final connected = await device.connect(); - _result('注册设备连接', connected); - - if (connected) { - final registered = await device.waitForMessage('registered'); - _result( - '注册成功收到确认', - registered != null, - detail: registered != null - ? 'payload=${registered['payload']}' - : '超时未收到', - ); - - final hasServerId = device.serverId != null; - _result('服务器分配ID', hasServerId, detail: 'serverId=${device.serverId}'); - } - - await device.disconnect(); - await Future.delayed(const Duration(seconds: 1)); -} - -Future testDiscoverMyDevicesDedup() async { - print('\n━━━ 3. 设备发现测试(discoverMyDevices 去重验证) ━━━'); - - final testUserId = 'dedup_test_user_${DateTime.now().millisecondsSinceEpoch}'; - - final device1 = TestDevice( - localId: 'dedup-a-${DateTime.now().millisecondsSinceEpoch}', - alias: 'DedupDevice-A', - userId: testUserId, - ); - final device2 = TestDevice( - localId: 'dedup-b-${DateTime.now().millisecondsSinceEpoch}', - alias: 'DedupDevice-B', - userId: testUserId, - ); - - final c1 = await device1.connect(); - final c2 = await device2.connect(); - _result('两台设备均连接成功', c1 && c2); - - if (!c1 || !c2) { - await device1.disconnect(); - await device2.disconnect(); - return; - } - - await Future.delayed(const Duration(seconds: 2)); - - final discoverer = TestDevice( - localId: 'dedup-disc-${DateTime.now().millisecondsSinceEpoch}', - alias: 'Discoverer', - userId: testUserId, - ); - final c3 = await discoverer.connect(); - _result('发现者设备连接', c3); - - if (!c3) { - await device1.disconnect(); - await device2.disconnect(); - return; - } - - await Future.delayed(const Duration(seconds: 1)); - - discoverer.sendDiscoverMyDevices(testUserId); - - final response = await discoverer.waitForMessage('myDevicesResponse'); - _result( - '收到myDevicesResponse', - response != null, - detail: response != null ? 'raw=${jsonEncode(response)}' : '超时未收到', - ); - - if (response != null) { - final devicesList = - (response['devices'] as List?) ?? - (response['payload']?['devices'] as List?) ?? - []; - _result( - '发现设备数量≥1', - devicesList.isNotEmpty, - detail: '发现${devicesList.length}台设备', - ); - - final fingerprints = {}; - var duplicateCount = 0; - for (final d in devicesList) { - if (d is Map) { - final fp = - d['fingerprint'] as String? ?? d['id'] as String? ?? ''; - if (fp.isNotEmpty) { - if (fingerprints.contains(fp)) { - duplicateCount++; - print(' ⚠️ 发现重复设备: fingerprint=$fp'); - } - fingerprints.add(fp); - } - } - } - _result( - '去重验证: 无重复设备', - duplicateCount == 0, - detail: duplicateCount > 0 - ? '发现$duplicateCount个重复项' - : '所有设备fingerprint唯一', - ); - } - - await device1.disconnect(); - await device2.disconnect(); - await discoverer.disconnect(); - await Future.delayed(const Duration(seconds: 1)); -} - -Future testTextMessageForwarding() async { - print('\n━━━ 4. 文本消息转发测试 ━━━'); - - final sender = TestDevice( - localId: 'txt-snd-${DateTime.now().millisecondsSinceEpoch}', - alias: 'TextSender', - ); - final receiver = TestDevice( - localId: 'txt-rcv-${DateTime.now().millisecondsSinceEpoch}', - alias: 'TextReceiver', - ); - - final c1 = await sender.connect(); - final c2 = await receiver.connect(); - _result('发送方和接收方连接', c1 && c2); - - if (!c1 || !c2) { - await sender.disconnect(); - await receiver.disconnect(); - return; - } - - await Future.delayed(const Duration(seconds: 2)); - - final targetId = receiver.serverId ?? receiver.localId; - final testText = 'Hello from signaling_test at ${DateTime.now()}'; - - sender.sendTextMessage(targetId, testText); - - final received = await receiver.waitForMessage('text-message'); - _result( - '接收方收到text-message', - received != null, - detail: received != null - ? 'from=${received['from']}, text=${received['payload']?['text']}' - : '超时未收到', - ); - - if (received != null) { - final receivedText = received['payload']?['text'] as String? ?? ''; - _result( - '消息内容匹配', - receivedText == testText, - detail: receivedText == testText - ? '内容一致' - : '不匹配: 期望"$testText", 实际"$receivedText"', - ); - } - - await sender.disconnect(); - await receiver.disconnect(); - await Future.delayed(const Duration(seconds: 1)); -} - -Future testFileMetaForwarding() async { - print('\n━━━ 5. 文件元数据转发测试 ━━━'); - - final sender = TestDevice( - localId: 'file-snd-${DateTime.now().millisecondsSinceEpoch}', - alias: 'FileSender', - ); - final receiver = TestDevice( - localId: 'file-rcv-${DateTime.now().millisecondsSinceEpoch}', - alias: 'FileReceiver', - ); - - final c1 = await sender.connect(); - final c2 = await receiver.connect(); - _result('文件发送方和接收方连接', c1 && c2); - - if (!c1 || !c2) { - await sender.disconnect(); - await receiver.disconnect(); - return; - } - - await Future.delayed(const Duration(seconds: 2)); - - final targetId = receiver.serverId ?? receiver.localId; - const testFileName = 'test_document.pdf'; - const testFileSize = 1048576; - const testMimeType = 'application/pdf'; - const testTaskId = 'task-file-meta-test-001'; - - sender.sendFileMeta( - targetId, - fileName: testFileName, - fileSize: testFileSize, - mimeType: testMimeType, - taskId: testTaskId, - ); - - final received = await receiver.waitForMessage('file-meta'); - _result( - '接收方收到file-meta', - received != null, - detail: received != null - ? 'from=${received['from']}, fileName=${received['payload']?['fileName']}' - : '超时未收到', - ); - - if (received != null) { - final payload = received['payload'] as Map? ?? {}; - _result( - '文件名匹配', - payload['fileName'] == testFileName, - detail: '期望"$testFileName", 实际"${payload['fileName']}"', - ); - _result( - '文件大小匹配', - payload['fileSize'] == testFileSize, - detail: '期望$testFileSize, 实际${payload['fileSize']}', - ); - _result( - 'MIME类型匹配', - payload['mimeType'] == testMimeType, - detail: '期望"$testMimeType", 实际"${payload['mimeType']}"', - ); - _result( - '任务ID匹配', - payload['taskId'] == testTaskId, - detail: '期望"$testTaskId", 实际"${payload['taskId']}"', - ); - } - - await sender.disconnect(); - await receiver.disconnect(); - await Future.delayed(const Duration(seconds: 1)); -} - -Future testPairRequestResponse() async { - print('\n━━━ 6. 配对请求/接受测试 ━━━'); - - final deviceA = TestDevice( - localId: 'pair-a-${DateTime.now().millisecondsSinceEpoch}', - alias: 'PairDevice-A', - ); - final deviceB = TestDevice( - localId: 'pair-b-${DateTime.now().millisecondsSinceEpoch}', - alias: 'PairDevice-B', - ); - - final c1 = await deviceA.connect(); - final c2 = await deviceB.connect(); - _result('配对双方连接', c1 && c2); - - if (!c1 || !c2) { - await deviceA.disconnect(); - await deviceB.disconnect(); - return; - } - - await Future.delayed(const Duration(seconds: 2)); - - final targetB = deviceB.serverId ?? deviceB.localId; - deviceA.sendPairRequest(targetB); - - final pairReq = await deviceB.waitForMessage('pair-request'); - _result( - 'B收到配对请求', - pairReq != null, - detail: pairReq != null - ? 'from=${pairReq['from']}, pin=${pairReq['payload']?['pin']}' - : '超时未收到', - ); - - if (pairReq != null) { - final targetA = deviceA.serverId ?? deviceA.localId; - deviceB.sendPairResponse(targetA, true); - - final pairResp = await deviceA.waitForMessage('pair-response'); - _result( - 'A收到配对响应', - pairResp != null, - detail: pairResp != null - ? 'accepted=${pairResp['payload']?['accepted']}' - : '超时未收到', - ); - - if (pairResp != null) { - final accepted = pairResp['payload']?['accepted'] as bool? ?? false; - _result('配对已接受', accepted, detail: 'accepted=$accepted'); - } - } - - await deviceA.disconnect(); - await deviceB.disconnect(); - await Future.delayed(const Duration(seconds: 1)); -} - -Future testHeartbeat() async { - print('\n━━━ 7. 心跳测试 ━━━'); - - final device = TestDevice( - localId: 'hb-test-${DateTime.now().millisecondsSinceEpoch}', - alias: 'HeartbeatTest', - ); - - final connected = await device.connect(); - _result('心跳测试设备连接', connected); - - if (!connected) { - await device.disconnect(); - return; - } - - await Future.delayed(const Duration(seconds: 1)); - - device.sendHeartbeat(); - print(' 📤 已发送心跳包, 等待服务器响应...'); - - await Future.delayed(const Duration(seconds: 3)); - - final stillConnected = device.isConnected; - _result( - '心跳后连接仍保持', - stillConnected, - detail: stillConnected ? '连接正常' : '连接已断开', - ); - - final pingReceived = await device.waitForMessage('ping', - timeout: const Duration(seconds: 5)); - _result( - '收到服务器ping', - pingReceived != null, - detail: pingReceived != null - ? '服务器主动ping, 连接保活正常' - : '未收到ping(可能服务器不主动ping, 连接仍正常)', - ); - - await device.disconnect(); - await Future.delayed(const Duration(seconds: 1)); -} diff --git a/scripts/signin_api_test.dart b/scripts/signin_api_test.dart deleted file mode 100644 index da6b0785..00000000 --- a/scripts/signin_api_test.dart +++ /dev/null @@ -1,356 +0,0 @@ -// ============================================================ -// 闲言APP — 签到接口测试脚本 -// 创建时间: 2026-05-14 -// 更新时间: 2026-05-14 -// 作用: 测试签到系统HTTP接口 -// - 签到日历数据获取 -// - 签到状态验证 -// - 补签接口测试 -// - API基础URL: https://tools.wktyl.com -// 上次更新: 初始版本 -// 运行: dart run Scripts/signin_api_test.dart -// ============================================================ - -import 'dart:convert'; -import 'dart:io'; - -const String kApiBase = 'https://tools.wktyl.com'; -const String kSigninBase = '/api/user_center'; -const Duration kTimeout = Duration(seconds: 15); - -int _passCount = 0; -int _failCount = 0; - -void _result(String name, bool pass, {String? detail}) { - final icon = pass ? '✅' : '❌'; - final status = pass ? 'PASS' : 'FAIL'; - _passCount += pass ? 1 : 0; - _failCount += pass ? 0 : 1; - print('$icon [$status] $name${detail != null ? ' — $detail' : ''}'); -} - -Future?> _httpGet( - String path, { - Map? queryParameters, - String? token, -}) async { - try { - final uri = Uri.parse('$kApiBase$path').replace( - queryParameters: queryParameters?.map( - (k, v) => MapEntry(k, v.toString()), - ), - ); - final client = HttpClient(); - client.connectionTimeout = kTimeout; - final request = await client.getUrl(uri); - request.headers.set('Accept', 'application/json'); - request.headers.set('Content-Type', 'application/x-www-form-urlencoded'); - if (token != null && token.isNotEmpty) { - request.headers.set('Authorization', 'Bearer $token'); - } - final response = await request.close(); - final body = await response.transform(utf8.decoder).join(); - client.close(); - return jsonDecode(body) as Map; - } catch (e) { - print(' ⚠️ HTTP GET $path 失败: $e'); - return null; - } -} - -Future?> _httpPost( - String path, { - Map? data, - String? token, -}) async { - try { - final uri = Uri.parse('$kApiBase$path'); - final client = HttpClient(); - client.connectionTimeout = kTimeout; - final request = await client.postUrl(uri); - request.headers.set('Accept', 'application/json'); - request.headers.set('Content-Type', 'application/x-www-form-urlencoded'); - if (token != null && token.isNotEmpty) { - request.headers.set('Authorization', 'Bearer $token'); - } - if (data != null && data.isNotEmpty) { - final formData = data.entries - .map((e) => - '${Uri.encodeComponent(e.key)}=${Uri.encodeComponent(e.value.toString())}') - .join('&'); - request.write(formData); - } - final response = await request.close(); - final body = await response.transform(utf8.decoder).join(); - client.close(); - return jsonDecode(body) as Map; - } catch (e) { - print(' ⚠️ HTTP POST $path 失败: $e'); - return null; - } -} - -String? _testToken; - -Future main(List args) async { - print('╔══════════════════════════════════════════════════════════╗'); - print('║ 闲言APP — 签到接口测试 ║'); - print('║ API Base: $kApiBase ║'); - print('╚══════════════════════════════════════════════════════════╝\n'); - - if (args.isNotEmpty) { - _testToken = args[0]; - print('🔑 使用提供的Token: ${_testToken!.substring(0, 10)}...\n'); - } else { - print('⚠️ 未提供Token, 部分需要登录的接口可能返回401'); - print(' 用法: dart run Scripts/signin_api_test.dart \n'); - } - - await testSigninCalendar(); - await testSigninStatus(); - await testSigninAction(); - await testSigninMakeup(); - - print('\n╔══════════════════════════════════════════════════════════╗'); - print('║ 测试结果汇总 ║'); - print('╠══════════════════════════════════════════════════════════╣'); - print('║ ✅ 通过: $_passCount ║'); - print('║ ❌ 失败: $_failCount ║'); - print('║ 📊 总计: ${_passCount + _failCount} ║'); - print('╚══════════════════════════════════════════════════════════╝'); - - exit(_failCount > 0 ? 1 : 0); -} - -Future testSigninCalendar() async { - print('\n━━━ 1. 签到日历数据获取 ━━━'); - - final result = await _httpGet( - '$kSigninBase/signin_calendar', - token: _testToken, - ); - - _result( - '签到日历接口响应', - result != null, - detail: result != null ? 'code=${result['code']}' : '请求失败', - ); - - if (result != null) { - final code = result['code'] as int? ?? 0; - _result( - '接口返回成功(code=1)', - code == 1, - detail: 'code=$code, msg=${result['msg']}', - ); - - if (code == 1) { - final data = result['data'] as Map? ?? {}; - _result( - '日历数据非空', - data.isNotEmpty, - detail: 'keys=${data.keys.join(', ')}', - ); - - final calendar = data['calendar']; - _result( - '包含calendar字段', - calendar != null, - detail: calendar != null - ? '类型=${calendar.runtimeType}, 数据摘要=${_truncate(calendar.toString(), 120)}' - : '缺失calendar字段', - ); - - final continuous = data['current_continuous'] as int?; - _result( - '包含连续签到天数', - continuous != null, - detail: 'current_continuous=$continuous', - ); - - final totalSignins = data['total_signins'] as int?; - _result( - '包含累计签到天数', - totalSignins != null, - detail: 'total_signins=$totalSignins', - ); - } - } - - final now = DateTime.now(); - final monthStr = - '${now.year}-${now.month.toString().padLeft(2, '0')}'; - final resultWithMonth = await _httpGet( - '$kSigninBase/signin_calendar', - queryParameters: {'month': monthStr}, - token: _testToken, - ); - - _result( - '指定月份查询日历', - resultWithMonth != null, - detail: resultWithMonth != null - ? 'month=$monthStr, code=${resultWithMonth['code']}' - : '请求失败', - ); -} - -Future testSigninStatus() async { - print('\n━━━ 2. 签到状态验证 ━━━'); - - final calendarResult = await _httpGet( - '$kSigninBase/signin_calendar', - token: _testToken, - ); - - if (calendarResult == null) { - _result('签到状态验证(依赖日历接口)', false, detail: '日历接口请求失败'); - return; - } - - final code = calendarResult['code'] as int? ?? 0; - if (code != 1) { - _result( - '签到状态验证', - false, - detail: '日历接口返回code=$code, msg=${calendarResult['msg']}', - ); - return; - } - - final data = calendarResult['data'] as Map? ?? {}; - final calendar = data['calendar']; - final now = DateTime.now(); - final todayStr = - '${now.year}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')}'; - - bool todaySigned = false; - - if (calendar is Map) { - todaySigned = calendar[todayStr] == true || - calendar[todayStr] == 1; - } else if (calendar is List) { - for (final item in calendar) { - if (item is Map) { - final date = item['date']?.toString() ?? ''; - final signed = item['signed'] == true || item['signed'] == 1; - if (date == todayStr && signed) { - todaySigned = true; - break; - } - } else if (item?.toString() == todayStr) { - todaySigned = true; - break; - } - } - } - - _result( - '今日签到状态可解析', - true, - detail: 'today=$todayStr, todaySigned=$todaySigned', - ); - - final continuous = data['current_continuous'] as int? ?? 0; - _result( - '连续签到天数有效', - continuous >= 0, - detail: 'current_continuous=$continuous', - ); -} - -Future testSigninAction() async { - print('\n━━━ 3. 签到接口测试 ━━━'); - - if (_testToken == null) { - _result('签到接口(需登录)', false, detail: '未提供Token, 跳过'); - return; - } - - final result = await _httpPost( - '$kSigninBase/signin', - token: _testToken, - ); - - _result( - '签到接口响应', - result != null, - detail: result != null ? 'code=${result['code']}' : '请求失败', - ); - - if (result != null) { - final code = result['code'] as int? ?? 0; - final msg = result['msg'] as String? ?? ''; - - if (code == 1) { - final data = result['data'] as Map? ?? {}; - final continuous = data['continuous'] as int? ?? 0; - final coinReward = data['coin_reward'] as int? ?? 0; - final todaySigned = data['today_signed'] as bool? ?? true; - - _result('签到成功', true, detail: '连续$continuous天, +$coinReward积分'); - _result( - '返回today_signed', - data.containsKey('today_signed'), - detail: 'today_signed=$todaySigned', - ); - _result( - '返回coin_reward', - data.containsKey('coin_reward'), - detail: 'coin_reward=$coinReward', - ); - } else if (msg.contains('已签到')) { - _result('今日已签到', true, detail: 'msg=$msg'); - } else { - _result('签到接口返回', false, detail: 'code=$code, msg=$msg'); - } - } -} - -Future testSigninMakeup() async { - print('\n━━━ 4. 补签接口测试 ━━━'); - - if (_testToken == null) { - _result('补签接口(需登录)', false, detail: '未提供Token, 跳过'); - return; - } - - final now = DateTime.now(); - final yesterday = now.subtract(const Duration(days: 1)); - final yesterdayStr = - '${yesterday.year}-${yesterday.month.toString().padLeft(2, '0')}-${yesterday.day.toString().padLeft(2, '0')}'; - - print(' ℹ️ 尝试补签日期: $yesterdayStr (注意: 如已签到或积分不足会失败)'); - - final result = await _httpPost( - '$kSigninBase/signin_makeup', - data: {'date': yesterdayStr}, - token: _testToken, - ); - - _result( - '补签接口响应', - result != null, - detail: result != null ? 'code=${result['code']}' : '请求失败', - ); - - if (result != null) { - final code = result['code'] as int? ?? 0; - final msg = result['msg'] as String? ?? ''; - - if (code == 1) { - _result('补签成功', true, detail: 'date=$yesterdayStr'); - } else { - _result( - '补签接口返回', - false, - detail: 'code=$code, msg=$msg (可能已签到/积分不足/每日限1次)', - ); - } - } -} - -String _truncate(String s, int maxLen) { - if (s.length <= maxLen) return s; - return '${s.substring(0, maxLen)}...'; -}