Compare commits

40 Commits

Author SHA1 Message Date
sepakeloudest 4fdf4da4f3 feat: 节点测速auto调整 2025-11-26 22:57:52 -08:00
sepakeloudest b059d01556 feat: 节点测速调整 2025-11-26 19:23:15 -08:00
sepakeloudest 909020654f feat: 节点测速调整 2025-11-25 23:06:20 -08:00
sepakeloudest 1809c11473 feat: macos配置和ios配置 2025-11-25 22:17:04 -08:00
shanshanzhong147 343bb50c77 fix: 调整窗口最小尺寸为400x334以改善用户体验 2025-11-24 18:38:23 -08:00
shanshanzhong147 11afee1d6f fix: 调整窗口最小尺寸并更新版本号
将窗口最小尺寸从800x668调整为400x334以提升用户体验
移除版本号中的build后缀以简化版本管理
2025-11-24 18:22:37 -08:00
shanshanzhong147 96d1a107cf fix(windows打包): 修改安装程序任务选项默认勾选状态
将桌面图标和开机启动任务的默认勾选状态从unchecked改为checkedonce
2025-11-24 03:48:35 -08:00
shanshanzhong147 52f241a5a7 fix(打包): 修改安装程序默认不创建桌面图标和开机启动
将桌面图标和开机启动选项的默认状态从选中改为未选中,以提供更合理的默认安装体验
2025-11-24 03:34:25 -08:00
shanshanzhong147 5dfb2b7432 fix(打包): 禁用Inno Setup的UsePreviousTasks选项
防止安装程序继承之前安装的任务选择
2025-11-24 03:17:05 -08:00
shanshanzhong147 85dc810421 fix(windows打包): 修改桌面图标创建任务为仅首次安装时选中
将桌面图标创建任务的Flags从checked改为checkedonce,使其仅在首次安装时默认选中
2025-11-24 03:07:44 -08:00
shanshanzhong147 ee3fdd91fa fix(windows打包): 修改桌面图标创建任务默认选中状态
移除CurPageChanged过程中强制选中桌面图标的逻辑,改为直接在Tasks中设置默认选中
2025-11-24 02:52:16 -08:00
shanshanzhong147 e537775e71 fix(打包配置): 添加语言目录配置并修复桌面图标任务选择
在make_config.yaml中添加languages_dir配置项,用于指定语言文件目录
修改inno_setup.sas脚本,在任务选择页面自动勾选桌面图标选项
2025-11-24 02:36:03 -08:00
shanshanzhong147 f97a21be32 feat(打包配置): 添加中文语言支持并更新图标路径
添加简体中文语言支持(zh, zh-cn, zh_CN)到安装配置
更新应用图标路径为绝对路径
在安装脚本中添加语言检测方法设置
2025-11-24 01:39:58 -08:00
shanshanzhong147 d3f48e345e build(windows): 添加应用图标文件并更新安装配置
将应用图标文件添加到安装包中,并更新安装配置中的图标路径引用
2025-11-24 01:25:46 -08:00
shanshanzhong147 ac8a04fd24 merge: 合并 dev 分支到 main 并保留本地重命名 2025-11-24 01:02:00 -08:00
sepakeloudest becc03acfd feat: 更改节点测速
Build Windows / 编译 libcore (Windows) (20.15.1) (push) Successful in 20m53s
Build Windows / build (push) Has been cancelled
2025-11-21 02:52:23 -08:00
sepakeloudest 9123d5f0e4 feat: 登录接口修改
Build Windows / build (push) Has been cancelled
Build Windows / 编译 libcore (Windows) (20.15.1) (push) Successful in 20m33s
2025-11-21 01:24:44 -08:00
sepakeloudest 91185a593a Revert "feat: 修改win的配置"
Build Windows / 编译 libcore (Windows) (20.15.1) (push) Successful in 20m11s
Build Windows / build (push) Has been cancelled
This reverts commit d99f588a5b.
2025-11-20 19:22:45 -08:00
sepakeloudest d6386ecfa9 Revert "feat: ai修改win 名称信息"
This reverts commit bcb4d17236.
2025-11-20 19:21:17 -08:00
sepakeloudest 62d1af8517 feat: 修改接口方式
Build Windows / 编译 libcore (Windows) (20.15.1) (push) Successful in 20m26s
Build Windows / build (push) Has been cancelled
2025-11-20 18:11:29 -08:00
sepakeloudest 3b05d4ff72 feat: 修改国家选择匹配问题 2025-11-20 09:08:23 -08:00
sepakeloudest bcb4d17236 feat: ai修改win 名称信息 2025-11-20 09:07:40 -08:00
sepakeloudest d99f588a5b feat: 修改win的配置
Build Windows / 编译 libcore (Windows) (20.15.1) (push) Successful in 21m20s
Build Windows / build (push) Has been cancelled
2025-11-20 07:02:10 -08:00
sepakeloudest 691dd6e1d6 feat: 修改bug
Build Windows / 编译 libcore (Windows) (20.15.1) (push) Successful in 21m44s
Build Windows / build (push) Has been cancelled
2025-11-20 06:34:12 -08:00
sepakeloudest b442289b8a feat: 修改bug
Build Windows / build (push) Has been cancelled
Build Windows / 编译 libcore (Windows) (20.15.1) (push) Has started running
2025-11-20 06:19:05 -08:00
sepakeloudest 89d13e6237 feat: 修改兼容 2025-11-20 00:51:14 -08:00
sepakeloudest 8fd742a688 feat: 修改兼容 2025-11-19 04:06:46 -08:00
sepakeloudest 325a63d35f feat: 完成国家配置
Build Windows / 编译 libcore (Windows) (20.15.1) (push) Successful in 20m34s
Build Windows / build (push) Has been cancelled
2025-11-19 01:06:48 -08:00
sepakeloudest 679c303457 feat: 1.新增在 VPN 配置中添加了一条规则,2.修复全局代理无法使用 2025-11-18 05:15:06 -08:00
sepakeloudest 59aa67d456 feat: 提交国家分组功能和debug功能
Build Windows / 编译 libcore (Windows) (20.15.1) (push) Successful in 20m13s
Build Windows / build (push) Has been cancelled
2025-11-16 22:51:30 -08:00
sepakeloudest 2a1ee3018d feat: 提交国家分组功能和debug功能 2025-11-16 22:50:09 -08:00
sepakeloudest 76e83e0135 feat: 先改变节点选择方法
Build Windows / 编译 libcore (Windows) (20.15.1) (push) Successful in 20m11s
Build Windows / build (push) Has been cancelled
2025-11-14 20:19:10 -08:00
sepakeloudest 89d6ec3c76 feat: 先改变节点选择方法
Build Windows / build (push) Has been cancelled
Build Windows / 编译 libcore (Windows) (20.15.1) (push) Has been cancelled
2025-11-14 20:18:38 -08:00
sepakeloudest 670eb7ebc9 feat: 保存节点调试进度
Build Windows / 编译 libcore (Windows) (20.15.1) (push) Successful in 20m7s
Build Windows / build (push) Has been cancelled
2025-11-13 22:43:27 -08:00
sepakeloudest 46295f4543 feat: 修改windows构建
Build Windows / 编译 libcore (Windows) (20.15.1) (push) Successful in 29m17s
Build Windows / build (push) Successful in 7h8m10s
2025-11-13 05:24:35 -08:00
sepakeloudest a47174df56 feat: 初始化并发问题处理
Build Windows / 编译 libcore (Windows) (20.15.1) (push) Has been cancelled
Build Windows / build (push) Has been cancelled
2025-11-13 05:15:18 -08:00
sepakeloudest 748ec6bee9 feat: 提交国家选择
Build Windows / build (push) Successful in 7h7m43s
Build Windows / 编译 libcore (Windows) (20.15.1) (push) Successful in 20m1s
2025-11-11 01:29:16 -08:00
sepakeloudest a884e6c838 feat: 修改id和sn显示
Build Windows / build (push) Has been cancelled
Build Windows / 编译 libcore (Windows) (20.15.1) (push) Has been cancelled
2025-11-09 22:55:01 -08:00
sepakeloudest 920b86e56a feat: 修改代码冲突
Build Windows / build (push) Has been cancelled
Build Windows / 编译 libcore (Windows) (20.15.1) (push) Has been cancelled
2025-11-09 22:46:53 -08:00
sepakeloudest a78b40c339 feat: 修改代码冲突
Build Windows / build (push) Has been cancelled
Build Windows / 编译 libcore (Windows) (20.15.1) (push) Has been cancelled
2025-11-09 22:43:57 -08:00
51 changed files with 2521 additions and 1047 deletions
+3 -2
View File
@@ -1,3 +1,4 @@
[submodule "libcore"]
path = libcore
url = https://github.com/hiddify/hiddify-next-core
path = libcore
url = https://github.com/hiddify/hiddify-next-core
branch = main
+3 -3
View File
@@ -57,7 +57,7 @@ android {
defaultConfig {
applicationId "app.hifastvpn.com"
minSdkVersion flutter.minSdkVersion
targetSdkVersion 36
targetSdkVersion 34
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
multiDexEnabled true
@@ -105,8 +105,8 @@ android {
debugSymbolLevel 'FULL'
}
// 禁用代码混淆和资源压缩,解决VPN连接问题
minifyEnabled true
shrinkResources true
minifyEnabled false
shrinkResources false
// proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
+5
View File
@@ -34,6 +34,11 @@
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<!-- 如果 targetSdkVersion >= 33 -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application
android:name=".Application"
android:banner="@mipmap/ic_banner"
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

+2 -2
View File
@@ -193,7 +193,7 @@
"rewardDetails": "Reward Details >",
"steps": "Invitation Steps",
"inviteFriend": "Invite Friends",
"acceptInvite": "Friends accept invitation\nPlace order and register",
"acceptInvite": "Friends accept invitationPlace order and register",
"getReward": "Get Reward",
"shareLink": "Share Link",
"shareQR": "Share QR Code",
@@ -398,7 +398,7 @@
"content": "Would you like to update now?",
"updateNow": "Update Now",
"updateLater": "Later",
"defaultContent": "1. Optimize app performance\n2. Fix known issues\n3. Improve user experience"
"defaultContent": "1. Optimize app performance2. Fix known issues3. Improve user experience"
},
"country": {
"cn": "China",
+2 -2
View File
@@ -256,7 +256,7 @@
"rewardDetails": "Detalles de recompensa >",
"steps": "Pasos de invitación",
"inviteFriend": "Invitar amigo",
"acceptInvite": "El amigo acepta la invitación\ny se registra",
"acceptInvite": "El amigo acepta la invitacióny se registra",
"getReward": "Obtener recompensa",
"shareLink": "Compartir por enlace",
"shareQR": "Compartir por código QR",
@@ -404,7 +404,7 @@
"content": "¿Actualizar ahora?",
"updateNow": "Actualizar ahora",
"updateLater": "Más tarde",
"defaultContent": "1. Optimización del rendimiento de la aplicación\n2. Corrección de problemas conocidos\n3. Mejora de la experiencia del usuario"
"defaultContent": "1. Optimización del rendimiento de la aplicación2. Corrección de problemas conocidos3. Mejora de la experiencia del usuario"
},
"kr_invite": {
"close": "Cerrar",
+2 -2
View File
@@ -193,7 +193,7 @@
"rewardDetails": "Tasu üksikasjad >",
"steps": "Kutse Sammud",
"inviteFriend": "Kutsu Sõbrad",
"acceptInvite": "Sõbrad aktsepteerivad kutset\nTee tellimus ja registreeru",
"acceptInvite": "Sõbrad aktsepteerivad kutsetTee tellimus ja registreeru",
"getReward": "Saada Tasu",
"shareLink": "Jaga Linki",
"shareQR": "Jaga QR-koodi",
@@ -395,7 +395,7 @@
"content": "Uuendada nüüd?",
"updateNow": "Uuenda nüüd",
"updateLater": "Hiljem",
"defaultContent": "1. Rakenduse jõudluse optimeerimine\n2. Teadaolevate probleemide parandamine\n3. Kasutajamugavuse parandamine"
"defaultContent": "1. Rakenduse jõudluse optimeerimine2. Teadaolevate probleemide parandamine3. Kasutajamugavuse parandamine"
},
"country": {
"cn": "Hiina",
+2 -2
View File
@@ -209,7 +209,7 @@
"rewardDetails": "報酬の詳細 >",
"steps": "招待の手順",
"inviteFriend": "友達を招待",
"acceptInvite": "友達が招待を受け入れ\n注文して登録",
"acceptInvite": "友達が招待を受け入れ注文して登録",
"getReward": "報酬を獲得",
"shareLink": "リンクを共有",
"shareQR": "QRコードを共有",
@@ -412,7 +412,7 @@
"content": "今すぐアップデートしますか?",
"updateNow": "今すぐアップデート",
"updateLater": "後で",
"defaultContent": "1. アプリのパフォーマンス最適化\n2. 既知の問題の修正\n3. ユーザー体験の向上"
"defaultContent": "1. アプリのパフォーマンス最適化2. 既知の問題の修正3. ユーザー体験の向上"
},
"country": {
"cn": "中国",
+1 -1
View File
@@ -387,7 +387,7 @@
"content": "Хотите обновить сейчас?",
"updateNow": "Обновить сейчас",
"later": "Позже",
"defaultContent": "1. Оптимизация производительности приложения\n2. Исправление известных проблем\n3. Улучшение пользовательского опыта"
"defaultContent": "1. Оптимизация производительности приложения2. Исправление известных проблем3. Улучшение пользовательского опыта"
},
"country": {
"cn": "Китай",
+9 -9
View File
@@ -232,13 +232,13 @@
"longestConnection": "最长连接时间",
"days": "{days}天",
"daysOfWeek": {
"monday": "周\n一",
"tuesday": "周\n二",
"wednesday": "周\n三",
"thursday": "周\n四",
"friday": "周\n五",
"saturday": "周\n六",
"sunday": "周\n日"
"monday": "周一",
"tuesday": "周二",
"wednesday": "周三",
"thursday": "周四",
"friday": "周五",
"saturday": "周六",
"sunday": "周日"
},
"processTrafficFailed": "处理流量日志数据失败"
},
@@ -256,7 +256,7 @@
"rewardDetails": "奖励明细 >",
"steps": "邀请步骤",
"inviteFriend": "邀请好友",
"acceptInvite": "好友接受邀请\n下单并注册",
"acceptInvite": "好友接受邀请下单并注册",
"getReward": "获得奖励",
"shareLink": "分享链接",
"shareQR": "分享二维码",
@@ -415,7 +415,7 @@
"content": "是否立即更新?",
"updateNow": "立即更新",
"updateLater": "稍后再说",
"defaultContent": "1. 优化应用性能\n2. 修复已知问题\n3. 提升用户体验"
"defaultContent": "1. 优化应用性能2. 修复已知问题3. 提升用户体验"
},
"country": {
"cn": "中国",
@@ -170,13 +170,13 @@
"longestConnection": "最長連接時間",
"days": "{days}天",
"daysOfWeek": {
"monday": "週\n一",
"tuesday": "週\n二",
"wednesday": "週\n三",
"thursday": "週\n四",
"friday": "週\n五",
"saturday": "週\n六",
"sunday": "週\n日"
"monday": "週一",
"tuesday": "週二",
"wednesday": "週三",
"thursday": "週四",
"friday": "週五",
"saturday": "週六",
"sunday": "週日"
},
"processTrafficFailed": "處理流量日誌資料失敗"
},
@@ -194,7 +194,7 @@
"rewardDetails": "獎勵明細 >",
"steps": "邀請步驟",
"inviteFriend": "邀請好友",
"acceptInvite": "好友接受邀請\n下單並註冊",
"acceptInvite": "好友接受邀請下單並註冊",
"getReward": "獲得獎勵",
"shareLink": "分享連結",
"shareQR": "分享二維碼",
@@ -332,7 +332,7 @@
"content": "是否立即更新?",
"updateNow": "立即更新",
"updateLater": "稍後",
"defaultContent": "1. 優化應用性能\n2. 修復已知問題\n3. 改進用戶體驗"
"defaultContent": "1. 優化應用性能2. 修復已知問題3. 改進用戶體驗"
},
"orderStatus": {
"title": "訂單狀態",
+5
View File
@@ -1,4 +1,5 @@
name: BearVPN
app_name: BearVPN
version: 1.0.0
build_number: 1
targets:
@@ -11,3 +12,7 @@ targets:
enable: true
# 不签名
sign: false
windows:
exe:
enable: true
icon: assets/images/tray_icon.ico
+2 -2
View File
@@ -5,6 +5,6 @@
// Created by GFWFighter on 7/24/1402 AP.
//
BASE_BUNDLE_IDENTIFIER=app.hi.com
BASE_BUNDLE_IDENTIFIER=com.taw.hifastvpn
SERVICE_IDENTIFIER=com.hi.app
DEVELOPMENT_TEAM=3UR892FAP3
DEVELOPMENT_TEAM=NJRRF427XB
@@ -10,7 +10,7 @@
<true/>
<key>com.apple.security.application-groups</key>
<array>
<string>group.app.hi.com</string>
<string>group.$(BASE_BUNDLE_IDENTIFIER)</string>
</array>
<key>com.apple.security.network.client</key>
<true/>
+31 -19
View File
@@ -758,11 +758,11 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = PacketTunnel/HiddifyPacketTunnel.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 3UR892FAP3;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = NJRRF427XB;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
EXCLUDED_ARCHS = armv7;
GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -788,7 +788,7 @@
PRODUCT_BUNDLE_IDENTIFIER = "$(BASE_BUNDLE_IDENTIFIER).PacketTunnel";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = dev_p;
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "HiFastVPN-iOS-Pord-PacketTunnel";
SKIP_INSTALL = YES;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
@@ -814,9 +814,11 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = PacketTunnel/PacketTunnelRelease.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = Q5PC7SNX27;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = NJRRF427XB;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
EXCLUDED_ARCHS = armv7;
GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -842,6 +844,7 @@
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;
@@ -865,9 +868,11 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = PacketTunnel/HiddifyPacketTunnel.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = Q5PC7SNX27;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = NJRRF427XB;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
EXCLUDED_ARCHS = armv7;
GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -893,6 +898,7 @@
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;
@@ -933,7 +939,7 @@
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
@@ -975,7 +981,7 @@
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 3UR892FAP3;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = NJRRF427XB;
ENABLE_BITCODE = NO;
"EXCLUDED_ARCHS[sdk=iphoneos*]" = armv7;
INFOPLIST_FILE = Runner/Info.plist;
@@ -1004,9 +1010,10 @@
"-lresolv",
);
PRODUCT_BUNDLE_IDENTIFIER = "$(BASE_BUNDLE_IDENTIFIER)";
"PRODUCT_BUNDLE_IDENTIFIER[sdk=iphoneos*]" = com.taw.hifastvpn;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = hot;
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "HiFastVPN-iOS-Pord";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@@ -1023,7 +1030,7 @@
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 3UR892FAP3;
DEVELOPMENT_TEAM = NJRRF427XB;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 2.5.72.5.52.5.31.0;
PRODUCT_BUNDLE_IDENTIFIER = com.hiddify.ios.RunnerTests;
@@ -1042,7 +1049,7 @@
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 3UR892FAP3;
DEVELOPMENT_TEAM = NJRRF427XB;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 2.5.72.5.52.5.31.0;
PRODUCT_BUNDLE_IDENTIFIER = com.hiddify.ios.RunnerTests;
@@ -1059,7 +1066,7 @@
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 3UR892FAP3;
DEVELOPMENT_TEAM = NJRRF427XB;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 2.5.72.5.52.5.31.0;
PRODUCT_BUNDLE_IDENTIFIER = com.hiddify.ios.RunnerTests;
@@ -1161,7 +1168,7 @@
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Distribution";
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
@@ -1201,11 +1208,11 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 3UR892FAP3;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = NJRRF427XB;
ENABLE_BITCODE = NO;
"EXCLUDED_ARCHS[sdk=iphoneos*]" = armv7;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "i386 arm64";
@@ -1234,9 +1241,10 @@
"-lresolv",
);
PRODUCT_BUNDLE_IDENTIFIER = "$(BASE_BUNDLE_IDENTIFIER)";
"PRODUCT_BUNDLE_IDENTIFIER[sdk=iphoneos*]" = com.taw.hifastvpn;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = dev;
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "HiFastVPN-iOS-Pord";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@@ -1257,9 +1265,11 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/RunnerRelease.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = Q5PC7SNX27;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = NJRRF427XB;
ENABLE_BITCODE = NO;
"EXCLUDED_ARCHS[sdk=iphoneos*]" = armv7;
INFOPLIST_FILE = Runner/Info.plist;
@@ -1288,8 +1298,10 @@
"-lresolv",
);
PRODUCT_BUNDLE_IDENTIFIER = "$(BASE_BUNDLE_IDENTIFIER)";
"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;
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+2 -2
View File
@@ -52,10 +52,10 @@
<string>需要相机权限以支持拍照功能</string>
<key>NSMicrophoneUsageDescription</key>
<string>需要麦克风权限以支持语音消息功能</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>需要相册权限以支持图片上传功能</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>需要相册添加权限以保存图片</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>需要相册权限以支持图片上传功能</string>
<key>SERVICE_IDENTIFIER</key>
<string>$(SERVICE_IDENTIFIER)</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
+1 -1
View File
@@ -12,7 +12,7 @@
<true/>
<key>com.apple.security.application-groups</key>
<array>
<string>group.app.hi.com</string>
<string>group.com.taw.hifastvpn</string>
</array>
<key>com.apple.security.network.client</key>
<true/>
+1 -1
View File
@@ -12,7 +12,7 @@
<true/>
<key>com.apple.security.application-groups</key>
<array>
<string>group.app.hi.com</string>
<string>group.com.taw.hifastvpn</string>
</array>
<key>com.apple.security.network.client</key>
<true/>
+7 -6
View File
@@ -10,19 +10,20 @@
<string>app-store</string>
<key>provisioningProfiles</key>
<dict>
<key>app.myhiddify.com</key>
<string>dist.apple.app.myhiddify.com</string>
<key>app.myhiddify.com.SingBoxPacketTunnel</key>
<string>dist.apple.app.myhiddify.com.singboxpackettunnel</string>
<key>com.taw.hifastvpn</key>
<string>HiFastVPN-iOS-Pord</string>
<key>com.taw.hifastvpn.PacketTunnel</key>
<string>HiFastVPN-iOS-Pord-PacketTunnel</string>
</dict>
<key>signingCertificate</key>
<string>E2217AF6F3AD11BA09F9FD95E025D7E637F8B081</string>
<string>545725EC39FB3B586B4F63F4BF8D9834FDCFE946</string>
<key>signingStyle</key>
<string>manual</string>
<key>stripSwiftSymbols</key>
<true/>
<key>teamID</key>
<string>3UR892FAP3</string>
<string>NJRRF427XB</string>
<key>uploadSymbols</key>
<true/>
</dict>
+1 -1
View File
@@ -1447,7 +1447,7 @@ class AppConfig {
kr_official_telegram = config.kr_official_telegram;
kr_official_telephone = config.kr_official_telephone;
kr_invitation_link = config.kr_invitation_link;
kr_website_id = config.kr_website_id;
// kr_website_id = config.kr_website_id;
if (config.kr_domains.isNotEmpty) {
KRDomain.kr_handleDomains(config.kr_domains);
}
+93 -32
View File
@@ -28,13 +28,10 @@ class KROutboundItem {
/// URL
String url = "";
@override
String toString() {
return 'KROutboundItem(tag: $tag, country: $country, delay: ${urlTestDelay.value}ms)';
}
/// 服务器类型
/// 构造函数,接受 KrNodeListItem 对象并初始化 KROutboundItem
/// 构造函数,接受 KrItem 对象并初始化 KROutboundItem
KROutboundItem(KrNodeListItem nodeListItem) {
id = nodeListItem.id.toString();
protocol = nodeListItem.protocol;
@@ -152,7 +149,7 @@ class KROutboundItem {
break;
case "hysteria":
case "hysteria2":
// 后端的 "hysteria" 实际上是 Hysteria2 协议
// 后端的 "hysteria" 实际上是 Hysteria2 协议
final securityConfig =
json["security_config"] as Map<String, dynamic>? ?? {};
config = {
@@ -274,6 +271,7 @@ class KROutboundItem {
// 🔧 尝试从 config 字段解析 transport 配置
Map<String, dynamic>? transportConfig;
Map<String, dynamic>? securityConfig;
int actualPort = nodeListItem.port; // 🔧 使用局部变量存储实际端口
// 🔧 关键修复:优先从 protocols 字段解析配置
if (nodeListItem.protocols.isNotEmpty) {
@@ -307,6 +305,28 @@ class KROutboundItem {
}
if (matchedProtocol != null) {
// 🔧 关键修复:只在顶层端口为0时,才使用 protocols 中的端口
// 这样可以保留顶层的正确端口(如 53441),不被 protocols 数组中的端口(如 287)覆盖
if (actualPort == 0 && matchedProtocol['port'] != null) {
// 安全解析端口号
int protocolPort = 0;
final portValue = matchedProtocol['port'];
if (portValue is int) {
protocolPort = portValue;
} else if (portValue is String) {
protocolPort = int.tryParse(portValue) ?? 0;
}
if (protocolPort > 0) {
actualPort = protocolPort;
if (kDebugMode) {
print(' ✅ 从 protocols 使用端口: $protocolPort (顶层端口为0)');
}
}
} else if (kDebugMode && matchedProtocol['port'] != null) {
print(' ✅ 保留顶层端口: $actualPort (protocols中的端口: ${matchedProtocol['port']})');
}
// 提取 transport 配置
if (matchedProtocol['network'] != null || matchedProtocol['transport'] != null) {
final network = matchedProtocol['network'] ?? matchedProtocol['transport'];
@@ -402,7 +422,7 @@ class KROutboundItem {
switch (nodeListItem.protocol) {
case "shadowsocks":
// 优先使用 protocols 解析出来的 cipher,其次是 method 字段,最后才是默认值
// 优先使用 protocols 解析出来的 cipher,其次是 method 字段,最后才是默认值
String finalMethod = nodeListItem.method.isNotEmpty
? nodeListItem.method
: "2022-blake3-aes-256-gcm";
@@ -411,22 +431,21 @@ class KROutboundItem {
"type": "shadowsocks",
"tag": nodeListItem.name,
"server": nodeListItem.serverAddr,
"server_port": nodeListItem.port,
"server_port": actualPort,
"method": finalMethod,
"password": nodeListItem.uuid
};
if (kDebugMode) {
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
print('✅ Shadowsocks 节点配置构建成功: ${nodeListItem.name}');
}
if (kDebugMode) {
print('📄 使用加密方法: $finalMethod');
}
if (kDebugMode) {
print('📄 完整配置: $config');
print('📄 完整配置 JSON:');
print(jsonEncode(config));
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
}
break;
case "vless":
// 判断是否为域名(非IP地址)
// 判断是否为域名(非IP地址)
final bool isDomain = !RegExp(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$')
.hasMatch(nodeListItem.serverAddr);
@@ -436,17 +455,25 @@ class KROutboundItem {
serverName = securityConfig['sni'].toString();
}
// 🔧 关键修复:根据 security_config 判断是否启用 TLS
final bool vlessTlsEnabled = securityConfig?['tls_enabled'] ?? false;
// 🔧 关键修复:智能判断是否启用 TLS
// 1. 优先使用 securityConfig['tls_enabled']
// 2. 如果没有明确配置,根据端口和域名智能判断
bool vlessTlsEnabled = securityConfig?['tls_enabled'] ?? true; // 默认启用 TLS
// 如果端口是标准非TLS端口(80, 8080等),则禁用 TLS
if (nodeListItem.port == 80 || nodeListItem.port == 8080) {
vlessTlsEnabled = false;
}
if (kDebugMode) {
print('🔐 VLESS TLS 状态: enabled=$vlessTlsEnabled');
print('🔐 VLESS TLS 状态: enabled=$vlessTlsEnabled (port=${nodeListItem.port}, isDomain=$isDomain)');
}
config = {
"type": "vless",
"tag": nodeListItem.name,
"server": nodeListItem.serverAddr,
"server_port": nodeListItem.port,
"server_port": actualPort,
"uuid": nodeListItem.uuid,
if (transportConfig != null) "transport": transportConfig,
if (vlessTlsEnabled) "tls": {
@@ -460,14 +487,27 @@ class KROutboundItem {
}
};
if (kDebugMode) {
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
print('✅ VLESS 节点配置构建成功: ${nodeListItem.name}');
}
if (kDebugMode) {
print('📄 完整配置: $config');
print('📋 原始节点信息:');
print(' - serverAddr: ${nodeListItem.serverAddr}');
print(' - port: ${nodeListItem.port}');
print(' - uuid: ${nodeListItem.uuid}');
print(' - protocols: ${nodeListItem.protocols}');
print('🔐 安全配置:');
print(' - TLS 启用: $vlessTlsEnabled');
print(' - 是域名: $isDomain');
print(' - server_name: $serverName');
print(' - securityConfig: $securityConfig');
print('📡 传输配置:');
print(' - transportConfig: $transportConfig');
print('📄 最终生成的完整配置 JSON:');
print(jsonEncode(config));
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
}
break;
case "vmess":
// 判断是否为域名(非IP地址)
// 判断是否为域名(非IP地址)
final bool isDomain = !RegExp(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$')
.hasMatch(nodeListItem.serverAddr);
@@ -477,17 +517,25 @@ class KROutboundItem {
serverName = securityConfig['sni'].toString();
}
// 🔧 关键修复:根据 security_config 判断是否启用 TLS
final bool tlsEnabled = securityConfig?['tls_enabled'] ?? false;
// 🔧 关键修复:智能判断是否启用 TLS
// 1. 优先使用 securityConfig['tls_enabled']
// 2. 如果没有明确配置,根据端口和域名智能判断
bool tlsEnabled = securityConfig?['tls_enabled'] ?? true; // 默认启用 TLS
// 如果端口是标准非TLS端口(80, 8080等),则禁用 TLS
if (nodeListItem.port == 80 || nodeListItem.port == 8080) {
tlsEnabled = false;
}
if (kDebugMode) {
print('🔐 TLS 状态: enabled=$tlsEnabled');
print('🔐 VMess TLS 状态: enabled=$tlsEnabled (port=${nodeListItem.port}, isDomain=$isDomain)');
}
config = {
"type": "vmess",
"tag": nodeListItem.name,
"server": nodeListItem.serverAddr,
"server_port": nodeListItem.port,
"server_port": actualPort,
"uuid": nodeListItem.uuid,
"alter_id": 0,
"security": "auto",
@@ -503,14 +551,27 @@ class KROutboundItem {
}
};
if (kDebugMode) {
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
print('✅ VMess 节点配置构建成功: ${nodeListItem.name}');
}
if (kDebugMode) {
print('📄 完整配置: $config');
print('📋 原始节点信息:');
print(' - serverAddr: ${nodeListItem.serverAddr}');
print(' - port: ${nodeListItem.port}');
print(' - uuid: ${nodeListItem.uuid}');
print(' - protocols: ${nodeListItem.protocols}');
print('🔐 安全配置:');
print(' - TLS 启用: $tlsEnabled');
print(' - 是域名: $isDomain');
print(' - server_name: $serverName');
print(' - securityConfig: $securityConfig');
print('📡 传输配置:');
print(' - transportConfig: $transportConfig');
print('📄 最终生成的完整配置 JSON:');
print(jsonEncode(config));
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
}
break;
case "trojan":
// 判断是否为域名(非IP地址)
// 判断是否为域名(非IP地址)
final bool isDomain = !RegExp(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$')
.hasMatch(nodeListItem.serverAddr);
@@ -524,7 +585,7 @@ class KROutboundItem {
"type": "trojan",
"tag": nodeListItem.name,
"server": nodeListItem.serverAddr,
"server_port": nodeListItem.port,
"server_port": actualPort,
"password": nodeListItem.uuid,
if (transportConfig != null) "transport": transportConfig,
"tls": {
@@ -546,7 +607,7 @@ class KROutboundItem {
break;
case "hysteria":
case "hysteria2":
// 后端的 "hysteria" 实际上是 Hysteria2 协议
// 后端的 "hysteria" 实际上是 Hysteria2 协议
if (kDebugMode) {
print('🔍 构建 Hysteria2 节点: ${nodeListItem.name}');
}
@@ -90,7 +90,7 @@ class KrOutboundsList {
// 将标签分组转换为 KRGroupOutboundList 并添加到 groupOutboundList
for (var tag in tagGroups.keys) {
final item = KRGroupOutboundList(
final item = KRGroupOutboundList(
tag: tag, outboundList: tagGroups[tag]!);
for (var group in groupList) {
+3 -3
View File
@@ -7,16 +7,16 @@ class KRIsRegister {
KRIsRegister({this.kr_isRegister = false});
KRIsRegister.fromJson(Map<String, dynamic> json) {
kr_isRegister = json['Status'] == "true" || json['Status'] == true
kr_isRegister = json['exist'] == "true" || json['exist'] == true
? true
: false || json['status'] == "true" || json['status'] == true
: false || json['exist'] == "true" || json['exist'] == true
? true
: false;
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['Status'] = kr_isRegister;
data['exist'] = kr_isRegister;
return data;
}
}
+56 -13
View File
@@ -2,7 +2,7 @@ import 'dart:convert';
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
class KRNodeList {
final List<KrNodeListItem> list;
final List<KrNodeListItem> list;
final String subscribeId;
final String startTime;
final String expireTime;
@@ -35,8 +35,8 @@ class KRNodeList {
// 查找 id 匹配的订阅项
try {
subscribeData = listData.firstWhere(
(item) => (item as Map<String, dynamic>)['id']?.toString() == requestSubscribeId,
orElse: () => listData[0] as Map<String, dynamic>
(item) => (item as Map<String, dynamic>)['id']?.toString() == requestSubscribeId,
orElse: () => listData[0] as Map<String, dynamic>
) as Map<String, dynamic>;
KRLogUtil.kr_i('✅ 找到匹配的订阅项: id=$requestSubscribeId', tag: 'NodeList');
} catch (e) {
@@ -70,7 +70,7 @@ class KRNodeList {
class KrNodeListItem {
final int id;
String name;
String name;
final String uuid;
final String protocol;
final String relayMode;
@@ -136,25 +136,68 @@ class KrNodeListItem {
String method = json['method']?.toString() ?? ''; // 加密方法(Shadowsocks等)
final protocols = json['protocols']?.toString() ?? ''; // 协议配置JSON
// 🔧 如果有 protocols 字段,从中解析 port 和 cipher
// 🔧 打印原始节点 JSON(用于调试)
KRLogUtil.kr_i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', tag: 'NodeList');
KRLogUtil.kr_i('📥 收到节点 API 原始数据:', tag: 'NodeList');
KRLogUtil.kr_i('节点名称: ${json['name']}', tag: 'NodeList');
KRLogUtil.kr_i('协议类型: ${json['protocol']}', tag: 'NodeList');
KRLogUtil.kr_i('完整 JSON:', tag: 'NodeList');
KRLogUtil.kr_i(jsonEncode(json), tag: 'NodeList');
// 🔧 如果有 protocols 字段,从中解析 cipher(但不覆盖顶层的 port
if (protocols.isNotEmpty) {
try {
final protocolsList = jsonDecode(protocols) as List;
final currentProtocol = json['protocol']?.toString().toLowerCase() ?? '';
KRLogUtil.kr_i('📋 protocols 字段内容 (${protocolsList.length} 个协议):', tag: 'NodeList');
for (var i = 0; i < protocolsList.length; i++) {
KRLogUtil.kr_i(' 协议 $i: ${jsonEncode(protocolsList[i])}', tag: 'NodeList');
}
if (protocolsList.isNotEmpty) {
final firstProtocol = protocolsList[0] as Map<String, dynamic>;
// 优先使用 protocols 中的配置
if (firstProtocol['port'] != null) {
port = _parseIntSafely(firstProtocol['port']);
// 🔧 修复:查找与当前协议类型匹配的配置,而不是直接使用第一个
Map<String, dynamic>? matchedProtocolConfig;
// 尝试找到协议类型匹配的配置
for (var protocolConfig in protocolsList) {
final configMap = protocolConfig as Map<String, dynamic>;
// 🔧 修复:API 使用的字段名是 'type',不是 'protocol'
final protocolType = (configMap['type'] ?? configMap['protocol'])?.toString().toLowerCase() ?? '';
// 检查是否匹配(支持 shadowsocks/ss, vmess, vless, trojan 等)
if (protocolType == currentProtocol ||
(currentProtocol == 'shadowsocks' && protocolType == 'ss') ||
(currentProtocol == 'ss' && protocolType == 'shadowsocks')) {
matchedProtocolConfig = configMap;
KRLogUtil.kr_i('🎯 找到匹配的协议配置: $protocolType', tag: 'NodeList');
break;
}
}
if (firstProtocol['cipher'] != null && firstProtocol['cipher'].toString().isNotEmpty) {
method = firstProtocol['cipher'].toString();
// 如果没找到匹配的,使用第一个配置(兼容旧API)
final targetProtocol = matchedProtocolConfig ?? (protocolsList[0] as Map<String, dynamic>);
// 🔧 关键修复:只在顶层没有 port 字段时,才使用 protocols 中的端口
// 这样可以保留顶层的正确端口(如 53441),不被 protocols 数组中的端口(如 287)覆盖
if (port == 0 && targetProtocol['port'] != null) {
port = _parseIntSafely(targetProtocol['port']);
KRLogUtil.kr_i('✅ 从 protocols 解析端口: $port', tag: 'NodeList');
} else {
KRLogUtil.kr_i('✅ 保留顶层端口: $port (protocols中的端口: ${targetProtocol['port']})', tag: 'NodeList');
}
// 提取 cipher(加密方法)
if (targetProtocol['cipher'] != null && targetProtocol['cipher'].toString().isNotEmpty) {
method = targetProtocol['cipher'].toString();
KRLogUtil.kr_i('✅ 从 protocols 解析 cipher: $method', tag: 'NodeList');
}
KRLogUtil.kr_i('从 protocols 解析: port=$port, cipher=$method', tag: 'NodeList');
}
} catch (e) {
KRLogUtil.kr_w('解析 protocols 字段失败: $e', tag: 'NodeList');
KRLogUtil.kr_w('⚠️ 解析 protocols 字段失败: $e', tag: 'NodeList');
}
}
KRLogUtil.kr_i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', tag: 'NodeList');
return KrNodeListItem(
id: _parseIntSafely(json['id']),
@@ -59,6 +59,8 @@ class UserInfoCard extends StatelessWidget {
children: [
Text(
'ID: $userId',
overflow: TextOverflow.ellipsis,
maxLines: 1,
style: TextStyle(
color: Colors.white,
fontSize: 14.sp,
@@ -1,17 +1,28 @@
import 'package:get/get.dart';
import 'package:flutter/material.dart';
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
import 'package:kaer_with_panels/app/services/kr_subscribe_service.dart';
import 'package:kaer_with_panels/app/services/singbox_imp/kr_sing_box_imp.dart';
import 'package:kaer_with_panels/app/utils/kr_secure_storage.dart';
import '../../../localization/app_translations.dart';
import 'package:kaer_with_panels/app/modules/kr_home/controllers/kr_home_controller.dart';
class HINodeListController extends GetxController {
class HINodeListController extends GetxController with WidgetsBindingObserver {
/// 订阅服务
final KRSubscribeService kr_subscribeService = KRSubscribeService();
/// 首页服务
final KRHomeController homeController = Get.find<KRHomeController>();
/// 调试模式状态
final RxBool isDebugMode = false.obs;
/// 模式按钮点击计数器
int modeButtonClickCount = 0;
/// 最后一次点击时间
DateTime? lastModeButtonClickTime;
/// 获取连接类型字符串
String kr_getConnectionTypeString() {
final connectionType = KRSingBoxImp.instance.kr_connectionType.value;
@@ -30,15 +41,98 @@ class HINodeListController extends GetxController {
KRLogUtil.kr_i('连接类型已更新为: $type', tag: 'HINodeListController');
// 这里可以添加其他需要的逻辑
}
}
// 如果当前有选择的国家,触发重选检查
if (homeController.currentSelectedCountry.isNotEmpty) {
KRLogUtil.kr_i('连接类型更新后,检查是否需要重选节点', tag: 'HINodeListController');
// 延迟一下让连接类型更新完成
Future.delayed(const Duration(milliseconds: 500), () {
// homeController.checkCountryReselection(KRSingBoxImp.instance.kr_activeGroups);
});
/// 处理模式按钮点击(用于激活调试模式)
void kr_handleModeButtonClick() {
final now = DateTime.now();
// 检查是否在2秒内连续点击
if (lastModeButtonClickTime != null &&
now.difference(lastModeButtonClickTime!).inSeconds > 2) {
// 超过2秒,重置计数器
modeButtonClickCount = 0;
}
modeButtonClickCount++;
lastModeButtonClickTime = now;
KRLogUtil.kr_i('模式按钮点击次数: $modeButtonClickCount',
tag: 'HINodeListController');
if (modeButtonClickCount >= 5) {
// 激活调试模式
isDebugMode.value = true;
modeButtonClickCount = 0; // 重置计数器
KRLogUtil.kr_i('🐛 调试模式已激活!', tag: 'HINodeListController');
}
}
/// 获取要显示的节点列表(根据调试模式过滤)
/// 重置调试模式
void kr_resetDebugMode() {
isDebugMode.value = false;
modeButtonClickCount = 0;
lastModeButtonClickTime = null;
KRLogUtil.kr_i('调试模式已重置', tag: 'HINodeListController');
}
Future<void> kr_handleRefresh() async {
await kr_subscribeService.kr_refreshAll();
try {
final savedNode =
await KRSecureStorage().kr_readData(key: 'SELECTED_NODE_TAG');
if (savedNode != null && savedNode.isNotEmpty) {
homeController.kr_currentNodeName.value = savedNode;
homeController.kr_cutTag.value = savedNode;
homeController.kr_cutSeletedTag.value = savedNode;
}
} catch (_) {}
if (!homeController.kr_isLatency.value) {
homeController.kr_urlTest();
}
}
@override
void onInit() {
super.onInit();
WidgetsBinding.instance.addObserver(this);
_loadSelectedCountry();
}
@override
void onReady() {
super.onReady();
if (homeController.kr_isLatency.value) return;
KRLogUtil.kr_i('🔄 节点列表显示 - 自动触发延迟测试', tag: 'HINodeListView');
homeController.kr_urlTest();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
// 当应用从后台切换回前台时
if (state == AppLifecycleState.resumed) {
if (homeController.kr_isLatency.value) return;
KRLogUtil.kr_i('🔄 节点列表显示 - 自动触发延迟测试', tag: 'HINodeListView');
homeController.kr_urlTest();
}
}
@override
void onClose() {
WidgetsBinding.instance.removeObserver(this);
super.onClose();
}
Future<void> _loadSelectedCountry() async {
try {
final v =
await KRSecureStorage().kr_readData(key: 'SELECTED_COUNTRY_TAG');
if (v != null && v.isNotEmpty) {
homeController.kr_coutryText.value = v;
}
} catch (_) {}
}
}
@@ -1,6 +1,5 @@
// hi_node_list_view.dart
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
@@ -9,6 +8,7 @@ import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
import 'package:kaer_with_panels/app/widgets/kr_country_flag.dart';
import '../../../model/business/kr_outbound_item.dart';
import '../../../utils/kr_log_util.dart';
import 'package:kaer_with_panels/app/utils/kr_secure_storage.dart';
import '../controllers/hi_node_list_controller.dart';
import 'package:kaer_with_panels/app/modules/kr_home/models/kr_home_views_status.dart';
import 'package:kaer_with_panels/app/services/singbox_imp/kr_sing_box_imp.dart';
@@ -24,41 +24,18 @@ class HINodeListView extends GetView<HINodeListController> {
static const Color krModernGreen = Color(0xFF4CAF50);
static const Color krModernGreenLight = Color(0xFF81C784);
// 存储随机延迟值,用于UI展示
static final Map<String, int> _fakeDelays = {};
/// 获取用于显示的延迟值
int _getDisplayDelay(HINodeListController controller, KROutboundItem item) {
return item.urlTestDelay.value;
// if (controller.homeController.kr_isConnected.value) {
//
// }
// if (!_fakeDelays.containsKey(item.tag)) {
// final random = Random();
// _fakeDelays[item.tag] = 30 + random.nextInt(71); // 30-100ms
// }
// return _fakeDelays[item.tag] ?? 0;
}
/// 获取分组内最快节点的延迟值(单位:ms)
/// 如果列表为空,返回 0
int getFastestNodeDelay(
HINodeListController controller,
List<KROutboundItem> outboundList,
) {
HINodeListController controller,
List<KROutboundItem> outboundList,
) {
if (outboundList.isEmpty) return 0;
// 过滤掉不可用节点(延迟标记为 65535 或负数)
final validNodes = outboundList.where((node) {
final delay = node.urlTestDelay.value;
return delay > 0 && delay < 65535;
}).toList();
if (validNodes.isEmpty) return 0;
// 返回最小延迟值
validNodes.sort((a, b) => a.urlTestDelay.value.compareTo(b.urlTestDelay.value));
return validNodes.first.urlTestDelay.value;
outboundList
.sort((a, b) => a.urlTestDelay.value.compareTo(b.urlTestDelay.value));
return outboundList.first.urlTestDelay.value;
}
/// 找出延迟最低(最快)的节点对象
@@ -86,23 +63,20 @@ class HINodeListView extends GetView<HINodeListController> {
Widget build(BuildContext context) {
// 1. 使用 Material 作为根组件,确保 InkWell 的水波纹效果正常
// 并设置透明背景,让父组件的背景可以透出来
return Material(
color: Colors.transparent,
child: _buildSubscribeList(context)
// child: _kr_buildRegionList(context)
);
return Obx(() {
// Material 保持透明背景,让 InkWell 水波纹正常
return Material(
color: Colors.transparent,
child: controller.isDebugMode.value
? _buildSubscribeList(context)
: _kr_buildRegionList(context),
);
});
}
/// 构建国家/地区分组列表
Widget _kr_buildRegionList(BuildContext context) {
return Obx(() {
// 自动触发延迟测试(仅在未连接状态下)
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!controller.homeController.kr_isConnected.value && !controller.homeController.kr_isLatency.value) {
KRLogUtil.kr_i('🔄 节点列表显示 - 自动触发延迟测试', tag: 'HINodeListView');
controller.homeController.kr_urlTest();
}
});
return _kr_buildListContainer(
context,
child: ListView(
@@ -110,21 +84,24 @@ class HINodeListView extends GetView<HINodeListController> {
// 2. 使用 children 属性,并一次性构建所有列表项
children: [
if (controller.kr_subscribeService.countryOutboundList.isEmpty)
_buildEmptyListPlaceholder(context, AppTranslations.kr_home.noRegions)
_buildEmptyListPlaceholder(
context, AppTranslations.kr_home.noRegions)
else ...[
InkWell(
// 🔧 修复:改为 async,等待节点切换完成后再关闭列表
onTap: () async {
try {
final success =
await controller.homeController.kr_performNodeSwitch('auto');
await KRSecureStorage().kr_saveData(
key: 'SELECTED_COUNTRY_TAG', value: 'auto');
controller.homeController.kr_coutryText.value = 'auto';
final success = await controller.homeController
.kr_performNodeSwitch('auto');
if (success) {
controller.homeController.kr_currentListStatus.value =
KRHomeViewsListStatus.kr_none;
}
} catch (e) {
KRLogUtil.kr_e('Auto选项切换异常: $e',
tag: 'NodeListView');
KRLogUtil.kr_e('Auto选项切换异常: $e', tag: 'NodeListView');
}
},
child: Container(
@@ -136,7 +113,8 @@ class HINodeListView extends GetView<HINodeListController> {
),
),
),
padding: EdgeInsets.symmetric(vertical: 12.w, horizontal: 16.w),
padding:
EdgeInsets.symmetric(vertical: 12.w, horizontal: 16.w),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
@@ -180,143 +158,196 @@ class HINodeListView extends GetView<HINodeListController> {
],
),
),
Obx(() => controller.homeController.kr_coutryText.value == 'auto'
? KrLocalImage(
imageName: 'radio-active-icon',
imageType: ImageType.svg,
width: 16.w, // 适当缩小 SVG 尺寸,留出白边
height: 16.h,
)
: KrLocalImage(
imageName: 'radio-icon',
imageType: ImageType.svg,
width: 16.w, // 适当缩小 SVG 尺寸,留出白边
height: 16.h,
)),
Obx(() =>
controller.homeController.kr_coutryText.value ==
'auto'
? KrLocalImage(
imageName: 'radio-active-icon',
imageType: ImageType.svg,
width: 16.w, // 适当缩小 SVG 尺寸,留出白边
height: 16.h,
)
: KrLocalImage(
imageName: 'radio-icon',
imageType: ImageType.svg,
width: 16.w, // 适当缩小 SVG 尺寸,留出白边
height: 16.h,
)),
],
),
),
),
...controller.kr_subscribeService.countryOutboundList.map((country) {
...controller.kr_subscribeService.countryOutboundList
.map((country) {
return InkWell(
onTap: () {
// 自动选择这个国家下的节点延迟中最快的
controller.homeController.onCountrySelected(country.country);
onTap: () async {
try {
await KRSecureStorage().kr_saveData(
key: 'SELECTED_COUNTRY_TAG',
value: country.country);
controller.homeController.kr_coutryText.value =
country.country;
final fastest = findFastestNode(country.outboundList);
final success = await controller.homeController
.kr_performNodeSwitch(fastest.tag);
if (success) {
controller.homeController.kr_currentListStatus.value =
KRHomeViewsListStatus.kr_none;
}
} catch (e) {
KRLogUtil.kr_e('国家选择切换异常: $e', tag: 'NodeListView');
}
},
child: _kr_buildCountryListItem(context, country: country),
);
}).toList(),
]
] //
),
),
);
});
}
/// 构建默认的订阅节点列表
Widget _buildSubscribeList(BuildContext context) {
return Obx(() {
// 自动触发延迟测试(仅在未连接状态下)
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!controller.homeController.kr_isConnected.value && !controller.homeController.kr_isLatency.value) {
KRLogUtil.kr_i('🔄 节点列表显示 - 自动触发延迟测试', tag: 'HINodeListView');
controller.homeController.kr_urlTest();
}
});
return _kr_buildListContainer(
context,
child: ListView(
padding: EdgeInsets.symmetric(vertical: 8.w),
// 2. 使用 children 属性,并一次性构建所有列表项
children: [
if (controller.kr_subscribeService.allList.isEmpty)
_buildEmptyListPlaceholder(context, AppTranslations.kr_home.noNodes)
else ...[
InkWell(
onTap: () {
controller.homeController.kr_selectNode('auto');
controller.homeController.kr_currentListStatus.value =
KRHomeViewsListStatus.kr_none;
},
child: Container(
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Colors.white.withOpacity(0.3),
width: 1.0,
),
),
),
padding: EdgeInsets.symmetric(vertical: 12.w, horizontal: 16.w),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
width: 32.w,
height: 22.w,
padding: EdgeInsets.symmetric(vertical: 8.w),
// 2. 使用 children 属性,并一次性构建所有列表项
children: [
...[
InkWell(
// 🔧 修复:改为 async,等待节点切换完成后再关闭列表
onTap: () async {
try {
final success = await controller.homeController
.kr_performNodeSwitch('auto');
if (success) {
controller.homeController.kr_currentListStatus.value =
KRHomeViewsListStatus.kr_none;
}
} catch (e) {
KRLogUtil.kr_e('Auto选项切换异常: $e', tag: 'NodeListView');
}
},
child: Container(
decoration: BoxDecoration(
// 2. 设置背景色为主题色
color: Theme.of(context).primaryColor,
),
// 4. 使用 Center 来确保内部的图片水平和垂直居中
child: Center(
child: KrLocalImage(
imageName: "hi-home-logo",
width: 14.w,
height: 14.h,
border: Border(
bottom: BorderSide(
color: Colors.white.withOpacity(0.3),
width: 1.0,
),
),
),
),
SizedBox(width: 8.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
padding:
EdgeInsets.symmetric(vertical: 12.w, horizontal: 16.w),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
'自动匹配最快网络', // 您指定的文本
style: KrAppTextStyle(
fontSize: 14,
color: Colors.white,
fontWeight: FontWeight.w600, // 600 加粗
Container(
width: 32.w,
height: 22.w,
decoration: BoxDecoration(
// 2. 设置背景色为主题色
color: Theme.of(context).primaryColor,
),
// 4. 使用 Center 来确保内部的图片水平和垂直居中
child: Center(
child: KrLocalImage(
imageName: "hi-home-logo",
width: 14.w,
height: 14.h,
),
),
),
// 2. 第二个 Text: "根据网络IP自动匹配最快线路"
Text(
'根据网络IP自动匹配最快线路', // 您指定的文本
style: KrAppTextStyle(
fontSize: 10,
color: Colors.white,
SizedBox(width: 8.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'自动匹配最快网络', // 您指定的文本
style: KrAppTextStyle(
fontSize: 14,
color: Colors.white,
fontWeight: FontWeight.w600, // 600 加粗
),
),
// 2. 第二个 Text: "根据网络IP自动匹配最快线路"
Text(
'根据网络IP自动匹配最快线路', // 默认文本
style: KrAppTextStyle(
fontSize: 10,
color: Colors.white,
),
),
/* Obx(() {
// 当选择全局 auto 时,显示当前选中的节点信息
if (controller.homeController.kr_cutTag.value == 'auto') {
final autoNodeInfo = controller.homeController.kr_getGlobalAutoSelectedNode();
if (autoNodeInfo != null) {
return Text(
'当前: ${autoNodeInfo['tag']} (${autoNodeInfo['delay']}ms)',
style: KrAppTextStyle(
fontSize: 10,
color: Colors.white.withOpacity(0.8),
),
);
}
}
return Text(
'根据网络IP自动匹配最快线路', // 默认文本
style: KrAppTextStyle(
fontSize: 10,
color: Colors.white,
),
);
}),*/
],
),
),
Obx(() =>
controller.homeController.kr_cutTag.value == 'auto'
? KrLocalImage(
imageName: 'radio-active-icon',
imageType: ImageType.svg,
width: 16.w, // 适当缩小 SVG 尺寸,留出白边
height: 16.h,
)
: KrLocalImage(
imageName: 'radio-icon',
imageType: ImageType.svg,
width: 16.w, // 适当缩小 SVG 尺寸,留出白边
height: 16.h,
)),
],
),
),
Obx(() => controller.homeController.kr_cutTag.value == 'auto'
? KrLocalImage(
imageName: 'radio-active-icon',
imageType: ImageType.svg,
width: 16.w, // 适当缩小 SVG 尺寸,留出白边
height: 16.h,
)
: KrLocalImage(
imageName: 'radio-icon',
imageType: ImageType.svg,
width: 16.w, // 适当缩小 SVG 尺寸,留出白边
height: 16.h,
)),
],
),
),
...controller.kr_subscribeService.allList().map((item) {
return InkWell(
// 🔧 修复:改为 async,等待节点切换完成后再关闭列表
onTap: () async {
try {
KRLogUtil.kr_i('🔄 用户点击节点: ${item.tag}');
final success = await controller.homeController
.kr_performNodeSwitch(item.tag);
if (success) {
controller.homeController.kr_currentListStatus.value =
KRHomeViewsListStatus.kr_none;
}
} catch (e) {
KRLogUtil.kr_e('节点切换异常: $e', tag: 'NodeListView');
}
},
child: _kr_buildNodeListItem(context, item: item),
);
}).toList(),
]
] //
),
),
...controller.kr_subscribeService.allList.map((item) {
return InkWell(
onTap: () => _onNodeSelected(item),
child: _kr_buildNodeListItem(context, item: item),
);
}).toList(),
]
] //
),
);
});
}
@@ -328,7 +359,8 @@ class HINodeListView extends GetView<HINodeListController> {
alignment: Alignment.center,
child: Text(
text,
style: KrAppTextStyle(fontSize: 14, color: Theme.of(context).textTheme.bodySmall?.color),
style: KrAppTextStyle(
fontSize: 14, color: Theme.of(context).textTheme.bodySmall?.color),
),
);
}
@@ -344,17 +376,9 @@ class HINodeListView extends GetView<HINodeListController> {
);
}
/// 节点选中时的通用处理逻辑
void _onNodeSelected(KROutboundItem item) {
KRLogUtil.kr_i('Node selected: ${item.tag}');
KRSingBoxImp.instance.kr_selectOutbound(item.tag);
controller.homeController.kr_selectNode(item.tag);
// 切换回主页的仪表盘视图
controller.homeController.kr_currentListStatus.value = KRHomeViewsListStatus.kr_none;
}
/// 构建单个节点列表项的UI
Widget _kr_buildNodeListItem(BuildContext context, {required KROutboundItem item}) {
Widget _kr_buildNodeListItem(BuildContext context,
{required KROutboundItem item}) {
return Container(
key: ValueKey(item.id),
decoration: BoxDecoration(
@@ -369,71 +393,92 @@ class HINodeListView extends GetView<HINodeListController> {
padding: EdgeInsets.symmetric(vertical: 12.w, horizontal: 16.w),
child: Row(
children: [
KRCountryFlag(countryCode: item.country, width: 30.w, height: 20.w, isCircle: false, maintainSize: false),
KRCountryFlag(
countryCode: item.country,
width: 30.w,
height: 20.w,
isCircle: false,
maintainSize: false),
SizedBox(width: 12.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Flexible(
child: Text(
controller.homeController.kr_getCountryFullName(item.country),
style: KrAppTextStyle(fontSize: 14, color: Colors.white),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
Obx(() => controller.homeController.kr_cutTag.value == item.tag
? Container(
margin: EdgeInsets.only(left: 4.w),
padding: EdgeInsets.symmetric(horizontal: 4.w, vertical: 1.w),
decoration: BoxDecoration(
color: krModernGreenLight.withOpacity(0.1),
borderRadius: BorderRadius.circular(4.w),
),
child: Text(
AppTranslations.kr_home.selected,
style: KrAppTextStyle(fontSize: 10, color: krModernGreen, fontWeight: FontWeight.w500),
),
)
: const SizedBox.shrink()),
],
child: Obx(() {
final isDebug = controller.isDebugMode.value;
final text = isDebug
? '${controller.homeController.kr_getCountryFullName(item.country)} - ${item.tag}'
: '${controller.homeController.kr_getCountryFullName(item.country)}';
return Text(
text,
style: KrAppTextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Colors.white,
),
SizedBox(height: 2.w),
Obx(() {
// 2. 获取用于显示的延迟值
final int delay = _getDisplayDelay(controller, item);
return Text(
'${delay}ms',
style: KrAppTextStyle(
fontSize: 10,
color: krModernGreen,
fontWeight: FontWeight.w500,
),
);
}),
Text(
item.city,
style: KrAppTextStyle(fontSize: 12, color: Theme.of(context).textTheme.bodySmall?.color),
),
],
),
);
}),
),
Obx(() => controller.homeController.kr_cutTag.value == item.tag
? KrLocalImage(
imageName: 'radio-active-icon',
imageType: ImageType.svg,
width: 16.w, // 适当缩小 SVG 尺寸,留出白边
height: 16.h,
)
: KrLocalImage(
imageName: 'radio-icon',
imageType: ImageType.svg,
width: 16.w, // 适当缩小 SVG 尺寸,留出白边
height: 16.h,
)),
Obx(() {
// 1. 获取延迟值和测速状态
int displayDelay;
bool isTesting = controller.homeController.kr_isLatency.value;
// 极简逻辑:只根据节点类型决定显示方式
print(
'🔄 _kr_buildNodeListItem节点: item.tag=${item.tag}, country=${item.country}');
// 普通节点,直接显示节点自身的速度
displayDelay = item.urlTestDelay.value;
print('🔄 _kr_buildNodeListItem使用普通节点延迟: $displayDelay');
// 2. 声明文本和颜色变量
String delayText;
Color delayColor;
// 3. 根据状态设置文本和颜色
if (isTesting && displayDelay == 0) {
delayText = '测速中...';
delayColor = Colors.grey; // 测速时使用灰色
} else if (displayDelay == 0) {
delayText = '- ms'; // 未测速或初始状态
delayColor = Colors.grey;
} else if (displayDelay >= 3000) {
delayText = AppTranslations.kr_home.timeout; // "超时"
delayColor = Colors.red; // 超时状态使用红色
} else {
delayText = '${displayDelay}ms';
// 正常延迟,根据快慢设置不同颜色
delayColor = (displayDelay < 500) ? krModernGreen : Colors.orange;
}
// 4. 返回最终的 Text 组件
return Text(
delayText,
style: KrAppTextStyle(
fontSize: 10,
color: delayColor, // 使用动态计算出的颜色
fontWeight: FontWeight.w500,
),
);
}),
SizedBox(width: 12.w),
Obx(() {
final selected =
controller.homeController.kr_cutSeletedTag.value == item.tag;
return selected
? KrLocalImage(
imageName: 'radio-active-icon',
imageType: ImageType.svg,
width: 16.w,
height: 16.h,
)
: KrLocalImage(
imageName: 'radio-icon',
imageType: ImageType.svg,
width: 16.w,
height: 16.h,
);
}),
],
),
);
@@ -441,18 +486,6 @@ class HINodeListView extends GetView<HINodeListController> {
/// 构建国家列表项的UI
Widget _kr_buildCountryListItem(BuildContext context, {required country}) {
// 获取延迟颜色
Color getLatencyColor(int delay) {
if (delay == 0) {
return Colors.transparent;
} else if (delay < 500) {
return krModernGreen;
} else if (delay < 3000) {
return Color(0xFFFFB700); // 使用更容易看清的黄色
} else {
return Colors.red;
}
}
return Container(
key: ValueKey(country),
decoration: BoxDecoration(
@@ -467,43 +500,85 @@ class HINodeListView extends GetView<HINodeListController> {
padding: EdgeInsets.symmetric(vertical: 12.w, horizontal: 16.w),
child: Row(
children: [
KRCountryFlag(countryCode: country.country, width: 30.w, height: 20.w, isCircle: false, maintainSize: false),
KRCountryFlag(
countryCode: country.country,
width: 30.w,
height: 20.w,
isCircle: false,
maintainSize: false),
SizedBox(width: 12.w),
Expanded(
child: Text(
controller.homeController.kr_getCountryFullName(country.country),
style: KrAppTextStyle(fontSize: 16, fontWeight: FontWeight.w500, color: Colors.white),
style: KrAppTextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Colors.white),
),
),
Obx(() {
final int delay = getFastestNodeDelay(controller, country.outboundList);
// 1. 获取延迟值和测速状态
int displayDelay =
getFastestNodeDelay(controller, country.outboundList);
bool isTesting = controller.homeController.kr_isLatency.value;
// 2. 声明文本和颜色变量
String delayText;
Color delayColor;
// 3. 根据状态设置文本和颜色
if (isTesting && displayDelay == 0) {
delayText = '测速中...';
delayColor = Colors.grey; // 测速时使用灰色
} else if (displayDelay == 0) {
delayText = '- ms'; // 未测速或初始状态
delayColor = Colors.grey;
} else if (displayDelay >= 3000) {
delayText = AppTranslations.kr_home.timeout; // "超时"
delayColor = Colors.red; // 超时状态使用红色
} else {
delayText = '${displayDelay}ms';
// 正常延迟,根据快慢设置不同颜色
delayColor = (displayDelay < 500) ? krModernGreen : Colors.orange;
}
// 4. 返回最终的 Text 组件
return Text(
delay == 0
? ''
: delay >= 3000
? AppTranslations.kr_home.timeout
: '${delay}ms',
delayText,
style: KrAppTextStyle(
fontSize: 10,
color: getLatencyColor(delay),
color: delayColor, // 使用动态计算出的颜色
fontWeight: FontWeight.w500,
),
);
}),
SizedBox(width: 12.w),
Obx(() => controller.homeController.kr_coutryText.value == country.country
? KrLocalImage(
imageName: 'radio-active-icon',
imageType: ImageType.svg,
width: 16.w, // 适当缩小 SVG 尺寸,留出白边
height: 16.h,
)
: KrLocalImage(
imageName: 'radio-icon',
imageType: ImageType.svg,
width: 16.w, // 适当缩小 SVG 尺寸,留出白边
height: 16.h,
)),
Obx(() {
final selectedCountryField =
controller.homeController.kr_coutryText.value;
if (selectedCountryField == 'auto') {
return KrLocalImage(
imageName: 'radio-icon',
imageType: ImageType.svg,
width: 16.w,
height: 16.h,
);
}
final selected = selectedCountryField == country.country;
return selected
? KrLocalImage(
imageName: 'radio-active-icon',
imageType: ImageType.svg,
width: 16.w,
height: 16.h,
)
: KrLocalImage(
imageName: 'radio-icon',
imageType: ImageType.svg,
width: 16.w,
height: 16.h,
);
}),
],
),
);
@@ -19,10 +19,47 @@ class HINodePageView extends GetView<HINodeListController> {
return HIBaseScaffold(
child: Stack(
children: [
Positioned(
left: 0,
child: Obx(() => controller.isDebugMode.value
? GestureDetector(
onTap: () {
controller.kr_resetDebugMode();
},
child: Container(
padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 4.w),
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.8),
borderRadius: BorderRadius.circular(12.w),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.bug_report,
size: 12.w,
color: Colors.white,
),
SizedBox(width: 3.w),
Text(
'关闭调试',
style: TextStyle(
fontSize: 10.sp,
color: Colors.white,
fontWeight: FontWeight.w500,
),
),
],
),
),
)
: const SizedBox.shrink()),
),
// 主要内容区域
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 模式切换器
Padding(
padding: EdgeInsets.only(left: 100.w, right: 60.w),
@@ -122,8 +159,7 @@ class HINodePageView extends GetView<HINodeListController> {
padding: EdgeInsets.only(left: 60.w, right: 60.w, bottom: 90.w), // 为HIHelpEntrance预留空间
// 2. 在 Padding 内部使用 EasyRefresh
child: EasyRefresh(
// 3. 绑定 onRefresh 回调
onRefresh: controller.kr_subscribeService.kr_refreshAll,
onRefresh: controller.kr_handleRefresh,
// 4. 自定义 Header 样式
header: ClassicHeader(
dragText: '下拉刷新',
@@ -159,7 +195,12 @@ class HINodePageView extends GetView<HINodeListController> {
}) {
return Expanded(
child: GestureDetector(
onTap: onTap,
onTap: () {
// 处理模式按钮点击(用于调试模式激活)
controller.kr_handleModeButtonClick();
// 执行原有的点击逻辑
onTap();
},
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: EdgeInsets.symmetric(vertical: 4.w),
@@ -69,8 +69,12 @@ class HIUserInfoView extends GetView<HIUserInfoController> {
children: [
Obx(() {
final account = KRAppRunData.getInstance().kr_account.value;
final isDeviceLogin = account != null && account.startsWith('9000');
if (isDeviceLogin) return const SizedBox();
return Text(
(account != null && account.isNotEmpty) ? account : '未绑定',
account ?? '',
style: TextStyle(
color: Colors.white,
fontSize: 20.sp,
@@ -425,7 +429,7 @@ class HIUserInfoView extends GetView<HIUserInfoController> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'设备:${_extractDeviceModel(userAgent)}',
'设备:${_extractDeviceModel(userAgent, deviceType)}',
style: TextStyle(
color: Colors.white,
fontSize: 10.sp, //
@@ -433,7 +437,7 @@ class HIUserInfoView extends GetView<HIUserInfoController> {
),
SizedBox(height: 4.h),
Text(
'ID: ${identifier.substring(0, identifier.length > 4 ? 4 : identifier.length)}$id',
'SN: ${identifier.substring(0, identifier.length > 4 ? 4 : identifier.length)}$id',
style: TextStyle(
color: Colors.white.withOpacity(0.9),
fontSize: 10.sp,
@@ -510,21 +514,22 @@ class HIUserInfoView extends GetView<HIUserInfoController> {
),
),
SizedBox(height: 20.w),
Obx(() {
final current = controller.kr_subscribeService.kr_currentSubscribe.value;
Text(
'每个账号最多允许同时使用2台设备同时在线',
style: KrAppTextStyle(
color: Colors.black,
fontSize: 14.sp,
fontWeight: FontWeight.w600,
),
)
/*Obx(() {
// final current = controller.kr_subscribeService.kr_currentSubscribe.value;
// 如果 current 为 null,显示默认值 0
final deviceLimit = current?.deviceLimit ?? 0;
// final deviceLimit = current?.deviceLimit ?? 0;
return Text(
'每个账号最多允许同时使用$deviceLimit台设备同时在线',
style: KrAppTextStyle(
color: Colors.black,
fontSize: 14.sp,
fontWeight: FontWeight.w600,
),
);
}),
return
}),*/
],
),
),
@@ -585,12 +590,12 @@ class HIUserInfoView extends GetView<HIUserInfoController> {
);
}
String _extractDeviceModel(String deviceName) {
String _extractDeviceModel(String deviceName, String deviceType) {
// 匹配括号内内容
final RegExp regExp = RegExp(r'\((.*?)\)');
final Match? match = regExp.firstMatch(deviceName);
if (match != null && match.groupCount >= 1) {
if (match != null && match.groupCount >= 1 ) {
// 获取括号内内容
final inside = match.group(1)!; // "Android; google Pixel 9; 15"
File diff suppressed because it is too large Load Diff
@@ -34,7 +34,7 @@ class HIAnimatedConnectButton extends GetView<KRHomeController> {
print('🔵 Switch UI 更新: status=${status.runtimeType}, isConnected=$isConnected, isSwitching=$isSwitching');
final isShow = isConnected; // delay == -1 || isConnected;
final isShow = delay == -1 || isConnected;
final Color buttonColor = Theme.of(context).primaryColor;
final double screenWidth = Get.width;
@@ -305,19 +305,27 @@ class KRLoginController extends GetxController
/// 发送验证码(仅支持邮箱)
void kr_sendCode() async {
final either = await KRAuthApi().kr_sendCode(
accountController.text,
2
); // 重置密码验证码类型为3
/*
*
* kr_loginStatus.value == KRLoginProgressStatus.kr_registerSendCode
? 2 // 注册验证码类型为2
: 3*/
if (accountController.text.isEmpty) {
KRCommonUtil.kr_showToast(AppTranslations.kr_login.enterAccount);
return;
}
int type;
final check = await KRAuthApi().kr_isRegister(accountController.text);
final result = check.fold((l) {
KRCommonUtil.kr_showToast(l.msg);
return null;
}, (isRegistered) {
print('isRegistered $isRegistered');
return isRegistered ? 2 : 1;
});
if (result == null) return;
type = result;
final either = await KRAuthApi().kr_sendCode(accountController.text, type);
either.fold((l) {
KRCommonUtil.kr_showToast(l.msg);
}, (r) async {
/// 开始倒计时
_startCountdown();
});
}
@@ -76,10 +76,13 @@ class KRLoginView extends GetView<KRLoginController> {
mainAxisAlignment: MainAxisAlignment.center,
children: [
Obx(() {
final _ = KRAppRunData.getInstance().kr_isLogin.value;
final email = KRAppRunData.getInstance().kr_account.value ?? '';
final account = KRAppRunData.getInstance().kr_account.value;
final isDeviceLogin = account != null && account.startsWith('9000');
if (isDeviceLogin) return const SizedBox();
return Text(
email.isNotEmpty ? email : '',
account ?? '',
style: TextStyle(
color: Colors.white,
fontSize: 20.sp,
@@ -285,7 +285,7 @@ class KRSplashController extends GetxController {
KRLogUtil.kr_e('⏱️ 初始化超时: $e', tag: 'SplashController');
print('⏱️ 初始化超时,直接跳转到主页');
// 超时后直接跳转到主页,让用户可以手动重试
// Get.offAllNamed(Routes.KR_MAIN);
// Get.offAllNamed(Routes.KR_HOME);
HIDialog.show(
message: '初始化超时,请检查网络或重试',
confirmText: '重试',
@@ -484,7 +484,7 @@ class KRSplashController extends GetxController {
_initLog.logWarning('网络权限检查失败或超时,执行降级初始化', tag: 'Init');
KRLogUtil.kr_w('⚠️ 网络权限检查失败或超时,执行降级初始化', tag: 'SplashController');
await _executeMinimalInitialization();
Get.offAllNamed(Routes.KR_MAIN);
Get.offAllNamed(Routes.KR_HOME);
return;
}
@@ -493,7 +493,7 @@ class KRSplashController extends GetxController {
_initLog.logError('网络权限检查异常,执行降级初始化', tag: 'Init', error: e);
KRLogUtil.kr_w('⚠️ 网络权限检查异常: $e,执行降级初始化', tag: 'SplashController');
await _executeMinimalInitialization();
Get.offAllNamed(Routes.KR_MAIN);
Get.offAllNamed(Routes.KR_HOME);
return;
}
} else {
@@ -582,7 +582,7 @@ class KRSplashController extends GetxController {
_initLog.logError('启动页初始化异常,执行降级策略', tag: 'Continue', error: e);
KRLogUtil.kr_w('⚠️ 启动页初始化异常,执行降级策略: $e', tag: 'SplashController');
await _executeMinimalInitialization();
Get.offAllNamed(Routes.KR_MAIN);
Get.offAllNamed(Routes.KR_HOME);
}
}
@@ -726,7 +726,7 @@ class KRSplashController extends GetxController {
kr_isLoading.value = false;
// 直接跳转到主页
Get.offAllNamed(Routes.KR_MAIN);
Get.offAllNamed(Routes.KR_HOME);
}
/// 🔧 修复1.1:清理旧的本地存储数据(DEBUG模式专用)
+1 -1
View File
@@ -1,7 +1,7 @@
/// 接口名称
abstract class Api {
/// 游客登录查看是否已经注册
static const String kr_isRegister = "/v1/app/auth/check";
static const String kr_isRegister = "/v1/auth/check";
/// 判断邮箱和当前设备是否已存在订阅
static const String kr_checkSubscription = "/v1/public/user/subscribe_status";
@@ -36,7 +36,7 @@ class KRAuthApi {
BaseResponse<KRIsRegister> baseResponse = await HttpUtil.getInstance()
.request<KRIsRegister>(Api.kr_isRegister, data,
method: HttpMethod.POST, isShowLoading: true);
method: HttpMethod.GET, isShowLoading: true);
if (!baseResponse.isSuccess) {
return left(
@@ -298,7 +298,7 @@ class KRAuthApi {
BaseResponse<dynamic> baseResponse = await HttpUtil.getInstance()
.request<dynamic>(Api.kr_deleteAccount, data,
method: HttpMethod.DELETE, isShowLoading: true);
method: HttpMethod.POST, isShowLoading: true);
if (!baseResponse.isSuccess) {
return left(
HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
@@ -661,6 +661,7 @@ class KRSubscribeService {
// 保存配置
KRSingBoxImp.instance.kr_saveOutbounds(listModel.configJsonList);
// 更新试用和订阅状态
_kr_updateSubscribeStatus();
@@ -722,6 +723,7 @@ class KRSubscribeService {
// 保存配置
KRSingBoxImp.instance.kr_saveOutbounds([]);
}
/// 获取当前订阅
+502 -132
View File
@@ -21,6 +21,7 @@ import '../../../singbox/model/singbox_status.dart';
import '../../utils/kr_country_util.dart';
import '../../utils/kr_log_util.dart';
import '../../utils/kr_secure_storage.dart';
import '../../utils/kr_windows_dns_util.dart';
import '../../common/app_run_data.dart';
import 'package:flutter/foundation.dart';
@@ -98,6 +99,9 @@ class KRSingBoxImp {
/// 活动的出站分组
RxList<SingboxOutboundGroup> kr_activeGroups = <SingboxOutboundGroup>[].obs;
// 🔒 Windows DNS 优化:标志位,记录 DNS 是否已备份
bool _dnsBackedUp = false;
/// 所有的出站分组
RxList<SingboxOutboundGroup> kr_allGroups = <SingboxOutboundGroup>[].obs;
@@ -107,6 +111,9 @@ class KRSingBoxImp {
/// 初始化标志,防止重复初始化
bool _kr_isInitialized = false;
/// 初始化进行中共享 Futuresingle-flight 防并发)
Future<void>? _kr_initFuture;
/// 当前混合代理端口是否就绪
bool get kr_isProxyReady => kr_status.value is SingboxStarted;
@@ -139,12 +146,23 @@ class KRSingBoxImp {
/// 初始化
Future<void> init() async {
// 防止重复初始化
// 防止重复初始化(已完成)
if (_kr_isInitialized) {
KRLogUtil.kr_i('SingBox 已经初始化,跳过重复初始化', tag: 'SingBox');
return;
}
// 防止并发重复初始化(进行中复用同一个 Future)
if (_kr_initFuture != null) {
KRLogUtil.kr_i('SingBox 初始化进行中,等待完成(single-flight', tag: 'SingBox');
await _kr_initFuture;
return;
}
// 建立 single-flight 共享 Future
final completer = Completer<void>();
_kr_initFuture = completer.future;
try {
KRLogUtil.kr_i('开始初始化 SingBox');
// 在应用启动时初始化
@@ -167,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 路径规范化:确保使用正确的路径分隔符
@@ -190,9 +208,9 @@ 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('其他平台路径初始化完成');
}
@@ -400,22 +418,32 @@ 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');
},
);
KRLogUtil.kr_i('✅ SingBox 初始化完成');
// 🔧 提取 geosite 文件到 workingDir
KRLogUtil.kr_i('📦 开始提取 geosite 文件...', tag: 'SingBox');
await _kr_extractGeositeFiles();
KRLogUtil.kr_i('✅ geosite 文件提取完成', tag: 'SingBox');
_kr_isInitialized = true;
// 🔑 关键:在初始化完成后立即订阅状态变化流
// 这样可以确保 UI 始终与 libcore 的实际状态同步
_kr_subscribeToStatus();
KRLogUtil.kr_i('✅ 状态订阅已设置', tag: 'SingBox');
// 完成 single-flight
completer.complete();
_kr_initFuture = null;
} catch (e, stackTrace) {
KRLogUtil.kr_e('❌ SingBox 初始化失败: $e');
KRLogUtil.kr_e('📚 错误堆栈: $stackTrace');
@@ -451,26 +479,87 @@ class KRSingBoxImp {
// 如果初始化失败,允许下次重试
_kr_isInitialized = false;
// 失败时通知等待者并清理 single-flight
if (!completer.isCompleted) {
completer.completeError(e, stackTrace);
}
_kr_initFuture = null;
rethrow;
}
}
/// 提取 geosite 文件到 workingDir
Future<void> _kr_extractGeositeFiles() async {
try {
// 创建 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');
}
// 需要提取的文件列表
final files = ['geoip-cn.srs', 'geosite-cn.srs'];
for (final filename in files) {
final assetPath = 'assets/geosite/$filename';
final targetPath = p.join(geositeDir.path, filename);
final targetFile = File(targetPath);
// 检查文件是否已存在
if (targetFile.existsSync()) {
final fileSize = await targetFile.length();
KRLogUtil.kr_i('📄 $filename 已存在 (${fileSize} bytes), 跳过', tag: 'SingBox');
continue;
}
// 从 assets 加载文件
KRLogUtil.kr_i('📥 正在提取 $filename...', tag: 'SingBox');
final byteData = await rootBundle.load(assetPath);
final bytes = byteData.buffer.asUint8List();
// 写入文件系统
await targetFile.writeAsBytes(bytes);
final writtenSize = await targetFile.length();
KRLogUtil.kr_i('✅ 提取成功: $filename (${writtenSize} bytes)', tag: 'SingBox');
}
KRLogUtil.kr_i('🎉 所有 geosite 文件提取完成', tag: 'SingBox');
} catch (e, stackTrace) {
KRLogUtil.kr_e('❌ 提取 geosite 文件失败: $e', tag: 'SingBox');
KRLogUtil.kr_e('堆栈: $stackTrace', tag: 'SingBox');
// 不抛出异常,让应用继续运行(使用远程规则集作为后备)
}
}
Map<String, dynamic> _getConfigOption() {
// 不使用缓存,每次都重新生成配置
// if (kr_configOption.isNotEmpty) {
// return kr_configOption;
// }
// 🔧 关键修复:全局代理模式下,region 强制设为 'other'libcore 就不会生成国家直连规则
final String effectiveRegion;
if (kr_connectionType.value == KRConnectionType.global) {
effectiveRegion = 'other'; // 全局代理:不添加任何国家规则
KRLogUtil.kr_i('🌐 [全局代理模式] region 设为 other,所有流量走代理', tag: 'SingBox');
} else {
effectiveRegion = KRCountryUtil.kr_getCurrentCountryCode(); // 智能代理:使用用户选择的国家
KRLogUtil.kr_i('✅ [智能代理模式] region 设为 $effectiveRegion', tag: 'SingBox');
}
final op = {
"region": "other", // 参考 hiddify-app: 默认使用 "other" 跳过规则集下载
"region": effectiveRegion, // 🔧 修复:根据出站模式动态设置 region
"block-ads": false, // 参考 hiddify-app: 默认关闭广告拦截
"use-xray-core-when-possible": false,
"execute-config-as-is": false,
"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": "udp://1.1.1.1", // 参考 hiddify-app: 使用 Cloudflare DNS
"remote-dns-address": "https://dns.google/dns-query", // 使用 Google DoH,避免中转节点 DNS 死锁
"remote-dns-domain-strategy": "prefer_ipv4",
"direct-dns-address": "udp://1.1.1.1", // 参考 hiddify-app: 统一使用 1.1.1.1
"direct-dns-address": "local", // 使用系统 DNS,确保中转服务器域名能被解析
"direct-dns-domain-strategy": "prefer_ipv4",
"mixed-port": kr_port,
"tproxy-port": kr_port,
@@ -485,13 +574,13 @@ class KRSingBoxImp {
"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,
"enable-dns-routing": true,
"independent-dns-cache": true,
"rules": [],
"rules": _kr_buildHiddifyRules(),
"mux": {
"enable": false,
"padding": false,
@@ -536,9 +625,65 @@ class KRSingBoxImp {
}
};
kr_configOption = op;
// 🔧 调试日志:确认自定义规则已添加
final rules = op["rules"] as List?;
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');
}
}
return op;
}
List<Map<String, dynamic>> _kr_buildHiddifyRules() {
final rules = <Map<String, dynamic>>[];
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"
});
}
KRLogUtil.kr_i('✅ 节点域名白名单数量: ${nodeDomains.length}', tag: 'SingBox');
KRLogUtil.kr_i('✅ 节点域名白名单集合: ${jsonEncode(nodeDomains.toList())}', tag: 'SingBox');
return rules;
}
Set<String> _kr_collectNodeDomains() {
final set = <String>{};
void addFromOutbound(Map<String, dynamic> o) {
final server = o['server']?.toString();
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) {
set.add(sni.toLowerCase());
}
}
}
for (final g in kr_outbounds) {
addFromOutbound(g);
}
return set;
}
/// 订阅状态变化流
/// 参考 hiddify-app: 监听 libcore 发送的状态事件来自动更新 UI
void _kr_subscribeToStatus() {
@@ -561,7 +706,7 @@ class KRSingBoxImp {
_kr_subscriptions.add(
kr_singBox.watchStatus().listen(
(status) {
(status) {
if (kDebugMode) {
print('🔵 收到 Native 状态更新: ${status.runtimeType}');
}
@@ -599,7 +744,7 @@ class KRSingBoxImp {
// 所以外层必须有 try-catch
final stream = kr_singBox.watchStats();
final subscription = stream.listen(
(stats) {
(stats) {
kr_stats.value = stats;
},
onError: (error) {
@@ -624,7 +769,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;
@@ -651,7 +796,7 @@ class KRSingBoxImp {
_kr_subscriptions.add(
kr_singBox.watchGroups().listen(
(groups) {
(groups) {
print('[watchGroups] 📡 收到所有组更新,数量: ${groups.length}');
kr_allGroups.value = groups;
// 打印每个组的基本信息
@@ -679,7 +824,7 @@ class KRSingBoxImp {
// 查找 "select" 组
final selectGroup = kr_activeGroups.firstWhere(
(group) => group.tag == 'select',
(group) => group.tag == 'select',
orElse: () => throw Exception('未找到 "select" 选择器组'),
);
@@ -719,11 +864,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;
@@ -1010,6 +1155,9 @@ class KRSingBoxImp {
if (outbound['method'] != null) {
KRLogUtil.kr_i(' - method: ${outbound['method']}', tag: 'SingBox');
}
if (outbound['interval'] != null) {
KRLogUtil.kr_i(' - interval: ${outbound['interval']}', tag: 'SingBox');
}
if (outbound['password'] != null) {
KRLogUtil.kr_i(' - password: ${outbound['password']?.toString().substring(0, 8)}...', tag: 'SingBox');
}
@@ -1031,17 +1179,80 @@ class KRSingBoxImp {
KRLogUtil.kr_i('✅ 过滤后节点数量: ${kr_outbounds.length}/${outbounds.length}', tag: 'SingBox');
// 只保存 outboundsMobile.buildConfig() 会添加其他配置
final map = {
"outbounds": kr_outbounds
// 🔧 修复:生成完整的 SingBox 配置
// 之前只保存 {"outbounds": [...]}, 导致 libcore 无法正确处理
// 现在保存完整配置,包含所有必需字段
final Map<String, dynamic> fullConfig = {
"log": {
"level": "debug",
"timestamp": true
},
"dns": {
"servers": [
{
"tag": "dns-remote",
"address": "https://1.1.1.1/dns-query",
"address_resolver": "dns-direct"
},
{
"tag": "dns-direct",
"address": "local",
"detour": "direct"
}
],
"rules": _kr_buildDnsRules(), // ✅ 使用动态构建的 DNS 规则
"final": "dns-remote",
"strategy": "prefer_ipv4"
},
"inbounds": [
{
"type": "tun",
"tag": "tun-in",
"interface_name": "utun",
"inet4_address": "172.19.0.1/30",
"auto_route": true,
"strict_route": true,
"sniff": true,
"sniff_override_destination": false
}
],
"outbounds": [
// 🔧 修复:添加 selector 组,让用户可以手动选择节点
{
"type": "selector",
"tag": "proxy",
"outbounds": kr_outbounds.map((o) => o['tag'] as String).toList(),
"default": kr_outbounds.isNotEmpty ? kr_outbounds[0]['tag'] : "direct",
},
...kr_outbounds,
{
"type": "direct",
"tag": "direct"
},
{
"type": "block",
"tag": "block"
},
{
"type": "dns",
"tag": "dns-out"
}
],
"route": {
"rules": _kr_buildRouteRules(), // ✅ 使用动态构建的路由规则
"rule_set": _kr_buildRuleSets(), // ✅ 使用动态构建的规则集
"final": "proxy", // 🔧 修复:使用 selector 组作为默认出站
"auto_detect_interface": true
}
};
final file = _file(kr_configName);
final temp = _tempFile(kr_configName);
final mapStr = jsonEncode(map);
final mapStr = jsonEncode(fullConfig);
KRLogUtil.kr_i('📄 配置文件内容长度: ${mapStr.length}', tag: 'SingBox');
KRLogUtil.kr_i('📄 完整配置文件内容: $mapStr', tag: 'SingBox');
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');
await file.writeAsString(mapStr);
await temp.writeAsString(mapStr);
@@ -1058,6 +1269,128 @@ class KRSingBoxImp {
KRLogUtil.kr_i('✅ 配置文件保存完成', tag: 'SingBox');
}
/// 构建 DNS 规则
List<Map<String, dynamic>> _kr_buildDnsRules() {
final currentCountryCode = KRCountryUtil.kr_getCurrentCountryCode();
final rules = <Map<String, dynamic>>[];
// 🔧 关键修复:只有在"智能代理"模式下,才根据国家添加直连规则
// 如果是"全局代理"模式,即使选择了国家也不添加直连规则
if (kr_connectionType.value == KRConnectionType.rule && currentCountryCode != 'other') {
rules.add({
"rule_set": [
"geoip-$currentCountryCode",
"geosite-$currentCountryCode",
],
"server": "dns-direct"
});
KRLogUtil.kr_i('✅ [智能代理模式] 添加 DNS 规则: $currentCountryCode 域名使用直连 DNS', tag: 'SingBox');
} else if (kr_connectionType.value == KRConnectionType.global) {
KRLogUtil.kr_i('🌐 [全局代理模式] 跳过国家 DNS 规则,所有DNS查询走代理', tag: 'SingBox');
}
return rules;
}
/// 构建路由规则
List<Map<String, dynamic>> _kr_buildRouteRules() {
final currentCountryCode = KRCountryUtil.kr_getCurrentCountryCode();
final rules = <Map<String, dynamic>>[];
// 基础规则: DNS 查询走 dns-out
rules.add({
"protocol": "dns",
"outbound": "dns-out"
});
// ✅ 自定义域名直连规则(优先级高,放在前面)
// rules.add({
// "domain_suffix": ["ip138.com"],
// "outbound": "direct"
// });
// KRLogUtil.kr_i('✅ 添加自定义域名直连规则: ip138.com -> direct', tag: 'SingBox');
// 🔧 关键修复:只有在"智能代理"模式下,才根据国家添加直连规则
// 如果是"全局代理"模式,即使选择了国家也不添加直连规则
if (kr_connectionType.value == KRConnectionType.rule && currentCountryCode != 'other') {
rules.add({
"rule_set": [
"geoip-$currentCountryCode",
"geosite-$currentCountryCode",
],
"outbound": "direct"
});
KRLogUtil.kr_i('✅ [智能代理模式] 添加路由规则: $currentCountryCode IP/域名直连', tag: 'SingBox');
} else if (kr_connectionType.value == KRConnectionType.global) {
KRLogUtil.kr_i('🌐 [全局代理模式] 跳过国家路由规则,所有流量走代理', tag: 'SingBox');
}
return rules;
}
/// 构建规则集配置
List<Map<String, dynamic>> _kr_buildRuleSets() {
final currentCountryCode = KRCountryUtil.kr_getCurrentCountryCode();
final ruleSets = <Map<String, dynamic>>[];
// 🔧 关键修复:只有在"智能代理"模式下,才加载国家规则集
// 如果是"全局代理"模式,即使选择了国家也不加载规则集
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'));
if (geoipFile.existsSync() && geositeFile.existsSync()) {
// ✅ 使用本地文件
ruleSets.add({
"type": "local",
"tag": "geoip-$currentCountryCode",
"format": "binary",
"path": "./geosite/geoip-$currentCountryCode.srs" // 相对于 workingDir
});
ruleSets.add({
"type": "local",
"tag": "geosite-$currentCountryCode",
"format": "binary",
"path": "./geosite/geosite-$currentCountryCode.srs"
});
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');
} else {
// ❌ 本地文件不存在,使用远程规则集作为后备
KRLogUtil.kr_w('⚠️ 本地规则集不存在,使用远程规则集', 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",
"download_detour": "direct",
"update_interval": "7d"
});
ruleSets.add({
"type": "remote",
"tag": "geosite-$currentCountryCode",
"format": "binary",
"url": "https://raw.githubusercontent.com/hiddify/hiddify-geo/rule-set/country/geosite-$currentCountryCode.srs",
"download_detour": "direct",
"update_interval": "7d"
});
}
}
return ruleSets;
}
Future<void> kr_start() async {
// 不再手动设置状态,libcore 会通过 status stream 自动发送状态更新
try {
@@ -1069,9 +1402,36 @@ class KRSingBoxImp {
// ⚠️ 强制编译标记 - v2.0-lazy-load
KRLogUtil.kr_i('🚀🚀🚀 [v2.0-lazy-load] 开始启动 SingBox...', tag: 'SingBox');
// 🔧 强制重新生成配置文件(确保最新的路由规则生效)
if (kr_outbounds.isNotEmpty) {
KRLogUtil.kr_i('🔄 启动前强制重新生成配置文件...', tag: 'SingBox');
kr_saveOutbounds(kr_outbounds);
// 等待配置文件写入完成
await Future.delayed(const Duration(milliseconds: 100));
}
KRLogUtil.kr_i('📁 配置文件路径: $_cutPath', tag: 'SingBox');
KRLogUtil.kr_i('📝 配置名称: $kr_configName', tag: 'SingBox');
// 🔑 Windows 平台:仅在首次启动时备份 DNS 设置(优化:避免重复备份)
if (Platform.isWindows && !_dnsBackedUp) {
KRLogUtil.kr_i('🪟 Windows 平台,首次启动备份 DNS 设置...', tag: 'SingBox');
try {
final backupSuccess = await KRWindowsDnsUtil.instance.kr_backupDnsSettings();
if (backupSuccess) {
_dnsBackedUp = true; // 标记已备份
KRLogUtil.kr_i('✅ Windows DNS 备份成功', tag: 'SingBox');
} else {
KRLogUtil.kr_w('⚠️ Windows DNS 备份失败,将在停止时使用兜底恢复', tag: 'SingBox');
}
} catch (e) {
KRLogUtil.kr_w('⚠️ Windows DNS 备份异常: $e,将在停止时使用兜底恢复', tag: 'SingBox');
}
} else if (Platform.isWindows && _dnsBackedUp) {
KRLogUtil.kr_i('⏭️ Windows 平台,DNS 已备份,跳过重复备份(节点切换优化)', tag: 'SingBox');
}
// 🔑 先尝试停止旧实例,避免 command.sock 冲突
// 注意:如果没有旧实例,libcore 会报错 "command.sock: no such file",这是正常的
try {
@@ -1089,11 +1449,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');
},
);
@@ -1112,7 +1472,7 @@ class KRSingBoxImp {
_kr_subscribeToStatus();
await kr_singBox.start(_cutPath, kr_configName, false).map(
(r) {
(r) {
KRLogUtil.kr_i('✅ SingBox 启动成功', tag: 'SingBox');
},
).mapLeft((err) {
@@ -1225,12 +1585,12 @@ class KRSingBoxImp {
await Future.delayed(const Duration(milliseconds: 100));
// 添加超时保护,防止 stop() 调用阻塞
// 缩短超时时间,避免用户等待过久
// 🔧 延长超时时间到 10 秒,给 Windows DNS 清理足够时间
try {
await kr_singBox.stop().run().timeout(
const Duration(seconds: 3),
const Duration(seconds: 10), // ← 从 3 秒延长到 10 秒
onTimeout: () {
KRLogUtil.kr_w('⚠️ 停止操作超时(3秒),强制继续', tag: 'SingBox');
KRLogUtil.kr_w('⚠️ 停止操作超时(10秒),强制继续', tag: 'SingBox');
return const Left('timeout');
},
);
@@ -1239,7 +1599,66 @@ class KRSingBoxImp {
// 继续执行清理操作
}
await Future.delayed(const Duration(milliseconds: 500));
// 🔑 Windows 平台:仅在有备份时恢复 DNS 设置
if (Platform.isWindows && _dnsBackedUp) {
KRLogUtil.kr_i('🪟 Windows 平台,等待 sing-box 完全停止...', tag: 'SingBox');
// 🔧 P3优化: 监听状态而非固定延迟,确保 sing-box 真正停止后再恢复 DNS
try {
// 如果当前已经是停止状态,直接继续
if (kr_status.value is SingboxStopped) {
KRLogUtil.kr_i('✅ sing-box 已经是停止状态,立即恢复 DNS', tag: 'SingBox');
} else {
// 等待状态变为停止,最多等待3秒
final completer = Completer<void>();
late final Worker worker;
worker = ever(kr_status, (status) {
if (status is SingboxStopped) {
if (!completer.isCompleted) {
completer.complete();
worker.dispose();
}
}
});
await completer.future.timeout(
const Duration(seconds: 3),
onTimeout: () {
KRLogUtil.kr_w('⏱️ 等待停止状态超时,继续执行 DNS 恢复', tag: 'SingBox');
worker.dispose();
},
);
KRLogUtil.kr_i('✅ sing-box 已完全停止,开始恢复 DNS...', tag: 'SingBox');
}
} catch (e) {
KRLogUtil.kr_w('⚠️ 状态监听异常: $e,继续执行 DNS 恢复', tag: 'SingBox');
}
try {
// 尝试恢复 DNS
final restoreSuccess = await KRWindowsDnsUtil.instance.kr_restoreDnsSettings();
if (restoreSuccess) {
KRLogUtil.kr_i('✅ Windows DNS 恢复成功', tag: 'SingBox');
} else {
KRLogUtil.kr_e('❌ Windows DNS 恢复失败', tag: 'SingBox');
}
} catch (e) {
KRLogUtil.kr_e('❌ Windows DNS 恢复异常: $e', tag: 'SingBox');
// 异常时也会在工具类内部执行兜底恢复
} finally {
// 🔧 P0修复2: 使用 finally 确保标志位始终被重置,避免下次启动跳过备份
_dnsBackedUp = false;
KRLogUtil.kr_i('🔄 重置 DNS 备份标志位', tag: 'SingBox');
}
} else if (Platform.isWindows && !_dnsBackedUp) {
KRLogUtil.kr_i('⏭️ Windows 平台,DNS 未备份,跳过恢复', tag: 'SingBox');
await Future.delayed(const Duration(milliseconds: 500));
} else {
// 非 Windows 平台正常等待
await Future.delayed(const Duration(milliseconds: 500));
}
// 取消统计和分组订阅,但保留状态订阅以便继续接收状态更新
final subscriptionsToCancel = _kr_subscriptions.where((sub) {
@@ -1261,6 +1680,21 @@ class KRSingBoxImp {
} catch (e, stackTrace) {
KRLogUtil.kr_e('停止服务时出错: $e');
KRLogUtil.kr_e('错误堆栈: $stackTrace');
// 🔑 即使出错,也要尝试恢复 Windows DNS
if (Platform.isWindows && _dnsBackedUp) {
KRLogUtil.kr_w('⚠️ 停止异常,强制执行 DNS 恢复', tag: 'SingBox');
try {
await KRWindowsDnsUtil.instance.kr_restoreDnsSettings();
} catch (dnsError) {
KRLogUtil.kr_e('❌ 强制 DNS 恢复失败: $dnsError', tag: 'SingBox');
} finally {
// 🔧 P0修复2: 异常路径也要重置标志位
_dnsBackedUp = false;
KRLogUtil.kr_i('🔄 异常后重置 DNS 备份标志位', tag: 'SingBox');
}
}
// 不手动设置状态,信任 libcore 的状态管理
rethrow;
}
@@ -1284,10 +1718,25 @@ class KRSingBoxImp {
}
Future<void> kr_restart() async {
KRLogUtil.kr_i("restart");
kr_singBox.restart(_cutPath, kr_configName, false).mapLeft((err) {
KRLogUtil.kr_e('重启失败: $err');
}).run();
KRLogUtil.kr_i('🔄 重启 SingBox...', tag: 'SingBox');
// 🔧 修复:使用 stop + start 而不是 restart
// 这样可以触发 Windows DNS 恢复和备份逻辑
try {
// 1. 先停止(会触发 DNS 恢复)
await kr_stop();
// 2. 等待完全停止
await Future.delayed(const Duration(milliseconds: 500));
// 3. 重新启动(会触发 DNS 备份)
await kr_start();
KRLogUtil.kr_i('✅ SingBox 重启完成', tag: 'SingBox');
} catch (e) {
KRLogUtil.kr_e('❌ 重启失败: $e', tag: 'SingBox');
rethrow;
}
}
//// 设置出站模式
@@ -1316,9 +1765,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');
@@ -1429,9 +1878,11 @@ class KRSingBoxImp {
await _kr_ensureCommandClientInitialized();
KRLogUtil.kr_i('✅ Command client 已就绪,执行节点切换', tag: 'SingBox');
// 🔧 关键修复:使用 await 等待节点选择完成
KRLogUtil.kr_i('⏳ 调用 selectOutbound("select", "$tag")...', tag: 'SingBox');
await _kr_selectOutboundWithRetry("select", tag, maxAttempts: 3, initialDelay: 50);
// 🔧 关键修复:使用正确的 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);
KRLogUtil.kr_i('✅ 节点切换API调用完成: $tag', tag: 'SingBox');
// 🔧 新增:验证节点切换是否生效
@@ -1454,8 +1905,8 @@ class KRSingBoxImp {
// 使用 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');
@@ -1465,87 +1916,6 @@ class KRSingBoxImp {
}
}
/// 🌍 国家级节点选择
/// 如果传入 'auto',则调用原有 kr_selectOutbound('auto')
/// 否则根据国家名筛选该国节点,自动选择延迟最低的节点
Future<void> kr_selectCountry(String country) async {
KRLogUtil.kr_i('🌎 [v2.1] 开始选择国家: $country', tag: 'SingBox');
if (country == 'auto') {
KRLogUtil.kr_i('🌀 国家为 auto,执行自动节点选择逻辑', tag: 'SingBox');
await kr_selectOutbound('auto');
return;
}
if (kr_activeGroups.isEmpty) {
KRLogUtil.kr_w('⚠️ kr_activeGroups 当前为空,无法进行国家选择', tag: 'SingBox');
return;
}
/*final selectGroup = kr_activeGroups.firstWhere(
(group) => group.tag == 'select',
orElse: () => throw Exception('未找到 "select" 组'),
);
final countryNodes = selectGroup.items.where((item) {
final nodeCountry = item.options?['country'] ?? '';
return nodeCountry.toString().toLowerCase() == country.toLowerCase();
}).toList();
if (countryNodes.isEmpty) {
KRLogUtil.kr_w('⚠️ 未找到该国家的节点: $country', tag: 'SingBox');
for (var item in selectGroup.items) {
KRLogUtil.kr_d(
' 可用节点: ${item.tag} (${item.options?['country'] ?? '未知'})',
tag: 'SingBox',
);
}
return;
}
KRLogUtil.kr_i('🇨🇭 找到 ${countryNodes.length} 个属于 "$country" 的节点', tag: 'SingBox');
// 按延迟排序(无延迟则 9999)
countryNodes.sort((a, b) =>
(a.urlTestDelay ?? 9999).compareTo(b.urlTestDelay ?? 9999));
final fastestNode = countryNodes.first;
final selectedTag = fastestNode.tag;
final delay = fastestNode.urlTestDelay ?? -1;
KRLogUtil.kr_i('✅ 选中该国家最快节点: $selectedTag (${delay}ms)', tag: 'SingBox');
try {
await KRSecureStorage().kr_saveData(key: 'selected_country', value: country);
await KRSecureStorage().kr_saveData(key: _keySelectedNode, value: selectedTag);
} catch (e) {
KRLogUtil.kr_w('⚠️ 保存国家/节点信息失败: $e', tag: 'SingBox');
}
try {
await _kr_ensureCommandClientInitialized();
await _kr_selectOutboundWithRetry('select', selectedTag,
maxAttempts: 3, initialDelay: 100);
KRLogUtil.kr_i('✅ 国家内节点切换成功: $selectedTag', tag: 'SingBox');
} catch (e) {
KRLogUtil.kr_e('❌ 国家内节点选择失败: $e', tag: 'SingBox');
rethrow;
}
_nodeSelectionTimer?.cancel();
KRLogUtil.kr_i('🔁 启动定时器防止节点被 auto 覆盖', tag: 'SingBox');
_nodeSelectionTimer =
Timer.periodic(const Duration(seconds: 20), (timer) async {
try {
await kr_singBox.selectOutbound('select', selectedTag).run();
KRLogUtil.kr_d('🔁 定时确认国家内节点: $selectedTag', tag: 'SingBox');
} catch (e) {
KRLogUtil.kr_w('🔁 定时确认失败: $e', tag: 'SingBox');
}
});*/
}
/// 配合文件地址
Directory get directory =>
+11 -8
View File
@@ -15,7 +15,8 @@ enum KRCountry {
ru('俄罗斯'),
id('印度尼西亚'),
tr('土耳其'),
br('巴西');
br('巴西'),
other('全局代理'); // ✅ 新增:全局代理模式(所有流量走代理,不添加国家直连规则)
final String kr_name;
const KRCountry(this.kr_name);
@@ -30,7 +31,7 @@ enum KRCountry {
static KRCountry? kr_fromCode(String code) {
try {
return KRCountry.values.firstWhere(
(country) => country.kr_code == code.toLowerCase(),
(country) => country.kr_code == code.toLowerCase(),
);
} catch (e) {
return null;
@@ -53,7 +54,7 @@ class KRCountryUtil {
static Future<void> kr_init() async {
try {
final String? kr_savedCountry =
await _kr_storage.kr_readData(key: _kr_countryKey);
await _kr_storage.kr_readData(key: _kr_countryKey);
if (kr_savedCountry != null) {
final KRCountry? kr_country = KRCountry.kr_fromCode(kr_savedCountry);
if (kr_country != null) {
@@ -106,7 +107,7 @@ class KRCountryUtil {
return kr_currentCountry.value.kr_code;
}
static String kr_getCurrentCountryName() {
static String kr_getCurrentCountryName() {
return kr_getCountryName(kr_currentCountry.value);
}
@@ -131,12 +132,14 @@ class KRCountryUtil {
return AppTranslations.kr_country.tr;
case KRCountry.br:
return AppTranslations.kr_country.br;
case KRCountry.other:
return '全局代理'; // ✅ 新增:全局代理
}
}
/// 获取所有支持的国家列表
static List<KRCountry> kr_getSupportedCountries() {
if (AppConfig().kr_is_daytime == false) {
if (AppConfig().kr_is_daytime == false) {
return KRCountry.values.where((element) => element != KRCountry.cn).toList();
}
return KRCountry.values;
@@ -146,9 +149,9 @@ class KRCountryUtil {
static List<Map<String, String>> kr_getCountryInfoList() {
return KRCountry.values
.map((country) => {
'code': country.kr_code,
'name': country.kr_countryName,
})
'code': country.kr_code,
'name': country.kr_countryName,
})
.toList();
}
}
+101 -77
View File
@@ -1,127 +1,151 @@
import 'dart:io';
import 'dart:async';
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
import 'kr_log_util.dart';
/// 延迟测试工具
/// 提供真实的 TCP 连接延迟测试功能
/// 真正的节点延迟测试工具
class KRLatencyTester {
/// 测试单个节点的延迟
///
/// 参数:
/// - host: 主机地址
/// - port: 端口号
/// - timeout: 超时时间(毫秒)
///
/// 返回:
/// - 延迟时间(毫秒),如果失败返回 65535
static Future<int> testNode({
/// TCP 连接测试延迟(真实测试)
/// 返回延迟毫秒数,失败返回 65535
static Future<int> testTcpLatency({
required String host,
required int port,
int timeout = 5000,
Duration timeout = const Duration(seconds: 5),
}) async {
try {
final stopwatch = Stopwatch()..start();
Socket? socket;
final stopwatch = Stopwatch();
final socket = await Socket.connect(
try {
KRLogUtil.kr_i('🔌 开始测试: $host:$port', tag: 'LatencyTester');
stopwatch.start();
// 尝试 TCP 连接
socket = await Socket.connect(
host,
port,
timeout: Duration(milliseconds: timeout),
).timeout(Duration(milliseconds: timeout));
timeout: timeout,
);
stopwatch.stop();
// 立即关闭连接
await socket.close();
socket.destroy();
final latency = stopwatch.elapsedMilliseconds;
KRLogUtil.kr_i('✅ 延迟测试成功: $host:$port = ${latency}ms', tag: 'KRLatencyTester');
KRLogUtil.kr_i('✅ 测试成功: $host:$port - ${latency}ms', tag: 'LatencyTester');
return latency;
} on SocketException catch (e) {
stopwatch.stop();
KRLogUtil.kr_w('❌ 连接失败: $host:$port - ${e.message}', tag: 'LatencyTester');
return 65535;
} on TimeoutException catch (e) {
stopwatch.stop();
KRLogUtil.kr_w('⏱️ 连接超时: $host:$port - $e', tag: 'LatencyTester');
return 65535;
} catch (e) {
KRLogUtil.kr_w('❌ 延迟测试失败: $host:$port - $e', tag: 'KRLatencyTester');
return 65535; // 测试失败返回最大值
stopwatch.stop();
KRLogUtil.kr_e('❌ 测试异常: $host:$port - $e', tag: 'LatencyTester');
return 65535;
} finally {
// 确保关闭连接
try {
await socket?.close();
} catch (e) {
// 忽略关闭错误
}
}
}
/// 批量测试多个节点延迟
///
/// 参数:
/// - nodes: 节点列表,格式为 [{"host": "example.com", "port": 443}]
/// - concurrency: 并发数量
/// - timeout: 超时时间(毫秒)
///
/// 返回:
/// - 测试结果映射,键为 "host:port",值为延迟时间
/// 批量测试多个节点延迟(并发测试)
/// 返回 Map<节点tag, 延迟ms>
static Future<Map<String, int>> testMultipleNodes({
required List<MapEntry<String, SocketAddress>> nodes,
int concurrency = 10,
int timeout = 5000,
int concurrency = 10, // 并发数
Duration timeout = const Duration(seconds: 5),
}) async {
final results = <String, int>{};
final semaphore = Completer<void>();
var activeCount = 0;
var completedCount = 0;
final List<List<MapEntry<String, SocketAddress>>> batches = [];
KRLogUtil.kr_i('🚀 开始批量延迟测试,共 ${nodes.length} 个节点,并发数: $concurrency', tag: 'KRLatencyTester');
// 分批处理
for (int i = 0; i < nodes.length; i += concurrency) {
batches.add(
nodes.sublist(i, i + concurrency > nodes.length ? nodes.length : i + concurrency)
);
}
Future<void> processNode(MapEntry<String, SocketAddress> node) async {
try {
final host = node.value.address;
final port = node.value.port;
final key = node.key;
KRLogUtil.kr_i('📊 开始批量测试: ${nodes.length} 个节点,分 ${batches.length} 批,每批 $concurrency', tag: 'LatencyTester');
final latency = await testNode(
host: host,
port: port,
int completedCount = 0;
// 逐批测试
for (int batchIndex = 0; batchIndex < batches.length; batchIndex++) {
final batch = batches[batchIndex];
KRLogUtil.kr_i('📦 测试第 ${batchIndex + 1}/${batches.length} 批(${batch.length} 个节点)', tag: 'LatencyTester');
// 并发测试当前批次
final futures = batch.map((node) async {
final tag = node.key;
final address = node.value;
final latency = await testTcpLatency(
host: address.host,
port: address.port,
timeout: timeout,
);
results[key] = latency;
} catch (e) {
results[node.key] = 65535;
KRLogUtil.kr_e('❌ 节点测试异常: ${node.key} - $e', tag: 'KRLatencyTester');
} finally {
completedCount++;
activeCount--;
if (completedCount >= nodes.length) {
semaphore.complete();
if (completedCount % 5 == 0 || completedCount == nodes.length) {
KRLogUtil.kr_i('📈 测试进度: $completedCount/${nodes.length}', tag: 'LatencyTester');
}
}
}
// 分批处理节点
for (var i = 0; i < nodes.length; i += concurrency) {
final batch = nodes.skip(i).take(concurrency);
for (final node in batch) {
activeCount++;
processNode(node);
}
return MapEntry(tag, latency);
}).toList();
// 等待当前批次完成
if (i + concurrency < nodes.length) {
await Future.delayed(Duration(milliseconds: 100));
final batchResults = await Future.wait(futures);
// 收集结果
for (final result in batchResults) {
results[result.key] = result.value;
}
}
// 等待所有任务完成
await semaphore.future;
// 统计结果
final successCount = results.values.where((latency) => latency < 65535).length;
final failCount = results.length - successCount;
KRLogUtil.kr_i('✅ 批量测试完成', tag: 'LatencyTester');
KRLogUtil.kr_i('📊 成功: $successCount, 失败: $failCount', tag: 'LatencyTester');
// 显示延迟最低的前3个
final successNodes = results.entries
.where((e) => e.value < 65535)
.toList()
..sort((a, b) => a.value.compareTo(b.value));
if (successNodes.isNotEmpty) {
KRLogUtil.kr_i('🏆 延迟最低的前3个节点:', tag: 'LatencyTester');
for (int i = 0; i < 3 && i < successNodes.length; i++) {
KRLogUtil.kr_i(' ${i + 1}. ${successNodes[i].key}: ${successNodes[i].value}ms', tag: 'LatencyTester');
}
}
KRLogUtil.kr_i('✅ 批量延迟测试完成,成功: ${results.length}', tag: 'KRLatencyTester');
return results;
}
}
/// Socket 地址类
/// 表示网络地址和端口的组合
/// 节点地址信息
class SocketAddress {
final String address;
final String host;
final int port;
SocketAddress(this.address, this.port);
SocketAddress(this.host, this.port);
@override
String toString() => '$address:$port';
String toString() => '$host:$port';
}
+3 -3
View File
@@ -26,7 +26,7 @@ class KRWindowManager with WindowListener, TrayListener {
const WindowOptions windowOptions = WindowOptions(
size: Size(800, 668),
minimumSize: Size(800, 668),
minimumSize: Size(400, 334),
center: true,
backgroundColor: Colors.white,
skipTaskbar: false,
@@ -47,7 +47,7 @@ class KRWindowManager with WindowListener, TrayListener {
await windowManager.setTitleBarStyle(TitleBarStyle.normal);
await windowManager.setTitle('HiFastVPN');
await windowManager.setSize(const Size(800, 668));
await windowManager.setMinimumSize(const Size(800, 668));
await windowManager.setMinimumSize(const Size(400, 334));
await windowManager.center();
await windowManager.show();
// 阻止窗口关闭
@@ -55,7 +55,7 @@ class KRWindowManager with WindowListener, TrayListener {
} else {
await windowManager.setTitle('Kaer VPN');
await windowManager.setSize(const Size(800, 668));
await windowManager.setMinimumSize(const Size(800, 668));
await windowManager.setMinimumSize(const Size(400, 334));
await windowManager.center();
}
+439
View File
@@ -0,0 +1,439 @@
import 'dart:io';
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
/// Windows DNS 管理工具类
///
/// 用于在 Windows 平台上管理系统 DNS 设置
/// 主要功能:
/// 1. 备份原始 DNS 设置
/// 2. 恢复 DNS 设置
/// 3. 兜底设置为国内公共 DNS (223.5.5.5 和 114.114.114.114)
class KRWindowsDnsUtil {
/// 私有构造函数
KRWindowsDnsUtil._();
/// 单例实例
static final KRWindowsDnsUtil _instance = KRWindowsDnsUtil._();
/// 工厂构造函数
factory KRWindowsDnsUtil() => _instance;
/// 获取实例的静态方法
static KRWindowsDnsUtil get instance => _instance;
/// 原始 DNS 服务器地址(连接前备份)
List<String>? _originalDnsServers;
/// 主网络接口名称
String? _primaryInterfaceName;
/// 备份当前 DNS 设置
///
/// 在连接 VPN 之前调用,保存原始 DNS 配置
/// 返回:true-成功,false-失败
Future<bool> kr_backupDnsSettings() async {
if (!Platform.isWindows) {
KRLogUtil.kr_w('❌ 非 Windows 平台,跳过 DNS 备份', tag: 'WindowsDNS');
return false;
}
try {
KRLogUtil.kr_i('📦 开始备份 Windows DNS 设置...', tag: 'WindowsDNS');
// 1. 获取主网络接口
final interfaceName = await _kr_getPrimaryNetworkInterface();
if (interfaceName == null) {
KRLogUtil.kr_e('❌ 无法获取主网络接口', tag: 'WindowsDNS');
return false;
}
_primaryInterfaceName = interfaceName;
KRLogUtil.kr_i('🔍 主网络接口: $_primaryInterfaceName', tag: 'WindowsDNS');
// 2. 获取当前 DNS 服务器
final dnsServers = await _kr_getCurrentDnsServers(interfaceName);
if (dnsServers.isEmpty) {
KRLogUtil.kr_w('⚠️ 当前 DNS 为空,可能是自动获取', tag: 'WindowsDNS');
_originalDnsServers = []; // 空列表表示 DHCP 自动获取
} else {
_originalDnsServers = dnsServers;
KRLogUtil.kr_i('✅ 已备份 DNS: ${dnsServers.join(", ")}', tag: 'WindowsDNS');
}
return true;
} catch (e) {
KRLogUtil.kr_e('❌ 备份 DNS 设置失败: $e', tag: 'WindowsDNS');
return false;
}
}
/// 恢复原始 DNS 设置
///
/// 在断开 VPN 后调用,恢复备份的 DNS 配置
/// 如果恢复失败,会自动调用兜底机制设置为公共DNS
/// 返回:true-成功,false-失败
Future<bool> kr_restoreDnsSettings() async {
if (!Platform.isWindows) {
KRLogUtil.kr_w('❌ 非 Windows 平台,跳过 DNS 恢复', tag: 'WindowsDNS');
return false;
}
try {
KRLogUtil.kr_i('🔄 开始恢复 Windows DNS 设置...', tag: 'WindowsDNS');
// 1. 检查是否有备份
if (_primaryInterfaceName == null) {
KRLogUtil.kr_w('⚠️ 没有备份的网络接口,尝试自动检测', tag: 'WindowsDNS');
_primaryInterfaceName = await _kr_getPrimaryNetworkInterface();
if (_primaryInterfaceName == null) {
KRLogUtil.kr_e('❌ 无法检测网络接口,执行兜底恢复', tag: 'WindowsDNS');
return await _kr_fallbackRestoreDns();
}
}
// 2. 恢复原始 DNS
if (_originalDnsServers == null) {
KRLogUtil.kr_w('⚠️ 没有备份的 DNS,执行兜底恢复', tag: 'WindowsDNS');
return await _kr_fallbackRestoreDns();
}
if (_originalDnsServers!.isEmpty) {
// 原本是 DHCP 自动获取
KRLogUtil.kr_i('🔄 恢复为 DHCP 自动获取 DNS', tag: 'WindowsDNS');
final success = await _kr_setDnsToAuto(_primaryInterfaceName!);
if (!success) {
KRLogUtil.kr_w('⚠️ 恢复 DHCP 失败,执行兜底恢复', tag: 'WindowsDNS');
return await _kr_fallbackRestoreDns();
}
} else {
// 恢复指定的 DNS 服务器
KRLogUtil.kr_i('🔄 恢复原始 DNS: ${_originalDnsServers!.join(", ")}', tag: 'WindowsDNS');
final success = await _kr_setDnsServers(
_primaryInterfaceName!,
_originalDnsServers!,
);
if (!success) {
KRLogUtil.kr_w('⚠️ 恢复原始 DNS 失败,执行兜底恢复', tag: 'WindowsDNS');
return await _kr_fallbackRestoreDns();
}
}
// 3. 验证 DNS 是否恢复成功
await Future.delayed(const Duration(milliseconds: 500));
final currentDns = await _kr_getCurrentDnsServers(_primaryInterfaceName!);
KRLogUtil.kr_i('✅ 当前 DNS: ${currentDns.join(", ")}', tag: 'WindowsDNS');
// 4. 额外验证:确认 DNS 不再指向 127.0.0.1sing-box 的本地 DNS
final hasLocalhost = currentDns.any((dns) => dns.startsWith('127.'));
if (hasLocalhost) {
KRLogUtil.kr_w('⚠️ DNS 仍包含 127.0.0.1,可能未完全恢复', tag: 'WindowsDNS');
KRLogUtil.kr_w('⚠️ 执行兜底恢复', tag: 'WindowsDNS');
return await _kr_fallbackRestoreDns();
}
return true;
} catch (e) {
KRLogUtil.kr_e('❌ 恢复 DNS 设置失败: $e', tag: 'WindowsDNS');
KRLogUtil.kr_w('⚠️ 执行兜底恢复', tag: 'WindowsDNS');
return await _kr_fallbackRestoreDns();
}
}
/// 兜底恢复:强制设置为国内公共 DNS
///
/// 当正常恢复失败时,设置为安全的公共 DNS 服务器
/// - 主 DNS: 223.5.5.5 (阿里云)
/// - 备用 DNS: 114.114.114.114 (114DNS)
Future<bool> _kr_fallbackRestoreDns() async {
try {
KRLogUtil.kr_w('🆘 执行 DNS 兜底恢复机制', tag: 'WindowsDNS');
KRLogUtil.kr_i('🔧 设置为国内公共 DNS: 223.5.5.5, 114.114.114.114', tag: 'WindowsDNS');
// 1. 获取主网络接口(如果还没有)
if (_primaryInterfaceName == null) {
_primaryInterfaceName = await _kr_getPrimaryNetworkInterface();
if (_primaryInterfaceName == null) {
KRLogUtil.kr_e('❌ 无法检测网络接口,兜底恢复失败', tag: 'WindowsDNS');
return false;
}
}
// 2. 设置为公共 DNS
final fallbackDns = ['223.5.5.5', '114.114.114.114'];
final success = await _kr_setDnsServers(_primaryInterfaceName!, fallbackDns);
if (success) {
KRLogUtil.kr_i('✅ 兜底 DNS 设置成功', tag: 'WindowsDNS');
// 3. 验证设置
await Future.delayed(const Duration(milliseconds: 500));
final currentDns = await _kr_getCurrentDnsServers(_primaryInterfaceName!);
KRLogUtil.kr_i('✅ 验证当前 DNS: ${currentDns.join(", ")}', tag: 'WindowsDNS');
return true;
} else {
KRLogUtil.kr_e('❌ 兜底 DNS 设置失败', tag: 'WindowsDNS');
return false;
}
} catch (e) {
KRLogUtil.kr_e('❌ 兜底恢复失败: $e', tag: 'WindowsDNS');
return false;
}
}
/// 获取主网络接口名称
///
/// 通过 netsh 命令查找处于"已连接"状态的主网络接口
/// 返回:接口名称,失败返回 null
Future<String?> _kr_getPrimaryNetworkInterface() async {
try {
// 使用 netsh 获取接口列表
final result = await Process.run('netsh', ['interface', 'show', 'interface']);
if (result.exitCode != 0) {
KRLogUtil.kr_e('❌ 获取网络接口失败: ${result.stderr}', tag: 'WindowsDNS');
return null;
}
final output = result.stdout.toString();
final lines = output.split('\n');
// 收集所有已连接的接口
final connectedInterfaces = <String>[];
// 查找所有"已连接"的接口
// 支持中文和英文 Windows 系统
for (var line in lines) {
// 中文: "已连接", 英文: "Connected", "Enabled"
if (line.contains('已连接') ||
line.contains('Connected') ||
line.toLowerCase().contains('enabled')) {
// 跳过表头行
if (line.contains('Admin') ||
line.contains('管理') ||
line.contains('State') ||
line.contains('状态')) {
continue;
}
// 解析接口名称(最后一列)
final parts = line.trim().split(RegExp(r'\s{2,}'));
if (parts.length >= 4) {
final interfaceName = parts.last.trim();
// 排除空接口名
if (interfaceName.isNotEmpty && interfaceName.length > 1) {
connectedInterfaces.add(interfaceName);
}
}
}
}
if (connectedInterfaces.isEmpty) {
KRLogUtil.kr_w('⚠️ 未找到已连接的网络接口', tag: 'WindowsDNS');
return null;
}
// 🔧 优化:优先选择有线网络(以太网),然后才是 Wi-Fi
// 有线网络通常更稳定
String? selectedInterface;
for (var interface in connectedInterfaces) {
final lowerName = interface.toLowerCase();
// 优先选择以太网
if (lowerName.contains('ethernet') ||
lowerName.contains('以太网') ||
lowerName.contains('lan') ||
lowerName.contains('local')) {
selectedInterface = interface;
KRLogUtil.kr_i('🔍 选择有线网络接口: $interface', tag: 'WindowsDNS');
break;
}
}
// 如果没有有线网络,选择第一个(通常是 Wi-Fi)
selectedInterface ??= connectedInterfaces.first;
if (selectedInterface != connectedInterfaces.first) {
KRLogUtil.kr_d('🔍 选择网络接口: $selectedInterface', tag: 'WindowsDNS');
} else {
KRLogUtil.kr_i('🔍 选择网络接口: $selectedInterface', tag: 'WindowsDNS');
}
return selectedInterface;
} catch (e) {
KRLogUtil.kr_e('❌ 获取网络接口异常: $e', tag: 'WindowsDNS');
return null;
}
}
/// 获取指定接口的当前 DNS 服务器
///
/// 参数:
/// - interfaceName: 网络接口名称
///
/// 返回:DNS 服务器列表
Future<List<String>> _kr_getCurrentDnsServers(String interfaceName) async {
try {
final result = await Process.run('netsh', [
'interface',
'ipv4',
'show',
'dnsservers',
'name="$interfaceName"',
]);
if (result.exitCode != 0) {
KRLogUtil.kr_e('❌ 获取 DNS 失败: ${result.stderr}', tag: 'WindowsDNS');
return [];
}
final output = result.stdout.toString();
final dnsServers = <String>[];
// 解析 DNS 服务器地址
final lines = output.split('\n');
for (var line in lines) {
// 查找 IP 地址格式的行
final ipMatch = RegExp(r'\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b').firstMatch(line);
if (ipMatch != null) {
final ip = ipMatch.group(0)!;
// 排除本地回环地址
if (!ip.startsWith('127.')) {
dnsServers.add(ip);
}
}
}
KRLogUtil.kr_d('🔍 当前 DNS: ${dnsServers.join(", ")}', tag: 'WindowsDNS');
return dnsServers;
} catch (e) {
KRLogUtil.kr_e('❌ 获取 DNS 异常: $e', tag: 'WindowsDNS');
return [];
}
}
/// 设置指定接口的 DNS 服务器
///
/// 参数:
/// - interfaceName: 网络接口名称
/// - dnsServers: DNS 服务器列表
///
/// 返回:true-成功,false-失败
Future<bool> _kr_setDnsServers(String interfaceName, List<String> dnsServers) async {
try {
if (dnsServers.isEmpty) {
KRLogUtil.kr_w('⚠️ DNS 列表为空,无法设置', tag: 'WindowsDNS');
return false;
}
// 1. 设置主 DNS
KRLogUtil.kr_i('🔧 设置主 DNS: ${dnsServers[0]}', tag: 'WindowsDNS');
var result = await Process.run('netsh', [
'interface',
'ipv4',
'set',
'dnsservers',
'name="$interfaceName"',
'source=static',
'address=${dnsServers[0]}',
'validate=no',
]);
if (result.exitCode != 0) {
KRLogUtil.kr_e('❌ 设置主 DNS 失败: ${result.stderr}', tag: 'WindowsDNS');
return false;
}
// 2. 设置备用 DNS(如果有)
if (dnsServers.length > 1) {
for (int i = 1; i < dnsServers.length; i++) {
KRLogUtil.kr_i('🔧 设置备用 DNS ${i}: ${dnsServers[i]}', tag: 'WindowsDNS');
result = await Process.run('netsh', [
'interface',
'ipv4',
'add',
'dnsservers',
'name="$interfaceName"',
'address=${dnsServers[i]}',
'index=${i + 1}',
'validate=no',
]);
if (result.exitCode != 0) {
KRLogUtil.kr_w('⚠️ 设置备用 DNS $i 失败: ${result.stderr}', tag: 'WindowsDNS');
// 继续设置下一个,不中断
}
}
}
// 3. 刷新 DNS 缓存
await _kr_flushDnsCache();
KRLogUtil.kr_i('✅ DNS 服务器设置完成', tag: 'WindowsDNS');
return true;
} catch (e) {
KRLogUtil.kr_e('❌ 设置 DNS 异常: $e', tag: 'WindowsDNS');
return false;
}
}
/// 设置 DNS 为自动获取(DHCP
///
/// 参数:
/// - interfaceName: 网络接口名称
///
/// 返回:true-成功,false-失败
Future<bool> _kr_setDnsToAuto(String interfaceName) async {
try {
KRLogUtil.kr_i('🔧 设置 DNS 为自动获取 (DHCP)', tag: 'WindowsDNS');
final result = await Process.run('netsh', [
'interface',
'ipv4',
'set',
'dnsservers',
'name="$interfaceName"',
'source=dhcp',
]);
if (result.exitCode != 0) {
KRLogUtil.kr_e('❌ 设置 DHCP 失败: ${result.stderr}', tag: 'WindowsDNS');
return false;
}
// 刷新 DNS 缓存
await _kr_flushDnsCache();
KRLogUtil.kr_i('✅ DNS 已设置为自动获取', tag: 'WindowsDNS');
return true;
} catch (e) {
KRLogUtil.kr_e('❌ 设置 DHCP 异常: $e', tag: 'WindowsDNS');
return false;
}
}
/// 刷新 DNS 缓存
///
/// 执行 ipconfig /flushdns 命令清空 DNS 解析缓存
Future<void> _kr_flushDnsCache() async {
try {
KRLogUtil.kr_i('🔄 刷新 DNS 缓存...', tag: 'WindowsDNS');
final result = await Process.run('ipconfig', ['/flushdns']);
if (result.exitCode == 0) {
KRLogUtil.kr_i('✅ DNS 缓存已刷新', tag: 'WindowsDNS');
} else {
KRLogUtil.kr_w('⚠️ 刷新 DNS 缓存失败: ${result.stderr}', tag: 'WindowsDNS');
}
} catch (e) {
KRLogUtil.kr_w('⚠️ 刷新 DNS 缓存异常: $e', tag: 'WindowsDNS');
}
}
/// 清除备份数据
///
/// 在应用退出或不需要时调用
void kr_clearBackup() {
_originalDnsServers = null;
_primaryInterfaceName = null;
KRLogUtil.kr_d('🗑️ 已清除 DNS 备份数据', tag: 'WindowsDNS');
}
}
+15 -9
View File
@@ -571,10 +571,11 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
CODE_SIGN_STYLE = Automatic;
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application";
CODE_SIGN_STYLE = Manual;
COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=macosx*]" = NJRRF427XB;
ENABLE_HARDENED_RUNTIME = NO;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = HiFastVPN;
@@ -583,8 +584,9 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.15;
PRODUCT_BUNDLE_IDENTIFIER = app.hi.com;
PRODUCT_BUNDLE_IDENTIFIER = com.taw.hifastvpn.mac;
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "";
SWIFT_VERSION = 5.0;
};
name = Profile;
@@ -704,10 +706,11 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
CODE_SIGN_STYLE = Automatic;
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application";
CODE_SIGN_STYLE = Manual;
COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=macosx*]" = NJRRF427XB;
ENABLE_HARDENED_RUNTIME = NO;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = HiFastVPN;
@@ -716,8 +719,9 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.15;
PRODUCT_BUNDLE_IDENTIFIER = app.hi.com;
PRODUCT_BUNDLE_IDENTIFIER = com.taw.hifastvpn.mac;
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
};
@@ -731,10 +735,11 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
CODE_SIGN_STYLE = Automatic;
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application";
CODE_SIGN_STYLE = Manual;
COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=macosx*]" = NJRRF427XB;
ENABLE_HARDENED_RUNTIME = NO;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = HiFastVPN;
@@ -743,8 +748,9 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.15;
PRODUCT_BUNDLE_IDENTIFIER = app.hi.com;
PRODUCT_BUNDLE_IDENTIFIER = com.taw.hifastvpn.mac;
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "";
SWIFT_VERSION = 5.0;
};
name = Release;
+1 -1
View File
@@ -8,7 +8,7 @@
PRODUCT_NAME = HiFastVPN
// The application's bundle identifier
PRODUCT_BUNDLE_IDENTIFIER = app.hi.com
PRODUCT_BUNDLE_IDENTIFIER = app.taw.hifastvpn.mac
// The copyright displayed in application information
PRODUCT_COPYRIGHT = Copyright © 2025 HiFastVPN.com. All rights reserved.
+2 -2
View File
@@ -2,10 +2,10 @@
// 用于发布版本的签名设置
// 使用 Developer ID Application 证书进行签名(用于 App Store 外分发)
CODE_SIGN_IDENTITY = Developer ID Application: Civil Rights Corps (3UR892FAP3)
CODE_SIGN_IDENTITY = Developer ID Application: Civil Rights Corps (NJRRF427XB)
// 开发团队 ID
DEVELOPMENT_TEAM = 3UR892FAP3
DEVELOPMENT_TEAM = NJRRF427XB
// 启用代码签名
CODE_SIGN_STYLE = Manual
+1 -1
View File
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 0.0.1+1
version: 0.0.1
environment:
sdk: '>=3.5.0 <4.0.0'
+5
View File
@@ -126,6 +126,11 @@ install(FILES "../libcore/bin/HiFastVPNCli.exe"
COMPONENT Runtime
OPTIONAL)
install(FILES "runner/resources/app_icon.ico"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime
OPTIONAL)
if(PLUGIN_BUNDLED_LIBRARIES)
install(FILES "${PLUGIN_BUNDLED_LIBRARIES}"
+1
View File
@@ -7,6 +7,7 @@ echo 正在复制必要的 DLL 文件...
copy "%VC_REDIST_DIR%\msvcp140.dll" "%DLL_DIR%"
copy "%VC_REDIST_DIR%\vcruntime140.dll" "%DLL_DIR%"
copy "%VC_REDIST_DIR%\vcruntime140_1.dll" "%DLL_DIR%"
copy "%~dp0..\libcore\libcore.dll" "%DLL_DIR%"
echo DLL 文件复制完成!
pause
+5 -1
View File
@@ -14,6 +14,8 @@ Compression=lzma
SolidCompression=yes
SetupIconFile={{SETUP_ICON_FILE}}
WizardStyle=modern
LanguageDetectionMethod=uilanguage
UsePreviousTasks=no
PrivilegesRequired={{PRIVILEGES_REQUIRED}}
ArchitecturesAllowed=x64
ArchitecturesInstallIn64BitMode=x64
@@ -26,6 +28,8 @@ CloseApplications=force
{% if locale == 'bg' %}Name: "bulgarian"; MessagesFile: "compiler:Languages\\Bulgarian.isl"{% endif %}
{% if locale == 'ca' %}Name: "catalan"; MessagesFile: "compiler:Languages\\Catalan.isl"{% endif %}
{% if locale == 'zh' %}Name: "chinesesimplified"; MessagesFile: "compiler:Languages\\ChineseSimplified.isl"{% endif %}
{% if locale == 'zh-cn' %}Name: "chinesesimplified"; MessagesFile: "compiler:Languages\\ChineseSimplified.isl"{% endif %}
{% if locale == 'zh_CN' %}Name: "chinesesimplified"; MessagesFile: "compiler:Languages\\ChineseSimplified.isl"{% endif %}
{% if locale == 'co' %}Name: "corsican"; MessagesFile: "compiler:Languages\\Corsican.isl"{% endif %}
{% if locale == 'cs' %}Name: "czech"; MessagesFile: "compiler:Languages\\Czech.isl"{% endif %}
{% if locale == 'da' %}Name: "danish"; MessagesFile: "compiler:Languages\\Danish.isl"{% endif %}
@@ -50,7 +54,7 @@ CloseApplications=force
[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: checkedonce
Name: "launchAtStartup"; Description: "{cm:AutoStartProgram,{{DISPLAY_NAME}}}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: {% if LAUNCH_AT_STARTUP != true %}unchecked{% else %}checkedonce{% endif %}
Name: "launchAtStartup"; Description: "{cm:AutoStartProgram,{{DISPLAY_NAME}}}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: checkedonce
[Files]
Source: "{{SOURCE_DIR}}\\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
+5 -1
View File
@@ -6,8 +6,11 @@ executable_name: HiFastVPN.exe
output_base_file_name: HiFastVPN.exe
create_desktop_icon: true
install_dir_name: "{autopf64}\\HiFastVPN"
setup_icon_file: ..\..\windows\runner\resources\app_icon.ico
setup_icon_file: C:\Users\xprotocal\Downloads\hi-client\windows\runner\resources\app_icon.ico
locales:
- zh
- zh-cn
- zh_CN
- ar
- en
- fa
@@ -15,3 +18,4 @@ locales:
- pt
- tr
script_template: inno_setup.sas
languages_dir: compiler:Languages
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB