feat: 多端邀请接入

This commit is contained in:
speakeloudest 2026-01-19 07:21:28 -08:00
parent b441346630
commit 8e48a6ff8a
19 changed files with 476 additions and 71 deletions

View File

@ -62,7 +62,8 @@ android {
versionName flutterVersionName
multiDexEnabled true
manifestPlaceholders = [
'android.permission.ACCESS_NETWORK_STATE': true
'android.permission.ACCESS_NETWORK_STATE': true,
'OPENINSTALL_APPKEY' : "alf57p",
]
android.defaultConfig.manifestPlaceholders += [
'android:screenOrientation': "portrait"

View File

@ -23,10 +23,6 @@
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<!-- Crisp 聊天所需权限 -->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES"
@ -108,11 +104,27 @@
<data android:host="import" />
</intent-filter>
<!-- OpenInstall Deep Link -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="alf57p" />
</intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" android:host="alf57p.oplinking.com" />
<data android:scheme="https" android:host="alf57p.oplinking.com" />
</intent-filter>
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
</intent-filter>
</activity>
<activity
android:name=".ShortcutActivity"
android:excludeFromRecents="true"

View File

@ -2,12 +2,6 @@ PODS:
- connectivity_plus (0.0.1):
- Flutter
- ReachabilitySwift
- Crisp (2.8.2):
- Crisp/Crisp (= 2.8.2)
- Crisp/Crisp (2.8.2)
- crisp_chat (2.4.1):
- Crisp (~> 2.8.2)
- Flutter
- device_info_plus (0.0.1):
- Flutter
- EasyPermissionX/Camera (0.0.2)
@ -30,6 +24,10 @@ PODS:
- in_app_purchase_storekit (0.0.1):
- Flutter
- FlutterMacOS
- libOpenInstallSDK (2.9.2)
- openinstall_flutter_plugin (0.0.1):
- Flutter
- libOpenInstallSDK
- OrderedSet (6.0.3)
- package_info_plus (0.4.5):
- Flutter
@ -47,7 +45,6 @@ PODS:
DEPENDENCIES:
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- crisp_chat (from `.symlinks/plugins/crisp_chat/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- EasyPermissionX/Camera
- Flutter (from `Flutter`)
@ -56,6 +53,7 @@ DEPENDENCIES:
- flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
- gal (from `.symlinks/plugins/gal/darwin`)
- in_app_purchase_storekit (from `.symlinks/plugins/in_app_purchase_storekit/darwin`)
- openinstall_flutter_plugin (from `.symlinks/plugins/openinstall_flutter_plugin/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
@ -64,8 +62,8 @@ DEPENDENCIES:
SPEC REPOS:
https://mirrors.tuna.tsinghua.edu.cn/git/CocoaPods/Specs.git:
- Crisp
- EasyPermissionX
- libOpenInstallSDK
- OrderedSet
- ReachabilitySwift
- SAMKeychain
@ -73,8 +71,6 @@ SPEC REPOS:
EXTERNAL SOURCES:
connectivity_plus:
:path: ".symlinks/plugins/connectivity_plus/ios"
crisp_chat:
:path: ".symlinks/plugins/crisp_chat/ios"
device_info_plus:
:path: ".symlinks/plugins/device_info_plus/ios"
Flutter:
@ -89,6 +85,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/gal/darwin"
in_app_purchase_storekit:
:path: ".symlinks/plugins/in_app_purchase_storekit/darwin"
openinstall_flutter_plugin:
:path: ".symlinks/plugins/openinstall_flutter_plugin/ios"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation:
@ -102,8 +100,6 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d
Crisp: 6747c96b2b2c2a81babf1eaecd1688a65d98edd4
crisp_chat: 30994104495de23443af8d5b2c041a6df1e8464d
device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342
EasyPermissionX: ff4c438f6ee80488f873b4cb921e32d982523067
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
@ -112,6 +108,8 @@ SPEC CHECKSUMS:
flutter_udid: b2417673f287ee62817a1de3d1643f47b9f508ab
gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5
in_app_purchase_storekit: a1ce04056e23eecc666b086040239da7619cd783
libOpenInstallSDK: 1e123fde796902007e6a25797cdf34c20552fc6e
openinstall_flutter_plugin: e6b8486f834eb60b336546442a8b747d4b664cf4
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46

View File

@ -762,11 +762,9 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = PacketTunnel/HiddifyPacketTunnel.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = NJRRF427XB;
DEVELOPMENT_TEAM = NJRRF427XB;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
EXCLUDED_ARCHS = armv7;
GCC_C_LANGUAGE_STANDARD = gnu17;
@ -792,7 +790,6 @@
PRODUCT_BUNDLE_IDENTIFIER = "$(BASE_BUNDLE_IDENTIFIER).PacketTunnel";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "HiFastVPN-iOS-Pord-PacketTunnel";
SKIP_INSTALL = YES;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
@ -818,11 +815,9 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = PacketTunnel/PacketTunnelRelease.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = NJRRF427XB;
DEVELOPMENT_TEAM = NJRRF427XB;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
EXCLUDED_ARCHS = armv7;
GCC_C_LANGUAGE_STANDARD = gnu17;
@ -848,7 +843,6 @@
PRODUCT_BUNDLE_IDENTIFIER = "$(BASE_BUNDLE_IDENTIFIER).PacketTunnel";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "HiFastVPN-iOS-Pord-PacketTunnel";
SKIP_INSTALL = YES;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
@ -872,11 +866,9 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = PacketTunnel/HiddifyPacketTunnel.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = NJRRF427XB;
DEVELOPMENT_TEAM = NJRRF427XB;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
EXCLUDED_ARCHS = armv7;
GCC_C_LANGUAGE_STANDARD = gnu17;
@ -902,7 +894,6 @@
PRODUCT_BUNDLE_IDENTIFIER = "$(BASE_BUNDLE_IDENTIFIER).PacketTunnel";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "HiFastVPN-iOS-Pord-PacketTunnel";
SKIP_INSTALL = YES;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
@ -980,11 +971,9 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = NJRRF427XB;
DEVELOPMENT_TEAM = NJRRF427XB;
ENABLE_BITCODE = NO;
"EXCLUDED_ARCHS[sdk=iphoneos*]" = armv7;
INFOPLIST_FILE = Runner/Info.plist;
@ -1016,7 +1005,6 @@
"PRODUCT_BUNDLE_IDENTIFIER[sdk=iphoneos*]" = com.taw.hifastvpn;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "HiFastVPN-iOS-Pord";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
@ -1212,11 +1200,9 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = NJRRF427XB;
DEVELOPMENT_TEAM = NJRRF427XB;
ENABLE_BITCODE = NO;
"EXCLUDED_ARCHS[sdk=iphoneos*]" = armv7;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "i386 arm64";
@ -1248,7 +1234,6 @@
"PRODUCT_BUNDLE_IDENTIFIER[sdk=iphoneos*]" = com.taw.hifastvpn;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "HiFastVPN-iOS-Pord";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
@ -1270,11 +1255,9 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/RunnerRelease.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = NJRRF427XB;
DEVELOPMENT_TEAM = NJRRF427XB;
ENABLE_BITCODE = NO;
"EXCLUDED_ARCHS[sdk=iphoneos*]" = armv7;
INFOPLIST_FILE = Runner/Info.plist;
@ -1306,7 +1289,6 @@
"PRODUCT_BUNDLE_IDENTIFIER[sdk=iphoneos*]" = com.taw.hifastvpn;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "HiFastVPN-iOS-Pord";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;

View File

@ -15,6 +15,14 @@ import Libcore
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
override func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
return super.application(app, open: url, options: options)
}
override func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
return super.application(application, continue: userActivity, restorationHandler: restorationHandler)
}
func setupFileManager() {
try? FileManager.default.createDirectory(at: FilePath.workingDirectory, withIntermediateDirectories: true)
FileManager.default.changeCurrentDirectoryPath(FilePath.sharedDirectory.path)

View File

@ -88,5 +88,7 @@
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>com.openinstall.APP_KEY</key>
<string>alf57p</string>
</dict>
</plist>

View File

@ -4,6 +4,11 @@
<dict>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:alf57p.openinstall.com</string>
<string>applinks:alf57p.oplinking.com</string>
</array>
<key>com.apple.developer.networking.networkextension</key>
<array>
<string>packet-tunnel-provider</string>

View File

@ -4,6 +4,11 @@
<dict>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:alf57p.openinstall.com</string>
<string>applinks:alf57p.oplinking.com</string>
</array>
<key>com.apple.developer.networking.networkextension</key>
<array>
<string>packet-tunnel-provider</string>

12
ios/exportOptions_dev.plist Executable file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-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>compileBitcode</key>
<false/>
<key>method</key>
<string>development</string> <key>signingStyle</key>
<string>automatic</string> <key>thinning</key>
<string>&lt;none&gt;</string>
</dict>
</plist>

View File

@ -25,11 +25,15 @@ import 'package:kaer_with_panels/app/widgets/dialogs/hi_dialog.dart';
import 'package:kaer_with_panels/app/services/api_service/kr_auth_api.dart';
import 'package:kaer_with_panels/app/services/kr_subscribe_service.dart';
import 'package:kaer_with_panels/app/utils/kr_common_util.dart';
import 'package:kaer_with_panels/app/utils/kr_device_util.dart';
import 'package:openinstall_flutter_plugin/openinstall_flutter_plugin.dart';
import 'dart:io' show Platform;
class KRAppRunData {
static final KRAppRunData _instance = KRAppRunData._internal();
static const String _keyUserInfo = 'USER_INFO';
static const bool inviteDebugMode = true;
/// token
String? kr_token;
@ -61,6 +65,9 @@ class KRAppRunData {
// obs
final kr_isLogin = false.obs;
//
String? _kr_pendingInviteCode;
KRAppRunData._internal();
factory KRAppRunData() => _instance;
@ -447,6 +454,16 @@ class KRAppRunData {
kr_isLogin.value = true;
print('✅ 已标记为登录状态');
//
if (inviteDebugMode) {
// Debug
await _kr_handleSilentInvitation();
} else {
//
_kr_handleSilentInvitation();
}
_logStepTiming('设备登录完成');
return true;
},
@ -496,6 +513,9 @@ class KRAppRunData {
KRLogUtil.kr_i('✅ Token和账号验证通过设置登录状态为true', tag: 'AppRunData');
KRLogUtil.kr_i('📊 恢复账号: ${kr_account.value}', tag: 'AppRunData');
kr_isLogin.value = true;
// 🔧
_kr_handleSilentInvitation();
} else {
//
KRLogUtil.kr_w('⚠️ 账号信息为空,清理该条用户数据', tag: 'AppRunData');
@ -633,4 +653,125 @@ class KRAppRunData {
KRLogUtil.kr_e('📚 [AppRunData] 错误堆栈: $stackTrace', tag: 'AppRunData');
}
}
///
Future<void> _kr_handleSilentInvitation() async {
KRLogUtil.kr_i('🚀 开始处理静默邀请...', tag: 'AppRunData');
String? inviteCode;
// 1. /
if (_kr_pendingInviteCode != null && _kr_pendingInviteCode!.isNotEmpty) {
inviteCode = _kr_pendingInviteCode;
KRLogUtil.kr_i('📎 使用暂存的待绑定邀请码: $inviteCode', tag: 'AppRunData');
}
// 2.
if (inviteCode == null || inviteCode!.isEmpty) {
try {
if (Platform.isMacOS || Platform.isWindows) {
inviteCode = await KRDeviceUtil().kr_getDesktopInviteCode();
} else if (Platform.isAndroid || Platform.isIOS) {
final Completer<String?> completer = Completer<String?>();
OpeninstallFlutterPlugin().install((data) async {
final code = kr_parseInviteCodeFromData(data);
KRLogUtil.kr_i('收到 OpenInstall 安装数据: $data, 解析出邀请码: $code', tag: 'AppRunData');
if (!completer.isCompleted) completer.complete(code);
});
inviteCode = await completer.future
.timeout(const Duration(seconds: 8), onTimeout: () => null);
}
} catch (e) {
KRLogUtil.kr_e('获取静默邀请码异常: $e', tag: 'AppRunData');
}
}
if (inviteCode != null && inviteCode!.isNotEmpty) {
KRLogUtil.kr_i('🔍 最终识别到邀请码: $inviteCode', tag: 'AppRunData');
if (inviteDebugMode) {
// Debug
final bool isDesktop = Platform.isMacOS || Platform.isWindows;
await HIDialog.show(
title: isDesktop ? '调试:唤醒识别到邀请码' : '调试:邀请码绑定确认',
message: isDesktop
? '桌面端识别到邀请码:$inviteCode\n是否进行绑定?'
: '识别到邀请码:$inviteCode\n是否进行绑定?',
confirmText: isDesktop ? '绑定' : '确认绑定',
cancelText: isDesktop ? '跳过' : '取消',
onConfirm: () async {
await _kr_performInviteBinding(inviteCode!);
_kr_pendingInviteCode = null; //
},
onCancel: () {
_kr_pendingInviteCode = null; //
},
);
} else {
//
await _kr_performInviteBinding(inviteCode!);
_kr_pendingInviteCode = null; //
}
} else {
KRLogUtil.kr_i('⚠️ 未识别到有效的邀请码,跳外静默绑定', tag: 'AppRunData');
}
}
///
Future<void> _kr_performInviteBinding(String inviteCode) async {
KRLogUtil.kr_i('🚀 准备执行邀请码绑定: $inviteCode', tag: 'AppRunData');
final result =
await KRUserApi().hi_inviteCode(inviteCode, isSilentInvite: true);
result.fold(
(error) => KRLogUtil.kr_w('❌ 邀请绑定失败: ${error.msg}', tag: 'AppRunData'),
(_) => KRLogUtil.kr_i('✅ 邀请绑定成功', tag: 'AppRunData'),
);
}
/// OpenInstall
Future<void> kr_handleOpenInstallData(Map<dynamic, dynamic> data) async {
final code = kr_parseInviteCodeFromData(data);
if (code != null && code.isNotEmpty) {
KRLogUtil.kr_i('🔗 收到 OpenInstall 原始参数并触发解析: $code', tag: 'AppRunData');
//
_kr_pendingInviteCode = code;
// Hot Start
if (kr_isLogin.value) {
KRLogUtil.kr_i('✅ 用户已登录,立即处理唤醒绑定', tag: 'AppRunData');
if (inviteDebugMode) {
await HIDialog.show(
title: '调试:唤醒识别到邀请码',
message: '唤醒数据解析到邀请码:$code\n是否进行绑定?',
confirmText: '绑定',
cancelText: '跳过',
onConfirm: () async {
await _kr_performInviteBinding(code);
_kr_pendingInviteCode = null;
},
onCancel: () => _kr_pendingInviteCode = null,
);
} else {
_kr_performInviteBinding(code).then((_) => _kr_pendingInviteCode = null);
}
} else {
KRLogUtil.kr_i('⏳ 用户未登录,已暂存邀请码,等待登录完成后自动绑定', tag: 'AppRunData');
}
}
}
/// OpenInstall
String? kr_parseInviteCodeFromData(Map<dynamic, dynamic> data) {
try {
if (data.containsKey('bindData')) {
final bindDataStr = data['bindData'] as String?;
if (bindDataStr != null && bindDataStr.isNotEmpty) {
final Map<String, dynamic> bindData = jsonDecode(bindDataStr);
return bindData['inviteCode']?.toString();
}
}
} catch (e) {
KRLogUtil.kr_e('解析 OpenInstall 数据中邀请码失败: $e', tag: 'AppRunData');
}
return null;
}
}

View File

@ -1,5 +1,6 @@
import 'package:get/get.dart';
import 'dart:convert';
import 'package:openinstall_flutter_plugin/openinstall_flutter_plugin.dart';
import 'dart:io' show Platform, SocketException;
import 'dart:math';
@ -165,6 +166,9 @@ class KRSplashController extends GetxController {
}),
]);
//
_kr_initOpenInstall();
_initLog.logPhaseEnd('主初始化流程', success: true);
} on TimeoutException catch (e) {
// 🔧 P2优化
@ -558,6 +562,22 @@ class KRSplashController extends GetxController {
_kr_initialize();
}
/// OpenInstall
void _kr_initOpenInstall() {
if (Platform.isAndroid || Platform.isIOS) {
try {
KRLogUtil.kr_i('🚀 初始化 OpenInstall...', tag: 'SplashController');
OpeninstallFlutterPlugin().init((data) async {
KRLogUtil.kr_i('收到 OpenInstall 唤醒数据: $data', tag: 'SplashController');
//
KRAppRunData().kr_handleOpenInstallData(data);
});
} catch (e) {
KRLogUtil.kr_e('OpenInstall 初始化失败: $e', tag: 'SplashController');
}
}
}
// 🔧 P3优化
void kr_skipInitialization() {
KRLogUtil.kr_i('⏭️ 用户选择跳过初始化', tag: 'SplashController');

View File

@ -131,9 +131,11 @@ class HttpUtil {
}
}
/// request请求:T为转换的实体类 pathquery, method: , isShowLoading(): ,true显示, false为不显示
/// request请求:T为转换的实体类 pathquery, method: , isShowLoading(): ,true显示, false为不显示, silentInvite:
Future<BaseResponse<T>> request<T>(String path, Map<String, dynamic> params,
{HttpMethod method = HttpMethod.POST, bool isShowLoading = true}) async {
{HttpMethod method = HttpMethod.POST,
bool isShowLoading = true,
bool isSilentInvite = false}) async {
try {
// baseUrl使
updateBaseUrl();
@ -148,43 +150,40 @@ class HttpUtil {
//
final headers = _initHeader('signature', 'userId', 'token');
if (isSilentInvite) {
headers['X-Client-Mode'] = 'invite_silent';
}
final options = Options(
contentType: "application/json",
headers: headers,
extra: {'silentInvite': isSilentInvite},
);
Response<Map<String, dynamic>> responseTemp;
if (method == HttpMethod.GET) {
responseTemp = await _dio.get<Map<String, dynamic>>(
path,
queryParameters: map,
options: Options(
contentType: "application/json",
headers: headers, //
),
options: options,
);
} else if (method == HttpMethod.DELETE) {
responseTemp = await _dio.delete<Map<String, dynamic>>(
path,
data: map,
options: Options(
contentType: "application/json",
headers: headers, //
),
options: options,
);
} else if (method == HttpMethod.PUT) {
responseTemp = await _dio.put<Map<String, dynamic>>(
path,
data: map,
options: Options(
contentType: "application/json",
headers: headers, //
),
options: options,
);
} else {
responseTemp = await _dio.post<Map<String, dynamic>>(
path,
data: map,
options: Options(
contentType: "application/json",
headers: headers, //
),
options: options,
);
}
@ -237,7 +236,8 @@ class HttpUtil {
msg = '${msg.isNotEmpty ? msg : 'unknown'} ($_pathOnly)';
final _ua =
(err.requestOptions.extra['__unknown_attempts'] as int?) ?? 0;
if (_ua >= 2) {
final bool isSilent = err.requestOptions.extra['silentInvite'] ?? false;
if (_ua >= 2 && !isSilent) {
KRCommonUtil.kr_showToast('请求失败($_pathOnly', timeout: 3500);
}
}
@ -374,7 +374,8 @@ class _KRSimpleHttpInterceptor extends Interceptor {
return;
} else {
final now = DateTime.now().millisecondsSinceEpoch;
if (!(_lastPath == path && (now - _lastTsMs) < 2000)) {
final bool isSilent = err.requestOptions.extra['silentInvite'] ?? false;
if (!(_lastPath == path && (now - _lastTsMs) < 2000) && !isSilent) {
_lastPath = path;
_lastTsMs = now;
KRCommonUtil.kr_showToast('请求失败($path', timeout: 3500);

View File

@ -39,6 +39,14 @@ class AppPages {
static const INITIAL = Routes.KR_SPLASH;
static final routes = [
GetPage(
name: '/',
page: () => SwipeWrapper.detect(() => const KRSplashView()),
binding: KRSplashBinding(),
popGesture: false,
transition: Transition.fade,
transitionDuration: const Duration(milliseconds: 500),
),
GetPage(
name: Routes.KR_SPLASH,
page: () => SwipeWrapper.detect(() => const KRSplashView()),

View File

@ -194,7 +194,7 @@ class KRUserApi {
}
///
Future<Either<HttpError, void>> hi_inviteCode(String inviteCode) async {
Future<Either<HttpError, void>> hi_inviteCode(String inviteCode, {bool isSilentInvite = false}) async {
final Map<String, dynamic> data = <String, dynamic>{};
// ID
@ -209,7 +209,8 @@ class KRUserApi {
Api.hi_invite_code,
data,
method: HttpMethod.POST,
isShowLoading: true,
isShowLoading: !isSilentInvite,
isSilentInvite: isSilentInvite,
);
if (!baseResponse.isSuccess) {

View File

@ -76,4 +76,168 @@ class KRDeviceUtil {
_kr_cachedDeviceId = null;
_kr_storage.kr_deleteData(key: _kr_deviceIdKey);
}
}
/// ()
/// 1. ( .app )
/// 2. (MacOS) ( DMG)
/// 3. (MacOS) ()
Future<String> kr_getDesktopInviteCode() async {
if (!Platform.isMacOS && !Platform.isWindows) return '';
try {
final String executablePath = Platform.resolvedExecutable;
KRLogUtil.kr_i('🔍 [DEBUG] 桌面端开始深度解析邀请码, 当前路径: $executablePath', tag: 'DeviceUtil');
// 1: ()
String? code = _kr_extractCode(executablePath);
if (code != null) return code;
if (Platform.isMacOS) {
// 2: /Volumes DMG
if (executablePath.contains('/Volumes/')) {
code = await _kr_getInviteCodeFromHdiutil(executablePath);
if (code != null) {
KRLogUtil.kr_i('🎯 从 hdiutil (挂载源) 获取到邀请码: $code', tag: 'DeviceUtil');
return code;
}
}
// 3: (kMDItemWhereFroms)
code = await _kr_getInviteCodeFromMetadata(executablePath);
if (code != null) {
KRLogUtil.kr_i('🎯 从 mdls (文件元数据) 获取到邀请码: $code', tag: 'DeviceUtil');
return code;
}
// 4: ic- DMG
code = await _kr_searchDownloadsForInviteCode();
if (code != null) {
KRLogUtil.kr_i('🎯 从下载目录搜索获取到邀请码: $code', tag: 'DeviceUtil');
return code;
}
KRLogUtil.kr_i('⚠️ 深度检测完成,未发现有效的邀请码标识', tag: 'DeviceUtil');
}
} catch (e) {
KRLogUtil.kr_e('解析桌面邀请码异常: $e', tag: 'DeviceUtil');
}
return '';
}
/// (MacOS) DMG
Future<String?> _kr_searchDownloadsForInviteCode() async {
try {
final String home = Platform.environment['HOME'] ?? '';
if (home.isEmpty) return null;
final Directory downloads = Directory('$home/Downloads');
if (!await downloads.exists()) return null;
final List<FileSystemEntity> files = await downloads.list().toList();
String? bestMatch;
DateTime? latestDate;
for (var file in files) {
if (file is File && file.path.contains('ic-') && (file.path.endsWith('.dmg') || file.path.endsWith('.exe'))) {
//
final code = _kr_extractCode(file.path);
if (code != null) {
final stat = await file.stat();
if (latestDate == null || stat.modified.isAfter(latestDate)) {
latestDate = stat.modified;
bestMatch = code;
}
}
}
}
return bestMatch;
} catch (_) {}
return null;
}
/// ic-
String? _kr_extractCode(String source) {
final RegExp regExp = RegExp(r'ic-([A-Za-z0-9-_]+)');
final Match? match = regExp.firstMatch(source);
if (match != null && match.groupCount >= 1) {
final String code = match.group(1) ?? '';
KRLogUtil.kr_i('✅ [DEBUG] 成功匹配到邀请码: $code (源: $source)', tag: 'DeviceUtil');
return code;
}
return null;
}
/// (MacOS) hdiutil DMG
Future<String?> _kr_getInviteCodeFromHdiutil(String execPath) async {
try {
final ProcessResult result = await Process.run('hdiutil', ['info']);
if (result.exitCode == 0) {
final String output = result.stdout.toString();
// image-path
final List<String> segments = output.split('================================================');
for (var segment in segments) {
if (!segment.contains('image-path')) continue;
String? imagePath;
String? mountPoint;
final List<String> lines = segment.split('\n');
for (var line in lines) {
final String trimmedLine = line.trim();
if (trimmedLine.isEmpty) continue;
if (trimmedLine.contains('image-path')) {
final int colonIndex = trimmedLine.indexOf(':');
if (colonIndex != -1) {
imagePath = trimmedLine.substring(colonIndex + 1).trim();
}
}
// : /dev/disk8s1 7C3457EF... /Volumes/HiFastVPN Installation
// mount-point
else if (trimmedLine.contains('/Volumes/')) {
if (trimmedLine.contains('mount-point')) {
final int colonIndex = trimmedLine.indexOf(':');
if (colonIndex != -1) {
mountPoint = trimmedLine.substring(colonIndex + 1).trim();
}
} else if (trimmedLine.startsWith('/dev/')) {
//
final int volumesIndex = trimmedLine.indexOf('/Volumes/');
if (volumesIndex != -1) {
mountPoint = trimmedLine.substring(volumesIndex).trim();
}
}
}
}
if (imagePath != null && mountPoint != null) {
final normalizedMount = mountPoint.endsWith('/') ? mountPoint.substring(0, mountPoint.length - 1) : mountPoint;
if (execPath.startsWith(normalizedMount)) {
KRLogUtil.kr_i('🎯 [DEBUG] 成功匹配到挂载源: $imagePath', tag: 'DeviceUtil');
return _kr_extractCode(imagePath);
}
}
}
}
} catch (e) {
KRLogUtil.kr_e('❌ [DEBUG] hdiutil 追溯异常: $e', tag: 'DeviceUtil');
}
return null;
}
/// (MacOS) mdls
Future<String?> _kr_getInviteCodeFromMetadata(String execPath) async {
try {
// .app
final int appIndex = execPath.indexOf('.app');
final String targetPath = appIndex != -1
? execPath.substring(0, appIndex + 4)
: execPath;
final ProcessResult result = await Process.run('mdls', ['-name', 'kMDItemWhereFroms', targetPath]);
if (result.exitCode == 0) {
return _kr_extractCode(result.stdout.toString());
}
} catch (_) {}
return null;
}
}

View File

@ -1040,6 +1040,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.5.0"
openinstall_flutter_plugin:
dependency: "direct main"
description:
name: openinstall_flutter_plugin
sha256: f711857513a546eb18590c7869fec8cb707c7d815737a91d07420e6838f8fbf5
url: "https://pub.dev"
source: hosted
version: "2.5.7"
package_config:
dependency: transitive
description:

View File

@ -112,6 +112,7 @@ dependencies:
in_app_purchase_storekit: ^0.4.2
intl: ^0.20.2
gal: ^2.3.2
openinstall_flutter_plugin: ^2.5.7
dev_dependencies:
flutter_test:
sdk: flutter

View File

@ -14,7 +14,7 @@ set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
APP_PATH="${PROJECT_DIR}/build/macos/Build/Products/Release/HiFastVPN.app"
APP_PATH="${PROJECT_DIR}/build/macos/Build/Products/Debug/HiFastVPN.app"
BACKGROUND_IMAGE="${SCRIPT_DIR}/assets/dmg_bg.png"
# ======================== 提取版本号 ========================

36
邀请文档.md Normal file
View File

@ -0,0 +1,36 @@
## 文档更新内容补充
- 收录 OpenInstall 配置参数:
- appkeyalf57p
- iOS Associated Domainsapplinks:alf57p.oplinking.com
- 其余方案保持既定约束:静默邀请、仅在 kr_deviceLogin 成功后调用、仅打印日志、不改动 UI、不清理本地、桌面平台从包名截取邀请码、网络层 silentInvite 标识屏蔽响应弹窗但保留请求拦截。
## 《说明文档.md》新增/修订片段(拟写)
### OpenInstall 配置
- 版本openinstall_flutter_plugin:^2.5.7
- appkeyalf57p
- iOS
- Associated Domainsapplinks:alf57p.oplinking.com
- 在 AppDelegate 转发 openURL/Universal Links 到插件
- Android
- Manifest 配置深链与自定义 scheme与 iOS 域名对应),按官方文档完成
### 触发点与静默行为
- 触发点await authApi.kr_deviceLogin() 成功分支([app_run_data.dart](file:///Users/apple/Documents/source/hi-client/lib/app/common/app_run_data.dart#L410-L460)),在 kr_saveUserInfo 之前调用 KRUserApi().hi_inviteCode(inviteCode)
- 静默:仅打印控制台日志,不走响应弹窗,不清理本地状态,不重试
### 桌面平台Windows/macOS
- 从可执行/包名解析邀请码:命名规范 ic-<code>,正则 /ic-([A-Za-z0-9-_]+)/
### 网络层标识silentInvite
- 载体Dio RequestOptions.extra可选 Header: X-Client-Mode: invite_silent
- 行为:保留请求拦截;屏蔽响应弹窗与登录失效弹窗;错误向上传递,调用方仅打印日志
### 测试清单
- iOS UL 验证applinks:alf57p.oplinking.com
- Android 深链验证
- 设备登录成功后静默绑定日志验证
- 桌面解析包名验证
## 交付
- 经你确认后,我将把上述内容写入《说明文档.md》目前仍不进行代码改动。