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
|
||||
default: 'https://xpp5.oss-ap-southeast-1.aliyuncs.com/bear1.txt'
|
||||
type: string
|
||||
encryption_key:
|
||||
description: '加密密钥'
|
||||
required: true
|
||||
default: 'c0qhq99a-nq8h-ropg-wrlc-ezj4dlkxqpzx'
|
||||
type: string
|
||||
platforms:
|
||||
description: '构建平台 (多选: android,windows,macos,linux)'
|
||||
description: '构建平台 (多选: android,windows,macos,linux,ios)'
|
||||
required: true
|
||||
default: 'android,windows,macos,linux'
|
||||
type: string
|
||||
|
||||
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) ====================
|
||||
build-libcore-android:
|
||||
name: 编译 libcore (Android)
|
||||
@ -293,7 +344,7 @@ jobs:
|
||||
name: libcore-android
|
||||
path: android/app/libs/
|
||||
|
||||
- name: ⚙️ 配置 API 和 OSS
|
||||
- name: ⚙️ 配置 API、OSS 和加密密钥
|
||||
run: |
|
||||
CONFIG_FILE="lib/app/common/app_config.dart"
|
||||
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_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' }}"
|
||||
|
||||
echo "🔧 配置参数:"
|
||||
echo " API: $API_DOMAIN"
|
||||
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|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"
|
||||
|
||||
echo "✅ 配置完成"
|
||||
|
||||
@ -370,9 +427,70 @@ jobs:
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
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
|
||||
run: |
|
||||
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_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' }}"
|
||||
|
||||
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: |
|
||||
@ -415,10 +537,70 @@ jobs:
|
||||
}
|
||||
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)
|
||||
run: |
|
||||
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
|
||||
shell: bash
|
||||
run: |
|
||||
@ -466,7 +648,7 @@ jobs:
|
||||
run: |
|
||||
chmod +x libcore/bin/HiddifyCli
|
||||
|
||||
- name: ⚙️ 配置 API 和 OSS
|
||||
- name: ⚙️ 配置 API、OSS 和加密密钥
|
||||
run: |
|
||||
CONFIG_FILE="lib/app/common/app_config.dart"
|
||||
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_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' }}"
|
||||
|
||||
# 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|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"
|
||||
|
||||
echo "✅ 配置完成"
|
||||
|
||||
- name: 📦 安装 Flutter 依赖
|
||||
run: |
|
||||
@ -549,7 +746,7 @@ jobs:
|
||||
run: |
|
||||
chmod +x libcore/bin/HiddifyCli
|
||||
|
||||
- name: ⚙️ 配置 API 和 OSS
|
||||
- name: ⚙️ 配置 API、OSS 和加密密钥
|
||||
run: |
|
||||
CONFIG_FILE="lib/app/common/app_config.dart"
|
||||
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_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' }}"
|
||||
|
||||
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: |
|
||||
@ -588,10 +789,90 @@ jobs:
|
||||
path: BearVPN-linux-*.tar.gz
|
||||
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 ====================
|
||||
create-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
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
|
||||
@ -614,6 +895,7 @@ jobs:
|
||||
| **Android** | arm64-v8a | BearVPN-android-arm64-v8a-*.apk |
|
||||
| **Android** | armeabi-v7a | BearVPN-android-armeabi-v7a-*.apk |
|
||||
| **Android** | x86_64 | BearVPN-android-x86_64-*.apk |
|
||||
| **iOS** | Universal | BearVPN-ios-*.ipa |
|
||||
| **Windows** | x64 | BearVPN-windows-x64-*.zip |
|
||||
| **macOS** | Universal | BearVPN-macos-*.zip |
|
||||
| **Linux** | x64 | BearVPN-linux-x64-*.tar.gz |
|
||||
@ -628,6 +910,7 @@ jobs:
|
||||
### 📥 安装指南
|
||||
|
||||
**Android:** 下载 APK 直接安装
|
||||
**iOS:** 下载 IPA 使用 AltStore/Sideloadly 安装
|
||||
**Windows:** 解压 ZIP 运行 BearVPN.exe
|
||||
**macOS:** 解压 ZIP 拖拽到应用程序
|
||||
**Linux:** 解压 tar.gz 运行可执行文件
|
||||
@ -650,6 +933,7 @@ jobs:
|
||||
with:
|
||||
files: |
|
||||
artifacts/android-apk/*.apk
|
||||
artifacts/ios-app/*.ipa
|
||||
artifacts/windows-x64/*.zip
|
||||
artifacts/macos-app/*.zip
|
||||
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:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: windows-latest
|
||||
# 先编译 libcore
|
||||
build-libcore:
|
||||
name: 编译 libcore (Windows)
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
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
|
||||
uses: subosito/flutter-action@v2
|
||||
@ -26,6 +123,9 @@ jobs:
|
||||
- name: Get dependencies
|
||||
run: flutter pub get
|
||||
|
||||
- name: Generate code
|
||||
run: dart run build_runner build --delete-conflicting-outputs
|
||||
|
||||
- name: Build Windows Debug
|
||||
run: flutter build windows
|
||||
|
||||
|
||||
9
.gitignore
vendored
9
.gitignore
vendored
@ -58,7 +58,6 @@ app.*.map.json
|
||||
/android/app/profile
|
||||
/android/app/release
|
||||
|
||||
|
||||
/data
|
||||
/.gradle/
|
||||
|
||||
@ -99,3 +98,11 @@ libcore/*.aar
|
||||
|
||||
# Android 编译产物
|
||||
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;
|
||||
try {
|
||||
// Debug 模式下直接使用固定地址,跳过所有配置请求和域名切换逻辑
|
||||
// if (kDebugMode) {
|
||||
// KRLogUtil.kr_i('🐛 Debug 模式,使用固定 API 地址,跳过配置请求', tag: 'AppConfig');
|
||||
// if (onSuccess != null) {
|
||||
// await onSuccess();
|
||||
// }
|
||||
// return;
|
||||
// }
|
||||
if (onSuccess != null) {
|
||||
await onSuccess();
|
||||
}
|
||||
// await _startAutoRetry(onSuccess);
|
||||
// 所有模式都走正常的配置请求流程
|
||||
KRLogUtil.kr_i('🚀 开始配置初始化', tag: 'AppConfig');
|
||||
await _startAutoRetry(onSuccess);
|
||||
} finally {
|
||||
_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/kr_announcement_service.dart';
|
||||
import '../services/singbox_imp/kr_sing_box_imp.dart';
|
||||
import '../services/kr_site_config_service.dart';
|
||||
import '../utils/kr_event_bus.dart';
|
||||
import '../../singbox/model/singbox_status.dart';
|
||||
|
||||
|
||||
@ -92,7 +92,7 @@ class KROutboundItem {
|
||||
"tls": {
|
||||
"enabled": json["security"] == "tls",
|
||||
"server_name": serverName,
|
||||
"insecure": securityConfig["allow_insecure"] ?? false,
|
||||
"insecure": securityConfig["allow_insecure"] ?? true,
|
||||
"utls": {
|
||||
"enabled": true,
|
||||
"fingerprint": securityConfig["fingerprint"] ?? "chrome"
|
||||
@ -123,7 +123,7 @@ class KROutboundItem {
|
||||
"tls": {
|
||||
"enabled": json["security"] == "tls",
|
||||
"server_name": serverName,
|
||||
"insecure": securityConfig["allow_insecure"] ?? false,
|
||||
"insecure": securityConfig["allow_insecure"] ?? true,
|
||||
"utls": {"enabled": true, "fingerprint": "chrome"}
|
||||
}
|
||||
};
|
||||
@ -138,7 +138,9 @@ class KROutboundItem {
|
||||
"password": nodeListItem.uuid
|
||||
};
|
||||
break;
|
||||
case "hysteria":
|
||||
case "hysteria2":
|
||||
// 后端的 "hysteria" 实际上是 Hysteria2 协议
|
||||
final securityConfig =
|
||||
json["security_config"] as Map<String, dynamic>? ?? {};
|
||||
config = {
|
||||
@ -156,8 +158,7 @@ class KROutboundItem {
|
||||
"tls": {
|
||||
"enabled": true,
|
||||
"server_name": securityConfig["sni"] ?? "",
|
||||
"insecure": securityConfig["allow_insecure"] ?? false,
|
||||
"alpn": ["h3"]
|
||||
"insecure": securityConfig["allow_insecure"] ?? true
|
||||
}
|
||||
};
|
||||
break;
|
||||
@ -181,7 +182,7 @@ class KROutboundItem {
|
||||
"tls": {
|
||||
"enabled": json["security"] == "tls",
|
||||
"server_name": serverName,
|
||||
"insecure": securityConfig["allow_insecure"] ?? false,
|
||||
"insecure": securityConfig["allow_insecure"] ?? true,
|
||||
"utls": {"enabled": true, "fingerprint": "chrome"}
|
||||
}
|
||||
};
|
||||
@ -260,6 +261,10 @@ class KROutboundItem {
|
||||
print('📄 完整配置: $config');
|
||||
break;
|
||||
case "vless":
|
||||
// 判断是否为域名(非IP地址)
|
||||
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,
|
||||
@ -268,8 +273,8 @@ class KROutboundItem {
|
||||
"uuid": nodeListItem.uuid,
|
||||
"tls": {
|
||||
"enabled": true,
|
||||
"server_name": nodeListItem.serverAddr,
|
||||
"insecure": false,
|
||||
if (isDomain) "server_name": nodeListItem.serverAddr,
|
||||
"insecure": true,
|
||||
"utls": {
|
||||
"enabled": true,
|
||||
"fingerprint": "chrome"
|
||||
@ -280,6 +285,10 @@ class KROutboundItem {
|
||||
print('📄 完整配置: $config');
|
||||
break;
|
||||
case "vmess":
|
||||
// 判断是否为域名(非IP地址)
|
||||
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,
|
||||
@ -290,8 +299,8 @@ class KROutboundItem {
|
||||
"security": "auto",
|
||||
"tls": {
|
||||
"enabled": true,
|
||||
"server_name": nodeListItem.serverAddr,
|
||||
"insecure": false,
|
||||
if (isDomain) "server_name": nodeListItem.serverAddr,
|
||||
"insecure": true,
|
||||
"utls": {"enabled": true, "fingerprint": "chrome"}
|
||||
}
|
||||
};
|
||||
@ -299,6 +308,10 @@ class KROutboundItem {
|
||||
print('📄 完整配置: $config');
|
||||
break;
|
||||
case "trojan":
|
||||
// 判断是否为域名(非IP地址)
|
||||
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,
|
||||
@ -307,32 +320,39 @@ class KROutboundItem {
|
||||
"password": nodeListItem.uuid,
|
||||
"tls": {
|
||||
"enabled": true,
|
||||
"server_name": nodeListItem.serverAddr,
|
||||
"insecure": false,
|
||||
if (isDomain) "server_name": nodeListItem.serverAddr,
|
||||
"insecure": true,
|
||||
"utls": {"enabled": true, "fingerprint": "chrome"}
|
||||
}
|
||||
};
|
||||
print('✅ Trojan 节点配置构建成功: ${nodeListItem.name}');
|
||||
print('📄 完整配置: $config');
|
||||
break;
|
||||
case "hysteria":
|
||||
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 = {
|
||||
"type": "hysteria2",
|
||||
"tag": nodeListItem.name,
|
||||
"server": nodeListItem.serverAddr,
|
||||
"server_port": nodeListItem.port,
|
||||
"password": nodeListItem.uuid,
|
||||
"up_mbps": 100,
|
||||
"down_mbps": 100,
|
||||
"tls": {
|
||||
"enabled": true,
|
||||
"server_name": nodeListItem.serverAddr,
|
||||
"insecure": false,
|
||||
"alpn": ["h3"]
|
||||
if (isDomain) "server_name": nodeListItem.serverAddr,
|
||||
}
|
||||
};
|
||||
print('✅ Hysteria2 节点配置构建成功: ${nodeListItem.name}');
|
||||
print('📄 完整配置: $config');
|
||||
print('✅ Hysteria2 节点配置构建成功');
|
||||
print('📄 完整配置: ${jsonEncode(config)}');
|
||||
break;
|
||||
default:
|
||||
print('⚠️ 不支持的协议类型: ${nodeListItem.protocol}');
|
||||
|
||||
@ -103,9 +103,6 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
// 添加最后的地图中心点
|
||||
final kr_lastMapCenter = LatLng(35.0, 105.0).obs;
|
||||
|
||||
// 添加一个标志来防止重复操作
|
||||
bool kr_isSwitching = false;
|
||||
|
||||
// 为"闪连"Checkbox添加一个响应式变量,默认为 false
|
||||
final isQuickConnectEnabled = false.obs;
|
||||
|
||||
@ -114,7 +111,7 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
|
||||
// 闪连状态存储键
|
||||
static const String _quickConnectKey = 'kr_quick_connect_enabled';
|
||||
|
||||
|
||||
// 国家内节点重选相关属性
|
||||
final RxString currentSelectedCountry = ''.obs;
|
||||
final RxBool isCountryReselectionEnabled = true.obs;
|
||||
@ -206,7 +203,7 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
KRLogUtil.kr_i('开始执行闪连自动连接', tag: 'QuickConnect');
|
||||
|
||||
// 防止重复操作
|
||||
if (kr_isSwitching) {
|
||||
if (KRSingBoxImp.instance.kr_status == SingboxStarted) {
|
||||
KRLogUtil.kr_w('连接操作正在进行中,跳过自动连接', tag: 'QuickConnect');
|
||||
return;
|
||||
}
|
||||
@ -503,6 +500,7 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
void _bindConnectionStatus() {
|
||||
// 添加更详细的状态监听
|
||||
ever(KRSingBoxImp.instance.kr_status, (status) {
|
||||
print('🔵 Controller 收到状态变化: ${status.runtimeType}');
|
||||
KRLogUtil.kr_i('🔄 连接状态变化: $status', tag: 'HomeController');
|
||||
KRLogUtil.kr_i('📊 当前状态类型: ${status.runtimeType}', tag: 'HomeController');
|
||||
|
||||
@ -594,7 +592,7 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
|
||||
// 强制更新UI
|
||||
update();
|
||||
|
||||
|
||||
// 检查是否需要进行国家内节点重选
|
||||
_checkCountryReselection(value);
|
||||
} catch (e) {
|
||||
@ -656,40 +654,79 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
***/
|
||||
}
|
||||
|
||||
void kr_toggleSwitch(bool value) async {
|
||||
// 如果正在切换中,直接返回
|
||||
if (kr_isSwitching) {
|
||||
KRLogUtil.kr_i('正在切换中,忽略本次操作', tag: 'HomeController');
|
||||
/// 🔧 重构: 参考 hiddify-app 的 toggleConnection 实现
|
||||
Future<void> kr_toggleSwitch(bool value) async {
|
||||
final currentStatus = KRSingBoxImp.instance.kr_status.value;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
try {
|
||||
kr_isSwitching = true;
|
||||
if (value) {
|
||||
// 开启连接
|
||||
KRLogUtil.kr_i('🔄 开始连接...', tag: 'HomeController');
|
||||
print('🔵 执行 kr_start()');
|
||||
await KRSingBoxImp.instance.kr_start();
|
||||
KRLogUtil.kr_i('✅ 连接命令已发送', tag: 'HomeController');
|
||||
print('🔵 kr_start() 完成');
|
||||
|
||||
// 启动成功后立即同步一次,确保UI及时更新
|
||||
Future.delayed(const Duration(milliseconds: 300), () {
|
||||
kr_forceSyncConnectionStatus();
|
||||
});
|
||||
|
||||
// 再次延迟验证,确保状态稳定
|
||||
Future.delayed(const Duration(seconds: 2), () {
|
||||
kr_forceSyncConnectionStatus();
|
||||
});
|
||||
// 🔧 修复: 等待状态更新,最多3秒
|
||||
await _waitForStatus(SingboxStarted, maxSeconds: 3);
|
||||
} 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) {
|
||||
KRLogUtil.kr_e('切换失败: $e', tag: 'HomeController');
|
||||
// 当启动失败时(如VPN权限被拒绝),强制同步状态
|
||||
Future.delayed(const Duration(milliseconds: 100), () {
|
||||
kr_forceSyncConnectionStatus();
|
||||
});
|
||||
} finally {
|
||||
// 确保在任何情况下都会重置标志
|
||||
kr_isSwitching = false;
|
||||
KRLogUtil.kr_e('❌ 切换失败: $e', tag: 'HomeController');
|
||||
print('🔵 切换失败: $e');
|
||||
// 发生错误时强制同步状态
|
||||
kr_forceSyncConnectionStatus();
|
||||
}
|
||||
|
||||
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)
|
||||
return node.urlTestDelay.value < 65535 && node.urlTestDelay.value > 0;
|
||||
}
|
||||
|
||||
|
||||
/// 检查是否需要进行国家内节点重选
|
||||
void _checkCountryReselection(List<dynamic> activeGroups) {
|
||||
// 如果未启用国家内重选功能,直接返回
|
||||
if (!isCountryReselectionEnabled.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// 如果未连接,直接返回
|
||||
if (!kr_isConnected.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// 如果没有选择国家,直接返回
|
||||
if (currentSelectedCountry.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
// 获取当前节点信息
|
||||
final currentNodeInfo = kr_getRealConnectedNodeInfo();
|
||||
final currentDelay = currentNodeInfo['delay'] as int;
|
||||
|
||||
|
||||
// 检查当前节点延迟是否超过阈值
|
||||
if (currentDelay > 0 && currentDelay < countryReselectionLatencyThreshold) {
|
||||
// 延迟正常,无需重选
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// 如果延迟超过阈值或无效,尝试在当前国家内重选
|
||||
if (currentDelay >= countryReselectionLatencyThreshold || currentDelay <= 0) {
|
||||
KRLogUtil.kr_w('🔄 当前节点延迟过高(${currentDelay}ms),尝试国家内重选', tag: 'HomeController');
|
||||
@ -938,7 +975,7 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
KRLogUtil.kr_e('检查国家内重选时出错: $e', tag: 'HomeController');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// 执行国家内节点重选
|
||||
void _performCountryReselection(String country) {
|
||||
try {
|
||||
@ -946,16 +983,16 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
final countryNodes = kr_subscribeService.allList
|
||||
.where((node) => node.country == country && node.tag != 'auto')
|
||||
.toList();
|
||||
|
||||
|
||||
if (countryNodes.isEmpty) {
|
||||
KRLogUtil.kr_w('⚠️ 国家 $country 内没有可用节点', tag: 'HomeController');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// 找到延迟最小的有效节点
|
||||
String? bestNode;
|
||||
int minDelay = 65535;
|
||||
|
||||
|
||||
for (var node in countryNodes) {
|
||||
final delay = node.urlTestDelay.value;
|
||||
if (delay > 0 && delay < 65535 && delay < minDelay) {
|
||||
@ -963,7 +1000,7 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
bestNode = node.tag;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (bestNode != null && bestNode != kr_cutSeletedTag.value) {
|
||||
KRLogUtil.kr_i('🎯 国家内重选: $bestNode (${minDelay}ms)', tag: 'HomeController');
|
||||
kr_selectNode(bestNode);
|
||||
@ -974,33 +1011,33 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
KRLogUtil.kr_e('执行国家内重选时出错: $e', tag: 'HomeController');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// 设置当前选择的国家(由hi_node_list_controller调用)
|
||||
void setCurrentSelectedCountry(String country) {
|
||||
currentSelectedCountry.value = country;
|
||||
KRLogUtil.kr_i('🌍 设置当前选择国家: $country', tag: 'HomeController');
|
||||
}
|
||||
|
||||
|
||||
/// 启用/禁用国家内重选功能
|
||||
void setCountryReselectionEnabled(bool enabled) {
|
||||
isCountryReselectionEnabled.value = enabled;
|
||||
KRLogUtil.kr_i('🔄 国家内重选功能已${enabled ? "启用" : "禁用"}', tag: 'HomeController');
|
||||
}
|
||||
|
||||
|
||||
/// 获取当前国家内的节点统计信息
|
||||
Map<String, dynamic> getCurrentCountryNodeStats() {
|
||||
if (currentSelectedCountry.isEmpty) {
|
||||
return {'error': '没有选择国家'};
|
||||
}
|
||||
|
||||
|
||||
final countryNodes = kr_subscribeService.allList
|
||||
.where((node) => node.country == currentSelectedCountry.value && node.tag != 'auto')
|
||||
.toList();
|
||||
|
||||
|
||||
if (countryNodes.isEmpty) {
|
||||
return {'error': '国家内没有节点'};
|
||||
}
|
||||
|
||||
|
||||
List<int> validDelays = [];
|
||||
int totalNodes = countryNodes.length;
|
||||
int validNodes = 0;
|
||||
@ -1008,27 +1045,27 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
int maxDelay = 0;
|
||||
String? fastestNode;
|
||||
String? slowestNode;
|
||||
|
||||
|
||||
for (var node in countryNodes) {
|
||||
final delay = node.urlTestDelay.value;
|
||||
if (delay > 0 && delay < 65535) {
|
||||
validDelays.add(delay);
|
||||
validNodes++;
|
||||
|
||||
|
||||
if (delay < minDelay) {
|
||||
minDelay = delay;
|
||||
fastestNode = node.tag;
|
||||
}
|
||||
|
||||
|
||||
if (delay > maxDelay) {
|
||||
maxDelay = delay;
|
||||
slowestNode = node.tag;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
double avgDelay = validDelays.isEmpty ? 0 : validDelays.reduce((a, b) => a + b) / validDelays.length;
|
||||
|
||||
|
||||
return {
|
||||
'country': currentSelectedCountry.value,
|
||||
'totalNodes': totalNodes,
|
||||
@ -1088,10 +1125,11 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
// 更新连接信息
|
||||
kr_updateConnectionInfo();
|
||||
|
||||
// 🔧 修复:只有在核心已启动时才选择节点,避免触发重启
|
||||
if (KRSingBoxImp.instance.kr_status.value == SingboxStarted()) {
|
||||
KRSingBoxImp.instance.kr_selectOutbound(tag);
|
||||
|
||||
// 🔧 修复:选择节点后启动延迟值更新(带超时保护)
|
||||
// 🔧 修复:选择节点后启动延迟值更新(带超时保护)
|
||||
Future.delayed(const Duration(milliseconds: 500), () {
|
||||
if (kr_currentNodeLatency.value == -1 && kr_isConnected.value) {
|
||||
KRLogUtil.kr_w('⚠️ 选择节点后延迟值未更新,尝试手动更新', tag: 'HomeController');
|
||||
@ -1101,7 +1139,13 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
}
|
||||
});
|
||||
} 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/localization/app_translations.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 '../models/kr_home_views_status.dart';
|
||||
|
||||
@ -209,13 +210,31 @@ class KRHomeConnectionInfoView extends GetView<KRHomeController> {
|
||||
),
|
||||
],
|
||||
),
|
||||
CupertinoSwitch(
|
||||
value: controller.kr_isConnected.value,
|
||||
onChanged: (bool value) {
|
||||
controller.kr_toggleSwitch(value);
|
||||
},
|
||||
activeColor: Colors.blue,
|
||||
),
|
||||
// 🔧 修复: 使用多层监听确保状态更新
|
||||
Obx(() {
|
||||
// 🔧 关键: 强制读取两个 observable 确保追踪
|
||||
final _ = KRSingBoxImp.instance.kr_status.value; // 强制追踪
|
||||
final isConnected = controller.kr_isConnected.value; // 使用 controller 的状态
|
||||
|
||||
// 再次读取状态用于判断
|
||||
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_config = "/v1/app/auth/config";
|
||||
static const String kr_config = "/v1/common/site/config";
|
||||
|
||||
/// 获取用户在线时长统计
|
||||
static const String kr_getUserOnlineTimeStatistics =
|
||||
|
||||
@ -123,7 +123,7 @@ class KRUserApi {
|
||||
await HttpUtil.getInstance().request<KRConfigData>(
|
||||
Api.kr_config,
|
||||
data,
|
||||
method: HttpMethod.POST,
|
||||
method: HttpMethod.GET,
|
||||
isShowLoading: false,
|
||||
);
|
||||
if (!baseResponse.isSuccess) {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import 'dart:io';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_udid/flutter_udid.dart';
|
||||
import '../utils/kr_secure_storage.dart';
|
||||
import '../utils/kr_log_util.dart';
|
||||
import 'package:crypto/crypto.dart';
|
||||
@ -51,37 +52,28 @@ class KRDeviceInfoService {
|
||||
}
|
||||
|
||||
/// 获取设备唯一标识
|
||||
/// 使用多因子组合策略确保唯一性和稳定性
|
||||
Future<String> _getDeviceId() async {
|
||||
try {
|
||||
String? identifier;
|
||||
String identifier;
|
||||
|
||||
if (Platform.isAndroid) {
|
||||
final androidInfo = await _deviceInfo.androidInfo;
|
||||
// Android使用androidId作为唯一标识
|
||||
identifier = androidInfo.id;
|
||||
identifier = await _getAndroidDeviceId();
|
||||
} else if (Platform.isIOS) {
|
||||
final iosInfo = await _deviceInfo.iosInfo;
|
||||
// iOS使用identifierForVendor作为唯一标识
|
||||
identifier = iosInfo.identifierForVendor;
|
||||
identifier = await _getIOSDeviceId();
|
||||
} else if (Platform.isMacOS) {
|
||||
final macInfo = await _deviceInfo.macOsInfo;
|
||||
// macOS使用systemGUID
|
||||
identifier = macInfo.systemGUID;
|
||||
identifier = await _getMacOSDeviceId();
|
||||
} else if (Platform.isWindows) {
|
||||
final windowsInfo = await _deviceInfo.windowsInfo;
|
||||
// Windows使用计算机名作为唯一标识
|
||||
identifier = windowsInfo.computerName;
|
||||
identifier = await _getWindowsDeviceId();
|
||||
} else if (Platform.isLinux) {
|
||||
final linuxInfo = await _deviceInfo.linuxInfo;
|
||||
// Linux使用machineId
|
||||
identifier = linuxInfo.machineId;
|
||||
identifier = await _getLinuxDeviceId();
|
||||
} else {
|
||||
// Web或其他平台,使用生成的UUID
|
||||
// Web或其他平台,使用生成的UUID
|
||||
identifier = await _getOrCreateStoredDeviceId();
|
||||
}
|
||||
|
||||
// 如果获取失败,使用存储的或生成新的ID
|
||||
if (identifier == null || identifier.isEmpty) {
|
||||
// 如果获取失败,使用存储的或生成新的ID
|
||||
if (identifier.isEmpty) {
|
||||
identifier = await _getOrCreateStoredDeviceId();
|
||||
}
|
||||
|
||||
@ -89,11 +81,201 @@ class KRDeviceInfoService {
|
||||
} catch (e) {
|
||||
print('❌ 获取设备ID失败: $e');
|
||||
KRLogUtil.kr_e('❌ 获取设备ID失败 - $e', tag: 'KRDeviceInfoService');
|
||||
// 如果获取失败,返回存储的或生成新的ID
|
||||
// 如果获取失败,返回存储的或生成新的ID
|
||||
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
|
||||
Future<String> _getOrCreateStoredDeviceId() async {
|
||||
try {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -23,17 +23,73 @@ class KRSecureStorage {
|
||||
// 初始化 Hive
|
||||
Future<void> kr_initHive() async {
|
||||
try {
|
||||
if (Platform.isMacOS) {
|
||||
// 根据不同平台指定数据库路径
|
||||
if (Platform.isMacOS || Platform.isWindows || Platform.isLinux) {
|
||||
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);
|
||||
} else {
|
||||
// Android 和 iOS 使用默认路径
|
||||
await Hive.initFlutter();
|
||||
}
|
||||
|
||||
// 使用加密适配器
|
||||
final key = HiveAesCipher(_generateKey());
|
||||
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('错误类型: ${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:io';
|
||||
import 'dart:isolate';
|
||||
|
||||
// import 'package:combine/combine.dart'; // 暂时注释掉,使用 Isolate.run 替代
|
||||
import 'package:combine/combine.dart';
|
||||
import 'package:ffi/ffi.dart';
|
||||
import 'package:fpdart/fpdart.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/singbox/model/singbox_config_option.dart';
|
||||
import 'package:kaer_with_panels/singbox/model/singbox_outbound.dart';
|
||||
import 'package:kaer_with_panels/singbox/model/singbox_stats.dart';
|
||||
@ -50,14 +48,10 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
Future<void> init() async {
|
||||
loggy.debug("initializing");
|
||||
_statusReceiver = ReceivePort('service status receiver');
|
||||
final source = _statusReceiver
|
||||
.asBroadcastStream()
|
||||
.map((event) => jsonDecode(event as String))
|
||||
.map(SingboxStatus.fromEvent);
|
||||
final source = _statusReceiver.asBroadcastStream().map((event) => jsonDecode(event as String)).map(SingboxStatus.fromEvent);
|
||||
_status = ValueConnectableStream.seeded(
|
||||
source,
|
||||
const SingboxStopped(),
|
||||
@ -71,7 +65,7 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
||||
) {
|
||||
final port = _statusReceiver.sendPort.nativePort;
|
||||
return TaskEither(
|
||||
() => Isolate.run(
|
||||
() => CombineWorker().execute(
|
||||
() {
|
||||
_box.setupOnce(NativeApi.initializeApiDLData);
|
||||
final err = _box
|
||||
@ -100,7 +94,7 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
||||
bool debug,
|
||||
) {
|
||||
return TaskEither(
|
||||
() => Isolate.run(
|
||||
() => CombineWorker().execute(
|
||||
() {
|
||||
final err = _box
|
||||
.parse(
|
||||
@ -122,13 +116,10 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
||||
@override
|
||||
TaskEither<String, Unit> changeOptions(SingboxConfigOption options) {
|
||||
return TaskEither(
|
||||
() => Isolate.run(
|
||||
() => CombineWorker().execute(
|
||||
() {
|
||||
final json = jsonEncode(options.toJson());
|
||||
final err = _box
|
||||
.changeHiddifyOptions(json.toNativeUtf8().cast())
|
||||
.cast<Utf8>()
|
||||
.toDartString();
|
||||
final err = _box.changeHiddifyOptions(json.toNativeUtf8().cast()).cast<Utf8>().toDartString();
|
||||
if (err.isNotEmpty) {
|
||||
return left(err);
|
||||
}
|
||||
@ -143,7 +134,7 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
||||
String path,
|
||||
) {
|
||||
return TaskEither(
|
||||
() => Isolate.run(
|
||||
() => CombineWorker().execute(
|
||||
() {
|
||||
final response = _box
|
||||
.generateConfig(
|
||||
@ -168,7 +159,7 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
||||
) {
|
||||
loggy.debug("starting, memory limit: [${!disableMemoryLimit}]");
|
||||
return TaskEither(
|
||||
() => Isolate.run(
|
||||
() => CombineWorker().execute(
|
||||
() {
|
||||
final err = _box
|
||||
.start(
|
||||
@ -189,7 +180,7 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
||||
@override
|
||||
TaskEither<String, Unit> stop() {
|
||||
return TaskEither(
|
||||
() => Isolate.run(
|
||||
() => CombineWorker().execute(
|
||||
() {
|
||||
final err = _box.stop().cast<Utf8>().toDartString();
|
||||
if (err.isNotEmpty) {
|
||||
@ -209,7 +200,7 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
||||
) {
|
||||
loggy.debug("restarting, memory limit: [${!disableMemoryLimit}]");
|
||||
return TaskEither(
|
||||
() => Isolate.run(
|
||||
() => CombineWorker().execute(
|
||||
() {
|
||||
final err = _box
|
||||
.restart(
|
||||
@ -267,10 +258,7 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
||||
},
|
||||
);
|
||||
|
||||
final err = _box
|
||||
.startCommandClient(1, receiver.sendPort.nativePort)
|
||||
.cast<Utf8>()
|
||||
.toDartString();
|
||||
final err = _box.startCommandClient(1, receiver.sendPort.nativePort).cast<Utf8>().toDartString();
|
||||
if (err.isNotEmpty) {
|
||||
loggy.error("error starting status command: $err");
|
||||
throw err;
|
||||
@ -312,10 +300,7 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
||||
);
|
||||
|
||||
try {
|
||||
final err = _box
|
||||
.startCommandClient(5, receiver.sendPort.nativePort)
|
||||
.cast<Utf8>()
|
||||
.toDartString();
|
||||
final err = _box.startCommandClient(5, receiver.sendPort.nativePort).cast<Utf8>().toDartString();
|
||||
if (err.isNotEmpty) {
|
||||
logger.error("error starting group command: $err");
|
||||
throw err;
|
||||
@ -359,10 +344,7 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
||||
);
|
||||
|
||||
try {
|
||||
final err = _box
|
||||
.startCommandClient(13, receiver.sendPort.nativePort)
|
||||
.cast<Utf8>()
|
||||
.toDartString();
|
||||
final err = _box.startCommandClient(13, receiver.sendPort.nativePort).cast<Utf8>().toDartString();
|
||||
if (err.isNotEmpty) {
|
||||
logger.error("error starting: $err");
|
||||
throw err;
|
||||
@ -378,7 +360,7 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
||||
@override
|
||||
TaskEither<String, Unit> selectOutbound(String groupTag, String outboundTag) {
|
||||
return TaskEither(
|
||||
() => Isolate.run(
|
||||
() => CombineWorker().execute(
|
||||
() {
|
||||
final err = _box
|
||||
.selectOutbound(
|
||||
@ -399,12 +381,9 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
||||
@override
|
||||
TaskEither<String, Unit> urlTest(String groupTag) {
|
||||
return TaskEither(
|
||||
() => Isolate.run(
|
||||
() => CombineWorker().execute(
|
||||
() {
|
||||
final err = _box
|
||||
.urlTest(groupTag.toNativeUtf8().cast())
|
||||
.cast<Utf8>()
|
||||
.toDartString();
|
||||
final err = _box.urlTest(groupTag.toNativeUtf8().cast()).cast<Utf8>().toDartString();
|
||||
if (err.isNotEmpty) {
|
||||
return left(err);
|
||||
}
|
||||
@ -420,9 +399,7 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
||||
@override
|
||||
Stream<List<String>> watchLogs(String path) async* {
|
||||
yield await _readLogFile(File(path));
|
||||
yield* Watcher(path, pollingDelay: const Duration(seconds: 1))
|
||||
.events
|
||||
.asyncMap((event) async {
|
||||
yield* Watcher(path, pollingDelay: const Duration(seconds: 1)).events.asyncMap((event) async {
|
||||
if (event.type == ChangeType.MODIFY) {
|
||||
await _readLogFile(File(path));
|
||||
}
|
||||
@ -433,7 +410,7 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
||||
@override
|
||||
TaskEither<String, Unit> clearLogs() {
|
||||
return TaskEither(
|
||||
() => Isolate.run(
|
||||
() => CombineWorker().execute(
|
||||
() {
|
||||
_logBuffer.clear();
|
||||
return right(unit);
|
||||
@ -444,8 +421,7 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
||||
|
||||
Future<List<String>> _readLogFile(File file) async {
|
||||
if (_logFilePosition == 0 && file.lengthSync() == 0) return [];
|
||||
final content =
|
||||
await file.openRead(_logFilePosition).transform(utf8.decoder).join();
|
||||
final content = await file.openRead(_logFilePosition).transform(utf8.decoder).join();
|
||||
_logFilePosition = file.lengthSync();
|
||||
final lines = const LineSplitter().convert(content);
|
||||
if (lines.length > 300) {
|
||||
@ -468,7 +444,7 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
||||
}) {
|
||||
loggy.debug("generating warp config");
|
||||
return TaskEither(
|
||||
() => Isolate.run(
|
||||
() => CombineWorker().execute(
|
||||
() {
|
||||
final response = _box
|
||||
.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(),
|
||||
"application-id", APPLICATION_ID,
|
||||
"flags", G_APPLICATION_FLAGS_NONE,
|
||||
"flags", G_APPLICATION_DEFAULT_FLAGS,
|
||||
nullptr));
|
||||
}
|
||||
|
||||
@ -82,7 +82,7 @@ SPEC CHECKSUMS:
|
||||
device_info_plus: 4fb280989f669696856f8b129e4a5e3cd6c48f76
|
||||
flutter_inappwebview_macos: c2d68649f9f8f1831bfcd98d73fd6256366d9d1d
|
||||
flutter_udid: d26e455e8c06174e6aff476e147defc6cae38495
|
||||
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
|
||||
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
|
||||
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
|
||||
package_info_plus: f0052d280d17aa382b932f399edf32507174e870
|
||||
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
|
||||
dartx: ^1.2.0
|
||||
rxdart: ^0.27.7
|
||||
# combine: ^0.5.8 # 暂时移除,使用 Isolate.run 替代
|
||||
combine: 0.5.7 # 精确版本,兼容 Flutter 3.24.3(与 hiddify-app 相同)
|
||||
encrypt: ^5.0.0
|
||||
path: ^1.8.3
|
||||
path_provider: ^2.1.1
|
||||
@ -140,6 +140,7 @@ dev_dependencies:
|
||||
flutter:
|
||||
assets:
|
||||
- assets/images/
|
||||
- assets/geosite/
|
||||
- assets/translations/strings_en.i18n.json
|
||||
- assets/translations/strings_zh.i18n.json
|
||||
- assets/translations/strings_es.i18n.json
|
||||
|
||||
@ -2,6 +2,12 @@
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
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_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} /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 on-disk name of your application.
|
||||
@ -50,7 +58,7 @@ add_definitions(-DUNICODE -D_UNICODE)
|
||||
function(APPLY_STANDARD_SETTINGS TARGET)
|
||||
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 /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 "$<$<CONFIG:Debug>:_DEBUG>")
|
||||
# 确保目标使用静态链接
|
||||
@ -70,6 +78,14 @@ add_subdirectory("runner")
|
||||
# them to the application.
|
||||
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 ===
|
||||
# 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_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}"
|
||||
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}"
|
||||
COMPONENT Runtime)
|
||||
|
||||
# install(FILES "../libcore/bin/libcore.dll" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||
# COMPONENT Runtime)
|
||||
# 安装 libcore.dll 到可执行文件目录(与 BearVPN.exe 同级)
|
||||
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}"
|
||||
COMPONENT Runtime RENAME libcore.dll)
|
||||
|
||||
install(FILES "../libcore/bin/BearVPNCli.exe" DESTINATION "${CMAKE_INSTALL_PREFIX}"
|
||||
COMPONENT Runtime RENAME BearVPNCli.exe)
|
||||
# 安装 BearVPNCli.exe(从 libcore/bin 复制并重命名)
|
||||
# 注意:libcore 编译的是 HiddifyCli.exe,打包脚本会自动重命名为 BearVPNCli.exe
|
||||
# 这里需要安装 BearVPNCli.exe,因为它已经被重命名了
|
||||
install(FILES "../libcore/bin/BearVPNCli.exe"
|
||||
DESTINATION "${CMAKE_INSTALL_PREFIX}"
|
||||
COMPONENT Runtime
|
||||
OPTIONAL)
|
||||
|
||||
|
||||
if(PLUGIN_BUNDLED_LIBRARIES)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user