Compare commits
22 Commits
fc4ecf874b
...
74df08144f
| Author | SHA1 | Date | |
|---|---|---|---|
| 74df08144f | |||
| 5c8f0ca1fc | |||
| c1c5f1a2e0 | |||
| d94e7fd44a | |||
| 064a0a7402 | |||
| 8bba2441c2 | |||
| 7011ba1551 | |||
| bba8acfe76 | |||
| 9c2f9be6c5 | |||
| 0067017ca6 | |||
| 1970bbb6fd | |||
| fbdf4a2337 | |||
| 4250f61345 | |||
| ee20cc93e2 | |||
| 01ea786ef0 | |||
| 11c66d0314 | |||
| 0449e04f42 | |||
| 41f85a6747 | |||
| 3328919f7e | |||
| fde4bfa464 | |||
| 8204895199 | |||
| 773047838c |
298
.github/workflows/build-multiplatform.yml
vendored
298
.github/workflows/build-multiplatform.yml
vendored
@ -45,13 +45,64 @@ on:
|
|||||||
required: true
|
required: true
|
||||||
default: 'https://xpp5.oss-ap-southeast-1.aliyuncs.com/bear1.txt'
|
default: 'https://xpp5.oss-ap-southeast-1.aliyuncs.com/bear1.txt'
|
||||||
type: string
|
type: string
|
||||||
|
encryption_key:
|
||||||
|
description: '加密密钥'
|
||||||
|
required: true
|
||||||
|
default: 'c0qhq99a-nq8h-ropg-wrlc-ezj4dlkxqpzx'
|
||||||
|
type: string
|
||||||
platforms:
|
platforms:
|
||||||
description: '构建平台 (多选: android,windows,macos,linux)'
|
description: '构建平台 (多选: android,windows,macos,linux,ios)'
|
||||||
required: true
|
required: true
|
||||||
default: 'android,windows,macos,linux'
|
default: 'android,windows,macos,linux'
|
||||||
type: string
|
type: string
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
# ==================== 编译 libcore (iOS/tvOS) ====================
|
||||||
|
build-libcore-ios:
|
||||||
|
name: 编译 libcore (iOS/tvOS)
|
||||||
|
runs-on: macos-latest
|
||||||
|
if: contains(inputs.platforms || 'ios', 'ios')
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: 📥 Checkout 代码
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: 🔧 设置 Go 环境
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: '1.23'
|
||||||
|
cache: true
|
||||||
|
cache-dependency-path: libcore/go.sum
|
||||||
|
|
||||||
|
- name: 🔧 设置 Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
|
||||||
|
- name: 📦 编译 libcore.xcframework (支持 iOS/tvOS)
|
||||||
|
working-directory: libcore
|
||||||
|
run: |
|
||||||
|
echo "🚀 开始编译 iOS/tvOS libcore..."
|
||||||
|
make ios-full
|
||||||
|
|
||||||
|
if [ -d "bin/Libcore.xcframework" ]; then
|
||||||
|
echo "✅ iOS/tvOS libcore 编译成功"
|
||||||
|
ls -lh bin/
|
||||||
|
else
|
||||||
|
echo "❌ iOS/tvOS libcore 编译失败"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: 📤 上传 iOS libcore
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: libcore-ios
|
||||||
|
path: libcore/bin/Libcore.xcframework
|
||||||
|
retention-days: 7
|
||||||
|
|
||||||
# ==================== 编译 libcore (Android) ====================
|
# ==================== 编译 libcore (Android) ====================
|
||||||
build-libcore-android:
|
build-libcore-android:
|
||||||
name: 编译 libcore (Android)
|
name: 编译 libcore (Android)
|
||||||
@ -293,7 +344,7 @@ jobs:
|
|||||||
name: libcore-android
|
name: libcore-android
|
||||||
path: android/app/libs/
|
path: android/app/libs/
|
||||||
|
|
||||||
- name: ⚙️ 配置 API 和 OSS
|
- name: ⚙️ 配置 API、OSS 和加密密钥
|
||||||
run: |
|
run: |
|
||||||
CONFIG_FILE="lib/app/common/app_config.dart"
|
CONFIG_FILE="lib/app/common/app_config.dart"
|
||||||
API_DOMAIN="${{ inputs.api_domain || 'api.maodag.top' }}"
|
API_DOMAIN="${{ inputs.api_domain || 'api.maodag.top' }}"
|
||||||
@ -301,16 +352,22 @@ jobs:
|
|||||||
OSS_URL_2="${{ inputs.oss_url_2 || 'https://xgp3.oss-ap-northeast-1.aliyuncs.com/bear1.txt' }}"
|
OSS_URL_2="${{ inputs.oss_url_2 || 'https://xgp3.oss-ap-northeast-1.aliyuncs.com/bear1.txt' }}"
|
||||||
OSS_URL_3="${{ inputs.oss_url_3 || 'https://xpp4.oss-ap-northeast-2.aliyuncs.com/bear1.txt' }}"
|
OSS_URL_3="${{ inputs.oss_url_3 || 'https://xpp4.oss-ap-northeast-2.aliyuncs.com/bear1.txt' }}"
|
||||||
OSS_URL_4="${{ inputs.oss_url_4 || 'https://xpp5.oss-ap-southeast-1.aliyuncs.com/bear1.txt' }}"
|
OSS_URL_4="${{ inputs.oss_url_4 || 'https://xpp5.oss-ap-southeast-1.aliyuncs.com/bear1.txt' }}"
|
||||||
|
ENCRYPTION_KEY="${{ inputs.encryption_key || 'c0qhq99a-nq8h-ropg-wrlc-ezj4dlkxqpzx' }}"
|
||||||
|
|
||||||
echo "🔧 配置参数:"
|
echo "🔧 配置参数:"
|
||||||
echo " API: $API_DOMAIN"
|
echo " API: $API_DOMAIN"
|
||||||
echo " OSS: $OSS_URL_1"
|
echo " OSS: $OSS_URL_1"
|
||||||
|
echo " 密钥: ${ENCRYPTION_KEY:0:10}..."
|
||||||
|
|
||||||
|
# 转义密钥中的特殊字符(使用 perl 兼容所有平台)
|
||||||
|
ENCRYPTION_KEY_ESCAPED=$(echo "$ENCRYPTION_KEY" | perl -pe 's/([\[\].*^$()+?{|\\])/\\$1/g')
|
||||||
|
|
||||||
sed -i "s|api\.maodag\.top|$API_DOMAIN|g" "$CONFIG_FILE"
|
sed -i "s|api\.maodag\.top|$API_DOMAIN|g" "$CONFIG_FILE"
|
||||||
sed -i "s|https://ppp2\.oss-cn-hongkong\.aliyuncs\.com/bear1\.txt|$OSS_URL_1|g" "$CONFIG_FILE"
|
sed -i "s|https://ppp2\.oss-cn-hongkong\.aliyuncs\.com/bear1\.txt|$OSS_URL_1|g" "$CONFIG_FILE"
|
||||||
sed -i "s|https://xgp3\.oss-ap-northeast-1\.aliyuncs\.com/bear1\.txt|$OSS_URL_2|g" "$CONFIG_FILE"
|
sed -i "s|https://xgp3\.oss-ap-northeast-1\.aliyuncs\.com/bear1\.txt|$OSS_URL_2|g" "$CONFIG_FILE"
|
||||||
sed -i "s|https://xpp4\.oss-ap-northeast-2\.aliyuncs\.com/bear1\.txt|$OSS_URL_3|g" "$CONFIG_FILE"
|
sed -i "s|https://xpp4\.oss-ap-northeast-2\.aliyuncs\.com/bear1\.txt|$OSS_URL_3|g" "$CONFIG_FILE"
|
||||||
sed -i "s|https://xpp5\.oss-ap-southeast-1\.aliyuncs\.com/bear1\.txt|$OSS_URL_4|g" "$CONFIG_FILE"
|
sed -i "s|https://xpp5\.oss-ap-southeast-1\.aliyuncs\.com/bear1\.txt|$OSS_URL_4|g" "$CONFIG_FILE"
|
||||||
|
sed -i "s|c0qhq99a-nq8h-ropg-wrlc-ezj4dlkxqpzx|$ENCRYPTION_KEY_ESCAPED|g" "$CONFIG_FILE"
|
||||||
|
|
||||||
echo "✅ 配置完成"
|
echo "✅ 配置完成"
|
||||||
|
|
||||||
@ -370,9 +427,70 @@ jobs:
|
|||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: libcore-windows
|
name: libcore-windows
|
||||||
path: libcore/bin/
|
path: libcore_windows_temp
|
||||||
|
|
||||||
|
- name: 🔧 复制 libcore 文件到正确位置并重命名
|
||||||
|
run: |
|
||||||
|
Write-Host "📋 开始复制 libcore 文件..."
|
||||||
|
|
||||||
|
# 显示下载的文件结构
|
||||||
|
Write-Host "🔍 检查下载的文件结构:"
|
||||||
|
Get-ChildItem -Recurse libcore_windows_temp -ErrorAction SilentlyContinue | Format-Table Name, FullName
|
||||||
|
|
||||||
|
# 确保目标目录存在
|
||||||
|
New-Item -ItemType Directory -Force -Path "libcore\bin" | Out-Null
|
||||||
|
|
||||||
|
# 查找 libcore.dll(可能在 libcore_windows_temp/bin/ 或 libcore_windows_temp/libcore/bin/)
|
||||||
|
$dllFiles = Get-ChildItem -Path libcore_windows_temp -Recurse -Filter "libcore.dll" -ErrorAction SilentlyContinue
|
||||||
|
if ($dllFiles) {
|
||||||
|
$sourceDll = $dllFiles[0].FullName
|
||||||
|
Write-Host "✅ 找到 libcore.dll: $sourceDll"
|
||||||
|
Copy-Item $sourceDll "libcore\bin\libcore.dll" -Force
|
||||||
|
} else {
|
||||||
|
Write-Host "❌ 未找到 libcore.dll"
|
||||||
|
Write-Host "当前目录内容:"
|
||||||
|
Get-ChildItem -Path . -Recurse | Select-Object -First 20 | Format-Table Name, FullName
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# 查找并复制 HiddifyCli.exe,重命名为 BearVPNCli.exe
|
||||||
|
$exeFiles = Get-ChildItem -Path libcore_windows_temp -Recurse -Filter "HiddifyCli.exe" -ErrorAction SilentlyContinue
|
||||||
|
if ($exeFiles) {
|
||||||
|
$sourceExe = $exeFiles[0].FullName
|
||||||
|
Write-Host "✅ 找到 HiddifyCli.exe: $sourceExe"
|
||||||
|
Write-Host "📝 复制并重命名为 BearVPNCli.exe"
|
||||||
|
Copy-Item $sourceExe "libcore\bin\BearVPNCli.exe" -Force
|
||||||
|
Write-Host "✅ 重命名完成:HiddifyCli.exe → BearVPNCli.exe"
|
||||||
|
} else {
|
||||||
|
Write-Host "⚠️ 未找到 HiddifyCli.exe(这不是致命错误)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 复制 webui 目录
|
||||||
|
$webuiDir = Get-ChildItem -Path libcore_windows_temp -Recurse -Filter "webui" -Directory -ErrorAction SilentlyContinue
|
||||||
|
if ($webuiDir) {
|
||||||
|
Write-Host "✅ 找到 webui 目录: $($webuiDir[0].FullName)"
|
||||||
|
Copy-Item -Path $webuiDir[0].FullName -Destination "libcore\bin\webui" -Recurse -Force
|
||||||
|
} else {
|
||||||
|
Write-Host "⚠️ 未找到 webui 目录(这不是致命错误)"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "📄 验证复制后的文件结构:"
|
||||||
|
if (Test-Path "libcore\bin") {
|
||||||
|
Get-ChildItem libcore\bin\ -Recurse | Format-Table Name, FullName, Length
|
||||||
|
} else {
|
||||||
|
Write-Host "❌ libcore\bin 目录不存在"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path "libcore\bin\libcore.dll")) {
|
||||||
|
Write-Host "❌ libcore.dll 未正确复制到 libcore\bin\"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "✅ libcore 文件复制完成"
|
||||||
|
shell: pwsh
|
||||||
|
|
||||||
- name: ⚙️ 配置 API 和 OSS
|
- name: ⚙️ 配置 API、OSS 和加密密钥
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
CONFIG_FILE="lib/app/common/app_config.dart"
|
CONFIG_FILE="lib/app/common/app_config.dart"
|
||||||
@ -381,12 +499,16 @@ jobs:
|
|||||||
OSS_URL_2="${{ inputs.oss_url_2 || 'https://xgp3.oss-ap-northeast-1.aliyuncs.com/bear1.txt' }}"
|
OSS_URL_2="${{ inputs.oss_url_2 || 'https://xgp3.oss-ap-northeast-1.aliyuncs.com/bear1.txt' }}"
|
||||||
OSS_URL_3="${{ inputs.oss_url_3 || 'https://xpp4.oss-ap-northeast-2.aliyuncs.com/bear1.txt' }}"
|
OSS_URL_3="${{ inputs.oss_url_3 || 'https://xpp4.oss-ap-northeast-2.aliyuncs.com/bear1.txt' }}"
|
||||||
OSS_URL_4="${{ inputs.oss_url_4 || 'https://xpp5.oss-ap-southeast-1.aliyuncs.com/bear1.txt' }}"
|
OSS_URL_4="${{ inputs.oss_url_4 || 'https://xpp5.oss-ap-southeast-1.aliyuncs.com/bear1.txt' }}"
|
||||||
|
ENCRYPTION_KEY="${{ inputs.encryption_key || 'c0qhq99a-nq8h-ropg-wrlc-ezj4dlkxqpzx' }}"
|
||||||
|
|
||||||
|
ENCRYPTION_KEY_ESCAPED=$(echo "$ENCRYPTION_KEY" | perl -pe 's/([\[\].*^$()+?{|\\])/\\$1/g')
|
||||||
|
|
||||||
sed -i "s|api\.maodag\.top|$API_DOMAIN|g" "$CONFIG_FILE"
|
sed -i "s|api\.maodag\.top|$API_DOMAIN|g" "$CONFIG_FILE"
|
||||||
sed -i "s|https://ppp2\.oss-cn-hongkong\.aliyuncs\.com/bear1\.txt|$OSS_URL_1|g" "$CONFIG_FILE"
|
sed -i "s|https://ppp2\.oss-cn-hongkong\.aliyuncs\.com/bear1\.txt|$OSS_URL_1|g" "$CONFIG_FILE"
|
||||||
sed -i "s|https://xgp3\.oss-ap-northeast-1\.aliyuncs\.com/bear1\.txt|$OSS_URL_2|g" "$CONFIG_FILE"
|
sed -i "s|https://xgp3\.oss-ap-northeast-1\.aliyuncs\.com/bear1\.txt|$OSS_URL_2|g" "$CONFIG_FILE"
|
||||||
sed -i "s|https://xpp4\.oss-ap-northeast-2\.aliyuncs\.com/bear1\.txt|$OSS_URL_3|g" "$CONFIG_FILE"
|
sed -i "s|https://xpp4\.oss-ap-northeast-2\.aliyuncs\.com/bear1\.txt|$OSS_URL_3|g" "$CONFIG_FILE"
|
||||||
sed -i "s|https://xpp5\.oss-ap-southeast-1\.aliyuncs\.com/bear1\.txt|$OSS_URL_4|g" "$CONFIG_FILE"
|
sed -i "s|https://xpp5\.oss-ap-southeast-1\.aliyuncs\.com/bear1\.txt|$OSS_URL_4|g" "$CONFIG_FILE"
|
||||||
|
sed -i "s|c0qhq99a-nq8h-ropg-wrlc-ezj4dlkxqpzx|$ENCRYPTION_KEY_ESCAPED|g" "$CONFIG_FILE"
|
||||||
|
|
||||||
- name: 📦 安装 Flutter 依赖
|
- name: 📦 安装 Flutter 依赖
|
||||||
run: |
|
run: |
|
||||||
@ -415,10 +537,70 @@ jobs:
|
|||||||
}
|
}
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
|
|
||||||
|
- name: 🔧 验证 libcore 文件存在
|
||||||
|
run: |
|
||||||
|
Write-Host "📋 验证 libcore 文件是否存在..."
|
||||||
|
|
||||||
|
if (Test-Path "libcore\bin\libcore.dll") {
|
||||||
|
$dllInfo = Get-Item "libcore\bin\libcore.dll"
|
||||||
|
Write-Host "✅ libcore.dll 存在: $($dllInfo.FullName) - 大小: $($dllInfo.Length) bytes"
|
||||||
|
} else {
|
||||||
|
Write-Host "❌ libcore.dll 不存在"
|
||||||
|
Write-Host "当前 libcore\bin 目录内容:"
|
||||||
|
if (Test-Path "libcore\bin") {
|
||||||
|
Get-ChildItem "libcore\bin" | Format-Table Name, FullName, Length
|
||||||
|
} else {
|
||||||
|
Write-Host "libcore\bin 目录不存在"
|
||||||
|
}
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Test-Path "libcore\bin\BearVPNCli.exe") {
|
||||||
|
$exeInfo = Get-Item "libcore\bin\BearVPNCli.exe"
|
||||||
|
Write-Host "✅ BearVPNCli.exe 存在: $($exeInfo.FullName) - 大小: $($exeInfo.Length) bytes"
|
||||||
|
} else {
|
||||||
|
Write-Host "⚠️ BearVPNCli.exe 不存在"
|
||||||
|
}
|
||||||
|
shell: pwsh
|
||||||
|
|
||||||
- name: 🔨 构建 Windows (Release)
|
- name: 🔨 构建 Windows (Release)
|
||||||
run: |
|
run: |
|
||||||
flutter build windows --release
|
flutter build windows --release
|
||||||
|
|
||||||
|
- name: 🔍 验证 Windows 文件结构
|
||||||
|
run: |
|
||||||
|
Write-Host "📋 检查 Release 目录文件结构..."
|
||||||
|
|
||||||
|
$releaseDir = "build\windows\x64\runner\Release"
|
||||||
|
if (Test-Path $releaseDir) {
|
||||||
|
Write-Host "✅ Release 目录存在"
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "📄 文件列表:"
|
||||||
|
Get-ChildItem $releaseDir | Format-Table Name, Length, LastWriteTime
|
||||||
|
|
||||||
|
# 检查关键文件
|
||||||
|
if (Test-Path "$releaseDir\BearVPN.exe") {
|
||||||
|
Write-Host "✅ BearVPN.exe 存在"
|
||||||
|
} else {
|
||||||
|
Write-Host "❌ BearVPN.exe 不存在"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Test-Path "$releaseDir\BearVPNCli.exe") {
|
||||||
|
Write-Host "✅ BearVPNCli.exe 存在"
|
||||||
|
} else {
|
||||||
|
Write-Host "⚠️ BearVPNCli.exe 不存在"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Test-Path "$releaseDir\libcore.dll") {
|
||||||
|
Write-Host "✅ libcore.dll 存在"
|
||||||
|
} else {
|
||||||
|
Write-Host "❌ libcore.dll 不存在"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Host "❌ Release 目录不存在"
|
||||||
|
}
|
||||||
|
shell: pwsh
|
||||||
|
|
||||||
- name: 📦 打包 Windows
|
- name: 📦 打包 Windows
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
@ -466,7 +648,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
chmod +x libcore/bin/HiddifyCli
|
chmod +x libcore/bin/HiddifyCli
|
||||||
|
|
||||||
- name: ⚙️ 配置 API 和 OSS
|
- name: ⚙️ 配置 API、OSS 和加密密钥
|
||||||
run: |
|
run: |
|
||||||
CONFIG_FILE="lib/app/common/app_config.dart"
|
CONFIG_FILE="lib/app/common/app_config.dart"
|
||||||
API_DOMAIN="${{ inputs.api_domain || 'api.maodag.top' }}"
|
API_DOMAIN="${{ inputs.api_domain || 'api.maodag.top' }}"
|
||||||
@ -474,12 +656,27 @@ jobs:
|
|||||||
OSS_URL_2="${{ inputs.oss_url_2 || 'https://xgp3.oss-ap-northeast-1.aliyuncs.com/bear1.txt' }}"
|
OSS_URL_2="${{ inputs.oss_url_2 || 'https://xgp3.oss-ap-northeast-1.aliyuncs.com/bear1.txt' }}"
|
||||||
OSS_URL_3="${{ inputs.oss_url_3 || 'https://xpp4.oss-ap-northeast-2.aliyuncs.com/bear1.txt' }}"
|
OSS_URL_3="${{ inputs.oss_url_3 || 'https://xpp4.oss-ap-northeast-2.aliyuncs.com/bear1.txt' }}"
|
||||||
OSS_URL_4="${{ inputs.oss_url_4 || 'https://xpp5.oss-ap-southeast-1.aliyuncs.com/bear1.txt' }}"
|
OSS_URL_4="${{ inputs.oss_url_4 || 'https://xpp5.oss-ap-southeast-1.aliyuncs.com/bear1.txt' }}"
|
||||||
|
ENCRYPTION_KEY="${{ inputs.encryption_key || 'c0qhq99a-nq8h-ropg-wrlc-ezj4dlkxqpzx' }}"
|
||||||
|
|
||||||
|
# macOS 使用 BSD sed,转义特殊字符
|
||||||
|
# 使用 perl 来转义特殊字符,更可靠
|
||||||
|
ENCRYPTION_KEY_ESCAPED=$(echo "$ENCRYPTION_KEY" | perl -pe 's/([\[\].*^$()+?{|\\])/\\$1/g')
|
||||||
|
|
||||||
|
echo "🔧 配置参数:"
|
||||||
|
echo " API: $API_DOMAIN"
|
||||||
|
echo " 密钥: ${ENCRYPTION_KEY:0:10}..."
|
||||||
|
|
||||||
|
# macOS sed 需要使用 -i '' 和正确的语法
|
||||||
sed -i '' "s|api\.maodag\.top|$API_DOMAIN|g" "$CONFIG_FILE"
|
sed -i '' "s|api\.maodag\.top|$API_DOMAIN|g" "$CONFIG_FILE"
|
||||||
sed -i '' "s|https://ppp2\.oss-cn-hongkong\.aliyuncs\.com/bear1\.txt|$OSS_URL_1|g" "$CONFIG_FILE"
|
sed -i '' "s|https://ppp2\.oss-cn-hongkong\.aliyuncs\.com/bear1\.txt|$OSS_URL_1|g" "$CONFIG_FILE"
|
||||||
sed -i '' "s|https://xgp3\.oss-ap-northeast-1\.aliyuncs\.com/bear1\.txt|$OSS_URL_2|g" "$CONFIG_FILE"
|
sed -i '' "s|https://xgp3\.oss-ap-northeast-1\.aliyuncs\.com/bear1\.txt|$OSS_URL_2|g" "$CONFIG_FILE"
|
||||||
sed -i '' "s|https://xpp4\.oss-ap-northeast-2\.aliyuncs\.com/bear1\.txt|$OSS_URL_3|g" "$CONFIG_FILE"
|
sed -i '' "s|https://xpp4\.oss-ap-northeast-2\.aliyuncs\.com/bear1\.txt|$OSS_URL_3|g" "$CONFIG_FILE"
|
||||||
sed -i '' "s|https://xpp5\.oss-ap-southeast-1\.aliyuncs\.com/bear1\.txt|$OSS_URL_4|g" "$CONFIG_FILE"
|
sed -i '' "s|https://xpp5\.oss-ap-southeast-1\.aliyuncs\.com/bear1\.txt|$OSS_URL_4|g" "$CONFIG_FILE"
|
||||||
|
|
||||||
|
# 使用不同的分隔符避免转义问题
|
||||||
|
sed -i '' "s|c0qhq99a-nq8h-ropg-wrlc-ezj4dlkxqpzx|$ENCRYPTION_KEY_ESCAPED|g" "$CONFIG_FILE"
|
||||||
|
|
||||||
|
echo "✅ 配置完成"
|
||||||
|
|
||||||
- name: 📦 安装 Flutter 依赖
|
- name: 📦 安装 Flutter 依赖
|
||||||
run: |
|
run: |
|
||||||
@ -549,7 +746,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
chmod +x libcore/bin/HiddifyCli
|
chmod +x libcore/bin/HiddifyCli
|
||||||
|
|
||||||
- name: ⚙️ 配置 API 和 OSS
|
- name: ⚙️ 配置 API、OSS 和加密密钥
|
||||||
run: |
|
run: |
|
||||||
CONFIG_FILE="lib/app/common/app_config.dart"
|
CONFIG_FILE="lib/app/common/app_config.dart"
|
||||||
API_DOMAIN="${{ inputs.api_domain || 'api.maodag.top' }}"
|
API_DOMAIN="${{ inputs.api_domain || 'api.maodag.top' }}"
|
||||||
@ -557,12 +754,16 @@ jobs:
|
|||||||
OSS_URL_2="${{ inputs.oss_url_2 || 'https://xgp3.oss-ap-northeast-1.aliyuncs.com/bear1.txt' }}"
|
OSS_URL_2="${{ inputs.oss_url_2 || 'https://xgp3.oss-ap-northeast-1.aliyuncs.com/bear1.txt' }}"
|
||||||
OSS_URL_3="${{ inputs.oss_url_3 || 'https://xpp4.oss-ap-northeast-2.aliyuncs.com/bear1.txt' }}"
|
OSS_URL_3="${{ inputs.oss_url_3 || 'https://xpp4.oss-ap-northeast-2.aliyuncs.com/bear1.txt' }}"
|
||||||
OSS_URL_4="${{ inputs.oss_url_4 || 'https://xpp5.oss-ap-southeast-1.aliyuncs.com/bear1.txt' }}"
|
OSS_URL_4="${{ inputs.oss_url_4 || 'https://xpp5.oss-ap-southeast-1.aliyuncs.com/bear1.txt' }}"
|
||||||
|
ENCRYPTION_KEY="${{ inputs.encryption_key || 'c0qhq99a-nq8h-ropg-wrlc-ezj4dlkxqpzx' }}"
|
||||||
|
|
||||||
|
ENCRYPTION_KEY_ESCAPED=$(echo "$ENCRYPTION_KEY" | perl -pe 's/([\[\].*^$()+?{|\\])/\\$1/g')
|
||||||
|
|
||||||
sed -i "s|api\.maodag\.top|$API_DOMAIN|g" "$CONFIG_FILE"
|
sed -i "s|api\.maodag\.top|$API_DOMAIN|g" "$CONFIG_FILE"
|
||||||
sed -i "s|https://ppp2\.oss-cn-hongkong\.aliyuncs\.com/bear1\.txt|$OSS_URL_1|g" "$CONFIG_FILE"
|
sed -i "s|https://ppp2\.oss-cn-hongkong\.aliyuncs\.com/bear1\.txt|$OSS_URL_1|g" "$CONFIG_FILE"
|
||||||
sed -i "s|https://xgp3\.oss-ap-northeast-1\.aliyuncs\.com/bear1\.txt|$OSS_URL_2|g" "$CONFIG_FILE"
|
sed -i "s|https://xgp3\.oss-ap-northeast-1\.aliyuncs\.com/bear1\.txt|$OSS_URL_2|g" "$CONFIG_FILE"
|
||||||
sed -i "s|https://xpp4\.oss-ap-northeast-2\.aliyuncs\.com/bear1\.txt|$OSS_URL_3|g" "$CONFIG_FILE"
|
sed -i "s|https://xpp4\.oss-ap-northeast-2\.aliyuncs\.com/bear1\.txt|$OSS_URL_3|g" "$CONFIG_FILE"
|
||||||
sed -i "s|https://xpp5\.oss-ap-southeast-1\.aliyuncs\.com/bear1\.txt|$OSS_URL_4|g" "$CONFIG_FILE"
|
sed -i "s|https://xpp5\.oss-ap-southeast-1\.aliyuncs\.com/bear1\.txt|$OSS_URL_4|g" "$CONFIG_FILE"
|
||||||
|
sed -i "s|c0qhq99a-nq8h-ropg-wrlc-ezj4dlkxqpzx|$ENCRYPTION_KEY_ESCAPED|g" "$CONFIG_FILE"
|
||||||
|
|
||||||
- name: 📦 安装 Flutter 依赖
|
- name: 📦 安装 Flutter 依赖
|
||||||
run: |
|
run: |
|
||||||
@ -588,10 +789,90 @@ jobs:
|
|||||||
path: BearVPN-linux-*.tar.gz
|
path: BearVPN-linux-*.tar.gz
|
||||||
retention-days: 30
|
retention-days: 30
|
||||||
|
|
||||||
|
# ==================== iOS 构建 ====================
|
||||||
|
build-ios:
|
||||||
|
name: 构建 iOS
|
||||||
|
needs: build-libcore-ios
|
||||||
|
runs-on: macos-latest
|
||||||
|
if: contains(inputs.platforms || 'ios', 'ios')
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: 📥 Checkout 代码
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: 🔧 设置 Flutter
|
||||||
|
uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
flutter-version: '3.24.5'
|
||||||
|
channel: 'stable'
|
||||||
|
cache: true
|
||||||
|
|
||||||
|
- name: 📥 下载 iOS libcore
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: libcore-ios
|
||||||
|
path: ios/Frameworks/
|
||||||
|
|
||||||
|
- name: ⚙️ 配置 API、OSS 和加密密钥
|
||||||
|
run: |
|
||||||
|
CONFIG_FILE="lib/app/common/app_config.dart"
|
||||||
|
API_DOMAIN="${{ inputs.api_domain || 'api.maodag.top' }}"
|
||||||
|
OSS_URL_1="${{ inputs.oss_url_1 || 'https://ppp2.oss-cn-hongkong.aliyuncs.com/bear1.txt' }}"
|
||||||
|
OSS_URL_2="${{ inputs.oss_url_2 || 'https://xgp3.oss-ap-northeast-1.aliyuncs.com/bear1.txt' }}"
|
||||||
|
OSS_URL_3="${{ inputs.oss_url_3 || 'https://xpp4.oss-ap-northeast-2.aliyuncs.com/bear1.txt' }}"
|
||||||
|
OSS_URL_4="${{ inputs.oss_url_4 || 'https://xpp5.oss-ap-southeast-1.aliyuncs.com/bear1.txt' }}"
|
||||||
|
ENCRYPTION_KEY="${{ inputs.encryption_key || 'c0qhq99a-nq8h-ropg-wrlc-ezj4dlkxqpzx' }}"
|
||||||
|
|
||||||
|
# 使用 perl 转义特殊字符
|
||||||
|
ENCRYPTION_KEY_ESCAPED=$(echo "$ENCRYPTION_KEY" | perl -pe 's/([\[\].*^$()+?{|\\])/\\$1/g')
|
||||||
|
|
||||||
|
sed -i '' "s|api\.maodag\.top|$API_DOMAIN|g" "$CONFIG_FILE"
|
||||||
|
sed -i '' "s|https://ppp2\.oss-cn-hongkong\.aliyuncs\.com/bear1\.txt|$OSS_URL_1|g" "$CONFIG_FILE"
|
||||||
|
sed -i '' "s|https://xgp3\.oss-ap-northeast-1\.aliyuncs\.com/bear1\.txt|$OSS_URL_2|g" "$CONFIG_FILE"
|
||||||
|
sed -i '' "s|https://xpp4\.oss-ap-northeast-2\.aliyuncs\.com/bear1\.txt|$OSS_URL_3|g" "$CONFIG_FILE"
|
||||||
|
sed -i '' "s|https://xpp5\.oss-ap-southeast-1\.aliyuncs\.com/bear1\.txt|$OSS_URL_4|g" "$CONFIG_FILE"
|
||||||
|
sed -i '' "s|c0qhq99a-nq8h-ropg-wrlc-ezj4dlkxqpzx|$ENCRYPTION_KEY_ESCAPED|g" "$CONFIG_FILE"
|
||||||
|
|
||||||
|
- name: 📦 安装 Flutter 依赖
|
||||||
|
run: |
|
||||||
|
flutter pub get
|
||||||
|
flutter pub run build_runner build --delete-conflicting-outputs
|
||||||
|
|
||||||
|
- name: 🔨 构建 iOS (Release)
|
||||||
|
run: |
|
||||||
|
flutter build ios --release --no-codesign
|
||||||
|
|
||||||
|
- name: 📦 打包 iOS
|
||||||
|
run: |
|
||||||
|
COMMIT_SHA=${GITHUB_SHA::7}
|
||||||
|
DATE=$(date '+%Y%m%d')
|
||||||
|
|
||||||
|
cd build/ios/iphoneos
|
||||||
|
|
||||||
|
# 创建 Payload 目录
|
||||||
|
mkdir -p Payload
|
||||||
|
cp -r Runner.app Payload/
|
||||||
|
|
||||||
|
# 创建 IPA 文件
|
||||||
|
zip -r "../../../../BearVPN-ios-release-${DATE}-${COMMIT_SHA}.ipa" Payload
|
||||||
|
|
||||||
|
echo "✅ iOS IPA 创建完成"
|
||||||
|
ls -lh BearVPN-ios-*.ipa
|
||||||
|
|
||||||
|
- name: 📤 上传 iOS
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ios-app
|
||||||
|
path: BearVPN-ios-*.ipa
|
||||||
|
retention-days: 30
|
||||||
|
|
||||||
# ==================== 创建 Release ====================
|
# ==================== 创建 Release ====================
|
||||||
create-release:
|
create-release:
|
||||||
name: 创建 Release
|
name: 创建 Release
|
||||||
needs: [build-android, build-windows, build-macos, build-linux]
|
needs: [build-android, build-windows, build-macos, build-linux, build-ios]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: startsWith(github.ref, 'refs/tags/v')
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
|
|
||||||
@ -614,6 +895,7 @@ jobs:
|
|||||||
| **Android** | arm64-v8a | BearVPN-android-arm64-v8a-*.apk |
|
| **Android** | arm64-v8a | BearVPN-android-arm64-v8a-*.apk |
|
||||||
| **Android** | armeabi-v7a | BearVPN-android-armeabi-v7a-*.apk |
|
| **Android** | armeabi-v7a | BearVPN-android-armeabi-v7a-*.apk |
|
||||||
| **Android** | x86_64 | BearVPN-android-x86_64-*.apk |
|
| **Android** | x86_64 | BearVPN-android-x86_64-*.apk |
|
||||||
|
| **iOS** | Universal | BearVPN-ios-*.ipa |
|
||||||
| **Windows** | x64 | BearVPN-windows-x64-*.zip |
|
| **Windows** | x64 | BearVPN-windows-x64-*.zip |
|
||||||
| **macOS** | Universal | BearVPN-macos-*.zip |
|
| **macOS** | Universal | BearVPN-macos-*.zip |
|
||||||
| **Linux** | x64 | BearVPN-linux-x64-*.tar.gz |
|
| **Linux** | x64 | BearVPN-linux-x64-*.tar.gz |
|
||||||
@ -628,6 +910,7 @@ jobs:
|
|||||||
### 📥 安装指南
|
### 📥 安装指南
|
||||||
|
|
||||||
**Android:** 下载 APK 直接安装
|
**Android:** 下载 APK 直接安装
|
||||||
|
**iOS:** 下载 IPA 使用 AltStore/Sideloadly 安装
|
||||||
**Windows:** 解压 ZIP 运行 BearVPN.exe
|
**Windows:** 解压 ZIP 运行 BearVPN.exe
|
||||||
**macOS:** 解压 ZIP 拖拽到应用程序
|
**macOS:** 解压 ZIP 拖拽到应用程序
|
||||||
**Linux:** 解压 tar.gz 运行可执行文件
|
**Linux:** 解压 tar.gz 运行可执行文件
|
||||||
@ -650,6 +933,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
artifacts/android-apk/*.apk
|
artifacts/android-apk/*.apk
|
||||||
|
artifacts/ios-app/*.ipa
|
||||||
artifacts/windows-x64/*.zip
|
artifacts/windows-x64/*.zip
|
||||||
artifacts/macos-app/*.zip
|
artifacts/macos-app/*.zip
|
||||||
artifacts/linux-x64/*.tar.gz
|
artifacts/linux-x64/*.tar.gz
|
||||||
|
|||||||
106
.github/workflows/build-windows.yml
vendored
106
.github/workflows/build-windows.yml
vendored
@ -8,11 +8,108 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
# 先编译 libcore
|
||||||
runs-on: windows-latest
|
build-libcore:
|
||||||
|
name: 编译 libcore (Windows)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- name: 📥 Checkout 代码
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: 🔧 设置 Go 环境
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: '1.23'
|
||||||
|
cache: true
|
||||||
|
cache-dependency-path: libcore/go.sum
|
||||||
|
|
||||||
|
- name: 🔧 设置 Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
|
||||||
|
- name: 🔧 安装 MinGW
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y mingw-w64
|
||||||
|
|
||||||
|
- name: 📦 编译 libcore.dll
|
||||||
|
working-directory: libcore
|
||||||
|
run: |
|
||||||
|
echo "🚀 开始编译 Windows libcore..."
|
||||||
|
make windows-amd64
|
||||||
|
|
||||||
|
if [ -f "bin/libcore.dll" ] && [ -f "bin/HiddifyCli.exe" ]; then
|
||||||
|
echo "✅ Windows libcore 编译成功"
|
||||||
|
ls -lh bin/
|
||||||
|
else
|
||||||
|
echo "❌ Windows libcore 编译失败"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: 📤 上传 Windows libcore
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: libcore-windows
|
||||||
|
path: |
|
||||||
|
libcore/bin/libcore.dll
|
||||||
|
libcore/bin/HiddifyCli.exe
|
||||||
|
libcore/bin/webui/**
|
||||||
|
retention-days: 7
|
||||||
|
|
||||||
|
# 构建 Windows 应用
|
||||||
|
build:
|
||||||
|
runs-on: windows-latest
|
||||||
|
needs: build-libcore
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: 📥 Checkout 代码
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: 📥 下载 libcore
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: libcore-windows
|
||||||
|
path: .
|
||||||
|
|
||||||
|
- name: 🔧 复制 libcore 文件到正确位置并重命名
|
||||||
|
run: |
|
||||||
|
Write-Host "📋 复制 libcore 文件..."
|
||||||
|
|
||||||
|
# 创建目标目录
|
||||||
|
New-Item -ItemType Directory -Force -Path libcore\bin
|
||||||
|
|
||||||
|
# 查找并复制 HiddifyCli.exe,重命名为 BearVPNCli.exe
|
||||||
|
$exeFiles = Get-ChildItem -Recurse -Filter "HiddifyCli.exe" -ErrorAction SilentlyContinue
|
||||||
|
if ($exeFiles) {
|
||||||
|
$sourceExe = $exeFiles[0].FullName
|
||||||
|
Write-Host "✅ 找到 HiddifyCli.exe: $sourceExe"
|
||||||
|
Write-Host "📝 复制并重命名为 BearVPNCli.exe"
|
||||||
|
Copy-Item $sourceExe libcore\bin\BearVPNCli.exe
|
||||||
|
Write-Host "✅ 重命名完成:HiddifyCli.exe → BearVPNCli.exe"
|
||||||
|
} else {
|
||||||
|
Write-Host "⚠️ 未找到 HiddifyCli.exe"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 复制 libcore.dll
|
||||||
|
$dllFiles = Get-ChildItem -Recurse -Filter "libcore.dll" -ErrorAction SilentlyContinue
|
||||||
|
if ($dllFiles) {
|
||||||
|
$sourceDll = $dllFiles[0].FullName
|
||||||
|
Write-Host "✅ 找到 libcore.dll: $sourceDll"
|
||||||
|
Copy-Item $sourceDll libcore\bin\libcore.dll
|
||||||
|
} else {
|
||||||
|
Write-Host "⚠️ 未找到 libcore.dll"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "📄 验证文件:"
|
||||||
|
if (Test-Path libcore\bin) {
|
||||||
|
Get-ChildItem libcore\bin\ -ErrorAction SilentlyContinue | Format-Table Name, Length
|
||||||
|
}
|
||||||
|
|
||||||
- name: Setup Flutter
|
- name: Setup Flutter
|
||||||
uses: subosito/flutter-action@v2
|
uses: subosito/flutter-action@v2
|
||||||
@ -26,6 +123,9 @@ jobs:
|
|||||||
- name: Get dependencies
|
- name: Get dependencies
|
||||||
run: flutter pub get
|
run: flutter pub get
|
||||||
|
|
||||||
|
- name: Generate code
|
||||||
|
run: dart run build_runner build --delete-conflicting-outputs
|
||||||
|
|
||||||
- name: Build Windows Debug
|
- name: Build Windows Debug
|
||||||
run: flutter build windows
|
run: flutter build windows
|
||||||
|
|
||||||
|
|||||||
9
.gitignore
vendored
9
.gitignore
vendored
@ -58,7 +58,6 @@ app.*.map.json
|
|||||||
/android/app/profile
|
/android/app/profile
|
||||||
/android/app/release
|
/android/app/release
|
||||||
|
|
||||||
|
|
||||||
/data
|
/data
|
||||||
/.gradle/
|
/.gradle/
|
||||||
|
|
||||||
@ -99,3 +98,11 @@ libcore/*.aar
|
|||||||
|
|
||||||
# Android 编译产物
|
# Android 编译产物
|
||||||
android/app/libs/*.aar
|
android/app/libs/*.aar
|
||||||
|
|
||||||
|
# FVM Version Cache
|
||||||
|
.fvm/
|
||||||
|
# Build scripts configuration files
|
||||||
|
scripts/*.json
|
||||||
|
!scripts/build_config.template.json
|
||||||
|
scripts/*.bak
|
||||||
|
lib/app/common/app_config.dart.bak
|
||||||
|
|||||||
@ -1135,18 +1135,9 @@ class AppConfig {
|
|||||||
|
|
||||||
_isInitializing = true;
|
_isInitializing = true;
|
||||||
try {
|
try {
|
||||||
// Debug 模式下直接使用固定地址,跳过所有配置请求和域名切换逻辑
|
// 所有模式都走正常的配置请求流程
|
||||||
// if (kDebugMode) {
|
KRLogUtil.kr_i('🚀 开始配置初始化', tag: 'AppConfig');
|
||||||
// KRLogUtil.kr_i('🐛 Debug 模式,使用固定 API 地址,跳过配置请求', tag: 'AppConfig');
|
await _startAutoRetry(onSuccess);
|
||||||
// if (onSuccess != null) {
|
|
||||||
// await onSuccess();
|
|
||||||
// }
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
if (onSuccess != null) {
|
|
||||||
await onSuccess();
|
|
||||||
}
|
|
||||||
// await _startAutoRetry(onSuccess);
|
|
||||||
} finally {
|
} finally {
|
||||||
_isInitializing = false;
|
_isInitializing = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
|
|||||||
import '../services/api_service/kr_api.user.dart';
|
import '../services/api_service/kr_api.user.dart';
|
||||||
import '../services/kr_announcement_service.dart';
|
import '../services/kr_announcement_service.dart';
|
||||||
import '../services/singbox_imp/kr_sing_box_imp.dart';
|
import '../services/singbox_imp/kr_sing_box_imp.dart';
|
||||||
|
import '../services/kr_site_config_service.dart';
|
||||||
import '../utils/kr_event_bus.dart';
|
import '../utils/kr_event_bus.dart';
|
||||||
import '../../singbox/model/singbox_status.dart';
|
import '../../singbox/model/singbox_status.dart';
|
||||||
|
|
||||||
|
|||||||
@ -92,7 +92,7 @@ class KROutboundItem {
|
|||||||
"tls": {
|
"tls": {
|
||||||
"enabled": json["security"] == "tls",
|
"enabled": json["security"] == "tls",
|
||||||
"server_name": serverName,
|
"server_name": serverName,
|
||||||
"insecure": securityConfig["allow_insecure"] ?? false,
|
"insecure": securityConfig["allow_insecure"] ?? true,
|
||||||
"utls": {
|
"utls": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"fingerprint": securityConfig["fingerprint"] ?? "chrome"
|
"fingerprint": securityConfig["fingerprint"] ?? "chrome"
|
||||||
@ -123,7 +123,7 @@ class KROutboundItem {
|
|||||||
"tls": {
|
"tls": {
|
||||||
"enabled": json["security"] == "tls",
|
"enabled": json["security"] == "tls",
|
||||||
"server_name": serverName,
|
"server_name": serverName,
|
||||||
"insecure": securityConfig["allow_insecure"] ?? false,
|
"insecure": securityConfig["allow_insecure"] ?? true,
|
||||||
"utls": {"enabled": true, "fingerprint": "chrome"}
|
"utls": {"enabled": true, "fingerprint": "chrome"}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -138,7 +138,9 @@ class KROutboundItem {
|
|||||||
"password": nodeListItem.uuid
|
"password": nodeListItem.uuid
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
|
case "hysteria":
|
||||||
case "hysteria2":
|
case "hysteria2":
|
||||||
|
// 后端的 "hysteria" 实际上是 Hysteria2 协议
|
||||||
final securityConfig =
|
final securityConfig =
|
||||||
json["security_config"] as Map<String, dynamic>? ?? {};
|
json["security_config"] as Map<String, dynamic>? ?? {};
|
||||||
config = {
|
config = {
|
||||||
@ -156,8 +158,7 @@ class KROutboundItem {
|
|||||||
"tls": {
|
"tls": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"server_name": securityConfig["sni"] ?? "",
|
"server_name": securityConfig["sni"] ?? "",
|
||||||
"insecure": securityConfig["allow_insecure"] ?? false,
|
"insecure": securityConfig["allow_insecure"] ?? true
|
||||||
"alpn": ["h3"]
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
@ -181,7 +182,7 @@ class KROutboundItem {
|
|||||||
"tls": {
|
"tls": {
|
||||||
"enabled": json["security"] == "tls",
|
"enabled": json["security"] == "tls",
|
||||||
"server_name": serverName,
|
"server_name": serverName,
|
||||||
"insecure": securityConfig["allow_insecure"] ?? false,
|
"insecure": securityConfig["allow_insecure"] ?? true,
|
||||||
"utls": {"enabled": true, "fingerprint": "chrome"}
|
"utls": {"enabled": true, "fingerprint": "chrome"}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -260,6 +261,10 @@ class KROutboundItem {
|
|||||||
print('📄 完整配置: $config');
|
print('📄 完整配置: $config');
|
||||||
break;
|
break;
|
||||||
case "vless":
|
case "vless":
|
||||||
|
// 判断是否为域名(非IP地址)
|
||||||
|
final bool isDomain = !RegExp(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$')
|
||||||
|
.hasMatch(nodeListItem.serverAddr);
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
"type": "vless",
|
"type": "vless",
|
||||||
"tag": nodeListItem.name,
|
"tag": nodeListItem.name,
|
||||||
@ -268,8 +273,8 @@ class KROutboundItem {
|
|||||||
"uuid": nodeListItem.uuid,
|
"uuid": nodeListItem.uuid,
|
||||||
"tls": {
|
"tls": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"server_name": nodeListItem.serverAddr,
|
if (isDomain) "server_name": nodeListItem.serverAddr,
|
||||||
"insecure": false,
|
"insecure": true,
|
||||||
"utls": {
|
"utls": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"fingerprint": "chrome"
|
"fingerprint": "chrome"
|
||||||
@ -280,6 +285,10 @@ class KROutboundItem {
|
|||||||
print('📄 完整配置: $config');
|
print('📄 完整配置: $config');
|
||||||
break;
|
break;
|
||||||
case "vmess":
|
case "vmess":
|
||||||
|
// 判断是否为域名(非IP地址)
|
||||||
|
final bool isDomain = !RegExp(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$')
|
||||||
|
.hasMatch(nodeListItem.serverAddr);
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
"type": "vmess",
|
"type": "vmess",
|
||||||
"tag": nodeListItem.name,
|
"tag": nodeListItem.name,
|
||||||
@ -290,8 +299,8 @@ class KROutboundItem {
|
|||||||
"security": "auto",
|
"security": "auto",
|
||||||
"tls": {
|
"tls": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"server_name": nodeListItem.serverAddr,
|
if (isDomain) "server_name": nodeListItem.serverAddr,
|
||||||
"insecure": false,
|
"insecure": true,
|
||||||
"utls": {"enabled": true, "fingerprint": "chrome"}
|
"utls": {"enabled": true, "fingerprint": "chrome"}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -299,6 +308,10 @@ class KROutboundItem {
|
|||||||
print('📄 完整配置: $config');
|
print('📄 完整配置: $config');
|
||||||
break;
|
break;
|
||||||
case "trojan":
|
case "trojan":
|
||||||
|
// 判断是否为域名(非IP地址)
|
||||||
|
final bool isDomain = !RegExp(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$')
|
||||||
|
.hasMatch(nodeListItem.serverAddr);
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
"type": "trojan",
|
"type": "trojan",
|
||||||
"tag": nodeListItem.name,
|
"tag": nodeListItem.name,
|
||||||
@ -307,32 +320,39 @@ class KROutboundItem {
|
|||||||
"password": nodeListItem.uuid,
|
"password": nodeListItem.uuid,
|
||||||
"tls": {
|
"tls": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"server_name": nodeListItem.serverAddr,
|
if (isDomain) "server_name": nodeListItem.serverAddr,
|
||||||
"insecure": false,
|
"insecure": true,
|
||||||
"utls": {"enabled": true, "fingerprint": "chrome"}
|
"utls": {"enabled": true, "fingerprint": "chrome"}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
print('✅ Trojan 节点配置构建成功: ${nodeListItem.name}');
|
print('✅ Trojan 节点配置构建成功: ${nodeListItem.name}');
|
||||||
print('📄 完整配置: $config');
|
print('📄 完整配置: $config');
|
||||||
break;
|
break;
|
||||||
|
case "hysteria":
|
||||||
case "hysteria2":
|
case "hysteria2":
|
||||||
|
// 后端的 "hysteria" 实际上是 Hysteria2 协议
|
||||||
|
print('🔍 构建 Hysteria2 节点: ${nodeListItem.name}');
|
||||||
|
print(' - serverAddr: ${nodeListItem.serverAddr}');
|
||||||
|
print(' - port: ${nodeListItem.port}');
|
||||||
|
print(' - uuid: ${nodeListItem.uuid}');
|
||||||
|
|
||||||
|
//判断是否为域名
|
||||||
|
final bool isDomain = !RegExp(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$')
|
||||||
|
.hasMatch(nodeListItem.serverAddr);
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
"type": "hysteria2",
|
"type": "hysteria2",
|
||||||
"tag": nodeListItem.name,
|
"tag": nodeListItem.name,
|
||||||
"server": nodeListItem.serverAddr,
|
"server": nodeListItem.serverAddr,
|
||||||
"server_port": nodeListItem.port,
|
"server_port": nodeListItem.port,
|
||||||
"password": nodeListItem.uuid,
|
"password": nodeListItem.uuid,
|
||||||
"up_mbps": 100,
|
|
||||||
"down_mbps": 100,
|
|
||||||
"tls": {
|
"tls": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"server_name": nodeListItem.serverAddr,
|
if (isDomain) "server_name": nodeListItem.serverAddr,
|
||||||
"insecure": false,
|
|
||||||
"alpn": ["h3"]
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
print('✅ Hysteria2 节点配置构建成功: ${nodeListItem.name}');
|
print('✅ Hysteria2 节点配置构建成功');
|
||||||
print('📄 完整配置: $config');
|
print('📄 完整配置: ${jsonEncode(config)}');
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
print('⚠️ 不支持的协议类型: ${nodeListItem.protocol}');
|
print('⚠️ 不支持的协议类型: ${nodeListItem.protocol}');
|
||||||
|
|||||||
@ -103,9 +103,6 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
|||||||
// 添加最后的地图中心点
|
// 添加最后的地图中心点
|
||||||
final kr_lastMapCenter = LatLng(35.0, 105.0).obs;
|
final kr_lastMapCenter = LatLng(35.0, 105.0).obs;
|
||||||
|
|
||||||
// 添加一个标志来防止重复操作
|
|
||||||
bool kr_isSwitching = false;
|
|
||||||
|
|
||||||
// 为"闪连"Checkbox添加一个响应式变量,默认为 false
|
// 为"闪连"Checkbox添加一个响应式变量,默认为 false
|
||||||
final isQuickConnectEnabled = false.obs;
|
final isQuickConnectEnabled = false.obs;
|
||||||
|
|
||||||
@ -114,7 +111,7 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
|||||||
|
|
||||||
// 闪连状态存储键
|
// 闪连状态存储键
|
||||||
static const String _quickConnectKey = 'kr_quick_connect_enabled';
|
static const String _quickConnectKey = 'kr_quick_connect_enabled';
|
||||||
|
|
||||||
// 国家内节点重选相关属性
|
// 国家内节点重选相关属性
|
||||||
final RxString currentSelectedCountry = ''.obs;
|
final RxString currentSelectedCountry = ''.obs;
|
||||||
final RxBool isCountryReselectionEnabled = true.obs;
|
final RxBool isCountryReselectionEnabled = true.obs;
|
||||||
@ -206,7 +203,7 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
|||||||
KRLogUtil.kr_i('开始执行闪连自动连接', tag: 'QuickConnect');
|
KRLogUtil.kr_i('开始执行闪连自动连接', tag: 'QuickConnect');
|
||||||
|
|
||||||
// 防止重复操作
|
// 防止重复操作
|
||||||
if (kr_isSwitching) {
|
if (KRSingBoxImp.instance.kr_status == SingboxStarted) {
|
||||||
KRLogUtil.kr_w('连接操作正在进行中,跳过自动连接', tag: 'QuickConnect');
|
KRLogUtil.kr_w('连接操作正在进行中,跳过自动连接', tag: 'QuickConnect');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -503,6 +500,7 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
|||||||
void _bindConnectionStatus() {
|
void _bindConnectionStatus() {
|
||||||
// 添加更详细的状态监听
|
// 添加更详细的状态监听
|
||||||
ever(KRSingBoxImp.instance.kr_status, (status) {
|
ever(KRSingBoxImp.instance.kr_status, (status) {
|
||||||
|
print('🔵 Controller 收到状态变化: ${status.runtimeType}');
|
||||||
KRLogUtil.kr_i('🔄 连接状态变化: $status', tag: 'HomeController');
|
KRLogUtil.kr_i('🔄 连接状态变化: $status', tag: 'HomeController');
|
||||||
KRLogUtil.kr_i('📊 当前状态类型: ${status.runtimeType}', tag: 'HomeController');
|
KRLogUtil.kr_i('📊 当前状态类型: ${status.runtimeType}', tag: 'HomeController');
|
||||||
|
|
||||||
@ -594,7 +592,7 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
|||||||
|
|
||||||
// 强制更新UI
|
// 强制更新UI
|
||||||
update();
|
update();
|
||||||
|
|
||||||
// 检查是否需要进行国家内节点重选
|
// 检查是否需要进行国家内节点重选
|
||||||
_checkCountryReselection(value);
|
_checkCountryReselection(value);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -656,40 +654,79 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
|||||||
***/
|
***/
|
||||||
}
|
}
|
||||||
|
|
||||||
void kr_toggleSwitch(bool value) async {
|
/// 🔧 重构: 参考 hiddify-app 的 toggleConnection 实现
|
||||||
// 如果正在切换中,直接返回
|
Future<void> kr_toggleSwitch(bool value) async {
|
||||||
if (kr_isSwitching) {
|
final currentStatus = KRSingBoxImp.instance.kr_status.value;
|
||||||
KRLogUtil.kr_i('正在切换中,忽略本次操作', tag: 'HomeController');
|
|
||||||
|
KRLogUtil.kr_i('🔵 toggleSwitch 被调用: value=$value, currentStatus=$currentStatus', tag: 'HomeController');
|
||||||
|
print('🔵 toggleSwitch: value=$value, currentStatus=$currentStatus');
|
||||||
|
|
||||||
|
// 🔧 关键: 如果正在切换中,直接忽略(参考 hiddify-app 的 "switching status, debounce")
|
||||||
|
if (currentStatus is SingboxStarting || currentStatus is SingboxStopping) {
|
||||||
|
KRLogUtil.kr_i('🔄 正在切换中,忽略本次操作 (当前状态: $currentStatus)', tag: 'HomeController');
|
||||||
|
print('🔵 忽略操作:正在切换中');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
kr_isSwitching = true;
|
|
||||||
if (value) {
|
if (value) {
|
||||||
|
// 开启连接
|
||||||
|
KRLogUtil.kr_i('🔄 开始连接...', tag: 'HomeController');
|
||||||
|
print('🔵 执行 kr_start()');
|
||||||
await KRSingBoxImp.instance.kr_start();
|
await KRSingBoxImp.instance.kr_start();
|
||||||
|
KRLogUtil.kr_i('✅ 连接命令已发送', tag: 'HomeController');
|
||||||
|
print('🔵 kr_start() 完成');
|
||||||
|
|
||||||
// 启动成功后立即同步一次,确保UI及时更新
|
// 🔧 修复: 等待状态更新,最多3秒
|
||||||
Future.delayed(const Duration(milliseconds: 300), () {
|
await _waitForStatus(SingboxStarted, maxSeconds: 3);
|
||||||
kr_forceSyncConnectionStatus();
|
|
||||||
});
|
|
||||||
|
|
||||||
// 再次延迟验证,确保状态稳定
|
|
||||||
Future.delayed(const Duration(seconds: 2), () {
|
|
||||||
kr_forceSyncConnectionStatus();
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
await KRSingBoxImp.instance.kr_stop();
|
// 关闭连接
|
||||||
|
KRLogUtil.kr_i('🛑 开始断开连接...', tag: 'HomeController');
|
||||||
|
print('🔵 执行 kr_stop()');
|
||||||
|
await KRSingBoxImp.instance.kr_stop().timeout(
|
||||||
|
const Duration(seconds: 10),
|
||||||
|
onTimeout: () {
|
||||||
|
KRLogUtil.kr_e('⚠️ 停止操作超时', tag: 'HomeController');
|
||||||
|
throw TimeoutException('Stop operation timeout');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
KRLogUtil.kr_i('✅ 断开命令已发送', tag: 'HomeController');
|
||||||
|
print('🔵 kr_stop() 完成');
|
||||||
|
|
||||||
|
// 🔧 修复: 等待状态更新,最多2秒
|
||||||
|
await _waitForStatus(SingboxStopped, maxSeconds: 2);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
KRLogUtil.kr_e('切换失败: $e', tag: 'HomeController');
|
KRLogUtil.kr_e('❌ 切换失败: $e', tag: 'HomeController');
|
||||||
// 当启动失败时(如VPN权限被拒绝),强制同步状态
|
print('🔵 切换失败: $e');
|
||||||
Future.delayed(const Duration(milliseconds: 100), () {
|
// 发生错误时强制同步状态
|
||||||
kr_forceSyncConnectionStatus();
|
kr_forceSyncConnectionStatus();
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
// 确保在任何情况下都会重置标志
|
|
||||||
kr_isSwitching = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
print('🔵 toggleSwitch 完成,当前 kr_isConnected=${kr_isConnected.value}');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 🔧 等待状态达到预期值
|
||||||
|
Future<void> _waitForStatus(Type expectedType, {int maxSeconds = 3}) async {
|
||||||
|
print('🔵 等待状态变为: $expectedType');
|
||||||
|
final startTime = DateTime.now();
|
||||||
|
|
||||||
|
while (DateTime.now().difference(startTime).inSeconds < maxSeconds) {
|
||||||
|
final currentStatus = KRSingBoxImp.instance.kr_status.value;
|
||||||
|
print('🔵 当前状态: ${currentStatus.runtimeType}');
|
||||||
|
|
||||||
|
if (currentStatus.runtimeType == expectedType) {
|
||||||
|
print('🔵 状态已达到: $expectedType');
|
||||||
|
// 强制同步确保 kr_isConnected 正确
|
||||||
|
kr_forceSyncConnectionStatus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
print('🔵 等待超时,强制同步状态');
|
||||||
|
kr_forceSyncConnectionStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 处理选择器代理
|
/// 处理选择器代理
|
||||||
@ -900,35 +937,35 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
|||||||
// 检查延迟是否有效(小于65535且大于0)
|
// 检查延迟是否有效(小于65535且大于0)
|
||||||
return node.urlTestDelay.value < 65535 && node.urlTestDelay.value > 0;
|
return node.urlTestDelay.value < 65535 && node.urlTestDelay.value > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 检查是否需要进行国家内节点重选
|
/// 检查是否需要进行国家内节点重选
|
||||||
void _checkCountryReselection(List<dynamic> activeGroups) {
|
void _checkCountryReselection(List<dynamic> activeGroups) {
|
||||||
// 如果未启用国家内重选功能,直接返回
|
// 如果未启用国家内重选功能,直接返回
|
||||||
if (!isCountryReselectionEnabled.value) {
|
if (!isCountryReselectionEnabled.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果未连接,直接返回
|
// 如果未连接,直接返回
|
||||||
if (!kr_isConnected.value) {
|
if (!kr_isConnected.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果没有选择国家,直接返回
|
// 如果没有选择国家,直接返回
|
||||||
if (currentSelectedCountry.isEmpty) {
|
if (currentSelectedCountry.isEmpty) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 获取当前节点信息
|
// 获取当前节点信息
|
||||||
final currentNodeInfo = kr_getRealConnectedNodeInfo();
|
final currentNodeInfo = kr_getRealConnectedNodeInfo();
|
||||||
final currentDelay = currentNodeInfo['delay'] as int;
|
final currentDelay = currentNodeInfo['delay'] as int;
|
||||||
|
|
||||||
// 检查当前节点延迟是否超过阈值
|
// 检查当前节点延迟是否超过阈值
|
||||||
if (currentDelay > 0 && currentDelay < countryReselectionLatencyThreshold) {
|
if (currentDelay > 0 && currentDelay < countryReselectionLatencyThreshold) {
|
||||||
// 延迟正常,无需重选
|
// 延迟正常,无需重选
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果延迟超过阈值或无效,尝试在当前国家内重选
|
// 如果延迟超过阈值或无效,尝试在当前国家内重选
|
||||||
if (currentDelay >= countryReselectionLatencyThreshold || currentDelay <= 0) {
|
if (currentDelay >= countryReselectionLatencyThreshold || currentDelay <= 0) {
|
||||||
KRLogUtil.kr_w('🔄 当前节点延迟过高(${currentDelay}ms),尝试国家内重选', tag: 'HomeController');
|
KRLogUtil.kr_w('🔄 当前节点延迟过高(${currentDelay}ms),尝试国家内重选', tag: 'HomeController');
|
||||||
@ -938,7 +975,7 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
|||||||
KRLogUtil.kr_e('检查国家内重选时出错: $e', tag: 'HomeController');
|
KRLogUtil.kr_e('检查国家内重选时出错: $e', tag: 'HomeController');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 执行国家内节点重选
|
/// 执行国家内节点重选
|
||||||
void _performCountryReselection(String country) {
|
void _performCountryReselection(String country) {
|
||||||
try {
|
try {
|
||||||
@ -946,16 +983,16 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
|||||||
final countryNodes = kr_subscribeService.allList
|
final countryNodes = kr_subscribeService.allList
|
||||||
.where((node) => node.country == country && node.tag != 'auto')
|
.where((node) => node.country == country && node.tag != 'auto')
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
if (countryNodes.isEmpty) {
|
if (countryNodes.isEmpty) {
|
||||||
KRLogUtil.kr_w('⚠️ 国家 $country 内没有可用节点', tag: 'HomeController');
|
KRLogUtil.kr_w('⚠️ 国家 $country 内没有可用节点', tag: 'HomeController');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 找到延迟最小的有效节点
|
// 找到延迟最小的有效节点
|
||||||
String? bestNode;
|
String? bestNode;
|
||||||
int minDelay = 65535;
|
int minDelay = 65535;
|
||||||
|
|
||||||
for (var node in countryNodes) {
|
for (var node in countryNodes) {
|
||||||
final delay = node.urlTestDelay.value;
|
final delay = node.urlTestDelay.value;
|
||||||
if (delay > 0 && delay < 65535 && delay < minDelay) {
|
if (delay > 0 && delay < 65535 && delay < minDelay) {
|
||||||
@ -963,7 +1000,7 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
|||||||
bestNode = node.tag;
|
bestNode = node.tag;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bestNode != null && bestNode != kr_cutSeletedTag.value) {
|
if (bestNode != null && bestNode != kr_cutSeletedTag.value) {
|
||||||
KRLogUtil.kr_i('🎯 国家内重选: $bestNode (${minDelay}ms)', tag: 'HomeController');
|
KRLogUtil.kr_i('🎯 国家内重选: $bestNode (${minDelay}ms)', tag: 'HomeController');
|
||||||
kr_selectNode(bestNode);
|
kr_selectNode(bestNode);
|
||||||
@ -974,33 +1011,33 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
|||||||
KRLogUtil.kr_e('执行国家内重选时出错: $e', tag: 'HomeController');
|
KRLogUtil.kr_e('执行国家内重选时出错: $e', tag: 'HomeController');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 设置当前选择的国家(由hi_node_list_controller调用)
|
/// 设置当前选择的国家(由hi_node_list_controller调用)
|
||||||
void setCurrentSelectedCountry(String country) {
|
void setCurrentSelectedCountry(String country) {
|
||||||
currentSelectedCountry.value = country;
|
currentSelectedCountry.value = country;
|
||||||
KRLogUtil.kr_i('🌍 设置当前选择国家: $country', tag: 'HomeController');
|
KRLogUtil.kr_i('🌍 设置当前选择国家: $country', tag: 'HomeController');
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 启用/禁用国家内重选功能
|
/// 启用/禁用国家内重选功能
|
||||||
void setCountryReselectionEnabled(bool enabled) {
|
void setCountryReselectionEnabled(bool enabled) {
|
||||||
isCountryReselectionEnabled.value = enabled;
|
isCountryReselectionEnabled.value = enabled;
|
||||||
KRLogUtil.kr_i('🔄 国家内重选功能已${enabled ? "启用" : "禁用"}', tag: 'HomeController');
|
KRLogUtil.kr_i('🔄 国家内重选功能已${enabled ? "启用" : "禁用"}', tag: 'HomeController');
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取当前国家内的节点统计信息
|
/// 获取当前国家内的节点统计信息
|
||||||
Map<String, dynamic> getCurrentCountryNodeStats() {
|
Map<String, dynamic> getCurrentCountryNodeStats() {
|
||||||
if (currentSelectedCountry.isEmpty) {
|
if (currentSelectedCountry.isEmpty) {
|
||||||
return {'error': '没有选择国家'};
|
return {'error': '没有选择国家'};
|
||||||
}
|
}
|
||||||
|
|
||||||
final countryNodes = kr_subscribeService.allList
|
final countryNodes = kr_subscribeService.allList
|
||||||
.where((node) => node.country == currentSelectedCountry.value && node.tag != 'auto')
|
.where((node) => node.country == currentSelectedCountry.value && node.tag != 'auto')
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
if (countryNodes.isEmpty) {
|
if (countryNodes.isEmpty) {
|
||||||
return {'error': '国家内没有节点'};
|
return {'error': '国家内没有节点'};
|
||||||
}
|
}
|
||||||
|
|
||||||
List<int> validDelays = [];
|
List<int> validDelays = [];
|
||||||
int totalNodes = countryNodes.length;
|
int totalNodes = countryNodes.length;
|
||||||
int validNodes = 0;
|
int validNodes = 0;
|
||||||
@ -1008,27 +1045,27 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
|||||||
int maxDelay = 0;
|
int maxDelay = 0;
|
||||||
String? fastestNode;
|
String? fastestNode;
|
||||||
String? slowestNode;
|
String? slowestNode;
|
||||||
|
|
||||||
for (var node in countryNodes) {
|
for (var node in countryNodes) {
|
||||||
final delay = node.urlTestDelay.value;
|
final delay = node.urlTestDelay.value;
|
||||||
if (delay > 0 && delay < 65535) {
|
if (delay > 0 && delay < 65535) {
|
||||||
validDelays.add(delay);
|
validDelays.add(delay);
|
||||||
validNodes++;
|
validNodes++;
|
||||||
|
|
||||||
if (delay < minDelay) {
|
if (delay < minDelay) {
|
||||||
minDelay = delay;
|
minDelay = delay;
|
||||||
fastestNode = node.tag;
|
fastestNode = node.tag;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (delay > maxDelay) {
|
if (delay > maxDelay) {
|
||||||
maxDelay = delay;
|
maxDelay = delay;
|
||||||
slowestNode = node.tag;
|
slowestNode = node.tag;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
double avgDelay = validDelays.isEmpty ? 0 : validDelays.reduce((a, b) => a + b) / validDelays.length;
|
double avgDelay = validDelays.isEmpty ? 0 : validDelays.reduce((a, b) => a + b) / validDelays.length;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'country': currentSelectedCountry.value,
|
'country': currentSelectedCountry.value,
|
||||||
'totalNodes': totalNodes,
|
'totalNodes': totalNodes,
|
||||||
@ -1088,10 +1125,11 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
|||||||
// 更新连接信息
|
// 更新连接信息
|
||||||
kr_updateConnectionInfo();
|
kr_updateConnectionInfo();
|
||||||
|
|
||||||
|
// 🔧 修复:只有在核心已启动时才选择节点,避免触发重启
|
||||||
if (KRSingBoxImp.instance.kr_status.value == SingboxStarted()) {
|
if (KRSingBoxImp.instance.kr_status.value == SingboxStarted()) {
|
||||||
KRSingBoxImp.instance.kr_selectOutbound(tag);
|
KRSingBoxImp.instance.kr_selectOutbound(tag);
|
||||||
|
|
||||||
// 🔧 修复:选择节点后启动延迟值更新(带超时保护)
|
// 🔧 修复:选择节点后启动延迟值更新(带超时保护)
|
||||||
Future.delayed(const Duration(milliseconds: 500), () {
|
Future.delayed(const Duration(milliseconds: 500), () {
|
||||||
if (kr_currentNodeLatency.value == -1 && kr_isConnected.value) {
|
if (kr_currentNodeLatency.value == -1 && kr_isConnected.value) {
|
||||||
KRLogUtil.kr_w('⚠️ 选择节点后延迟值未更新,尝试手动更新', tag: 'HomeController');
|
KRLogUtil.kr_w('⚠️ 选择节点后延迟值未更新,尝试手动更新', tag: 'HomeController');
|
||||||
@ -1101,7 +1139,13 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
KRSingBoxImp().kr_start();
|
// 🔧 修复:核心未启动时,仍需保存用户选择,以便启动VPN时应用
|
||||||
|
KRLogUtil.kr_i('💾 核心未启动,保存节点选择以便稍后应用: $tag', tag: 'HomeController');
|
||||||
|
KRSecureStorage().kr_saveData(key: 'SELECTED_NODE_TAG', value: tag).then((_) {
|
||||||
|
KRLogUtil.kr_i('✅ 节点选择已保存: $tag', tag: 'HomeController');
|
||||||
|
}).catchError((e) {
|
||||||
|
KRLogUtil.kr_e('❌ 保存节点选择失败: $e', tag: 'HomeController');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 移动到选中的节点
|
// 移动到选中的节点
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import 'package:kaer_with_panels/app/widgets/kr_country_flag.dart';
|
|||||||
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
|
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
|
||||||
import 'package:kaer_with_panels/app/localization/app_translations.dart';
|
import 'package:kaer_with_panels/app/localization/app_translations.dart';
|
||||||
import 'package:kaer_with_panels/app/services/singbox_imp/kr_sing_box_imp.dart';
|
import 'package:kaer_with_panels/app/services/singbox_imp/kr_sing_box_imp.dart';
|
||||||
|
import 'package:kaer_with_panels/singbox/model/singbox_status.dart';
|
||||||
import '../controllers/kr_home_controller.dart';
|
import '../controllers/kr_home_controller.dart';
|
||||||
import '../models/kr_home_views_status.dart';
|
import '../models/kr_home_views_status.dart';
|
||||||
|
|
||||||
@ -209,13 +210,31 @@ class KRHomeConnectionInfoView extends GetView<KRHomeController> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
CupertinoSwitch(
|
// 🔧 修复: 使用多层监听确保状态更新
|
||||||
value: controller.kr_isConnected.value,
|
Obx(() {
|
||||||
onChanged: (bool value) {
|
// 🔧 关键: 强制读取两个 observable 确保追踪
|
||||||
controller.kr_toggleSwitch(value);
|
final _ = KRSingBoxImp.instance.kr_status.value; // 强制追踪
|
||||||
},
|
final isConnected = controller.kr_isConnected.value; // 使用 controller 的状态
|
||||||
activeColor: Colors.blue,
|
|
||||||
),
|
// 再次读取状态用于判断
|
||||||
|
final status = KRSingBoxImp.instance.kr_status.value;
|
||||||
|
final isSwitching = status is SingboxStarting || status is SingboxStopping;
|
||||||
|
|
||||||
|
// 🔧 调试日志
|
||||||
|
print('🔵 Switch UI 更新: status=${status.runtimeType}, isConnected=$isConnected, isSwitching=$isSwitching');
|
||||||
|
|
||||||
|
return CupertinoSwitch(
|
||||||
|
value: isConnected,
|
||||||
|
// 🔧 关键: 切换中时 onChanged 为 null,Switch 自动禁用
|
||||||
|
onChanged: isSwitching
|
||||||
|
? null
|
||||||
|
: (bool value) {
|
||||||
|
print('🔵 Switch onChanged 触发: 请求=$value, 当前状态=$status');
|
||||||
|
controller.kr_toggleSwitch(value);
|
||||||
|
},
|
||||||
|
activeColor: Colors.blue,
|
||||||
|
);
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@ -72,7 +72,7 @@ abstract class Api {
|
|||||||
// static const String kr_getInviteData = "/v1/public/invite/code";
|
// static const String kr_getInviteData = "/v1/public/invite/code";
|
||||||
|
|
||||||
/// 配置信息
|
/// 配置信息
|
||||||
static const String kr_config = "/v1/app/auth/config";
|
static const String kr_config = "/v1/common/site/config";
|
||||||
|
|
||||||
/// 获取用户在线时长统计
|
/// 获取用户在线时长统计
|
||||||
static const String kr_getUserOnlineTimeStatistics =
|
static const String kr_getUserOnlineTimeStatistics =
|
||||||
|
|||||||
@ -123,7 +123,7 @@ class KRUserApi {
|
|||||||
await HttpUtil.getInstance().request<KRConfigData>(
|
await HttpUtil.getInstance().request<KRConfigData>(
|
||||||
Api.kr_config,
|
Api.kr_config,
|
||||||
data,
|
data,
|
||||||
method: HttpMethod.POST,
|
method: HttpMethod.GET,
|
||||||
isShowLoading: false,
|
isShowLoading: false,
|
||||||
);
|
);
|
||||||
if (!baseResponse.isSuccess) {
|
if (!baseResponse.isSuccess) {
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:device_info_plus/device_info_plus.dart';
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter_udid/flutter_udid.dart';
|
||||||
import '../utils/kr_secure_storage.dart';
|
import '../utils/kr_secure_storage.dart';
|
||||||
import '../utils/kr_log_util.dart';
|
import '../utils/kr_log_util.dart';
|
||||||
import 'package:crypto/crypto.dart';
|
import 'package:crypto/crypto.dart';
|
||||||
@ -51,37 +52,28 @@ class KRDeviceInfoService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 获取设备唯一标识
|
/// 获取设备唯一标识
|
||||||
|
/// 使用多因子组合策略确保唯一性和稳定性
|
||||||
Future<String> _getDeviceId() async {
|
Future<String> _getDeviceId() async {
|
||||||
try {
|
try {
|
||||||
String? identifier;
|
String identifier;
|
||||||
|
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
final androidInfo = await _deviceInfo.androidInfo;
|
identifier = await _getAndroidDeviceId();
|
||||||
// Android使用androidId作为唯一标识
|
|
||||||
identifier = androidInfo.id;
|
|
||||||
} else if (Platform.isIOS) {
|
} else if (Platform.isIOS) {
|
||||||
final iosInfo = await _deviceInfo.iosInfo;
|
identifier = await _getIOSDeviceId();
|
||||||
// iOS使用identifierForVendor作为唯一标识
|
|
||||||
identifier = iosInfo.identifierForVendor;
|
|
||||||
} else if (Platform.isMacOS) {
|
} else if (Platform.isMacOS) {
|
||||||
final macInfo = await _deviceInfo.macOsInfo;
|
identifier = await _getMacOSDeviceId();
|
||||||
// macOS使用systemGUID
|
|
||||||
identifier = macInfo.systemGUID;
|
|
||||||
} else if (Platform.isWindows) {
|
} else if (Platform.isWindows) {
|
||||||
final windowsInfo = await _deviceInfo.windowsInfo;
|
identifier = await _getWindowsDeviceId();
|
||||||
// Windows使用计算机名作为唯一标识
|
|
||||||
identifier = windowsInfo.computerName;
|
|
||||||
} else if (Platform.isLinux) {
|
} else if (Platform.isLinux) {
|
||||||
final linuxInfo = await _deviceInfo.linuxInfo;
|
identifier = await _getLinuxDeviceId();
|
||||||
// Linux使用machineId
|
|
||||||
identifier = linuxInfo.machineId;
|
|
||||||
} else {
|
} else {
|
||||||
// Web或其他平台,使用生成的UUID
|
// Web或其他平台,使用生成的UUID
|
||||||
identifier = await _getOrCreateStoredDeviceId();
|
identifier = await _getOrCreateStoredDeviceId();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果获取失败,使用存储的或生成新的ID
|
// 如果获取失败,使用存储的或生成新的ID
|
||||||
if (identifier == null || identifier.isEmpty) {
|
if (identifier.isEmpty) {
|
||||||
identifier = await _getOrCreateStoredDeviceId();
|
identifier = await _getOrCreateStoredDeviceId();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,11 +81,201 @@ class KRDeviceInfoService {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('❌ 获取设备ID失败: $e');
|
print('❌ 获取设备ID失败: $e');
|
||||||
KRLogUtil.kr_e('❌ 获取设备ID失败 - $e', tag: 'KRDeviceInfoService');
|
KRLogUtil.kr_e('❌ 获取设备ID失败 - $e', tag: 'KRDeviceInfoService');
|
||||||
// 如果获取失败,返回存储的或生成新的ID
|
// 如果获取失败,返回存储的或生成新的ID
|
||||||
return await _getOrCreateStoredDeviceId();
|
return await _getOrCreateStoredDeviceId();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Android设备ID - 多因子组合
|
||||||
|
/// 组合: AndroidID + 设备型号 + 主板信息 + 硬件信息
|
||||||
|
Future<String> _getAndroidDeviceId() async {
|
||||||
|
try {
|
||||||
|
final androidInfo = await _deviceInfo.androidInfo;
|
||||||
|
|
||||||
|
// 优先使用 flutter_udid (封装了多种Android标识获取方式)
|
||||||
|
String udid = await FlutterUdid.consistentUdid;
|
||||||
|
|
||||||
|
// 构建多因子字符串
|
||||||
|
final factors = [
|
||||||
|
udid,
|
||||||
|
androidInfo.id, // Android ID
|
||||||
|
androidInfo.board, // 主板
|
||||||
|
androidInfo.bootloader, // Bootloader
|
||||||
|
androidInfo.brand, // 品牌
|
||||||
|
androidInfo.device, // 设备名
|
||||||
|
androidInfo.fingerprint, // 系统指纹
|
||||||
|
androidInfo.hardware, // 硬件名
|
||||||
|
androidInfo.manufacturer, // 制造商
|
||||||
|
androidInfo.model, // 型号
|
||||||
|
androidInfo.product, // 产品名
|
||||||
|
];
|
||||||
|
|
||||||
|
// 过滤空值并组合
|
||||||
|
final combined = factors
|
||||||
|
.where((f) => f != null && f.isNotEmpty)
|
||||||
|
.join('|');
|
||||||
|
|
||||||
|
// 生成SHA256哈希
|
||||||
|
final bytes = utf8.encode(combined);
|
||||||
|
final hash = sha256.convert(bytes);
|
||||||
|
|
||||||
|
print('📱 Android多因子ID生成 - 因子数: ${factors.where((f) => f != null && f.isNotEmpty).length}');
|
||||||
|
KRLogUtil.kr_i('📱 Android多因子ID - $hash', tag: 'KRDeviceInfoService');
|
||||||
|
|
||||||
|
return hash.toString();
|
||||||
|
} catch (e) {
|
||||||
|
print('❌ Android设备ID获取失败: $e');
|
||||||
|
KRLogUtil.kr_e('❌ Android设备ID获取失败 - $e', tag: 'KRDeviceInfoService');
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// iOS设备ID - 优先使用identifierForVendor,配合持久化存储
|
||||||
|
/// iOS限制较严,无法获取IMEI等敏感信息
|
||||||
|
Future<String> _getIOSDeviceId() async {
|
||||||
|
try {
|
||||||
|
final iosInfo = await _deviceInfo.iosInfo;
|
||||||
|
|
||||||
|
// 优先使用 flutter_udid
|
||||||
|
String udid = await FlutterUdid.consistentUdid;
|
||||||
|
|
||||||
|
// 构建多因子字符串
|
||||||
|
final factors = [
|
||||||
|
udid,
|
||||||
|
iosInfo.identifierForVendor ?? '', // Vendor标识
|
||||||
|
iosInfo.model, // 型号
|
||||||
|
iosInfo.systemName, // 系统名
|
||||||
|
iosInfo.systemVersion, // 系统版本
|
||||||
|
iosInfo.name, // 设备名(如"iPhone 14 Pro")
|
||||||
|
iosInfo.utsname.machine, // 机器类型
|
||||||
|
];
|
||||||
|
|
||||||
|
final combined = factors
|
||||||
|
.where((f) => f.isNotEmpty)
|
||||||
|
.join('|');
|
||||||
|
|
||||||
|
final bytes = utf8.encode(combined);
|
||||||
|
final hash = sha256.convert(bytes);
|
||||||
|
|
||||||
|
print('📱 iOS多因子ID生成 - 因子数: ${factors.where((f) => f.isNotEmpty).length}');
|
||||||
|
KRLogUtil.kr_i('📱 iOS多因子ID - $hash', tag: 'KRDeviceInfoService');
|
||||||
|
|
||||||
|
return hash.toString();
|
||||||
|
} catch (e) {
|
||||||
|
print('❌ iOS设备ID获取失败: $e');
|
||||||
|
KRLogUtil.kr_e('❌ iOS设备ID获取失败 - $e', tag: 'KRDeviceInfoService');
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// macOS设备ID - 使用硬件UUID
|
||||||
|
Future<String> _getMacOSDeviceId() async {
|
||||||
|
try {
|
||||||
|
final macInfo = await _deviceInfo.macOsInfo;
|
||||||
|
|
||||||
|
// 优先使用 flutter_udid
|
||||||
|
String udid = await FlutterUdid.consistentUdid;
|
||||||
|
|
||||||
|
// 构建多因子字符串
|
||||||
|
final factors = [
|
||||||
|
udid,
|
||||||
|
macInfo.systemGUID ?? '', // 系统GUID (最稳定)
|
||||||
|
macInfo.model, // 型号
|
||||||
|
macInfo.hostName, // 主机名
|
||||||
|
macInfo.arch, // 架构
|
||||||
|
macInfo.kernelVersion, // 内核版本
|
||||||
|
];
|
||||||
|
|
||||||
|
final combined = factors
|
||||||
|
.where((f) => f.isNotEmpty)
|
||||||
|
.join('|');
|
||||||
|
|
||||||
|
final bytes = utf8.encode(combined);
|
||||||
|
final hash = sha256.convert(bytes);
|
||||||
|
|
||||||
|
print('📱 macOS多因子ID生成 - 因子数: ${factors.where((f) => f.isNotEmpty).length}');
|
||||||
|
KRLogUtil.kr_i('📱 macOS多因子ID - $hash', tag: 'KRDeviceInfoService');
|
||||||
|
|
||||||
|
return hash.toString();
|
||||||
|
} catch (e) {
|
||||||
|
print('❌ macOS设备ID获取失败: $e');
|
||||||
|
KRLogUtil.kr_e('❌ macOS设备ID获取失败 - $e', tag: 'KRDeviceInfoService');
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Windows设备ID - 使用机器GUID
|
||||||
|
Future<String> _getWindowsDeviceId() async {
|
||||||
|
try {
|
||||||
|
final windowsInfo = await _deviceInfo.windowsInfo;
|
||||||
|
|
||||||
|
// 优先使用 flutter_udid
|
||||||
|
String udid = await FlutterUdid.consistentUdid;
|
||||||
|
|
||||||
|
// 构建多因子字符串
|
||||||
|
final factors = [
|
||||||
|
udid,
|
||||||
|
windowsInfo.deviceId, // 设备ID
|
||||||
|
windowsInfo.computerName, // 计算机名
|
||||||
|
windowsInfo.productName, // 产品名
|
||||||
|
windowsInfo.numberOfCores.toString(), // CPU核心数
|
||||||
|
windowsInfo.systemMemoryInMegabytes.toString(), // 内存大小
|
||||||
|
];
|
||||||
|
|
||||||
|
final combined = factors
|
||||||
|
.where((f) => f.isNotEmpty)
|
||||||
|
.join('|');
|
||||||
|
|
||||||
|
final bytes = utf8.encode(combined);
|
||||||
|
final hash = sha256.convert(bytes);
|
||||||
|
|
||||||
|
print('📱 Windows多因子ID生成 - 因子数: ${factors.where((f) => f.isNotEmpty).length}');
|
||||||
|
KRLogUtil.kr_i('📱 Windows多因子ID - $hash', tag: 'KRDeviceInfoService');
|
||||||
|
|
||||||
|
return hash.toString();
|
||||||
|
} catch (e) {
|
||||||
|
print('❌ Windows设备ID获取失败: $e');
|
||||||
|
KRLogUtil.kr_e('❌ Windows设备ID获取失败 - $e', tag: 'KRDeviceInfoService');
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Linux设备ID - 使用machine-id
|
||||||
|
Future<String> _getLinuxDeviceId() async {
|
||||||
|
try {
|
||||||
|
final linuxInfo = await _deviceInfo.linuxInfo;
|
||||||
|
|
||||||
|
// 优先使用 flutter_udid
|
||||||
|
String udid = await FlutterUdid.consistentUdid;
|
||||||
|
|
||||||
|
// 构建多因子字符串
|
||||||
|
final factors = [
|
||||||
|
udid,
|
||||||
|
linuxInfo.machineId ?? '', // Machine ID (最稳定)
|
||||||
|
linuxInfo.id, // 发行版ID
|
||||||
|
linuxInfo.name, // 发行版名称
|
||||||
|
linuxInfo.version ?? '', // 版本
|
||||||
|
linuxInfo.variant ?? '', // 变体
|
||||||
|
];
|
||||||
|
|
||||||
|
final combined = factors
|
||||||
|
.where((f) => f.isNotEmpty)
|
||||||
|
.join('|');
|
||||||
|
|
||||||
|
final bytes = utf8.encode(combined);
|
||||||
|
final hash = sha256.convert(bytes);
|
||||||
|
|
||||||
|
print('📱 Linux多因子ID生成 - 因子数: ${factors.where((f) => f.isNotEmpty).length}');
|
||||||
|
KRLogUtil.kr_i('📱 Linux多因子ID - $hash', tag: 'KRDeviceInfoService');
|
||||||
|
|
||||||
|
return hash.toString();
|
||||||
|
} catch (e) {
|
||||||
|
print('❌ Linux设备ID获取失败: $e');
|
||||||
|
KRLogUtil.kr_e('❌ Linux设备ID获取失败 - $e', tag: 'KRDeviceInfoService');
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 获取或创建存储的设备ID
|
/// 获取或创建存储的设备ID
|
||||||
Future<String> _getOrCreateStoredDeviceId() async {
|
Future<String> _getOrCreateStoredDeviceId() async {
|
||||||
try {
|
try {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -23,17 +23,73 @@ class KRSecureStorage {
|
|||||||
// 初始化 Hive
|
// 初始化 Hive
|
||||||
Future<void> kr_initHive() async {
|
Future<void> kr_initHive() async {
|
||||||
try {
|
try {
|
||||||
if (Platform.isMacOS) {
|
// 根据不同平台指定数据库路径
|
||||||
|
if (Platform.isMacOS || Platform.isWindows || Platform.isLinux) {
|
||||||
final baseDir = await getApplicationSupportDirectory();
|
final baseDir = await getApplicationSupportDirectory();
|
||||||
|
KRLogUtil.kr_i('初始化 Hive,路径: ${baseDir.path}', tag: 'SecureStorage');
|
||||||
|
|
||||||
|
// 确保目录存在
|
||||||
|
if (!baseDir.existsSync()) {
|
||||||
|
await baseDir.create(recursive: true);
|
||||||
|
KRLogUtil.kr_i('已创建 Hive 目录: ${baseDir.path}', tag: 'SecureStorage');
|
||||||
|
}
|
||||||
|
|
||||||
await Hive.initFlutter(baseDir.path);
|
await Hive.initFlutter(baseDir.path);
|
||||||
} else {
|
} else {
|
||||||
|
// Android 和 iOS 使用默认路径
|
||||||
await Hive.initFlutter();
|
await Hive.initFlutter();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用加密适配器
|
// 使用加密适配器
|
||||||
final key = HiveAesCipher(_generateKey());
|
final key = HiveAesCipher(_generateKey());
|
||||||
await Hive.openBox(_boxName, encryptionCipher: key);
|
await Hive.openBox(_boxName, encryptionCipher: key);
|
||||||
} catch (e) {
|
KRLogUtil.kr_i('Hive 初始化成功', tag: 'SecureStorage');
|
||||||
|
} catch (e, stackTrace) {
|
||||||
KRLogUtil.kr_e('初始化 Hive 失败: $e', tag: 'SecureStorage');
|
KRLogUtil.kr_e('初始化 Hive 失败: $e', tag: 'SecureStorage');
|
||||||
|
KRLogUtil.kr_e('错误类型: ${e.runtimeType}', tag: 'SecureStorage');
|
||||||
|
KRLogUtil.kr_e('错误堆栈: $stackTrace', tag: 'SecureStorage');
|
||||||
|
|
||||||
|
// 对于 Windows 和 Linux,如果初始化失败,尝试删除旧文件并重试
|
||||||
|
if (Platform.isWindows || Platform.isLinux) {
|
||||||
|
try {
|
||||||
|
KRLogUtil.kr_i('尝试清理并重新初始化 Hive', tag: 'SecureStorage');
|
||||||
|
final baseDir = await getApplicationSupportDirectory();
|
||||||
|
final hiveDir = Directory(baseDir.path);
|
||||||
|
|
||||||
|
// 查找并删除相关的 Hive 文件
|
||||||
|
if (hiveDir.existsSync()) {
|
||||||
|
final files = hiveDir.listSync();
|
||||||
|
for (var entity in files) {
|
||||||
|
if (entity is File) {
|
||||||
|
final fileName = entity.path.split(Platform.pathSeparator).last;
|
||||||
|
// 删除 Hive 数据库文件(通常是 .hive 或 .lock 文件)
|
||||||
|
if (fileName.startsWith(_boxName) ||
|
||||||
|
fileName.endsWith('.hive') ||
|
||||||
|
fileName.endsWith('.lock')) {
|
||||||
|
try {
|
||||||
|
await entity.delete();
|
||||||
|
KRLogUtil.kr_i('已删除文件: $fileName', tag: 'SecureStorage');
|
||||||
|
} catch (deleteError) {
|
||||||
|
KRLogUtil.kr_e('删除文件失败: $deleteError', tag: 'SecureStorage');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新初始化
|
||||||
|
await Hive.initFlutter(baseDir.path);
|
||||||
|
final key = HiveAesCipher(_generateKey());
|
||||||
|
await Hive.openBox(_boxName, encryptionCipher: key);
|
||||||
|
KRLogUtil.kr_i('Hive 重新初始化成功', tag: 'SecureStorage');
|
||||||
|
} catch (retryError, retryStack) {
|
||||||
|
KRLogUtil.kr_e('重新初始化 Hive 仍然失败: $retryError', tag: 'SecureStorage');
|
||||||
|
KRLogUtil.kr_e('重试堆栈: $retryStack', tag: 'SecureStorage');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,13 +3,11 @@ import 'dart:convert';
|
|||||||
import 'dart:ffi';
|
import 'dart:ffi';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:isolate';
|
import 'dart:isolate';
|
||||||
|
import 'package:combine/combine.dart';
|
||||||
// import 'package:combine/combine.dart'; // 暂时注释掉,使用 Isolate.run 替代
|
|
||||||
import 'package:ffi/ffi.dart';
|
import 'package:ffi/ffi.dart';
|
||||||
import 'package:fpdart/fpdart.dart';
|
import 'package:fpdart/fpdart.dart';
|
||||||
import 'package:kaer_with_panels/core/model/directories.dart';
|
import 'package:kaer_with_panels/core/model/directories.dart';
|
||||||
import 'package:kaer_with_panels/gen/singbox_generated_bindings.dart';
|
import 'package:kaer_with_panels/gen/singbox_generated_bindings.dart';
|
||||||
|
|
||||||
import 'package:kaer_with_panels/singbox/model/singbox_config_option.dart';
|
import 'package:kaer_with_panels/singbox/model/singbox_config_option.dart';
|
||||||
import 'package:kaer_with_panels/singbox/model/singbox_outbound.dart';
|
import 'package:kaer_with_panels/singbox/model/singbox_outbound.dart';
|
||||||
import 'package:kaer_with_panels/singbox/model/singbox_stats.dart';
|
import 'package:kaer_with_panels/singbox/model/singbox_stats.dart';
|
||||||
@ -50,14 +48,10 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
||||||
Future<void> init() async {
|
Future<void> init() async {
|
||||||
loggy.debug("initializing");
|
loggy.debug("initializing");
|
||||||
_statusReceiver = ReceivePort('service status receiver');
|
_statusReceiver = ReceivePort('service status receiver');
|
||||||
final source = _statusReceiver
|
final source = _statusReceiver.asBroadcastStream().map((event) => jsonDecode(event as String)).map(SingboxStatus.fromEvent);
|
||||||
.asBroadcastStream()
|
|
||||||
.map((event) => jsonDecode(event as String))
|
|
||||||
.map(SingboxStatus.fromEvent);
|
|
||||||
_status = ValueConnectableStream.seeded(
|
_status = ValueConnectableStream.seeded(
|
||||||
source,
|
source,
|
||||||
const SingboxStopped(),
|
const SingboxStopped(),
|
||||||
@ -71,7 +65,7 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
|||||||
) {
|
) {
|
||||||
final port = _statusReceiver.sendPort.nativePort;
|
final port = _statusReceiver.sendPort.nativePort;
|
||||||
return TaskEither(
|
return TaskEither(
|
||||||
() => Isolate.run(
|
() => CombineWorker().execute(
|
||||||
() {
|
() {
|
||||||
_box.setupOnce(NativeApi.initializeApiDLData);
|
_box.setupOnce(NativeApi.initializeApiDLData);
|
||||||
final err = _box
|
final err = _box
|
||||||
@ -100,7 +94,7 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
|||||||
bool debug,
|
bool debug,
|
||||||
) {
|
) {
|
||||||
return TaskEither(
|
return TaskEither(
|
||||||
() => Isolate.run(
|
() => CombineWorker().execute(
|
||||||
() {
|
() {
|
||||||
final err = _box
|
final err = _box
|
||||||
.parse(
|
.parse(
|
||||||
@ -122,13 +116,10 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
|||||||
@override
|
@override
|
||||||
TaskEither<String, Unit> changeOptions(SingboxConfigOption options) {
|
TaskEither<String, Unit> changeOptions(SingboxConfigOption options) {
|
||||||
return TaskEither(
|
return TaskEither(
|
||||||
() => Isolate.run(
|
() => CombineWorker().execute(
|
||||||
() {
|
() {
|
||||||
final json = jsonEncode(options.toJson());
|
final json = jsonEncode(options.toJson());
|
||||||
final err = _box
|
final err = _box.changeHiddifyOptions(json.toNativeUtf8().cast()).cast<Utf8>().toDartString();
|
||||||
.changeHiddifyOptions(json.toNativeUtf8().cast())
|
|
||||||
.cast<Utf8>()
|
|
||||||
.toDartString();
|
|
||||||
if (err.isNotEmpty) {
|
if (err.isNotEmpty) {
|
||||||
return left(err);
|
return left(err);
|
||||||
}
|
}
|
||||||
@ -143,7 +134,7 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
|||||||
String path,
|
String path,
|
||||||
) {
|
) {
|
||||||
return TaskEither(
|
return TaskEither(
|
||||||
() => Isolate.run(
|
() => CombineWorker().execute(
|
||||||
() {
|
() {
|
||||||
final response = _box
|
final response = _box
|
||||||
.generateConfig(
|
.generateConfig(
|
||||||
@ -168,7 +159,7 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
|||||||
) {
|
) {
|
||||||
loggy.debug("starting, memory limit: [${!disableMemoryLimit}]");
|
loggy.debug("starting, memory limit: [${!disableMemoryLimit}]");
|
||||||
return TaskEither(
|
return TaskEither(
|
||||||
() => Isolate.run(
|
() => CombineWorker().execute(
|
||||||
() {
|
() {
|
||||||
final err = _box
|
final err = _box
|
||||||
.start(
|
.start(
|
||||||
@ -189,7 +180,7 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
|||||||
@override
|
@override
|
||||||
TaskEither<String, Unit> stop() {
|
TaskEither<String, Unit> stop() {
|
||||||
return TaskEither(
|
return TaskEither(
|
||||||
() => Isolate.run(
|
() => CombineWorker().execute(
|
||||||
() {
|
() {
|
||||||
final err = _box.stop().cast<Utf8>().toDartString();
|
final err = _box.stop().cast<Utf8>().toDartString();
|
||||||
if (err.isNotEmpty) {
|
if (err.isNotEmpty) {
|
||||||
@ -209,7 +200,7 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
|||||||
) {
|
) {
|
||||||
loggy.debug("restarting, memory limit: [${!disableMemoryLimit}]");
|
loggy.debug("restarting, memory limit: [${!disableMemoryLimit}]");
|
||||||
return TaskEither(
|
return TaskEither(
|
||||||
() => Isolate.run(
|
() => CombineWorker().execute(
|
||||||
() {
|
() {
|
||||||
final err = _box
|
final err = _box
|
||||||
.restart(
|
.restart(
|
||||||
@ -267,10 +258,7 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
final err = _box
|
final err = _box.startCommandClient(1, receiver.sendPort.nativePort).cast<Utf8>().toDartString();
|
||||||
.startCommandClient(1, receiver.sendPort.nativePort)
|
|
||||||
.cast<Utf8>()
|
|
||||||
.toDartString();
|
|
||||||
if (err.isNotEmpty) {
|
if (err.isNotEmpty) {
|
||||||
loggy.error("error starting status command: $err");
|
loggy.error("error starting status command: $err");
|
||||||
throw err;
|
throw err;
|
||||||
@ -312,10 +300,7 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final err = _box
|
final err = _box.startCommandClient(5, receiver.sendPort.nativePort).cast<Utf8>().toDartString();
|
||||||
.startCommandClient(5, receiver.sendPort.nativePort)
|
|
||||||
.cast<Utf8>()
|
|
||||||
.toDartString();
|
|
||||||
if (err.isNotEmpty) {
|
if (err.isNotEmpty) {
|
||||||
logger.error("error starting group command: $err");
|
logger.error("error starting group command: $err");
|
||||||
throw err;
|
throw err;
|
||||||
@ -359,10 +344,7 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final err = _box
|
final err = _box.startCommandClient(13, receiver.sendPort.nativePort).cast<Utf8>().toDartString();
|
||||||
.startCommandClient(13, receiver.sendPort.nativePort)
|
|
||||||
.cast<Utf8>()
|
|
||||||
.toDartString();
|
|
||||||
if (err.isNotEmpty) {
|
if (err.isNotEmpty) {
|
||||||
logger.error("error starting: $err");
|
logger.error("error starting: $err");
|
||||||
throw err;
|
throw err;
|
||||||
@ -378,7 +360,7 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
|||||||
@override
|
@override
|
||||||
TaskEither<String, Unit> selectOutbound(String groupTag, String outboundTag) {
|
TaskEither<String, Unit> selectOutbound(String groupTag, String outboundTag) {
|
||||||
return TaskEither(
|
return TaskEither(
|
||||||
() => Isolate.run(
|
() => CombineWorker().execute(
|
||||||
() {
|
() {
|
||||||
final err = _box
|
final err = _box
|
||||||
.selectOutbound(
|
.selectOutbound(
|
||||||
@ -399,12 +381,9 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
|||||||
@override
|
@override
|
||||||
TaskEither<String, Unit> urlTest(String groupTag) {
|
TaskEither<String, Unit> urlTest(String groupTag) {
|
||||||
return TaskEither(
|
return TaskEither(
|
||||||
() => Isolate.run(
|
() => CombineWorker().execute(
|
||||||
() {
|
() {
|
||||||
final err = _box
|
final err = _box.urlTest(groupTag.toNativeUtf8().cast()).cast<Utf8>().toDartString();
|
||||||
.urlTest(groupTag.toNativeUtf8().cast())
|
|
||||||
.cast<Utf8>()
|
|
||||||
.toDartString();
|
|
||||||
if (err.isNotEmpty) {
|
if (err.isNotEmpty) {
|
||||||
return left(err);
|
return left(err);
|
||||||
}
|
}
|
||||||
@ -420,9 +399,7 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
|||||||
@override
|
@override
|
||||||
Stream<List<String>> watchLogs(String path) async* {
|
Stream<List<String>> watchLogs(String path) async* {
|
||||||
yield await _readLogFile(File(path));
|
yield await _readLogFile(File(path));
|
||||||
yield* Watcher(path, pollingDelay: const Duration(seconds: 1))
|
yield* Watcher(path, pollingDelay: const Duration(seconds: 1)).events.asyncMap((event) async {
|
||||||
.events
|
|
||||||
.asyncMap((event) async {
|
|
||||||
if (event.type == ChangeType.MODIFY) {
|
if (event.type == ChangeType.MODIFY) {
|
||||||
await _readLogFile(File(path));
|
await _readLogFile(File(path));
|
||||||
}
|
}
|
||||||
@ -433,7 +410,7 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
|||||||
@override
|
@override
|
||||||
TaskEither<String, Unit> clearLogs() {
|
TaskEither<String, Unit> clearLogs() {
|
||||||
return TaskEither(
|
return TaskEither(
|
||||||
() => Isolate.run(
|
() => CombineWorker().execute(
|
||||||
() {
|
() {
|
||||||
_logBuffer.clear();
|
_logBuffer.clear();
|
||||||
return right(unit);
|
return right(unit);
|
||||||
@ -444,8 +421,7 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
|||||||
|
|
||||||
Future<List<String>> _readLogFile(File file) async {
|
Future<List<String>> _readLogFile(File file) async {
|
||||||
if (_logFilePosition == 0 && file.lengthSync() == 0) return [];
|
if (_logFilePosition == 0 && file.lengthSync() == 0) return [];
|
||||||
final content =
|
final content = await file.openRead(_logFilePosition).transform(utf8.decoder).join();
|
||||||
await file.openRead(_logFilePosition).transform(utf8.decoder).join();
|
|
||||||
_logFilePosition = file.lengthSync();
|
_logFilePosition = file.lengthSync();
|
||||||
final lines = const LineSplitter().convert(content);
|
final lines = const LineSplitter().convert(content);
|
||||||
if (lines.length > 300) {
|
if (lines.length > 300) {
|
||||||
@ -468,7 +444,7 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
|||||||
}) {
|
}) {
|
||||||
loggy.debug("generating warp config");
|
loggy.debug("generating warp config");
|
||||||
return TaskEither(
|
return TaskEither(
|
||||||
() => Isolate.run(
|
() => CombineWorker().execute(
|
||||||
() {
|
() {
|
||||||
final response = _box
|
final response = _box
|
||||||
.generateWarpConfig(
|
.generateWarpConfig(
|
||||||
|
|||||||
2
libcore
2
libcore
@ -1 +1 @@
|
|||||||
Subproject commit f993a57755c37e08b02042037cbbf508c66c51f9
|
Subproject commit 4e7fe336554e87880be08fddbfc778cfc5f6b9f1
|
||||||
@ -137,6 +137,6 @@ MyApplication *my_application_new()
|
|||||||
{
|
{
|
||||||
return MY_APPLICATION(g_object_new(my_application_get_type(),
|
return MY_APPLICATION(g_object_new(my_application_get_type(),
|
||||||
"application-id", APPLICATION_ID,
|
"application-id", APPLICATION_ID,
|
||||||
"flags", G_APPLICATION_FLAGS_NONE,
|
"flags", G_APPLICATION_DEFAULT_FLAGS,
|
||||||
nullptr));
|
nullptr));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -82,7 +82,7 @@ SPEC CHECKSUMS:
|
|||||||
device_info_plus: 4fb280989f669696856f8b129e4a5e3cd6c48f76
|
device_info_plus: 4fb280989f669696856f8b129e4a5e3cd6c48f76
|
||||||
flutter_inappwebview_macos: c2d68649f9f8f1831bfcd98d73fd6256366d9d1d
|
flutter_inappwebview_macos: c2d68649f9f8f1831bfcd98d73fd6256366d9d1d
|
||||||
flutter_udid: d26e455e8c06174e6aff476e147defc6cae38495
|
flutter_udid: d26e455e8c06174e6aff476e147defc6cae38495
|
||||||
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
|
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
|
||||||
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
|
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
|
||||||
package_info_plus: f0052d280d17aa382b932f399edf32507174e870
|
package_info_plus: f0052d280d17aa382b932f399edf32507174e870
|
||||||
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
||||||
|
|||||||
800
pubspec.lock
Executable file → Normal file
800
pubspec.lock
Executable file → Normal file
File diff suppressed because it is too large
Load Diff
@ -61,7 +61,7 @@ dependencies:
|
|||||||
fpdart: ^1.1.0
|
fpdart: ^1.1.0
|
||||||
dartx: ^1.2.0
|
dartx: ^1.2.0
|
||||||
rxdart: ^0.27.7
|
rxdart: ^0.27.7
|
||||||
# combine: ^0.5.8 # 暂时移除,使用 Isolate.run 替代
|
combine: 0.5.7 # 精确版本,兼容 Flutter 3.24.3(与 hiddify-app 相同)
|
||||||
encrypt: ^5.0.0
|
encrypt: ^5.0.0
|
||||||
path: ^1.8.3
|
path: ^1.8.3
|
||||||
path_provider: ^2.1.1
|
path_provider: ^2.1.1
|
||||||
@ -140,6 +140,7 @@ dev_dependencies:
|
|||||||
flutter:
|
flutter:
|
||||||
assets:
|
assets:
|
||||||
- assets/images/
|
- assets/images/
|
||||||
|
- assets/geosite/
|
||||||
- assets/translations/strings_en.i18n.json
|
- assets/translations/strings_en.i18n.json
|
||||||
- assets/translations/strings_zh.i18n.json
|
- assets/translations/strings_zh.i18n.json
|
||||||
- assets/translations/strings_es.i18n.json
|
- assets/translations/strings_es.i18n.json
|
||||||
|
|||||||
@ -2,6 +2,12 @@
|
|||||||
cmake_minimum_required(VERSION 3.14)
|
cmake_minimum_required(VERSION 3.14)
|
||||||
project(BearVPN LANGUAGES CXX)
|
project(BearVPN LANGUAGES CXX)
|
||||||
|
|
||||||
|
# 设置 CMake 策略以兼容旧版本插件
|
||||||
|
# CMP0175: add_custom_command() 拒绝无效参数(用于兼容 flutter_inappwebview_windows 插件)
|
||||||
|
if(POLICY CMP0175)
|
||||||
|
cmake_policy(SET CMP0175 OLD)
|
||||||
|
endif()
|
||||||
|
|
||||||
# 设置静态链接
|
# 设置静态链接
|
||||||
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
|
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
|
||||||
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /MT")
|
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /MT")
|
||||||
@ -11,6 +17,8 @@ set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_PROFILE} /MT")
|
|||||||
# 添加编码和警告处理
|
# 添加编码和警告处理
|
||||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /utf-8")
|
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /utf-8")
|
||||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4819 /wd4244 /wd4458")
|
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4819 /wd4244 /wd4458")
|
||||||
|
# 添加 /FS 标志以支持并行编译时的 PDB 文件访问(解决 C1041 错误)
|
||||||
|
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /FS")
|
||||||
|
|
||||||
# The name of the executable created for the application. Change this to change
|
# The name of the executable created for the application. Change this to change
|
||||||
# the on-disk name of your application.
|
# the on-disk name of your application.
|
||||||
@ -50,7 +58,7 @@ add_definitions(-DUNICODE -D_UNICODE)
|
|||||||
function(APPLY_STANDARD_SETTINGS TARGET)
|
function(APPLY_STANDARD_SETTINGS TARGET)
|
||||||
target_compile_features(${TARGET} PUBLIC cxx_std_17)
|
target_compile_features(${TARGET} PUBLIC cxx_std_17)
|
||||||
target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100" /wd"4819" /wd"4244" /wd"4458")
|
target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100" /wd"4819" /wd"4244" /wd"4458")
|
||||||
target_compile_options(${TARGET} PRIVATE /EHsc /utf-8)
|
target_compile_options(${TARGET} PRIVATE /EHsc /utf-8 /FS)
|
||||||
target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0")
|
target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0")
|
||||||
target_compile_definitions(${TARGET} PRIVATE "$<$<CONFIG:Debug>:_DEBUG>")
|
target_compile_definitions(${TARGET} PRIVATE "$<$<CONFIG:Debug>:_DEBUG>")
|
||||||
# 确保目标使用静态链接
|
# 确保目标使用静态链接
|
||||||
@ -70,6 +78,14 @@ add_subdirectory("runner")
|
|||||||
# them to the application.
|
# them to the application.
|
||||||
include(flutter/generated_plugins.cmake)
|
include(flutter/generated_plugins.cmake)
|
||||||
|
|
||||||
|
# 为所有插件应用 /FS 标志,解决 PDB 文件锁冲突问题
|
||||||
|
# 这确保 flutter_inappwebview_windows 等插件能够正确编译
|
||||||
|
foreach(plugin ${FLUTTER_PLUGIN_LIST})
|
||||||
|
if(TARGET ${plugin}_plugin)
|
||||||
|
target_compile_options(${plugin}_plugin PRIVATE /FS)
|
||||||
|
endif()
|
||||||
|
endforeach(plugin)
|
||||||
|
|
||||||
|
|
||||||
# === Installation ===
|
# === Installation ===
|
||||||
# Support files are copied into place next to the executable, so that it can
|
# Support files are copied into place next to the executable, so that it can
|
||||||
@ -84,6 +100,8 @@ 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)
|
||||||
|
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}"
|
||||||
COMPONENT Runtime)
|
COMPONENT Runtime)
|
||||||
@ -94,14 +112,19 @@ install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}
|
|||||||
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||||
COMPONENT Runtime)
|
COMPONENT Runtime)
|
||||||
|
|
||||||
# install(FILES "../libcore/bin/libcore.dll" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
# 安装 libcore.dll 到可执行文件目录(与 BearVPN.exe 同级)
|
||||||
# COMPONENT Runtime)
|
install(FILES "../libcore/bin/libcore.dll"
|
||||||
|
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||||
|
COMPONENT Runtime
|
||||||
|
OPTIONAL)
|
||||||
|
|
||||||
install(FILES "../libcore/bin/libcore.dll" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
# 安装 BearVPNCli.exe(从 libcore/bin 复制并重命名)
|
||||||
COMPONENT Runtime RENAME libcore.dll)
|
# 注意:libcore 编译的是 HiddifyCli.exe,打包脚本会自动重命名为 BearVPNCli.exe
|
||||||
|
# 这里需要安装 BearVPNCli.exe,因为它已经被重命名了
|
||||||
install(FILES "../libcore/bin/BearVPNCli.exe" DESTINATION "${CMAKE_INSTALL_PREFIX}"
|
install(FILES "../libcore/bin/BearVPNCli.exe"
|
||||||
COMPONENT Runtime RENAME BearVPNCli.exe)
|
DESTINATION "${CMAKE_INSTALL_PREFIX}"
|
||||||
|
COMPONENT Runtime
|
||||||
|
OPTIONAL)
|
||||||
|
|
||||||
|
|
||||||
if(PLUGIN_BUNDLED_LIBRARIES)
|
if(PLUGIN_BUNDLED_LIBRARIES)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user