build: 修复iOS/macOS构建配置,适配macOS Keychain问题
1. 添加Pods依赖配置到Xcode工作区 2. 调整macOS权限配置,临时替换Keychain为shared_preferences 3. 重构secure_storage适配macOS兼容性问题 4. 整理iOS权限配置,移除重复声明 5. 更新插件依赖和Podfile配置
This commit is contained in:
@@ -20,7 +20,5 @@
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>13.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
|
||||
#include "Generated.xcconfig"
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
|
||||
#include "Generated.xcconfig"
|
||||
|
||||
339
ios/Podfile.lock
Normal file
339
ios/Podfile.lock
Normal file
@@ -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
|
||||
@@ -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 = "<group>"; };
|
||||
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
||||
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
@@ -55,13 +62,25 @@
|
||||
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
/* 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 = "<group>";
|
||||
};
|
||||
681D3589D0B414F14382F95F /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
8516FA250E04F5DCA0BF879B /* Pods_Runner.framework */,
|
||||
6910A24D128D2AE7EC75E064 /* Pods_RunnerTests.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9740EEB11CF90186004384FC /* Flutter */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -94,6 +122,8 @@
|
||||
97C146F01CF9000F007C117D /* Runner */,
|
||||
97C146EF1CF9000F007C117D /* Products */,
|
||||
331C8082294A63A400263BE5 /* RunnerTests */,
|
||||
F309E78E8D171786452C7A57 /* Pods */,
|
||||
681D3589D0B414F14382F95F /* Frameworks */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
@@ -121,6 +151,19 @@
|
||||
path = Runner;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
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 = "<group>";
|
||||
};
|
||||
/* 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;
|
||||
|
||||
3
ios/Runner.xcworkspace/contents.xcworkspacedata
generated
3
ios/Runner.xcworkspace/contents.xcworkspacedata
generated
@@ -4,4 +4,7 @@
|
||||
<FileRef
|
||||
location = "group:Runner.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Pods/Pods.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded</key>
|
||||
<false/>
|
||||
<key>PreviewsEnabled</key>
|
||||
<false/>
|
||||
</dict>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
@@ -20,10 +22,67 @@
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Viewer</string>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>apps.xy.xianyan</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>xianyan</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NFCReaderUsageDescription</key>
|
||||
<string>闲言需要使用NFC以触碰配对设备进行文件传输</string>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>NSBluetoothAlwaysUsageDescription</key>
|
||||
<string>闲言需要使用蓝牙以发现和配对附近设备进行文件传输</string>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>闲言需要使用相机以拍照制作壁纸、扫描二维码和文件传输扫码配对</string>
|
||||
<key>NSDocumentsFolderUsageDescription</key>
|
||||
<string>闲言需要访问文件以选择图片和导出内容</string>
|
||||
<key>NSLocalNetworkUsageDescription</key>
|
||||
<string>闲言需要本地网络权限以发现和连接局域网设备</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>闲言需要使用麦克风录制语音消息</string>
|
||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||
<string>闲言需要保存编辑好的卡片到您的相册</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>闲言需要访问您的相册以选择图片作为卡片背景</string>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
<false/>
|
||||
<key>UISceneConfigurations</key>
|
||||
<dict>
|
||||
<key>UIWindowSceneSessionRoleApplication</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UISceneClassName</key>
|
||||
<string>UIWindowScene</string>
|
||||
<key>UISceneConfigurationName</key>
|
||||
<string>flutter</string>
|
||||
<key>UISceneDelegateClassName</key>
|
||||
<string>FlutterSceneDelegate</string>
|
||||
<key>UISceneStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
@@ -41,74 +100,6 @@
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- 权限描述 — 相机 / 扫码 / 拍照 -->
|
||||
<!-- ============================================================ -->
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>闲言需要使用相机以拍照制作壁纸、扫描二维码和文件传输扫码配对</string>
|
||||
|
||||
<!-- 权限描述 — file_picker / 编辑器选择图片 -->
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>闲言需要访问您的相册以选择图片作为卡片背景</string>
|
||||
|
||||
<!-- 权限描述 — 编辑器导出保存到相册 -->
|
||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||
<string>闲言需要保存编辑好的卡片到您的相册</string>
|
||||
|
||||
<!-- 权限描述 — file_picker 选择文件 -->
|
||||
<key>NSDocumentsFolderUsageDescription</key>
|
||||
<string>闲言需要访问文件以选择图片和导出内容</string>
|
||||
|
||||
<!-- 权限描述 — 麦克风录音 — 语音消息 -->
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>闲言需要使用麦克风录制语音消息</string>
|
||||
|
||||
<!-- 权限描述 — 蓝牙BLE — 文件传输配对 -->
|
||||
<key>NSBluetoothAlwaysUsageDescription</key>
|
||||
<string>闲言需要使用蓝牙以发现和配对附近设备进行文件传输</string>
|
||||
|
||||
<!-- 权限描述 — NFC — 文件传输触碰配对 -->
|
||||
<key>NFCReaderUsageDescription</key>
|
||||
<string>闲言需要使用NFC以触碰配对设备进行文件传输</string>
|
||||
|
||||
<!-- 权限描述 — 本地网络 — 设备发现 -->
|
||||
<key>NSLocalNetworkUsageDescription</key>
|
||||
<string>闲言需要本地网络权限以发现和连接局域网设备</string>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- 网络安全 — 允许 HTTP 连接 (开发阶段) -->
|
||||
<!-- ============================================================ -->
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- URL Scheme — 让其他App通过 xianyan:// 打开闲言 -->
|
||||
<!-- ============================================================ -->
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Viewer</string>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>apps.xy.xianyan</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>xianyan</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- App Group — ShareExtension和主App共享数据 -->
|
||||
<!-- ============================================================ -->
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.apps.xy.xianyan.share</string>
|
||||
|
||||
@@ -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<void> _ensurePrefs() async {
|
||||
_prefs ??= await SharedPreferences.getInstance();
|
||||
}
|
||||
|
||||
static bool get _isMacOS => defaultTargetPlatform == TargetPlatform.macOS;
|
||||
|
||||
// ============================================================
|
||||
// 通用读写
|
||||
// ============================================================
|
||||
|
||||
/// 读取
|
||||
static Future<String?> read(String key) => _storage.read(key: key);
|
||||
static Future<String?> read(String key) async {
|
||||
if (_isMacOS) {
|
||||
await _ensurePrefs();
|
||||
return _prefs!.getString(key);
|
||||
} else {
|
||||
return _storage.read(key: key);
|
||||
}
|
||||
}
|
||||
|
||||
/// 写入
|
||||
static Future<void> write(String key, String value) =>
|
||||
_storage.write(key: key, value: value);
|
||||
static Future<void> 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<void> delete(String key) => _storage.delete(key: key);
|
||||
static Future<void> delete(String key) async {
|
||||
if (_isMacOS) {
|
||||
await _ensurePrefs();
|
||||
await _prefs!.remove(key);
|
||||
} else {
|
||||
await _storage.delete(key: key);
|
||||
}
|
||||
}
|
||||
|
||||
/// 是否包含
|
||||
static Future<bool> containsKey(String key) => _storage.containsKey(key: key);
|
||||
static Future<bool> containsKey(String key) async {
|
||||
if (_isMacOS) {
|
||||
await _ensurePrefs();
|
||||
return _prefs!.containsKey(key);
|
||||
} else {
|
||||
return _storage.containsKey(key: key);
|
||||
}
|
||||
}
|
||||
|
||||
/// 清空所有
|
||||
static Future<void> deleteAll() => _storage.deleteAll();
|
||||
static Future<void> 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();
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 便捷方法
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
|
||||
#include "ephemeral/Flutter-Generated.xcconfig"
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
|
||||
#include "ephemeral/Flutter-Generated.xcconfig"
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = "<group>"; };
|
||||
333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
|
||||
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; };
|
||||
33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
|
||||
@@ -76,8 +78,16 @@
|
||||
33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = "<group>"; };
|
||||
33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = "<group>"; };
|
||||
33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
/* 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 = "<group>";
|
||||
};
|
||||
@@ -172,9 +185,25 @@
|
||||
path = Runner;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
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 = "<group>";
|
||||
};
|
||||
D73912EC22F37F3D000D13A0 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9097D4D2EB98F14D8827394A /* Pods_Runner.framework */,
|
||||
5EAD42DE88C17A1D7496A378 /* Pods_RunnerTests.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
@@ -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)",
|
||||
|
||||
@@ -4,4 +4,7 @@
|
||||
<FileRef
|
||||
location = "group:Runner.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Pods/Pods.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<false/>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
|
||||
@@ -14,5 +14,11 @@
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-write</key>
|
||||
<true/>
|
||||
<key>com.apple.security.keychain</key>
|
||||
<true/>
|
||||
<key>keychain-access-groups</key>
|
||||
<array>
|
||||
<string>$(AppIdentifierPrefix)$(CFBundleIdentifier)</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -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<Map<String, dynamic>?> _httpGet(
|
||||
String path, {
|
||||
Map<String, dynamic>? 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<String, dynamic>;
|
||||
} catch (e) {
|
||||
print(' ⚠️ HTTP GET $path 失败: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>?> _httpPost(
|
||||
String path, {
|
||||
Map<String, dynamic>? 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<String, dynamic>;
|
||||
} catch (e) {
|
||||
print(' ⚠️ HTTP POST $path 失败: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
String? _testToken;
|
||||
|
||||
Future<void> main(List<String> 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 <token>\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<void> 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<String, dynamic>? ?? {};
|
||||
final list = data['list'] as List<dynamic>? ?? [];
|
||||
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<String, dynamic>;
|
||||
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<void> 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<String, dynamic>? ?? {};
|
||||
final list = data['list'] as List<dynamic>? ?? [];
|
||||
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<String, dynamic>;
|
||||
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<void> 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<String, dynamic>? ?? {};
|
||||
final list = data['list'] as List<dynamic>? ?? [];
|
||||
if (list.isNotEmpty) {
|
||||
final firstItem = list.first as Map<String, dynamic>;
|
||||
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<String, dynamic>? ?? {};
|
||||
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<String, dynamic>? ?? {};
|
||||
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<void> 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<String, dynamic>? ?? {};
|
||||
final list = data['list'] as List<dynamic>? ?? [];
|
||||
if (list.isNotEmpty) {
|
||||
final item = list.last as Map<String, dynamic>;
|
||||
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<void>.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<void> 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<int> feedFavIds = [];
|
||||
|
||||
if (feedFavResult != null && feedFavResult['code'] == 1) {
|
||||
final data = feedFavResult['data'] as Map<String, dynamic>? ?? {};
|
||||
final list = data['list'] as List<dynamic>? ?? [];
|
||||
feedFavCount = data['total'] as int? ?? list.length;
|
||||
feedFavIds = list
|
||||
.map<int>((e) => (e as Map<String, dynamic>)['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<int> ucFavIds = [];
|
||||
|
||||
if (ucFavResult != null && ucFavResult['code'] == 1) {
|
||||
final data = ucFavResult['data'] as Map<String, dynamic>? ?? {};
|
||||
final list = data['list'] as List<dynamic>? ?? [];
|
||||
ucFavCount = data['total'] as int? ?? list.length;
|
||||
ucFavIds = list
|
||||
.map<int>(
|
||||
(e) => (e as Map<String, dynamic>)['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<String, dynamic>? ?? {};
|
||||
final counts = data['counts'] as Map<String, dynamic>? ?? {};
|
||||
_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<String, dynamic>? ?? {};
|
||||
final groups = data['groups'] as List<dynamic>? ?? [];
|
||||
_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收藏列表为空, 无法比较交集',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<Map<String, dynamic>> _messageController =
|
||||
StreamController<Map<String, dynamic>>.broadcast();
|
||||
Stream<Map<String, dynamic>> get onMessage => _messageController.stream;
|
||||
|
||||
final List<Map<String, dynamic>> allMessages = [];
|
||||
Timer? _heartbeatTimer;
|
||||
|
||||
Future<bool> 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<String, dynamic>;
|
||||
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<String, dynamic> 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<void> 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<Map<String, dynamic>?> waitForMessage(
|
||||
String type, {
|
||||
Duration timeout = kTimeout,
|
||||
bool Function(Map<String, dynamic>)? 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<bool> 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<void> main() async {
|
||||
print('============================================================');
|
||||
print(' 闲言APP 文件传输全流程验证脚本 v3');
|
||||
print(' 时间: ${DateTime.now()}');
|
||||
print(' 新增: deviceOnline/deviceOffline + text-message vs wsRelay对比');
|
||||
print('============================================================\n');
|
||||
|
||||
final results = <TestResult>[];
|
||||
|
||||
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<String, dynamic>;
|
||||
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<String, dynamic>;
|
||||
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<String, dynamic>;
|
||||
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<TestResult> 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广播是否正常');
|
||||
}
|
||||
}
|
||||
@@ -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<Map<String, dynamic>> _messageController =
|
||||
StreamController<Map<String, dynamic>>.broadcast();
|
||||
Stream<Map<String, dynamic>> get onMessage => _messageController.stream;
|
||||
|
||||
final List<Map<String, dynamic>> allMessages = [];
|
||||
Timer? _heartbeatTimer;
|
||||
|
||||
Future<bool> 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<String, dynamic>;
|
||||
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<String, dynamic> 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<Map<String, dynamic>?> 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<void> 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<void> 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<void> 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<void>.delayed(const Duration(seconds: 1));
|
||||
}
|
||||
|
||||
Future<void> 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<void>.delayed(const Duration(seconds: 1));
|
||||
}
|
||||
|
||||
Future<void> 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<void>.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<void>.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<dynamic>?) ??
|
||||
(response['payload']?['devices'] as List<dynamic>?) ??
|
||||
[];
|
||||
_result(
|
||||
'发现设备数量≥1',
|
||||
devicesList.isNotEmpty,
|
||||
detail: '发现${devicesList.length}台设备',
|
||||
);
|
||||
|
||||
final fingerprints = <String>{};
|
||||
var duplicateCount = 0;
|
||||
for (final d in devicesList) {
|
||||
if (d is Map<String, dynamic>) {
|
||||
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<void>.delayed(const Duration(seconds: 1));
|
||||
}
|
||||
|
||||
Future<void> 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<void>.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<void>.delayed(const Duration(seconds: 1));
|
||||
}
|
||||
|
||||
Future<void> 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<void>.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<String, dynamic>? ?? {};
|
||||
_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<void>.delayed(const Duration(seconds: 1));
|
||||
}
|
||||
|
||||
Future<void> 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<void>.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<void>.delayed(const Duration(seconds: 1));
|
||||
}
|
||||
|
||||
Future<void> 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<void>.delayed(const Duration(seconds: 1));
|
||||
|
||||
device.sendHeartbeat();
|
||||
print(' 📤 已发送心跳包, 等待服务器响应...');
|
||||
|
||||
await Future<void>.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<void>.delayed(const Duration(seconds: 1));
|
||||
}
|
||||
@@ -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<Map<String, dynamic>?> _httpGet(
|
||||
String path, {
|
||||
Map<String, dynamic>? 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<String, dynamic>;
|
||||
} catch (e) {
|
||||
print(' ⚠️ HTTP GET $path 失败: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>?> _httpPost(
|
||||
String path, {
|
||||
Map<String, dynamic>? 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<String, dynamic>;
|
||||
} catch (e) {
|
||||
print(' ⚠️ HTTP POST $path 失败: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
String? _testToken;
|
||||
|
||||
Future<void> main(List<String> 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 <token>\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<void> 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<String, dynamic>? ?? {};
|
||||
_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<void> 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<String, dynamic>? ?? {};
|
||||
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<String, dynamic>) {
|
||||
todaySigned = calendar[todayStr] == true ||
|
||||
calendar[todayStr] == 1;
|
||||
} else if (calendar is List) {
|
||||
for (final item in calendar) {
|
||||
if (item is Map<String, dynamic>) {
|
||||
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<void> 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<String, dynamic>? ?? {};
|
||||
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<void> 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)}...';
|
||||
}
|
||||
Reference in New Issue
Block a user