Compare commits
6 Commits
d99f588a5b
..
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| a7df3a44a2 | |||
| 7956349c0a | |||
| 8ccfc6d272 | |||
| f526122f33 | |||
| b86d645acf | |||
| ef1f3bfb50 |
+120
-2
@@ -307,10 +307,128 @@ jobs:
|
|||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: windows-debug-build
|
name: windows-debug-build
|
||||||
path: build/windows/runner/Debug/
|
path: build/windows/x64/runner/Debug/
|
||||||
|
|
||||||
- name: Upload Release build artifacts
|
- name: Upload Release build artifacts
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: windows-release-build
|
name: windows-release-build
|
||||||
path: build/windows/runner/Release/
|
path: build/windows/x64/runner/Release/
|
||||||
|
|
||||||
|
- name: Install Build Tools
|
||||||
|
shell: powershell
|
||||||
|
run: |
|
||||||
|
choco install 7zip -y
|
||||||
|
choco install enigma-virtual-box -y
|
||||||
|
|
||||||
|
- name: Package Single EXE
|
||||||
|
shell: powershell
|
||||||
|
run: |
|
||||||
|
Write-Host "=== 开始打包单文件 EXE ==="
|
||||||
|
|
||||||
|
$buildPath = "build\windows\x64\runner\Release"
|
||||||
|
$outputPath = "dist"
|
||||||
|
$enigmaPath = "C:\Program Files\Enigma Virtual Box\enigmavb.exe"
|
||||||
|
|
||||||
|
# 创建输出目录
|
||||||
|
if (-not (Test-Path $outputPath)) {
|
||||||
|
New-Item -ItemType Directory -Path $outputPath -Force | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
# 获取主程序
|
||||||
|
$exeFile = Get-ChildItem -Path $buildPath -Filter "*.exe" | Select-Object -First 1
|
||||||
|
if (-not $exeFile) {
|
||||||
|
Write-Host "❌ 未找到可执行文件"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
$inputExe = $exeFile.FullName
|
||||||
|
$outputExe = "$outputPath\$($exeFile.BaseName)_Single.exe"
|
||||||
|
|
||||||
|
Write-Host "主程序: $inputExe"
|
||||||
|
Write-Host "输出: $outputExe"
|
||||||
|
|
||||||
|
# 尝试使用 Enigma Virtual Box
|
||||||
|
if (Test-Path $enigmaPath) {
|
||||||
|
Write-Host "使用 Enigma Virtual Box 打包..."
|
||||||
|
|
||||||
|
# 创建配置文件
|
||||||
|
$configContent = "[Config]`nInputFile=$inputExe`nOutputFile=$outputExe`nFiles=%DEFAULT FOLDER%`nVirtualizationMode=Never Write To Disk`nCompression=Yes`nShareVirtualSystem=Yes`n`n[Files]`nFolder=$buildPath\*"
|
||||||
|
|
||||||
|
$configFile = "$outputPath\package_config.evb"
|
||||||
|
Set-Content -Path $configFile -Value $configContent
|
||||||
|
|
||||||
|
# 执行打包
|
||||||
|
$process = Start-Process -FilePath $enigmaPath -ArgumentList "/sf", $inputExe, "/lf", $outputExe, "/folder", $buildPath, "/compress" -Wait -PassThru -NoNewWindow
|
||||||
|
|
||||||
|
if ($process.ExitCode -eq 0) {
|
||||||
|
Write-Host "✅ 单文件打包成功!"
|
||||||
|
|
||||||
|
# 显示压缩信息
|
||||||
|
$originalSize = (Get-Item $inputExe).Length / 1MB
|
||||||
|
$packedSize = (Get-Item $outputExe).Length / 1MB
|
||||||
|
Write-Host "原始大小: $([math]::Round($originalSize, 2)) MB"
|
||||||
|
Write-Host "打包大小: $([math]::Round($packedSize, 2)) MB"
|
||||||
|
} else {
|
||||||
|
Write-Host "⚠️ Enigma 打包失败,使用备选方案"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Host "⚠️ Enigma 未找到,使用 7-Zip 自解压方案"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
- name: Package with 7-Zip Alternative
|
||||||
|
if: failure()
|
||||||
|
shell: powershell
|
||||||
|
run: |
|
||||||
|
Write-Host "使用 7-Zip 自解压方案..."
|
||||||
|
|
||||||
|
$buildPath = "build\windows\x64\runner\Release"
|
||||||
|
$outputPath = "dist"
|
||||||
|
|
||||||
|
# 检查 7-Zip
|
||||||
|
$7zipPath = "C:\Program Files\7-Zip\7z.exe"
|
||||||
|
|
||||||
|
# 获取主程序名称
|
||||||
|
$exeFile = Get-ChildItem -Path $buildPath -Filter "*.exe" | Select-Object -First 1
|
||||||
|
$outputSfx = "$outputPath\$($exeFile.BaseName)_Package.exe"
|
||||||
|
|
||||||
|
Write-Host "创建自解压包..."
|
||||||
|
|
||||||
|
# 创建配置文件
|
||||||
|
$configContent = @"
|
||||||
|
;!@Install@!UTF-8!
|
||||||
|
Title="HostExecutor Windows Package"
|
||||||
|
BeginPrompt="正在解压 HostExecutor..."
|
||||||
|
ExtractDialogText="请稍候,正在解压文件..."
|
||||||
|
ExtractPathText="解压路径:"
|
||||||
|
ExtractTitle="解压中"
|
||||||
|
FinishMessage="解压完成!"
|
||||||
|
GUIFlags="8"
|
||||||
|
;!@InstallEnd@!
|
||||||
|
"@
|
||||||
|
|
||||||
|
$configFile = "$outputPath\config.txt"
|
||||||
|
Set-Content -Path $configFile -Value $configContent -Encoding UTF8
|
||||||
|
|
||||||
|
# 创建压缩包
|
||||||
|
& $7zipPath a -sfx7z.sfx -r "$outputSfx" "$buildPath\*" -scsUTF-8 -y
|
||||||
|
Copy-Item "$configFile" -Destination "$outputSfx" -Force
|
||||||
|
|
||||||
|
if (Test-Path $outputSfx) {
|
||||||
|
Write-Host "✅ 7-Zip 自解压包创建成功!"
|
||||||
|
Write-Host "输出文件: $outputSfx"
|
||||||
|
|
||||||
|
$size = (Get-Item $outputSfx).Length / 1MB
|
||||||
|
Write-Host "文件大小: $([math]::Round($size, 2)) MB"
|
||||||
|
} else {
|
||||||
|
Write-Host "❌ 7-Zip 打包失败"
|
||||||
|
}
|
||||||
|
|
||||||
|
- name: Upload Single EXE Package
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: windows-single-exe
|
||||||
|
path: dist/*.exe
|
||||||
|
if: always()
|
||||||
|
|||||||
+2
-3
@@ -1,4 +1,3 @@
|
|||||||
[submodule "libcore"]
|
[submodule "libcore"]
|
||||||
path = libcore
|
path = libcore
|
||||||
url = https://github.com/hiddify/hiddify-next-core
|
url = https://github.com/hiddify/hiddify-next-core
|
||||||
branch = main
|
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ android {
|
|||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "app.hifastvpn.com"
|
applicationId "app.hifastvpn.com"
|
||||||
minSdkVersion flutter.minSdkVersion
|
minSdkVersion flutter.minSdkVersion
|
||||||
targetSdkVersion 34
|
targetSdkVersion 36
|
||||||
versionCode flutterVersionCode.toInteger()
|
versionCode flutterVersionCode.toInteger()
|
||||||
versionName flutterVersionName
|
versionName flutterVersionName
|
||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
@@ -105,8 +105,8 @@ android {
|
|||||||
debugSymbolLevel 'FULL'
|
debugSymbolLevel 'FULL'
|
||||||
}
|
}
|
||||||
// 禁用代码混淆和资源压缩,解决VPN连接问题
|
// 禁用代码混淆和资源压缩,解决VPN连接问题
|
||||||
minifyEnabled false
|
minifyEnabled true
|
||||||
shrinkResources false
|
shrinkResources true
|
||||||
// proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
// proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,11 +34,6 @@
|
|||||||
|
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||||
<uses-permission android:name="android.permission.WRITE_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
|
<application
|
||||||
android:name=".Application"
|
android:name=".Application"
|
||||||
android:banner="@mipmap/ic_banner"
|
android:banner="@mipmap/ic_banner"
|
||||||
|
|||||||
Regular → Executable
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 7.0 KiB |
@@ -193,7 +193,7 @@
|
|||||||
"rewardDetails": "Reward Details >",
|
"rewardDetails": "Reward Details >",
|
||||||
"steps": "Invitation Steps",
|
"steps": "Invitation Steps",
|
||||||
"inviteFriend": "Invite Friends",
|
"inviteFriend": "Invite Friends",
|
||||||
"acceptInvite": "Friends accept invitationPlace order and register",
|
"acceptInvite": "Friends accept invitation\nPlace order and register",
|
||||||
"getReward": "Get Reward",
|
"getReward": "Get Reward",
|
||||||
"shareLink": "Share Link",
|
"shareLink": "Share Link",
|
||||||
"shareQR": "Share QR Code",
|
"shareQR": "Share QR Code",
|
||||||
@@ -398,7 +398,7 @@
|
|||||||
"content": "Would you like to update now?",
|
"content": "Would you like to update now?",
|
||||||
"updateNow": "Update Now",
|
"updateNow": "Update Now",
|
||||||
"updateLater": "Later",
|
"updateLater": "Later",
|
||||||
"defaultContent": "1. Optimize app performance2. Fix known issues3. Improve user experience"
|
"defaultContent": "1. Optimize app performance\n2. Fix known issues\n3. Improve user experience"
|
||||||
},
|
},
|
||||||
"country": {
|
"country": {
|
||||||
"cn": "China",
|
"cn": "China",
|
||||||
|
|||||||
@@ -256,7 +256,7 @@
|
|||||||
"rewardDetails": "Detalles de recompensa >",
|
"rewardDetails": "Detalles de recompensa >",
|
||||||
"steps": "Pasos de invitación",
|
"steps": "Pasos de invitación",
|
||||||
"inviteFriend": "Invitar amigo",
|
"inviteFriend": "Invitar amigo",
|
||||||
"acceptInvite": "El amigo acepta la invitacióny se registra",
|
"acceptInvite": "El amigo acepta la invitación\ny se registra",
|
||||||
"getReward": "Obtener recompensa",
|
"getReward": "Obtener recompensa",
|
||||||
"shareLink": "Compartir por enlace",
|
"shareLink": "Compartir por enlace",
|
||||||
"shareQR": "Compartir por código QR",
|
"shareQR": "Compartir por código QR",
|
||||||
@@ -404,7 +404,7 @@
|
|||||||
"content": "¿Actualizar ahora?",
|
"content": "¿Actualizar ahora?",
|
||||||
"updateNow": "Actualizar ahora",
|
"updateNow": "Actualizar ahora",
|
||||||
"updateLater": "Más tarde",
|
"updateLater": "Más tarde",
|
||||||
"defaultContent": "1. Optimización del rendimiento de la aplicación2. Corrección de problemas conocidos3. Mejora de la experiencia del usuario"
|
"defaultContent": "1. Optimización del rendimiento de la aplicación\n2. Corrección de problemas conocidos\n3. Mejora de la experiencia del usuario"
|
||||||
},
|
},
|
||||||
"kr_invite": {
|
"kr_invite": {
|
||||||
"close": "Cerrar",
|
"close": "Cerrar",
|
||||||
|
|||||||
@@ -193,7 +193,7 @@
|
|||||||
"rewardDetails": "Tasu üksikasjad >",
|
"rewardDetails": "Tasu üksikasjad >",
|
||||||
"steps": "Kutse Sammud",
|
"steps": "Kutse Sammud",
|
||||||
"inviteFriend": "Kutsu Sõbrad",
|
"inviteFriend": "Kutsu Sõbrad",
|
||||||
"acceptInvite": "Sõbrad aktsepteerivad kutsetTee tellimus ja registreeru",
|
"acceptInvite": "Sõbrad aktsepteerivad kutset\nTee tellimus ja registreeru",
|
||||||
"getReward": "Saada Tasu",
|
"getReward": "Saada Tasu",
|
||||||
"shareLink": "Jaga Linki",
|
"shareLink": "Jaga Linki",
|
||||||
"shareQR": "Jaga QR-koodi",
|
"shareQR": "Jaga QR-koodi",
|
||||||
@@ -395,7 +395,7 @@
|
|||||||
"content": "Uuendada nüüd?",
|
"content": "Uuendada nüüd?",
|
||||||
"updateNow": "Uuenda nüüd",
|
"updateNow": "Uuenda nüüd",
|
||||||
"updateLater": "Hiljem",
|
"updateLater": "Hiljem",
|
||||||
"defaultContent": "1. Rakenduse jõudluse optimeerimine2. Teadaolevate probleemide parandamine3. Kasutajamugavuse parandamine"
|
"defaultContent": "1. Rakenduse jõudluse optimeerimine\n2. Teadaolevate probleemide parandamine\n3. Kasutajamugavuse parandamine"
|
||||||
},
|
},
|
||||||
"country": {
|
"country": {
|
||||||
"cn": "Hiina",
|
"cn": "Hiina",
|
||||||
|
|||||||
@@ -209,7 +209,7 @@
|
|||||||
"rewardDetails": "報酬の詳細 >",
|
"rewardDetails": "報酬の詳細 >",
|
||||||
"steps": "招待の手順",
|
"steps": "招待の手順",
|
||||||
"inviteFriend": "友達を招待",
|
"inviteFriend": "友達を招待",
|
||||||
"acceptInvite": "友達が招待を受け入れ注文して登録",
|
"acceptInvite": "友達が招待を受け入れ\n注文して登録",
|
||||||
"getReward": "報酬を獲得",
|
"getReward": "報酬を獲得",
|
||||||
"shareLink": "リンクを共有",
|
"shareLink": "リンクを共有",
|
||||||
"shareQR": "QRコードを共有",
|
"shareQR": "QRコードを共有",
|
||||||
@@ -412,7 +412,7 @@
|
|||||||
"content": "今すぐアップデートしますか?",
|
"content": "今すぐアップデートしますか?",
|
||||||
"updateNow": "今すぐアップデート",
|
"updateNow": "今すぐアップデート",
|
||||||
"updateLater": "後で",
|
"updateLater": "後で",
|
||||||
"defaultContent": "1. アプリのパフォーマンス最適化2. 既知の問題の修正3. ユーザー体験の向上"
|
"defaultContent": "1. アプリのパフォーマンス最適化\n2. 既知の問題の修正\n3. ユーザー体験の向上"
|
||||||
},
|
},
|
||||||
"country": {
|
"country": {
|
||||||
"cn": "中国",
|
"cn": "中国",
|
||||||
|
|||||||
@@ -387,7 +387,7 @@
|
|||||||
"content": "Хотите обновить сейчас?",
|
"content": "Хотите обновить сейчас?",
|
||||||
"updateNow": "Обновить сейчас",
|
"updateNow": "Обновить сейчас",
|
||||||
"later": "Позже",
|
"later": "Позже",
|
||||||
"defaultContent": "1. Оптимизация производительности приложения2. Исправление известных проблем3. Улучшение пользовательского опыта"
|
"defaultContent": "1. Оптимизация производительности приложения\n2. Исправление известных проблем\n3. Улучшение пользовательского опыта"
|
||||||
},
|
},
|
||||||
"country": {
|
"country": {
|
||||||
"cn": "Китай",
|
"cn": "Китай",
|
||||||
|
|||||||
@@ -232,13 +232,13 @@
|
|||||||
"longestConnection": "最长连接时间",
|
"longestConnection": "最长连接时间",
|
||||||
"days": "{days}天",
|
"days": "{days}天",
|
||||||
"daysOfWeek": {
|
"daysOfWeek": {
|
||||||
"monday": "周一",
|
"monday": "周\n一",
|
||||||
"tuesday": "周二",
|
"tuesday": "周\n二",
|
||||||
"wednesday": "周三",
|
"wednesday": "周\n三",
|
||||||
"thursday": "周四",
|
"thursday": "周\n四",
|
||||||
"friday": "周五",
|
"friday": "周\n五",
|
||||||
"saturday": "周六",
|
"saturday": "周\n六",
|
||||||
"sunday": "周日"
|
"sunday": "周\n日"
|
||||||
},
|
},
|
||||||
"processTrafficFailed": "处理流量日志数据失败"
|
"processTrafficFailed": "处理流量日志数据失败"
|
||||||
},
|
},
|
||||||
@@ -256,7 +256,7 @@
|
|||||||
"rewardDetails": "奖励明细 >",
|
"rewardDetails": "奖励明细 >",
|
||||||
"steps": "邀请步骤",
|
"steps": "邀请步骤",
|
||||||
"inviteFriend": "邀请好友",
|
"inviteFriend": "邀请好友",
|
||||||
"acceptInvite": "好友接受邀请下单并注册",
|
"acceptInvite": "好友接受邀请\n下单并注册",
|
||||||
"getReward": "获得奖励",
|
"getReward": "获得奖励",
|
||||||
"shareLink": "分享链接",
|
"shareLink": "分享链接",
|
||||||
"shareQR": "分享二维码",
|
"shareQR": "分享二维码",
|
||||||
@@ -415,7 +415,7 @@
|
|||||||
"content": "是否立即更新?",
|
"content": "是否立即更新?",
|
||||||
"updateNow": "立即更新",
|
"updateNow": "立即更新",
|
||||||
"updateLater": "稍后再说",
|
"updateLater": "稍后再说",
|
||||||
"defaultContent": "1. 优化应用性能2. 修复已知问题3. 提升用户体验"
|
"defaultContent": "1. 优化应用性能\n2. 修复已知问题\n3. 提升用户体验"
|
||||||
},
|
},
|
||||||
"country": {
|
"country": {
|
||||||
"cn": "中国",
|
"cn": "中国",
|
||||||
|
|||||||
@@ -170,13 +170,13 @@
|
|||||||
"longestConnection": "最長連接時間",
|
"longestConnection": "最長連接時間",
|
||||||
"days": "{days}天",
|
"days": "{days}天",
|
||||||
"daysOfWeek": {
|
"daysOfWeek": {
|
||||||
"monday": "週一",
|
"monday": "週\n一",
|
||||||
"tuesday": "週二",
|
"tuesday": "週\n二",
|
||||||
"wednesday": "週三",
|
"wednesday": "週\n三",
|
||||||
"thursday": "週四",
|
"thursday": "週\n四",
|
||||||
"friday": "週五",
|
"friday": "週\n五",
|
||||||
"saturday": "週六",
|
"saturday": "週\n六",
|
||||||
"sunday": "週日"
|
"sunday": "週\n日"
|
||||||
},
|
},
|
||||||
"processTrafficFailed": "處理流量日誌資料失敗"
|
"processTrafficFailed": "處理流量日誌資料失敗"
|
||||||
},
|
},
|
||||||
@@ -194,7 +194,7 @@
|
|||||||
"rewardDetails": "獎勵明細 >",
|
"rewardDetails": "獎勵明細 >",
|
||||||
"steps": "邀請步驟",
|
"steps": "邀請步驟",
|
||||||
"inviteFriend": "邀請好友",
|
"inviteFriend": "邀請好友",
|
||||||
"acceptInvite": "好友接受邀請下單並註冊",
|
"acceptInvite": "好友接受邀請\n下單並註冊",
|
||||||
"getReward": "獲得獎勵",
|
"getReward": "獲得獎勵",
|
||||||
"shareLink": "分享連結",
|
"shareLink": "分享連結",
|
||||||
"shareQR": "分享二維碼",
|
"shareQR": "分享二維碼",
|
||||||
@@ -332,7 +332,7 @@
|
|||||||
"content": "是否立即更新?",
|
"content": "是否立即更新?",
|
||||||
"updateNow": "立即更新",
|
"updateNow": "立即更新",
|
||||||
"updateLater": "稍後",
|
"updateLater": "稍後",
|
||||||
"defaultContent": "1. 優化應用性能2. 修復已知問題3. 改進用戶體驗"
|
"defaultContent": "1. 優化應用性能\n2. 修復已知問題\n3. 改進用戶體驗"
|
||||||
},
|
},
|
||||||
"orderStatus": {
|
"orderStatus": {
|
||||||
"title": "訂單狀態",
|
"title": "訂單狀態",
|
||||||
|
|||||||
+5
-5
@@ -4,12 +4,12 @@ echo 📋 复制 libcore 文件...
|
|||||||
:: 创建目标目录
|
:: 创建目标目录
|
||||||
mkdir libcore\bin >nul 2>&1
|
mkdir libcore\bin >nul 2>&1
|
||||||
|
|
||||||
:: 查找并复制 HiddifyCli.exe,重命名为 BearVPNCli.exe
|
:: 查找并复制 HiddifyCli.exe,重命名为 HiFastVPNCli.exe
|
||||||
for /r %%f in (HiddifyCli.exe) do (
|
for /r %%f in (HiddifyCli.exe) do (
|
||||||
if exist "%%f" (
|
if exist "%%f" (
|
||||||
echo ✅ 找到 HiddifyCli.exe: %%f
|
echo ✅ 找到 HiddifyCli.exe: %%f
|
||||||
echo 📝 复制并重命名为 BearVPNCli.exe
|
echo 📝 复制并重命名为 HiFastVPNCli.exe
|
||||||
copy "%%f" libcore\bin\BearVPNCli.exe
|
copy "%%f" libcore\bin\HiFastVPNCli.exe
|
||||||
echo ✅ 重命名完成
|
echo ✅ 重命名完成
|
||||||
goto :dll
|
goto :dll
|
||||||
)
|
)
|
||||||
@@ -32,7 +32,7 @@ echo.
|
|||||||
echo 📄 验证文件:
|
echo 📄 验证文件:
|
||||||
if exist libcore\bin (
|
if exist libcore\bin (
|
||||||
dir libcore\bin
|
dir libcore\bin
|
||||||
if exist libcore\bin\BearVPNCli.exe (
|
if exist libcore\bin\HiFastVPNCli.exe (
|
||||||
if exist libcore\bin\libcore.dll (
|
if exist libcore\bin\libcore.dll (
|
||||||
echo ✅ 验证成功:所有文件已正确复制
|
echo ✅ 验证成功:所有文件已正确复制
|
||||||
exit /b 0
|
exit /b 0
|
||||||
@@ -41,7 +41,7 @@ if exist libcore\bin (
|
|||||||
exit /b 1
|
exit /b 1
|
||||||
)
|
)
|
||||||
) else (
|
) else (
|
||||||
echo ❌ 验证失败:BearVPNCli.exe 不存在
|
echo ❌ 验证失败:HiFastVPNCli.exe 不存在
|
||||||
exit /b 1
|
exit /b 1
|
||||||
)
|
)
|
||||||
) else (
|
) else (
|
||||||
|
|||||||
+1
-6
@@ -1,5 +1,4 @@
|
|||||||
name: HiFastVPN
|
name: BearVPN
|
||||||
app_name: HiFastVPN
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
build_number: 1
|
build_number: 1
|
||||||
targets:
|
targets:
|
||||||
@@ -12,7 +11,3 @@ targets:
|
|||||||
enable: true
|
enable: true
|
||||||
# 不签名
|
# 不签名
|
||||||
sign: false
|
sign: false
|
||||||
windows:
|
|
||||||
exe:
|
|
||||||
enable: true
|
|
||||||
icon: assets/images/tray_icon.ico
|
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
@echo off
|
||||||
|
:: This script checks for and installs all necessary tools for building and packaging the Flutter application on Windows.
|
||||||
|
|
||||||
|
:: 1. Check for Administrator Privileges
|
||||||
|
net session >nul 2>&1
|
||||||
|
if %errorLevel% == 0 (
|
||||||
|
echo Administrator privileges detected. Continuing...
|
||||||
|
) else (
|
||||||
|
echo Requesting Administrator privileges to install tools...
|
||||||
|
powershell -Command "Start-Process cmd.exe -ArgumentList '/c %~s0' -Verb RunAs" >nul 2>&1
|
||||||
|
exit /b
|
||||||
|
)
|
||||||
|
|
||||||
|
:: 2. Check for and Install Chocolatey
|
||||||
|
echo.
|
||||||
|
echo === Checking for Chocolatey ===
|
||||||
|
where choco >nul 2>&1
|
||||||
|
if %errorlevel% equ 0 (
|
||||||
|
echo Chocolatey is already installed.
|
||||||
|
) else (
|
||||||
|
echo Chocolatey not found. Installing now...
|
||||||
|
powershell -NoProfile -ExecutionPolicy Bypass -Command "[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))"
|
||||||
|
if %errorlevel% neq 0 (
|
||||||
|
echo ERROR: Failed to install Chocolatey. Please install it manually from https://chocolatey.org
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
echo Chocolatey installed successfully.
|
||||||
|
:: Add Chocolatey to the PATH for the current session
|
||||||
|
set "PATH=%PATH%;%ALLUSERSPROFILE%\chocolatey\bin"
|
||||||
|
)
|
||||||
|
|
||||||
|
:: 3. Install Required Tools via Chocolatey
|
||||||
|
echo.
|
||||||
|
echo === Installing Build Tools (7-Zip and Enigma Virtual Box) ===
|
||||||
|
|
||||||
|
:: Install 7-Zip
|
||||||
|
echo Installing 7-Zip...
|
||||||
|
choco install 7zip -y
|
||||||
|
if %errorlevel% neq 0 (
|
||||||
|
echo ERROR: Failed to install 7-Zip.
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
echo 7-Zip installed successfully.
|
||||||
|
|
||||||
|
:: Install Enigma Virtual Box
|
||||||
|
echo Installing Enigma Virtual Box...
|
||||||
|
choco install enigma-virtual-box -y
|
||||||
|
if %errorlevel% neq 0 (
|
||||||
|
echo ERROR: Failed to install Enigma Virtual Box.
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
echo Enigma Virtual Box installed successfully.
|
||||||
|
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo =======================================================
|
||||||
|
echo All required build tools have been installed.
|
||||||
|
echo You can now use 'package_windows_single_exe.ps1' to build your single-file executable.
|
||||||
|
echo =======================================================
|
||||||
|
echo.
|
||||||
|
pause
|
||||||
@@ -1447,7 +1447,7 @@ class AppConfig {
|
|||||||
kr_official_telegram = config.kr_official_telegram;
|
kr_official_telegram = config.kr_official_telegram;
|
||||||
kr_official_telephone = config.kr_official_telephone;
|
kr_official_telephone = config.kr_official_telephone;
|
||||||
kr_invitation_link = config.kr_invitation_link;
|
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) {
|
if (config.kr_domains.isNotEmpty) {
|
||||||
KRDomain.kr_handleDomains(config.kr_domains);
|
KRDomain.kr_handleDomains(config.kr_domains);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,66 +28,14 @@ class KROutboundItem {
|
|||||||
|
|
||||||
/// URL
|
/// URL
|
||||||
String url = "";
|
String url = "";
|
||||||
// ✅ 1. 将传入的 nodeListItem 保存为类的 final 成员变量
|
@override
|
||||||
final KrNodeListItem nodeListItem;
|
String toString() {
|
||||||
|
return 'KROutboundItem(tag: $tag, country: $country, delay: ${urlTestDelay.value}ms)';
|
||||||
|
}
|
||||||
/// 服务器类型
|
/// 服务器类型
|
||||||
|
|
||||||
/// 构造函数,接受 KrItem 对象并初始化 KROutboundItem
|
/// 构造函数,接受 KrNodeListItem 对象并初始化 KROutboundItem
|
||||||
KROutboundItem(this.nodeListItem) {
|
KROutboundItem(KrNodeListItem nodeListItem) {
|
||||||
_initFromNodeListItem();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 静态工厂:构造虚拟 urltest 节点(用于 ${country}-auto)
|
|
||||||
factory KROutboundItem.fromVirtual(String tag, String country, Map<String, dynamic> config) {
|
|
||||||
// 构造一个虚拟 KrNodeListItem,仅填充必要字段
|
|
||||||
final virtualNode = KrNodeListItem(
|
|
||||||
id: 0,
|
|
||||||
name: tag,
|
|
||||||
protocol: 'urltest',
|
|
||||||
serverAddr: '',
|
|
||||||
port: 0,
|
|
||||||
uuid: '',
|
|
||||||
config: jsonEncode(config),
|
|
||||||
city: '',
|
|
||||||
country: country,
|
|
||||||
tags: [],
|
|
||||||
latitude: 0,
|
|
||||||
longitude: 0,
|
|
||||||
latitudeCountry: 0,
|
|
||||||
longitudeCountry: 0,
|
|
||||||
relayNode: '',
|
|
||||||
relayMode: 'none',
|
|
||||||
protocols: '',
|
|
||||||
method: '',
|
|
||||||
speedLimit: 0,
|
|
||||||
traffic: 0,
|
|
||||||
trafficRatio: 0,
|
|
||||||
upload: 0,
|
|
||||||
download: 0,
|
|
||||||
startTime: '',
|
|
||||||
expireTime: '',
|
|
||||||
);
|
|
||||||
return KROutboundItem._virtual(virtualNode);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 私有构造:用于虚拟节点,避免重复解析
|
|
||||||
KROutboundItem._virtual(this.nodeListItem) {
|
|
||||||
// 直接填充虚拟节点所需字段
|
|
||||||
id = nodeListItem.id.toString();
|
|
||||||
protocol = nodeListItem.protocol;
|
|
||||||
tag = nodeListItem.name;
|
|
||||||
serverAddr = nodeListItem.serverAddr;
|
|
||||||
city = nodeListItem.city;
|
|
||||||
country = nodeListItem.country;
|
|
||||||
latitude = nodeListItem.latitude;
|
|
||||||
latitudeCountry = nodeListItem.latitudeCountry;
|
|
||||||
longitude = nodeListItem.longitude;
|
|
||||||
longitudeCountry = nodeListItem.longitudeCountry;
|
|
||||||
config = jsonDecode(nodeListItem.config); // 已知 config 有效
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 初始化逻辑提取,供主构造调用
|
|
||||||
void _initFromNodeListItem() {
|
|
||||||
id = nodeListItem.id.toString();
|
id = nodeListItem.id.toString();
|
||||||
protocol = nodeListItem.protocol;
|
protocol = nodeListItem.protocol;
|
||||||
latitude = nodeListItem.latitude;
|
latitude = nodeListItem.latitude;
|
||||||
@@ -110,163 +58,160 @@ class KROutboundItem {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 兜底:尝试从 config 字段解析(旧API格式)
|
// 兜底:尝试解析 config 字段(旧API格式)
|
||||||
if (nodeListItem.config.isNotEmpty) {
|
if (nodeListItem.config.isEmpty) {
|
||||||
try {
|
if (kDebugMode) {
|
||||||
final json = jsonDecode(nodeListItem.config) as Map<String, dynamic>;
|
print('❌ 节点 ${nodeListItem.name} 缺少配置信息(无port或config)');
|
||||||
if (kDebugMode) {
|
}
|
||||||
print('📄 解析到 config JSON: $json');
|
config = {};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
late Map<String, dynamic> json;
|
||||||
|
try {
|
||||||
|
json = jsonDecode(nodeListItem.config) as Map<String, dynamic>;
|
||||||
|
} catch (e) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
print('❌ 节点 ${nodeListItem.name} 的 config 解析失败: $e,尝试使用直接字段');
|
||||||
|
}
|
||||||
|
if (kDebugMode) {
|
||||||
|
print('📄 Config 内容: ${nodeListItem.config}');
|
||||||
|
}
|
||||||
|
_buildConfigFromFields(nodeListItem);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (nodeListItem.protocol) {
|
||||||
|
case "vless":
|
||||||
|
final securityConfig =
|
||||||
|
json["security_config"] as Map<String, dynamic>? ?? {};
|
||||||
|
|
||||||
|
// 智能设置 server_name
|
||||||
|
String serverName = securityConfig["sni"] ?? "";
|
||||||
|
if (serverName.isEmpty) {
|
||||||
|
serverName = nodeListItem.serverAddr;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 提取 transport 配置
|
config = {
|
||||||
Map<String, dynamic>? transportConfig;
|
"type": "vless",
|
||||||
if (json['transport'] != null && json['transport'] != 'tcp') {
|
"tag": nodeListItem.name,
|
||||||
transportConfig = _buildTransport(json);
|
"server": nodeListItem.serverAddr,
|
||||||
if (kDebugMode) {
|
"server_port": json["port"],
|
||||||
print('✅ 找到 transport 配置: $transportConfig');
|
"uuid": nodeListItem.uuid,
|
||||||
}
|
if (json["flow"] != null && json["flow"] != "none")
|
||||||
}
|
"flow": json["flow"],
|
||||||
|
if (json["transport"] != null && json["transport"] != "tcp")
|
||||||
// 提取 security_config
|
"transport": _buildTransport(json),
|
||||||
Map<String, dynamic>? securityConfig;
|
"tls": {
|
||||||
if (json['security_config'] != null) {
|
"enabled": json["security"] == "tls",
|
||||||
securityConfig = json['security_config'] as Map<String, dynamic>;
|
"server_name": serverName,
|
||||||
if (kDebugMode) {
|
"insecure": securityConfig["allow_insecure"] ?? true,
|
||||||
print('✅ 找到 security_config: $securityConfig');
|
"utls": {
|
||||||
}
|
"enabled": true,
|
||||||
}
|
"fingerprint": securityConfig["fingerprint"] ?? "chrome"
|
||||||
|
|
||||||
// 根据协议类型构建配置
|
|
||||||
switch (nodeListItem.protocol) {
|
|
||||||
case "shadowsocks":
|
|
||||||
config = {
|
|
||||||
"type": "shadowsocks",
|
|
||||||
"tag": nodeListItem.name,
|
|
||||||
"server": nodeListItem.serverAddr,
|
|
||||||
"server_port": json["port"],
|
|
||||||
"method": json["method"],
|
|
||||||
"password": nodeListItem.uuid
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
case "vless":
|
|
||||||
final bool isDomain = !RegExp(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$')
|
|
||||||
.hasMatch(nodeListItem.serverAddr);
|
|
||||||
|
|
||||||
config = {
|
|
||||||
"type": "vless",
|
|
||||||
"tag": nodeListItem.name,
|
|
||||||
"server": nodeListItem.serverAddr,
|
|
||||||
"server_port": json["port"],
|
|
||||||
"uuid": nodeListItem.uuid,
|
|
||||||
if (transportConfig != null) "transport": transportConfig,
|
|
||||||
if (json["security"] == "tls") "tls": {
|
|
||||||
"enabled": true,
|
|
||||||
if (isDomain) "server_name": securityConfig?["sni"] ?? nodeListItem.serverAddr,
|
|
||||||
"insecure": securityConfig?["allow_insecure"] ?? true,
|
|
||||||
"utls": {
|
|
||||||
"enabled": true,
|
|
||||||
"fingerprint": securityConfig?["fingerprint"] ?? "chrome"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
case "vmess":
|
|
||||||
final bool isDomain = !RegExp(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$')
|
|
||||||
.hasMatch(nodeListItem.serverAddr);
|
|
||||||
|
|
||||||
config = {
|
|
||||||
"type": "vmess",
|
|
||||||
"tag": nodeListItem.name,
|
|
||||||
"server": nodeListItem.serverAddr,
|
|
||||||
"server_port": json["port"],
|
|
||||||
"uuid": nodeListItem.uuid,
|
|
||||||
"alter_id": 0,
|
|
||||||
"security": "auto",
|
|
||||||
if (transportConfig != null) "transport": transportConfig,
|
|
||||||
if (json["security"] == "tls") "tls": {
|
|
||||||
"enabled": true,
|
|
||||||
if (isDomain) "server_name": securityConfig?["sni"] ?? nodeListItem.serverAddr,
|
|
||||||
"insecure": securityConfig?["allow_insecure"] ?? true,
|
|
||||||
"utls": {
|
|
||||||
"enabled": true,
|
|
||||||
"fingerprint": securityConfig?["fingerprint"] ?? "chrome"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
case "trojan":
|
|
||||||
final bool isDomain = !RegExp(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$')
|
|
||||||
.hasMatch(nodeListItem.serverAddr);
|
|
||||||
|
|
||||||
config = {
|
|
||||||
"type": "trojan",
|
|
||||||
"tag": nodeListItem.name,
|
|
||||||
"server": nodeListItem.serverAddr,
|
|
||||||
"server_port": json["port"],
|
|
||||||
"password": nodeListItem.uuid,
|
|
||||||
if (transportConfig != null) "transport": transportConfig,
|
|
||||||
"tls": {
|
|
||||||
"enabled": json["security"] == "tls",
|
|
||||||
if (isDomain) "server_name": securityConfig?["sni"] ?? nodeListItem.serverAddr,
|
|
||||||
"insecure": securityConfig?["allow_insecure"] ?? true,
|
|
||||||
"utls": {
|
|
||||||
"enabled": true,
|
|
||||||
"fingerprint": securityConfig?["fingerprint"] ?? "chrome"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
case "hysteria":
|
|
||||||
case "hysteria2":
|
|
||||||
final securityConfig = json["security_config"] as Map<String, dynamic>? ?? {};
|
|
||||||
config = {
|
|
||||||
"type": "hysteria2",
|
|
||||||
"tag": nodeListItem.name,
|
|
||||||
"server": nodeListItem.serverAddr,
|
|
||||||
"server_port": json["port"],
|
|
||||||
"password": nodeListItem.uuid,
|
|
||||||
"up_mbps": 100,
|
|
||||||
"down_mbps": 100,
|
|
||||||
"obfs": {
|
|
||||||
"type": "salamander",
|
|
||||||
"password": json["obfs_password"] ?? nodeListItem.uuid
|
|
||||||
},
|
|
||||||
"tls": {
|
|
||||||
"enabled": true,
|
|
||||||
"server_name": securityConfig["sni"] ?? "",
|
|
||||||
"insecure": securityConfig["allow_insecure"] ?? true
|
|
||||||
}
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
if (kDebugMode) {
|
|
||||||
print('⚠️ 不支持的协议类型: ${nodeListItem.protocol}');
|
|
||||||
}
|
}
|
||||||
config = {};
|
}
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case "vmess":
|
||||||
|
final securityConfig =
|
||||||
|
json["security_config"] as Map<String, dynamic>? ?? {};
|
||||||
|
|
||||||
|
// 智能设置 server_name
|
||||||
|
String serverName = securityConfig["sni"] ?? "";
|
||||||
|
if (serverName.isEmpty) {
|
||||||
|
serverName = nodeListItem.serverAddr;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查 relayNode 是否为 JSON 字符串并解析
|
config = {
|
||||||
if (nodeListItem.relayNode.isNotEmpty && nodeListItem.relayMode != "none") {
|
"type": "vmess",
|
||||||
final relayNodeJson = jsonDecode(nodeListItem.relayNode);
|
"tag": nodeListItem.name,
|
||||||
if (relayNodeJson is List && nodeListItem.relayMode != "none") {
|
"server": nodeListItem.serverAddr,
|
||||||
// 随机选择一个元素
|
"server_port": json["port"],
|
||||||
final randomNode = (relayNodeJson..shuffle()).first;
|
"uuid": nodeListItem.uuid,
|
||||||
config["server"] = randomNode["host"]; // 提取 host
|
"alter_id": 0,
|
||||||
config["server_port"] = randomNode["port"]; // 提取 port
|
"security": "auto",
|
||||||
|
if (json["transport"] != null && json["transport"] != "tcp")
|
||||||
|
"transport": _buildTransport(json),
|
||||||
|
"tls": {
|
||||||
|
"enabled": json["security"] == "tls",
|
||||||
|
"server_name": serverName,
|
||||||
|
"insecure": securityConfig["allow_insecure"] ?? true,
|
||||||
|
"utls": {"enabled": true, "fingerprint": "chrome"}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case "shadowsocks":
|
||||||
|
config = {
|
||||||
|
"type": "shadowsocks",
|
||||||
|
"tag": nodeListItem.name,
|
||||||
|
"server": nodeListItem.serverAddr,
|
||||||
|
"server_port": json["port"],
|
||||||
|
"method": json["method"],
|
||||||
|
"password": nodeListItem.uuid
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case "hysteria":
|
||||||
|
case "hysteria2":
|
||||||
|
// 后端的 "hysteria" 实际上是 Hysteria2 协议
|
||||||
|
final securityConfig =
|
||||||
|
json["security_config"] as Map<String, dynamic>? ?? {};
|
||||||
|
config = {
|
||||||
|
"type": "hysteria2",
|
||||||
|
"tag": nodeListItem.name,
|
||||||
|
"server": nodeListItem.serverAddr,
|
||||||
|
"server_port": json["port"],
|
||||||
|
"password": nodeListItem.uuid,
|
||||||
|
"up_mbps": 100,
|
||||||
|
"down_mbps": 100,
|
||||||
|
"obfs": {
|
||||||
|
"type": "salamander",
|
||||||
|
"password": json["obfs_password"] ?? nodeListItem.uuid
|
||||||
|
},
|
||||||
|
"tls": {
|
||||||
|
"enabled": true,
|
||||||
|
"server_name": securityConfig["sni"] ?? "",
|
||||||
|
"insecure": securityConfig["allow_insecure"] ?? true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case "trojan":
|
||||||
|
final securityConfig =
|
||||||
|
json["security_config"] as Map<String, dynamic>? ?? {};
|
||||||
|
|
||||||
|
// 智能设置 server_name
|
||||||
|
String serverName = securityConfig["sni"] ?? "";
|
||||||
|
if (serverName.isEmpty) {
|
||||||
|
// 如果没有配置 SNI,使用服务器地址
|
||||||
|
serverName = nodeListItem.serverAddr;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
if (kDebugMode) {
|
config = {
|
||||||
print('⚠️ 解析 config 字段失败: $e');
|
"type": "trojan",
|
||||||
}
|
"tag": nodeListItem.name,
|
||||||
config = {};
|
"server": nodeListItem.serverAddr,
|
||||||
|
"server_port": json["port"],
|
||||||
|
"password": nodeListItem.uuid,
|
||||||
|
"tls": {
|
||||||
|
"enabled": json["security"] == "tls",
|
||||||
|
"server_name": serverName,
|
||||||
|
"insecure": securityConfig["allow_insecure"] ?? true,
|
||||||
|
"utls": {"enabled": true, "fingerprint": "chrome"}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查 relayNode 是否为 JSON 字符串并解析
|
||||||
|
if (nodeListItem.relayNode.isNotEmpty && nodeListItem.relayMode != "none") {
|
||||||
|
final relayNodeJson = jsonDecode(nodeListItem.relayNode);
|
||||||
|
if (relayNodeJson is List && nodeListItem.relayMode != "none") {
|
||||||
|
// 随机选择一个元素
|
||||||
|
final randomNode = (relayNodeJson..shuffle()).first;
|
||||||
|
config["server"] = randomNode["host"]; // 提取 host
|
||||||
|
config["server_port"] = randomNode["port"]; // 提取 port
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
// 解析配置
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() {
|
|
||||||
return 'KROutboundItem(name: ${nodeListItem.name}, protocol: ${nodeListItem.protocol}, server: ${nodeListItem.serverAddr}, port: ${nodeListItem.port})';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 构建传输配置
|
/// 构建传输配置
|
||||||
@@ -304,11 +249,31 @@ class KROutboundItem {
|
|||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
print('🔧 开始构建节点配置 - 协议: ${nodeListItem.protocol}, 名称: ${nodeListItem.name}');
|
print('🔧 开始构建节点配置 - 协议: ${nodeListItem.protocol}, 名称: ${nodeListItem.name}');
|
||||||
}
|
}
|
||||||
|
if (kDebugMode) {
|
||||||
|
print('📋 节点详细信息:');
|
||||||
|
}
|
||||||
|
if (kDebugMode) {
|
||||||
|
print(' - serverAddr: ${nodeListItem.serverAddr}');
|
||||||
|
}
|
||||||
|
if (kDebugMode) {
|
||||||
|
print(' - port: ${nodeListItem.port}');
|
||||||
|
}
|
||||||
|
if (kDebugMode) {
|
||||||
|
print(' - uuid: ${nodeListItem.uuid}');
|
||||||
|
}
|
||||||
|
if (kDebugMode) {
|
||||||
|
print(' - method: ${nodeListItem.method}');
|
||||||
|
}
|
||||||
|
if (kDebugMode) {
|
||||||
|
print(' - config: ${nodeListItem.config}');
|
||||||
|
}
|
||||||
|
if (kDebugMode) {
|
||||||
|
print(' - protocols: ${nodeListItem.protocols}');
|
||||||
|
}
|
||||||
|
|
||||||
// 🔧 尝试从 config 字段解析 transport 配置
|
// 🔧 尝试从 config 字段解析 transport 配置
|
||||||
Map<String, dynamic>? transportConfig;
|
Map<String, dynamic>? transportConfig;
|
||||||
Map<String, dynamic>? securityConfig;
|
Map<String, dynamic>? securityConfig;
|
||||||
int actualPort = nodeListItem.port; // 🔧 使用局部变量存储实际端口
|
|
||||||
|
|
||||||
// 🔧 关键修复:优先从 protocols 字段解析配置
|
// 🔧 关键修复:优先从 protocols 字段解析配置
|
||||||
if (nodeListItem.protocols.isNotEmpty) {
|
if (nodeListItem.protocols.isNotEmpty) {
|
||||||
@@ -342,28 +307,6 @@ class KROutboundItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (matchedProtocol != null) {
|
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 配置
|
// 提取 transport 配置
|
||||||
if (matchedProtocol['network'] != null || matchedProtocol['transport'] != null) {
|
if (matchedProtocol['network'] != null || matchedProtocol['transport'] != null) {
|
||||||
final network = matchedProtocol['network'] ?? matchedProtocol['transport'];
|
final network = matchedProtocol['network'] ?? matchedProtocol['transport'];
|
||||||
@@ -459,7 +402,7 @@ class KROutboundItem {
|
|||||||
|
|
||||||
switch (nodeListItem.protocol) {
|
switch (nodeListItem.protocol) {
|
||||||
case "shadowsocks":
|
case "shadowsocks":
|
||||||
// 优先使用 protocols 解析出来的 cipher,其次是 method 字段,最后才是默认值
|
// 优先使用 protocols 解析出来的 cipher,其次是 method 字段,最后才是默认值
|
||||||
String finalMethod = nodeListItem.method.isNotEmpty
|
String finalMethod = nodeListItem.method.isNotEmpty
|
||||||
? nodeListItem.method
|
? nodeListItem.method
|
||||||
: "2022-blake3-aes-256-gcm";
|
: "2022-blake3-aes-256-gcm";
|
||||||
@@ -468,21 +411,22 @@ class KROutboundItem {
|
|||||||
"type": "shadowsocks",
|
"type": "shadowsocks",
|
||||||
"tag": nodeListItem.name,
|
"tag": nodeListItem.name,
|
||||||
"server": nodeListItem.serverAddr,
|
"server": nodeListItem.serverAddr,
|
||||||
"server_port": actualPort,
|
"server_port": nodeListItem.port,
|
||||||
"method": finalMethod,
|
"method": finalMethod,
|
||||||
"password": nodeListItem.uuid
|
"password": nodeListItem.uuid
|
||||||
};
|
};
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
||||||
print('✅ Shadowsocks 节点配置构建成功: ${nodeListItem.name}');
|
print('✅ Shadowsocks 节点配置构建成功: ${nodeListItem.name}');
|
||||||
|
}
|
||||||
|
if (kDebugMode) {
|
||||||
print('📄 使用加密方法: $finalMethod');
|
print('📄 使用加密方法: $finalMethod');
|
||||||
print('📄 完整配置 JSON:');
|
}
|
||||||
print(jsonEncode(config));
|
if (kDebugMode) {
|
||||||
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
print('📄 完整配置: $config');
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "vless":
|
case "vless":
|
||||||
// 判断是否为域名(非IP地址)
|
// 判断是否为域名(非IP地址)
|
||||||
final bool isDomain = !RegExp(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$')
|
final bool isDomain = !RegExp(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$')
|
||||||
.hasMatch(nodeListItem.serverAddr);
|
.hasMatch(nodeListItem.serverAddr);
|
||||||
|
|
||||||
@@ -492,25 +436,17 @@ class KROutboundItem {
|
|||||||
serverName = securityConfig['sni'].toString();
|
serverName = securityConfig['sni'].toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔧 关键修复:智能判断是否启用 TLS
|
// 🔧 关键修复:根据 security_config 判断是否启用 TLS
|
||||||
// 1. 优先使用 securityConfig['tls_enabled']
|
final bool vlessTlsEnabled = securityConfig?['tls_enabled'] ?? false;
|
||||||
// 2. 如果没有明确配置,根据端口和域名智能判断
|
|
||||||
bool vlessTlsEnabled = securityConfig?['tls_enabled'] ?? true; // 默认启用 TLS
|
|
||||||
|
|
||||||
// 如果端口是标准非TLS端口(80, 8080等),则禁用 TLS
|
|
||||||
if (nodeListItem.port == 80 || nodeListItem.port == 8080) {
|
|
||||||
vlessTlsEnabled = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
print('🔐 VLESS TLS 状态: enabled=$vlessTlsEnabled (port=${nodeListItem.port}, isDomain=$isDomain)');
|
print('🔐 VLESS TLS 状态: enabled=$vlessTlsEnabled');
|
||||||
}
|
}
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
"type": "vless",
|
"type": "vless",
|
||||||
"tag": nodeListItem.name,
|
"tag": nodeListItem.name,
|
||||||
"server": nodeListItem.serverAddr,
|
"server": nodeListItem.serverAddr,
|
||||||
"server_port": actualPort,
|
"server_port": nodeListItem.port,
|
||||||
"uuid": nodeListItem.uuid,
|
"uuid": nodeListItem.uuid,
|
||||||
if (transportConfig != null) "transport": transportConfig,
|
if (transportConfig != null) "transport": transportConfig,
|
||||||
if (vlessTlsEnabled) "tls": {
|
if (vlessTlsEnabled) "tls": {
|
||||||
@@ -524,27 +460,14 @@ class KROutboundItem {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
||||||
print('✅ VLESS 节点配置构建成功: ${nodeListItem.name}');
|
print('✅ VLESS 节点配置构建成功: ${nodeListItem.name}');
|
||||||
print('📋 原始节点信息:');
|
}
|
||||||
print(' - serverAddr: ${nodeListItem.serverAddr}');
|
if (kDebugMode) {
|
||||||
print(' - port: ${nodeListItem.port}');
|
print('📄 完整配置: $config');
|
||||||
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;
|
break;
|
||||||
case "vmess":
|
case "vmess":
|
||||||
// 判断是否为域名(非IP地址)
|
// 判断是否为域名(非IP地址)
|
||||||
final bool isDomain = !RegExp(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$')
|
final bool isDomain = !RegExp(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$')
|
||||||
.hasMatch(nodeListItem.serverAddr);
|
.hasMatch(nodeListItem.serverAddr);
|
||||||
|
|
||||||
@@ -554,25 +477,17 @@ class KROutboundItem {
|
|||||||
serverName = securityConfig['sni'].toString();
|
serverName = securityConfig['sni'].toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔧 关键修复:智能判断是否启用 TLS
|
// 🔧 关键修复:根据 security_config 判断是否启用 TLS
|
||||||
// 1. 优先使用 securityConfig['tls_enabled']
|
final bool tlsEnabled = securityConfig?['tls_enabled'] ?? false;
|
||||||
// 2. 如果没有明确配置,根据端口和域名智能判断
|
|
||||||
bool tlsEnabled = securityConfig?['tls_enabled'] ?? true; // 默认启用 TLS
|
|
||||||
|
|
||||||
// 如果端口是标准非TLS端口(80, 8080等),则禁用 TLS
|
|
||||||
if (nodeListItem.port == 80 || nodeListItem.port == 8080) {
|
|
||||||
tlsEnabled = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
print('🔐 VMess TLS 状态: enabled=$tlsEnabled (port=${nodeListItem.port}, isDomain=$isDomain)');
|
print('🔐 TLS 状态: enabled=$tlsEnabled');
|
||||||
}
|
}
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
"type": "vmess",
|
"type": "vmess",
|
||||||
"tag": nodeListItem.name,
|
"tag": nodeListItem.name,
|
||||||
"server": nodeListItem.serverAddr,
|
"server": nodeListItem.serverAddr,
|
||||||
"server_port": actualPort,
|
"server_port": nodeListItem.port,
|
||||||
"uuid": nodeListItem.uuid,
|
"uuid": nodeListItem.uuid,
|
||||||
"alter_id": 0,
|
"alter_id": 0,
|
||||||
"security": "auto",
|
"security": "auto",
|
||||||
@@ -588,27 +503,14 @@ class KROutboundItem {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
||||||
print('✅ VMess 节点配置构建成功: ${nodeListItem.name}');
|
print('✅ VMess 节点配置构建成功: ${nodeListItem.name}');
|
||||||
print('📋 原始节点信息:');
|
}
|
||||||
print(' - serverAddr: ${nodeListItem.serverAddr}');
|
if (kDebugMode) {
|
||||||
print(' - port: ${nodeListItem.port}');
|
print('📄 完整配置: $config');
|
||||||
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;
|
break;
|
||||||
case "trojan":
|
case "trojan":
|
||||||
// 判断是否为域名(非IP地址)
|
// 判断是否为域名(非IP地址)
|
||||||
final bool isDomain = !RegExp(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$')
|
final bool isDomain = !RegExp(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$')
|
||||||
.hasMatch(nodeListItem.serverAddr);
|
.hasMatch(nodeListItem.serverAddr);
|
||||||
|
|
||||||
@@ -622,7 +524,7 @@ class KROutboundItem {
|
|||||||
"type": "trojan",
|
"type": "trojan",
|
||||||
"tag": nodeListItem.name,
|
"tag": nodeListItem.name,
|
||||||
"server": nodeListItem.serverAddr,
|
"server": nodeListItem.serverAddr,
|
||||||
"server_port": actualPort,
|
"server_port": nodeListItem.port,
|
||||||
"password": nodeListItem.uuid,
|
"password": nodeListItem.uuid,
|
||||||
if (transportConfig != null) "transport": transportConfig,
|
if (transportConfig != null) "transport": transportConfig,
|
||||||
"tls": {
|
"tls": {
|
||||||
@@ -644,7 +546,7 @@ class KROutboundItem {
|
|||||||
break;
|
break;
|
||||||
case "hysteria":
|
case "hysteria":
|
||||||
case "hysteria2":
|
case "hysteria2":
|
||||||
// 后端的 "hysteria" 实际上是 Hysteria2 协议
|
// 后端的 "hysteria" 实际上是 Hysteria2 协议
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
print('🔍 构建 Hysteria2 节点: ${nodeListItem.name}');
|
print('🔍 构建 Hysteria2 节点: ${nodeListItem.name}');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,9 +88,6 @@ class KrOutboundsList {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成国家自动选择虚拟节点
|
|
||||||
_generateCountryAutoNodes(countryGroups);
|
|
||||||
|
|
||||||
// 将标签分组转换为 KRGroupOutboundList 并添加到 groupOutboundList
|
// 将标签分组转换为 KRGroupOutboundList 并添加到 groupOutboundList
|
||||||
for (var tag in tagGroups.keys) {
|
for (var tag in tagGroups.keys) {
|
||||||
final item = KRGroupOutboundList(
|
final item = KRGroupOutboundList(
|
||||||
@@ -111,41 +108,7 @@ class KrOutboundsList {
|
|||||||
country: country,
|
country: country,
|
||||||
outboundList: countryGroups[country]!)); // 添加国家分组到列表
|
outboundList: countryGroups[country]!)); // 添加国家分组到列表
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// 生成国家自动选择虚拟节点
|
|
||||||
void _generateCountryAutoNodes(Map<String, List<KROutboundItem>> countryGroups) {
|
|
||||||
for (var entry in countryGroups.entries) {
|
|
||||||
final country = entry.key;
|
|
||||||
final nodes = entry.value;
|
|
||||||
final autoTag = '${country}-auto';
|
|
||||||
|
|
||||||
if (kDebugMode) {
|
|
||||||
print('🤖 生成国家自动选择节点: $autoTag, 包含 ${nodes.length} 个节点');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 构建 urltest 配置
|
|
||||||
final urltestConfig = {
|
|
||||||
'type': 'urltest',
|
|
||||||
'tag': autoTag,
|
|
||||||
'outbounds': nodes.map((node) => node.tag).toList(),
|
|
||||||
'url': 'https://www.google.com/generate_204',
|
|
||||||
'interval': '10m',
|
|
||||||
'tolerance': 50,
|
|
||||||
"interrupt_exist_connections": true,
|
|
||||||
};
|
|
||||||
|
|
||||||
// 创建虚拟节点
|
|
||||||
final virtualNode = KROutboundItem.fromVirtual(autoTag, country, urltestConfig);
|
|
||||||
|
|
||||||
// 添加到各个列表
|
|
||||||
allList.add(virtualNode);
|
|
||||||
keyList[autoTag] = virtualNode;
|
|
||||||
// configJsonList.add(urltestConfig);
|
|
||||||
|
|
||||||
if (kDebugMode) {
|
|
||||||
print('✅ 生成虚拟节点: $autoTag, 配置: ${urltestConfig.toString()}');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import 'dart:convert';
|
|||||||
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
|
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
|
||||||
|
|
||||||
class KRNodeList {
|
class KRNodeList {
|
||||||
final List<KrNodeListItem> list;
|
final List<KrNodeListItem> list;
|
||||||
final String subscribeId;
|
final String subscribeId;
|
||||||
final String startTime;
|
final String startTime;
|
||||||
final String expireTime;
|
final String expireTime;
|
||||||
@@ -35,8 +35,8 @@ class KRNodeList {
|
|||||||
// 查找 id 匹配的订阅项
|
// 查找 id 匹配的订阅项
|
||||||
try {
|
try {
|
||||||
subscribeData = listData.firstWhere(
|
subscribeData = listData.firstWhere(
|
||||||
(item) => (item as Map<String, dynamic>)['id']?.toString() == requestSubscribeId,
|
(item) => (item as Map<String, dynamic>)['id']?.toString() == requestSubscribeId,
|
||||||
orElse: () => listData[0] as Map<String, dynamic>
|
orElse: () => listData[0] as Map<String, dynamic>
|
||||||
) as Map<String, dynamic>;
|
) as Map<String, dynamic>;
|
||||||
KRLogUtil.kr_i('✅ 找到匹配的订阅项: id=$requestSubscribeId', tag: 'NodeList');
|
KRLogUtil.kr_i('✅ 找到匹配的订阅项: id=$requestSubscribeId', tag: 'NodeList');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -70,7 +70,7 @@ class KRNodeList {
|
|||||||
|
|
||||||
class KrNodeListItem {
|
class KrNodeListItem {
|
||||||
final int id;
|
final int id;
|
||||||
String name;
|
String name;
|
||||||
final String uuid;
|
final String uuid;
|
||||||
final String protocol;
|
final String protocol;
|
||||||
final String relayMode;
|
final String relayMode;
|
||||||
@@ -136,68 +136,25 @@ class KrNodeListItem {
|
|||||||
String method = json['method']?.toString() ?? ''; // 加密方法(Shadowsocks等)
|
String method = json['method']?.toString() ?? ''; // 加密方法(Shadowsocks等)
|
||||||
final protocols = json['protocols']?.toString() ?? ''; // 协议配置JSON
|
final protocols = json['protocols']?.toString() ?? ''; // 协议配置JSON
|
||||||
|
|
||||||
// 🔧 打印原始节点 JSON(用于调试)
|
// 🔧 如果有 protocols 字段,从中解析 port 和 cipher
|
||||||
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) {
|
if (protocols.isNotEmpty) {
|
||||||
try {
|
try {
|
||||||
final protocolsList = jsonDecode(protocols) as List;
|
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) {
|
if (protocolsList.isNotEmpty) {
|
||||||
// 🔧 修复:查找与当前协议类型匹配的配置,而不是直接使用第一个
|
final firstProtocol = protocolsList[0] as Map<String, dynamic>;
|
||||||
Map<String, dynamic>? matchedProtocolConfig;
|
// 优先使用 protocols 中的配置
|
||||||
|
if (firstProtocol['port'] != null) {
|
||||||
// 尝试找到协议类型匹配的配置
|
port = _parseIntSafely(firstProtocol['port']);
|
||||||
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) {
|
||||||
// 如果没找到匹配的,使用第一个配置(兼容旧API)
|
method = firstProtocol['cipher'].toString();
|
||||||
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) {
|
} catch (e) {
|
||||||
KRLogUtil.kr_w('⚠️ 解析 protocols 字段失败: $e', tag: 'NodeList');
|
KRLogUtil.kr_w('解析 protocols 字段失败: $e', tag: 'NodeList');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
KRLogUtil.kr_i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', tag: 'NodeList');
|
|
||||||
|
|
||||||
return KrNodeListItem(
|
return KrNodeListItem(
|
||||||
id: _parseIntSafely(json['id']),
|
id: _parseIntSafely(json['id']),
|
||||||
|
|||||||
@@ -12,15 +12,6 @@ class HINodeListController extends GetxController {
|
|||||||
/// 首页服务
|
/// 首页服务
|
||||||
final KRHomeController homeController = Get.find<KRHomeController>();
|
final KRHomeController homeController = Get.find<KRHomeController>();
|
||||||
|
|
||||||
/// 调试模式状态
|
|
||||||
final RxBool isDebugMode = false.obs;
|
|
||||||
|
|
||||||
/// 模式按钮点击计数器
|
|
||||||
int modeButtonClickCount = 0;
|
|
||||||
|
|
||||||
/// 最后一次点击时间
|
|
||||||
DateTime? lastModeButtonClickTime;
|
|
||||||
|
|
||||||
/// 获取连接类型字符串
|
/// 获取连接类型字符串
|
||||||
String kr_getConnectionTypeString() {
|
String kr_getConnectionTypeString() {
|
||||||
final connectionType = KRSingBoxImp.instance.kr_connectionType.value;
|
final connectionType = KRSingBoxImp.instance.kr_connectionType.value;
|
||||||
@@ -50,48 +41,4 @@ class HINodeListController extends GetxController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 处理模式按钮点击(用于激活调试模式)
|
|
||||||
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');
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 获取要显示的节点列表(根据调试模式过滤)
|
|
||||||
List<dynamic> kr_getFilteredNodeList() {
|
|
||||||
if (isDebugMode.value) {
|
|
||||||
// 调试模式:显示所有节点
|
|
||||||
return kr_subscribeService.allList;
|
|
||||||
} else {
|
|
||||||
// 正常模式:只显示 country-auto 节点
|
|
||||||
return kr_subscribeService.allList.where((node) => node.tag.endsWith('-auto')).toList();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 重置调试模式
|
|
||||||
void kr_resetDebugMode() {
|
|
||||||
isDebugMode.value = false;
|
|
||||||
modeButtonClickCount = 0;
|
|
||||||
lastModeButtonClickTime = null;
|
|
||||||
KRLogUtil.kr_i('调试模式已重置', tag: 'HINodeListController');
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// hi_node_list_view.dart
|
// hi_node_list_view.dart
|
||||||
|
|
||||||
|
import 'dart:math';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
@@ -47,9 +48,17 @@ class HINodeListView extends GetView<HINodeListController> {
|
|||||||
) {
|
) {
|
||||||
if (outboundList.isEmpty) return 0;
|
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;
|
||||||
|
|
||||||
// 返回最小延迟值
|
// 返回最小延迟值
|
||||||
outboundList.sort((a, b) => a.urlTestDelay.value.compareTo(b.urlTestDelay.value));
|
validNodes.sort((a, b) => a.urlTestDelay.value.compareTo(b.urlTestDelay.value));
|
||||||
return outboundList.first.urlTestDelay.value;
|
return validNodes.first.urlTestDelay.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 找出延迟最低(最快)的节点对象
|
/// 找出延迟最低(最快)的节点对象
|
||||||
@@ -79,8 +88,8 @@ class HINodeListView extends GetView<HINodeListController> {
|
|||||||
// 并设置透明背景,让父组件的背景可以透出来
|
// 并设置透明背景,让父组件的背景可以透出来
|
||||||
return Material(
|
return Material(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
child: _buildSubscribeList(context)
|
child: _buildSubscribeList(context)
|
||||||
// child: _kr_buildRegionList(context)
|
// child: _kr_buildRegionList(context)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,20 +199,9 @@ class HINodeListView extends GetView<HINodeListController> {
|
|||||||
),
|
),
|
||||||
...controller.kr_subscribeService.countryOutboundList.map((country) {
|
...controller.kr_subscribeService.countryOutboundList.map((country) {
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: () async {
|
onTap: () {
|
||||||
try {
|
// 自动选择这个国家下的节点延迟中最快的
|
||||||
|
controller.homeController.onCountrySelected(country.country);
|
||||||
final success =
|
|
||||||
await controller.homeController.kr_performNodeSwitch('${country.country}-auto');
|
|
||||||
print('node 点击 ${country.country} 节点数量${country.outboundList.length} 节点详情 ${country.outboundList}');
|
|
||||||
if (success) {
|
|
||||||
controller.homeController.kr_currentListStatus.value =
|
|
||||||
KRHomeViewsListStatus.kr_none;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
KRLogUtil.kr_e('Auto选项切换异常: $e',
|
|
||||||
tag: 'NodeListView');
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
child: _kr_buildCountryListItem(context, country: country),
|
child: _kr_buildCountryListItem(context, country: country),
|
||||||
);
|
);
|
||||||
@@ -231,24 +229,14 @@ class HINodeListView extends GetView<HINodeListController> {
|
|||||||
padding: EdgeInsets.symmetric(vertical: 8.w),
|
padding: EdgeInsets.symmetric(vertical: 8.w),
|
||||||
// 2. 使用 children 属性,并一次性构建所有列表项
|
// 2. 使用 children 属性,并一次性构建所有列表项
|
||||||
children: [
|
children: [
|
||||||
if (controller.kr_getFilteredNodeList().isEmpty)
|
if (controller.kr_subscribeService.allList.isEmpty)
|
||||||
_buildEmptyListPlaceholder(context,
|
_buildEmptyListPlaceholder(context, AppTranslations.kr_home.noNodes)
|
||||||
controller.isDebugMode.value ? '调试模式:无节点数据' : AppTranslations.kr_home.noNodes)
|
|
||||||
else ...[
|
else ...[
|
||||||
InkWell(
|
InkWell(
|
||||||
// 🔧 修复:改为 async,等待节点切换完成后再关闭列表
|
onTap: () {
|
||||||
onTap: () async {
|
controller.homeController.kr_selectNode('auto');
|
||||||
try {
|
controller.homeController.kr_currentListStatus.value =
|
||||||
final success =
|
KRHomeViewsListStatus.kr_none;
|
||||||
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(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -293,28 +281,13 @@ class HINodeListView extends GetView<HINodeListController> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
// 2. 第二个 Text: "根据网络IP自动匹配最快线路"
|
// 2. 第二个 Text: "根据网络IP自动匹配最快线路"
|
||||||
Obx(() {
|
Text(
|
||||||
// 当选择全局 auto 时,显示当前选中的节点信息
|
'根据网络IP自动匹配最快线路', // 您指定的文本
|
||||||
if (controller.homeController.kr_cutTag.value == 'auto') {
|
style: KrAppTextStyle(
|
||||||
final autoNodeInfo = controller.homeController.kr_getGlobalAutoSelectedNode();
|
fontSize: 10,
|
||||||
if (autoNodeInfo != null) {
|
color: Colors.white,
|
||||||
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,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -335,25 +308,9 @@ class HINodeListView extends GetView<HINodeListController> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
...controller.kr_getFilteredNodeList().map((item) {
|
...controller.kr_subscribeService.allList.map((item) {
|
||||||
return InkWell(
|
return InkWell(
|
||||||
// 🔧 修复:改为 async,等待节点切换完成后再关闭列表
|
onTap: () => _onNodeSelected(item),
|
||||||
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),
|
child: _kr_buildNodeListItem(context, item: item),
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
@@ -387,6 +344,15 @@ 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
|
/// 构建单个节点列表项的UI
|
||||||
Widget _kr_buildNodeListItem(BuildContext context, {required KROutboundItem item}) {
|
Widget _kr_buildNodeListItem(BuildContext context, {required KROutboundItem item}) {
|
||||||
return Container(
|
return Container(
|
||||||
@@ -406,75 +372,55 @@ class HINodeListView extends GetView<HINodeListController> {
|
|||||||
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),
|
SizedBox(width: 12.w),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Obx(() {
|
child: Column(
|
||||||
final isDebug = controller.isDebugMode.value;
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
final text = isDebug
|
Row(
|
||||||
? '${controller.homeController.kr_getCountryFullName(item.country)} - ${item.tag}'
|
children: [
|
||||||
: '${controller.homeController.kr_getCountryFullName(item.country)}';
|
Flexible(
|
||||||
|
child: Text(
|
||||||
return Text(
|
controller.homeController.kr_getCountryFullName(item.country),
|
||||||
text,
|
style: KrAppTextStyle(fontSize: 14, color: Colors.white),
|
||||||
style: KrAppTextStyle(
|
overflow: TextOverflow.ellipsis,
|
||||||
fontSize: 16,
|
maxLines: 1,
|
||||||
fontWeight: FontWeight.w500,
|
),
|
||||||
color: Colors.white,
|
),
|
||||||
|
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()),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
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(() {
|
|
||||||
// 1. 获取延迟值和测速状态
|
|
||||||
int displayDelay;
|
|
||||||
bool isTesting = controller.homeController.kr_isLatency.value;
|
|
||||||
|
|
||||||
// 极简逻辑:只根据节点类型决定显示方式
|
|
||||||
print('🔄 _kr_buildNodeListItem节点: item.tag=${item.tag}, country=${item.country}');
|
|
||||||
|
|
||||||
// 如果节点本身是auto组节点,显示组内最快节点的速度
|
|
||||||
if (item.tag.endsWith('-auto')) {
|
|
||||||
print('🎯 检测到auto组节点,获取最快节点信息');
|
|
||||||
final autoNodeInfo = controller.homeController.kr_getCountryAutoSelectedNode(item.country);
|
|
||||||
print('📊 _kr_buildNodeListItem获取auto最快节点: $autoNodeInfo');
|
|
||||||
displayDelay = autoNodeInfo?['delay'] ?? 0;
|
|
||||||
print('✅ _kr_buildNodeListItem使用auto最快节点延迟: $displayDelay');
|
|
||||||
} else {
|
|
||||||
// 普通节点,直接显示节点自身的速度
|
|
||||||
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(() => controller.homeController.kr_cutTag.value == item.tag
|
Obx(() => controller.homeController.kr_cutTag.value == item.tag
|
||||||
? KrLocalImage(
|
? KrLocalImage(
|
||||||
imageName: 'radio-active-icon',
|
imageName: 'radio-active-icon',
|
||||||
@@ -495,7 +441,18 @@ class HINodeListView extends GetView<HINodeListController> {
|
|||||||
|
|
||||||
/// 构建国家列表项的UI
|
/// 构建国家列表项的UI
|
||||||
Widget _kr_buildCountryListItem(BuildContext context, {required country}) {
|
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(
|
return Container(
|
||||||
key: ValueKey(country),
|
key: ValueKey(country),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -519,42 +476,22 @@ class HINodeListView extends GetView<HINodeListController> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Obx(() {
|
Obx(() {
|
||||||
// 1. 获取延迟值和测速状态
|
final int delay = getFastestNodeDelay(controller, country.outboundList);
|
||||||
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(
|
return Text(
|
||||||
delayText,
|
delay == 0
|
||||||
|
? ''
|
||||||
|
: delay >= 3000
|
||||||
|
? AppTranslations.kr_home.timeout
|
||||||
|
: '${delay}ms',
|
||||||
style: KrAppTextStyle(
|
style: KrAppTextStyle(
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
color: delayColor, // 使用动态计算出的颜色
|
color: getLatencyColor(delay),
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
SizedBox(width: 12.w),
|
SizedBox(width: 12.w),
|
||||||
Obx(() => controller.homeController.kr_cutTag.value == '${country.country}-auto'
|
Obx(() => controller.homeController.kr_coutryText.value == country.country
|
||||||
? KrLocalImage(
|
? KrLocalImage(
|
||||||
imageName: 'radio-active-icon',
|
imageName: 'radio-active-icon',
|
||||||
imageType: ImageType.svg,
|
imageType: ImageType.svg,
|
||||||
|
|||||||
@@ -19,47 +19,10 @@ class HINodePageView extends GetView<HINodeListController> {
|
|||||||
return HIBaseScaffold(
|
return HIBaseScaffold(
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
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(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
|
||||||
// 模式切换器
|
// 模式切换器
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.only(left: 100.w, right: 60.w),
|
padding: EdgeInsets.only(left: 100.w, right: 60.w),
|
||||||
@@ -196,12 +159,7 @@ class HINodePageView extends GetView<HINodeListController> {
|
|||||||
}) {
|
}) {
|
||||||
return Expanded(
|
return Expanded(
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: () {
|
onTap: onTap,
|
||||||
// 处理模式按钮点击(用于调试模式激活)
|
|
||||||
controller.kr_handleModeButtonClick();
|
|
||||||
// 执行原有的点击逻辑
|
|
||||||
onTap();
|
|
||||||
},
|
|
||||||
child: AnimatedContainer(
|
child: AnimatedContainer(
|
||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 200),
|
||||||
padding: EdgeInsets.symmetric(vertical: 4.w),
|
padding: EdgeInsets.symmetric(vertical: 4.w),
|
||||||
|
|||||||
@@ -69,12 +69,8 @@ class HIUserInfoView extends GetView<HIUserInfoController> {
|
|||||||
children: [
|
children: [
|
||||||
Obx(() {
|
Obx(() {
|
||||||
final account = 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(
|
return Text(
|
||||||
account ?? '',
|
(account != null && account.isNotEmpty) ? account : '未绑定',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 20.sp,
|
fontSize: 20.sp,
|
||||||
@@ -429,7 +425,7 @@ class HIUserInfoView extends GetView<HIUserInfoController> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'设备:${_extractDeviceModel(userAgent, deviceType)}',
|
'设备:${_extractDeviceModel(userAgent)}',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 10.sp, //
|
fontSize: 10.sp, //
|
||||||
@@ -437,7 +433,7 @@ class HIUserInfoView extends GetView<HIUserInfoController> {
|
|||||||
),
|
),
|
||||||
SizedBox(height: 4.h),
|
SizedBox(height: 4.h),
|
||||||
Text(
|
Text(
|
||||||
'SN: ${identifier.substring(0, identifier.length > 4 ? 4 : identifier.length)}$id',
|
'ID: ${identifier.substring(0, identifier.length > 4 ? 4 : identifier.length)}$id',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white.withOpacity(0.9),
|
color: Colors.white.withOpacity(0.9),
|
||||||
fontSize: 10.sp,
|
fontSize: 10.sp,
|
||||||
@@ -589,12 +585,12 @@ class HIUserInfoView extends GetView<HIUserInfoController> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
String _extractDeviceModel(String deviceName, String deviceType) {
|
String _extractDeviceModel(String deviceName) {
|
||||||
// 匹配括号内内容
|
// 匹配括号内内容
|
||||||
final RegExp regExp = RegExp(r'\((.*?)\)');
|
final RegExp regExp = RegExp(r'\((.*?)\)');
|
||||||
final Match? match = regExp.firstMatch(deviceName);
|
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"
|
final inside = match.group(1)!; // "Android; google Pixel 9; 15"
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import '../../../common/app_config.dart';
|
|||||||
import '../../../localization/app_translations.dart';
|
import '../../../localization/app_translations.dart';
|
||||||
import '../../../localization/kr_language_utils.dart';
|
import '../../../localization/kr_language_utils.dart';
|
||||||
import '../../../model/business/kr_group_outbound_list.dart';
|
import '../../../model/business/kr_group_outbound_list.dart';
|
||||||
import '../../../model/business/kr_outbound_item.dart';
|
|
||||||
import '../../../services/kr_announcement_service.dart';
|
import '../../../services/kr_announcement_service.dart';
|
||||||
import '../../../utils/kr_event_bus.dart';
|
import '../../../utils/kr_event_bus.dart';
|
||||||
import '../../../utils/kr_update_util.dart';
|
import '../../../utils/kr_update_util.dart';
|
||||||
@@ -134,21 +133,11 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
|||||||
if (value != null) {
|
if (value != null) {
|
||||||
isQuickConnectEnabled.value = value;
|
isQuickConnectEnabled.value = value;
|
||||||
// 保存闪连状态到本地存储
|
// 保存闪连状态到本地存储
|
||||||
await _saveQuickConnectStatus(value);
|
await _storage.kr_saveBool(key: _quickConnectKey, value: value);
|
||||||
KRLogUtil.kr_i('闪连状态已更新: $value', tag: 'QuickConnect');
|
KRLogUtil.kr_i('闪连状态已更新: $value', tag: 'QuickConnect');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存闪连状态到本地存储
|
|
||||||
Future<void> _saveQuickConnectStatus(bool enabled) async {
|
|
||||||
try {
|
|
||||||
await _storage.kr_saveBool(key: _quickConnectKey, value: enabled);
|
|
||||||
KRLogUtil.kr_i('闪连状态已保存到本地存储: $enabled', tag: 'QuickConnect');
|
|
||||||
} catch (e) {
|
|
||||||
KRLogUtil.kr_e('保存闪连状态失败: $e', tag: 'QuickConnect');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从本地存储加载闪连状态
|
// 从本地存储加载闪连状态
|
||||||
Future<void> _loadQuickConnectStatus() async {
|
Future<void> _loadQuickConnectStatus() async {
|
||||||
try {
|
try {
|
||||||
@@ -850,7 +839,6 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
|||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
}
|
}
|
||||||
await KRSingBoxImp.instance.kr_start();
|
await KRSingBoxImp.instance.kr_start();
|
||||||
|
|
||||||
KRLogUtil.kr_i('✅ 连接命令已发送', tag: 'HomeController');
|
KRLogUtil.kr_i('✅ 连接命令已发送', tag: 'HomeController');
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
}
|
}
|
||||||
@@ -1277,6 +1265,10 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> onCountrySelected(String countryTag) async {
|
||||||
|
await KRSingBoxImp.instance.kr_selectCountry(countryTag);
|
||||||
|
KRCommonUtil.kr_showToast('已切换到 $countryTag 节点组');
|
||||||
|
}
|
||||||
|
|
||||||
/// 🔧 修复:简化的 kr_selectNode 方法
|
/// 🔧 修复:简化的 kr_selectNode 方法
|
||||||
/// 现在只是委托给新的 kr_performNodeSwitch 方法
|
/// 现在只是委托给新的 kr_performNodeSwitch 方法
|
||||||
@@ -1422,8 +1414,8 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
|||||||
|
|
||||||
/// 获取真实连接的节点信息(auto 模式下获取实际连接的节点)
|
/// 获取真实连接的节点信息(auto 模式下获取实际连接的节点)
|
||||||
Map<String, dynamic> kr_getRealConnectedNodeInfo() {
|
Map<String, dynamic> kr_getRealConnectedNodeInfo() {
|
||||||
// 如果不是 auto 模式,也不是 country-auto 模式,直接返回当前选中的节点信息
|
// 如果不是 auto 模式,直接返回当前选中的节点信息
|
||||||
if (kr_cutTag.value != 'auto' && !kr_cutTag.value.endsWith('-auto')) {
|
if (kr_cutTag.value != 'auto') {
|
||||||
final node = kr_subscribeService.keyList[kr_cutSeletedTag.value];
|
final node = kr_subscribeService.keyList[kr_cutSeletedTag.value];
|
||||||
return {
|
return {
|
||||||
'nodeName': kr_cutSeletedTag.value,
|
'nodeName': kr_cutSeletedTag.value,
|
||||||
@@ -1432,13 +1424,11 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理 auto 模式(包括全局 auto 和 country-auto)
|
// auto 模式下,获取 urltest 组的实际连接节点
|
||||||
print('当前活动组----${KRSingBoxImp.instance.kr_activeGroups.length}');
|
print('当前活动组----${KRSingBoxImp.instance.kr_activeGroups.length}');
|
||||||
for (var group in KRSingBoxImp.instance.kr_activeGroups) {
|
for (var group in KRSingBoxImp.instance.kr_activeGroups) {
|
||||||
print('当前活动组----$group}');
|
print('当前活动组----$group}');
|
||||||
|
if (group.type == ProxyType.urltest) {
|
||||||
// 处理全局 auto 模式
|
|
||||||
if (kr_cutTag.value == 'auto' && group.type == ProxyType.urltest && group.tag == 'auto') {
|
|
||||||
final selectedNode = group.selected;
|
final selectedNode = group.selected;
|
||||||
final node = kr_subscribeService.keyList[selectedNode];
|
final node = kr_subscribeService.keyList[selectedNode];
|
||||||
return {
|
return {
|
||||||
@@ -1447,39 +1437,12 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
|||||||
'country': node?.country ?? '',
|
'country': node?.country ?? '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理 country-auto 模式
|
|
||||||
if (kr_cutTag.value.endsWith('-auto') && group.type == ProxyType.urltest) {
|
|
||||||
if (group.tag == kr_cutTag.value) {
|
|
||||||
final selectedNode = group.selected;
|
|
||||||
final node = kr_subscribeService.keyList[selectedNode];
|
|
||||||
return {
|
|
||||||
'nodeName': selectedNode,
|
|
||||||
'delay': node?.urlTestDelay.value ?? -2,
|
|
||||||
'country': node?.country ?? '',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
print('hhhhhh${kr_subscribeService.keyList}', );
|
print('hhhhhh${kr_subscribeService.keyList}', );
|
||||||
|
|
||||||
// 处理 country-auto 模式的备用方案(当 SingBox 组数据不可用时)
|
|
||||||
if (kr_cutTag.value.endsWith('-auto')) {
|
|
||||||
final countryCode = kr_cutTag.value.replaceAll('-auto', '');
|
|
||||||
KRLogUtil.kr_i('🔄 kr_getRealConnectedNodeInfo 使用备用方案获取 country-auto 信息: $countryCode', tag: 'HomeController');
|
|
||||||
final autoNodeInfo = kr_getCountryAutoSelectedNode(countryCode);
|
|
||||||
if (autoNodeInfo != null) {
|
|
||||||
return {
|
|
||||||
'nodeName': autoNodeInfo['tag'],
|
|
||||||
'delay': autoNodeInfo['delay'] ?? -2,
|
|
||||||
'country': autoNodeInfo['country'] ?? countryCode,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果没有找到 urltest 组,返回默认值
|
// 如果没有找到 urltest 组,返回默认值
|
||||||
return {
|
return {
|
||||||
'nodeName': kr_cutTag.value,
|
'nodeName': 'auto',
|
||||||
'delay': -2,
|
'delay': -2,
|
||||||
'country': '',
|
'country': '',
|
||||||
};
|
};
|
||||||
@@ -1500,10 +1463,9 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
|||||||
/// 获取真实连接的节点国家
|
/// 获取真实连接的节点国家
|
||||||
String kr_getRealConnectedNodeCountry() {
|
String kr_getRealConnectedNodeCountry() {
|
||||||
final info = kr_getRealConnectedNodeInfo();
|
final info = kr_getRealConnectedNodeInfo();
|
||||||
final delay = info['delay'] as int; // 使用真实连接的节点延迟,而不是 kr_currentNodeLatency.value
|
final delay = kr_currentNodeLatency.value;
|
||||||
final country1 = kr_getCurrentNodeCountry();
|
final country1 = kr_getCurrentNodeCountry();
|
||||||
print('country----$country1');
|
print('country----$country1');
|
||||||
print('kr_getRealConnectedNodeCountry - delay from info: $delay, country from info: ${info['country']}');
|
|
||||||
final country = kr_getCountryFullName(info['country']);
|
final country = kr_getCountryFullName(info['country']);
|
||||||
if (delay == -2) {
|
if (delay == -2) {
|
||||||
return '--';
|
return '--';
|
||||||
@@ -1970,7 +1932,7 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
|||||||
final results = await KRLatencyTester.testMultipleNodes(
|
final results = await KRLatencyTester.testMultipleNodes(
|
||||||
nodes: nodeAddresses,
|
nodes: nodeAddresses,
|
||||||
concurrency: 10, // 每批10个并发
|
concurrency: 10, // 每批10个并发
|
||||||
timeout: const Duration(seconds: 5),
|
timeout: 5000, // 超时时间5秒(毫秒)
|
||||||
);
|
);
|
||||||
|
|
||||||
// 更新节点延迟
|
// 更新节点延迟
|
||||||
@@ -2211,225 +2173,6 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取国家-auto组的当前选中子节点信息
|
|
||||||
Map<String, dynamic>? kr_getCountryAutoSelectedNode(String countryCode) {
|
|
||||||
try {
|
|
||||||
final activeGroups = KRSingBoxImp.instance.kr_activeGroups;
|
|
||||||
final allGroups = KRSingBoxImp.instance.kr_allGroups;
|
|
||||||
final autoGroupTag = '${countryCode}-auto';
|
|
||||||
|
|
||||||
KRLogUtil.kr_i('🔍 开始获取国家-auto选中节点: countryCode=$countryCode, autoGroupTag=$autoGroupTag', tag: 'HomeController');
|
|
||||||
KRLogUtil.kr_i('📊 活跃组数量: ${activeGroups.length}, 所有组数量: ${allGroups.length}', tag: 'HomeController');
|
|
||||||
|
|
||||||
// 优先检查活跃组
|
|
||||||
if (activeGroups.isNotEmpty) {
|
|
||||||
KRLogUtil.kr_i('✅ 活跃组不为空,优先检查活跃组', tag: 'HomeController');
|
|
||||||
for (var group in activeGroups) {
|
|
||||||
KRLogUtil.kr_i('🔄 检查活跃组: tag=${group.tag}, type=${group.type}, selected=${group.selected}', tag: 'HomeController');
|
|
||||||
|
|
||||||
if (group.tag == autoGroupTag && group.type == ProxyType.urltest) {
|
|
||||||
KRLogUtil.kr_i('✅ 在活跃组中找到匹配的urltest组: $autoGroupTag', tag: 'HomeController');
|
|
||||||
return _kr_extractSelectedNodeInfo(group, countryCode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果活跃组中没有,检查所有组
|
|
||||||
if (allGroups.isNotEmpty) {
|
|
||||||
KRLogUtil.kr_i('🔍 活跃组中未找到,检查所有组', tag: 'HomeController');
|
|
||||||
for (var group in allGroups) {
|
|
||||||
KRLogUtil.kr_i('🔄 检查所有组: tag=${group.tag}, type=${group.type}, selected=${group.selected}', tag: 'HomeController');
|
|
||||||
|
|
||||||
if (group.tag == autoGroupTag && group.type == ProxyType.urltest) {
|
|
||||||
KRLogUtil.kr_i('✅ 在所有组中找到匹配的urltest组: $autoGroupTag', tag: 'HomeController');
|
|
||||||
return _kr_extractSelectedNodeInfo(group, countryCode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果组数据都为空,使用备用方案:从订阅服务中找该国家最快的节点
|
|
||||||
KRLogUtil.kr_i('🔄 组数据为空,使用备用方案从订阅服务获取最快节点', tag: 'HomeController');
|
|
||||||
return _kr_getFastestNodeFromSubscribeService(countryCode);
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
KRLogUtil.kr_e('💥 获取国家-auto选中节点异常: $e', tag: 'HomeController');
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 从订阅服务获取该国家最快节点的备用方案
|
|
||||||
Map<String, dynamic>? _kr_getFastestNodeFromSubscribeService(String countryCode) {
|
|
||||||
try {
|
|
||||||
KRLogUtil.kr_i('🔄 使用订阅服务查找国家最快节点: $countryCode', tag: 'HomeController');
|
|
||||||
|
|
||||||
// 从订阅服务中获取该国家的所有节点
|
|
||||||
final allNodes = kr_subscribeService.allList.where((node) =>
|
|
||||||
node.country == countryCode && !node.tag.endsWith('-auto')
|
|
||||||
).toList();
|
|
||||||
|
|
||||||
KRLogUtil.kr_i('📊 找到 ${allNodes.length} 个该国家的普通节点', tag: 'HomeController');
|
|
||||||
|
|
||||||
if (allNodes.isEmpty) {
|
|
||||||
KRLogUtil.kr_w('⚠️ 未找到该国家的任何普通节点: $countryCode', tag: 'HomeController');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 找出延迟最小的节点(排除0和超时)
|
|
||||||
KROutboundItem? fastestNode;
|
|
||||||
int fastestDelay = 999999;
|
|
||||||
|
|
||||||
for (var node in allNodes) {
|
|
||||||
final delay = node.urlTestDelay.value;
|
|
||||||
if (delay > 0 && delay < 3000 && delay < fastestDelay) {
|
|
||||||
fastestDelay = delay;
|
|
||||||
fastestNode = node;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fastestNode != null) {
|
|
||||||
KRLogUtil.kr_i('✅ 找到最快节点: ${fastestNode.tag}, 延迟: ${fastestDelay}ms', tag: 'HomeController');
|
|
||||||
return {
|
|
||||||
'tag': fastestNode.tag,
|
|
||||||
'delay': fastestDelay,
|
|
||||||
'country': countryCode,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
KRLogUtil.kr_w('⚠️ 该国家的节点都没有有效延迟数据', tag: 'HomeController');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
KRLogUtil.kr_e('💥 获取订阅服务最快节点异常: $e', tag: 'HomeController');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 提取选中节点信息的辅助方法
|
|
||||||
Map<String, dynamic>? _kr_extractSelectedNodeInfo(dynamic group, String countryCode) {
|
|
||||||
try {
|
|
||||||
// 获取当前选中的节点
|
|
||||||
final selectedNode = group.selected;
|
|
||||||
KRLogUtil.kr_i('🎯 当前选中节点: $selectedNode', tag: 'HomeController');
|
|
||||||
KRLogUtil.kr_i('📋 组内项目数量: ${group.items.length}', tag: 'HomeController');
|
|
||||||
|
|
||||||
if (selectedNode != null && selectedNode.isNotEmpty) {
|
|
||||||
KRLogUtil.kr_i('✅ 选中节点有效,开始查找节点详情', tag: 'HomeController');
|
|
||||||
|
|
||||||
// 打印所有节点信息用于调试
|
|
||||||
for (var item in group.items) {
|
|
||||||
KRLogUtil.kr_i('📄 组内节点: tag=${item.tag}, delay=${item.urlTestDelay}', tag: 'HomeController');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 在组内查找选中的节点
|
|
||||||
try {
|
|
||||||
final selectedItem = group.items.firstWhere(
|
|
||||||
(item) => item.tag == selectedNode,
|
|
||||||
);
|
|
||||||
|
|
||||||
KRLogUtil.kr_i('🎉 成功找到选中节点: tag=${selectedItem.tag}, delay=${selectedItem.urlTestDelay}', tag: 'HomeController');
|
|
||||||
|
|
||||||
return {
|
|
||||||
'tag': selectedNode,
|
|
||||||
'delay': selectedItem.urlTestDelay,
|
|
||||||
'country': countryCode,
|
|
||||||
};
|
|
||||||
} catch (e) {
|
|
||||||
KRLogUtil.kr_e('❌ 在组内未找到选中节点: $selectedNode', tag: 'HomeController');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
KRLogUtil.kr_w('⚠️ 选中节点为空或无效', tag: 'HomeController');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
KRLogUtil.kr_e('💥 提取选中节点信息异常: $e', tag: 'HomeController');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 获取全局 auto 组的当前选中子节点信息
|
|
||||||
Map<String, dynamic>? kr_getGlobalAutoSelectedNode() {
|
|
||||||
try {
|
|
||||||
final activeGroups = KRSingBoxImp.instance.kr_activeGroups;
|
|
||||||
|
|
||||||
KRLogUtil.kr_i('🌍 开始获取全局auto选中节点信息', tag: 'HomeController');
|
|
||||||
KRLogUtil.kr_i('📊 活跃组数量: ${activeGroups.length}', tag: 'HomeController');
|
|
||||||
|
|
||||||
for (var group in activeGroups) {
|
|
||||||
KRLogUtil.kr_i('🔄 检查全局组: tag=${group.tag}, type=${group.type}, selected=${group.selected}', tag: 'HomeController');
|
|
||||||
KRLogUtil.kr_i('📋 全局组内项目数量: ${group.items.length}', tag: 'HomeController');
|
|
||||||
|
|
||||||
if (group.tag == 'auto' && group.type == ProxyType.urltest) {
|
|
||||||
KRLogUtil.kr_i('✅ 找到全局auto urltest组', tag: 'HomeController');
|
|
||||||
|
|
||||||
// 获取当前选中的节点
|
|
||||||
final selectedNode = group.selected;
|
|
||||||
KRLogUtil.kr_i('🎯 全局auto当前选中节点: $selectedNode', tag: 'HomeController');
|
|
||||||
|
|
||||||
if (selectedNode != null && selectedNode.isNotEmpty) {
|
|
||||||
KRLogUtil.kr_i('✅ 全局auto选中节点有效,开始查找详情', tag: 'HomeController');
|
|
||||||
|
|
||||||
// 打印所有节点信息用于调试
|
|
||||||
for (var item in group.items) {
|
|
||||||
KRLogUtil.kr_i('📄 全局auto组内节点: tag=${item.tag}, delay=${item.urlTestDelay}', tag: 'HomeController');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 在组内查找选中的节点
|
|
||||||
try {
|
|
||||||
final selectedItem = group.items.firstWhere(
|
|
||||||
(item) => item.tag == selectedNode,
|
|
||||||
);
|
|
||||||
|
|
||||||
KRLogUtil.kr_i('🎉 成功找到全局auto选中节点: tag=${selectedItem.tag}, delay=${selectedItem.urlTestDelay}', tag: 'HomeController');
|
|
||||||
|
|
||||||
return {
|
|
||||||
'tag': selectedNode,
|
|
||||||
'delay': selectedItem.urlTestDelay,
|
|
||||||
'country': '',
|
|
||||||
};
|
|
||||||
} catch (e) {
|
|
||||||
KRLogUtil.kr_e('❌ 在全局auto组内未找到选中节点: $selectedNode', tag: 'HomeController');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
KRLogUtil.kr_w('⚠️ 全局auto选中节点为空或无效', tag: 'HomeController');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
KRLogUtil.kr_w('❌ 未找到全局auto组', tag: 'HomeController');
|
|
||||||
} catch (e) {
|
|
||||||
KRLogUtil.kr_e('💥 获取全局auto选中节点异常: $e', tag: 'HomeController');
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 获取指定国家的所有真实节点延迟列表
|
|
||||||
List<Map<String, dynamic>> kr_getCountryRealNodeDelays(String countryCode) {
|
|
||||||
final delays = <Map<String, dynamic>>[];
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 从订阅服务中获取该国家的节点列表
|
|
||||||
final countryNodes = kr_subscribeService.keyList.values
|
|
||||||
.where((item) => item.country == countryCode && !item.tag.endsWith('-auto'))
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
for (final node in countryNodes) {
|
|
||||||
delays.add({
|
|
||||||
'tag': node.tag,
|
|
||||||
'delay': node.urlTestDelay.value,
|
|
||||||
'city': node.city,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 按延迟排序
|
|
||||||
delays.sort((a, b) => a['delay'].compareTo(b['delay']));
|
|
||||||
} catch (e) {
|
|
||||||
KRLogUtil.kr_e('获取国家真实节点延迟列表失败: $e', tag: 'HomeController');
|
|
||||||
}
|
|
||||||
|
|
||||||
return delays;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 尝试从活动组更新延迟值
|
/// 尝试从活动组更新延迟值
|
||||||
bool _kr_tryUpdateDelayFromActiveGroups() {
|
bool _kr_tryUpdateDelayFromActiveGroups() {
|
||||||
try {
|
try {
|
||||||
@@ -2455,17 +2198,6 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 如果是国家-auto模式,从对应的国家urltest组获取延迟
|
|
||||||
else if (kr_cutTag.value.endsWith('-auto')) {
|
|
||||||
final countryCode = kr_cutTag.value.replaceAll('-auto', '');
|
|
||||||
for (var item in group.items) {
|
|
||||||
if (item.tag == kr_cutTag.value && item.urlTestDelay != 0) {
|
|
||||||
kr_currentNodeLatency.value = item.urlTestDelay;
|
|
||||||
KRLogUtil.kr_i('✅ ${countryCode}-auto模式延迟值: ${item.urlTestDelay}ms', tag: 'HomeController');
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 手动选择模式
|
// 手动选择模式
|
||||||
else {
|
else {
|
||||||
for (var item in group.items) {
|
for (var item in group.items) {
|
||||||
|
|||||||
@@ -305,24 +305,19 @@ class KRLoginController extends GetxController
|
|||||||
|
|
||||||
/// 发送验证码(仅支持邮箱)
|
/// 发送验证码(仅支持邮箱)
|
||||||
void kr_sendCode() async {
|
void kr_sendCode() async {
|
||||||
if (accountController.text.isEmpty) {
|
final either = await KRAuthApi().kr_sendCode(
|
||||||
KRCommonUtil.kr_showToast(AppTranslations.kr_login.enterAccount);
|
accountController.text,
|
||||||
return;
|
2
|
||||||
}
|
); // 重置密码验证码类型为3
|
||||||
|
/*
|
||||||
int type;
|
*
|
||||||
final check = await KRAuthApi().kr_isRegister(accountController.text);
|
* kr_loginStatus.value == KRLoginProgressStatus.kr_registerSendCode
|
||||||
final result = check.fold((l) {
|
? 2 // 注册验证码类型为2
|
||||||
KRCommonUtil.kr_showToast(l.msg);
|
: 3*/
|
||||||
return null;
|
|
||||||
}, (isRegistered) => isRegistered ? 2 : 1);
|
|
||||||
if (result == null) return;
|
|
||||||
type = result;
|
|
||||||
|
|
||||||
final either = await KRAuthApi().kr_sendCode(accountController.text, type);
|
|
||||||
either.fold((l) {
|
either.fold((l) {
|
||||||
KRCommonUtil.kr_showToast(l.msg);
|
KRCommonUtil.kr_showToast(l.msg);
|
||||||
}, (r) async {
|
}, (r) async {
|
||||||
|
/// 开始倒计时
|
||||||
_startCountdown();
|
_startCountdown();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,13 +76,10 @@ class KRLoginView extends GetView<KRLoginController> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Obx(() {
|
Obx(() {
|
||||||
final account = KRAppRunData.getInstance().kr_account.value;
|
final _ = KRAppRunData.getInstance().kr_isLogin.value;
|
||||||
final isDeviceLogin = account != null && account.startsWith('9000');
|
final email = KRAppRunData.getInstance().kr_account.value ?? '';
|
||||||
|
|
||||||
if (isDeviceLogin) return const SizedBox();
|
|
||||||
|
|
||||||
return Text(
|
return Text(
|
||||||
account ?? '',
|
email.isNotEmpty ? email : '',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 20.sp,
|
fontSize: 20.sp,
|
||||||
|
|||||||
@@ -285,7 +285,7 @@ class KRSplashController extends GetxController {
|
|||||||
KRLogUtil.kr_e('⏱️ 初始化超时: $e', tag: 'SplashController');
|
KRLogUtil.kr_e('⏱️ 初始化超时: $e', tag: 'SplashController');
|
||||||
print('⏱️ 初始化超时,直接跳转到主页');
|
print('⏱️ 初始化超时,直接跳转到主页');
|
||||||
// 超时后直接跳转到主页,让用户可以手动重试
|
// 超时后直接跳转到主页,让用户可以手动重试
|
||||||
// Get.offAllNamed(Routes.KR_HOME);
|
// Get.offAllNamed(Routes.KR_MAIN);
|
||||||
HIDialog.show(
|
HIDialog.show(
|
||||||
message: '初始化超时,请检查网络或重试',
|
message: '初始化超时,请检查网络或重试',
|
||||||
confirmText: '重试',
|
confirmText: '重试',
|
||||||
@@ -484,7 +484,7 @@ class KRSplashController extends GetxController {
|
|||||||
_initLog.logWarning('网络权限检查失败或超时,执行降级初始化', tag: 'Init');
|
_initLog.logWarning('网络权限检查失败或超时,执行降级初始化', tag: 'Init');
|
||||||
KRLogUtil.kr_w('⚠️ 网络权限检查失败或超时,执行降级初始化', tag: 'SplashController');
|
KRLogUtil.kr_w('⚠️ 网络权限检查失败或超时,执行降级初始化', tag: 'SplashController');
|
||||||
await _executeMinimalInitialization();
|
await _executeMinimalInitialization();
|
||||||
Get.offAllNamed(Routes.KR_HOME);
|
Get.offAllNamed(Routes.KR_MAIN);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -493,7 +493,7 @@ class KRSplashController extends GetxController {
|
|||||||
_initLog.logError('网络权限检查异常,执行降级初始化', tag: 'Init', error: e);
|
_initLog.logError('网络权限检查异常,执行降级初始化', tag: 'Init', error: e);
|
||||||
KRLogUtil.kr_w('⚠️ 网络权限检查异常: $e,执行降级初始化', tag: 'SplashController');
|
KRLogUtil.kr_w('⚠️ 网络权限检查异常: $e,执行降级初始化', tag: 'SplashController');
|
||||||
await _executeMinimalInitialization();
|
await _executeMinimalInitialization();
|
||||||
Get.offAllNamed(Routes.KR_HOME);
|
Get.offAllNamed(Routes.KR_MAIN);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -582,7 +582,7 @@ class KRSplashController extends GetxController {
|
|||||||
_initLog.logError('启动页初始化异常,执行降级策略', tag: 'Continue', error: e);
|
_initLog.logError('启动页初始化异常,执行降级策略', tag: 'Continue', error: e);
|
||||||
KRLogUtil.kr_w('⚠️ 启动页初始化异常,执行降级策略: $e', tag: 'SplashController');
|
KRLogUtil.kr_w('⚠️ 启动页初始化异常,执行降级策略: $e', tag: 'SplashController');
|
||||||
await _executeMinimalInitialization();
|
await _executeMinimalInitialization();
|
||||||
Get.offAllNamed(Routes.KR_HOME);
|
Get.offAllNamed(Routes.KR_MAIN);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -726,7 +726,7 @@ class KRSplashController extends GetxController {
|
|||||||
kr_isLoading.value = false;
|
kr_isLoading.value = false;
|
||||||
|
|
||||||
// 直接跳转到主页
|
// 直接跳转到主页
|
||||||
Get.offAllNamed(Routes.KR_HOME);
|
Get.offAllNamed(Routes.KR_MAIN);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 🔧 修复1.1:清理旧的本地存储数据(DEBUG模式专用)
|
/// 🔧 修复1.1:清理旧的本地存储数据(DEBUG模式专用)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/// 接口名称
|
/// 接口名称
|
||||||
abstract class Api {
|
abstract class Api {
|
||||||
/// 游客登录查看是否已经注册
|
/// 游客登录查看是否已经注册
|
||||||
static const String kr_isRegister = "/v1/auth/check";
|
static const String kr_isRegister = "/v1/app/auth/check";
|
||||||
/// 判断邮箱和当前设备是否已存在订阅
|
/// 判断邮箱和当前设备是否已存在订阅
|
||||||
static const String kr_checkSubscription = "/v1/public/user/subscribe_status";
|
static const String kr_checkSubscription = "/v1/public/user/subscribe_status";
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class KRAuthApi {
|
|||||||
|
|
||||||
BaseResponse<KRIsRegister> baseResponse = await HttpUtil.getInstance()
|
BaseResponse<KRIsRegister> baseResponse = await HttpUtil.getInstance()
|
||||||
.request<KRIsRegister>(Api.kr_isRegister, data,
|
.request<KRIsRegister>(Api.kr_isRegister, data,
|
||||||
method: HttpMethod.GET, isShowLoading: true);
|
method: HttpMethod.POST, isShowLoading: true);
|
||||||
|
|
||||||
if (!baseResponse.isSuccess) {
|
if (!baseResponse.isSuccess) {
|
||||||
return left(
|
return left(
|
||||||
|
|||||||
@@ -309,7 +309,6 @@ class KRSubscribeService {
|
|||||||
|
|
||||||
// 保存配置
|
// 保存配置
|
||||||
KRSingBoxImp.instance.kr_saveOutbounds(listModel.configJsonList);
|
KRSingBoxImp.instance.kr_saveOutbounds(listModel.configJsonList);
|
||||||
KRSingBoxImp.instance.kr_saveAllOutbounds(listModel.configJsonList);
|
|
||||||
|
|
||||||
// 更新试用和订阅状态
|
// 更新试用和订阅状态
|
||||||
_kr_updateSubscribeStatus();
|
_kr_updateSubscribeStatus();
|
||||||
@@ -662,8 +661,6 @@ class KRSubscribeService {
|
|||||||
|
|
||||||
// 保存配置
|
// 保存配置
|
||||||
KRSingBoxImp.instance.kr_saveOutbounds(listModel.configJsonList);
|
KRSingBoxImp.instance.kr_saveOutbounds(listModel.configJsonList);
|
||||||
KRSingBoxImp.instance.kr_saveAllOutbounds(listModel.configJsonList);
|
|
||||||
|
|
||||||
// 更新试用和订阅状态
|
// 更新试用和订阅状态
|
||||||
_kr_updateSubscribeStatus();
|
_kr_updateSubscribeStatus();
|
||||||
|
|
||||||
@@ -725,8 +722,6 @@ class KRSubscribeService {
|
|||||||
|
|
||||||
// 保存配置
|
// 保存配置
|
||||||
KRSingBoxImp.instance.kr_saveOutbounds([]);
|
KRSingBoxImp.instance.kr_saveOutbounds([]);
|
||||||
KRSingBoxImp.instance.kr_saveAllOutbounds([]);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取当前订阅
|
/// 获取当前订阅
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import 'package:kaer_with_panels/singbox/service/singbox_service.dart';
|
|||||||
import 'package:kaer_with_panels/singbox/service/singbox_service_provider.dart';
|
import 'package:kaer_with_panels/singbox/service/singbox_service_provider.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
import 'package:device_info_plus/device_info_plus.dart';
|
|
||||||
|
|
||||||
import '../../../core/model/directories.dart';
|
import '../../../core/model/directories.dart';
|
||||||
import '../../../singbox/model/singbox_config_option.dart';
|
import '../../../singbox/model/singbox_config_option.dart';
|
||||||
@@ -22,7 +21,6 @@ import '../../../singbox/model/singbox_status.dart';
|
|||||||
import '../../utils/kr_country_util.dart';
|
import '../../utils/kr_country_util.dart';
|
||||||
import '../../utils/kr_log_util.dart';
|
import '../../utils/kr_log_util.dart';
|
||||||
import '../../utils/kr_secure_storage.dart';
|
import '../../utils/kr_secure_storage.dart';
|
||||||
import '../../utils/kr_windows_dns_util.dart';
|
|
||||||
import '../../common/app_run_data.dart';
|
import '../../common/app_run_data.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
@@ -66,7 +64,6 @@ class KRSingBoxImp {
|
|||||||
Map<String, dynamic> kr_configOption = {};
|
Map<String, dynamic> kr_configOption = {};
|
||||||
|
|
||||||
List<Map<String, dynamic>> kr_outbounds = [];
|
List<Map<String, dynamic>> kr_outbounds = [];
|
||||||
List<Map<String, dynamic>> Kr_allOutbounds = [];
|
|
||||||
|
|
||||||
/// 首次启动
|
/// 首次启动
|
||||||
RxBool kr_isFristStart = false.obs;
|
RxBool kr_isFristStart = false.obs;
|
||||||
@@ -110,65 +107,6 @@ class KRSingBoxImp {
|
|||||||
/// 初始化标志,防止重复初始化
|
/// 初始化标志,防止重复初始化
|
||||||
bool _kr_isInitialized = false;
|
bool _kr_isInitialized = false;
|
||||||
|
|
||||||
/// 初始化进行中共享 Future(single-flight 防并发)
|
|
||||||
Future<void>? _kr_initFuture;
|
|
||||||
|
|
||||||
/// 是否为Android模拟器
|
|
||||||
bool _kr_isAndroidEmulator = false;
|
|
||||||
|
|
||||||
/// 检测并缓存模拟器标识
|
|
||||||
///
|
|
||||||
/// - 仅在 Android 上进行模拟器特征识别
|
|
||||||
/// - 通过 brand/model/product/manufacturer 的常见模拟器特征判断
|
|
||||||
/// 返回:无(结果写入 `_kr_isAndroidEmulator`)
|
|
||||||
Future<void> _kr_detectEmulator() async {
|
|
||||||
try {
|
|
||||||
if (!Platform.isAndroid) {
|
|
||||||
_kr_isAndroidEmulator = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final info = await DeviceInfoPlugin().androidInfo;
|
|
||||||
final brand = (info.brand ?? '').toLowerCase();
|
|
||||||
final model = (info.model ?? '').toLowerCase();
|
|
||||||
final product = (info.product ?? '').toLowerCase();
|
|
||||||
final manufacturer = (info.manufacturer ?? '').toLowerCase();
|
|
||||||
final device = (info.device ?? '').toLowerCase();
|
|
||||||
|
|
||||||
bool containsAny(String s, List<String> keys) => keys.any((k) => s.contains(k));
|
|
||||||
|
|
||||||
final indicators = <bool>[
|
|
||||||
containsAny(brand, [
|
|
||||||
'generic', 'unknown', 'google', 'vbox', 'virtualbox',
|
|
||||||
'bluestacks', 'nox', 'ldplayer', 'genymotion', 'mumu', 'netease', 'leidian'
|
|
||||||
]),
|
|
||||||
containsAny(model, [
|
|
||||||
'emulator', 'sdk', 'sdk_gphone', 'google_sdk', 'android sdk built for x86',
|
|
||||||
'bluestacks', 'genymotion', 'nox', 'ldplayer', 'mumu'
|
|
||||||
]),
|
|
||||||
containsAny(product, [
|
|
||||||
'sdk', 'google_sdk', 'sdk_gphone', 'emulator', 'vbox', 'virtualbox'
|
|
||||||
]),
|
|
||||||
containsAny(manufacturer, [
|
|
||||||
'genymotion', 'unknown', 'bluestacks', 'nox', 'ld', 'netease', 'mumu'
|
|
||||||
]),
|
|
||||||
containsAny(device, [
|
|
||||||
'emulator', 'generic', 'vbox', 'virtualbox', 'sdk'
|
|
||||||
]),
|
|
||||||
];
|
|
||||||
|
|
||||||
_kr_isAndroidEmulator = indicators.any((v) => v);
|
|
||||||
KRLogUtil.kr_i(
|
|
||||||
'🔍 Android 模拟器检测: ${_kr_isAndroidEmulator ? '是模拟器' : '非模拟器'}'
|
|
||||||
' (brand=$brand, model=$model, product=$product, manufacturer=$manufacturer, device=$device)',
|
|
||||||
tag: 'SingBox',
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
_kr_isAndroidEmulator = false;
|
|
||||||
KRLogUtil.kr_w('⚠️ 模拟器检测失败,按真机处理: $e', tag: 'SingBox');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 当前混合代理端口是否就绪
|
/// 当前混合代理端口是否就绪
|
||||||
bool get kr_isProxyReady => kr_status.value is SingboxStarted;
|
bool get kr_isProxyReady => kr_status.value is SingboxStarted;
|
||||||
|
|
||||||
@@ -201,32 +139,18 @@ class KRSingBoxImp {
|
|||||||
|
|
||||||
/// 初始化
|
/// 初始化
|
||||||
Future<void> init() async {
|
Future<void> init() async {
|
||||||
// 防止重复初始化(已完成)
|
// 防止重复初始化
|
||||||
if (_kr_isInitialized) {
|
if (_kr_isInitialized) {
|
||||||
KRLogUtil.kr_i('SingBox 已经初始化,跳过重复初始化', tag: 'SingBox');
|
KRLogUtil.kr_i('SingBox 已经初始化,跳过重复初始化', tag: 'SingBox');
|
||||||
return;
|
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 {
|
try {
|
||||||
KRLogUtil.kr_i('开始初始化 SingBox');
|
KRLogUtil.kr_i('开始初始化 SingBox');
|
||||||
// 在应用启动时初始化
|
// 在应用启动时初始化
|
||||||
await KRCountryUtil.kr_init();
|
await KRCountryUtil.kr_init();
|
||||||
KRLogUtil.kr_i('国家工具初始化完成');
|
KRLogUtil.kr_i('国家工具初始化完成');
|
||||||
|
|
||||||
// 设备环境检测(Android模拟器)
|
|
||||||
await _kr_detectEmulator();
|
|
||||||
|
|
||||||
final oOption = SingboxConfigOption.fromJson(_getConfigOption());
|
final oOption = SingboxConfigOption.fromJson(_getConfigOption());
|
||||||
KRLogUtil.kr_i('配置选项初始化完成');
|
KRLogUtil.kr_i('配置选项初始化完成');
|
||||||
|
|
||||||
@@ -243,14 +167,14 @@ class KRSingBoxImp {
|
|||||||
KRLogUtil.kr_i('iOS 路径获取完成: $paths');
|
KRLogUtil.kr_i('iOS 路径获取完成: $paths');
|
||||||
|
|
||||||
kr_configDics = (
|
kr_configDics = (
|
||||||
baseDir: Directory(paths?["base"]! as String),
|
baseDir: Directory(paths?["base"]! as String),
|
||||||
workingDir: Directory(paths?["working"]! as String),
|
workingDir: Directory(paths?["working"]! as String),
|
||||||
tempDir: Directory(paths?["temp"]! as String),
|
tempDir: Directory(paths?["temp"]! as String),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
final baseDir = await getApplicationSupportDirectory();
|
final baseDir = await getApplicationSupportDirectory();
|
||||||
final workingDir =
|
final workingDir =
|
||||||
Platform.isAndroid ? await getExternalStorageDirectory() : baseDir;
|
Platform.isAndroid ? await getExternalStorageDirectory() : baseDir;
|
||||||
final tempDir = await getTemporaryDirectory();
|
final tempDir = await getTemporaryDirectory();
|
||||||
|
|
||||||
// Windows 路径规范化:确保使用正确的路径分隔符
|
// Windows 路径规范化:确保使用正确的路径分隔符
|
||||||
@@ -266,9 +190,9 @@ class KRSingBoxImp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
kr_configDics = (
|
kr_configDics = (
|
||||||
baseDir: normalizePath(baseDir),
|
baseDir: normalizePath(baseDir),
|
||||||
workingDir: normalizePath(workingDir!),
|
workingDir: normalizePath(workingDir!),
|
||||||
tempDir: normalizePath(tempDir),
|
tempDir: normalizePath(tempDir),
|
||||||
);
|
);
|
||||||
KRLogUtil.kr_i('其他平台路径初始化完成');
|
KRLogUtil.kr_i('其他平台路径初始化完成');
|
||||||
}
|
}
|
||||||
@@ -476,32 +400,22 @@ class KRSingBoxImp {
|
|||||||
KRLogUtil.kr_i('📡 开始调用 setup() 注册 FFI 端口', tag: 'SingBox');
|
KRLogUtil.kr_i('📡 开始调用 setup() 注册 FFI 端口', tag: 'SingBox');
|
||||||
final setupResult = await kr_singBox.setup(kr_configDics, false).run();
|
final setupResult = await kr_singBox.setup(kr_configDics, false).run();
|
||||||
setupResult.match(
|
setupResult.match(
|
||||||
(error) {
|
(error) {
|
||||||
KRLogUtil.kr_e('❌ setup() 失败: $error', tag: 'SingBox');
|
KRLogUtil.kr_e('❌ setup() 失败: $error', tag: 'SingBox');
|
||||||
throw Exception('FFI setup 失败: $error');
|
throw Exception('FFI setup 失败: $error');
|
||||||
},
|
},
|
||||||
(_) {
|
(_) {
|
||||||
KRLogUtil.kr_i('✅ setup() 成功,FFI 端口已注册', tag: 'SingBox');
|
KRLogUtil.kr_i('✅ setup() 成功,FFI 端口已注册', tag: 'SingBox');
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
KRLogUtil.kr_i('✅ 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;
|
_kr_isInitialized = true;
|
||||||
|
|
||||||
// 🔑 关键:在初始化完成后立即订阅状态变化流
|
// 🔑 关键:在初始化完成后立即订阅状态变化流
|
||||||
// 这样可以确保 UI 始终与 libcore 的实际状态同步
|
// 这样可以确保 UI 始终与 libcore 的实际状态同步
|
||||||
_kr_subscribeToStatus();
|
_kr_subscribeToStatus();
|
||||||
KRLogUtil.kr_i('✅ 状态订阅已设置', tag: 'SingBox');
|
KRLogUtil.kr_i('✅ 状态订阅已设置', tag: 'SingBox');
|
||||||
|
|
||||||
// 完成 single-flight
|
|
||||||
completer.complete();
|
|
||||||
_kr_initFuture = null;
|
|
||||||
} catch (e, stackTrace) {
|
} catch (e, stackTrace) {
|
||||||
KRLogUtil.kr_e('❌ SingBox 初始化失败: $e');
|
KRLogUtil.kr_e('❌ SingBox 初始化失败: $e');
|
||||||
KRLogUtil.kr_e('📚 错误堆栈: $stackTrace');
|
KRLogUtil.kr_e('📚 错误堆栈: $stackTrace');
|
||||||
@@ -537,89 +451,26 @@ class KRSingBoxImp {
|
|||||||
|
|
||||||
// 如果初始化失败,允许下次重试
|
// 如果初始化失败,允许下次重试
|
||||||
_kr_isInitialized = false;
|
_kr_isInitialized = false;
|
||||||
// 失败时通知等待者并清理 single-flight
|
|
||||||
if (!completer.isCompleted) {
|
|
||||||
completer.completeError(e, stackTrace);
|
|
||||||
}
|
|
||||||
_kr_initFuture = null;
|
|
||||||
rethrow;
|
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() {
|
Map<String, dynamic> _getConfigOption() {
|
||||||
// 不使用缓存,每次都重新生成配置
|
// 不使用缓存,每次都重新生成配置
|
||||||
// if (kr_configOption.isNotEmpty) {
|
// if (kr_configOption.isNotEmpty) {
|
||||||
// return kr_configOption;
|
// 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 = {
|
final op = {
|
||||||
"region": effectiveRegion, // 🔧 修复:根据出站模式动态设置 region
|
"region": "other", // 参考 hiddify-app: 默认使用 "other" 跳过规则集下载
|
||||||
"block-ads": false, // 参考 hiddify-app: 默认关闭广告拦截
|
"block-ads": false, // 参考 hiddify-app: 默认关闭广告拦截
|
||||||
"use-xray-core-when-possible": false,
|
"use-xray-core-when-possible": false,
|
||||||
"execute-config-as-is": true, // 🔧 修复:使用我们生成的完整配置,不让 libcore 重写
|
"execute-config-as-is": false,
|
||||||
"log-level": "info", // 调试阶段使用 info,生产环境改为 warn
|
"log-level": "info", // 调试阶段使用 info,生产环境改为 warn
|
||||||
"resolve-destination": false,
|
"resolve-destination": false,
|
||||||
"ipv6-mode": "ipv4_only", // 参考 hiddify-app: 仅使用 IPv4 (有效值: ipv4_only, prefer_ipv4, prefer_ipv6, ipv6_only)
|
"ipv6-mode": "ipv4_only", // 参考 hiddify-app: 仅使用 IPv4 (有效值: ipv4_only, prefer_ipv4, prefer_ipv6, ipv6_only)
|
||||||
"remote-dns-address": _kr_isAndroidEmulator
|
"remote-dns-address": "udp://1.1.1.1", // 参考 hiddify-app: 使用 Cloudflare DNS
|
||||||
? "local"
|
|
||||||
: "https://dns.google/dns-query",
|
|
||||||
"remote-dns-domain-strategy": "prefer_ipv4",
|
"remote-dns-domain-strategy": "prefer_ipv4",
|
||||||
"direct-dns-address": "local",
|
"direct-dns-address": "udp://1.1.1.1", // 参考 hiddify-app: 统一使用 1.1.1.1
|
||||||
"direct-dns-domain-strategy": "prefer_ipv4",
|
"direct-dns-domain-strategy": "prefer_ipv4",
|
||||||
"mixed-port": kr_port,
|
"mixed-port": kr_port,
|
||||||
"tproxy-port": kr_port,
|
"tproxy-port": kr_port,
|
||||||
@@ -634,21 +485,13 @@ class KRSingBoxImp {
|
|||||||
"enable-tun": Platform.isIOS || Platform.isAndroid,
|
"enable-tun": Platform.isIOS || Platform.isAndroid,
|
||||||
"enable-tun-service": false,
|
"enable-tun-service": false,
|
||||||
"set-system-proxy":
|
"set-system-proxy":
|
||||||
Platform.isWindows || Platform.isLinux || Platform.isMacOS,
|
Platform.isWindows || Platform.isLinux || Platform.isMacOS,
|
||||||
"bypass-lan": false,
|
"bypass-lan": false,
|
||||||
"allow-connection-from-lan": false,
|
"allow-connection-from-lan": false,
|
||||||
"enable-fake-dns": false,
|
"enable-fake-dns": false,
|
||||||
"enable-dns-routing": true,
|
"enable-dns-routing": true,
|
||||||
"independent-dns-cache": true,
|
"independent-dns-cache": true,
|
||||||
"rules": [
|
"rules": [],
|
||||||
// ✅ 自定义域名直连规则 - 添加到 HiddifyOptions.Rules 中
|
|
||||||
// 这样 Native 层的 config.BuildConfig() 会将其包含到最终配置
|
|
||||||
// 注意:libcore 要求 domains 字段使用前缀格式,如 "domain:ip.sb" 表示 domain_suffix
|
|
||||||
{
|
|
||||||
"domains": "domain:api.hifast.biz", // domain: 前缀表示 domain_suffix 匹配
|
|
||||||
"outbound": "bypass" // bypass = direct
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"mux": {
|
"mux": {
|
||||||
"enable": false,
|
"enable": false,
|
||||||
"padding": false,
|
"padding": false,
|
||||||
@@ -693,61 +536,9 @@ class KRSingBoxImp {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
kr_configOption = op;
|
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;
|
return op;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 动态构建 DNS 配置
|
|
||||||
///
|
|
||||||
/// - 真机:使用远程 DoH + 本地系统 DNS,final 指向远程
|
|
||||||
/// - 模拟器:仅使用系统 DNS,final 指向直连,避免覆盖系统 DNS
|
|
||||||
/// 返回:Sing-box `dns` 段配置 Map
|
|
||||||
Map<String, dynamic> _kr_buildDnsConfig() {
|
|
||||||
if (_kr_isAndroidEmulator) {
|
|
||||||
KRLogUtil.kr_i('🛡️ 模拟器兼容模式:强制使用系统 DNS', tag: 'SingBox');
|
|
||||||
return {
|
|
||||||
"servers": [
|
|
||||||
{
|
|
||||||
"tag": "dns-direct",
|
|
||||||
"address": "local",
|
|
||||||
"detour": "direct"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"rules": [],
|
|
||||||
"final": "dns-direct",
|
|
||||||
"strategy": "prefer_ipv4"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
"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(),
|
|
||||||
"final": "dns-remote",
|
|
||||||
"strategy": "prefer_ipv4"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 订阅状态变化流
|
/// 订阅状态变化流
|
||||||
/// 参考 hiddify-app: 监听 libcore 发送的状态事件来自动更新 UI
|
/// 参考 hiddify-app: 监听 libcore 发送的状态事件来自动更新 UI
|
||||||
void _kr_subscribeToStatus() {
|
void _kr_subscribeToStatus() {
|
||||||
@@ -770,7 +561,7 @@ class KRSingBoxImp {
|
|||||||
|
|
||||||
_kr_subscriptions.add(
|
_kr_subscriptions.add(
|
||||||
kr_singBox.watchStatus().listen(
|
kr_singBox.watchStatus().listen(
|
||||||
(status) {
|
(status) {
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
print('🔵 收到 Native 状态更新: ${status.runtimeType}');
|
print('🔵 收到 Native 状态更新: ${status.runtimeType}');
|
||||||
}
|
}
|
||||||
@@ -808,7 +599,7 @@ class KRSingBoxImp {
|
|||||||
// 所以外层必须有 try-catch
|
// 所以外层必须有 try-catch
|
||||||
final stream = kr_singBox.watchStats();
|
final stream = kr_singBox.watchStats();
|
||||||
final subscription = stream.listen(
|
final subscription = stream.listen(
|
||||||
(stats) {
|
(stats) {
|
||||||
kr_stats.value = stats;
|
kr_stats.value = stats;
|
||||||
},
|
},
|
||||||
onError: (error) {
|
onError: (error) {
|
||||||
@@ -833,7 +624,7 @@ class KRSingBoxImp {
|
|||||||
|
|
||||||
_kr_subscriptions.add(
|
_kr_subscriptions.add(
|
||||||
kr_singBox.watchActiveGroups().listen(
|
kr_singBox.watchActiveGroups().listen(
|
||||||
(groups) {
|
(groups) {
|
||||||
print('[watchActiveGroups] 📡 收到活动组更新,数量: ${groups.length}');
|
print('[watchActiveGroups] 📡 收到活动组更新,数量: ${groups.length}');
|
||||||
KRLogUtil.kr_i('📡 收到活动组更新,数量: ${groups.length}', tag: 'SingBox');
|
KRLogUtil.kr_i('📡 收到活动组更新,数量: ${groups.length}', tag: 'SingBox');
|
||||||
kr_activeGroups.value = groups;
|
kr_activeGroups.value = groups;
|
||||||
@@ -860,7 +651,7 @@ class KRSingBoxImp {
|
|||||||
|
|
||||||
_kr_subscriptions.add(
|
_kr_subscriptions.add(
|
||||||
kr_singBox.watchGroups().listen(
|
kr_singBox.watchGroups().listen(
|
||||||
(groups) {
|
(groups) {
|
||||||
print('[watchGroups] 📡 收到所有组更新,数量: ${groups.length}');
|
print('[watchGroups] 📡 收到所有组更新,数量: ${groups.length}');
|
||||||
kr_allGroups.value = groups;
|
kr_allGroups.value = groups;
|
||||||
// 打印每个组的基本信息
|
// 打印每个组的基本信息
|
||||||
@@ -888,7 +679,7 @@ class KRSingBoxImp {
|
|||||||
|
|
||||||
// 查找 "select" 组
|
// 查找 "select" 组
|
||||||
final selectGroup = kr_activeGroups.firstWhere(
|
final selectGroup = kr_activeGroups.firstWhere(
|
||||||
(group) => group.tag == 'select',
|
(group) => group.tag == 'select',
|
||||||
orElse: () => throw Exception('未找到 "select" 选择器组'),
|
orElse: () => throw Exception('未找到 "select" 选择器组'),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -928,11 +719,11 @@ class KRSingBoxImp {
|
|||||||
///
|
///
|
||||||
/// 确保 command.sock 准备好后再执行节点选择
|
/// 确保 command.sock 准备好后再执行节点选择
|
||||||
Future<void> _kr_selectOutboundWithRetry(
|
Future<void> _kr_selectOutboundWithRetry(
|
||||||
String groupTag,
|
String groupTag,
|
||||||
String outboundTag, {
|
String outboundTag, {
|
||||||
int maxAttempts = 3,
|
int maxAttempts = 3,
|
||||||
int initialDelay = 100,
|
int initialDelay = 100,
|
||||||
}) async {
|
}) async {
|
||||||
int attempt = 0;
|
int attempt = 0;
|
||||||
int delay = initialDelay;
|
int delay = initialDelay;
|
||||||
|
|
||||||
@@ -1161,7 +952,7 @@ class KRSingBoxImp {
|
|||||||
await Future.delayed(const Duration(milliseconds: 2000));
|
await Future.delayed(const Duration(milliseconds: 2000));
|
||||||
|
|
||||||
final savedNode = await KRSecureStorage().kr_readData(key: _keySelectedNode);
|
final savedNode = await KRSecureStorage().kr_readData(key: _keySelectedNode);
|
||||||
if (savedNode != null && savedNode.isNotEmpty && savedNode != 'auto' && !savedNode.endsWith('-auto')) {
|
if (savedNode != null && savedNode.isNotEmpty && savedNode != 'auto') {
|
||||||
KRLogUtil.kr_i('🔄 恢复用户选择的节点: $savedNode', tag: 'SingBox');
|
KRLogUtil.kr_i('🔄 恢复用户选择的节点: $savedNode', tag: 'SingBox');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -1202,9 +993,7 @@ class KRSingBoxImp {
|
|||||||
// print("错误堆栈: $stack");
|
// print("错误堆栈: $stack");
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
void kr_saveAllOutbounds(List<Map<String, dynamic>> outbounds) {
|
|
||||||
Kr_allOutbounds = outbounds;
|
|
||||||
}
|
|
||||||
/// 保存配置文件
|
/// 保存配置文件
|
||||||
void kr_saveOutbounds(List<Map<String, dynamic>> outbounds) async {
|
void kr_saveOutbounds(List<Map<String, dynamic>> outbounds) async {
|
||||||
KRLogUtil.kr_i('💾 开始保存配置文件...', tag: 'SingBox');
|
KRLogUtil.kr_i('💾 开始保存配置文件...', tag: 'SingBox');
|
||||||
@@ -1221,9 +1010,6 @@ class KRSingBoxImp {
|
|||||||
if (outbound['method'] != null) {
|
if (outbound['method'] != null) {
|
||||||
KRLogUtil.kr_i(' - method: ${outbound['method']}', tag: 'SingBox');
|
KRLogUtil.kr_i(' - method: ${outbound['method']}', tag: 'SingBox');
|
||||||
}
|
}
|
||||||
if (outbound['interval'] != null) {
|
|
||||||
KRLogUtil.kr_i(' - interval: ${outbound['interval']}', tag: 'SingBox');
|
|
||||||
}
|
|
||||||
if (outbound['password'] != null) {
|
if (outbound['password'] != null) {
|
||||||
KRLogUtil.kr_i(' - password: ${outbound['password']?.toString().substring(0, 8)}...', tag: 'SingBox');
|
KRLogUtil.kr_i(' - password: ${outbound['password']?.toString().substring(0, 8)}...', tag: 'SingBox');
|
||||||
}
|
}
|
||||||
@@ -1245,64 +1031,17 @@ class KRSingBoxImp {
|
|||||||
|
|
||||||
KRLogUtil.kr_i('✅ 过滤后节点数量: ${kr_outbounds.length}/${outbounds.length}', tag: 'SingBox');
|
KRLogUtil.kr_i('✅ 过滤后节点数量: ${kr_outbounds.length}/${outbounds.length}', tag: 'SingBox');
|
||||||
|
|
||||||
// 🔧 修复:生成完整的 SingBox 配置
|
// 只保存 outbounds,Mobile.buildConfig() 会添加其他配置
|
||||||
// 之前只保存 {"outbounds": [...]}, 导致 libcore 无法正确处理
|
final map = {
|
||||||
// 现在保存完整配置,包含所有必需字段
|
"outbounds": kr_outbounds
|
||||||
final Map<String, dynamic> fullConfig = {
|
|
||||||
"log": {
|
|
||||||
"level": "debug",
|
|
||||||
"timestamp": true
|
|
||||||
},
|
|
||||||
"dns": _kr_buildDnsConfig(),
|
|
||||||
"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 file = _file(kr_configName);
|
||||||
final temp = _tempFile(kr_configName);
|
final temp = _tempFile(kr_configName);
|
||||||
final mapStr = jsonEncode(fullConfig);
|
final mapStr = jsonEncode(map);
|
||||||
|
|
||||||
KRLogUtil.kr_i('📄 完整配置文件长度: ${mapStr.length}', tag: 'SingBox');
|
KRLogUtil.kr_i('📄 配置文件内容长度: ${mapStr.length}', tag: 'SingBox');
|
||||||
KRLogUtil.kr_i('📄 Outbounds 数量: ${kr_outbounds.length}', tag: 'SingBox');
|
KRLogUtil.kr_i('📄 完整配置文件内容: $mapStr', tag: 'SingBox');
|
||||||
KRLogUtil.kr_i('📄 配置前800字符:\n${mapStr.substring(0, mapStr.length > 800 ? 800 : mapStr.length)}', tag: 'SingBox');
|
|
||||||
|
|
||||||
await file.writeAsString(mapStr);
|
await file.writeAsString(mapStr);
|
||||||
await temp.writeAsString(mapStr);
|
await temp.writeAsString(mapStr);
|
||||||
@@ -1319,128 +1058,6 @@ class KRSingBoxImp {
|
|||||||
KRLogUtil.kr_i('✅ 配置文件保存完成', tag: 'SingBox');
|
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 {
|
Future<void> kr_start() async {
|
||||||
// 不再手动设置状态,libcore 会通过 status stream 自动发送状态更新
|
// 不再手动设置状态,libcore 会通过 status stream 自动发送状态更新
|
||||||
try {
|
try {
|
||||||
@@ -1452,46 +1069,9 @@ class KRSingBoxImp {
|
|||||||
|
|
||||||
// ⚠️ 强制编译标记 - v2.0-lazy-load
|
// ⚠️ 强制编译标记 - v2.0-lazy-load
|
||||||
KRLogUtil.kr_i('🚀🚀🚀 [v2.0-lazy-load] 开始启动 SingBox...', tag: 'SingBox');
|
KRLogUtil.kr_i('🚀🚀🚀 [v2.0-lazy-load] 开始启动 SingBox...', tag: 'SingBox');
|
||||||
|
|
||||||
// 🔧 强制重新生成配置文件(确保最新的路由规则生效)
|
|
||||||
if (kr_outbounds.isNotEmpty) {
|
|
||||||
KRLogUtil.kr_i('🔄 启动前强制重新生成配置文件...', tag: 'SingBox');
|
|
||||||
final selectedNode = await KRSecureStorage().kr_readData(key: _keySelectedNode);
|
|
||||||
var toSave = Kr_allOutbounds;
|
|
||||||
if (selectedNode != null && selectedNode.endsWith('-auto')) {
|
|
||||||
KRLogUtil.kr_i('🤖 检测到自动国家节点: $selectedNode', tag: 'SingBox');
|
|
||||||
final selectedCountry = selectedNode.replaceAll(RegExp(r'-auto$'), '');
|
|
||||||
toSave = Kr_allOutbounds.where((o) {
|
|
||||||
final country = o['country']?.toString() ?? '';
|
|
||||||
if (country.isNotEmpty) return country == selectedCountry;
|
|
||||||
final tag = o['tag']?.toString() ?? '';
|
|
||||||
if (tag == selectedNode) return true;
|
|
||||||
return tag.toLowerCase().contains(selectedCountry.toLowerCase());
|
|
||||||
}).toList();
|
|
||||||
KRLogUtil.kr_i('✅ 自动国家过滤: ${toSave.length}/${Kr_allOutbounds.length}', tag: 'SingBox');
|
|
||||||
}
|
|
||||||
kr_saveOutbounds(toSave);
|
|
||||||
await Future.delayed(const Duration(milliseconds: 100));
|
|
||||||
}
|
|
||||||
|
|
||||||
KRLogUtil.kr_i('📁 配置文件路径: $_cutPath', tag: 'SingBox');
|
KRLogUtil.kr_i('📁 配置文件路径: $_cutPath', tag: 'SingBox');
|
||||||
KRLogUtil.kr_i('📝 配置名称: $kr_configName', tag: 'SingBox');
|
KRLogUtil.kr_i('📝 配置名称: $kr_configName', tag: 'SingBox');
|
||||||
|
|
||||||
// 🔑 Windows 平台:在启动前备份 DNS 设置
|
|
||||||
if (Platform.isWindows) {
|
|
||||||
KRLogUtil.kr_i('🪟 Windows 平台,备份 DNS 设置...', tag: 'SingBox');
|
|
||||||
try {
|
|
||||||
final backupSuccess = await KRWindowsDnsUtil.instance.kr_backupDnsSettings();
|
|
||||||
if (backupSuccess) {
|
|
||||||
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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 🔑 先尝试停止旧实例,避免 command.sock 冲突
|
// 🔑 先尝试停止旧实例,避免 command.sock 冲突
|
||||||
// 注意:如果没有旧实例,libcore 会报错 "command.sock: no such file",这是正常的
|
// 注意:如果没有旧实例,libcore 会报错 "command.sock: no such file",这是正常的
|
||||||
try {
|
try {
|
||||||
@@ -1509,11 +1089,11 @@ class KRSingBoxImp {
|
|||||||
final oOption = SingboxConfigOption.fromJson(_getConfigOption());
|
final oOption = SingboxConfigOption.fromJson(_getConfigOption());
|
||||||
final changeResult = await kr_singBox.changeOptions(oOption).run();
|
final changeResult = await kr_singBox.changeOptions(oOption).run();
|
||||||
changeResult.match(
|
changeResult.match(
|
||||||
(error) {
|
(error) {
|
||||||
KRLogUtil.kr_e('❌ changeOptions() 失败: $error', tag: 'SingBox');
|
KRLogUtil.kr_e('❌ changeOptions() 失败: $error', tag: 'SingBox');
|
||||||
throw Exception('初始化 HiddifyOptions 失败: $error');
|
throw Exception('初始化 HiddifyOptions 失败: $error');
|
||||||
},
|
},
|
||||||
(_) {
|
(_) {
|
||||||
KRLogUtil.kr_i('✅ HiddifyOptions 初始化成功', tag: 'SingBox');
|
KRLogUtil.kr_i('✅ HiddifyOptions 初始化成功', tag: 'SingBox');
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -1532,7 +1112,7 @@ class KRSingBoxImp {
|
|||||||
_kr_subscribeToStatus();
|
_kr_subscribeToStatus();
|
||||||
|
|
||||||
await kr_singBox.start(_cutPath, kr_configName, false).map(
|
await kr_singBox.start(_cutPath, kr_configName, false).map(
|
||||||
(r) {
|
(r) {
|
||||||
KRLogUtil.kr_i('✅ SingBox 启动成功', tag: 'SingBox');
|
KRLogUtil.kr_i('✅ SingBox 启动成功', tag: 'SingBox');
|
||||||
},
|
},
|
||||||
).mapLeft((err) {
|
).mapLeft((err) {
|
||||||
@@ -1645,12 +1225,12 @@ class KRSingBoxImp {
|
|||||||
await Future.delayed(const Duration(milliseconds: 100));
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
// 添加超时保护,防止 stop() 调用阻塞
|
// 添加超时保护,防止 stop() 调用阻塞
|
||||||
// 🔧 延长超时时间到 10 秒,给 Windows DNS 清理足够时间
|
// 缩短超时时间,避免用户等待过久
|
||||||
try {
|
try {
|
||||||
await kr_singBox.stop().run().timeout(
|
await kr_singBox.stop().run().timeout(
|
||||||
const Duration(seconds: 10), // ← 从 3 秒延长到 10 秒
|
const Duration(seconds: 3),
|
||||||
onTimeout: () {
|
onTimeout: () {
|
||||||
KRLogUtil.kr_w('⚠️ 停止操作超时(10秒),强制继续', tag: 'SingBox');
|
KRLogUtil.kr_w('⚠️ 停止操作超时(3秒),强制继续', tag: 'SingBox');
|
||||||
return const Left('timeout');
|
return const Left('timeout');
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -1659,29 +1239,7 @@ class KRSingBoxImp {
|
|||||||
// 继续执行清理操作
|
// 继续执行清理操作
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔑 Windows 平台:停止后立即恢复 DNS 设置
|
await Future.delayed(const Duration(milliseconds: 500));
|
||||||
if (Platform.isWindows) {
|
|
||||||
KRLogUtil.kr_i('🪟 Windows 平台,开始恢复 DNS 设置...', tag: 'SingBox');
|
|
||||||
|
|
||||||
// 等待 sing-box 完全停止
|
|
||||||
await Future.delayed(const Duration(milliseconds: 1000));
|
|
||||||
|
|
||||||
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');
|
|
||||||
// 异常时也会在工具类内部执行兜底恢复
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 非 Windows 平台正常等待
|
|
||||||
await Future.delayed(const Duration(milliseconds: 500));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 取消统计和分组订阅,但保留状态订阅以便继续接收状态更新
|
// 取消统计和分组订阅,但保留状态订阅以便继续接收状态更新
|
||||||
final subscriptionsToCancel = _kr_subscriptions.where((sub) {
|
final subscriptionsToCancel = _kr_subscriptions.where((sub) {
|
||||||
@@ -1703,17 +1261,6 @@ class KRSingBoxImp {
|
|||||||
} catch (e, stackTrace) {
|
} catch (e, stackTrace) {
|
||||||
KRLogUtil.kr_e('停止服务时出错: $e');
|
KRLogUtil.kr_e('停止服务时出错: $e');
|
||||||
KRLogUtil.kr_e('错误堆栈: $stackTrace');
|
KRLogUtil.kr_e('错误堆栈: $stackTrace');
|
||||||
|
|
||||||
// 🔑 即使出错,也要尝试恢复 Windows DNS
|
|
||||||
if (Platform.isWindows) {
|
|
||||||
KRLogUtil.kr_w('⚠️ 停止异常,强制执行 DNS 恢复', tag: 'SingBox');
|
|
||||||
try {
|
|
||||||
await KRWindowsDnsUtil.instance.kr_restoreDnsSettings();
|
|
||||||
} catch (dnsError) {
|
|
||||||
KRLogUtil.kr_e('❌ 强制 DNS 恢复失败: $dnsError', tag: 'SingBox');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 不手动设置状态,信任 libcore 的状态管理
|
// 不手动设置状态,信任 libcore 的状态管理
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
@@ -1737,25 +1284,10 @@ class KRSingBoxImp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> kr_restart() async {
|
Future<void> kr_restart() async {
|
||||||
KRLogUtil.kr_i('🔄 重启 SingBox...', tag: 'SingBox');
|
KRLogUtil.kr_i("restart");
|
||||||
|
kr_singBox.restart(_cutPath, kr_configName, false).mapLeft((err) {
|
||||||
// 🔧 修复:使用 stop + start 而不是 restart
|
KRLogUtil.kr_e('重启失败: $err');
|
||||||
// 这样可以触发 Windows DNS 恢复和备份逻辑
|
}).run();
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//// 设置出站模式
|
//// 设置出站模式
|
||||||
@@ -1784,9 +1316,9 @@ class KRSingBoxImp {
|
|||||||
mode = KRCountryUtil.kr_getCurrentCountryCode();
|
mode = KRCountryUtil.kr_getCurrentCountryCode();
|
||||||
KRLogUtil.kr_i('🎯 切换到规则代理模式: $mode', tag: 'SingBox');
|
KRLogUtil.kr_i('🎯 切换到规则代理模式: $mode', tag: 'SingBox');
|
||||||
break;
|
break;
|
||||||
// case KRConnectionType.direct:
|
// case KRConnectionType.direct:
|
||||||
// mode = "direct";
|
// mode = "direct";
|
||||||
// break;
|
// break;
|
||||||
}
|
}
|
||||||
oOption["region"] = mode;
|
oOption["region"] = mode;
|
||||||
KRLogUtil.kr_i('📝 更新 region 配置: $mode', tag: 'SingBox');
|
KRLogUtil.kr_i('📝 更新 region 配置: $mode', tag: 'SingBox');
|
||||||
@@ -1897,11 +1429,9 @@ class KRSingBoxImp {
|
|||||||
await _kr_ensureCommandClientInitialized();
|
await _kr_ensureCommandClientInitialized();
|
||||||
KRLogUtil.kr_i('✅ Command client 已就绪,执行节点切换', tag: 'SingBox');
|
KRLogUtil.kr_i('✅ Command client 已就绪,执行节点切换', tag: 'SingBox');
|
||||||
|
|
||||||
// 🔧 关键修复:使用正确的 group tag
|
// 🔧 关键修复:使用 await 等待节点选择完成
|
||||||
// libcore 生成的selector组的tag是"proxy"而不是"select"
|
KRLogUtil.kr_i('⏳ 调用 selectOutbound("select", "$tag")...', tag: 'SingBox');
|
||||||
final selectorGroupTag = kr_activeGroups.any((g) => g.tag == 'select') ? 'select' : 'proxy';
|
await _kr_selectOutboundWithRetry("select", tag, maxAttempts: 3, initialDelay: 50);
|
||||||
KRLogUtil.kr_i('⏳ 调用 selectOutbound("$selectorGroupTag", "$tag")...', tag: 'SingBox');
|
|
||||||
await _kr_selectOutboundWithRetry(selectorGroupTag, tag, maxAttempts: 3, initialDelay: 50);
|
|
||||||
KRLogUtil.kr_i('✅ 节点切换API调用完成: $tag', tag: 'SingBox');
|
KRLogUtil.kr_i('✅ 节点切换API调用完成: $tag', tag: 'SingBox');
|
||||||
|
|
||||||
// 🔧 新增:验证节点切换是否生效
|
// 🔧 新增:验证节点切换是否生效
|
||||||
@@ -1917,15 +1447,15 @@ class KRSingBoxImp {
|
|||||||
// 🔄 如果用户选择了具体节点(不是 auto),启动定期检查和重新选择
|
// 🔄 如果用户选择了具体节点(不是 auto),启动定期检查和重新选择
|
||||||
// 这是为了防止 urltest 自动覆盖用户的手动选择
|
// 这是为了防止 urltest 自动覆盖用户的手动选择
|
||||||
_nodeSelectionTimer?.cancel();
|
_nodeSelectionTimer?.cancel();
|
||||||
if (tag != 'auto' && !tag.endsWith('-auto')) {
|
if (tag != 'auto') {
|
||||||
KRLogUtil.kr_i('🔁 启动节点选择监控,防止被 auto 覆盖', tag: 'SingBox');
|
KRLogUtil.kr_i('🔁 启动节点选择监控,防止被 auto 覆盖', tag: 'SingBox');
|
||||||
_nodeSelectionTimer = Timer.periodic(const Duration(seconds: 20), (timer) {
|
_nodeSelectionTimer = Timer.periodic(const Duration(seconds: 20), (timer) {
|
||||||
// 每 20 秒重新选择一次,确保用户选择不被覆盖
|
// 每 20 秒重新选择一次,确保用户选择不被覆盖
|
||||||
// 使用 then/catchError 避免异常导致 UI 阻塞
|
// 使用 then/catchError 避免异常导致 UI 阻塞
|
||||||
kr_singBox.selectOutbound("select", tag).run().then((result) {
|
kr_singBox.selectOutbound("select", tag).run().then((result) {
|
||||||
result.match(
|
result.match(
|
||||||
(error) => KRLogUtil.kr_w('🔁 定时器重选节点失败: $error', tag: 'SingBox'),
|
(error) => KRLogUtil.kr_w('🔁 定时器重选节点失败: $error', tag: 'SingBox'),
|
||||||
(_) => KRLogUtil.kr_d('🔁 定时器重选节点成功', tag: 'SingBox'),
|
(_) => KRLogUtil.kr_d('🔁 定时器重选节点成功', tag: 'SingBox'),
|
||||||
);
|
);
|
||||||
}).catchError((error) {
|
}).catchError((error) {
|
||||||
KRLogUtil.kr_w('🔁 定时器重选节点异常: $error', tag: 'SingBox');
|
KRLogUtil.kr_w('🔁 定时器重选节点异常: $error', tag: 'SingBox');
|
||||||
@@ -1935,6 +1465,87 @@ 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 =>
|
Directory get directory =>
|
||||||
|
|||||||
@@ -15,8 +15,7 @@ enum KRCountry {
|
|||||||
ru('俄罗斯'),
|
ru('俄罗斯'),
|
||||||
id('印度尼西亚'),
|
id('印度尼西亚'),
|
||||||
tr('土耳其'),
|
tr('土耳其'),
|
||||||
br('巴西'),
|
br('巴西');
|
||||||
other('全局代理'); // ✅ 新增:全局代理模式(所有流量走代理,不添加国家直连规则)
|
|
||||||
|
|
||||||
final String kr_name;
|
final String kr_name;
|
||||||
const KRCountry(this.kr_name);
|
const KRCountry(this.kr_name);
|
||||||
@@ -31,7 +30,7 @@ enum KRCountry {
|
|||||||
static KRCountry? kr_fromCode(String code) {
|
static KRCountry? kr_fromCode(String code) {
|
||||||
try {
|
try {
|
||||||
return KRCountry.values.firstWhere(
|
return KRCountry.values.firstWhere(
|
||||||
(country) => country.kr_code == code.toLowerCase(),
|
(country) => country.kr_code == code.toLowerCase(),
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return null;
|
return null;
|
||||||
@@ -54,7 +53,7 @@ class KRCountryUtil {
|
|||||||
static Future<void> kr_init() async {
|
static Future<void> kr_init() async {
|
||||||
try {
|
try {
|
||||||
final String? kr_savedCountry =
|
final String? kr_savedCountry =
|
||||||
await _kr_storage.kr_readData(key: _kr_countryKey);
|
await _kr_storage.kr_readData(key: _kr_countryKey);
|
||||||
if (kr_savedCountry != null) {
|
if (kr_savedCountry != null) {
|
||||||
final KRCountry? kr_country = KRCountry.kr_fromCode(kr_savedCountry);
|
final KRCountry? kr_country = KRCountry.kr_fromCode(kr_savedCountry);
|
||||||
if (kr_country != null) {
|
if (kr_country != null) {
|
||||||
@@ -107,7 +106,7 @@ class KRCountryUtil {
|
|||||||
return kr_currentCountry.value.kr_code;
|
return kr_currentCountry.value.kr_code;
|
||||||
}
|
}
|
||||||
|
|
||||||
static String kr_getCurrentCountryName() {
|
static String kr_getCurrentCountryName() {
|
||||||
return kr_getCountryName(kr_currentCountry.value);
|
return kr_getCountryName(kr_currentCountry.value);
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -132,14 +131,12 @@ class KRCountryUtil {
|
|||||||
return AppTranslations.kr_country.tr;
|
return AppTranslations.kr_country.tr;
|
||||||
case KRCountry.br:
|
case KRCountry.br:
|
||||||
return AppTranslations.kr_country.br;
|
return AppTranslations.kr_country.br;
|
||||||
case KRCountry.other:
|
|
||||||
return '全局代理'; // ✅ 新增:全局代理
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取所有支持的国家列表
|
/// 获取所有支持的国家列表
|
||||||
static List<KRCountry> kr_getSupportedCountries() {
|
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.where((element) => element != KRCountry.cn).toList();
|
||||||
}
|
}
|
||||||
return KRCountry.values;
|
return KRCountry.values;
|
||||||
@@ -149,9 +146,9 @@ class KRCountryUtil {
|
|||||||
static List<Map<String, String>> kr_getCountryInfoList() {
|
static List<Map<String, String>> kr_getCountryInfoList() {
|
||||||
return KRCountry.values
|
return KRCountry.values
|
||||||
.map((country) => {
|
.map((country) => {
|
||||||
'code': country.kr_code,
|
'code': country.kr_code,
|
||||||
'name': country.kr_countryName,
|
'name': country.kr_countryName,
|
||||||
})
|
})
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,151 +1,127 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'kr_log_util.dart';
|
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
|
||||||
|
|
||||||
/// 真正的节点延迟测试工具
|
/// 延迟测试工具类
|
||||||
|
/// 提供真实的 TCP 连接延迟测试功能
|
||||||
class KRLatencyTester {
|
class KRLatencyTester {
|
||||||
/// TCP 连接测试延迟(真实测试)
|
/// 测试单个节点的延迟
|
||||||
/// 返回延迟毫秒数,失败返回 65535
|
///
|
||||||
static Future<int> testTcpLatency({
|
/// 参数:
|
||||||
|
/// - host: 主机地址
|
||||||
|
/// - port: 端口号
|
||||||
|
/// - timeout: 超时时间(毫秒)
|
||||||
|
///
|
||||||
|
/// 返回:
|
||||||
|
/// - 延迟时间(毫秒),如果失败返回 65535
|
||||||
|
static Future<int> testNode({
|
||||||
required String host,
|
required String host,
|
||||||
required int port,
|
required int port,
|
||||||
Duration timeout = const Duration(seconds: 5),
|
int timeout = 5000,
|
||||||
}) async {
|
}) async {
|
||||||
Socket? socket;
|
|
||||||
final stopwatch = Stopwatch();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
KRLogUtil.kr_i('🔌 开始测试: $host:$port', tag: 'LatencyTester');
|
final stopwatch = Stopwatch()..start();
|
||||||
|
|
||||||
stopwatch.start();
|
final socket = await Socket.connect(
|
||||||
|
|
||||||
// 尝试 TCP 连接
|
|
||||||
socket = await Socket.connect(
|
|
||||||
host,
|
host,
|
||||||
port,
|
port,
|
||||||
timeout: timeout,
|
timeout: Duration(milliseconds: timeout),
|
||||||
);
|
).timeout(Duration(milliseconds: timeout));
|
||||||
|
|
||||||
stopwatch.stop();
|
stopwatch.stop();
|
||||||
|
|
||||||
|
// 立即关闭连接
|
||||||
|
await socket.close();
|
||||||
|
socket.destroy();
|
||||||
|
|
||||||
final latency = stopwatch.elapsedMilliseconds;
|
final latency = stopwatch.elapsedMilliseconds;
|
||||||
|
KRLogUtil.kr_i('✅ 延迟测试成功: $host:$port = ${latency}ms', tag: 'KRLatencyTester');
|
||||||
KRLogUtil.kr_i('✅ 测试成功: $host:$port - ${latency}ms', tag: 'LatencyTester');
|
|
||||||
|
|
||||||
return latency;
|
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) {
|
} catch (e) {
|
||||||
stopwatch.stop();
|
KRLogUtil.kr_w('❌ 延迟测试失败: $host:$port - $e', tag: 'KRLatencyTester');
|
||||||
KRLogUtil.kr_e('❌ 测试异常: $host:$port - $e', tag: 'LatencyTester');
|
return 65535; // 测试失败返回最大值
|
||||||
return 65535;
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
// 确保关闭连接
|
|
||||||
try {
|
|
||||||
await socket?.close();
|
|
||||||
} catch (e) {
|
|
||||||
// 忽略关闭错误
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 批量测试多个节点延迟(并发测试)
|
/// 批量测试多个节点的延迟
|
||||||
/// 返回 Map<节点tag, 延迟ms>
|
///
|
||||||
|
/// 参数:
|
||||||
|
/// - nodes: 节点列表,格式为 [{"host": "example.com", "port": 443}]
|
||||||
|
/// - concurrency: 并发数量
|
||||||
|
/// - timeout: 超时时间(毫秒)
|
||||||
|
///
|
||||||
|
/// 返回:
|
||||||
|
/// - 测试结果映射,键为 "host:port",值为延迟时间
|
||||||
static Future<Map<String, int>> testMultipleNodes({
|
static Future<Map<String, int>> testMultipleNodes({
|
||||||
required List<MapEntry<String, SocketAddress>> nodes,
|
required List<MapEntry<String, SocketAddress>> nodes,
|
||||||
int concurrency = 10, // 并发数
|
int concurrency = 10,
|
||||||
Duration timeout = const Duration(seconds: 5),
|
int timeout = 5000,
|
||||||
}) async {
|
}) async {
|
||||||
final results = <String, int>{};
|
final results = <String, int>{};
|
||||||
final List<List<MapEntry<String, SocketAddress>>> batches = [];
|
final semaphore = Completer<void>();
|
||||||
|
var activeCount = 0;
|
||||||
|
var completedCount = 0;
|
||||||
|
|
||||||
// 分批处理
|
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)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
KRLogUtil.kr_i('📊 开始批量测试: ${nodes.length} 个节点,分 ${batches.length} 批,每批 $concurrency 个', tag: 'LatencyTester');
|
Future<void> processNode(MapEntry<String, SocketAddress> node) async {
|
||||||
|
try {
|
||||||
|
final host = node.value.address;
|
||||||
|
final port = node.value.port;
|
||||||
|
final key = node.key;
|
||||||
|
|
||||||
int completedCount = 0;
|
final latency = await testNode(
|
||||||
|
host: host,
|
||||||
// 逐批测试
|
port: port,
|
||||||
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,
|
timeout: timeout,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
results[key] = latency;
|
||||||
|
} catch (e) {
|
||||||
|
results[node.key] = 65535;
|
||||||
|
KRLogUtil.kr_e('❌ 节点测试异常: ${node.key} - $e', tag: 'KRLatencyTester');
|
||||||
|
} finally {
|
||||||
completedCount++;
|
completedCount++;
|
||||||
|
activeCount--;
|
||||||
|
|
||||||
if (completedCount % 5 == 0 || completedCount == nodes.length) {
|
if (completedCount >= nodes.length) {
|
||||||
KRLogUtil.kr_i('📈 测试进度: $completedCount/${nodes.length}', tag: 'LatencyTester');
|
semaphore.complete();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return MapEntry(tag, latency);
|
// 分批处理节点
|
||||||
}).toList();
|
for (var i = 0; i < nodes.length; i += concurrency) {
|
||||||
|
final batch = nodes.skip(i).take(concurrency);
|
||||||
|
|
||||||
|
for (final node in batch) {
|
||||||
|
activeCount++;
|
||||||
|
processNode(node);
|
||||||
|
}
|
||||||
|
|
||||||
// 等待当前批次完成
|
// 等待当前批次完成
|
||||||
final batchResults = await Future.wait(futures);
|
if (i + concurrency < nodes.length) {
|
||||||
|
await Future.delayed(Duration(milliseconds: 100));
|
||||||
// 收集结果
|
|
||||||
for (final result in batchResults) {
|
|
||||||
results[result.key] = result.value;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 统计结果
|
// 等待所有任务完成
|
||||||
final successCount = results.values.where((latency) => latency < 65535).length;
|
await semaphore.future;
|
||||||
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;
|
return results;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 节点地址信息
|
/// Socket 地址类
|
||||||
|
/// 表示网络地址和端口的组合
|
||||||
class SocketAddress {
|
class SocketAddress {
|
||||||
final String host;
|
final String address;
|
||||||
final int port;
|
final int port;
|
||||||
|
|
||||||
SocketAddress(this.host, this.port);
|
SocketAddress(this.address, this.port);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => '$host:$port';
|
String toString() => '$address:$port';
|
||||||
}
|
}
|
||||||
@@ -45,7 +45,7 @@ class KRWindowManager with WindowListener, TrayListener {
|
|||||||
// 确保在 Windows 下正确设置窗口属性
|
// 确保在 Windows 下正确设置窗口属性
|
||||||
if (Platform.isWindows) {
|
if (Platform.isWindows) {
|
||||||
await windowManager.setTitleBarStyle(TitleBarStyle.normal);
|
await windowManager.setTitleBarStyle(TitleBarStyle.normal);
|
||||||
await windowManager.setTitle('Hi快VPN');
|
await windowManager.setTitle('HiFastVPN');
|
||||||
await windowManager.setSize(const Size(800, 668));
|
await windowManager.setSize(const Size(800, 668));
|
||||||
await windowManager.setMinimumSize(const Size(800, 668));
|
await windowManager.setMinimumSize(const Size(800, 668));
|
||||||
await windowManager.center();
|
await windowManager.center();
|
||||||
|
|||||||
@@ -1,439 +0,0 @@
|
|||||||
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.1(sing-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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
# Flutter Windows 单文件 EXE 打包指南
|
||||||
|
|
||||||
|
## 🎯 目标
|
||||||
|
将 Flutter Windows 应用打包成单个可执行文件,便于分发和部署。
|
||||||
|
|
||||||
|
## 📋 当前状态
|
||||||
|
|
||||||
|
### 现有构建输出
|
||||||
|
```
|
||||||
|
build/windows/x64/runner/Release/
|
||||||
|
├── hostexecutor.exe # 主程序
|
||||||
|
├── flutter_windows.dll # Flutter 引擎
|
||||||
|
├── msvcp140.dll # Visual C++ 运行时
|
||||||
|
├── vcruntime140.dll # Visual C++ 运行时
|
||||||
|
├── vcruntime140_1.dll # Visual C++ 运行时
|
||||||
|
└── data/ # 应用数据文件夹
|
||||||
|
├── app.so # Dart 代码编译结果
|
||||||
|
└── flutter_assets/ # 资源文件
|
||||||
|
```
|
||||||
|
|
||||||
|
### 问题分析
|
||||||
|
Flutter 默认构建会生成多个文件,因为:
|
||||||
|
1. **Flutter 引擎** (`flutter_windows.dll`) - 必须包含
|
||||||
|
2. **Visual C++ 运行时** - 系统依赖
|
||||||
|
3. **应用数据** (`data` 文件夹) - 包含资源和 Dart 代码
|
||||||
|
|
||||||
|
## 🔧 解决方案
|
||||||
|
|
||||||
|
### 方案一:使用 Enigma Virtual Box(推荐)
|
||||||
|
|
||||||
|
#### 步骤 1:下载安装
|
||||||
|
1. 下载 [Enigma Virtual Box](https://enigmaprotector.com/en/downloads.html)
|
||||||
|
2. 安装并运行
|
||||||
|
|
||||||
|
#### 步骤 2:打包配置
|
||||||
|
1. **主程序文件**: 选择 `build/windows/x64/runner/Release/hostexecutor.exe`
|
||||||
|
2. **添加文件夹**: 选择整个 `Release` 文件夹
|
||||||
|
3. **输出文件**: 设置输出路径和文件名
|
||||||
|
4. **文件选项**: 勾选"压缩文件"
|
||||||
|
|
||||||
|
#### 步骤 3:生成单文件
|
||||||
|
点击"Process"生成单个可执行文件。
|
||||||
|
|
||||||
|
### 方案二:使用 Inno Setup(安装程序)
|
||||||
|
|
||||||
|
#### 步骤 1:下载安装
|
||||||
|
1. 下载 [Inno Setup](https://jrsoftware.org/isinfo.php)
|
||||||
|
2. 安装并运行
|
||||||
|
|
||||||
|
#### 步骤 2:创建脚本
|
||||||
|
```ini
|
||||||
|
[Setup]
|
||||||
|
AppName=HostExecutor
|
||||||
|
AppVersion=1.0.0
|
||||||
|
DefaultDirName={autopf}\HostExecutor
|
||||||
|
OutputBaseFilename=HostExecutor_Setup
|
||||||
|
|
||||||
|
[Files]
|
||||||
|
Source: "build\windows\x64\runner\Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs
|
||||||
|
|
||||||
|
[Icons]
|
||||||
|
Name: "{group}\HostExecutor"; Filename: "{app}\hostexecutor.exe"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 步骤 3:编译生成
|
||||||
|
编译脚本生成安装程序。
|
||||||
|
|
||||||
|
### 方案三:使用 Windows 自带工具(高级)
|
||||||
|
|
||||||
|
#### 使用 `dotnet publish`(需要 .NET 包装)
|
||||||
|
```bash
|
||||||
|
# 需要创建 .NET 包装器项目
|
||||||
|
dotnet publish -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 自动化脚本
|
||||||
|
|
||||||
|
### Enigma Virtual Box 自动化脚本
|
||||||
|
```powershell
|
||||||
|
# package_single_exe.ps1
|
||||||
|
$enigmaPath = "C:\Program Files\Enigma Virtual Box\enigmavb.exe"
|
||||||
|
$inputFile = "build\windows\x64\runner\Release\hostexecutor.exe"
|
||||||
|
$outputFile = "dist\HostExecutor_Single.exe"
|
||||||
|
$folderPath = "build\windows\x64\runner\Release"
|
||||||
|
|
||||||
|
& $enigmaPath /sf $inputFile /lf $outputFile /folder $folderPath /compress
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📦 打包后文件结构
|
||||||
|
|
||||||
|
### 单文件输出
|
||||||
|
```
|
||||||
|
dist/
|
||||||
|
└── HostExecutor_Single.exe # 单个可执行文件(50-100MB)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 文件大小分析
|
||||||
|
- **原始文件**: 约 30-50MB
|
||||||
|
- **压缩后**: 约 25-40MB(取决于压缩率)
|
||||||
|
- **最终大小**: 50-100MB(包含所有依赖)
|
||||||
|
|
||||||
|
## ⚠️ 注意事项
|
||||||
|
|
||||||
|
### 运行时依赖
|
||||||
|
1. **Windows 版本**: Windows 10 版本 1903+ 或 Windows 11
|
||||||
|
2. **系统组件**: 确保目标系统有最新的 Windows 更新
|
||||||
|
3. **防病毒软件**: 单文件可能被误报,需要添加信任
|
||||||
|
|
||||||
|
### 性能影响
|
||||||
|
- **启动时间**: 单文件启动会稍慢(需要解压)
|
||||||
|
- **内存使用**: 运行时内存占用相同
|
||||||
|
- **文件大小**: 比多文件版本大约 10-20%
|
||||||
|
|
||||||
|
## 🎯 推荐方案
|
||||||
|
|
||||||
|
### 开发阶段
|
||||||
|
使用多文件版本,便于调试和更新。
|
||||||
|
|
||||||
|
### 分发阶段
|
||||||
|
使用 Enigma Virtual Box 创建单文件版本:
|
||||||
|
1. **简单易用** - 图形化界面
|
||||||
|
2. **压缩率高** - 有效减小文件大小
|
||||||
|
3. **兼容性好** - 支持所有 Windows 版本
|
||||||
|
4. **免费** - 个人和商业使用都免费
|
||||||
|
|
||||||
|
## 📋 下一步行动
|
||||||
|
|
||||||
|
1. **安装 Enigma Virtual Box**
|
||||||
|
2. **测试单文件打包**
|
||||||
|
3. **验证运行效果**
|
||||||
|
4. **创建自动化打包流程**
|
||||||
|
|
||||||
|
## 🔗 相关资源
|
||||||
|
|
||||||
|
- [Enigma Virtual Box 官网](https://enigmaprotector.com/en/downloads.html)
|
||||||
|
- [Inno Setup 官网](https://jrsoftware.org/isinfo.php)
|
||||||
|
- [Flutter Windows 文档](https://docs.flutter.dev/desktop)
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
#Requires -RunAsAdministrator
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Builds and packages a Flutter Windows application into a single executable.
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
This script automates the entire process of creating a single-file executable for a Flutter Windows project.
|
||||||
|
It performs the following steps:
|
||||||
|
1. Checks for and installs Chocolatey if not present.
|
||||||
|
2. Installs Enigma Virtual Box and 7-Zip using Chocolatey.
|
||||||
|
3. Builds the Flutter application in release mode.
|
||||||
|
4. Packages the build output into a single EXE using Enigma Virtual Box.
|
||||||
|
5. If Enigma fails, it falls back to creating a 7-Zip self-extracting archive.
|
||||||
|
6. Places the final packaged executable in the 'dist' directory.
|
||||||
|
|
||||||
|
.NOTES
|
||||||
|
- This script must be run with Administrator privileges to install software.
|
||||||
|
- An internet connection is required for the first run to download and install dependencies.
|
||||||
|
#>
|
||||||
|
|
||||||
|
# --- Configuration ---
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$buildPath = ".\build\windows\x64\runner\Release"
|
||||||
|
$outputPath = ".\dist"
|
||||||
|
|
||||||
|
# --- Helper Functions ---
|
||||||
|
function Test-CommandExists {
|
||||||
|
param($command)
|
||||||
|
return (Get-Command $command -ErrorAction SilentlyContinue)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Install-Chocolatey {
|
||||||
|
if (Test-CommandExists "choco") {
|
||||||
|
Write-Host "✅ Chocolatey is already installed."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Chocolatey not found. Installing..."
|
||||||
|
try {
|
||||||
|
Set-ExecutionPolicy Bypass -Scope Process -Force
|
||||||
|
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072
|
||||||
|
iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
|
||||||
|
Write-Host "✅ Chocolatey installed successfully."
|
||||||
|
} catch {
|
||||||
|
Write-Host "❌ Failed to install Chocolatey. Please install it manually and re-run the script."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Install-Tools {
|
||||||
|
Write-Host "Checking for required tools (Enigma Virtual Box, 7-Zip)..."
|
||||||
|
$tools = @("enigma-virtual-box", "7zip")
|
||||||
|
foreach ($tool in $tools) {
|
||||||
|
if (choco list --local-only --exact $tool) {
|
||||||
|
Write-Host "✅ $tool is already installed."
|
||||||
|
} else {
|
||||||
|
Write-Host "Installing $tool..."
|
||||||
|
try {
|
||||||
|
choco install $tool -y --force
|
||||||
|
Write-Host "✅ $tool installed successfully."
|
||||||
|
} catch {
|
||||||
|
Write-Host "❌ Failed to install $tool."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Main Script ---
|
||||||
|
|
||||||
|
# 1. Setup Environment
|
||||||
|
Write-Host "=== 1/4: Setting up build environment ==="
|
||||||
|
Install-Chocolatey
|
||||||
|
Install-Tools
|
||||||
|
|
||||||
|
# 2. Build Flutter App
|
||||||
|
Write-Host "=== 2/4: Building Flutter Windows application (Release) ==="
|
||||||
|
try {
|
||||||
|
flutter build windows --release
|
||||||
|
} catch {
|
||||||
|
Write-Host "❌ Flutter build failed."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# 3. Package Application
|
||||||
|
Write-Host "=== 3/4: Packaging into a single executable ==="
|
||||||
|
if (-not (Test-Path $buildPath)) {
|
||||||
|
Write-Host "❌ Build directory not found: $buildPath"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create output directory
|
||||||
|
New-Item -ItemType Directory -Path $outputPath -Force | Out-Null
|
||||||
|
|
||||||
|
# Get main executable
|
||||||
|
$exeFile = Get-ChildItem -Path $buildPath -Filter "*.exe" | Select-Object -First 1
|
||||||
|
if (-not $exeFile) {
|
||||||
|
Write-Host "❌ No executable found in build directory."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
$inputExe = $exeFile.FullName
|
||||||
|
$outputExe = "$outputPath\$($exeFile.BaseName)_Single.exe"
|
||||||
|
$enigmaCliPath = "C:\Program Files\Enigma Virtual Box\enigmavb.exe"
|
||||||
|
$packageSuccess = $false
|
||||||
|
|
||||||
|
# Attempt to package with Enigma Virtual Box
|
||||||
|
if (Test-Path $enigmaCliPath) {
|
||||||
|
Write-Host "Attempting to package with Enigma Virtual Box..."
|
||||||
|
try {
|
||||||
|
& $enigmaCliPath /quiet /project "$outputPath\project.evb" /input "$inputExe" /output "$outputExe" /folder "$buildPath" /compress 3 /deleteext
|
||||||
|
|
||||||
|
if ($?) {
|
||||||
|
Write-Host "✅ Enigma Virtual Box packaging successful."
|
||||||
|
$packageSuccess = $true
|
||||||
|
} else {
|
||||||
|
Write-Host "⚠️ Enigma Virtual Box packaging failed. Exit code: $lastexitcode"
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Write-Host "⚠️ An error occurred during Enigma Virtual Box packaging."
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Host "⚠️ Enigma Virtual Box not found at $enigmaCliPath."
|
||||||
|
}
|
||||||
|
|
||||||
|
# 4. Fallback to 7-Zip if Enigma failed
|
||||||
|
if (-not $packageSuccess) {
|
||||||
|
Write-Host "=== 4/4: Fallback: Packaging with 7-Zip SFX ==="
|
||||||
|
$7zipCliPath = "C:\Program Files\7-Zip\7z.exe"
|
||||||
|
$outputSfx = "$outputPath\$($exeFile.BaseName)_Package.exe"
|
||||||
|
|
||||||
|
if (-not (Test-Path $7zipCliPath)) {
|
||||||
|
Write-Host "❌ 7-Zip not found. Cannot create self-extracting archive."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
$sfxConfig = @'
|
||||||
|
;!@Install@!UTF-8!
|
||||||
|
Title="Flutter Application"
|
||||||
|
BeginPrompt="Do you want to extract and run the application?"
|
||||||
|
RunProgram="%%T\%%S\$($exeFile.Name)"
|
||||||
|
;!@InstallEnd@!
|
||||||
|
'@
|
||||||
|
$sfxConfigFile = "$outputPath\sfx_config.txt"
|
||||||
|
Set-Content -Path $sfxConfigFile -Value $sfxConfig
|
||||||
|
|
||||||
|
try {
|
||||||
|
& $7zipCliPath a -sfx7z.sfx "$outputSfx" "$buildPath\*" -r "-i!$sfxConfigFile"
|
||||||
|
Write-Host "✅ 7-Zip self-extracting archive created successfully."
|
||||||
|
$outputExe = $outputSfx
|
||||||
|
$packageSuccess = $true
|
||||||
|
} catch {
|
||||||
|
Write-Host "❌ 7-Zip packaging failed."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Final Summary ---
|
||||||
|
if ($packageSuccess) {
|
||||||
|
$finalSize = (Get-Item $outputExe).Length / 1MB
|
||||||
|
Write-Host "================================================"
|
||||||
|
Write-Host "✅ Packaging Complete!"
|
||||||
|
Write-Host " Output file: $outputExe"
|
||||||
|
Write-Host " Size: $([math]::Round($finalSize, 2)) MB"
|
||||||
|
Write-Host "================================================"
|
||||||
|
} else {
|
||||||
|
Write-Host "❌ All packaging attempts failed."
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
# Flutter Windows 单文件打包脚本 - Enigma Virtual Box
|
||||||
|
# 以管理员身份运行
|
||||||
|
|
||||||
|
Write-Host "=== Flutter Windows 单文件打包工具 ===" -ForegroundColor Green
|
||||||
|
|
||||||
|
# 配置参数
|
||||||
|
$projectRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||||
|
$buildPath = "$projectRoot\build\windows\x64\runner\Release"
|
||||||
|
$outputPath = "$projectRoot\dist"
|
||||||
|
$enigmaPath = "C:\Program Files\Enigma Virtual Box\enigmavb.exe"
|
||||||
|
|
||||||
|
# 检查构建文件
|
||||||
|
Write-Host "`n检查构建文件..." -ForegroundColor Yellow
|
||||||
|
if (-not (Test-Path $buildPath)) {
|
||||||
|
Write-Host "错误: 构建文件不存在,请先运行 flutter build windows" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# 创建输出目录
|
||||||
|
if (-not (Test-Path $outputPath)) {
|
||||||
|
New-Item -ItemType Directory -Path $outputPath -Force | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
# 检查 Enigma Virtual Box
|
||||||
|
if (-not (Test-Path $enigmaPath)) {
|
||||||
|
Write-Host "错误: Enigma Virtual Box 未安装" -ForegroundColor Red
|
||||||
|
Write-Host "请下载安装: https://enigmaprotector.com/en/downloads.html" -ForegroundColor Yellow
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# 获取主程序名称
|
||||||
|
$exeFile = Get-ChildItem -Path $buildPath -Filter "*.exe" | Select-Object -First 1
|
||||||
|
if (-not $exeFile) {
|
||||||
|
Write-Host "错误: 未找到可执行文件" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
$inputExe = $exeFile.FullName
|
||||||
|
$outputExe = "$outputPath\$($exeFile.BaseName)_Single.exe"
|
||||||
|
|
||||||
|
Write-Host "主程序: $inputExe" -ForegroundColor Cyan
|
||||||
|
Write-Host "输出文件: $outputExe" -ForegroundColor Cyan
|
||||||
|
|
||||||
|
# 创建打包配置文件
|
||||||
|
$configContent = @"
|
||||||
|
; Enigma Virtual Box 配置文件
|
||||||
|
[Config]
|
||||||
|
InputFile=$inputExe
|
||||||
|
OutputFile=$outputExe
|
||||||
|
Files=%DEFAULT FOLDER%
|
||||||
|
VirtualizationMode=Never Write To Disk
|
||||||
|
Compression=Yes
|
||||||
|
ShareVirtualSystem=Yes
|
||||||
|
|
||||||
|
[Files]
|
||||||
|
; 添加整个 Release 文件夹
|
||||||
|
Folder=$buildPath\*
|
||||||
|
"@
|
||||||
|
|
||||||
|
$configFile = "$outputPath\package_config.evb"
|
||||||
|
Set-Content -Path $configFile -Value $configContent
|
||||||
|
|
||||||
|
# 执行打包
|
||||||
|
Write-Host "`n开始打包..." -ForegroundColor Yellow
|
||||||
|
Write-Host "这可能需要几分钟时间,请耐心等待..." -ForegroundColor Yellow
|
||||||
|
|
||||||
|
$process = Start-Process -FilePath $enigmaPath -ArgumentList "/sf", $inputExe, "/lf", $outputExe, "/folder", $buildPath, "/compress" -Wait -PassThru
|
||||||
|
|
||||||
|
if ($process.ExitCode -eq 0) {
|
||||||
|
Write-Host "`n✅ 打包成功!" -ForegroundColor Green
|
||||||
|
|
||||||
|
# 显示结果信息
|
||||||
|
$originalSize = (Get-Item $inputExe).Length / 1MB
|
||||||
|
$packedSize = (Get-Item $outputExe).Length / 1MB
|
||||||
|
$compressionRatio = [math]::Round((1 - $packedSize / $originalSize) * 100, 2)
|
||||||
|
|
||||||
|
Write-Host "`n打包结果:" -ForegroundColor Cyan
|
||||||
|
Write-Host "原始大小: $([math]::Round($originalSize, 2)) MB" -ForegroundColor White
|
||||||
|
Write-Host "打包大小: $([math]::Round($packedSize, 2)) MB" -ForegroundColor White
|
||||||
|
Write-Host "压缩率: $compressionRatio%" -ForegroundColor White
|
||||||
|
Write-Host "输出文件: $outputExe" -ForegroundColor White
|
||||||
|
|
||||||
|
Write-Host "`n📋 使用说明:" -ForegroundColor Yellow
|
||||||
|
Write-Host "1. 将生成的单文件复制到目标计算机" -ForegroundColor White
|
||||||
|
Write-Host "2. 直接运行即可,无需安装其他依赖" -ForegroundColor White
|
||||||
|
Write-Host "3. 首次运行可能需要管理员权限" -ForegroundColor White
|
||||||
|
|
||||||
|
} else {
|
||||||
|
Write-Host "`n❌ 打包失败!" -ForegroundColor Red
|
||||||
|
Write-Host "错误代码: $($process.ExitCode)" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
|
||||||
|
# 清理临时文件
|
||||||
|
Remove-Item $configFile -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
Write-Host "`n按任意键退出..." -ForegroundColor Gray
|
||||||
|
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
|
||||||
@@ -100,7 +100,7 @@ endif()
|
|||||||
|
|
||||||
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
|
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
|
||||||
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}")
|
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}")
|
||||||
# CLI 工具目录(用于存放 BearVPNCli.exe)
|
# CLI 工具目录(用于存放 HiFastVPNCli.exe)
|
||||||
set(INSTALL_BUNDLE_CLI_DIR "${CMAKE_INSTALL_PREFIX}/cli")
|
set(INSTALL_BUNDLE_CLI_DIR "${CMAKE_INSTALL_PREFIX}/cli")
|
||||||
|
|
||||||
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
|
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
|
||||||
@@ -118,10 +118,10 @@ install(FILES "../libcore/bin/libcore.dll"
|
|||||||
COMPONENT Runtime
|
COMPONENT Runtime
|
||||||
OPTIONAL)
|
OPTIONAL)
|
||||||
|
|
||||||
# 安装 BearVPNCli.exe(从 libcore/bin 复制并重命名)
|
# 安装 HiFastVPNCli.exe(从 libcore/bin 复制并重命名)
|
||||||
# 注意:libcore 编译的是 HiddifyCli.exe,打包脚本会自动重命名为 BearVPNCli.exe
|
# 注意:libcore 编译的是 HiddifyCli.exe,打包脚本会自动重命名为 HiFastVPNCli.exe
|
||||||
# 这里需要安装 BearVPNCli.exe,因为它已经被重命名了
|
# 这里需要安装 HiFastVPNCli.exe,因为它已经被重命名了
|
||||||
install(FILES "../libcore/bin/BearVPNCli.exe"
|
install(FILES "../libcore/bin/HiFastVPNCli.exe"
|
||||||
DESTINATION "${CMAKE_INSTALL_PREFIX}"
|
DESTINATION "${CMAKE_INSTALL_PREFIX}"
|
||||||
COMPONENT Runtime
|
COMPONENT Runtime
|
||||||
OPTIONAL)
|
OPTIONAL)
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ echo 正在复制必要的 DLL 文件...
|
|||||||
copy "%VC_REDIST_DIR%\msvcp140.dll" "%DLL_DIR%"
|
copy "%VC_REDIST_DIR%\msvcp140.dll" "%DLL_DIR%"
|
||||||
copy "%VC_REDIST_DIR%\vcruntime140.dll" "%DLL_DIR%"
|
copy "%VC_REDIST_DIR%\vcruntime140.dll" "%DLL_DIR%"
|
||||||
copy "%VC_REDIST_DIR%\vcruntime140_1.dll" "%DLL_DIR%"
|
copy "%VC_REDIST_DIR%\vcruntime140_1.dll" "%DLL_DIR%"
|
||||||
copy "%~dp0..\libcore\libcore.dll" "%DLL_DIR%"
|
|
||||||
|
|
||||||
echo DLL 文件复制完成!
|
echo DLL 文件复制完成!
|
||||||
pause
|
pause
|
||||||
@@ -49,7 +49,7 @@ CloseApplications=force
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
[Tasks]
|
[Tasks]
|
||||||
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: {% if CREATE_DESKTOP_ICON != true %}unchecked{% else %}checkedonce{% endif %}
|
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: {% if LAUNCH_AT_STARTUP != true %}unchecked{% else %}checkedonce{% endif %}
|
||||||
[Files]
|
[Files]
|
||||||
Source: "{{SOURCE_DIR}}\\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
Source: "{{SOURCE_DIR}}\\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||||
@@ -62,14 +62,82 @@ Name: "{userstartup}\\{{DISPLAY_NAME}}"; Filename: "{app}\\{{EXECUTABLE_NAME}}";
|
|||||||
[Run]
|
[Run]
|
||||||
Filename: "{app}\\{{EXECUTABLE_NAME}}"; Description: "{cm:LaunchProgram,{{DISPLAY_NAME}}}"; Flags: {% if PRIVILEGES_REQUIRED == 'admin' %}runascurrentuser{% endif %} nowait postinstall skipifsilent
|
Filename: "{app}\\{{EXECUTABLE_NAME}}"; Description: "{cm:LaunchProgram,{{DISPLAY_NAME}}}"; Flags: {% if PRIVILEGES_REQUIRED == 'admin' %}runascurrentuser{% endif %} nowait postinstall skipifsilent
|
||||||
|
|
||||||
|
[UninstallDelete]
|
||||||
|
Type: filesandordirs; Name: "{app}"
|
||||||
|
Type: filesandordirs; Name: "{localappdata}\\{{DISPLAY_NAME}}"
|
||||||
|
Type: filesandordirs; Name: "{userappdata}\\{{DISPLAY_NAME}}"
|
||||||
|
Type: filesandordirs; Name: "{tmp}\\{{DISPLAY_NAME}}"
|
||||||
|
|
||||||
|
[Registry]
|
||||||
|
Root: HKCU; Subkey: "Software\\{{DISPLAY_NAME}}"; Flags: uninsdeletekey
|
||||||
|
Root: HKLM; Subkey: "Software\\{{DISPLAY_NAME}}"; Flags: uninsdeletekey
|
||||||
|
|
||||||
[Code]
|
[Code]
|
||||||
function InitializeSetup(): Boolean;
|
procedure AppendLog(S: string);
|
||||||
var
|
|
||||||
ResultCode: Integer;
|
|
||||||
begin
|
begin
|
||||||
Exec('taskkill', '/F /IM HiFastVPN.exe', '', SW_HIDE, ewWaitUntilTerminated, ResultCode)
|
SaveStringToFile(ExpandConstant('{tmp}\\{{DISPLAY_NAME}}_installer.log'), S + #13#10, True);
|
||||||
Exec('net', 'stop "HiFastVPNTunnelService"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode)
|
end;
|
||||||
Exec('sc.exe', 'delete "HiFastVPNTunnelService"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode)
|
|
||||||
|
procedure TerminateProcesses();
|
||||||
|
var ResultCode: Integer;
|
||||||
|
begin
|
||||||
|
try
|
||||||
|
Exec('taskkill', '/F /IM {{EXECUTABLE_NAME}}', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
|
||||||
|
except
|
||||||
|
AppendLog('terminate failed: {{EXECUTABLE_NAME}}');
|
||||||
|
end;
|
||||||
|
try
|
||||||
|
Exec('taskkill', '/F /IM HiFastVPNCli.exe', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
|
||||||
|
except
|
||||||
|
AppendLog('terminate failed: HiFastVPNCli.exe');
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure StopServices();
|
||||||
|
var ResultCode: Integer;
|
||||||
|
begin
|
||||||
|
try
|
||||||
|
Exec('net', 'stop "HiFastVPNTunnelService"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
|
||||||
|
Exec('sc.exe', 'delete "HiFastVPNTunnelService"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
|
||||||
|
except
|
||||||
|
AppendLog('service stop/delete failed: HiFastVPNTunnelService');
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure CleanPaths();
|
||||||
|
var ok: Boolean;
|
||||||
|
begin
|
||||||
|
try
|
||||||
|
if DirExists(ExpandConstant('{app}')) then
|
||||||
|
ok := DelTree(ExpandConstant('{app}'), True, True, True)
|
||||||
|
else ok := True;
|
||||||
|
if not ok then AppendLog('delete failed: {app}');
|
||||||
|
except
|
||||||
|
AppendLog('exception deleting: {app}');
|
||||||
|
end;
|
||||||
|
try
|
||||||
|
if DirExists(ExpandConstant('{localappdata}\\{{DISPLAY_NAME}}')) then
|
||||||
|
ok := DelTree(ExpandConstant('{localappdata}\\{{DISPLAY_NAME}}'), True, True, True);
|
||||||
|
if DirExists(ExpandConstant('{userappdata}\\{{DISPLAY_NAME}}')) then
|
||||||
|
ok := DelTree(ExpandConstant('{userappdata}\\{{DISPLAY_NAME}}'), True, True, True);
|
||||||
|
if DirExists(ExpandConstant('{tmp}\\{{DISPLAY_NAME}}')) then
|
||||||
|
ok := DelTree(ExpandConstant('{tmp}\\{{DISPLAY_NAME}}'), True, True, True);
|
||||||
|
except
|
||||||
|
AppendLog('exception deleting user data');
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function InitializeSetup(): Boolean;
|
||||||
|
begin
|
||||||
|
TerminateProcesses();
|
||||||
|
StopServices();
|
||||||
|
Result := True;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function InitializeUninstall(): Boolean;
|
||||||
|
begin
|
||||||
|
TerminateProcesses();
|
||||||
|
StopServices();
|
||||||
|
CleanPaths();
|
||||||
Result := True;
|
Result := True;
|
||||||
end;
|
end;
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
app_id: 6L903538-42B1-4596-G479-BJ779F21A65E
|
app_id: 6L903538-42B1-4596-G479-BJ779F21A65E
|
||||||
publisher: HiFastVPN
|
publisher: HiFastVPN
|
||||||
publisher_url: https://github.com/hiddify/hiddify-next
|
publisher_url: https://github.com/hiddify/hiddify-next
|
||||||
display_name: Hi快VPN
|
display_name: HiFastVPN
|
||||||
executable_name: HiFastVPN.exe
|
executable_name: HiFastVPN.exe
|
||||||
output_base_file_name: HiFastVPN.exe
|
output_base_file_name: HiFastVPN.exe
|
||||||
create_desktop_icon: true
|
create_desktop_icon: true
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ BEGIN
|
|||||||
VALUE "CompanyName", "HiFastVPN" "\0"
|
VALUE "CompanyName", "HiFastVPN" "\0"
|
||||||
VALUE "FileDescription", "HiFastVPN" "\0"
|
VALUE "FileDescription", "HiFastVPN" "\0"
|
||||||
VALUE "FileVersion", VERSION_AS_STRING "\0"
|
VALUE "FileVersion", VERSION_AS_STRING "\0"
|
||||||
VALUE "InternalName", "HiFastVPN" "\0"
|
VALUE "InternalName", "app.baer.com" "\0"
|
||||||
VALUE "LegalCopyright", "Copyright (C) 2024 HiFastVPN. All rights reserved." "\0"
|
VALUE "LegalCopyright", "Copyright (C) 2024 HiFastVPN. All rights reserved." "\0"
|
||||||
VALUE "OriginalFilename", "HiFastVPN.exe" "\0"
|
VALUE "OriginalFilename", "HiFastVPN.exe" "\0"
|
||||||
VALUE "ProductName", "HiFastVPN" "\0"
|
VALUE "ProductName", "HiFastVPN" "\0"
|
||||||
|
|||||||
Regular → Executable
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 109 KiB |
+3
-3
@@ -1,7 +1,7 @@
|
|||||||
#define MyAppName "Hi快VPN"
|
#define MyAppName "Kaer VPN"
|
||||||
#define MyAppVersion "1.0.0"
|
#define MyAppVersion "1.0.0"
|
||||||
#define MyAppPublisher "HiFastVPN"
|
#define MyAppPublisher "Kaer"
|
||||||
#define MyAppExeName "HiFastVPN.exe"
|
#define MyAppExeName "kaer_vpn.exe"
|
||||||
|
|
||||||
[Setup]
|
[Setup]
|
||||||
AppId={{YOUR-APP-ID-HERE}
|
AppId={{YOUR-APP-ID-HERE}
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
# Windows 构建日志分析
|
||||||
|
|
||||||
|
## 🎉 构建结果:成功!
|
||||||
|
|
||||||
|
### ✅ 成功状态
|
||||||
|
- **Debug 构建**: ✓ 成功 (282.5s)
|
||||||
|
- **Release 构建**: ✓ 成功 (27.0s)
|
||||||
|
- **最终状态**: ✅ Job succeeded
|
||||||
|
|
||||||
|
### 📁 构建输出位置
|
||||||
|
```
|
||||||
|
Debug: build\windows\x64\runner\Debug\hostexecutor.exe
|
||||||
|
Release: build\windows\x64\runner\Release\hostexecutor.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ 警告信息分析
|
||||||
|
|
||||||
|
### 1. CMake 警告(可忽略)
|
||||||
|
```
|
||||||
|
CMake Warning (dev) at flutter_inappwebview_windows/windows/CMakeLists.txt:31
|
||||||
|
Policy CMP0175 is not set: add_custom_command() rejects invalid arguments.
|
||||||
|
```
|
||||||
|
- **影响**: 无影响,这是开发者警告
|
||||||
|
- **解决方案**: 添加 `-Wno-dev` 参数可抑制
|
||||||
|
|
||||||
|
### 2. WebView2 编译警告(可忽略)
|
||||||
|
```
|
||||||
|
warning C4244: conversion from '__int64' to 'int', possible loss of data
|
||||||
|
warning C4458: declaration of 'value' hides class member
|
||||||
|
```
|
||||||
|
- **影响**: 无功能影响,只是类型转换警告
|
||||||
|
- **状态**: 正常现象,不影响使用
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📤 产物上传问题
|
||||||
|
|
||||||
|
### 问题描述
|
||||||
|
```
|
||||||
|
::warning::No files were found with the provided path: build/windows/runner/Debug/
|
||||||
|
::warning::No files were found with the provided path: build/windows/runner/Release/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 根本原因
|
||||||
|
**路径配置错误**:workflow 配置的路径 `build/windows/runner/Debug/` 与实际构建输出路径 `build/windows/x64/runner/Debug/` 不匹配。
|
||||||
|
|
||||||
|
### 🔧 解决方案
|
||||||
|
|
||||||
|
需要更新 `.gitea/workflows/docker.yml` 中的上传路径:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# 原配置(错误)
|
||||||
|
- name: Upload Debug build artifacts
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: windows-debug-build
|
||||||
|
path: build/windows/runner/Debug/ # ❌ 错误路径
|
||||||
|
|
||||||
|
# 正确配置
|
||||||
|
- name: Upload Debug build artifacts
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: windows-debug-build
|
||||||
|
path: build/windows/x64/runner/Debug/ # ✅ 正确路径
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏗️ 构建过程总结
|
||||||
|
|
||||||
|
### 成功的步骤
|
||||||
|
1. ✅ NuGet 安装成功(通过 Chocolatey)
|
||||||
|
2. ✅ Flutter 环境配置正确
|
||||||
|
3. ✅ Windows 桌面支持启用
|
||||||
|
4. ✅ 依赖获取成功
|
||||||
|
5. ✅ 代码生成成功
|
||||||
|
6. ✅ Windows Debug 构建成功
|
||||||
|
7. ✅ Windows Release 构建成功
|
||||||
|
|
||||||
|
### 耗时分析
|
||||||
|
- **Debug 构建**: 282.5秒(约4.7分钟)
|
||||||
|
- **Release 构建**: 27秒(优化后更快)
|
||||||
|
- **总耗时**: 约5分钟
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 下一步行动
|
||||||
|
|
||||||
|
### 立即修复
|
||||||
|
1. **修复上传路径** - 更新 workflow 中的产物路径
|
||||||
|
2. **验证可执行文件** - 确认 `hostexecutor.exe` 可正常运行
|
||||||
|
|
||||||
|
### 可选优化
|
||||||
|
1. **缓存优化** - 添加 Flutter 和依赖缓存
|
||||||
|
2. **并行构建** - Debug 和 Release 可并行执行
|
||||||
|
3. **压缩产物** - 减少上传文件大小
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 构建环境信息
|
||||||
|
|
||||||
|
### 软件版本
|
||||||
|
- **Flutter**: 3.24.5
|
||||||
|
- **Visual Studio**: 2022 Enterprise
|
||||||
|
- **NuGet**: 6.14.0 (通过 Chocolatey)
|
||||||
|
- **CMake**: 最新版本
|
||||||
|
|
||||||
|
### 系统环境
|
||||||
|
- **操作系统**: Windows Server
|
||||||
|
- **架构**: x64
|
||||||
|
- **构建工具**: MSBuild
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 结论
|
||||||
|
|
||||||
|
**构建完全成功!** 主要的可执行文件已经生成,只是上传配置需要微调。Windows 应用构建流程已经稳定运行。
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
# 工作流自动打包单文件 EXE 说明
|
||||||
|
|
||||||
|
## 🎯 功能概述
|
||||||
|
|
||||||
|
现在工作流会自动完成以下步骤:
|
||||||
|
1. ✅ 构建 Windows 应用(Debug 和 Release)
|
||||||
|
2. ✅ 自动下载打包工具
|
||||||
|
3. ✅ 创建单文件 EXE
|
||||||
|
4. ✅ 上传打包结果
|
||||||
|
|
||||||
|
## 🚀 工作流程
|
||||||
|
|
||||||
|
### 步骤 1: Enigma Virtual Box 打包(首选方案)
|
||||||
|
- **工具**: 自动下载 Enigma Virtual Box
|
||||||
|
- **输出**: `hostexecutor_single.exe`
|
||||||
|
- **特点**: 真正的单文件,高压缩率
|
||||||
|
|
||||||
|
### 步骤 2: 7-Zip 自解压方案(备选方案)
|
||||||
|
- **工具**: 使用系统自带的 7-Zip
|
||||||
|
- **输出**: `hostexecutor_package.exe`
|
||||||
|
- **特点**: 自解压安装包,兼容性更好
|
||||||
|
|
||||||
|
### 步骤 3: 产物上传
|
||||||
|
- **单文件 EXE**: `windows-single-exe` 工件
|
||||||
|
- **原始文件**: `windows-release-build` 工件
|
||||||
|
|
||||||
|
## 📦 打包产物
|
||||||
|
|
||||||
|
### 成功时你会得到:
|
||||||
|
```
|
||||||
|
工件列表:
|
||||||
|
├── windows-debug-build # Debug 版本(多文件)
|
||||||
|
├── windows-release-build # Release 版本(多文件)
|
||||||
|
└── windows-single-exe # 单文件 EXE(自动打包)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 文件结构:
|
||||||
|
```
|
||||||
|
单文件 EXE:
|
||||||
|
└── hostexecutor_single.exe # 50-80MB,直接运行
|
||||||
|
|
||||||
|
原始文件:
|
||||||
|
└── hostexecutor.exe # 主程序
|
||||||
|
├── flutter_windows.dll # Flutter 引擎
|
||||||
|
├── *.dll # 运行时库
|
||||||
|
└── data/ # 应用数据
|
||||||
|
```
|
||||||
|
|
||||||
|
## ⚙️ 技术实现
|
||||||
|
|
||||||
|
### 自动下载 Enigma Virtual Box
|
||||||
|
```powershell
|
||||||
|
$enigmaUrl = "https://enigmaprotector.com/assets/files/enigmavb.exe"
|
||||||
|
Invoke-WebRequest -Uri $enigmaUrl -OutFile "C:\enigmavb.exe"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 智能打包逻辑
|
||||||
|
1. **尝试 Enigma** - 创建真正的单文件
|
||||||
|
2. **失败时回退** - 使用 7-Zip 自解压
|
||||||
|
3. **总是成功** - 确保有输出文件
|
||||||
|
|
||||||
|
### 压缩优化
|
||||||
|
- **压缩级别**: 最高压缩率 (`/compress`)
|
||||||
|
- **虚拟化模式**: 内存运行,不写磁盘
|
||||||
|
- **多线程**: 并行处理,加快速度
|
||||||
|
|
||||||
|
## 📋 使用说明
|
||||||
|
|
||||||
|
### 获取单文件 EXE
|
||||||
|
1. 进入 Gitea Actions 页面
|
||||||
|
2. 找到最新的成功构建
|
||||||
|
3. 下载 `windows-single-exe` 工件
|
||||||
|
4. 直接运行 `hostexecutor_single.exe`
|
||||||
|
|
||||||
|
### 验证打包结果
|
||||||
|
```powershell
|
||||||
|
# 检查文件大小(应该在 50-80MB)
|
||||||
|
dir hostexecutor_single.exe
|
||||||
|
|
||||||
|
# 验证单文件(应该只有 1 个文件)
|
||||||
|
dir *.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 优势特点
|
||||||
|
|
||||||
|
### 完全自动化
|
||||||
|
- ✅ 无需手动操作
|
||||||
|
- ✅ 自动下载工具
|
||||||
|
- ✅ 智能错误处理
|
||||||
|
- ✅ 保证输出结果
|
||||||
|
|
||||||
|
### 高质量打包
|
||||||
|
- ✅ 真正的单文件
|
||||||
|
- ✅ 高压缩率
|
||||||
|
- ✅ 内存运行,不写临时文件
|
||||||
|
- ✅ 兼容所有 Windows 版本
|
||||||
|
|
||||||
|
### 可靠回退
|
||||||
|
- ✅ Enigma 失败时自动切换 7-Zip
|
||||||
|
- ✅ 总是生成可用的输出
|
||||||
|
- ✅ 详细的构建日志
|
||||||
|
|
||||||
|
## 🔧 自定义配置
|
||||||
|
|
||||||
|
### 修改压缩级别
|
||||||
|
在工作流脚本中找到:
|
||||||
|
```powershell
|
||||||
|
# 最高压缩(当前设置)
|
||||||
|
/compress
|
||||||
|
|
||||||
|
# 快速压缩(速度优先)
|
||||||
|
/compress:fast
|
||||||
|
|
||||||
|
# 平衡压缩(推荐)
|
||||||
|
/compress:normal
|
||||||
|
```
|
||||||
|
|
||||||
|
### 修改输出文件名
|
||||||
|
```powershell
|
||||||
|
# 当前设置
|
||||||
|
$outputExe = "$outputPath\$($exeFile.BaseName)_Single.exe"
|
||||||
|
|
||||||
|
# 自定义名称
|
||||||
|
$outputExe = "$outputPath\MyApp_Portable.exe"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 性能指标
|
||||||
|
|
||||||
|
### 构建时间
|
||||||
|
- **Debug 构建**: ~4.7 分钟
|
||||||
|
- **Release 构建**: ~27 秒
|
||||||
|
- **单文件打包**: ~2-5 分钟
|
||||||
|
- **总时间**: ~8-12 分钟
|
||||||
|
|
||||||
|
### 文件大小
|
||||||
|
- **原始文件**: ~70MB(多文件)
|
||||||
|
- **单文件**: ~50-80MB(压缩后)
|
||||||
|
- **压缩率**: 10-30% 减小
|
||||||
|
|
||||||
|
## 🎉 总结
|
||||||
|
|
||||||
|
**现在工作流完全自动化了!**
|
||||||
|
- 🔄 每次推送代码 → 自动构建 → 自动打包单文件
|
||||||
|
- 📦 构建完成后 → 直接下载单文件 EXE 使用
|
||||||
|
- 🔧 无需手动配置 → 开箱即用
|
||||||
|
|
||||||
|
**你只需要**:等待构建完成,下载单文件,直接运行!✨
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
# Windows 构建项目说明文档
|
||||||
|
|
||||||
|
## 🎯 项目概述
|
||||||
|
|
||||||
|
本项目是一个 Flutter Windows 应用程序,已成功配置完整的 Windows 构建流程。
|
||||||
|
|
||||||
|
## ✅ 构建状态
|
||||||
|
|
||||||
|
**当前状态**: ✅ **构建成功**
|
||||||
|
|
||||||
|
- **Debug 构建**: ✓ 成功 (282.5s)
|
||||||
|
- **Release 构建**: ✓ 成功 (27s)
|
||||||
|
- **构建环境**: Windows Server + Visual Studio 2022 Enterprise
|
||||||
|
|
||||||
|
## 🏗️ 构建流程
|
||||||
|
|
||||||
|
### 1. 环境准备
|
||||||
|
- ✅ Flutter 3.24.5 已安装
|
||||||
|
- ✅ Visual Studio 2022 Enterprise 已配置
|
||||||
|
- ✅ NuGet 6.14.0 已安装(通过 Chocolatey)
|
||||||
|
- ✅ Windows 长路径支持已启用
|
||||||
|
|
||||||
|
### 2. 构建步骤
|
||||||
|
1. **代码检出** - 从 Gitea 仓库获取代码
|
||||||
|
2. **依赖安装** - 安装 Flutter 依赖包
|
||||||
|
3. **代码生成** - 运行 build_runner 生成代码
|
||||||
|
4. **Windows 构建** - 构建 Debug 和 Release 版本
|
||||||
|
5. **产物上传** - 上传构建产物到 Gitea Actions
|
||||||
|
|
||||||
|
### 3. 构建输出
|
||||||
|
```
|
||||||
|
Debug: build/windows/x64/runner/Debug/hostexecutor.exe
|
||||||
|
Release: build/windows/x64/runner/Release/hostexecutor.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 关键修复记录
|
||||||
|
|
||||||
|
### 1. NuGet 安装问题
|
||||||
|
**问题**: SSL/TLS 安全通道错误
|
||||||
|
**解决方案**:
|
||||||
|
- 使用 Chocolatey 安装 NuGet
|
||||||
|
- 命令: `choco install nuget.commandline -y`
|
||||||
|
|
||||||
|
### 2. Flutter 路径配置
|
||||||
|
**问题**: Flutter 命令未找到
|
||||||
|
**解决方案**:
|
||||||
|
- 添加 Flutter 到 PATH: `C:\flutter\bin`
|
||||||
|
- 在每个构建步骤中显式设置 PATH
|
||||||
|
|
||||||
|
### 3. 长路径问题
|
||||||
|
**问题**: Windows 路径长度限制
|
||||||
|
**解决方案**:
|
||||||
|
- 启用 Windows 长路径支持
|
||||||
|
- 注册表设置: `LongPathsEnabled = 1`
|
||||||
|
|
||||||
|
### 4. 构建产物路径
|
||||||
|
**问题**: 上传路径配置错误
|
||||||
|
**解决方案**:
|
||||||
|
- 修正路径: `build/windows/x64/runner/Debug/`
|
||||||
|
- 原错误路径: `build/windows/runner/Debug/`
|
||||||
|
|
||||||
|
## 📁 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
/Users/Apple/vpn/hi-client/
|
||||||
|
├── .gitea/workflows/ # Gitea Actions 工作流配置
|
||||||
|
├── lib/ # Flutter 源代码
|
||||||
|
│ ├── app/ # 应用程序代码
|
||||||
|
│ ├── core/ # 核心功能
|
||||||
|
│ └── singbox/ # SingBox 相关
|
||||||
|
├── windows/ # Windows 平台配置
|
||||||
|
├── libcore/ # 核心库(子模块)
|
||||||
|
└── build/ # 构建输出(运行时生成)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 快速开始
|
||||||
|
|
||||||
|
### 本地构建
|
||||||
|
```bash
|
||||||
|
# 安装 Flutter 依赖
|
||||||
|
flutter pub get
|
||||||
|
|
||||||
|
# 生成代码
|
||||||
|
dart run build_runner build --delete-conflicting-outputs
|
||||||
|
|
||||||
|
# 构建 Windows Debug
|
||||||
|
flutter build windows
|
||||||
|
|
||||||
|
# 构建 Windows Release
|
||||||
|
flutter build windows --release
|
||||||
|
```
|
||||||
|
|
||||||
|
### 使用脚本
|
||||||
|
```bash
|
||||||
|
# 运行 Windows 构建修复脚本
|
||||||
|
./fix_windows_build.ps1
|
||||||
|
|
||||||
|
# 安装 NuGet(如果需要)
|
||||||
|
./install_nuget_simple.bat
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📋 注意事项
|
||||||
|
|
||||||
|
### 1. 构建环境要求
|
||||||
|
- Windows 10/11 或 Windows Server
|
||||||
|
- Visual Studio 2022(包含 C++ 开发工具)
|
||||||
|
- Flutter 3.24.5+
|
||||||
|
- NuGet CLI
|
||||||
|
|
||||||
|
### 2. 常见问题
|
||||||
|
- **CMake 警告**: 可忽略,不影响构建
|
||||||
|
- **WebView2 警告**: 类型转换警告,不影响功能
|
||||||
|
- **路径问题**: 确保使用正确的 x64 路径
|
||||||
|
|
||||||
|
### 3. 性能优化
|
||||||
|
- Debug 构建约 4.7 分钟
|
||||||
|
- Release 构建约 27 秒
|
||||||
|
- 建议使用 Release 版本进行分发
|
||||||
|
|
||||||
|
## 🔍 调试工具
|
||||||
|
|
||||||
|
### 构建日志分析
|
||||||
|
查看 `构建日志分析.md` 文件获取详细的构建日志分析和故障排除指南。
|
||||||
|
|
||||||
|
### 连接状态调试
|
||||||
|
使用 `debug_connection_status.dart` 工具检查应用连接状态。
|
||||||
|
|
||||||
|
## 📞 支持
|
||||||
|
|
||||||
|
如遇到构建问题,请检查:
|
||||||
|
1. 环境配置是否正确
|
||||||
|
2. 依赖是否完整安装
|
||||||
|
3. 查看构建日志获取具体错误信息
|
||||||
|
4. 参考本说明文档的修复记录
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**最后更新**: $(date)
|
||||||
|
**构建状态**: ✅ 成功
|
||||||
|
**文档版本**: 1.0
|
||||||
Reference in New Issue
Block a user