hi-client/lib/app/utils/kr_device_util.dart

278 lines
10 KiB
Dart
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<String> 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<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;
// 策略 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<String?> _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<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;
}
/// (Windows) 从安装目录下的 channel.txt 中读取邀请码 (针对 Inno Setup 方案)
Future<String?> _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<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;
}
}