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:
Developer
2026-05-22 05:00:41 +08:00
parent b9aa871678
commit 1a42e347cf
22 changed files with 741 additions and 2555 deletions

View File

@@ -20,7 +20,5 @@
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>13.0</string>
</dict>
</plist>

View File

@@ -1 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"

View File

@@ -1 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig"

339
ios/Podfile.lock Normal file
View 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

View File

@@ -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;

View File

@@ -4,4 +4,7 @@
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>

View File

@@ -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>

View File

@@ -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)
}
}

View File

@@ -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>

View File

@@ -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();
}
}
// ============================================================
// 便捷方法

View File

@@ -1 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "ephemeral/Flutter-Generated.xcconfig"

View File

@@ -1 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "ephemeral/Flutter-Generated.xcconfig"

View File

@@ -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"))

View File

@@ -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

View File

@@ -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)",

View File

@@ -4,4 +4,7 @@
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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收藏列表为空, 无法比较交集',
);
}
}

View File

@@ -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广播是否正常');
}
}

View File

@@ -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));
}

View File

@@ -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)}...';
}