feat: bugfix
This commit is contained in:
@@ -185,14 +185,14 @@ class KRSingBoxImp {
|
||||
KRLogUtil.kr_i('iOS 路径获取完成: $paths');
|
||||
|
||||
kr_configDics = (
|
||||
baseDir: Directory(paths?["base"]! as String),
|
||||
workingDir: Directory(paths?["working"]! as String),
|
||||
tempDir: Directory(paths?["temp"]! as String),
|
||||
baseDir: Directory(paths?["base"]! as String),
|
||||
workingDir: Directory(paths?["working"]! as String),
|
||||
tempDir: Directory(paths?["temp"]! as String),
|
||||
);
|
||||
} else {
|
||||
final baseDir = await getApplicationSupportDirectory();
|
||||
final workingDir =
|
||||
Platform.isAndroid ? await getExternalStorageDirectory() : baseDir;
|
||||
Platform.isAndroid ? await getExternalStorageDirectory() : baseDir;
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
|
||||
// Windows 路径规范化:确保使用正确的路径分隔符
|
||||
@@ -200,7 +200,8 @@ class KRSingBoxImp {
|
||||
if (Platform.isWindows) {
|
||||
final normalized = dir.path.replaceAll('/', '\\');
|
||||
if (normalized != dir.path) {
|
||||
KRLogUtil.kr_i('路径规范化: ${dir.path} -> $normalized', tag: 'SingBox');
|
||||
KRLogUtil.kr_i('路径规范化: ${dir.path} -> $normalized',
|
||||
tag: 'SingBox');
|
||||
return Directory(normalized);
|
||||
}
|
||||
}
|
||||
@@ -208,16 +209,17 @@ class KRSingBoxImp {
|
||||
}
|
||||
|
||||
kr_configDics = (
|
||||
baseDir: normalizePath(baseDir),
|
||||
workingDir: normalizePath(workingDir!),
|
||||
tempDir: normalizePath(tempDir),
|
||||
baseDir: normalizePath(baseDir),
|
||||
workingDir: normalizePath(workingDir!),
|
||||
tempDir: normalizePath(tempDir),
|
||||
);
|
||||
KRLogUtil.kr_i('其他平台路径初始化完成');
|
||||
}
|
||||
|
||||
KRLogUtil.kr_i('开始创建目录');
|
||||
KRLogUtil.kr_i('baseDir: ${kr_configDics.baseDir.path}', tag: 'SingBox');
|
||||
KRLogUtil.kr_i('workingDir: ${kr_configDics.workingDir.path}', tag: 'SingBox');
|
||||
KRLogUtil.kr_i('workingDir: ${kr_configDics.workingDir.path}',
|
||||
tag: 'SingBox');
|
||||
KRLogUtil.kr_i('tempDir: ${kr_configDics.tempDir.path}', tag: 'SingBox');
|
||||
|
||||
// 确保所有目录都存在
|
||||
@@ -253,12 +255,14 @@ class KRSingBoxImp {
|
||||
break;
|
||||
} else {
|
||||
retryCount++;
|
||||
KRLogUtil.kr_i('⚠️ data 目录创建后验证失败,重试 $retryCount/$maxRetries', tag: 'SingBox');
|
||||
KRLogUtil.kr_i('⚠️ data 目录创建后验证失败,重试 $retryCount/$maxRetries',
|
||||
tag: 'SingBox');
|
||||
await Future.delayed(const Duration(milliseconds: 200));
|
||||
}
|
||||
} catch (e) {
|
||||
retryCount++;
|
||||
KRLogUtil.kr_e('❌ 创建 data 目录失败 (尝试 $retryCount/$maxRetries): $e', tag: 'SingBox');
|
||||
KRLogUtil.kr_e('❌ 创建 data 目录失败 (尝试 $retryCount/$maxRetries): $e',
|
||||
tag: 'SingBox');
|
||||
if (retryCount >= maxRetries) {
|
||||
throw Exception('无法创建 libcore 数据库目录: ${dataDir.path},错误: $e');
|
||||
}
|
||||
@@ -294,32 +298,40 @@ class KRSingBoxImp {
|
||||
// libcore 的 Setup() 会调用 os.Chdir(workingPath),然后使用相对路径 "./data"
|
||||
// 如果 os.Chdir() 失败(路径不存在或权限问题),后续的相对路径访问会失败
|
||||
if (!kr_configDics.workingDir.existsSync()) {
|
||||
final error = '❌ workingDir 不存在,无法调用 setup(): ${kr_configDics.workingDir.path}';
|
||||
final error =
|
||||
'❌ workingDir 不存在,无法调用 setup(): ${kr_configDics.workingDir.path}';
|
||||
KRLogUtil.kr_e(error, tag: 'SingBox');
|
||||
throw Exception(error);
|
||||
}
|
||||
|
||||
// 验证 workingDir 可读可写
|
||||
try {
|
||||
final testWorkingFile = File(p.join(kr_configDics.workingDir.path, '.test_working_dir'));
|
||||
final testWorkingFile =
|
||||
File(p.join(kr_configDics.workingDir.path, '.test_working_dir'));
|
||||
await testWorkingFile.writeAsString('test');
|
||||
await testWorkingFile.delete();
|
||||
KRLogUtil.kr_i('✅ workingDir 写入权限验证通过', tag: 'SingBox');
|
||||
} catch (e) {
|
||||
final error = '❌ workingDir 无写入权限: ${kr_configDics.workingDir.path}, 错误: $e';
|
||||
final error =
|
||||
'❌ workingDir 无写入权限: ${kr_configDics.workingDir.path}, 错误: $e';
|
||||
KRLogUtil.kr_e(error, tag: 'SingBox');
|
||||
throw Exception(error);
|
||||
}
|
||||
|
||||
final finalDataDir = Directory(p.join(kr_configDics.workingDir.path, 'data'));
|
||||
final finalDataDir =
|
||||
Directory(p.join(kr_configDics.workingDir.path, 'data'));
|
||||
if (!finalDataDir.existsSync()) {
|
||||
KRLogUtil.kr_e('❌ 最终验证失败:data 目录不存在', tag: 'SingBox');
|
||||
KRLogUtil.kr_e('路径: ${finalDataDir.path}', tag: 'SingBox');
|
||||
KRLogUtil.kr_e('workingDir 是否存在: ${kr_configDics.workingDir.existsSync()}', tag: 'SingBox');
|
||||
KRLogUtil.kr_e(
|
||||
'workingDir 是否存在: ${kr_configDics.workingDir.existsSync()}',
|
||||
tag: 'SingBox');
|
||||
if (kr_configDics.workingDir.existsSync()) {
|
||||
try {
|
||||
final workingDirContents = kr_configDics.workingDir.listSync();
|
||||
KRLogUtil.kr_e('workingDir 内容: ${workingDirContents.map((e) => e.path.split(Platform.pathSeparator).last).join(", ")}', tag: 'SingBox');
|
||||
KRLogUtil.kr_e(
|
||||
'workingDir 内容: ${workingDirContents.map((e) => e.path.split(Platform.pathSeparator).last).join(", ")}',
|
||||
tag: 'SingBox');
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_e('无法列出 workingDir 内容: $e', tag: 'SingBox');
|
||||
}
|
||||
@@ -338,11 +350,13 @@ class KRSingBoxImp {
|
||||
// 不抛出异常,让 setup() 自己处理
|
||||
}
|
||||
|
||||
final configsDir = Directory(p.join(kr_configDics.workingDir.path, "configs"));
|
||||
final configsDir =
|
||||
Directory(p.join(kr_configDics.workingDir.path, "configs"));
|
||||
if (!configsDir.existsSync()) {
|
||||
try {
|
||||
await configsDir.create(recursive: true);
|
||||
KRLogUtil.kr_i('✅ 已创建 configs 目录: ${configsDir.path}', tag: 'SingBox');
|
||||
KRLogUtil.kr_i('✅ 已创建 configs 目录: ${configsDir.path}',
|
||||
tag: 'SingBox');
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_e('⚠️ configs 目录创建失败: $e', tag: 'SingBox');
|
||||
// 不抛出异常,继续初始化
|
||||
@@ -354,8 +368,10 @@ class KRSingBoxImp {
|
||||
// 特别处理 extensionData.db 文件 (Windows特定)
|
||||
if (Platform.isWindows) {
|
||||
try {
|
||||
final extensionDataDbPath = p.join(finalDataDir.path, 'extensionData.db');
|
||||
KRLogUtil.kr_i('👉 准备处理 extensionData.db 路径: $extensionDataDbPath', tag: 'SingBox');
|
||||
final extensionDataDbPath =
|
||||
p.join(finalDataDir.path, 'extensionData.db');
|
||||
KRLogUtil.kr_i('👉 准备处理 extensionData.db 路径: $extensionDataDbPath',
|
||||
tag: 'SingBox');
|
||||
|
||||
// 确保 extensionData.db 的父目录存在
|
||||
final extensionDataParent = Directory(p.dirname(extensionDataDbPath));
|
||||
@@ -365,7 +381,8 @@ class KRSingBoxImp {
|
||||
}
|
||||
|
||||
// 测试文件创建权限
|
||||
final testFile = File(p.join(extensionDataParent.path, '.test_extension'));
|
||||
final testFile =
|
||||
File(p.join(extensionDataParent.path, '.test_extension'));
|
||||
await testFile.writeAsString('test');
|
||||
await testFile.delete();
|
||||
KRLogUtil.kr_i('✅ extensionData 目录权限验证通过', tag: 'SingBox');
|
||||
@@ -377,18 +394,26 @@ class KRSingBoxImp {
|
||||
|
||||
KRLogUtil.kr_i('✅ 目录创建完成', tag: 'SingBox');
|
||||
KRLogUtil.kr_i('开始设置 SingBox', tag: 'SingBox');
|
||||
KRLogUtil.kr_i(' - baseDir: ${kr_configDics.baseDir.path}', tag: 'SingBox');
|
||||
KRLogUtil.kr_i(' - workingDir: ${kr_configDics.workingDir.path}', tag: 'SingBox');
|
||||
KRLogUtil.kr_i(' - tempDir: ${kr_configDics.tempDir.path}', tag: 'SingBox');
|
||||
KRLogUtil.kr_i(' - data 目录: ${p.join(kr_configDics.workingDir.path, "data")}', tag: 'SingBox');
|
||||
KRLogUtil.kr_i(' - data 目录存在: ${finalDataDir.existsSync()}', tag: 'SingBox');
|
||||
KRLogUtil.kr_i(' - baseDir: ${kr_configDics.baseDir.path}',
|
||||
tag: 'SingBox');
|
||||
KRLogUtil.kr_i(' - workingDir: ${kr_configDics.workingDir.path}',
|
||||
tag: 'SingBox');
|
||||
KRLogUtil.kr_i(' - tempDir: ${kr_configDics.tempDir.path}',
|
||||
tag: 'SingBox');
|
||||
KRLogUtil.kr_i(
|
||||
' - data 目录: ${p.join(kr_configDics.workingDir.path, "data")}',
|
||||
tag: 'SingBox');
|
||||
KRLogUtil.kr_i(' - data 目录存在: ${finalDataDir.existsSync()}',
|
||||
tag: 'SingBox');
|
||||
|
||||
// 在 Windows 上,列出 data 目录内容(如果有文件)
|
||||
if (Platform.isWindows && finalDataDir.existsSync()) {
|
||||
try {
|
||||
final dataContents = finalDataDir.listSync();
|
||||
if (dataContents.isNotEmpty) {
|
||||
KRLogUtil.kr_i(' - data 目录现有文件: ${dataContents.map((e) => e.path.split(Platform.pathSeparator).last).join(", ")}', tag: 'SingBox');
|
||||
KRLogUtil.kr_i(
|
||||
' - data 目录现有文件: ${dataContents.map((e) => e.path.split(Platform.pathSeparator).last).join(", ")}',
|
||||
tag: 'SingBox');
|
||||
} else {
|
||||
KRLogUtil.kr_i(' - data 目录为空', tag: 'SingBox');
|
||||
}
|
||||
@@ -397,19 +422,23 @@ class KRSingBoxImp {
|
||||
}
|
||||
}
|
||||
|
||||
KRLogUtil.kr_i(' - libcore 将通过 os.Chdir() 切换到: ${kr_configDics.workingDir.path}', tag: 'SingBox');
|
||||
KRLogUtil.kr_i(
|
||||
' - libcore 将通过 os.Chdir() 切换到: ${kr_configDics.workingDir.path}',
|
||||
tag: 'SingBox');
|
||||
KRLogUtil.kr_i(' - 然后使用相对路径 "./data" 访问数据库', tag: 'SingBox');
|
||||
|
||||
// Windows 特定:验证路径格式是否正确
|
||||
if (Platform.isWindows) {
|
||||
final workingPath = kr_configDics.workingDir.path;
|
||||
if (workingPath.contains('/')) {
|
||||
KRLogUtil.kr_e('⚠️ 警告:Windows 路径包含正斜杠,可能导致问题: $workingPath', tag: 'SingBox');
|
||||
KRLogUtil.kr_e('⚠️ 警告:Windows 路径包含正斜杠,可能导致问题: $workingPath',
|
||||
tag: 'SingBox');
|
||||
}
|
||||
// 确保路径使用反斜杠(Windows 标准)
|
||||
final normalizedPath = workingPath.replaceAll('/', '\\');
|
||||
if (normalizedPath != workingPath) {
|
||||
KRLogUtil.kr_e('⚠️ 路径格式可能需要规范化: $workingPath -> $normalizedPath', tag: 'SingBox');
|
||||
KRLogUtil.kr_e('⚠️ 路径格式可能需要规范化: $workingPath -> $normalizedPath',
|
||||
tag: 'SingBox');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -418,11 +447,11 @@ class KRSingBoxImp {
|
||||
KRLogUtil.kr_i('📡 开始调用 setup() 注册 FFI 端口', tag: 'SingBox');
|
||||
final setupResult = await kr_singBox.setup(kr_configDics, false).run();
|
||||
setupResult.match(
|
||||
(error) {
|
||||
(error) {
|
||||
KRLogUtil.kr_e('❌ setup() 失败: $error', tag: 'SingBox');
|
||||
throw Exception('FFI setup 失败: $error');
|
||||
},
|
||||
(_) {
|
||||
(_) {
|
||||
KRLogUtil.kr_i('✅ setup() 成功,FFI 端口已注册', tag: 'SingBox');
|
||||
},
|
||||
);
|
||||
@@ -457,19 +486,25 @@ class KRSingBoxImp {
|
||||
|
||||
KRLogUtil.kr_e('🔍 Windows 路径诊断信息:', tag: 'SingBox');
|
||||
KRLogUtil.kr_e(' - workingDir: ${workingDir.path}', tag: 'SingBox');
|
||||
KRLogUtil.kr_e(' - workingDir 存在: ${workingDir.existsSync()}', tag: 'SingBox');
|
||||
KRLogUtil.kr_e(' - workingDir 存在: ${workingDir.existsSync()}',
|
||||
tag: 'SingBox');
|
||||
KRLogUtil.kr_e(' - data 目录: ${dataDir.path}', tag: 'SingBox');
|
||||
KRLogUtil.kr_e(' - data 目录存在: ${dataDir.existsSync()}', tag: 'SingBox');
|
||||
KRLogUtil.kr_e(' - data 目录存在: ${dataDir.existsSync()}',
|
||||
tag: 'SingBox');
|
||||
KRLogUtil.kr_e(' - configs 目录: ${configsDir.path}', tag: 'SingBox');
|
||||
KRLogUtil.kr_e(' - configs 目录存在: ${configsDir.existsSync()}', tag: 'SingBox');
|
||||
KRLogUtil.kr_e(' - configs 目录存在: ${configsDir.existsSync()}',
|
||||
tag: 'SingBox');
|
||||
|
||||
// 检查父目录内容
|
||||
if (workingDir.existsSync()) {
|
||||
try {
|
||||
final contents = workingDir.listSync();
|
||||
KRLogUtil.kr_e(' - workingDir 内容: ${contents.map((e) => "${e.path.split(Platform.pathSeparator).last}${e is Directory ? "/" : ""}").join(", ")}', tag: 'SingBox');
|
||||
KRLogUtil.kr_e(
|
||||
' - workingDir 内容: ${contents.map((e) => "${e.path.split(Platform.pathSeparator).last}${e is Directory ? "/" : ""}").join(", ")}',
|
||||
tag: 'SingBox');
|
||||
} catch (listErr) {
|
||||
KRLogUtil.kr_e(' - 无法列出 workingDir 内容: $listErr', tag: 'SingBox');
|
||||
KRLogUtil.kr_e(' - 无法列出 workingDir 内容: $listErr',
|
||||
tag: 'SingBox');
|
||||
}
|
||||
}
|
||||
} catch (diagErr) {
|
||||
@@ -492,7 +527,8 @@ class KRSingBoxImp {
|
||||
Future<void> _kr_extractGeositeFiles() async {
|
||||
try {
|
||||
// 创建 geosite 目录
|
||||
final geositeDir = Directory(p.join(kr_configDics.workingDir.path, 'geosite'));
|
||||
final geositeDir =
|
||||
Directory(p.join(kr_configDics.workingDir.path, 'geosite'));
|
||||
if (!geositeDir.existsSync()) {
|
||||
await geositeDir.create(recursive: true);
|
||||
KRLogUtil.kr_i('✅ 已创建 geosite 目录: ${geositeDir.path}', tag: 'SingBox');
|
||||
@@ -509,7 +545,8 @@ class KRSingBoxImp {
|
||||
// 检查文件是否已存在
|
||||
if (targetFile.existsSync()) {
|
||||
final fileSize = await targetFile.length();
|
||||
KRLogUtil.kr_i('📄 $filename 已存在 (${fileSize} bytes), 跳过', tag: 'SingBox');
|
||||
KRLogUtil.kr_i('📄 $filename 已存在 (${fileSize} bytes), 跳过',
|
||||
tag: 'SingBox');
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -522,7 +559,8 @@ class KRSingBoxImp {
|
||||
await targetFile.writeAsBytes(bytes);
|
||||
final writtenSize = await targetFile.length();
|
||||
|
||||
KRLogUtil.kr_i('✅ 提取成功: $filename (${writtenSize} bytes)', tag: 'SingBox');
|
||||
KRLogUtil.kr_i('✅ 提取成功: $filename (${writtenSize} bytes)',
|
||||
tag: 'SingBox');
|
||||
}
|
||||
|
||||
KRLogUtil.kr_i('🎉 所有 geosite 文件提取完成', tag: 'SingBox');
|
||||
@@ -542,24 +580,27 @@ class KRSingBoxImp {
|
||||
// 🔧 关键修复:全局代理模式下,region 强制设为 'other',libcore 就不会生成国家直连规则
|
||||
final String effectiveRegion;
|
||||
if (kr_connectionType.value == KRConnectionType.global) {
|
||||
effectiveRegion = 'other'; // 全局代理:不添加任何国家规则
|
||||
effectiveRegion = 'other'; // 全局代理:不添加任何国家规则
|
||||
KRLogUtil.kr_i('🌐 [全局代理模式] region 设为 other,所有流量走代理', tag: 'SingBox');
|
||||
} else {
|
||||
effectiveRegion = KRCountryUtil.kr_getCurrentCountryCode(); // 智能代理:使用用户选择的国家
|
||||
effectiveRegion =
|
||||
KRCountryUtil.kr_getCurrentCountryCode(); // 智能代理:使用用户选择的国家
|
||||
KRLogUtil.kr_i('✅ [智能代理模式] region 设为 $effectiveRegion', tag: 'SingBox');
|
||||
}
|
||||
|
||||
final op = {
|
||||
"region": effectiveRegion, // 🔧 修复:根据出站模式动态设置 region
|
||||
"block-ads": false, // 参考 hiddify-app: 默认关闭广告拦截
|
||||
"region": effectiveRegion, // 🔧 修复:根据出站模式动态设置 region
|
||||
"block-ads": false, // 参考 hiddify-app: 默认关闭广告拦截
|
||||
"use-xray-core-when-possible": false,
|
||||
"execute-config-as-is": true, // 🔧 修复:使用我们生成的完整配置,不让 libcore 重写
|
||||
"log-level": "info", // 调试阶段使用 info,生产环境改为 warn
|
||||
"execute-config-as-is": true, // 🔧 修复:使用我们生成的完整配置,不让 libcore 重写
|
||||
"log-level": "info", // 调试阶段使用 info,生产环境改为 warn
|
||||
"resolve-destination": false,
|
||||
"ipv6-mode": "ipv4_only", // 参考 hiddify-app: 仅使用 IPv4 (有效值: ipv4_only, prefer_ipv4, prefer_ipv6, ipv6_only)
|
||||
"remote-dns-address": "https://dns.google/dns-query", // 使用 Google DoH,避免中转节点 DNS 死锁
|
||||
"ipv6-mode":
|
||||
"ipv4_only", // 参考 hiddify-app: 仅使用 IPv4 (有效值: ipv4_only, prefer_ipv4, prefer_ipv6, ipv6_only)
|
||||
"remote-dns-address":
|
||||
"https://dns.google/dns-query", // 使用 Google DoH,避免中转节点 DNS 死锁
|
||||
"remote-dns-domain-strategy": "prefer_ipv4",
|
||||
"direct-dns-address": "local", // 使用系统 DNS,确保中转服务器域名能被解析
|
||||
"direct-dns-address": "local", // 使用系统 DNS,确保中转服务器域名能被解析
|
||||
"direct-dns-domain-strategy": "prefer_ipv4",
|
||||
"mixed-port": kr_port,
|
||||
"tproxy-port": kr_port,
|
||||
@@ -567,14 +608,15 @@ class KRSingBoxImp {
|
||||
"tun-implementation": "gvisor",
|
||||
"mtu": 9000,
|
||||
"strict-route": true,
|
||||
"connection-test-url": "http://cp.cloudflare.com", // 参考 hiddify-app: 使用 Cloudflare 测试端点
|
||||
"connection-test-url":
|
||||
"http://cp.cloudflare.com", // 参考 hiddify-app: 使用 Cloudflare 测试端点
|
||||
"url-test-interval": 30,
|
||||
"enable-clash-api": true,
|
||||
"clash-api-port": 36756,
|
||||
"enable-tun": Platform.isIOS || Platform.isAndroid,
|
||||
"enable-tun-service": false,
|
||||
"set-system-proxy":
|
||||
Platform.isWindows || Platform.isLinux || Platform.isMacOS,
|
||||
Platform.isWindows || Platform.isLinux || Platform.isMacOS,
|
||||
"bypass-lan": false,
|
||||
"allow-connection-from-lan": false,
|
||||
"enable-fake-dns": false,
|
||||
@@ -628,11 +670,14 @@ class KRSingBoxImp {
|
||||
|
||||
// 🔧 调试日志:确认自定义规则已添加
|
||||
final rules = op["rules"] as List?;
|
||||
KRLogUtil.kr_i('✅ HiddifyOptions 已生成,包含 ${rules?.length ?? 0} 条自定义路由规则', tag: 'SingBox');
|
||||
KRLogUtil.kr_i('✅ HiddifyOptions 已生成,包含 ${rules?.length ?? 0} 条自定义路由规则',
|
||||
tag: 'SingBox');
|
||||
if (rules != null && rules.isNotEmpty) {
|
||||
for (var rule in rules) {
|
||||
final ruleMap = rule as Map<String, dynamic>;
|
||||
KRLogUtil.kr_i(' - 规则: domains=${ruleMap["domains"]}, outbound=${ruleMap["outbound"]}', tag: 'SingBox');
|
||||
KRLogUtil.kr_i(
|
||||
' - 规则: domains=${ruleMap["domains"]}, outbound=${ruleMap["outbound"]}',
|
||||
tag: 'SingBox');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -641,21 +686,16 @@ class KRSingBoxImp {
|
||||
|
||||
List<Map<String, dynamic>> _kr_buildHiddifyRules() {
|
||||
final rules = <Map<String, dynamic>>[];
|
||||
rules.add({
|
||||
"domains": "domain:api.hifast.biz",
|
||||
"outbound": "bypass"
|
||||
});
|
||||
rules.add({"domains": "domain:api.hifast.biz", "outbound": "bypass"});
|
||||
|
||||
final nodeDomains = _kr_collectNodeDomains();
|
||||
for (final d in nodeDomains) {
|
||||
rules.add({
|
||||
"domains": "domain:$d",
|
||||
"outbound": "bypass"
|
||||
});
|
||||
rules.add({"domains": "domain:$d", "outbound": "bypass"});
|
||||
}
|
||||
|
||||
KRLogUtil.kr_i('✅ 节点域名白名单数量: ${nodeDomains.length}', tag: 'SingBox');
|
||||
KRLogUtil.kr_i('✅ 节点域名白名单集合: ${jsonEncode(nodeDomains.toList())}', tag: 'SingBox');
|
||||
KRLogUtil.kr_i('✅ 节点域名白名单集合: ${jsonEncode(nodeDomains.toList())}',
|
||||
tag: 'SingBox');
|
||||
return rules;
|
||||
}
|
||||
|
||||
@@ -664,21 +704,25 @@ class KRSingBoxImp {
|
||||
|
||||
void addFromOutbound(Map<String, dynamic> o) {
|
||||
final server = o['server']?.toString();
|
||||
if (server != null && server.isNotEmpty && InternetAddress.tryParse(server) == null) {
|
||||
if (server != null &&
|
||||
server.isNotEmpty &&
|
||||
InternetAddress.tryParse(server) == null) {
|
||||
set.add(server.toLowerCase());
|
||||
}
|
||||
|
||||
final tls = o['tls'];
|
||||
if (tls is Map) {
|
||||
final sni = tls['server_name']?.toString();
|
||||
if (sni != null && sni.isNotEmpty && InternetAddress.tryParse(sni) == null) {
|
||||
if (sni != null &&
|
||||
sni.isNotEmpty &&
|
||||
InternetAddress.tryParse(sni) == null) {
|
||||
set.add(sni.toLowerCase());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (final g in kr_outbounds) {
|
||||
addFromOutbound(g);
|
||||
addFromOutbound(g);
|
||||
}
|
||||
|
||||
return set;
|
||||
@@ -706,7 +750,7 @@ class KRSingBoxImp {
|
||||
|
||||
_kr_subscriptions.add(
|
||||
kr_singBox.watchStatus().listen(
|
||||
(status) {
|
||||
(status) {
|
||||
if (kDebugMode) {
|
||||
print('🔵 收到 Native 状态更新: ${status.runtimeType}');
|
||||
}
|
||||
@@ -744,7 +788,7 @@ class KRSingBoxImp {
|
||||
// 所以外层必须有 try-catch
|
||||
final stream = kr_singBox.watchStats();
|
||||
final subscription = stream.listen(
|
||||
(stats) {
|
||||
(stats) {
|
||||
kr_stats.value = stats;
|
||||
},
|
||||
onError: (error) {
|
||||
@@ -769,7 +813,7 @@ class KRSingBoxImp {
|
||||
|
||||
_kr_subscriptions.add(
|
||||
kr_singBox.watchActiveGroups().listen(
|
||||
(groups) {
|
||||
(groups) {
|
||||
print('[watchActiveGroups] 📡 收到活动组更新,数量: ${groups.length}');
|
||||
KRLogUtil.kr_i('📡 收到活动组更新,数量: ${groups.length}', tag: 'SingBox');
|
||||
kr_activeGroups.value = groups;
|
||||
@@ -777,10 +821,14 @@ class KRSingBoxImp {
|
||||
// 详细打印每个组的信息
|
||||
for (int i = 0; i < groups.length; i++) {
|
||||
final group = groups[i];
|
||||
KRLogUtil.kr_i('📋 活动组[$i]: tag=${group.tag}, type=${group.type}, selected=${group.selected}', tag: 'SingBox');
|
||||
KRLogUtil.kr_i(
|
||||
'📋 活动组[$i]: tag=${group.tag}, type=${group.type}, selected=${group.selected}',
|
||||
tag: 'SingBox');
|
||||
for (int j = 0; j < group.items.length; j++) {
|
||||
final item = group.items[j];
|
||||
KRLogUtil.kr_i(' └─ 节点[$j]: tag=${item.tag}, type=${item.type}, delay=${item.urlTestDelay}', tag: 'SingBox');
|
||||
KRLogUtil.kr_i(
|
||||
' └─ 节点[$j]: tag=${item.tag}, type=${item.type}, delay=${item.urlTestDelay}',
|
||||
tag: 'SingBox');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -796,13 +844,14 @@ class KRSingBoxImp {
|
||||
|
||||
_kr_subscriptions.add(
|
||||
kr_singBox.watchGroups().listen(
|
||||
(groups) {
|
||||
(groups) {
|
||||
print('[watchGroups] 📡 收到所有组更新,数量: ${groups.length}');
|
||||
kr_allGroups.value = groups;
|
||||
// 打印每个组的基本信息
|
||||
for (int i = 0; i < groups.length; i++) {
|
||||
final group = groups[i];
|
||||
print('[watchGroups] 组[$i]: tag=${group.tag}, type=${group.type}, selected=${group.selected}');
|
||||
print(
|
||||
'[watchGroups] 组[$i]: tag=${group.tag}, type=${group.type}, selected=${group.selected}');
|
||||
}
|
||||
},
|
||||
onError: (error) {
|
||||
@@ -812,7 +861,8 @@ class KRSingBoxImp {
|
||||
cancelOnError: false,
|
||||
),
|
||||
);
|
||||
print('[_kr_subscribeToGroups] ✅ 分组数据流订阅完成,当前订阅数: ${_kr_subscriptions.length}');
|
||||
print(
|
||||
'[_kr_subscribeToGroups] ✅ 分组数据流订阅完成,当前订阅数: ${_kr_subscriptions.length}');
|
||||
}
|
||||
|
||||
/// 验证节点选择是否生效
|
||||
@@ -824,7 +874,7 @@ class KRSingBoxImp {
|
||||
|
||||
// 查找 "select" 组
|
||||
final selectGroup = kr_activeGroups.firstWhere(
|
||||
(group) => group.tag == 'select',
|
||||
(group) => group.tag == 'select',
|
||||
orElse: () => throw Exception('未找到 "select" 选择器组'),
|
||||
);
|
||||
|
||||
@@ -853,7 +903,8 @@ class KRSingBoxImp {
|
||||
throw Exception('节点切换失败:实际选中 ${selectGroup.selected},期望 $targetTag');
|
||||
}
|
||||
|
||||
KRLogUtil.kr_i('✅ 节点切换验证成功: ${selectGroup.selected} == $targetTag', tag: 'SingBox');
|
||||
KRLogUtil.kr_i('✅ 节点切换验证成功: ${selectGroup.selected} == $targetTag',
|
||||
tag: 'SingBox');
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_e('❌ 节点验证异常: $e', tag: 'SingBox');
|
||||
// 不抛出异常,只记录日志,避免阻塞流程
|
||||
@@ -864,11 +915,11 @@ class KRSingBoxImp {
|
||||
///
|
||||
/// 确保 command.sock 准备好后再执行节点选择
|
||||
Future<void> _kr_selectOutboundWithRetry(
|
||||
String groupTag,
|
||||
String outboundTag, {
|
||||
int maxAttempts = 3,
|
||||
int initialDelay = 100,
|
||||
}) async {
|
||||
String groupTag,
|
||||
String outboundTag, {
|
||||
int maxAttempts = 3,
|
||||
int initialDelay = 100,
|
||||
}) async {
|
||||
int attempt = 0;
|
||||
int delay = initialDelay;
|
||||
|
||||
@@ -931,7 +982,8 @@ class KRSingBoxImp {
|
||||
|
||||
try {
|
||||
// 先验证 command.sock 是否可访问
|
||||
final socketFile = File(p.join(kr_configDics.baseDir.path, 'command.sock'));
|
||||
final socketFile =
|
||||
File(p.join(kr_configDics.baseDir.path, 'command.sock'));
|
||||
KRLogUtil.kr_i('🔍 检查 socket 文件: ${socketFile.path}', tag: 'SingBox');
|
||||
|
||||
if (!socketFile.existsSync()) {
|
||||
@@ -949,7 +1001,7 @@ class KRSingBoxImp {
|
||||
// 这样可以避免阻塞当前的异步执行
|
||||
try {
|
||||
KRLogUtil.kr_i('📊 订阅统计数据流...', tag: 'SingBox');
|
||||
await Future.delayed(Duration.zero); // 让出 UI 线程
|
||||
await Future.delayed(Duration.zero); // 让出 UI 线程
|
||||
_kr_subscribeToStats();
|
||||
statsSubscribed = true;
|
||||
KRLogUtil.kr_i('✅ 统计数据流订阅成功', tag: 'SingBox');
|
||||
@@ -959,7 +1011,7 @@ class KRSingBoxImp {
|
||||
|
||||
try {
|
||||
KRLogUtil.kr_i('📋 订阅分组数据流...', tag: 'SingBox');
|
||||
await Future.delayed(Duration.zero); // 让出 UI 线程
|
||||
await Future.delayed(Duration.zero); // 让出 UI 线程
|
||||
_kr_subscribeToGroups();
|
||||
groupsSubscribed = true;
|
||||
KRLogUtil.kr_i('✅ 分组数据流订阅成功', tag: 'SingBox');
|
||||
@@ -981,7 +1033,8 @@ class KRSingBoxImp {
|
||||
throw Exception('订阅列表为空,command client 未成功连接');
|
||||
}
|
||||
|
||||
KRLogUtil.kr_i('✅ 命令客户端初始化成功,活跃订阅数: ${_kr_subscriptions.length}', tag: 'SingBox');
|
||||
KRLogUtil.kr_i('✅ 命令客户端初始化成功,活跃订阅数: ${_kr_subscriptions.length}',
|
||||
tag: 'SingBox');
|
||||
return;
|
||||
} catch (e, stackTrace) {
|
||||
// 详细记录失败原因和堆栈信息
|
||||
@@ -1024,7 +1077,8 @@ class KRSingBoxImp {
|
||||
await Future.delayed(const Duration(milliseconds: 1500));
|
||||
|
||||
// 检查 socket 文件
|
||||
final socketFile = File(p.join(kr_configDics.baseDir.path, 'command.sock'));
|
||||
final socketFile =
|
||||
File(p.join(kr_configDics.baseDir.path, 'command.sock'));
|
||||
if (!socketFile.existsSync()) {
|
||||
KRLogUtil.kr_w('⚠️ command.sock 尚未创建,预连接取消', tag: 'SingBox');
|
||||
return;
|
||||
@@ -1047,7 +1101,6 @@ class KRSingBoxImp {
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_w('⚠️ 分组流预订阅失败(正常,UI 调用时会重试): $e', tag: 'SingBox');
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
// 静默失败,不影响主流程
|
||||
KRLogUtil.kr_w('⚠️ 后台预连接任务失败(不影响正常使用): $e', tag: 'SingBox');
|
||||
@@ -1061,7 +1114,8 @@ class KRSingBoxImp {
|
||||
Future<void> _kr_ensureCommandClientInitialized() async {
|
||||
// 如果已经有订阅,说明 command client 已初始化
|
||||
if (_kr_subscriptions.isNotEmpty) {
|
||||
KRLogUtil.kr_i('✅ Command client 已初始化(订阅数: ${_kr_subscriptions.length})', tag: 'SingBox');
|
||||
KRLogUtil.kr_i('✅ Command client 已初始化(订阅数: ${_kr_subscriptions.length})',
|
||||
tag: 'SingBox');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1096,7 +1150,8 @@ class KRSingBoxImp {
|
||||
// 等待 command client 初始化
|
||||
await Future.delayed(const Duration(milliseconds: 2000));
|
||||
|
||||
final savedNode = await KRSecureStorage().kr_readData(key: _keySelectedNode);
|
||||
final savedNode =
|
||||
await KRSecureStorage().kr_readData(key: _keySelectedNode);
|
||||
if (savedNode != null && savedNode.isNotEmpty && savedNode != 'auto') {
|
||||
KRLogUtil.kr_i('🔄 恢复用户选择的节点: $savedNode', tag: 'SingBox');
|
||||
|
||||
@@ -1104,7 +1159,8 @@ class KRSingBoxImp {
|
||||
await _kr_selectOutboundWithRetry("select", savedNode);
|
||||
KRLogUtil.kr_i('✅ 节点恢复完成', tag: 'SingBox');
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_w('⚠️ 节点恢复失败(可能 command client 未就绪): $e', tag: 'SingBox');
|
||||
KRLogUtil.kr_w('⚠️ 节点恢复失败(可能 command client 未就绪): $e',
|
||||
tag: 'SingBox');
|
||||
}
|
||||
} else {
|
||||
KRLogUtil.kr_i('ℹ️ 使用默认节点选择 (auto)', tag: 'SingBox');
|
||||
@@ -1151,7 +1207,8 @@ class KRSingBoxImp {
|
||||
KRLogUtil.kr_i(' - type: ${outbound['type']}', tag: 'SingBox');
|
||||
KRLogUtil.kr_i(' - tag: ${outbound['tag']}', tag: 'SingBox');
|
||||
KRLogUtil.kr_i(' - server: ${outbound['server']}', tag: 'SingBox');
|
||||
KRLogUtil.kr_i(' - server_port: ${outbound['server_port']}', tag: 'SingBox');
|
||||
KRLogUtil.kr_i(' - server_port: ${outbound['server_port']}',
|
||||
tag: 'SingBox');
|
||||
if (outbound['method'] != null) {
|
||||
KRLogUtil.kr_i(' - method: ${outbound['method']}', tag: 'SingBox');
|
||||
}
|
||||
@@ -1159,10 +1216,14 @@ class KRSingBoxImp {
|
||||
KRLogUtil.kr_i(' - interval: ${outbound['interval']}', tag: 'SingBox');
|
||||
}
|
||||
if (outbound['password'] != null) {
|
||||
KRLogUtil.kr_i(' - password: ${outbound['password']?.toString().substring(0, 8)}...', tag: 'SingBox');
|
||||
KRLogUtil.kr_i(
|
||||
' - password: ${outbound['password']?.toString().substring(0, 8)}...',
|
||||
tag: 'SingBox');
|
||||
}
|
||||
if (outbound['uuid'] != null) {
|
||||
KRLogUtil.kr_i(' - uuid: ${outbound['uuid']?.toString().substring(0, 8)}...', tag: 'SingBox');
|
||||
KRLogUtil.kr_i(
|
||||
' - uuid: ${outbound['uuid']?.toString().substring(0, 8)}...',
|
||||
tag: 'SingBox');
|
||||
}
|
||||
KRLogUtil.kr_i(' - 完整配置: ${jsonEncode(outbound)}', tag: 'SingBox');
|
||||
}
|
||||
@@ -1171,22 +1232,21 @@ class KRSingBoxImp {
|
||||
kr_outbounds = outbounds.where((outbound) {
|
||||
final type = outbound['type'];
|
||||
if (type == 'hysteria2' || type == 'hysteria') {
|
||||
KRLogUtil.kr_w('⚠️ 跳过 Hysteria2 节点: ${outbound['tag']} (libcore bug)', tag: 'SingBox');
|
||||
KRLogUtil.kr_w('⚠️ 跳过 Hysteria2 节点: ${outbound['tag']} (libcore bug)',
|
||||
tag: 'SingBox');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}).toList();
|
||||
|
||||
KRLogUtil.kr_i('✅ 过滤后节点数量: ${kr_outbounds.length}/${outbounds.length}', tag: 'SingBox');
|
||||
KRLogUtil.kr_i('✅ 过滤后节点数量: ${kr_outbounds.length}/${outbounds.length}',
|
||||
tag: 'SingBox');
|
||||
|
||||
// 🔧 修复:生成完整的 SingBox 配置
|
||||
// 之前只保存 {"outbounds": [...]}, 导致 libcore 无法正确处理
|
||||
// 现在保存完整配置,包含所有必需字段
|
||||
final Map<String, dynamic> fullConfig = {
|
||||
"log": {
|
||||
"level": "debug",
|
||||
"timestamp": true
|
||||
},
|
||||
"log": {"level": "debug", "timestamp": true},
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
@@ -1194,13 +1254,9 @@ class KRSingBoxImp {
|
||||
"address": "https://1.1.1.1/dns-query",
|
||||
"address_resolver": "dns-direct"
|
||||
},
|
||||
{
|
||||
"tag": "dns-direct",
|
||||
"address": "local",
|
||||
"detour": "direct"
|
||||
}
|
||||
{"tag": "dns-direct", "address": "local", "detour": "direct"}
|
||||
],
|
||||
"rules": _kr_buildDnsRules(), // ✅ 使用动态构建的 DNS 规则
|
||||
"rules": _kr_buildDnsRules(), // ✅ 使用动态构建的 DNS 规则
|
||||
"final": "dns-remote",
|
||||
"strategy": "prefer_ipv4"
|
||||
},
|
||||
@@ -1222,26 +1278,18 @@ class KRSingBoxImp {
|
||||
"type": "selector",
|
||||
"tag": "proxy",
|
||||
"outbounds": kr_outbounds.map((o) => o['tag'] as String).toList(),
|
||||
"default": kr_outbounds.isNotEmpty ? kr_outbounds[0]['tag'] : "direct",
|
||||
"default":
|
||||
kr_outbounds.isNotEmpty ? kr_outbounds[0]['tag'] : "direct",
|
||||
},
|
||||
...kr_outbounds,
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct"
|
||||
},
|
||||
{
|
||||
"type": "block",
|
||||
"tag": "block"
|
||||
},
|
||||
{
|
||||
"type": "dns",
|
||||
"tag": "dns-out"
|
||||
}
|
||||
{"type": "direct", "tag": "direct"},
|
||||
{"type": "block", "tag": "block"},
|
||||
{"type": "dns", "tag": "dns-out"}
|
||||
],
|
||||
"route": {
|
||||
"rules": _kr_buildRouteRules(), // ✅ 使用动态构建的路由规则
|
||||
"rule_set": _kr_buildRuleSets(), // ✅ 使用动态构建的规则集
|
||||
"final": "proxy", // 🔧 修复:使用 selector 组作为默认出站
|
||||
"rules": _kr_buildRouteRules(), // ✅ 使用动态构建的路由规则
|
||||
"rule_set": _kr_buildRuleSets(), // ✅ 使用动态构建的规则集
|
||||
"final": "proxy", // 🔧 修复:使用 selector 组作为默认出站
|
||||
"auto_detect_interface": true
|
||||
}
|
||||
};
|
||||
@@ -1252,7 +1300,9 @@ class KRSingBoxImp {
|
||||
|
||||
KRLogUtil.kr_i('📄 完整配置文件长度: ${mapStr.length}', tag: 'SingBox');
|
||||
KRLogUtil.kr_i('📄 Outbounds 数量: ${kr_outbounds.length}', tag: 'SingBox');
|
||||
KRLogUtil.kr_i('📄 配置前800字符:\n${mapStr.substring(0, mapStr.length > 800 ? 800 : mapStr.length)}', tag: 'SingBox');
|
||||
KRLogUtil.kr_i(
|
||||
'📄 配置前800字符:\n${mapStr.substring(0, mapStr.length > 800 ? 800 : mapStr.length)}',
|
||||
tag: 'SingBox');
|
||||
|
||||
await file.writeAsString(mapStr);
|
||||
await temp.writeAsString(mapStr);
|
||||
@@ -1276,7 +1326,8 @@ class KRSingBoxImp {
|
||||
|
||||
// 🔧 关键修复:只有在"智能代理"模式下,才根据国家添加直连规则
|
||||
// 如果是"全局代理"模式,即使选择了国家也不添加直连规则
|
||||
if (kr_connectionType.value == KRConnectionType.rule && currentCountryCode != 'other') {
|
||||
if (kr_connectionType.value == KRConnectionType.rule &&
|
||||
currentCountryCode != 'other') {
|
||||
rules.add({
|
||||
"rule_set": [
|
||||
"geoip-$currentCountryCode",
|
||||
@@ -1285,7 +1336,8 @@ class KRSingBoxImp {
|
||||
"server": "dns-direct"
|
||||
});
|
||||
|
||||
KRLogUtil.kr_i('✅ [智能代理模式] 添加 DNS 规则: $currentCountryCode 域名使用直连 DNS', tag: 'SingBox');
|
||||
KRLogUtil.kr_i('✅ [智能代理模式] 添加 DNS 规则: $currentCountryCode 域名使用直连 DNS',
|
||||
tag: 'SingBox');
|
||||
} else if (kr_connectionType.value == KRConnectionType.global) {
|
||||
KRLogUtil.kr_i('🌐 [全局代理模式] 跳过国家 DNS 规则,所有DNS查询走代理', tag: 'SingBox');
|
||||
}
|
||||
@@ -1299,10 +1351,7 @@ class KRSingBoxImp {
|
||||
final rules = <Map<String, dynamic>>[];
|
||||
|
||||
// 基础规则: DNS 查询走 dns-out
|
||||
rules.add({
|
||||
"protocol": "dns",
|
||||
"outbound": "dns-out"
|
||||
});
|
||||
rules.add({"protocol": "dns", "outbound": "dns-out"});
|
||||
|
||||
// ✅ 自定义域名直连规则(优先级高,放在前面)
|
||||
// rules.add({
|
||||
@@ -1314,7 +1363,8 @@ class KRSingBoxImp {
|
||||
|
||||
// 🔧 关键修复:只有在"智能代理"模式下,才根据国家添加直连规则
|
||||
// 如果是"全局代理"模式,即使选择了国家也不添加直连规则
|
||||
if (kr_connectionType.value == KRConnectionType.rule && currentCountryCode != 'other') {
|
||||
if (kr_connectionType.value == KRConnectionType.rule &&
|
||||
currentCountryCode != 'other') {
|
||||
rules.add({
|
||||
"rule_set": [
|
||||
"geoip-$currentCountryCode",
|
||||
@@ -1323,7 +1373,8 @@ class KRSingBoxImp {
|
||||
"outbound": "direct"
|
||||
});
|
||||
|
||||
KRLogUtil.kr_i('✅ [智能代理模式] 添加路由规则: $currentCountryCode IP/域名直连', tag: 'SingBox');
|
||||
KRLogUtil.kr_i('✅ [智能代理模式] 添加路由规则: $currentCountryCode IP/域名直连',
|
||||
tag: 'SingBox');
|
||||
} else if (kr_connectionType.value == KRConnectionType.global) {
|
||||
KRLogUtil.kr_i('🌐 [全局代理模式] 跳过国家路由规则,所有流量走代理', tag: 'SingBox');
|
||||
}
|
||||
@@ -1338,10 +1389,13 @@ class KRSingBoxImp {
|
||||
|
||||
// 🔧 关键修复:只有在"智能代理"模式下,才加载国家规则集
|
||||
// 如果是"全局代理"模式,即使选择了国家也不加载规则集
|
||||
if (kr_connectionType.value == KRConnectionType.rule && currentCountryCode != 'other') {
|
||||
if (kr_connectionType.value == KRConnectionType.rule &&
|
||||
currentCountryCode != 'other') {
|
||||
// 检查本地文件是否存在
|
||||
final geoipFile = File(p.join(kr_configDics.workingDir.path, 'geosite', 'geoip-$currentCountryCode.srs'));
|
||||
final geositeFile = File(p.join(kr_configDics.workingDir.path, 'geosite', 'geosite-$currentCountryCode.srs'));
|
||||
final geoipFile = File(p.join(kr_configDics.workingDir.path, 'geosite',
|
||||
'geoip-$currentCountryCode.srs'));
|
||||
final geositeFile = File(p.join(kr_configDics.workingDir.path, 'geosite',
|
||||
'geosite-$currentCountryCode.srs'));
|
||||
|
||||
if (geoipFile.existsSync() && geositeFile.existsSync()) {
|
||||
// ✅ 使用本地文件
|
||||
@@ -1349,7 +1403,7 @@ class KRSingBoxImp {
|
||||
"type": "local",
|
||||
"tag": "geoip-$currentCountryCode",
|
||||
"format": "binary",
|
||||
"path": "./geosite/geoip-$currentCountryCode.srs" // 相对于 workingDir
|
||||
"path": "./geosite/geoip-$currentCountryCode.srs" // 相对于 workingDir
|
||||
});
|
||||
|
||||
ruleSets.add({
|
||||
@@ -1360,19 +1414,25 @@ class KRSingBoxImp {
|
||||
});
|
||||
|
||||
KRLogUtil.kr_i('✅ 使用本地规则集: $currentCountryCode', tag: 'SingBox');
|
||||
KRLogUtil.kr_i(' - geoip: ./geosite/geoip-$currentCountryCode.srs', tag: 'SingBox');
|
||||
KRLogUtil.kr_i(' - geosite: ./geosite/geosite-$currentCountryCode.srs', tag: 'SingBox');
|
||||
KRLogUtil.kr_i(' - geoip: ./geosite/geoip-$currentCountryCode.srs',
|
||||
tag: 'SingBox');
|
||||
KRLogUtil.kr_i(
|
||||
' - geosite: ./geosite/geosite-$currentCountryCode.srs',
|
||||
tag: 'SingBox');
|
||||
} else {
|
||||
// ❌ 本地文件不存在,使用远程规则集作为后备
|
||||
KRLogUtil.kr_w('⚠️ 本地规则集不存在,使用远程规则集', tag: 'SingBox');
|
||||
KRLogUtil.kr_w(' - geoip 文件存在: ${geoipFile.existsSync()}', tag: 'SingBox');
|
||||
KRLogUtil.kr_w(' - geosite 文件存在: ${geositeFile.existsSync()}', tag: 'SingBox');
|
||||
KRLogUtil.kr_w(' - geoip 文件存在: ${geoipFile.existsSync()}',
|
||||
tag: 'SingBox');
|
||||
KRLogUtil.kr_w(' - geosite 文件存在: ${geositeFile.existsSync()}',
|
||||
tag: 'SingBox');
|
||||
|
||||
ruleSets.add({
|
||||
"type": "remote",
|
||||
"tag": "geoip-$currentCountryCode",
|
||||
"format": "binary",
|
||||
"url": "https://raw.githubusercontent.com/hiddify/hiddify-geo/rule-set/country/geoip-$currentCountryCode.srs",
|
||||
"url":
|
||||
"https://raw.githubusercontent.com/hiddify/hiddify-geo/rule-set/country/geoip-$currentCountryCode.srs",
|
||||
"download_detour": "direct",
|
||||
"update_interval": "7d"
|
||||
});
|
||||
@@ -1381,7 +1441,8 @@ class KRSingBoxImp {
|
||||
"type": "remote",
|
||||
"tag": "geosite-$currentCountryCode",
|
||||
"format": "binary",
|
||||
"url": "https://raw.githubusercontent.com/hiddify/hiddify-geo/rule-set/country/geosite-$currentCountryCode.srs",
|
||||
"url":
|
||||
"https://raw.githubusercontent.com/hiddify/hiddify-geo/rule-set/country/geosite-$currentCountryCode.srs",
|
||||
"download_detour": "direct",
|
||||
"update_interval": "7d"
|
||||
});
|
||||
@@ -1418,7 +1479,8 @@ class KRSingBoxImp {
|
||||
if (Platform.isWindows && !_dnsBackedUp) {
|
||||
KRLogUtil.kr_i('🪟 Windows 平台,首次启动备份 DNS 设置...', tag: 'SingBox');
|
||||
try {
|
||||
final backupSuccess = await KRWindowsDnsUtil.instance.kr_backupDnsSettings();
|
||||
final backupSuccess =
|
||||
await KRWindowsDnsUtil.instance.kr_backupDnsSettings();
|
||||
if (backupSuccess) {
|
||||
_dnsBackedUp = true; // 标记已备份
|
||||
KRLogUtil.kr_i('✅ Windows DNS 备份成功', tag: 'SingBox');
|
||||
@@ -1449,11 +1511,11 @@ class KRSingBoxImp {
|
||||
final oOption = SingboxConfigOption.fromJson(_getConfigOption());
|
||||
final changeResult = await kr_singBox.changeOptions(oOption).run();
|
||||
changeResult.match(
|
||||
(error) {
|
||||
(error) {
|
||||
KRLogUtil.kr_e('❌ changeOptions() 失败: $error', tag: 'SingBox');
|
||||
throw Exception('初始化 HiddifyOptions 失败: $error');
|
||||
},
|
||||
(_) {
|
||||
(_) {
|
||||
KRLogUtil.kr_i('✅ HiddifyOptions 初始化成功', tag: 'SingBox');
|
||||
},
|
||||
);
|
||||
@@ -1463,7 +1525,9 @@ class KRSingBoxImp {
|
||||
if (await configFile.exists()) {
|
||||
final configContent = await configFile.readAsString();
|
||||
KRLogUtil.kr_i('📄 配置文件内容长度: ${configContent.length}', tag: 'SingBox');
|
||||
KRLogUtil.kr_i('📄 配置文件前500字符: ${configContent.substring(0, configContent.length > 500 ? 500 : configContent.length)}', tag: 'SingBox');
|
||||
KRLogUtil.kr_i(
|
||||
'📄 配置文件前500字符: ${configContent.substring(0, configContent.length > 500 ? 500 : configContent.length)}',
|
||||
tag: 'SingBox');
|
||||
} else {
|
||||
KRLogUtil.kr_w('⚠️ 配置文件不存在: $_cutPath', tag: 'SingBox');
|
||||
}
|
||||
@@ -1472,7 +1536,7 @@ class KRSingBoxImp {
|
||||
_kr_subscribeToStatus();
|
||||
|
||||
await kr_singBox.start(_cutPath, kr_configName, false).map(
|
||||
(r) {
|
||||
(r) {
|
||||
KRLogUtil.kr_i('✅ SingBox 启动成功', tag: 'SingBox');
|
||||
},
|
||||
).mapLeft((err) {
|
||||
@@ -1528,7 +1592,8 @@ class KRSingBoxImp {
|
||||
|
||||
// 🔧 关键修复:恢复用户选择的节点
|
||||
try {
|
||||
final selectedNode = await KRSecureStorage().kr_readData(key: _keySelectedNode);
|
||||
final selectedNode =
|
||||
await KRSecureStorage().kr_readData(key: _keySelectedNode);
|
||||
if (selectedNode != null && selectedNode.isNotEmpty) {
|
||||
KRLogUtil.kr_i('🎯 恢复用户选择的节点: $selectedNode', tag: 'SingBox');
|
||||
if (kDebugMode) {
|
||||
@@ -1588,7 +1653,7 @@ class KRSingBoxImp {
|
||||
// 🔧 延长超时时间到 10 秒,给 Windows DNS 清理足够时间
|
||||
try {
|
||||
await kr_singBox.stop().run().timeout(
|
||||
const Duration(seconds: 10), // ← 从 3 秒延长到 10 秒
|
||||
const Duration(seconds: 10), // ← 从 3 秒延长到 10 秒
|
||||
onTimeout: () {
|
||||
KRLogUtil.kr_w('⚠️ 停止操作超时(10秒),强制继续', tag: 'SingBox');
|
||||
return const Left('timeout');
|
||||
@@ -1638,7 +1703,8 @@ class KRSingBoxImp {
|
||||
|
||||
try {
|
||||
// 尝试恢复 DNS
|
||||
final restoreSuccess = await KRWindowsDnsUtil.instance.kr_restoreDnsSettings();
|
||||
final restoreSuccess =
|
||||
await KRWindowsDnsUtil.instance.kr_restoreDnsSettings();
|
||||
if (restoreSuccess) {
|
||||
KRLogUtil.kr_i('✅ Windows DNS 恢复成功', tag: 'SingBox');
|
||||
} else {
|
||||
@@ -1765,9 +1831,9 @@ class KRSingBoxImp {
|
||||
mode = KRCountryUtil.kr_getCurrentCountryCode();
|
||||
KRLogUtil.kr_i('🎯 切换到规则代理模式: $mode', tag: 'SingBox');
|
||||
break;
|
||||
// case KRConnectionType.direct:
|
||||
// mode = "direct";
|
||||
// break;
|
||||
// case KRConnectionType.direct:
|
||||
// mode = "direct";
|
||||
// break;
|
||||
}
|
||||
oOption["region"] = mode;
|
||||
KRLogUtil.kr_i('📝 更新 region 配置: $mode', tag: 'SingBox');
|
||||
@@ -1842,6 +1908,25 @@ class KRSingBoxImp {
|
||||
KRLogUtil.kr_i('🎯 [v2.1] 开始选择出站节点: $tag', tag: 'SingBox');
|
||||
KRLogUtil.kr_i('📊 当前活动组数量: ${kr_activeGroups.length}', tag: 'SingBox');
|
||||
|
||||
// 如果活动组尚未就绪,等待一段时间;仍为空则仅保存选择,稍后恢复
|
||||
if (kr_activeGroups.isEmpty) {
|
||||
for (int i = 0; i < 20; i++) {
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
if (kr_activeGroups.isNotEmpty) break;
|
||||
}
|
||||
if (kr_activeGroups.isEmpty) {
|
||||
KRLogUtil.kr_w('⚠️ 活动组为空,跳过即时切换,仅保存选择: $tag', tag: 'SingBox');
|
||||
try {
|
||||
await KRSecureStorage()
|
||||
.kr_saveData(key: _keySelectedNode, value: tag);
|
||||
KRLogUtil.kr_i('✅ 节点选择已保存: $tag', tag: 'SingBox');
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_e('❌ 保存节点选择失败: $e', tag: 'SingBox');
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 🔧 诊断:打印所有活动组的节点,确保目标节点存在
|
||||
KRLogUtil.kr_i('🔍 搜索目标节点 "$tag" 在活动组中...', tag: 'SingBox');
|
||||
bool foundNode = false;
|
||||
@@ -1880,9 +1965,12 @@ class KRSingBoxImp {
|
||||
|
||||
// 🔧 关键修复:使用正确的 group tag
|
||||
// libcore 生成的selector组的tag是"proxy"而不是"select"
|
||||
final selectorGroupTag = kr_activeGroups.any((g) => g.tag == 'select') ? 'select' : 'proxy';
|
||||
KRLogUtil.kr_i('⏳ 调用 selectOutbound("$selectorGroupTag", "$tag")...', tag: 'SingBox');
|
||||
await _kr_selectOutboundWithRetry(selectorGroupTag, tag, maxAttempts: 3, initialDelay: 50);
|
||||
final selectorGroupTag =
|
||||
kr_activeGroups.any((g) => g.tag == 'select') ? 'select' : 'proxy';
|
||||
KRLogUtil.kr_i('⏳ 调用 selectOutbound("$selectorGroupTag", "$tag")...',
|
||||
tag: 'SingBox');
|
||||
await _kr_selectOutboundWithRetry(selectorGroupTag, tag,
|
||||
maxAttempts: 3, initialDelay: 50);
|
||||
KRLogUtil.kr_i('✅ 节点切换API调用完成: $tag', tag: 'SingBox');
|
||||
|
||||
// 🔧 新增:验证节点切换是否生效
|
||||
@@ -1900,13 +1988,14 @@ class KRSingBoxImp {
|
||||
_nodeSelectionTimer?.cancel();
|
||||
if (tag != 'auto') {
|
||||
KRLogUtil.kr_i('🔁 启动节点选择监控,防止被 auto 覆盖', tag: 'SingBox');
|
||||
_nodeSelectionTimer = Timer.periodic(const Duration(seconds: 20), (timer) {
|
||||
_nodeSelectionTimer =
|
||||
Timer.periodic(const Duration(seconds: 20), (timer) {
|
||||
// 每 20 秒重新选择一次,确保用户选择不被覆盖
|
||||
// 使用 then/catchError 避免异常导致 UI 阻塞
|
||||
kr_singBox.selectOutbound("select", tag).run().then((result) {
|
||||
result.match(
|
||||
(error) => KRLogUtil.kr_w('🔁 定时器重选节点失败: $error', tag: 'SingBox'),
|
||||
(_) => KRLogUtil.kr_d('🔁 定时器重选节点成功', tag: 'SingBox'),
|
||||
(error) => KRLogUtil.kr_w('🔁 定时器重选节点失败: $error', tag: 'SingBox'),
|
||||
(_) => KRLogUtil.kr_d('🔁 定时器重选节点成功', tag: 'SingBox'),
|
||||
);
|
||||
}).catchError((error) {
|
||||
KRLogUtil.kr_w('🔁 定时器重选节点异常: $error', tag: 'SingBox');
|
||||
@@ -1935,10 +2024,14 @@ class KRSingBoxImp {
|
||||
// 打印所有活动组信息
|
||||
for (int i = 0; i < kr_activeGroups.length; i++) {
|
||||
final group = kr_activeGroups[i];
|
||||
KRLogUtil.kr_i('📋 活动组[$i]: tag=${group.tag}, type=${group.type}, selected=${group.selected}', tag: 'SingBox');
|
||||
KRLogUtil.kr_i(
|
||||
'📋 活动组[$i]: tag=${group.tag}, type=${group.type}, selected=${group.selected}',
|
||||
tag: 'SingBox');
|
||||
for (int j = 0; j < group.items.length; j++) {
|
||||
final item = group.items[j];
|
||||
KRLogUtil.kr_i(' └─ 节点[$j]: tag=${item.tag}, type=${item.type}, delay=${item.urlTestDelay}', tag: 'SingBox');
|
||||
KRLogUtil.kr_i(
|
||||
' └─ 节点[$j]: tag=${item.tag}, type=${item.type}, delay=${item.urlTestDelay}',
|
||||
tag: 'SingBox');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1954,10 +2047,14 @@ class KRSingBoxImp {
|
||||
KRLogUtil.kr_i('🔄 测试后活动组状态检查:', tag: 'SingBox');
|
||||
for (int i = 0; i < kr_activeGroups.length; i++) {
|
||||
final group = kr_activeGroups[i];
|
||||
KRLogUtil.kr_i('📋 活动组[$i]: tag=${group.tag}, type=${group.type}, selected=${group.selected}', tag: 'SingBox');
|
||||
KRLogUtil.kr_i(
|
||||
'📋 活动组[$i]: tag=${group.tag}, type=${group.type}, selected=${group.selected}',
|
||||
tag: 'SingBox');
|
||||
for (int j = 0; j < group.items.length; j++) {
|
||||
final item = group.items[j];
|
||||
KRLogUtil.kr_i(' └─ 节点[$j]: tag=${item.tag}, type=${item.type}, delay=${item.urlTestDelay}', tag: 'SingBox');
|
||||
KRLogUtil.kr_i(
|
||||
' └─ 节点[$j]: tag=${item.tag}, type=${item.type}, delay=${item.urlTestDelay}',
|
||||
tag: 'SingBox');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
Reference in New Issue
Block a user