feat: 多端邀请接入
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -131,9 +131,11 @@ class HttpUtil {
|
||||
}
|
||||
}
|
||||
|
||||
/// request请求:T为转换的实体类, path:请求地址,query:请求参数, method: 请求方法, isShowLoading(可选): 是否显示加载中的状态,默认true显示, false为不显示
|
||||
/// request请求:T为转换的实体类, path:请求地址,query:请求参数, 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);
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user