import 'package:flutter_udid/flutter_udid.dart'; import 'package:kaer_with_panels/app/utils/kr_log_util.dart'; import 'package:kaer_with_panels/app/utils/kr_secure_storage.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'dart:io'; import 'dart:convert'; import 'package:crypto/crypto.dart'; /// 设备工具类 class KRDeviceUtil { static final KRDeviceUtil _instance = KRDeviceUtil._internal(); /// 设备ID缓存 String? _kr_cachedDeviceId; /// 存储键 static const String _kr_deviceIdKey = 'kr_device_id'; /// 存储实例 final KRSecureStorage _kr_storage = KRSecureStorage(); KRDeviceUtil._internal(); factory KRDeviceUtil() => _instance; /// 获取设备ID /// 如果获取失败,返回空字符串 Future kr_getDeviceId() async { try { if (_kr_cachedDeviceId != null) { return _kr_cachedDeviceId!; } // 先从存储中获取 final String? kr_savedDeviceId = await _kr_storage.kr_readData(key: _kr_deviceIdKey); if (kr_savedDeviceId != null) { _kr_cachedDeviceId = kr_savedDeviceId; return _kr_cachedDeviceId!; } // 根据不同平台获取设备ID if (Platform.isMacOS || Platform.isWindows ) { // 获取系统信息 final PackageInfo kr_packageInfo = await PackageInfo.fromPlatform(); final String kr_platform = Platform.operatingSystem; final String kr_version = Platform.operatingSystemVersion; final String kr_localHostname = Platform.localHostname; // 组合信息生成唯一ID final String kr_deviceInfo = '$kr_platform-$kr_version-$kr_localHostname-${kr_packageInfo.packageName}-${kr_packageInfo.buildNumber}'; _kr_cachedDeviceId = md5.convert(utf8.encode(kr_deviceInfo)).toString(); } else if (Platform.isIOS || Platform.isAndroid ) { _kr_cachedDeviceId = await FlutterUdid.udid; } else { KRLogUtil.kr_e('不支持的平台: ${Platform.operatingSystem}', tag: 'DeviceUtil'); return ''; } // 保存到存储中 if (_kr_cachedDeviceId != null) { await _kr_storage.kr_saveData(key: _kr_deviceIdKey, value: _kr_cachedDeviceId!); } KRLogUtil.kr_i('获取设备ID: $_kr_cachedDeviceId', tag: 'DeviceUtil'); return _kr_cachedDeviceId ?? ''; } catch (e) { KRLogUtil.kr_e('获取设备ID失败: $e', tag: 'DeviceUtil'); return ''; } } /// 清除缓存的设备ID void kr_clearDeviceId() { _kr_cachedDeviceId = null; _kr_storage.kr_deleteData(key: _kr_deviceIdKey); } /// 从桌面平台提取邀请码 (深度检测) /// 1. 检测执行路径 (支持重命名的 .app 或二进制) /// 2. (MacOS) 检测挂载源 (支持重命名的 DMG) /// 3. (MacOS) 检测来源元数据 (支持从下载链接中识别) Future 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; // 策略 1.5: (Windows 独有) 检测安装目录下的 channel.txt (支持 Inno Setup 方案) if (Platform.isWindows) { code = await _kr_getInviteCodeFromChannelFile(executablePath); if (code != null) { KRLogUtil.kr_i('🎯 从 channel.txt 获取到邀请码: $code', tag: 'DeviceUtil'); 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- 的程序 (MacOS/Windows 通用) 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/Windows) 在下载目录下搜寻最近的、符合命名规范的安装包 Future _kr_searchDownloadsForInviteCode() async { try { // 适配不同平台的家目录环境变量 final String home = Platform.environment['HOME'] ?? Platform.environment['USERPROFILE'] ?? ''; if (home.isEmpty) return null; final Directory downloads = Directory('${home}${Platform.pathSeparator}Downloads'); if (!await downloads.exists()) return null; final List 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; } /// (Windows) 从安装目录下的 channel.txt 中读取邀请码 (针对 Inno Setup 方案) Future _kr_getInviteCodeFromChannelFile(String execPath) async { try { final File exeFile = File(execPath); final String dirPath = exeFile.parent.path; final File channelFile = File('$dirPath${Platform.pathSeparator}channel.txt'); if (await channelFile.exists()) { final String content = await channelFile.readAsString(); final String channel = content.trim(); if (channel.isNotEmpty) { // 兼容两种格式:纯邀请码 或 带 ic- 标识的邀请码 if (channel.contains('ic-')) { return _kr_extractCode(channel); } return channel; // 如果 Inno Setup 已经截取好了纯 code,直接返回 } } } catch (e) { KRLogUtil.kr_e('读取 Windows channel.txt 失败: $e', tag: 'DeviceUtil'); } 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 _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 segments = output.split('================================================'); for (var segment in segments) { if (!segment.contains('image-path')) continue; String? imagePath; String? mountPoint; final List 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 _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; } }