Compare commits
91 Commits
7a223d614b
..
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| a7df3a44a2 | |||
| 7956349c0a | |||
| 8ccfc6d272 | |||
| f526122f33 | |||
| b86d645acf | |||
| ef1f3bfb50 | |||
| 33917e774d | |||
| 78c20f9ae9 | |||
| 610eb4f20d | |||
| 8cc88be5f6 | |||
| 50f4237c74 | |||
| deac0adabd | |||
| 4f9d405c9f | |||
| 88091e84c0 | |||
| fafad1ff97 | |||
| fbcf5f7e61 | |||
| b21d189ca1 | |||
| 701acd561a | |||
| 3b419d1e24 | |||
| e3b9fea5b2 | |||
| 54dd4cbd35 | |||
| 77b721df4b | |||
| 8937024241 | |||
| 29c6e40940 | |||
| f75b109025 | |||
| eb4db67fa1 | |||
| 62d3157665 | |||
| 577327c40a | |||
| fed0b0e422 | |||
| ad5d0c9b7a | |||
| 9873f670fa | |||
| 8b8d29dd00 | |||
| 477d4086b2 | |||
| 7fc2a60e18 | |||
| 41e6ed433a | |||
| 8b055d9ee1 | |||
| 0808e2db10 | |||
| dcc9ad43fe | |||
| b47e0e984e | |||
| 51ed02de1a | |||
| e0b1cec87d | |||
| a9db68c29b | |||
| db7a776d2b | |||
| bfa3ec48d6 | |||
| ca74f9289e | |||
| f9ea310031 | |||
| 5d849c3b98 | |||
| b7be97f6c1 | |||
| 7212fd3132 | |||
| 067a715afe | |||
| ce969a6ad4 | |||
| cea406f660 | |||
| a09245cb3e | |||
| bbc1c03cca | |||
| b3935f8516 | |||
| 14b95b07b4 | |||
| 0e1c043e34 | |||
| cc5a732fa5 | |||
| 0e9d1bc157 | |||
| 6dfad4544d | |||
| 2eaa15cc8f | |||
| 5c1449155e | |||
| cc8744a188 | |||
| f72ffa6443 | |||
| 5e84fea7d0 | |||
| dce32e706b | |||
| 7e19e2cdb8 | |||
| 0e062a275a | |||
| 4cc3f98b0f | |||
| a6527d822b | |||
| aac7d84602 | |||
| f738ba9245 | |||
| 89e02cc707 | |||
| d1e45f0254 | |||
| dbf6175ad9 | |||
| 251564a5de | |||
| ef8bba71d5 | |||
| 961c655a92 | |||
| 4389770821 | |||
| 7c9b23edbd | |||
| 90fd476c45 | |||
| 7cd360e61e | |||
| 6e189671c7 | |||
| 4fbdb331d4 | |||
| 6131a80b2c | |||
| 33f45aa4a5 | |||
| 4dbcaff187 | |||
| 9e05a17fc4 | |||
| 1a9f0d79ac | |||
| c7b77c1ad8 | |||
| 7cd022093b |
@@ -0,0 +1,434 @@
|
||||
name: Build Windows
|
||||
run-name: 构建Win流程
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
|
||||
jobs:
|
||||
# 先编译 libcore
|
||||
build-libcore:
|
||||
name: 编译 libcore (Windows)
|
||||
runs-on: client-server
|
||||
container:
|
||||
image: node:20
|
||||
strategy:
|
||||
matrix:
|
||||
# 只有node支持版本号别名
|
||||
node: ['20.15.1']
|
||||
|
||||
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: 🔧 安装 MinGW
|
||||
run: |
|
||||
# 在 GitHub 托管 runner(有 sudo)与 act/自托管容器(无 sudo)均可运行
|
||||
if command -v sudo >/dev/null 2>&1; then
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y mingw-w64
|
||||
else
|
||||
echo "sudo 不存在,按 root 用户直接执行 apt-get"
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update || apt update
|
||||
apt-get install -y mingw-w64 || apt install -y mingw-w64
|
||||
fi
|
||||
|
||||
- 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@v3
|
||||
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: 🔧 Add Git to PATH (Windows)
|
||||
if: ${{ runner.os == 'Windows' }}
|
||||
shell: powershell
|
||||
run: |
|
||||
$paths = @(
|
||||
'C:\Program Files\Git\cmd',
|
||||
'C:\Program Files\Git\bin',
|
||||
'C:\Program Files (x86)\Git\cmd',
|
||||
'C:\Program Files (x86)\Git\bin'
|
||||
)
|
||||
foreach ($p in $paths) {
|
||||
if (Test-Path $p) {
|
||||
Add-Content -Path $env:GITHUB_PATH -Value $p
|
||||
$env:PATH = "$p;$env:PATH"
|
||||
Write-Host ("Added to PATH: {0}" -f $p)
|
||||
}
|
||||
}
|
||||
$candidates = @(
|
||||
'C:\Program Files\Git\cmd\git.exe',
|
||||
'C:\Program Files\Git\bin\git.exe',
|
||||
'C:\Program Files (x86)\Git\cmd\git.exe',
|
||||
'C:\Program Files (x86)\Git\bin\git.exe'
|
||||
)
|
||||
$found = $false
|
||||
foreach ($g in $candidates) {
|
||||
if (Test-Path $g) {
|
||||
& $g --version
|
||||
$found = $true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (-not $found) {
|
||||
Write-Host "Git executable not found in common locations; continuing."
|
||||
}
|
||||
|
||||
- name: 📥 Checkout 代码
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 📥 下载 libcore
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: libcore-windows
|
||||
path: .
|
||||
|
||||
- name: 🔧 复制 libcore 文件到正确位置并重命名
|
||||
shell: cmd
|
||||
run: call copy_libcore.bat
|
||||
|
||||
- name: 🔍 Verify jq Installation (ACT)
|
||||
shell: powershell
|
||||
run: |
|
||||
# Check if jq is available
|
||||
$jqFound = $false
|
||||
try {
|
||||
$version = jq --version 2>&1
|
||||
if ($version -match 'jq-[\d\.]+') {
|
||||
Write-Host "jq is already installed: $version"
|
||||
$jqFound = $true
|
||||
}
|
||||
} catch {
|
||||
Write-Host "jq not installed or not in PATH"
|
||||
}
|
||||
if (-not $jqFound) {
|
||||
Write-Host "Preparing to install jq..."
|
||||
# Check if Chocolatey is available
|
||||
$chocoFound = $false
|
||||
try {
|
||||
choco --version 2>&1 | Out-Null
|
||||
$chocoFound = $true
|
||||
} catch {}
|
||||
if (-not $chocoFound) {
|
||||
Write-Host "Installing Chocolatey..."
|
||||
Set-ExecutionPolicy Bypass -Scope Process -Force
|
||||
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072
|
||||
iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
|
||||
# Refresh PATH
|
||||
$env:PATH = [System.Environment]::GetEnvironmentVariable('PATH', 'Machine') + ';' + [System.Environment]::GetEnvironmentVariable('PATH', 'User')
|
||||
Write-Host "Chocolatey installed successfully"
|
||||
}
|
||||
Write-Host "Installing jq using Chocolatey..."
|
||||
choco install jq -y
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Host "jq installed successfully"
|
||||
# Verify again
|
||||
$version = jq --version 2>&1
|
||||
Write-Host "jq version: $version"
|
||||
} else {
|
||||
Write-Host "jq installation failed, exit code: $LASTEXITCODE"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
- name: Add jq to PATH
|
||||
shell: powershell
|
||||
run: |
|
||||
$jqPath = "C:\ProgramData\chocolatey\bin"
|
||||
if (Test-Path $jqPath) {
|
||||
Add-Content -Path $env:GITHUB_PATH -Value $jqPath
|
||||
$env:PATH = "$jqPath;$env:PATH"
|
||||
Write-Host "Added jq path to GITHUB_PATH and current PATH"
|
||||
} else {
|
||||
Write-Host "jq installation path not found"
|
||||
exit 1
|
||||
}
|
||||
|
||||
- name: Add Flutter to PATH
|
||||
shell: powershell
|
||||
run: |
|
||||
# Add Flutter to PATH for current session and GitHub Actions
|
||||
$flutterPath = "C:\flutter\bin"
|
||||
if (Test-Path $flutterPath) {
|
||||
Write-Host "Adding Flutter to PATH: $flutterPath"
|
||||
$env:PATH = "$flutterPath;$env:PATH"
|
||||
Add-Content -Path $env:GITHUB_PATH -Value $flutterPath
|
||||
|
||||
# Verify Flutter is accessible
|
||||
$flutterVersion = flutter --version 2>&1
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Host "Flutter verified: $flutterVersion"
|
||||
} else {
|
||||
Write-Host "Flutter not found at $flutterPath"
|
||||
exit 1
|
||||
}
|
||||
} else {
|
||||
Write-Host "ERROR: Flutter not found at C:\flutter\bin"
|
||||
exit 1
|
||||
}
|
||||
|
||||
- name: Enable Windows desktop
|
||||
shell: powershell
|
||||
run: |
|
||||
$env:PATH = "C:\flutter\bin;$env:PATH"
|
||||
flutter config --enable-windows-desktop
|
||||
|
||||
- name: Get dependencies
|
||||
shell: powershell
|
||||
run: |
|
||||
$env:PATH = "C:\flutter\bin;$env:PATH"
|
||||
flutter pub get
|
||||
|
||||
- name: Generate code
|
||||
shell: powershell
|
||||
run: |
|
||||
$env:PATH = "C:\flutter\bin;$env:PATH"
|
||||
dart run build_runner build --delete-conflicting-outputs
|
||||
|
||||
- name: Install NuGet
|
||||
shell: powershell
|
||||
run: |
|
||||
Write-Host "Installing NuGet..."
|
||||
|
||||
# Check if NuGet is already available
|
||||
$nugetPath = Get-Command nuget -ErrorAction SilentlyContinue
|
||||
if ($nugetPath) {
|
||||
Write-Host "NuGet already installed: $($nugetPath.Source)"
|
||||
$nugetVersion = nuget help | Select-String -Pattern "NuGet Version" | Select-Object -First 1
|
||||
Write-Host "NuGet version: $nugetVersion"
|
||||
return
|
||||
}
|
||||
|
||||
# Use Chocolatey to install NuGet (proven to work)
|
||||
Write-Host "Installing NuGet via Chocolatey..."
|
||||
choco install nuget.commandline -y
|
||||
|
||||
# Update PATH to include Chocolatey
|
||||
$env:PATH = "C:\ProgramData\chocolatey\bin;$env:PATH"
|
||||
Add-Content -Path $env:GITHUB_PATH -Value "C:\ProgramData\chocolatey\bin"
|
||||
|
||||
# Verify installation
|
||||
if (Get-Command nuget -ErrorAction SilentlyContinue) {
|
||||
Write-Host "NuGet installed successfully via Chocolatey"
|
||||
$nugetVersion = nuget help | Select-String -Pattern "NuGet Version" | Select-Object -First 1
|
||||
Write-Host "NuGet version: $nugetVersion"
|
||||
} else {
|
||||
Write-Host "WARNING: NuGet installation may have failed, but continuing..."
|
||||
}
|
||||
|
||||
- name: Fix long paths
|
||||
shell: powershell
|
||||
run: |
|
||||
Write-Host "Enabling long path support..."
|
||||
|
||||
# Enable long paths in Windows
|
||||
Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem" -Name "LongPathsEnabled" -Value 1 -Type DWORD -Force
|
||||
|
||||
# Also set for current user
|
||||
Set-ItemProperty -Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\AppModel\StateRepository\Cache\Package\Data\" -Name "LongPathsEnabled" -Value 1 -Type DWORD -Force -ErrorAction SilentlyContinue
|
||||
|
||||
Write-Host "Long path support enabled"
|
||||
|
||||
- name: Clean build cache
|
||||
shell: powershell
|
||||
run: |
|
||||
$env:PATH = "C:\flutter\bin;$env:PATH"
|
||||
Write-Host "Cleaning build cache..."
|
||||
flutter clean
|
||||
|
||||
Write-Host "Removing old build directories..."
|
||||
if (Test-Path "build") {
|
||||
Remove-Item -Path "build" -Recurse -Force
|
||||
}
|
||||
if (Test-Path "windows") {
|
||||
Remove-Item -Path "windows" -Recurse -Force
|
||||
}
|
||||
|
||||
Write-Host "Recreating Windows project..."
|
||||
flutter create --platforms=windows .
|
||||
|
||||
- name: Build Windows Debug
|
||||
shell: powershell
|
||||
run: |
|
||||
$env:PATH = "C:\flutter\bin;C:\ProgramData\chocolatey\bin;$env:PATH"
|
||||
flutter build windows
|
||||
|
||||
- name: Build Windows Release
|
||||
shell: powershell
|
||||
run: |
|
||||
$env:PATH = "C:\flutter\bin;C:\ProgramData\chocolatey\bin;$env:PATH"
|
||||
flutter build windows --release
|
||||
|
||||
- name: Upload Debug build artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: windows-debug-build
|
||||
path: build/windows/x64/runner/Debug/
|
||||
|
||||
- name: Upload Release build artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: windows-release-build
|
||||
path: build/windows/x64/runner/Release/
|
||||
|
||||
- name: Install Build Tools
|
||||
shell: powershell
|
||||
run: |
|
||||
choco install 7zip -y
|
||||
choco install enigma-virtual-box -y
|
||||
|
||||
- name: Package Single EXE
|
||||
shell: powershell
|
||||
run: |
|
||||
Write-Host "=== 开始打包单文件 EXE ==="
|
||||
|
||||
$buildPath = "build\windows\x64\runner\Release"
|
||||
$outputPath = "dist"
|
||||
$enigmaPath = "C:\Program Files\Enigma Virtual Box\enigmavb.exe"
|
||||
|
||||
# 创建输出目录
|
||||
if (-not (Test-Path $outputPath)) {
|
||||
New-Item -ItemType Directory -Path $outputPath -Force | Out-Null
|
||||
}
|
||||
|
||||
# 获取主程序
|
||||
$exeFile = Get-ChildItem -Path $buildPath -Filter "*.exe" | Select-Object -First 1
|
||||
if (-not $exeFile) {
|
||||
Write-Host "❌ 未找到可执行文件"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$inputExe = $exeFile.FullName
|
||||
$outputExe = "$outputPath\$($exeFile.BaseName)_Single.exe"
|
||||
|
||||
Write-Host "主程序: $inputExe"
|
||||
Write-Host "输出: $outputExe"
|
||||
|
||||
# 尝试使用 Enigma Virtual Box
|
||||
if (Test-Path $enigmaPath) {
|
||||
Write-Host "使用 Enigma Virtual Box 打包..."
|
||||
|
||||
# 创建配置文件
|
||||
$configContent = "[Config]`nInputFile=$inputExe`nOutputFile=$outputExe`nFiles=%DEFAULT FOLDER%`nVirtualizationMode=Never Write To Disk`nCompression=Yes`nShareVirtualSystem=Yes`n`n[Files]`nFolder=$buildPath\*"
|
||||
|
||||
$configFile = "$outputPath\package_config.evb"
|
||||
Set-Content -Path $configFile -Value $configContent
|
||||
|
||||
# 执行打包
|
||||
$process = Start-Process -FilePath $enigmaPath -ArgumentList "/sf", $inputExe, "/lf", $outputExe, "/folder", $buildPath, "/compress" -Wait -PassThru -NoNewWindow
|
||||
|
||||
if ($process.ExitCode -eq 0) {
|
||||
Write-Host "✅ 单文件打包成功!"
|
||||
|
||||
# 显示压缩信息
|
||||
$originalSize = (Get-Item $inputExe).Length / 1MB
|
||||
$packedSize = (Get-Item $outputExe).Length / 1MB
|
||||
Write-Host "原始大小: $([math]::Round($originalSize, 2)) MB"
|
||||
Write-Host "打包大小: $([math]::Round($packedSize, 2)) MB"
|
||||
} else {
|
||||
Write-Host "⚠️ Enigma 打包失败,使用备选方案"
|
||||
exit 1
|
||||
}
|
||||
} else {
|
||||
Write-Host "⚠️ Enigma 未找到,使用 7-Zip 自解压方案"
|
||||
exit 1
|
||||
}
|
||||
|
||||
- name: Package with 7-Zip Alternative
|
||||
if: failure()
|
||||
shell: powershell
|
||||
run: |
|
||||
Write-Host "使用 7-Zip 自解压方案..."
|
||||
|
||||
$buildPath = "build\windows\x64\runner\Release"
|
||||
$outputPath = "dist"
|
||||
|
||||
# 检查 7-Zip
|
||||
$7zipPath = "C:\Program Files\7-Zip\7z.exe"
|
||||
|
||||
# 获取主程序名称
|
||||
$exeFile = Get-ChildItem -Path $buildPath -Filter "*.exe" | Select-Object -First 1
|
||||
$outputSfx = "$outputPath\$($exeFile.BaseName)_Package.exe"
|
||||
|
||||
Write-Host "创建自解压包..."
|
||||
|
||||
# 创建配置文件
|
||||
$configContent = @"
|
||||
;!@Install@!UTF-8!
|
||||
Title="HostExecutor Windows Package"
|
||||
BeginPrompt="正在解压 HostExecutor..."
|
||||
ExtractDialogText="请稍候,正在解压文件..."
|
||||
ExtractPathText="解压路径:"
|
||||
ExtractTitle="解压中"
|
||||
FinishMessage="解压完成!"
|
||||
GUIFlags="8"
|
||||
;!@InstallEnd@!
|
||||
"@
|
||||
|
||||
$configFile = "$outputPath\config.txt"
|
||||
Set-Content -Path $configFile -Value $configContent -Encoding UTF8
|
||||
|
||||
# 创建压缩包
|
||||
& $7zipPath a -sfx7z.sfx -r "$outputSfx" "$buildPath\*" -scsUTF-8 -y
|
||||
Copy-Item "$configFile" -Destination "$outputSfx" -Force
|
||||
|
||||
if (Test-Path $outputSfx) {
|
||||
Write-Host "✅ 7-Zip 自解压包创建成功!"
|
||||
Write-Host "输出文件: $outputSfx"
|
||||
|
||||
$size = (Get-Item $outputSfx).Length / 1MB
|
||||
Write-Host "文件大小: $([math]::Round($size, 2)) MB"
|
||||
} else {
|
||||
Write-Host "❌ 7-Zip 打包失败"
|
||||
}
|
||||
|
||||
- name: Upload Single EXE Package
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: windows-single-exe
|
||||
path: dist/*.exe
|
||||
if: always()
|
||||
@@ -1,311 +0,0 @@
|
||||
# 📖 GitHub Actions 构建完全指南
|
||||
|
||||
## 🎯 功能概述
|
||||
|
||||
本 workflow 支持:
|
||||
|
||||
✅ **自动编译 libcore.aar** - sing-box 核心库
|
||||
✅ **Release 构建** - 默认生成正式版 APK
|
||||
✅ **可配置 API 域名** - 支持自定义后端域名
|
||||
✅ **可配置 OSS 地址** - 支持自定义配置文件 CDN
|
||||
✅ **分架构打包** - 生成 arm64-v8a / armeabi-v7a / x86_64 版本
|
||||
✅ **自动创建 Release** - 推送标签自动发布
|
||||
|
||||
---
|
||||
|
||||
## 🚀 使用方法
|
||||
|
||||
### 方法一:手动触发(推荐,支持自定义配置)
|
||||
|
||||
1. **打开 Actions 页面**
|
||||
- 进入 GitHub 仓库
|
||||
- 点击顶部 **Actions** 标签
|
||||
|
||||
2. **选择 Workflow**
|
||||
- 左侧选择 **Build Android APK**
|
||||
- 右侧点击 **Run workflow** 下拉框
|
||||
|
||||
3. **配置参数**
|
||||
|
||||
| 参数 | 说明 | 默认值 |
|
||||
|-----|------|--------|
|
||||
| **构建类型** | debug 或 release | `release` |
|
||||
| **API 域名** | 后端 API 服务器域名 | `api.maodag.top` |
|
||||
| **OSS 地址 1** | 配置文件 CDN 地址(香港) | `https://ppp2.oss-cn-hongkong.aliyuncs.com/bear1.txt` |
|
||||
| **OSS 地址 2** | 配置文件 CDN 地址(东京) | `https://xgp3.oss-ap-northeast-1.aliyuncs.com/bear1.txt` |
|
||||
| **OSS 地址 3** | 配置文件 CDN 地址(首尔) | `https://xpp4.oss-ap-northeast-2.aliyuncs.com/bear1.txt` |
|
||||
| **OSS 地址 4** | 配置文件 CDN 地址(新加坡) | `https://xpp5.oss-ap-southeast-1.aliyuncs.com/bear1.txt` |
|
||||
|
||||
4. **开始构建**
|
||||
- 点击绿色 **Run workflow** 按钮
|
||||
- 等待约 30 分钟完成
|
||||
|
||||
5. **下载 APK**
|
||||
- 构建完成后,在运行记录页面找到 **Artifacts** 区域
|
||||
- 下载对应架构的 APK
|
||||
|
||||
---
|
||||
|
||||
### 方法二:推送代码自动构建(使用默认配置)
|
||||
|
||||
```bash
|
||||
# 提交代码
|
||||
git add .
|
||||
git commit -m "feat: 新功能"
|
||||
|
||||
# 推送到 main 分支
|
||||
git push origin main
|
||||
```
|
||||
|
||||
**注意:** 自动触发的构建使用默认配置(`api.maodag.top` 和默认 OSS 地址)
|
||||
|
||||
---
|
||||
|
||||
### 方法三:创建 Release 版本
|
||||
|
||||
```bash
|
||||
# 1. 打标签(必须以 v 开头)
|
||||
git tag v1.0.0
|
||||
|
||||
# 2. 推送标签
|
||||
git push origin v1.0.0
|
||||
|
||||
# 3. 自动触发构建并创建 Release
|
||||
# 访问 https://github.com/你的用户名/LighthouseApp/releases 查看
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📦 构建产物
|
||||
|
||||
### APK 命名规则
|
||||
|
||||
```
|
||||
BearVPN-{架构}-{类型}-{日期}-{提交哈希}.apk
|
||||
```
|
||||
|
||||
**示例:**
|
||||
```
|
||||
BearVPN-arm64-v8a-release-20251027-abc1234.apk
|
||||
BearVPN-armeabi-v7a-release-20251027-abc1234.apk
|
||||
BearVPN-x86_64-release-20251027-abc1234.apk
|
||||
```
|
||||
|
||||
### 架构说明
|
||||
|
||||
| 架构 | 适用设备 | 推荐度 |
|
||||
|------|---------|-------|
|
||||
| **arm64-v8a** | 2017年后的现代手机 | ⭐⭐⭐⭐⭐ |
|
||||
| **armeabi-v7a** | 2012-2017年的老旧手机 | ⭐⭐ |
|
||||
| **x86_64** | Android 模拟器 | ⭐⭐⭐ |
|
||||
|
||||
**大多数用户应下载 `arm64-v8a` 版本**
|
||||
|
||||
---
|
||||
|
||||
## 🔧 配置示例
|
||||
|
||||
### 示例 1:修改 API 域名
|
||||
|
||||
如果你的后端部署在 `api.example.com`:
|
||||
|
||||
1. 手动触发 workflow
|
||||
2. 将 **API 域名** 改为 `api.example.com`
|
||||
3. OSS 地址保持默认
|
||||
4. 点击 Run workflow
|
||||
|
||||
### 示例 2:使用自己的 CDN
|
||||
|
||||
如果你有自己的配置文件 CDN:
|
||||
|
||||
1. 手动触发 workflow
|
||||
2. API 域名保持默认
|
||||
3. 修改 **OSS 地址 1-4** 为你的 CDN 地址,例如:
|
||||
- OSS 地址 1: `https://cdn1.example.com/config.txt`
|
||||
- OSS 地址 2: `https://cdn2.example.com/config.txt`
|
||||
- OSS 地址 3: `https://cdn3.example.com/config.txt`
|
||||
- OSS 地址 4: `https://cdn4.example.com/config.txt`
|
||||
4. 点击 Run workflow
|
||||
|
||||
### 示例 3:完全自定义配置
|
||||
|
||||
```yaml
|
||||
构建类型: release
|
||||
API 域名: api.mycompany.com
|
||||
OSS 地址 1: https://config.mycdn.com/v1/nodes.txt
|
||||
OSS 地址 2: https://backup1.mycdn.com/v1/nodes.txt
|
||||
OSS 地址 3: https://backup2.mycdn.com/v1/nodes.txt
|
||||
OSS 地址 4: https://backup3.mycdn.com/v1/nodes.txt
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⏱️ 构建时间
|
||||
|
||||
| 阶段 | 时间 |
|
||||
|-----|------|
|
||||
| 编译 libcore.aar | 10-15 分钟 |
|
||||
| 编译 Flutter APK | 15-20 分钟 |
|
||||
| **总计** | **约 30 分钟** |
|
||||
|
||||
---
|
||||
|
||||
## 🔍 查看构建日志
|
||||
|
||||
1. 打开 Actions 页面
|
||||
2. 点击对应的运行记录
|
||||
3. 点击具体的 job(如 "编译 Android APK")
|
||||
4. 展开步骤查看详细日志
|
||||
|
||||
**关键步骤:**
|
||||
- `⚙️ 配置 API 域名和 OSS 地址` - 查看配置是否正确替换
|
||||
- `🔨 构建 APK` - 查看编译过程
|
||||
- `📦 重命名 APK 文件` - 查看生成的文件名和 MD5
|
||||
|
||||
---
|
||||
|
||||
## 🐛 常见问题
|
||||
|
||||
### Q1: 如何验证配置是否生效?
|
||||
|
||||
**A:** 查看 `⚙️ 配置 API 域名和 OSS 地址` 步骤的日志:
|
||||
|
||||
```
|
||||
🔧 配置构建参数:
|
||||
API 域名: api.example.com
|
||||
OSS 地址 1: https://cdn1.example.com/config.txt
|
||||
...
|
||||
|
||||
✅ 配置替换完成
|
||||
|
||||
📄 查看修改后的配置:
|
||||
static List<String> kr_baseDomains = ["api.example.com","api.example.com"];
|
||||
static String kr_currentDomain = "api.example.com";
|
||||
```
|
||||
|
||||
如果看到你设置的域名,说明配置成功。
|
||||
|
||||
---
|
||||
|
||||
### Q2: Release 构建和 Debug 构建有什么区别?
|
||||
|
||||
| 特性 | Debug | Release |
|
||||
|-----|-------|---------|
|
||||
| **文件大小** | 较大 | 较小(优化后) |
|
||||
| **性能** | 较慢 | 快 |
|
||||
| **调试信息** | 包含 | 移除 |
|
||||
| **代码混淆** | 无 | 有 |
|
||||
| **适用场景** | 开发测试 | 正式发布 |
|
||||
|
||||
**推荐:** 生产环境使用 **release** 构建
|
||||
|
||||
---
|
||||
|
||||
### Q3: 如何使用环境变量配置(不想每次手动输入)?
|
||||
|
||||
在仓库 **Settings → Secrets and variables → Actions → Variables** 添加:
|
||||
|
||||
| 变量名 | 值 |
|
||||
|-------|---|
|
||||
| `DEFAULT_API_DOMAIN` | `api.example.com` |
|
||||
| `DEFAULT_OSS_URL_1` | `https://cdn1.example.com/config.txt` |
|
||||
| `DEFAULT_OSS_URL_2` | `https://cdn2.example.com/config.txt` |
|
||||
| `DEFAULT_OSS_URL_3` | `https://cdn3.example.com/config.txt` |
|
||||
| `DEFAULT_OSS_URL_4` | `https://cdn4.example.com/config.txt` |
|
||||
|
||||
然后修改 workflow 文件,将默认值改为:
|
||||
|
||||
```yaml
|
||||
default: ${{ vars.DEFAULT_API_DOMAIN || 'api.maodag.top' }}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Q4: 构建失败 - "libcore.aar not found"
|
||||
|
||||
**原因:** libcore 编译失败
|
||||
|
||||
**解决:**
|
||||
1. 检查 `build-libcore` job 的日志
|
||||
2. 确认 Go 环境和 gomobile 安装成功
|
||||
3. 确认 libcore 子模块已正确初始化
|
||||
|
||||
---
|
||||
|
||||
### Q5: 如何配置签名(Release 构建必需)?
|
||||
|
||||
详见主文档 `.github/workflows/README.md` 的签名配置章节。
|
||||
|
||||
---
|
||||
|
||||
## 💡 高级技巧
|
||||
|
||||
### 技巧 1:同时构建多个版本
|
||||
|
||||
修改 `build-apk` job:
|
||||
|
||||
```yaml
|
||||
strategy:
|
||||
matrix:
|
||||
build_type: [debug, release]
|
||||
api_domain: ['api.maodag.top', 'api.example.com']
|
||||
```
|
||||
|
||||
这样会生成 4 个 APK(2种类型 × 2个域名)
|
||||
|
||||
---
|
||||
|
||||
### 技巧 2:添加构建完成通知
|
||||
|
||||
在 workflow 最后添加:
|
||||
|
||||
```yaml
|
||||
- name: 📢 发送 Telegram 通知
|
||||
if: success()
|
||||
run: |
|
||||
curl -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \
|
||||
-d chat_id=${{ secrets.TELEGRAM_CHAT_ID }} \
|
||||
-d text="✅ BearVPN 构建成功!下载: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||
```
|
||||
|
||||
需要先配置 `TELEGRAM_BOT_TOKEN` 和 `TELEGRAM_CHAT_ID` Secrets。
|
||||
|
||||
---
|
||||
|
||||
### 技巧 3:缓存加速构建
|
||||
|
||||
workflow 已配置 Go 和 Flutter 缓存,首次构建约 30 分钟,后续构建可缩短至 15-20 分钟。
|
||||
|
||||
---
|
||||
|
||||
## 📊 构建状态徽章
|
||||
|
||||
在 README.md 中添加:
|
||||
|
||||
```markdown
|
||||

|
||||
```
|
||||
|
||||
效果:
|
||||
|
||||
---
|
||||
|
||||
## 🔗 相关文件
|
||||
|
||||
- **主 Workflow:** `.github/workflows/build-android-apk.yml`
|
||||
- **配置文件:** `lib/app/common/app_config.dart`
|
||||
- **libcore 源码:** `libcore/` 子模块
|
||||
|
||||
---
|
||||
|
||||
## 📞 需要帮助?
|
||||
|
||||
- 查看 [GitHub Actions 文档](https://docs.github.com/actions)
|
||||
- 查看构建日志排查问题
|
||||
- 提交 Issue 到仓库
|
||||
|
||||
---
|
||||
|
||||
**生成时间:** 2025-10-27
|
||||
**作者:** Claude Code
|
||||
**版本:** 1.0.0
|
||||
@@ -1,366 +0,0 @@
|
||||
# 🎯 如何在 GitHub 上构建你的应用
|
||||
|
||||
## 📋 前提条件
|
||||
|
||||
✅ 已将配置推送到 GitHub
|
||||
✅ 有 GitHub 账号访问权限
|
||||
|
||||
---
|
||||
|
||||
## 🚀 方法一:手动触发构建(推荐新手)
|
||||
|
||||
### 步骤 1: 访问 GitHub Actions 页面
|
||||
|
||||
1. 打开浏览器,访问你的仓库:
|
||||
```
|
||||
https://github.com/你的用户名/LighthouseApp
|
||||
```
|
||||
|
||||
2. 点击顶部导航栏的 **Actions** 标签
|
||||
```
|
||||
Code Issues Pull requests Actions Projects Wiki Security Insights
|
||||
↑ 点这里
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 步骤 2: 选择 Workflow
|
||||
|
||||
在左侧栏看到可用的 workflows:
|
||||
|
||||
```
|
||||
All workflows
|
||||
├── Build Android APK ← 仅构建 Android
|
||||
├── Build Multi-Platform ← 构建所有平台 ⭐推荐
|
||||
└── Build Clash Core
|
||||
```
|
||||
|
||||
**选择建议:**
|
||||
- 🟢 **首次测试:** 选择 **Build Android APK**(快速)
|
||||
- 🔵 **正式发布:** 选择 **Build Multi-Platform**(全平台)
|
||||
|
||||
---
|
||||
|
||||
### 步骤 3: 运行 Workflow
|
||||
|
||||
1. 点击选择的 workflow 名称
|
||||
|
||||
2. 右侧出现 **"Run workflow"** 下拉按钮
|
||||
```
|
||||
[Run workflow ▼]
|
||||
```
|
||||
|
||||
3. 点击下拉按钮,展开配置面板
|
||||
|
||||
---
|
||||
|
||||
### 步骤 4: 配置参数
|
||||
|
||||
#### 如果选择 "Build Android APK":
|
||||
|
||||
```yaml
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Run workflow │
|
||||
├─────────────────────────────────────────┤
|
||||
│ Use workflow from │
|
||||
│ Branch: main [▼] │
|
||||
│ │
|
||||
│ 构建类型 │
|
||||
│ ○ debug │
|
||||
│ ● release │
|
||||
│ │
|
||||
│ API 域名 │
|
||||
│ [api.maodag.top________________] │
|
||||
│ │
|
||||
│ OSS 配置地址 1 │
|
||||
│ [https://ppp2.oss-cn-hongkong...] │
|
||||
│ │
|
||||
│ OSS 配置地址 2 │
|
||||
│ [https://xgp3.oss-ap-northeast-1...] │
|
||||
│ │
|
||||
│ OSS 配置地址 3 │
|
||||
│ [https://xpp4.oss-ap-northeast-2...] │
|
||||
│ │
|
||||
│ OSS 配置地址 4 │
|
||||
│ [https://xpp5.oss-ap-southeast-1...] │
|
||||
│ │
|
||||
│ [Cancel] [Run workflow] │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**配置说明:**
|
||||
- **构建类型:** 选择 `release`(生产环境)
|
||||
- **API 域名:** 填写你的后端域名(默认 `api.maodag.top`)
|
||||
- **OSS 地址:** 保持默认或填写你的 CDN 地址
|
||||
|
||||
#### 如果选择 "Build Multi-Platform":
|
||||
|
||||
额外多一个参数:
|
||||
```yaml
|
||||
│ 构建平台 │
|
||||
│ [android,windows,macos,linux______] │
|
||||
```
|
||||
|
||||
**选项:**
|
||||
- `android` - 仅 Android
|
||||
- `windows` - 仅 Windows
|
||||
- `macos` - 仅 macOS
|
||||
- `linux` - 仅 Linux
|
||||
- `android,windows` - Android + Windows
|
||||
- `android,windows,macos,linux` - 全部平台 ⭐
|
||||
|
||||
---
|
||||
|
||||
### 步骤 5: 开始构建
|
||||
|
||||
1. 检查所有参数是否正确
|
||||
|
||||
2. 点击绿色的 **"Run workflow"** 按钮
|
||||
|
||||
3. 页面刷新,顶部出现黄色进度条:
|
||||
```
|
||||
⚠️ Build Android APK #1
|
||||
Queued - This workflow is in queue waiting to run
|
||||
```
|
||||
|
||||
4. 几秒后变为蓝色(运行中):
|
||||
```
|
||||
🔵 Build Android APK #1
|
||||
In progress - This workflow is currently running
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 步骤 6: 监控构建进度
|
||||
|
||||
1. 点击运行记录(蓝色行)
|
||||
|
||||
2. 看到构建阶段:
|
||||
```
|
||||
编译 libcore.aar 🔵 Running (15分钟)
|
||||
编译 Android APK ⏸️ Pending
|
||||
```
|
||||
|
||||
3. 可以展开查看实时日志:
|
||||
```
|
||||
▼ 编译 libcore.aar
|
||||
▼ 📦 编译 libcore.aar
|
||||
🚀 开始编译 libcore...
|
||||
[gomobile] installing...
|
||||
✅ libcore.aar 生成成功
|
||||
```
|
||||
|
||||
4. 等待所有阶段完成(约 30-85 分钟)
|
||||
|
||||
---
|
||||
|
||||
### 步骤 7: 下载构建产物
|
||||
|
||||
构建成功后:
|
||||
|
||||
1. 向下滚动到 **Artifacts** 区域
|
||||
```
|
||||
📦 Artifacts
|
||||
|
||||
Produced during runtime
|
||||
|
||||
Name Size Expires
|
||||
apk-arm64-v8a-release 42.5 MB in 30 days [Download]
|
||||
apk-armeabi-v7a-release 38.2 MB in 30 days [Download]
|
||||
apk-x86_64-release 45.1 MB in 30 days [Download]
|
||||
```
|
||||
|
||||
2. 点击 **[Download]** 下载对应文件
|
||||
|
||||
3. 下载的是 ZIP 文件,解压后得到 APK:
|
||||
```
|
||||
apk-arm64-v8a-release.zip
|
||||
└── BearVPN-android-arm64-v8a-release-20251027-abc1234.apk
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🏷️ 方法二:推送标签自动构建
|
||||
|
||||
这种方式会自动构建并创建 GitHub Release。
|
||||
|
||||
### 步骤 1: 在本地打标签
|
||||
|
||||
```bash
|
||||
cd /Users/mac/Project/Dart/LighthouseApp
|
||||
|
||||
# 创建标签(版本号必须以 v 开头)
|
||||
git tag v1.0.0
|
||||
|
||||
# 查看标签
|
||||
git tag
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 步骤 2: 推送标签到 GitHub
|
||||
|
||||
```bash
|
||||
git push origin v1.0.0
|
||||
```
|
||||
|
||||
**输出:**
|
||||
```
|
||||
Enumerating objects: 1, done.
|
||||
Counting objects: 100% (1/1), done.
|
||||
Writing objects: 100% (1/1), 160 bytes | 160.00 KiB/s, done.
|
||||
Total 1 (delta 0), reused 0 (delta 0)
|
||||
To github.com:你的用户名/LighthouseApp.git
|
||||
* [new tag] v1.0.0 -> v1.0.0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 步骤 3: 自动触发构建
|
||||
|
||||
1. 推送标签后,GitHub 自动检测到标签
|
||||
2. 触发 **Build Multi-Platform** workflow
|
||||
3. 使用默认配置构建所有平台
|
||||
|
||||
---
|
||||
|
||||
### 步骤 4: 查看构建进度
|
||||
|
||||
1. 访问 Actions 页面
|
||||
2. 看到自动创建的构建任务:
|
||||
```
|
||||
🔵 Build Multi-Platform
|
||||
v1.0.0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 步骤 5: 下载 Release
|
||||
|
||||
构建成功后:
|
||||
|
||||
1. 访问 **Releases** 页面:
|
||||
```
|
||||
https://github.com/你的用户名/LighthouseApp/releases
|
||||
```
|
||||
|
||||
2. 看到新创建的 Release:
|
||||
```
|
||||
📦 v1.0.0
|
||||
|
||||
Latest Pre-release
|
||||
|
||||
🎉 BearVPN 多平台版本发布
|
||||
|
||||
Assets (7)
|
||||
├── BearVPN-android-arm64-v8a-release-*.apk 42.5 MB
|
||||
├── BearVPN-android-armeabi-v7a-release-*.apk 38.2 MB
|
||||
├── BearVPN-android-x86_64-release-*.apk 45.1 MB
|
||||
├── BearVPN-windows-x64-release-*.zip 52.3 MB
|
||||
├── BearVPN-macos-release-*.zip 48.7 MB
|
||||
├── BearVPN-linux-x64-release-*.tar.gz 45.9 MB
|
||||
└── Source code (zip)
|
||||
```
|
||||
|
||||
3. 直接下载需要的文件
|
||||
|
||||
---
|
||||
|
||||
## ⏱️ 构建时间参考
|
||||
|
||||
| Workflow | 平台 | 时间 |
|
||||
|---------|------|------|
|
||||
| Build Android APK | Android | 30 分钟 |
|
||||
| Build Multi-Platform | Android | 35 分钟 |
|
||||
| Build Multi-Platform | Windows | 30 分钟 |
|
||||
| Build Multi-Platform | macOS | 35 分钟 |
|
||||
| Build Multi-Platform | Linux | 30 分钟 |
|
||||
| Build Multi-Platform | 全部平台 | **60-85 分钟** |
|
||||
|
||||
**提示:** 平台是并行构建的,不是累加时间。
|
||||
|
||||
---
|
||||
|
||||
## 🎯 推荐流程
|
||||
|
||||
### 首次测试(验证流程)
|
||||
|
||||
```
|
||||
1. 选择 "Build Android APK"
|
||||
2. 使用默认配置
|
||||
3. 运行构建
|
||||
4. 等待 30 分钟
|
||||
5. 下载 arm64-v8a APK
|
||||
6. 安装测试
|
||||
```
|
||||
|
||||
### 正式发布(生产环境)
|
||||
|
||||
```
|
||||
1. 确保代码已测试
|
||||
2. 本地打标签: git tag v1.0.0
|
||||
3. 推送标签: git push origin v1.0.0
|
||||
4. 等待 60-85 分钟
|
||||
5. 在 Releases 页面下载所有平台
|
||||
6. 分发给用户
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🐛 常见问题
|
||||
|
||||
### Q1: 找不到 "Run workflow" 按钮
|
||||
|
||||
**原因:** 可能还在查看历史运行记录
|
||||
|
||||
**解决:**
|
||||
1. 点击左侧的 workflow 名称
|
||||
2. 确保在 workflow 主页面
|
||||
3. 右侧会出现 "Run workflow" 按钮
|
||||
|
||||
---
|
||||
|
||||
### Q2: 构建失败
|
||||
|
||||
**排查步骤:**
|
||||
1. 点击失败的运行记录
|
||||
2. 展开红色的步骤
|
||||
3. 查看错误日志
|
||||
4. 常见错误:
|
||||
- libcore 编译失败 → 检查 Go 环境
|
||||
- APK 构建失败 → 检查 Flutter 依赖
|
||||
|
||||
---
|
||||
|
||||
### Q3: Artifacts 下载后是空的
|
||||
|
||||
**原因:** 构建可能失败
|
||||
|
||||
**解决:**
|
||||
1. 检查构建日志
|
||||
2. 确保所有步骤都是绿色 ✅
|
||||
3. 重新运行构建
|
||||
|
||||
---
|
||||
|
||||
### Q4: 如何修改 API 域名
|
||||
|
||||
**方法 1:** 手动触发时修改参数
|
||||
|
||||
**方法 2:** 修改代码中的默认值
|
||||
```dart
|
||||
// lib/app/common/app_config.dart
|
||||
static String kr_currentDomain = "api.example.com";
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📞 需要帮助?
|
||||
|
||||
- **查看详细文档:** `.github/workflows/INDEX.md`
|
||||
- **快速开始:** `.github/workflows/QUICKSTART.md`
|
||||
- **问题反馈:** GitHub Issues
|
||||
|
||||
---
|
||||
|
||||
**准备好了吗?立即开始构建!** 🚀
|
||||
@@ -1,318 +0,0 @@
|
||||
# 📚 GitHub Actions 构建系统总览
|
||||
|
||||
## 🎯 快速导航
|
||||
|
||||
| 文档 | 用途 | 适合人群 |
|
||||
|-----|------|---------|
|
||||
| **[QUICKSTART.md](./QUICKSTART.md)** | 3步快速开始 | 新手 |
|
||||
| **[README.md](./README.md)** | 基础说明 | 所有人 |
|
||||
| **[BUILD_GUIDE.md](./BUILD_GUIDE.md)** | Android 详细指南 | Android 开发者 |
|
||||
| **[MULTIPLATFORM_GUIDE.md](./MULTIPLATFORM_GUIDE.md)** | 多平台构建指南 | 全平台开发者 |
|
||||
|
||||
---
|
||||
|
||||
## 📋 可用的 Workflows
|
||||
|
||||
### 1. `build-android-apk.yml` - Android 专用构建
|
||||
|
||||
**功能:**
|
||||
- ✅ 编译 libcore.aar
|
||||
- ✅ 构建 Android APK (arm64/armv7/x86_64)
|
||||
- ✅ 配置 API 域名和 OSS 地址
|
||||
- ✅ Release 构建
|
||||
|
||||
**触发条件:**
|
||||
- 推送到 main/develop 分支
|
||||
- 创建 v* 标签
|
||||
- 手动触发
|
||||
|
||||
**文档:** [BUILD_GUIDE.md](./BUILD_GUIDE.md)
|
||||
|
||||
---
|
||||
|
||||
### 2. `build-multiplatform.yml` - 多平台构建 ⭐推荐
|
||||
|
||||
**功能:**
|
||||
- ✅ Android APK
|
||||
- ✅ Windows 可执行文件
|
||||
- ✅ macOS 应用
|
||||
- ✅ Linux 可执行文件
|
||||
- ✅ 可选择构建平台
|
||||
- ✅ 统一配置管理
|
||||
|
||||
**触发条件:**
|
||||
- 推送到 main/develop 分支
|
||||
- 创建 v* 标签
|
||||
- 手动触发
|
||||
|
||||
**文档:** [MULTIPLATFORM_GUIDE.md](./MULTIPLATFORM_GUIDE.md)
|
||||
|
||||
---
|
||||
|
||||
### 3. `build-clash-core.yml` - Clash 核心编译
|
||||
|
||||
**功能:**
|
||||
- 编译 Clash Meta 核心
|
||||
- 支持多架构 (arm64/armv7/x86_64)
|
||||
|
||||
**触发条件:**
|
||||
- core/ 目录变更
|
||||
- 手动触发
|
||||
|
||||
---
|
||||
|
||||
## 🚀 使用建议
|
||||
|
||||
### 场景 1: 日常开发(仅 Android)
|
||||
|
||||
**使用:** `build-android-apk.yml`
|
||||
```bash
|
||||
# 推送代码自动触发
|
||||
git push origin main
|
||||
```
|
||||
|
||||
**时间:** 约 30 分钟
|
||||
**产物:** 3 个 Android APK
|
||||
|
||||
---
|
||||
|
||||
### 场景 2: 正式发布(所有平台)
|
||||
|
||||
**使用:** `build-multiplatform.yml`
|
||||
```bash
|
||||
# 1. 打标签
|
||||
git tag v1.0.0
|
||||
git push origin v1.0.0
|
||||
|
||||
# 2. 自动构建所有平台并创建 Release
|
||||
```
|
||||
|
||||
**时间:** 约 60-85 分钟(并行)
|
||||
**产物:** Android + Windows + macOS + Linux
|
||||
|
||||
---
|
||||
|
||||
### 场景 3: 测试特定平台
|
||||
|
||||
**使用:** `build-multiplatform.yml` 手动触发
|
||||
```yaml
|
||||
构建平台: android,windows # 只构建这两个
|
||||
```
|
||||
|
||||
**时间:** 约 35 分钟
|
||||
**产物:** 指定平台
|
||||
|
||||
---
|
||||
|
||||
## 📦 构建产物对比
|
||||
|
||||
### Android 专用构建
|
||||
|
||||
```
|
||||
BearVPN-android-arm64-v8a-release-20251027-abc1234.apk
|
||||
BearVPN-android-armeabi-v7a-release-20251027-abc1234.apk
|
||||
BearVPN-android-x86_64-release-20251027-abc1234.apk
|
||||
```
|
||||
|
||||
### 多平台构建
|
||||
|
||||
```
|
||||
# Android
|
||||
BearVPN-android-arm64-v8a-release-20251027-abc1234.apk
|
||||
BearVPN-android-armeabi-v7a-release-20251027-abc1234.apk
|
||||
BearVPN-android-x86_64-release-20251027-abc1234.apk
|
||||
|
||||
# Windows
|
||||
BearVPN-windows-x64-release-20251027-abc1234.zip
|
||||
|
||||
# macOS
|
||||
BearVPN-macos-release-20251027-abc1234.zip
|
||||
|
||||
# Linux
|
||||
BearVPN-linux-x64-release-20251027-abc1234.tar.gz
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ 配置参数说明
|
||||
|
||||
### 所有 Workflow 通用参数
|
||||
|
||||
| 参数 | 默认值 | 说明 |
|
||||
|-----|-------|------|
|
||||
| **构建类型** | `release` | debug 或 release |
|
||||
| **API 域名** | `api.maodag.top` | 后端服务器地址 |
|
||||
| **OSS 地址 1** | 香港 CDN | 配置文件源 |
|
||||
| **OSS 地址 2** | 东京 CDN | 备用配置源 |
|
||||
| **OSS 地址 3** | 首尔 CDN | 备用配置源 |
|
||||
| **OSS 地址 4** | 新加坡 CDN | 备用配置源 |
|
||||
|
||||
### 多平台专用参数
|
||||
|
||||
| 参数 | 默认值 | 说明 |
|
||||
|-----|-------|------|
|
||||
| **构建平台** | `android,windows,macos,linux` | 选择构建的平台 |
|
||||
|
||||
---
|
||||
|
||||
## ⏱️ 构建时间对比
|
||||
|
||||
| Workflow | 单平台 | 全平台 |
|
||||
|---------|-------|-------|
|
||||
| **Android 专用** | 30分钟 | - |
|
||||
| **多平台** | 30-40分钟 | 60-85分钟 |
|
||||
|
||||
---
|
||||
|
||||
## 🎯 选择 Workflow 的建议
|
||||
|
||||
### 选择 `build-android-apk.yml` 如果:
|
||||
|
||||
- ✅ 只需要 Android 版本
|
||||
- ✅ 快速迭代开发
|
||||
- ✅ CI/CD 自动触发
|
||||
|
||||
### 选择 `build-multiplatform.yml` 如果:
|
||||
|
||||
- ✅ 需要桌面版本
|
||||
- ✅ 正式版本发布
|
||||
- ✅ 需要灵活选择平台
|
||||
|
||||
---
|
||||
|
||||
## 🔧 环境配置
|
||||
|
||||
### 必需的 Secrets(用于 Release 签名)
|
||||
|
||||
**Android:**
|
||||
```
|
||||
KEYSTORE_BASE64
|
||||
KEYSTORE_PASSWORD
|
||||
KEY_ALIAS
|
||||
KEY_PASSWORD
|
||||
```
|
||||
|
||||
**macOS:**
|
||||
```
|
||||
MACOS_CERTIFICATE_BASE64
|
||||
MACOS_CERTIFICATE_PASSWORD
|
||||
```
|
||||
|
||||
**Windows:**
|
||||
```
|
||||
WINDOWS_CERTIFICATE_BASE64
|
||||
WINDOWS_CERTIFICATE_PASSWORD
|
||||
```
|
||||
|
||||
**说明:** 如果不配置签名,可以使用 debug 构建。
|
||||
|
||||
---
|
||||
|
||||
## 📊 构建流程图
|
||||
|
||||
### Android 专用
|
||||
|
||||
```
|
||||
Checkout 代码
|
||||
↓
|
||||
编译 libcore.aar (15min)
|
||||
↓
|
||||
配置 API/OSS
|
||||
↓
|
||||
构建 APK (20min)
|
||||
↓
|
||||
上传产物
|
||||
```
|
||||
|
||||
### 多平台
|
||||
|
||||
```
|
||||
Checkout 代码
|
||||
↓
|
||||
编译 libcore.aar (15min)
|
||||
↓
|
||||
├─→ Android (20min) ─┐
|
||||
├─→ Windows (15min) ─┤
|
||||
├─→ macOS (20min) ─┼─→ 创建 Release
|
||||
└─→ Linux (15min) ─┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🐛 故障排查
|
||||
|
||||
### 问题 1: libcore 编译失败
|
||||
|
||||
**检查:**
|
||||
1. Go 环境是否正常
|
||||
2. gomobile 是否安装
|
||||
3. 网络连接(下载依赖)
|
||||
|
||||
**日志位置:** `build-libcore` job
|
||||
|
||||
---
|
||||
|
||||
### 问题 2: Android 构建失败
|
||||
|
||||
**常见原因:**
|
||||
- libcore.aar 未生成
|
||||
- Gradle 依赖问题
|
||||
- 签名配置错误
|
||||
|
||||
**解决:** 查看 `build-android` job 日志
|
||||
|
||||
---
|
||||
|
||||
### 问题 3: macOS 构建失败
|
||||
|
||||
**常见原因:**
|
||||
- 签名证书未配置
|
||||
- Xcode 版本不兼容
|
||||
|
||||
**解决:**
|
||||
- 使用 debug 构建
|
||||
- 或配置签名证书
|
||||
|
||||
---
|
||||
|
||||
## 💡 高级用法
|
||||
|
||||
### 1. 定时构建
|
||||
|
||||
添加到 workflow:
|
||||
|
||||
```yaml
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 2 * * *' # 每天UTC 2:00
|
||||
```
|
||||
|
||||
### 2. PR 自动构建
|
||||
|
||||
已配置:推送 PR 到 main 分支自动触发
|
||||
|
||||
### 3. 构建通知
|
||||
|
||||
可集成 Telegram/Slack/钉钉通知
|
||||
|
||||
---
|
||||
|
||||
## 📞 获取帮助
|
||||
|
||||
- **查看文档:** 本目录下的 Markdown 文件
|
||||
- **查看日志:** GitHub Actions 页面
|
||||
- **提交问题:** Issues 页面
|
||||
|
||||
---
|
||||
|
||||
## 🔗 相关链接
|
||||
|
||||
- [GitHub Actions 文档](https://docs.github.com/actions)
|
||||
- [Flutter CI/CD](https://docs.flutter.dev/deployment/cd)
|
||||
- [sing-box 文档](https://sing-box.sagernet.org/)
|
||||
|
||||
---
|
||||
|
||||
**版本:** 1.0.0
|
||||
**更新:** 2025-10-27
|
||||
**作者:** Claude Code
|
||||
@@ -1,407 +0,0 @@
|
||||
# 🌐 多平台构建完全指南
|
||||
|
||||
## 📋 支持的平台
|
||||
|
||||
✅ **Android** - arm64-v8a / armeabi-v7a / x86_64
|
||||
✅ **Windows** - x64
|
||||
✅ **macOS** - Universal (Intel + Apple Silicon)
|
||||
✅ **Linux** - x64
|
||||
|
||||
---
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 方法 1: 构建所有平台(推荐)
|
||||
|
||||
1. 打开 GitHub Actions 页面
|
||||
2. 选择 **Build Multi-Platform**
|
||||
3. 点击 **Run workflow**
|
||||
4. 配置参数:
|
||||
|
||||
```yaml
|
||||
构建类型: release
|
||||
API 域名: api.maodag.top
|
||||
OSS 地址 1-4: 使用默认值
|
||||
构建平台: android,windows,macos,linux # 构建所有平台
|
||||
```
|
||||
|
||||
5. 点击 **Run workflow** 开始
|
||||
|
||||
---
|
||||
|
||||
### 方法 2: 仅构建指定平台
|
||||
|
||||
**场景:** 只需要 Android 和 Windows 版本
|
||||
|
||||
```yaml
|
||||
构建平台: android,windows # 只构建这两个平台
|
||||
```
|
||||
|
||||
**可选值:**
|
||||
- `android` - 仅 Android
|
||||
- `windows` - 仅 Windows
|
||||
- `macos` - 仅 macOS
|
||||
- `linux` - 仅 Linux
|
||||
- `android,windows` - Android + Windows
|
||||
- `android,windows,macos,linux` - 全部平台
|
||||
|
||||
---
|
||||
|
||||
## ⏱️ 构建时间估算
|
||||
|
||||
| 平台 | 时间 | 说明 |
|
||||
|-----|------|-----|
|
||||
| **libcore** | 10-15分钟 | Android 依赖 |
|
||||
| **Android** | 15-20分钟 | 3个架构APK |
|
||||
| **Windows** | 10-15分钟 | x64可执行文件 |
|
||||
| **macOS** | 15-20分钟 | Universal应用 |
|
||||
| **Linux** | 10-15分钟 | x64可执行文件 |
|
||||
| **总计(全平台)** | **60-85分钟** | 并行构建 |
|
||||
|
||||
---
|
||||
|
||||
## 📦 构建产物
|
||||
|
||||
### Android
|
||||
|
||||
```
|
||||
BearVPN-android-arm64-v8a-release-20251027-abc1234.apk (推荐)
|
||||
BearVPN-android-armeabi-v7a-release-20251027-abc1234.apk (老设备)
|
||||
BearVPN-android-x86_64-release-20251027-abc1234.apk (模拟器)
|
||||
```
|
||||
|
||||
**安装:** 直接点击APK安装
|
||||
|
||||
---
|
||||
|
||||
### Windows
|
||||
|
||||
```
|
||||
BearVPN-windows-x64-release-20251027-abc1234.zip
|
||||
```
|
||||
|
||||
**内容结构:**
|
||||
```
|
||||
BearVPN.exe # 主程序
|
||||
flutter_windows.dll # Flutter运行时
|
||||
data/ # 资源文件
|
||||
```
|
||||
|
||||
**运行:**
|
||||
1. 解压 ZIP 文件
|
||||
2. 双击 `BearVPN.exe` 运行
|
||||
3. 如提示缺少依赖,安装 [VC++ Redistributable](https://aka.ms/vs/17/release/vc_redist.x64.exe)
|
||||
|
||||
---
|
||||
|
||||
### macOS
|
||||
|
||||
```
|
||||
BearVPN-macos-release-20251027-abc1234.zip
|
||||
```
|
||||
|
||||
**内容:**
|
||||
```
|
||||
BearVPN.app # 应用程序包(Universal)
|
||||
```
|
||||
|
||||
**安装:**
|
||||
1. 解压 ZIP 文件
|
||||
2. 将 `BearVPN.app` 拖到 **应用程序** 文件夹
|
||||
3. 首次运行右键点击 → 打开(绕过 Gatekeeper)
|
||||
|
||||
**支持架构:**
|
||||
- Intel (x86_64)
|
||||
- Apple Silicon (arm64)
|
||||
|
||||
---
|
||||
|
||||
### Linux
|
||||
|
||||
```
|
||||
BearVPN-linux-x64-release-20251027-abc1234.tar.gz
|
||||
```
|
||||
|
||||
**内容结构:**
|
||||
```
|
||||
bearvpn # 可执行文件
|
||||
lib/ # 共享库
|
||||
data/ # 资源文件
|
||||
```
|
||||
|
||||
**运行:**
|
||||
```bash
|
||||
# 1. 解压
|
||||
tar -xzf BearVPN-linux-x64-release-*.tar.gz
|
||||
cd bundle
|
||||
|
||||
# 2. 添加执行权限
|
||||
chmod +x bearvpn
|
||||
|
||||
# 3. 运行
|
||||
./bearvpn
|
||||
```
|
||||
|
||||
**依赖要求:**
|
||||
- GTK+ 3
|
||||
- libstdc++12
|
||||
|
||||
**安装依赖(Ubuntu/Debian):**
|
||||
```bash
|
||||
sudo apt-get install libgtk-3-0 libstdc++6
|
||||
```
|
||||
|
||||
**安装依赖(Fedora/RHEL):**
|
||||
```bash
|
||||
sudo dnf install gtk3 libstdc++
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 配置说明
|
||||
|
||||
### 所有平台通用配置
|
||||
|
||||
| 参数 | 默认值 | 说明 |
|
||||
|-----|-------|------|
|
||||
| **构建类型** | `release` | debug 或 release |
|
||||
| **API 域名** | `api.maodag.top` | 后端服务器 |
|
||||
| **OSS 地址 1** | 香港 CDN | 配置文件源 |
|
||||
| **OSS 地址 2** | 东京 CDN | 备用源 |
|
||||
| **OSS 地址 3** | 首尔 CDN | 备用源 |
|
||||
| **OSS 地址 4** | 新加坡 CDN | 备用源 |
|
||||
| **构建平台** | `android,windows,macos,linux` | 选择平台 |
|
||||
|
||||
---
|
||||
|
||||
## 📥 下载构建产物
|
||||
|
||||
### 从 Actions 页面下载
|
||||
|
||||
1. 打开运行记录
|
||||
2. 滚动到 **Artifacts** 区域
|
||||
3. 下载对应平台:
|
||||
- `android-apk` - Android APK (所有架构)
|
||||
- `windows-x64` - Windows 可执行程序
|
||||
- `macos-app` - macOS 应用
|
||||
- `linux-x64` - Linux 可执行程序
|
||||
|
||||
### 从 Releases 下载(推送标签时)
|
||||
|
||||
1. 访问 Releases 页面
|
||||
2. 选择版本
|
||||
3. 在 Assets 中下载对应平台文件
|
||||
|
||||
---
|
||||
|
||||
## 🎯 使用场景
|
||||
|
||||
### 场景 1: 开发测试(仅 Android)
|
||||
|
||||
```yaml
|
||||
构建类型: debug
|
||||
构建平台: android
|
||||
API 域名: api.test.com
|
||||
```
|
||||
|
||||
**优点:** 快速,约20分钟
|
||||
|
||||
---
|
||||
|
||||
### 场景 2: 正式发布(全平台)
|
||||
|
||||
```yaml
|
||||
构建类型: release
|
||||
构建平台: android,windows,macos,linux
|
||||
API 域名: api.maodag.top
|
||||
```
|
||||
|
||||
**优点:** 一次构建,覆盖所有用户
|
||||
|
||||
---
|
||||
|
||||
### 场景 3: 仅桌面平台
|
||||
|
||||
```yaml
|
||||
构建类型: release
|
||||
构建平台: windows,macos,linux
|
||||
```
|
||||
|
||||
**用途:** 桌面版更新
|
||||
|
||||
---
|
||||
|
||||
## 🐛 常见问题
|
||||
|
||||
### Q1: Windows 构建失败 - "7z command not found"
|
||||
|
||||
**原因:** Windows runner 默认有 7z
|
||||
|
||||
**解决:** 检查构建日志,可能是其他错误
|
||||
|
||||
---
|
||||
|
||||
### Q2: macOS 构建失败 - "Code signing required"
|
||||
|
||||
**原因:** Release 构建需要签名
|
||||
|
||||
**解决方案:**
|
||||
|
||||
**方法 1:** 使用 debug 构建(测试用)
|
||||
|
||||
**方法 2:** 配置签名证书(生产用)
|
||||
```yaml
|
||||
# 在 Secrets 中添加
|
||||
MACOS_CERTIFICATE_BASE64
|
||||
MACOS_CERTIFICATE_PASSWORD
|
||||
MACOS_KEYCHAIN_PASSWORD
|
||||
PROVISIONING_PROFILE_BASE64
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Q3: Linux 运行时提示缺少库
|
||||
|
||||
**错误:**
|
||||
```
|
||||
error while loading shared libraries: libgtk-3.so.0
|
||||
```
|
||||
|
||||
**解决:**
|
||||
```bash
|
||||
sudo apt-get update
|
||||
sudo apt-get install libgtk-3-0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Q4: 如何只更新某个平台?
|
||||
|
||||
**方法 1:** 手动触发指定平台
|
||||
```yaml
|
||||
构建平台: android # 只构建 Android
|
||||
```
|
||||
|
||||
**方法 2:** 创建平台专用 workflow(推荐)
|
||||
|
||||
---
|
||||
|
||||
### Q5: 构建时间太长怎么办?
|
||||
|
||||
**优化方案:**
|
||||
|
||||
1. **仅构建需要的平台**
|
||||
```yaml
|
||||
构建平台: android # 而不是全部
|
||||
```
|
||||
|
||||
2. **使用缓存**(已配置)
|
||||
- Flutter SDK 缓存
|
||||
- Gradle 缓存
|
||||
- Go modules 缓存
|
||||
|
||||
3. **分批构建**
|
||||
- 白天构建 Android/Windows
|
||||
- 晚上构建 macOS/Linux
|
||||
|
||||
---
|
||||
|
||||
## 💡 高级用法
|
||||
|
||||
### 技巧 1: 矩阵构建多个配置
|
||||
|
||||
修改 workflow:
|
||||
|
||||
```yaml
|
||||
strategy:
|
||||
matrix:
|
||||
api_domain: ['api.prod.com', 'api.test.com']
|
||||
platform: ['android', 'windows', 'macos', 'linux']
|
||||
```
|
||||
|
||||
**效果:** 生成 8 个版本(2域名 × 4平台)
|
||||
|
||||
---
|
||||
|
||||
### 技巧 2: 定时自动构建
|
||||
|
||||
添加到 workflow:
|
||||
|
||||
```yaml
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 2 * * *' # 每天凌晨2点(UTC)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 技巧 3: 构建完成通知
|
||||
|
||||
添加 Telegram 通知:
|
||||
|
||||
```yaml
|
||||
- name: 📢 通知
|
||||
run: |
|
||||
curl -X POST "https://api.telegram.org/bot${{ secrets.TG_BOT_TOKEN }}/sendMessage" \
|
||||
-d chat_id=${{ secrets.TG_CHAT_ID }} \
|
||||
-d text="✅ 多平台构建完成!"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 构建并行示意图
|
||||
|
||||
```
|
||||
libcore (15min)
|
||||
↓
|
||||
├─→ Android (20min) ─┐
|
||||
├─→ Windows (15min) ─┤
|
||||
├─→ macOS (20min) ─┼─→ Release (2min)
|
||||
└─→ Linux (15min) ─┘
|
||||
|
||||
总时间: 约 35-40分钟(并行)
|
||||
如果串行: 约 85分钟
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔐 安全建议
|
||||
|
||||
### Release 版本签名
|
||||
|
||||
**Android:**
|
||||
```yaml
|
||||
# Secrets 配置
|
||||
KEYSTORE_BASE64
|
||||
KEYSTORE_PASSWORD
|
||||
KEY_ALIAS
|
||||
KEY_PASSWORD
|
||||
```
|
||||
|
||||
**macOS:**
|
||||
```yaml
|
||||
# Secrets 配置
|
||||
MACOS_CERTIFICATE_BASE64
|
||||
MACOS_CERTIFICATE_PASSWORD
|
||||
```
|
||||
|
||||
**Windows:**
|
||||
```yaml
|
||||
# Secrets 配置
|
||||
WINDOWS_CERTIFICATE_BASE64
|
||||
WINDOWS_CERTIFICATE_PASSWORD
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📞 需要帮助?
|
||||
|
||||
- 查看 [GitHub Actions 文档](https://docs.github.com/actions)
|
||||
- 查看 [Flutter 桌面支持](https://docs.flutter.dev/desktop)
|
||||
- 提交 Issue
|
||||
|
||||
---
|
||||
|
||||
**版本:** 1.0.0
|
||||
**更新:** 2025-10-27
|
||||
**作者:** Claude Code
|
||||
@@ -1,78 +0,0 @@
|
||||
# 🚀 GitHub Actions 快速开始
|
||||
|
||||
## 📋 3 步开始使用
|
||||
|
||||
### 步骤 1: 推送配置文件
|
||||
|
||||
```bash
|
||||
cd /Users/mac/Project/Dart/LighthouseApp
|
||||
|
||||
git add .github/workflows/
|
||||
git commit -m "feat: 添加 GitHub Actions 自动构建配置"
|
||||
git push origin main
|
||||
```
|
||||
|
||||
### 步骤 2: 手动触发构建
|
||||
|
||||
1. 打开 GitHub 仓库页面
|
||||
2. 点击 **Actions** 标签
|
||||
3. 左侧选择 **Build Android APK**
|
||||
4. 右侧点击 **Run workflow** 下拉按钮
|
||||
5. 配置参数(可以使用默认值):
|
||||
|
||||
```
|
||||
构建类型: release
|
||||
API 域名: api.maodag.top
|
||||
OSS 地址 1-4: 使用默认值
|
||||
```
|
||||
|
||||
6. 点击绿色 **Run workflow** 按钮
|
||||
|
||||
### 步骤 3: 下载 APK
|
||||
|
||||
- 等待约 30 分钟
|
||||
- 在构建记录页面找到 **Artifacts**
|
||||
- 下载 `apk-arm64-v8a-release`(推荐)
|
||||
|
||||
---
|
||||
|
||||
## 🎯 自定义配置示例
|
||||
|
||||
### 场景 1: 更换 API 域名
|
||||
|
||||
```yaml
|
||||
构建类型: release
|
||||
API 域名: api.example.com ← 修改这里
|
||||
OSS 地址: 保持默认
|
||||
```
|
||||
|
||||
### 场景 2: 使用自己的 CDN
|
||||
|
||||
```yaml
|
||||
构建类型: release
|
||||
API 域名: api.maodag.top
|
||||
OSS 地址 1: https://your-cdn.com/config1.txt ← 修改这里
|
||||
OSS 地址 2: https://your-cdn.com/config2.txt ← 修改这里
|
||||
OSS 地址 3: https://your-cdn.com/config3.txt ← 修改这里
|
||||
OSS 地址 4: https://your-cdn.com/config4.txt ← 修改这里
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📖 详细文档
|
||||
|
||||
- **完整指南:** [BUILD_GUIDE.md](./BUILD_GUIDE.md)
|
||||
- **基础说明:** [README.md](./README.md)
|
||||
|
||||
---
|
||||
|
||||
## ✅ 核心功能
|
||||
|
||||
✅ **Release 构建** - 生产环境优化版本
|
||||
✅ **可配置域名** - 支持自定义 API 和 CDN
|
||||
✅ **分架构打包** - arm64/armv7/x86_64
|
||||
✅ **自动 Release** - 推送标签自动发布
|
||||
|
||||
---
|
||||
|
||||
**需要帮助?** 查看 [BUILD_GUIDE.md](./BUILD_GUIDE.md)
|
||||
@@ -1,90 +0,0 @@
|
||||
# GitHub Actions 构建说明
|
||||
|
||||
## 📋 工作流概览
|
||||
|
||||
### `build-android-apk.yml` - Android APK 自动构建
|
||||
|
||||
**触发条件:**
|
||||
- ✅ 推送到 `main` 或 `develop` 分支
|
||||
- ✅ 打 `v*` 标签时自动创建 Release
|
||||
- ✅ 手动触发(Actions 页面点击 "Run workflow")
|
||||
|
||||
**构建产物:**
|
||||
- `libcore.aar` - sing-box 核心库
|
||||
- `BearVPN-arm64-v8a-*.apk` - 64位 ARM 版本(推荐)
|
||||
- `BearVPN-armeabi-v7a-*.apk` - 32位 ARM 版本
|
||||
- `BearVPN-x86_64-*.apk` - 模拟器版本
|
||||
|
||||
---
|
||||
|
||||
## 🚀 使用方法
|
||||
|
||||
### 方法一:推送代码自动构建
|
||||
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "feat: 新功能"
|
||||
git push origin main
|
||||
```
|
||||
|
||||
### 方法二:手动触发
|
||||
|
||||
1. 打开仓库 **Actions** 页面
|
||||
2. 点击 **Build Android APK**
|
||||
3. 点击 **Run workflow**
|
||||
|
||||
### 方法三:创建 Release
|
||||
|
||||
```bash
|
||||
git tag v1.0.0
|
||||
git push origin v1.0.0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📦 下载构建产物
|
||||
|
||||
- **Actions 页面:** 点击运行记录 → Artifacts 区域下载
|
||||
- **Releases 页面:** 选择版本 → Assets 下载
|
||||
|
||||
---
|
||||
|
||||
## 🔧 配置 Release 签名(可选)
|
||||
|
||||
1. 生成密钥:
|
||||
```bash
|
||||
keytool -genkey -v -keystore release.keystore -alias bearvpn -keyalg RSA -keysize 2048 -validity 10000
|
||||
```
|
||||
|
||||
2. 转换为 Base64:
|
||||
```bash
|
||||
base64 release.keystore > keystore.base64.txt
|
||||
```
|
||||
|
||||
3. 在 GitHub 仓库 **Settings → Secrets** 添加:
|
||||
- `KEYSTORE_BASE64`
|
||||
- `KEYSTORE_PASSWORD`
|
||||
- `KEY_ALIAS`
|
||||
- `KEY_PASSWORD`
|
||||
|
||||
---
|
||||
|
||||
## ⏱️ 构建时间
|
||||
|
||||
- libcore.aar: 10-15 分钟
|
||||
- Flutter APK: 15-20 分钟
|
||||
- **总计: 约 30 分钟**
|
||||
|
||||
---
|
||||
|
||||
## 🐛 常见问题
|
||||
|
||||
**Q: 构建失败 "libcore.aar not found"**
|
||||
A: 检查 libcore 子模块是否正确初始化
|
||||
|
||||
**Q: Release 未创建**
|
||||
A: 标签必须以 `v` 开头,如 `v1.0.0`
|
||||
|
||||
---
|
||||
|
||||
生成时间: 2025-10-27
|
||||
@@ -1,319 +0,0 @@
|
||||
name: Build Android APK
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- develop
|
||||
tags:
|
||||
- 'v*'
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch: # 允许手动触发
|
||||
inputs:
|
||||
build_type:
|
||||
description: '构建类型'
|
||||
required: true
|
||||
default: 'release'
|
||||
type: choice
|
||||
options:
|
||||
- debug
|
||||
- release
|
||||
api_domain:
|
||||
description: 'API 域名'
|
||||
required: true
|
||||
default: 'api.maodag.top'
|
||||
type: string
|
||||
oss_url_1:
|
||||
description: 'OSS 配置地址 1'
|
||||
required: true
|
||||
default: 'https://ppp2.oss-cn-hongkong.aliyuncs.com/bear1.txt'
|
||||
type: string
|
||||
oss_url_2:
|
||||
description: 'OSS 配置地址 2'
|
||||
required: true
|
||||
default: 'https://xgp3.oss-ap-northeast-1.aliyuncs.com/bear1.txt'
|
||||
type: string
|
||||
oss_url_3:
|
||||
description: 'OSS 配置地址 3'
|
||||
required: true
|
||||
default: 'https://xpp4.oss-ap-northeast-2.aliyuncs.com/bear1.txt'
|
||||
type: string
|
||||
oss_url_4:
|
||||
description: 'OSS 配置地址 4'
|
||||
required: true
|
||||
default: 'https://xpp5.oss-ap-southeast-1.aliyuncs.com/bear1.txt'
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
build-libcore:
|
||||
name: 编译 libcore.aar
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
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: 🔧 设置 Java 环境
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '17'
|
||||
|
||||
- name: 🔧 安装 gomobile
|
||||
run: |
|
||||
go install golang.org/x/mobile/cmd/gomobile@latest
|
||||
gomobile init
|
||||
|
||||
- name: 📦 编译 libcore.aar
|
||||
working-directory: libcore
|
||||
run: |
|
||||
echo "🚀 开始编译 libcore..."
|
||||
make android
|
||||
|
||||
echo "✅ 编译完成,检查产物..."
|
||||
ls -lh bin/
|
||||
|
||||
if [ -f "bin/libcore.aar" ]; then
|
||||
echo "✅ libcore.aar 生成成功"
|
||||
cp bin/libcore.aar ../android/app/libs/
|
||||
else
|
||||
echo "❌ libcore.aar 未找到"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: 📤 上传 libcore.aar
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: libcore-aar
|
||||
path: android/app/libs/libcore.aar
|
||||
retention-days: 7
|
||||
|
||||
build-apk:
|
||||
name: 编译 Android APK
|
||||
needs: build-libcore
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
build_type: [release] # 默认 release 构建
|
||||
|
||||
steps:
|
||||
- name: 📥 Checkout 代码
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 🔧 设置 Java 环境
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '17'
|
||||
|
||||
- name: 🔧 设置 Flutter 环境
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '3.24.5'
|
||||
channel: 'stable'
|
||||
cache: true
|
||||
|
||||
- name: 📥 下载 libcore.aar
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: libcore-aar
|
||||
path: android/app/libs/
|
||||
|
||||
- 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' }}"
|
||||
|
||||
echo "🔧 配置构建参数:"
|
||||
echo " API 域名: $API_DOMAIN"
|
||||
echo " OSS 地址 1: $OSS_URL_1"
|
||||
echo " OSS 地址 2: $OSS_URL_2"
|
||||
echo " OSS 地址 3: $OSS_URL_3"
|
||||
echo " OSS 地址 4: $OSS_URL_4"
|
||||
|
||||
# 替换 API 域名
|
||||
sed -i "s|api\.maodag\.top|$API_DOMAIN|g" "$CONFIG_FILE"
|
||||
|
||||
# 替换 OSS 地址
|
||||
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"
|
||||
|
||||
echo "✅ 配置替换完成"
|
||||
echo ""
|
||||
echo "📄 查看修改后的配置:"
|
||||
grep -A 15 "kr_baseDomains" "$CONFIG_FILE" || true
|
||||
|
||||
- name: 📦 安装 Flutter 依赖
|
||||
run: |
|
||||
flutter pub get
|
||||
flutter pub run build_runner build --delete-conflicting-outputs
|
||||
|
||||
- name: 🔨 构建 APK (Release)
|
||||
run: |
|
||||
flutter build apk --release --split-per-abi
|
||||
|
||||
- name: 📋 生成构建信息
|
||||
id: build_info
|
||||
run: |
|
||||
BUILD_DATE=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
COMMIT_SHA=${GITHUB_SHA::7}
|
||||
|
||||
echo "build_date=$BUILD_DATE" >> $GITHUB_OUTPUT
|
||||
echo "commit_sha=$COMMIT_SHA" >> $GITHUB_OUTPUT
|
||||
|
||||
echo "📊 构建信息:"
|
||||
echo " - 日期: $BUILD_DATE"
|
||||
echo " - 提交: $COMMIT_SHA"
|
||||
echo " - 分支: ${GITHUB_REF#refs/heads/}"
|
||||
|
||||
- name: 📦 重命名 APK 文件
|
||||
run: |
|
||||
COMMIT_SHA=${{ steps.build_info.outputs.commit_sha }}
|
||||
DATE=$(date '+%Y%m%d')
|
||||
|
||||
cd build/app/outputs/flutter-apk/
|
||||
|
||||
for file in app-*-release.apk; do
|
||||
if [ -f "$file" ]; then
|
||||
# 提取架构名称 (arm64-v8a, armeabi-v7a, x86_64)
|
||||
ARCH=$(echo "$file" | sed "s/app-\(.*\)-release.apk/\1/")
|
||||
NEW_NAME="BearVPN-${ARCH}-release-${DATE}-${COMMIT_SHA}.apk"
|
||||
|
||||
mv "$file" "$NEW_NAME"
|
||||
|
||||
# 计算文件大小和 MD5
|
||||
SIZE=$(ls -lh "$NEW_NAME" | awk '{print $5}')
|
||||
MD5=$(md5sum "$NEW_NAME" | awk '{print $1}')
|
||||
|
||||
echo "✅ $NEW_NAME"
|
||||
echo " 大小: $SIZE"
|
||||
echo " MD5: $MD5"
|
||||
fi
|
||||
done
|
||||
|
||||
ls -lh
|
||||
|
||||
- name: 📤 上传 APK (arm64-v8a)
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: apk-arm64-v8a-release
|
||||
path: build/app/outputs/flutter-apk/*arm64-v8a*.apk
|
||||
retention-days: 30
|
||||
|
||||
- name: 📤 上传 APK (armeabi-v7a)
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: apk-armeabi-v7a-release
|
||||
path: build/app/outputs/flutter-apk/*armeabi-v7a*.apk
|
||||
retention-days: 30
|
||||
|
||||
- name: 📤 上传 APK (x86_64)
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: apk-x86_64-release
|
||||
path: build/app/outputs/flutter-apk/*x86_64*.apk
|
||||
retention-days: 30
|
||||
|
||||
create-release:
|
||||
name: 创建 GitHub Release
|
||||
needs: build-apk
|
||||
runs-on: ubuntu-latest
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
|
||||
steps:
|
||||
- name: 📥 下载所有 APK
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: artifacts
|
||||
pattern: apk-*
|
||||
|
||||
- name: 📋 生成 Release 说明
|
||||
id: release_notes
|
||||
run: |
|
||||
cat > release_notes.md << 'EOF'
|
||||
## 🎉 BearVPN Android 版本发布
|
||||
|
||||
### 📦 安装包说明
|
||||
|
||||
| 架构 | 适用设备 | 文件大小 |
|
||||
|------|----------|----------|
|
||||
| arm64-v8a | 64位 ARM 设备(推荐,现代手机) | ~40MB |
|
||||
| armeabi-v7a | 32位 ARM 设备(老旧手机) | ~35MB |
|
||||
| x86_64 | x86_64 模拟器 | ~45MB |
|
||||
|
||||
### ✨ 主要特性
|
||||
|
||||
- ✅ 支持 Shadowsocks/VLESS/Trojan/Hysteria2 等协议
|
||||
- ✅ 内置 sing-box 核心
|
||||
- ✅ 支持自定义路由规则
|
||||
- ✅ 支持 URL Test 节点延迟测试
|
||||
|
||||
### 📥 下载建议
|
||||
|
||||
**不确定选哪个?** 下载 `arm64-v8a` 版本即可,适用于绝大多数现代 Android 手机。
|
||||
|
||||
### 🔒 文件校验
|
||||
|
||||
下载后请验证 MD5 以确保文件完整性(见下方 Assets 描述)。
|
||||
|
||||
---
|
||||
|
||||
**构建信息:**
|
||||
- 提交: ${GITHUB_SHA::7}
|
||||
- 构建时间: $(date '+%Y-%m-%d %H:%M:%S UTC')
|
||||
- Flutter: 3.24.5
|
||||
- sing-box: latest
|
||||
EOF
|
||||
|
||||
echo "release_notes<<EOF" >> $GITHUB_OUTPUT
|
||||
cat release_notes.md >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: 🚀 创建 GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: |
|
||||
artifacts/apk-arm64-v8a-*/*.apk
|
||||
artifacts/apk-armeabi-v7a-*/*.apk
|
||||
artifacts/apk-x86_64-*/*.apk
|
||||
body: ${{ steps.release_notes.outputs.release_notes }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
generate_release_notes: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: 📢 构建成功通知
|
||||
run: |
|
||||
echo "✅ 构建完成!"
|
||||
echo "📦 Release 已创建: ${{ github.ref }}"
|
||||
echo "🔗 查看: https://github.com/${{ github.repository }}/releases"
|
||||
@@ -1,142 +0,0 @@
|
||||
name: Build Clash Core
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'core/**'
|
||||
- '.github/workflows/build-clash-core.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'core/**'
|
||||
workflow_dispatch: # 允许手动触发
|
||||
|
||||
jobs:
|
||||
build-android:
|
||||
name: Build Android (${{ matrix.arch }})
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- arch: arm64
|
||||
abi: arm64-v8a
|
||||
cc: aarch64-linux-android29-clang
|
||||
- arch: armv7
|
||||
abi: armeabi-v7a
|
||||
cc: armv7a-linux-androideabi29-clang
|
||||
- arch: x86_64
|
||||
abi: x86_64
|
||||
cc: x86_64-linux-android29-clang
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.20'
|
||||
cache: true
|
||||
cache-dependency-path: core/go.sum
|
||||
|
||||
- name: Setup Android NDK
|
||||
uses: nttld/setup-ndk@v1
|
||||
with:
|
||||
ndk-version: r25c
|
||||
add-to-path: true
|
||||
|
||||
- name: Build Core
|
||||
working-directory: core
|
||||
run: make android-${{ matrix.arch }}
|
||||
env:
|
||||
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
|
||||
|
||||
- name: Verify Build
|
||||
run: |
|
||||
echo "📦 检查编译产物..."
|
||||
ls -lh android/app/src/main/jniLibs/${{ matrix.abi }}/libclash.so
|
||||
file android/app/src/main/jniLibs/${{ matrix.abi }}/libclash.so
|
||||
|
||||
echo "🔍 验证导出函数..."
|
||||
nm -D android/app/src/main/jniLibs/${{ matrix.abi }}/libclash.so | grep -E "(quickStart|getAndroidVpnOptions|startTUN)"
|
||||
|
||||
- name: Calculate MD5
|
||||
id: md5
|
||||
run: |
|
||||
MD5=$(md5sum android/app/src/main/jniLibs/${{ matrix.abi }}/libclash.so | awk '{print $1}')
|
||||
echo "md5=$MD5" >> $GITHUB_OUTPUT
|
||||
echo "📋 MD5: $MD5"
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: libclash-${{ matrix.abi }}
|
||||
path: android/app/src/main/jniLibs/${{ matrix.abi }}/libclash.so
|
||||
retention-days: 30
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Comment Build Info
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const stats = fs.statSync('android/app/src/main/jniLibs/${{ matrix.abi }}/libclash.so');
|
||||
const sizeMB = (stats.size / 1024 / 1024).toFixed(2);
|
||||
|
||||
github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: `### 🔨 Clash Core 构建完成 (${{ matrix.abi }})
|
||||
|
||||
- **架构:** ${{ matrix.abi }}
|
||||
- **大小:** ${sizeMB} MB
|
||||
- **MD5:** ${{ steps.md5.outputs.md5 }}
|
||||
- **提交:** ${context.sha.substring(0, 7)}
|
||||
|
||||
[下载构建产物](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})`
|
||||
});
|
||||
|
||||
create-release:
|
||||
name: Create Release
|
||||
needs: build-android
|
||||
runs-on: ubuntu-latest
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
|
||||
steps:
|
||||
- name: Download All Artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: artifacts
|
||||
|
||||
- name: Create Release Archive
|
||||
run: |
|
||||
cd artifacts
|
||||
tar -czf clash-core-android-all.tar.gz libclash-*/*.so
|
||||
ls -lh clash-core-android-all.tar.gz
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: artifacts/clash-core-android-all.tar.gz
|
||||
body: |
|
||||
## Clash Meta 核心 Android 构建
|
||||
|
||||
此版本包含所有 Android 架构的预编译二进制文件:
|
||||
- arm64-v8a (ARM64)
|
||||
- armeabi-v7a (ARMv7)
|
||||
- x86_64 (模拟器)
|
||||
|
||||
**使用方法:**
|
||||
```bash
|
||||
tar -xzf clash-core-android-all.tar.gz
|
||||
cp libclash-*/libclash.so android/app/src/main/jniLibs/
|
||||
```
|
||||
draft: false
|
||||
prerelease: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -1,950 +0,0 @@
|
||||
name: Build Multi-Platform
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- develop
|
||||
tags:
|
||||
- 'v*'
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
build_type:
|
||||
description: '构建类型'
|
||||
required: true
|
||||
default: 'release'
|
||||
type: choice
|
||||
options:
|
||||
- debug
|
||||
- release
|
||||
api_domain:
|
||||
description: 'API 域名'
|
||||
required: true
|
||||
default: 'api.maodag.top'
|
||||
type: string
|
||||
oss_url_1:
|
||||
description: 'OSS 配置地址 1'
|
||||
required: true
|
||||
default: 'https://ppp2.oss-cn-hongkong.aliyuncs.com/bear1.txt'
|
||||
type: string
|
||||
oss_url_2:
|
||||
description: 'OSS 配置地址 2'
|
||||
required: true
|
||||
default: 'https://xgp3.oss-ap-northeast-1.aliyuncs.com/bear1.txt'
|
||||
type: string
|
||||
oss_url_3:
|
||||
description: 'OSS 配置地址 3'
|
||||
required: true
|
||||
default: 'https://xpp4.oss-ap-northeast-2.aliyuncs.com/bear1.txt'
|
||||
type: string
|
||||
oss_url_4:
|
||||
description: 'OSS 配置地址 4'
|
||||
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,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)
|
||||
runs-on: ubuntu-latest
|
||||
if: contains(inputs.platforms || 'android', 'android')
|
||||
|
||||
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: 🔧 设置 Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '17'
|
||||
|
||||
- name: 🔧 安装 gomobile
|
||||
run: |
|
||||
go install golang.org/x/mobile/cmd/gomobile@latest
|
||||
gomobile init
|
||||
|
||||
- name: 📦 编译 libcore.aar
|
||||
working-directory: libcore
|
||||
run: |
|
||||
echo "🚀 开始编译 Android libcore..."
|
||||
make android
|
||||
|
||||
if [ -f "bin/libcore.aar" ]; then
|
||||
echo "✅ libcore.aar 生成成功"
|
||||
ls -lh bin/libcore.aar
|
||||
else
|
||||
echo "❌ libcore.aar 未找到"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: 📤 上传 libcore.aar
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: libcore-android
|
||||
path: libcore/bin/libcore.aar
|
||||
retention-days: 7
|
||||
|
||||
# ==================== 编译 libcore (Windows) ====================
|
||||
build-libcore-windows:
|
||||
name: 编译 libcore (Windows)
|
||||
runs-on: ubuntu-latest
|
||||
if: contains(inputs.platforms || 'windows', 'windows')
|
||||
|
||||
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: 🔧 安装 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/libcore.dll bin/HiddifyCli.exe
|
||||
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
|
||||
|
||||
# ==================== 编译 libcore (macOS) ====================
|
||||
build-libcore-macos:
|
||||
name: 编译 libcore (macOS)
|
||||
runs-on: macos-latest
|
||||
if: contains(inputs.platforms || 'macos', 'macos')
|
||||
|
||||
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.dylib
|
||||
working-directory: libcore
|
||||
run: |
|
||||
echo "🚀 开始编译 macOS libcore..."
|
||||
make macos-universal
|
||||
|
||||
if [ -f "bin/libcore.dylib" ] && [ -f "bin/HiddifyCli" ]; then
|
||||
echo "✅ macOS libcore 编译成功"
|
||||
ls -lh bin/libcore.dylib bin/HiddifyCli
|
||||
else
|
||||
echo "❌ macOS libcore 编译失败"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: 📤 上传 macOS libcore
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: libcore-macos
|
||||
path: |
|
||||
libcore/bin/libcore.dylib
|
||||
libcore/bin/HiddifyCli
|
||||
retention-days: 7
|
||||
|
||||
# ==================== 编译 libcore (Linux) ====================
|
||||
build-libcore-linux:
|
||||
name: 编译 libcore (Linux)
|
||||
runs-on: ubuntu-latest
|
||||
if: contains(inputs.platforms || 'linux', 'linux')
|
||||
|
||||
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.so
|
||||
working-directory: libcore
|
||||
run: |
|
||||
echo "🚀 开始编译 Linux libcore..."
|
||||
make linux-amd64
|
||||
|
||||
if [ -f "bin/lib/libcore.so" ] && [ -f "bin/HiddifyCli" ]; then
|
||||
echo "✅ Linux libcore 编译成功"
|
||||
ls -lh bin/lib/libcore.so bin/HiddifyCli
|
||||
else
|
||||
echo "❌ Linux libcore 编译失败"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: 📤 上传 Linux libcore
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: libcore-linux
|
||||
path: |
|
||||
libcore/bin/lib/libcore.so
|
||||
libcore/bin/HiddifyCli
|
||||
libcore/bin/webui/**
|
||||
retention-days: 7
|
||||
|
||||
# ==================== Android 构建 ====================
|
||||
build-android:
|
||||
name: 构建 Android APK
|
||||
needs: build-libcore-android
|
||||
runs-on: ubuntu-latest
|
||||
if: contains(inputs.platforms || 'android', 'android')
|
||||
|
||||
steps:
|
||||
- name: 📥 Checkout 代码
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 🔧 设置 Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '17'
|
||||
|
||||
- name: 🔧 设置 Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '3.24.5'
|
||||
channel: 'stable'
|
||||
cache: true
|
||||
|
||||
- name: 📥 下载 libcore.aar
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: libcore-android
|
||||
path: android/app/libs/
|
||||
|
||||
- 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' }}"
|
||||
|
||||
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 "✅ 配置完成"
|
||||
|
||||
- name: 📦 安装 Flutter 依赖
|
||||
run: |
|
||||
flutter pub get
|
||||
flutter pub run build_runner build --delete-conflicting-outputs
|
||||
|
||||
- name: 🔨 构建 APK (Release)
|
||||
run: |
|
||||
flutter build apk --release --split-per-abi
|
||||
|
||||
- name: 📦 重命名 APK
|
||||
run: |
|
||||
COMMIT_SHA=${GITHUB_SHA::7}
|
||||
DATE=$(date '+%Y%m%d')
|
||||
cd build/app/outputs/flutter-apk/
|
||||
|
||||
for file in app-*-release.apk; do
|
||||
if [ -f "$file" ]; then
|
||||
ARCH=$(echo "$file" | sed "s/app-\(.*\)-release.apk/\1/")
|
||||
NEW_NAME="BearVPN-android-${ARCH}-release-${DATE}-${COMMIT_SHA}.apk"
|
||||
mv "$file" "$NEW_NAME"
|
||||
echo "✅ $NEW_NAME"
|
||||
fi
|
||||
done
|
||||
|
||||
- name: 📤 上传 APK
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: android-apk
|
||||
path: build/app/outputs/flutter-apk/*.apk
|
||||
retention-days: 30
|
||||
|
||||
# ==================== Windows 构建 ====================
|
||||
build-windows:
|
||||
name: 构建 Windows
|
||||
needs: build-libcore-windows
|
||||
runs-on: windows-latest
|
||||
if: contains(inputs.platforms || 'windows', 'windows')
|
||||
|
||||
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: 📥 下载 Windows libcore
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: libcore-windows
|
||||
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 和加密密钥
|
||||
shell: bash
|
||||
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' }}"
|
||||
|
||||
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
|
||||
|
||||
- name: 🔧 生成代码文件 (build_runner)
|
||||
run: |
|
||||
echo "🔧 开始运行 build_runner..."
|
||||
flutter pub run build_runner build --delete-conflicting-outputs
|
||||
|
||||
echo ""
|
||||
echo "✅ build_runner 完成,检查生成的文件..."
|
||||
|
||||
if (Test-Path "lib\singbox\model\singbox_status.freezed.dart") {
|
||||
echo "✅ singbox_status.freezed.dart 已生成"
|
||||
} else {
|
||||
echo "❌ singbox_status.freezed.dart 未生成"
|
||||
exit 1
|
||||
}
|
||||
|
||||
if (Test-Path "lib\singbox\service\singbox_service_provider.g.dart") {
|
||||
echo "✅ singbox_service_provider.g.dart 已生成"
|
||||
} else {
|
||||
echo "❌ singbox_service_provider.g.dart 未生成"
|
||||
exit 1
|
||||
}
|
||||
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: |
|
||||
COMMIT_SHA=${GITHUB_SHA::7}
|
||||
DATE=$(date '+%Y%m%d')
|
||||
|
||||
cd build/windows/x64/runner/Release
|
||||
7z a -tzip "../../../../../BearVPN-windows-x64-release-${DATE}-${COMMIT_SHA}.zip" ./*
|
||||
|
||||
- name: 📤 上传 Windows
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: windows-x64
|
||||
path: BearVPN-windows-*.zip
|
||||
retention-days: 30
|
||||
|
||||
# ==================== macOS 构建 ====================
|
||||
build-macos:
|
||||
name: 构建 macOS
|
||||
needs: build-libcore-macos
|
||||
runs-on: macos-latest
|
||||
if: contains(inputs.platforms || 'macos', 'macos')
|
||||
|
||||
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: 📥 下载 macOS libcore
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: libcore-macos
|
||||
path: libcore/bin/
|
||||
|
||||
- name: 🔧 设置 libcore 执行权限
|
||||
run: |
|
||||
chmod +x libcore/bin/HiddifyCli
|
||||
|
||||
- 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' }}"
|
||||
|
||||
# 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: |
|
||||
flutter pub get
|
||||
flutter pub run build_runner build --delete-conflicting-outputs
|
||||
|
||||
- name: 🔨 构建 macOS (Release)
|
||||
run: |
|
||||
flutter build macos --release
|
||||
|
||||
- name: 📦 打包 macOS
|
||||
run: |
|
||||
COMMIT_SHA=${GITHUB_SHA::7}
|
||||
DATE=$(date '+%Y%m%d')
|
||||
|
||||
cd build/macos/Build/Products/Release
|
||||
zip -r -y "../../../../../BearVPN-macos-release-${DATE}-${COMMIT_SHA}.zip" BearVPN.app
|
||||
|
||||
- name: 📤 上传 macOS
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: macos-app
|
||||
path: BearVPN-macos-*.zip
|
||||
retention-days: 30
|
||||
|
||||
# ==================== Linux 构建 ====================
|
||||
build-linux:
|
||||
name: 构建 Linux
|
||||
needs: build-libcore-linux
|
||||
runs-on: ubuntu-latest
|
||||
if: contains(inputs.platforms || 'linux', 'linux')
|
||||
|
||||
steps:
|
||||
- name: 📥 Checkout 代码
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 🔧 安装 Linux 依赖
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
clang \
|
||||
cmake \
|
||||
ninja-build \
|
||||
pkg-config \
|
||||
libgtk-3-dev \
|
||||
liblzma-dev \
|
||||
libstdc++-12-dev \
|
||||
libayatana-appindicator3-dev
|
||||
|
||||
- name: 🔧 设置 Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '3.24.5'
|
||||
channel: 'stable'
|
||||
cache: true
|
||||
|
||||
- name: 📥 下载 Linux libcore
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: libcore-linux
|
||||
path: libcore/bin/
|
||||
|
||||
- name: 🔧 设置 libcore 执行权限
|
||||
run: |
|
||||
chmod +x libcore/bin/HiddifyCli
|
||||
|
||||
- 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' }}"
|
||||
|
||||
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: 🔨 构建 Linux (Release)
|
||||
run: |
|
||||
flutter build linux --release
|
||||
|
||||
- name: 📦 打包 Linux
|
||||
run: |
|
||||
COMMIT_SHA=${GITHUB_SHA::7}
|
||||
DATE=$(date '+%Y%m%d')
|
||||
|
||||
cd build/linux/x64/release/bundle
|
||||
tar -czf "../../../../../BearVPN-linux-x64-release-${DATE}-${COMMIT_SHA}.tar.gz" ./*
|
||||
|
||||
- name: 📤 上传 Linux
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: linux-x64
|
||||
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, build-ios]
|
||||
runs-on: ubuntu-latest
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
|
||||
steps:
|
||||
- name: 📥 下载所有构建产物
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: artifacts
|
||||
|
||||
- name: 📋 生成 Release 说明
|
||||
id: release_notes
|
||||
run: |
|
||||
cat > release_notes.md << 'EOF'
|
||||
## 🎉 BearVPN 多平台版本发布
|
||||
|
||||
### 📦 平台支持
|
||||
|
||||
| 平台 | 架构 | 文件 |
|
||||
|-----|------|-----|
|
||||
| **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 |
|
||||
|
||||
### ✨ 主要特性
|
||||
|
||||
- ✅ 支持 Shadowsocks/VLESS/Trojan/Hysteria2
|
||||
- ✅ 内置 sing-box 核心 v3.1.7
|
||||
- ✅ 跨平台支持
|
||||
- ✅ 自定义路由规则
|
||||
|
||||
### 📥 安装指南
|
||||
|
||||
**Android:** 下载 APK 直接安装
|
||||
**iOS:** 下载 IPA 使用 AltStore/Sideloadly 安装
|
||||
**Windows:** 解压 ZIP 运行 BearVPN.exe
|
||||
**macOS:** 解压 ZIP 拖拽到应用程序
|
||||
**Linux:** 解压 tar.gz 运行可执行文件
|
||||
|
||||
---
|
||||
|
||||
**构建信息:**
|
||||
- 提交: ${GITHUB_SHA::7}
|
||||
- 时间: $(date '+%Y-%m-%d %H:%M:%S UTC')
|
||||
- Flutter: 3.24.5
|
||||
- sing-box: v3.1.7
|
||||
EOF
|
||||
|
||||
echo "release_notes<<EOF" >> $GITHUB_OUTPUT
|
||||
cat release_notes.md >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: 🚀 创建 Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: |
|
||||
artifacts/android-apk/*.apk
|
||||
artifacts/ios-app/*.ipa
|
||||
artifacts/windows-x64/*.zip
|
||||
artifacts/macos-app/*.zip
|
||||
artifacts/linux-x64/*.tar.gz
|
||||
body: ${{ steps.release_notes.outputs.release_notes }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: 📢 构建完成
|
||||
run: |
|
||||
echo "✅ 所有平台构建完成!"
|
||||
echo "📦 Release: ${{ github.ref }}"
|
||||
echo "🔗 https://github.com/${{ github.repository }}/releases"
|
||||
@@ -1,145 +0,0 @@
|
||||
name: Build Windows
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, master ]
|
||||
pull_request:
|
||||
branches: [ main, master ]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
# 先编译 libcore
|
||||
build-libcore:
|
||||
name: 编译 libcore (Windows)
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
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: 🔧 安装 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
|
||||
with:
|
||||
flutter-version: '3.24.5'
|
||||
channel: 'stable'
|
||||
|
||||
- name: Enable Windows desktop
|
||||
run: flutter config --enable-windows-desktop
|
||||
|
||||
- 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
|
||||
|
||||
- name: Build Windows Release
|
||||
run: flutter build windows --release
|
||||
|
||||
- name: Upload Debug build artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: windows-debug-build
|
||||
path: build/windows/runner/Debug/
|
||||
|
||||
- name: Upload Release build artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: windows-release-build
|
||||
path: build/windows/runner/Release/
|
||||
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 24 KiB |
@@ -14,7 +14,7 @@ class ActiveGroupsChannel(private val scope: CoroutineScope) : FlutterPlugin,
|
||||
CommandClient.Handler {
|
||||
companion object {
|
||||
const val TAG = "A/ActiveGroupsChannel"
|
||||
const val CHANNEL = "com.baer.app/active-groups"
|
||||
const val CHANNEL = "com.hi.app/active-groups"
|
||||
val gson = Gson()
|
||||
}
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@ class EventHandler : FlutterPlugin {
|
||||
|
||||
companion object {
|
||||
const val TAG = "A/EventHandler"
|
||||
const val SERVICE_STATUS = "com.baer.app/service.status"
|
||||
const val SERVICE_ALERTS = "com.baer.app/service.alerts"
|
||||
const val SERVICE_STATUS = "com.hi.app/service.status"
|
||||
const val SERVICE_ALERTS = "com.hi.app/service.alerts"
|
||||
}
|
||||
|
||||
private var statusChannel: EventChannel? = null
|
||||
|
||||
@@ -12,7 +12,7 @@ import kotlinx.coroutines.CoroutineScope
|
||||
class GroupsChannel(private val scope: CoroutineScope) : FlutterPlugin, CommandClient.Handler {
|
||||
companion object {
|
||||
const val TAG = "A/GroupsChannel"
|
||||
const val CHANNEL = "com.baer.app/groups"
|
||||
const val CHANNEL = "com.hi.app/groups"
|
||||
val gson = Gson()
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ class LogHandler : FlutterPlugin {
|
||||
|
||||
companion object {
|
||||
const val TAG = "A/LogHandler"
|
||||
const val SERVICE_LOGS = "com.baer.app/service.logs"
|
||||
const val SERVICE_LOGS = "com.hi.app/service.logs"
|
||||
}
|
||||
|
||||
private lateinit var logsChannel: EventChannel
|
||||
|
||||
@@ -21,7 +21,7 @@ class MethodHandler(private val scope: CoroutineScope) : FlutterPlugin,
|
||||
|
||||
companion object {
|
||||
const val TAG = "A/MethodHandler"
|
||||
const val channelName = "com.baer.app/method"
|
||||
const val channelName = "com.hi.app/method"
|
||||
|
||||
enum class Trigger(val method: String) {
|
||||
Setup("setup"),
|
||||
|
||||
@@ -33,7 +33,7 @@ class PlatformSettingsHandler : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
private lateinit var ignoreRequestResult: MethodChannel.Result
|
||||
|
||||
companion object {
|
||||
const val channelName = "com.baer.app/platform"
|
||||
const val channelName = "com.hi.app/platform"
|
||||
|
||||
const val REQUEST_IGNORE_BATTERY_OPTIMIZATIONS = 44
|
||||
val gson = Gson()
|
||||
|
||||
@@ -11,7 +11,7 @@ import kotlinx.coroutines.CoroutineScope
|
||||
class StatsChannel(private val scope: CoroutineScope) : FlutterPlugin, CommandClient.Handler{
|
||||
companion object {
|
||||
const val TAG = "A/StatsChannel"
|
||||
const val STATS_CHANNEL = "com.baer.app/stats"
|
||||
const val STATS_CHANNEL = "com.hi.app/stats"
|
||||
}
|
||||
|
||||
private val commandClient =
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.hiddify.hiddify.constant
|
||||
|
||||
object Action {
|
||||
const val SERVICE = "com.baer.app.SERVICE"
|
||||
const val SERVICE_CLOSE = "com.baer.app.SERVICE_CLOSE"
|
||||
const val SERVICE_RELOAD = "com.baer.app.sfa.SERVICE_RELOAD"
|
||||
const val SERVICE = "com.hi.app.SERVICE"
|
||||
const val SERVICE_CLOSE = "com.hi.app.SERVICE_CLOSE"
|
||||
const val SERVICE_RELOAD = "com.hi.app.sfa.SERVICE_RELOAD"
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
@echo off
|
||||
echo 📋 复制 libcore 文件...
|
||||
|
||||
:: 创建目标目录
|
||||
mkdir libcore\bin >nul 2>&1
|
||||
|
||||
:: 查找并复制 HiddifyCli.exe,重命名为 HiFastVPNCli.exe
|
||||
for /r %%f in (HiddifyCli.exe) do (
|
||||
if exist "%%f" (
|
||||
echo ✅ 找到 HiddifyCli.exe: %%f
|
||||
echo 📝 复制并重命名为 HiFastVPNCli.exe
|
||||
copy "%%f" libcore\bin\HiFastVPNCli.exe
|
||||
echo ✅ 重命名完成
|
||||
goto :dll
|
||||
)
|
||||
)
|
||||
echo ⚠️ 未找到 HiddifyCli.exe
|
||||
:dll
|
||||
|
||||
:: 复制 libcore.dll
|
||||
for /r %%f in (libcore.dll) do (
|
||||
if exist "%%f" (
|
||||
echo ✅ 找到 libcore.dll: %%f
|
||||
copy "%%f" libcore\bin\libcore.dll
|
||||
goto :verify
|
||||
)
|
||||
)
|
||||
echo ⚠️ 未找到 libcore.dll
|
||||
:verify
|
||||
|
||||
echo.
|
||||
echo 📄 验证文件:
|
||||
if exist libcore\bin (
|
||||
dir libcore\bin
|
||||
if exist libcore\bin\HiFastVPNCli.exe (
|
||||
if exist libcore\bin\libcore.dll (
|
||||
echo ✅ 验证成功:所有文件已正确复制
|
||||
exit /b 0
|
||||
) else (
|
||||
echo ❌ 验证失败:libcore.dll 不存在
|
||||
exit /b 1
|
||||
)
|
||||
) else (
|
||||
echo ❌ 验证失败:HiFastVPNCli.exe 不存在
|
||||
exit /b 1
|
||||
)
|
||||
) else (
|
||||
echo ⚠️ libcore\bin 目录不存在
|
||||
exit /b 1
|
||||
)
|
||||
@@ -0,0 +1,35 @@
|
||||
@echo off
|
||||
REM Fix NuGet installation using Chocolatey
|
||||
|
||||
echo Installing NuGet via Chocolatey...
|
||||
|
||||
REM Check if Chocolatey is available
|
||||
where choco >nul 2>nul
|
||||
if %errorlevel% neq 0 (
|
||||
echo Chocolatey not found, installing Chocolatey first...
|
||||
powershell -Command "Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))"
|
||||
)
|
||||
|
||||
echo Installing NuGet via Chocolatey...
|
||||
choco install nuget.commandline -y
|
||||
|
||||
if %errorlevel% equ 0 (
|
||||
echo SUCCESS: NuGet installed via Chocolatey
|
||||
echo Updating PATH...
|
||||
set PATH=C:\ProgramData\chocolatey\bin;%PATH%
|
||||
setx PATH "C:\ProgramData\chocolatey\bin;%PATH%"
|
||||
|
||||
echo Verifying installation...
|
||||
nuget help | findstr "NuGet"
|
||||
|
||||
if %errorlevel% equ 0 (
|
||||
echo NuGet is working correctly!
|
||||
) else (
|
||||
echo WARNING: NuGet installed but not accessible
|
||||
)
|
||||
) else (
|
||||
echo ERROR: NuGet installation failed
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
pause
|
||||
@@ -0,0 +1,67 @@
|
||||
# NuGet 安装脚本 - 解决 SSL 问题
|
||||
Write-Host "=== 安装 NuGet ===" -ForegroundColor Green
|
||||
|
||||
# 设置 TLS 1.2
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
|
||||
# 检查是否已安装
|
||||
$nugetExists = Get-Command nuget -ErrorAction SilentlyContinue
|
||||
if ($nugetExists) {
|
||||
Write-Host "NuGet 已安装: $($nugetExists.Source)" -ForegroundColor Green
|
||||
exit 0
|
||||
}
|
||||
|
||||
# 下载 NuGet
|
||||
Write-Host "下载 NuGet CLI..." -ForegroundColor Yellow
|
||||
$downloadUrls = @(
|
||||
"https://dist.nuget.org/win-x86-commandline/latest/nuget.exe",
|
||||
"https://dist.nuget.org/win-x86-commandline/v6.7.0/nuget.exe",
|
||||
"http://dist.nuget.org/win-x86-commandline/v6.7.0/nuget.exe"
|
||||
)
|
||||
|
||||
$downloaded = $false
|
||||
foreach ($url in $downloadUrls) {
|
||||
try {
|
||||
Write-Host "尝试下载: $url"
|
||||
$webClient = New-Object System.Net.WebClient
|
||||
$webClient.DownloadFile($url, "C:\nuget.exe")
|
||||
$downloaded = $true
|
||||
Write-Host "下载成功!" -ForegroundColor Green
|
||||
break
|
||||
} catch {
|
||||
Write-Host "下载失败: $($_.Exception.Message)" -ForegroundColor Red
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $downloaded) {
|
||||
Write-Host "所有下载都失败了,尝试使用 Chocolatey..." -ForegroundColor Yellow
|
||||
try {
|
||||
choco install nuget.commandline -y
|
||||
if (Get-Command nuget -ErrorAction SilentlyContinue) {
|
||||
Write-Host "通过 Chocolatey 安装成功!" -ForegroundColor Green
|
||||
exit 0
|
||||
}
|
||||
} catch {
|
||||
Write-Host "Chocolatey 安装也失败了: $($_.Exception.Message)" -ForegroundColor Red
|
||||
}
|
||||
|
||||
Write-Host "NuGet 安装失败,但继续构建..." -ForegroundColor Yellow
|
||||
exit 0
|
||||
}
|
||||
|
||||
# 验证安装
|
||||
if (Test-Path "C:\nuget.exe") {
|
||||
$env:PATH = "C:\;$env:PATH"
|
||||
Write-Host "NuGet 安装完成" -ForegroundColor Green
|
||||
|
||||
# 测试
|
||||
try {
|
||||
$version = & "C:\nuget.exe" help | Select-String -Pattern "NuGet Version" | Select-Object -First 1
|
||||
Write-Host "版本信息: $version" -ForegroundColor Green
|
||||
} catch {
|
||||
Write-Host "NuGet 可用,但版本检查失败" -ForegroundColor Yellow
|
||||
}
|
||||
} else {
|
||||
Write-Host "NuGet 文件不存在" -ForegroundColor Red
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
# Windows 构建修复脚本
|
||||
# 以管理员身份运行
|
||||
|
||||
Write-Host "=== Windows Flutter 构建修复 ===" -ForegroundColor Green
|
||||
|
||||
# 1. 安装 NuGet
|
||||
Write-Host "`n1. 安装 NuGet..." -ForegroundColor Yellow
|
||||
$nugetPath = "C:\nuget.exe"
|
||||
if (-not (Test-Path $nugetPath)) {
|
||||
try {
|
||||
Write-Host "下载 NuGet..."
|
||||
Invoke-WebRequest -Uri "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe" -OutFile $nugetPath
|
||||
$env:PATH = "C:\;$env:PATH"
|
||||
Write-Host "NuGet 安装成功" -ForegroundColor Green
|
||||
} catch {
|
||||
Write-Host "NuGet 下载失败: $_" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
} else {
|
||||
Write-Host "NuGet 已存在" -ForegroundColor Green
|
||||
}
|
||||
|
||||
# 2. 启用长路径支持
|
||||
Write-Host "`n2. 启用长路径支持..." -ForegroundColor Yellow
|
||||
try {
|
||||
Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem" -Name "LongPathsEnabled" -Value 1 -Type DWORD -Force
|
||||
Write-Host "长路径支持已启用" -ForegroundColor Green
|
||||
} catch {
|
||||
Write-Host "长路径设置失败: $_" -ForegroundColor Red
|
||||
}
|
||||
|
||||
# 3. 清理构建缓存
|
||||
Write-Host "`n3. 清理构建缓存..." -ForegroundColor Yellow
|
||||
if (Test-Path "build") {
|
||||
Remove-Item -Path "build" -Recurse -Force
|
||||
Write-Host "Build 目录已清理" -ForegroundColor Green
|
||||
}
|
||||
|
||||
if (Test-Path "windows") {
|
||||
Remove-Item -Path "windows" -Recurse -Force
|
||||
Write-Host "Windows 目录已清理" -ForegroundColor Green
|
||||
}
|
||||
|
||||
# 4. 重新创建 Windows 项目
|
||||
Write-Host "`n4. 重新创建 Windows 项目..." -ForegroundColor Yellow
|
||||
flutter create --platforms=windows .
|
||||
|
||||
# 5. 获取依赖
|
||||
Write-Host "`n5. 获取依赖..." -ForegroundColor Yellow
|
||||
flutter pub get
|
||||
|
||||
# 6. 构建 Debug 版本
|
||||
Write-Host "`n6. 构建 Debug 版本..." -ForegroundColor Yellow
|
||||
flutter build windows
|
||||
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Host "`n=== 构建成功! ===" -ForegroundColor Green
|
||||
Write-Host "输出目录: build\windows\runner\Debug\" -ForegroundColor Cyan
|
||||
} else {
|
||||
Write-Host "`n=== 构建失败 ===" -ForegroundColor Red
|
||||
Write-Host "请检查错误信息并尝试手动修复" -ForegroundColor Red
|
||||
}
|
||||
|
||||
Write-Host "`n修复完成!" -ForegroundColor Green
|
||||
@@ -0,0 +1,64 @@
|
||||
@echo off
|
||||
:: This script checks for and installs all necessary tools for building and packaging the Flutter application on Windows.
|
||||
|
||||
:: 1. Check for Administrator Privileges
|
||||
net session >nul 2>&1
|
||||
if %errorLevel% == 0 (
|
||||
echo Administrator privileges detected. Continuing...
|
||||
) else (
|
||||
echo Requesting Administrator privileges to install tools...
|
||||
powershell -Command "Start-Process cmd.exe -ArgumentList '/c %~s0' -Verb RunAs" >nul 2>&1
|
||||
exit /b
|
||||
)
|
||||
|
||||
:: 2. Check for and Install Chocolatey
|
||||
echo.
|
||||
echo === Checking for Chocolatey ===
|
||||
where choco >nul 2>&1
|
||||
if %errorlevel% equ 0 (
|
||||
echo Chocolatey is already installed.
|
||||
) else (
|
||||
echo Chocolatey not found. Installing now...
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -Command "[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))"
|
||||
if %errorlevel% neq 0 (
|
||||
echo ERROR: Failed to install Chocolatey. Please install it manually from https://chocolatey.org
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
echo Chocolatey installed successfully.
|
||||
:: Add Chocolatey to the PATH for the current session
|
||||
set "PATH=%PATH%;%ALLUSERSPROFILE%\chocolatey\bin"
|
||||
)
|
||||
|
||||
:: 3. Install Required Tools via Chocolatey
|
||||
echo.
|
||||
echo === Installing Build Tools (7-Zip and Enigma Virtual Box) ===
|
||||
|
||||
:: Install 7-Zip
|
||||
echo Installing 7-Zip...
|
||||
choco install 7zip -y
|
||||
if %errorlevel% neq 0 (
|
||||
echo ERROR: Failed to install 7-Zip.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
echo 7-Zip installed successfully.
|
||||
|
||||
:: Install Enigma Virtual Box
|
||||
echo Installing Enigma Virtual Box...
|
||||
choco install enigma-virtual-box -y
|
||||
if %errorlevel% neq 0 (
|
||||
echo ERROR: Failed to install Enigma Virtual Box.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
echo Enigma Virtual Box installed successfully.
|
||||
|
||||
|
||||
echo.
|
||||
echo =======================================================
|
||||
echo All required build tools have been installed.
|
||||
echo You can now use 'package_windows_single_exe.ps1' to build your single-file executable.
|
||||
echo =======================================================
|
||||
echo.
|
||||
pause
|
||||
@@ -0,0 +1,44 @@
|
||||
@echo off
|
||||
REM Simple Flutter installer for Windows Gitea Runner
|
||||
|
||||
echo Installing Flutter for Windows...
|
||||
|
||||
REM Create flutter directory
|
||||
mkdir C:\flutter 2>nul
|
||||
|
||||
REM Download Flutter stable version
|
||||
echo Downloading Flutter...
|
||||
powershell -Command "(New-Object Net.WebClient).DownloadFile('https://storage.googleapis.com/flutter_infra_release/releases/stable/windows/flutter_windows_3.24.5-stable.zip', 'C:\flutter.zip')"
|
||||
|
||||
if %errorlevel% neq 0 (
|
||||
echo Download failed, trying alternative...
|
||||
powershell -Command "Invoke-WebRequest -Uri 'https://storage.googleapis.com/flutter_infra_release/releases/stable/windows/flutter_windows_3.24.5-stable.zip' -OutFile 'C:\flutter.zip'"
|
||||
)
|
||||
|
||||
REM Extract Flutter
|
||||
echo Extracting Flutter...
|
||||
powershell -Command "Expand-Archive -Path 'C:\flutter.zip' -DestinationPath 'C:\' -Force"
|
||||
|
||||
REM Set PATH
|
||||
echo Setting PATH...
|
||||
set PATH=C:\flutter\bin;%PATH%
|
||||
setx PATH "C:\flutter\bin;%PATH%"
|
||||
|
||||
REM Clean up
|
||||
del C:\flutter.zip
|
||||
|
||||
REM Verify installation
|
||||
echo Verifying Flutter installation...
|
||||
C:\flutter\bin\flutter --version
|
||||
|
||||
if %errorlevel% equ 0 (
|
||||
echo SUCCESS: Flutter installed successfully
|
||||
echo Running flutter doctor...
|
||||
C:\flutter\bin\flutter doctor
|
||||
) else (
|
||||
echo ERROR: Flutter installation failed
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo Flutter installation complete!
|
||||
pause
|
||||
@@ -0,0 +1,39 @@
|
||||
@echo off
|
||||
REM Simple NuGet installer for Windows
|
||||
|
||||
echo Installing NuGet...
|
||||
|
||||
REM Check if NuGet exists
|
||||
where nuget >nul 2>nul
|
||||
if %errorlevel% == 0 (
|
||||
echo NuGet already installed
|
||||
nuget help | findstr "NuGet"
|
||||
pause
|
||||
exit /b 0
|
||||
)
|
||||
|
||||
REM Download NuGet using PowerShell
|
||||
echo Downloading NuGet CLI...
|
||||
powershell -Command "(New-Object Net.WebClient).DownloadFile('http://dist.nuget.org/win-x86-commandline/v6.7.0/nuget.exe', 'C:\nuget.exe')"
|
||||
|
||||
if %errorlevel% neq 0 (
|
||||
echo Download failed, trying alternative...
|
||||
powershell -Command "(New-Object Net.WebClient).DownloadFile('https://dist.nuget.org/win-x86-commandline/v6.7.0/nuget.exe', 'C:\nuget.exe')"
|
||||
)
|
||||
|
||||
REM Add to PATH
|
||||
set PATH=C:\;%PATH%
|
||||
setx PATH "C:\;%PATH%"
|
||||
|
||||
REM Verify installation
|
||||
echo Verifying NuGet installation...
|
||||
C:\nuget.exe help | findstr "NuGet"
|
||||
|
||||
if %errorlevel% equ 0 (
|
||||
echo SUCCESS: NuGet installed successfully
|
||||
) else (
|
||||
echo ERROR: NuGet installation failed
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
pause
|
||||
@@ -5,6 +5,6 @@
|
||||
// Created by GFWFighter on 7/24/1402 AP.
|
||||
//
|
||||
|
||||
BASE_BUNDLE_IDENTIFIER=app.baer.com
|
||||
SERVICE_IDENTIFIER=com.baer.app
|
||||
BASE_BUNDLE_IDENTIFIER=app.hi.com
|
||||
SERVICE_IDENTIFIER=com.hi.app
|
||||
DEVELOPMENT_TEAM=3UR892FAP3
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<true/>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.app.baer.com</string>
|
||||
<string>group.app.hi.com</string>
|
||||
</array>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
|
||||
@@ -75,21 +75,21 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/webview_flutter_wkwebview/darwin"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
connectivity_plus: 481668c94744c30c53b8895afb39159d1e619bdf
|
||||
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
|
||||
connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d
|
||||
device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342
|
||||
EasyPermissionX: ff4c438f6ee80488f873b4cb921e32d982523067
|
||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||
flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
|
||||
flutter_udid: f7c3884e6ec2951efe4f9de082257fc77c4d15e9
|
||||
flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4
|
||||
flutter_udid: b2417673f287ee62817a1de3d1643f47b9f508ab
|
||||
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
|
||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
||||
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
|
||||
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
|
||||
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
||||
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
|
||||
ReachabilitySwift: 32793e867593cfc1177f5d16491e3a197d2fccda
|
||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
||||
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
|
||||
webview_flutter_wkwebview: 1821ceac936eba6f7984d89a9f3bcb4dea99ebb2
|
||||
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
|
||||
webview_flutter_wkwebview: a4af96a051138e28e29f60101d094683b9f82188
|
||||
|
||||
PODFILE CHECKSUM: 579a354deb8d6fdc55c12799569018594328642e
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
COCOAPODS: 1.15.2
|
||||
|
||||
@@ -814,11 +814,9 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_ENTITLEMENTS = PacketTunnel/PacketTunnelRelease.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 3UR892FAP3;
|
||||
DEVELOPMENT_TEAM = Q5PC7SNX27;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
EXCLUDED_ARCHS = armv7;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
@@ -844,7 +842,6 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BASE_BUNDLE_IDENTIFIER).PacketTunnel";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = hitoPacketTunnel;
|
||||
SKIP_INSTALL = YES;
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SUPPORTS_MACCATALYST = NO;
|
||||
@@ -868,11 +865,9 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_ENTITLEMENTS = PacketTunnel/HiddifyPacketTunnel.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 3UR892FAP3;
|
||||
DEVELOPMENT_TEAM = Q5PC7SNX27;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
EXCLUDED_ARCHS = armv7;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
@@ -898,7 +893,6 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BASE_BUNDLE_IDENTIFIER).PacketTunnel";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = hitoPacketTunnel;
|
||||
SKIP_INSTALL = YES;
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SUPPORTS_MACCATALYST = NO;
|
||||
@@ -986,6 +980,7 @@
|
||||
"EXCLUDED_ARCHS[sdk=iphoneos*]" = armv7;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(PROJECT_DIR)/build/ios/framework/$(CONFIGURATION)",
|
||||
"$(PROJECT_DIR)/../build/ios/framework/$(CONFIGURATION)",
|
||||
@@ -1216,6 +1211,7 @@
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "i386 arm64";
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(PROJECT_DIR)/build/ios/framework/$(CONFIGURATION)",
|
||||
"$(PROJECT_DIR)/../build/ios/framework/$(CONFIGURATION)",
|
||||
@@ -1261,15 +1257,14 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/RunnerRelease.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 3UR892FAP3;
|
||||
DEVELOPMENT_TEAM = Q5PC7SNX27;
|
||||
ENABLE_BITCODE = NO;
|
||||
"EXCLUDED_ARCHS[sdk=iphoneos*]" = armv7;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(PROJECT_DIR)/build/ios/framework/$(CONFIGURATION)",
|
||||
"$(PROJECT_DIR)/../build/ios/framework/$(CONFIGURATION)",
|
||||
@@ -1295,7 +1290,6 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BASE_BUNDLE_IDENTIFIER)";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = rls;
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SUPPORTS_MACCATALYST = NO;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
|
||||
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 69 B After Width: | Height: | Size: 844 KiB |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 3.2 KiB |
@@ -9,7 +9,7 @@
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>BearVPN</string>
|
||||
<string>Hi快VPN</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
@@ -30,10 +30,10 @@
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>com.BearVPN.ios</string>
|
||||
<string>com.hi.ios</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>hiddify</string>
|
||||
<string>HiFastVPN</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<true/>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.$(BASE_BUNDLE_IDENTIFIER)</string>
|
||||
<string>group.app.hi.com</string>
|
||||
</array>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<true/>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.app.baer.com</string>
|
||||
<string>group.app.hi.com</string>
|
||||
</array>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
|
||||
@@ -102,7 +102,7 @@ class VPNManager: ObservableObject {
|
||||
`protocol`.providerBundleIdentifier = Bundle.main.baseBundleIdentifier + ".PacketTunnel"
|
||||
`protocol`.serverAddress = "localhost"
|
||||
newManager.protocolConfiguration = `protocol`
|
||||
newManager.localizedDescription = "BearVPN"
|
||||
newManager.localizedDescription = "HiFastVPN"
|
||||
try await newManager.saveToPreferences()
|
||||
try await newManager.loadFromPreferences()
|
||||
self.manager = newManager
|
||||
|
||||
@@ -29,14 +29,15 @@ class HINodeListView extends GetView<HINodeListController> {
|
||||
|
||||
/// 获取用于显示的延迟值
|
||||
int _getDisplayDelay(HINodeListController controller, KROutboundItem item) {
|
||||
if (controller.homeController.kr_isConnected.value) {
|
||||
return item.urlTestDelay.value;
|
||||
}
|
||||
if (!_fakeDelays.containsKey(item.tag)) {
|
||||
final random = Random();
|
||||
_fakeDelays[item.tag] = 30 + random.nextInt(71); // 30-100ms
|
||||
}
|
||||
return _fakeDelays[item.tag] ?? 0;
|
||||
// if (controller.homeController.kr_isConnected.value) {
|
||||
//
|
||||
// }
|
||||
// if (!_fakeDelays.containsKey(item.tag)) {
|
||||
// final random = Random();
|
||||
// _fakeDelays[item.tag] = 30 + random.nextInt(71); // 30-100ms
|
||||
// }
|
||||
// return _fakeDelays[item.tag] ?? 0;
|
||||
}
|
||||
|
||||
/// 获取分组内最快节点的延迟值(单位:ms)
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'dart:convert';
|
||||
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
@@ -32,6 +35,7 @@ import 'package:kaer_with_panels/app/utils/kr_secure_storage.dart';
|
||||
import 'package:kaer_with_panels/app/services/singbox_imp/kr_sing_box_imp.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:kaer_with_panels/app/utils/kr_init_log_collector.dart'; // 🔧 新增:导入日志收集器
|
||||
import 'package:kaer_with_panels/app/utils/kr_latency_tester.dart'; // 🔧 新增:导入真实延迟测试工具
|
||||
|
||||
class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
// 🔧 新增:日志收集器实例
|
||||
@@ -52,7 +56,7 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
/// 底部面板高度常量
|
||||
static const double kr_baseHeight = 120.0; // 基础高度(连接选项)
|
||||
static const double kr_subscriptionCardHeight = 200.0; // 订阅卡片高度
|
||||
static const double kr_connectionInfoHeight = 126.0; // 连接信息卡片高度
|
||||
static const double kr_connectionInfoHeight = 126.0; // 连接信息卡片高度(修复后)
|
||||
static const double kr_trialCardHeight = 120.0; // 试用卡片高度
|
||||
static const double kr_lastDayCardHeight = 120.0; // 最后一天卡片高度
|
||||
static const double kr_nodeListHeight = 400.0; // 节点列表高度
|
||||
@@ -129,21 +133,11 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
if (value != null) {
|
||||
isQuickConnectEnabled.value = value;
|
||||
// 保存闪连状态到本地存储
|
||||
await _saveQuickConnectStatus(value);
|
||||
await _storage.kr_saveBool(key: _quickConnectKey, value: value);
|
||||
KRLogUtil.kr_i('闪连状态已更新: $value', tag: 'QuickConnect');
|
||||
}
|
||||
}
|
||||
|
||||
// 保存闪连状态到本地存储
|
||||
Future<void> _saveQuickConnectStatus(bool enabled) async {
|
||||
try {
|
||||
await _storage.kr_saveBool(key: _quickConnectKey, value: enabled);
|
||||
KRLogUtil.kr_i('闪连状态已保存到本地存储: $enabled', tag: 'QuickConnect');
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_e('保存闪连状态失败: $e', tag: 'QuickConnect');
|
||||
}
|
||||
}
|
||||
|
||||
// 从本地存储加载闪连状态
|
||||
Future<void> _loadQuickConnectStatus() async {
|
||||
try {
|
||||
@@ -164,6 +158,7 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 获取当前闪连状态
|
||||
bool get isQuickConnectActive => isQuickConnectEnabled.value;
|
||||
|
||||
@@ -247,9 +242,37 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
!kr_isConnected.value;
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
void onInit() async {
|
||||
super.onInit();
|
||||
|
||||
// 🔧 紧急诊断:直接写文件验证 onInit 是否被调用
|
||||
try {
|
||||
final dir = await getApplicationDocumentsDirectory();
|
||||
final debugFile = File('${dir.path}/HOME_CONTROLLER_DEBUG.txt');
|
||||
await debugFile.writeAsString(
|
||||
'=' * 60 + '\n'
|
||||
'HomeController.onInit 被调用!\n'
|
||||
'时间: ${DateTime.now()}\n'
|
||||
'实例 HashCode: ${hashCode}\n'
|
||||
'线程: ${Platform.isAndroid ? "Android" : "Unknown"}\n'
|
||||
'=' * 60 + '\n',
|
||||
mode: FileMode.append,
|
||||
);
|
||||
} catch (e) {
|
||||
// 忽略错误,确保不影响主流程
|
||||
}
|
||||
|
||||
// 🔧 新增:记录 HomeController 初始化开始
|
||||
_initLog.logPhaseStart('HomeController 初始化');
|
||||
_initLog.log('KRHomeController.onInit 被调用', tag: 'Home');
|
||||
|
||||
// 🔧 Android 15 紧急修复:立即设置默认高度,确保底部面板可见
|
||||
// kr_updateBottomPanelHeight();
|
||||
|
||||
/// 底部面板高度处理
|
||||
// _kr_initBottomPanelHeight();
|
||||
// 加载闪连状态
|
||||
_loadQuickConnectStatus();
|
||||
// 绑定订阅状态
|
||||
@@ -264,6 +287,9 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
// 注册应用生命周期监听
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
|
||||
// 🔧 新增:恢复上次选择的节点显示
|
||||
_restoreSelectedNode();
|
||||
|
||||
// 延迟同步连接状态,确保状态正确
|
||||
Future.delayed(const Duration(milliseconds: 500), () {
|
||||
kr_forceSyncConnectionStatus();
|
||||
@@ -279,7 +305,42 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
});
|
||||
}
|
||||
|
||||
/// 🔧 新增:恢复上次选择的节点显示
|
||||
Future<void> _restoreSelectedNode() async {
|
||||
try {
|
||||
// 从 Hive 读取保存的节点
|
||||
final savedNode = await KRSecureStorage().kr_readData(key: 'SELECTED_NODE_TAG');
|
||||
|
||||
if (savedNode != null && savedNode.isNotEmpty) {
|
||||
KRLogUtil.kr_i('📖 恢复上次选择的节点显示: $savedNode', tag: 'HomeController');
|
||||
|
||||
// 更新 UI 显示
|
||||
kr_currentNodeName.value = savedNode;
|
||||
kr_cutTag.value = savedNode;
|
||||
kr_cutSeletedTag.value = savedNode;
|
||||
|
||||
// 如果是国家节点,更新国家文本
|
||||
if (savedNode != 'auto') {
|
||||
kr_coutryText.value = savedNode;
|
||||
}
|
||||
|
||||
KRLogUtil.kr_i('✅ 节点显示已恢复: $savedNode', tag: 'HomeController');
|
||||
} else {
|
||||
KRLogUtil.kr_i('ℹ️ 没有保存的节点,使用默认 auto', tag: 'HomeController');
|
||||
}
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_e('❌ 恢复节点显示失败: $e', tag: 'HomeController');
|
||||
// 失败时保持默认值 'auto'
|
||||
}
|
||||
}
|
||||
|
||||
/// 底部面板高度处理
|
||||
void _kr_initBottomPanelHeight() {
|
||||
ever(kr_currentListStatus, (status) {
|
||||
kr_updateBottomPanelHeight();
|
||||
KRLogUtil.kr_i(status.toString(), tag: "_kr_initBottomPanelHeight");
|
||||
});
|
||||
}
|
||||
|
||||
void _kr_initLoginStatus() {
|
||||
_initLog.log('开始初始化登录状态处理', tag: 'Home');
|
||||
@@ -337,36 +398,138 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
/// 验证并设置登录状态
|
||||
void _kr_validateAndSetLoginStatus() {
|
||||
try {
|
||||
_initLog.log('🔍 开始验证登录状态', tag: 'Home');
|
||||
|
||||
// 多重验证登录状态
|
||||
final hasToken = KRAppRunData().kr_token != null &&
|
||||
KRAppRunData().kr_token!.isNotEmpty;
|
||||
final hasToken = KRAppRunData().kr_token != null && KRAppRunData().kr_token!.isNotEmpty;
|
||||
final isLoginFlag = KRAppRunData().kr_isLogin.value;
|
||||
final isValidLogin = hasToken && isLoginFlag;
|
||||
|
||||
KRLogUtil.kr_i(
|
||||
'登录状态验证: hasToken=$hasToken, isLogin=$isLoginFlag, isValid=$isValidLogin',
|
||||
tag: 'HomeController');
|
||||
KRLogUtil.kr_i(
|
||||
'Token内容: ${KRAppRunData().kr_token?.substring(0, 10)}...',
|
||||
tag: 'HomeController');
|
||||
_initLog.log('登录验证结果: hasToken=$hasToken, isLogin=$isLoginFlag, isValid=$isValidLogin', tag: 'Home');
|
||||
KRLogUtil.kr_i('登录状态验证: hasToken=$hasToken, isLogin=$isLoginFlag, isValid=$isValidLogin', tag: 'HomeController');
|
||||
KRLogUtil.kr_i('Token内容: ${KRAppRunData().kr_token?.substring(0, 10)}...', tag: 'HomeController');
|
||||
|
||||
if (isValidLogin) {
|
||||
kr_currentViewStatus.value = KRHomeViewsStatus.kr_loggedIn;
|
||||
_initLog.logSuccess('用户已登录,准备加载订阅数据', tag: 'Home');
|
||||
KRLogUtil.kr_i('设置为已登录状态', tag: 'HomeController');
|
||||
|
||||
// 订阅服务已在 splash 页面初始化,此处无需重复初始化
|
||||
KRLogUtil.kr_i('订阅服务已在启动页初始化,跳过重复初始化', tag: 'HomeController');
|
||||
} else {
|
||||
kr_currentViewStatus.value = KRHomeViewsStatus.kr_notLoggedIn;
|
||||
_initLog.logWarning('用户未登录,跳过订阅加载', tag: 'Home');
|
||||
KRLogUtil.kr_i('设置为未登录状态', tag: 'HomeController');
|
||||
}
|
||||
} catch (e) {
|
||||
_initLog.logError('登录状态验证失败', tag: 'Home', error: e);
|
||||
KRLogUtil.kr_e('登录状态验证失败: $e', tag: 'HomeController');
|
||||
kr_currentViewStatus.value = KRHomeViewsStatus.kr_notLoggedIn;
|
||||
}
|
||||
}
|
||||
|
||||
/// 确保订阅服务初始化
|
||||
void _kr_ensureSubscribeServiceInitialized() {
|
||||
try {
|
||||
_initLog.logSeparator();
|
||||
_initLog.log('📦 开始订阅服务初始化', tag: 'Subscribe');
|
||||
|
||||
// 🔧 修复1: 添加登录状态检查 - 只有已登录用户才能初始化订阅
|
||||
final isLoggedIn = KRAppRunData().kr_isLogin.value;
|
||||
if (!isLoggedIn) {
|
||||
_initLog.logWarning('未登录用户,跳过订阅服务初始化', tag: 'Subscribe');
|
||||
KRLogUtil.kr_w('⚠️ 未登录用户,不初始化订阅服务', tag: 'HomeController');
|
||||
kr_currentListStatus.value = KRHomeViewsListStatus.kr_none;
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查订阅服务状态
|
||||
final currentStatus = kr_subscribeService.kr_currentStatus.value;
|
||||
_initLog.log('当前订阅服务状态: $currentStatus', tag: 'Subscribe');
|
||||
KRLogUtil.kr_i('订阅服务当前状态: $currentStatus', tag: 'HomeController');
|
||||
|
||||
if (currentStatus == KRSubscribeServiceStatus.kr_none ||
|
||||
currentStatus == KRSubscribeServiceStatus.kr_error) {
|
||||
_initLog.log('订阅服务需要初始化,开始加载...', tag: 'Subscribe');
|
||||
KRLogUtil.kr_i('订阅服务未初始化或错误,开始初始化', tag: 'HomeController');
|
||||
|
||||
// 设置加载状态
|
||||
kr_currentListStatus.value = KRHomeViewsListStatus.kr_loading;
|
||||
_initLog.log('设置列表状态为: loading', tag: 'Subscribe');
|
||||
|
||||
final startTime = DateTime.now();
|
||||
_initLog.log('开始调用 kr_refreshAll() 加载订阅数据', tag: 'Subscribe');
|
||||
|
||||
// 🔧 Android 15 增强:添加 10 秒超时保护
|
||||
kr_subscribeService.kr_refreshAll().timeout(
|
||||
const Duration(seconds: 10),
|
||||
onTimeout: () {
|
||||
final elapsed = DateTime.now().difference(startTime).inMilliseconds;
|
||||
_initLog.logWarning('订阅服务初始化超时(10秒), 实际耗时: ${elapsed}ms', tag: 'Subscribe');
|
||||
KRLogUtil.kr_w('⏱️ 订阅服务初始化超时(10秒)', tag: 'HomeController');
|
||||
kr_currentListStatus.value = KRHomeViewsListStatus.kr_none;
|
||||
throw TimeoutException('订阅服务初始化超时');
|
||||
},
|
||||
).then((_) async {
|
||||
final elapsed = DateTime.now().difference(startTime).inMilliseconds;
|
||||
_initLog.logSuccess('订阅服务初始化完成, 耗时: ${elapsed}ms', tag: 'Subscribe');
|
||||
_initLog.log('最终列表状态: ${kr_currentListStatus.value}', tag: 'Subscribe');
|
||||
|
||||
// 🔧 新增:记录订阅数据详情
|
||||
final subscribeList = kr_subscribeService.kr_availableSubscribes;
|
||||
_initLog.log('订阅列表数量: ${subscribeList.length}', tag: 'Subscribe');
|
||||
if (subscribeList.isEmpty) {
|
||||
_initLog.logWarning('⚠️ 订阅列表为空!用户可能没有有效订阅', tag: 'Subscribe');
|
||||
} else {
|
||||
for (int i = 0; i < subscribeList.length; i++) {
|
||||
final sub = subscribeList[i];
|
||||
_initLog.log('订阅[$i]: ID=${sub.id}, 名称=${sub.name}, 试用=${sub.isTryOut}', tag: 'Subscribe');
|
||||
_initLog.log(' 过期时间: ${sub.expireTime}, 流量: ${sub.traffic}', tag: 'Subscribe');
|
||||
}
|
||||
}
|
||||
|
||||
// 记录当前选中的订阅
|
||||
final currentSub = kr_subscribeService.kr_currentSubscribe.value;
|
||||
if (currentSub != null) {
|
||||
_initLog.log('当前选中订阅: ${currentSub.name} (ID: ${currentSub.id})', tag: 'Subscribe');
|
||||
} else {
|
||||
_initLog.logWarning('⚠️ 没有选中任何订阅', tag: 'Subscribe');
|
||||
}
|
||||
|
||||
// 记录订阅服务的最终状态
|
||||
final finalServiceStatus = kr_subscribeService.kr_currentStatus.value;
|
||||
_initLog.log('订阅服务最终状态: $finalServiceStatus', tag: 'Subscribe');
|
||||
|
||||
KRLogUtil.kr_i('✅ 订阅服务初始化完成', tag: 'HomeController');
|
||||
|
||||
// 🔧 关键修复:订阅服务初始化完成后,关闭日志文件
|
||||
await _initLog.finalize();
|
||||
|
||||
// 成功后状态由服务自己控制,不在这里设置
|
||||
}).catchError((error) async {
|
||||
final elapsed = DateTime.now().difference(startTime).inMilliseconds;
|
||||
_initLog.logError('订阅服务初始化失败, 耗时: ${elapsed}ms', tag: 'Subscribe', error: error);
|
||||
_initLog.log('设置列表状态为: none (失败)', tag: 'Subscribe');
|
||||
KRLogUtil.kr_e('❌ 订阅服务初始化失败: $error', tag: 'HomeController');
|
||||
|
||||
// 🔧 关键修复:即使失败也要关闭日志文件
|
||||
await _initLog.finalize();
|
||||
// 🔧 Android 15 优化:失败时设置为 none 而非 error,允许用户手动重试
|
||||
kr_currentListStatus.value = KRHomeViewsListStatus.kr_none;
|
||||
});
|
||||
} else if (currentStatus == KRSubscribeServiceStatus.kr_loading) {
|
||||
KRLogUtil.kr_i('订阅服务正在初始化中', tag: 'HomeController');
|
||||
kr_currentListStatus.value = KRHomeViewsListStatus.kr_loading;
|
||||
} else if (currentStatus == KRSubscribeServiceStatus.kr_success) {
|
||||
KRLogUtil.kr_i('订阅服务已成功初始化', tag: 'HomeController');
|
||||
kr_currentListStatus.value = KRHomeViewsListStatus.kr_none;
|
||||
}
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_e('❌ 确保订阅服务初始化异常: $e', tag: 'HomeController');
|
||||
// 🔧 Android 15 优化:异常时设置为 none 而非 error
|
||||
kr_currentListStatus.value = KRHomeViewsListStatus.kr_none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// 处理登录状态变化
|
||||
@@ -374,8 +537,7 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
try {
|
||||
if (isLoggedIn) {
|
||||
// 再次验证登录状态的有效性
|
||||
final isValidLogin = KRAppRunData().kr_token != null &&
|
||||
KRAppRunData().kr_token!.isNotEmpty;
|
||||
final isValidLogin = KRAppRunData().kr_token != null && KRAppRunData().kr_token!.isNotEmpty;
|
||||
if (isValidLogin) {
|
||||
kr_currentViewStatus.value = KRHomeViewsStatus.kr_loggedIn;
|
||||
KRLogUtil.kr_i('登录状态变化:设置为已登录', tag: 'HomeController');
|
||||
@@ -385,8 +547,7 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
// 订阅服务已在 splash 页面初始化,此处无需重复初始化
|
||||
KRLogUtil.kr_i('订阅服务已在启动页初始化,跳过重复初始化', tag: 'HomeController');
|
||||
} else {
|
||||
KRLogUtil.kr_w(
|
||||
'登录状态为true但token为空,重置为未登录', tag: 'HomeController');
|
||||
KRLogUtil.kr_w('登录状态为true但token为空,重置为未登录', tag: 'HomeController');
|
||||
kr_currentViewStatus.value = KRHomeViewsStatus.kr_notLoggedIn;
|
||||
}
|
||||
} else {
|
||||
@@ -419,20 +580,14 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
final currentLoginStatus = KRAppRunData().kr_isLogin.value;
|
||||
final currentViewStatus = kr_currentViewStatus.value;
|
||||
|
||||
KRLogUtil.kr_i(
|
||||
'状态同步检查: login=$currentLoginStatus, view=$currentViewStatus',
|
||||
tag: 'HomeController');
|
||||
KRLogUtil.kr_i('状态同步检查: login=$currentLoginStatus, view=$currentViewStatus', tag: 'HomeController');
|
||||
|
||||
// 检查状态是否一致
|
||||
if (currentViewStatus == KRHomeViewsStatus.kr_loggedIn &&
|
||||
!currentLoginStatus) {
|
||||
KRLogUtil.kr_w('状态不一致:视图显示已登录但实际未登录,修正状态',
|
||||
tag: 'HomeController');
|
||||
if (currentViewStatus == KRHomeViewsStatus.kr_loggedIn && !currentLoginStatus) {
|
||||
KRLogUtil.kr_w('状态不一致:视图显示已登录但实际未登录,修正状态', tag: 'HomeController');
|
||||
kr_currentViewStatus.value = KRHomeViewsStatus.kr_notLoggedIn;
|
||||
} else if (currentViewStatus == KRHomeViewsStatus.kr_notLoggedIn &&
|
||||
currentLoginStatus) {
|
||||
KRLogUtil.kr_w('状态不一致:视图显示未登录但实际已登录,修正状态',
|
||||
tag: 'HomeController');
|
||||
} else if (currentViewStatus == KRHomeViewsStatus.kr_notLoggedIn && currentLoginStatus) {
|
||||
KRLogUtil.kr_w('状态不一致:视图显示未登录但实际已登录,修正状态', tag: 'HomeController');
|
||||
_kr_validateAndSetLoginStatus();
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -450,10 +605,7 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
ever(kr_subscribeService.kr_currentStatus, (data) {
|
||||
KRLogUtil.kr_i('订阅服务状态变化: $data', tag: 'HomeController');
|
||||
|
||||
if (KRAppRunData
|
||||
.getInstance()
|
||||
.kr_isLogin
|
||||
.value) {
|
||||
if (KRAppRunData.getInstance().kr_isLogin.value) {
|
||||
switch (data) {
|
||||
case KRSubscribeServiceStatus.kr_loading:
|
||||
KRLogUtil.kr_i('订阅服务加载中', tag: 'HomeController');
|
||||
@@ -527,8 +679,6 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
void _bindConnectionStatus() {
|
||||
// 添加更详细的状态监听
|
||||
ever(KRSingBoxImp.instance.kr_status, (status) {
|
||||
if (kDebugMode) {
|
||||
}
|
||||
KRLogUtil.kr_i('🔄 连接状态变化: $status', tag: 'HomeController');
|
||||
KRLogUtil.kr_i('📊 当前状态类型: ${status.runtimeType}', tag: 'HomeController');
|
||||
|
||||
@@ -757,28 +907,14 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
void _kr_handleSelectorProxy(dynamic element, List<dynamic> allGroups) {
|
||||
try {
|
||||
KRLogUtil.kr_d(
|
||||
'处理选择器代理 - 当前选择: ${element.selected}, 用户选择: ${kr_cutTag
|
||||
.value}',
|
||||
'处理选择器代理 - 当前选择: ${element.selected}, 用户选择: ${kr_cutTag.value}',
|
||||
tag: 'HomeController');
|
||||
|
||||
// 如果用户选择了auto但实际select类型不是auto
|
||||
if (kr_cutTag.value == "auto" && element.selected != "auto") {
|
||||
KRLogUtil.kr_d(
|
||||
'用户选择了auto但实际不是auto,重新选择auto', tag: 'HomeController');
|
||||
KRSingBoxImp.instance.kr_selectOutbound("auto");
|
||||
_kr_handleAutoMode(element, allGroups);
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果用户选择了具体节点但实际select类型不是该节点
|
||||
if (kr_cutTag.value != "auto" && element.selected != kr_cutTag.value) {
|
||||
KRLogUtil.kr_d(
|
||||
'用户选择了${kr_cutTag.value}但实际是${element.selected},更新选择',
|
||||
tag: 'HomeController');
|
||||
|
||||
kr_selectNode(kr_cutTag.value);
|
||||
|
||||
return;
|
||||
// 🔧 关键修复:仅更新 UI 状态,不要触发重新选择,避免死循环
|
||||
// 更新 kr_cutSeletedTag 以反映实际选中的节点
|
||||
if (element.selected.isNotEmpty) {
|
||||
kr_cutSeletedTag.value = element.selected;
|
||||
}
|
||||
|
||||
// 如果用户手动选择了节点(不是auto)
|
||||
@@ -797,28 +933,15 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
/// 处理手动模式
|
||||
void _kr_handleManualMode(dynamic element) {
|
||||
try {
|
||||
KRLogUtil.kr_d(
|
||||
'处理手动模式 - 选择: ${element.selected}', tag: 'HomeController');
|
||||
KRLogUtil.kr_d('处理手动模式 - 选择: ${element.selected}', tag: 'HomeController');
|
||||
|
||||
// 如果当前选择与用户选择不同,更新选择
|
||||
if (kr_cutTag.value != element.selected) {
|
||||
// 检查选择的节点是否有效
|
||||
if (_kr_isValidLatency(kr_cutTag.value)) {
|
||||
kr_selectNode(kr_cutTag.value);
|
||||
// 更新延迟值
|
||||
_kr_updateNodeLatency(element);
|
||||
} else {
|
||||
// 如果选择的节点无效,尝试选择延迟最小的节点
|
||||
_kr_selectBestLatencyNode(element.items);
|
||||
}
|
||||
} else {
|
||||
// 🔧 关键修复:仅更新 UI 状态,不要重新选择节点,避免死循环
|
||||
kr_cutSeletedTag.value = element.selected;
|
||||
// 更新延迟值
|
||||
_kr_updateNodeLatency(element);
|
||||
kr_currentNodeName.value =
|
||||
kr_truncateText(element.selected, maxLength: 25);
|
||||
// kr_moveToSelectedNode();
|
||||
}
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_e('处理手动模式出错: $e', tag: 'HomeController');
|
||||
}
|
||||
@@ -924,8 +1047,7 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
final selectedNode = kr_subscribeService.keyList[kr_cutSeletedTag.value];
|
||||
if (selectedNode != null) {
|
||||
KRLogUtil.kr_d(
|
||||
'更新节点信息 - 协议: ${selectedNode.protocol}, IP: ${selectedNode
|
||||
.serverAddr}',
|
||||
'更新节点信息 - 协议: ${selectedNode.protocol}, IP: ${selectedNode.serverAddr}',
|
||||
tag: 'HomeController');
|
||||
kr_currentProtocol.value =
|
||||
kr_truncateText(selectedNode.protocol, maxLength: 15);
|
||||
@@ -1075,7 +1197,7 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
// 后台切换成功,更新UI
|
||||
kr_cutSeletedTag.value = tag;
|
||||
kr_updateConnectionInfo();
|
||||
kr_moveToSelectedNode();
|
||||
// kr_moveToSelectedNode();
|
||||
|
||||
// 🚀 方案A增强:增加验证前等待时间,确保活动组完全更新
|
||||
KRLogUtil.kr_i('⏳ [增强] 等待活动组更新(500ms)...', tag: 'HomeController');
|
||||
@@ -1156,48 +1278,108 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
}
|
||||
|
||||
/// 获取当前节点国家
|
||||
/// 🔧 修复:使用 kr_cutTag 而不是 kr_cutSeletedTag,确保UI立即响应
|
||||
/// 🔧 修复:处理 "auto" 等特殊标签,从活动组中获取实际选中的节点
|
||||
/// 🔧 修复:优先使用 kr_cutSeletedTag,避免依赖可能为空的 kr_activeGroups
|
||||
/// 策略:
|
||||
/// 1. 如果 kr_cutTag 不是 auto,直接使用(用户手动选择的节点)
|
||||
/// 2. 如果是 auto,优先使用 kr_cutSeletedTag(保存了实际选中的节点)
|
||||
/// 3. 如果 kr_cutSeletedTag 也是 auto 或空,再尝试从 kr_activeGroups 获取
|
||||
String kr_getCurrentNodeCountry() {
|
||||
KRLogUtil.kr_i('========== 开始获取国家代码 ==========', tag: 'getCurrentNodeCountry');
|
||||
KRLogUtil.kr_i('kr_cutTag: ${kr_cutTag.value}', tag: 'getCurrentNodeCountry');
|
||||
KRLogUtil.kr_i('kr_cutSeletedTag: ${kr_cutSeletedTag.value}', tag: 'getCurrentNodeCountry');
|
||||
KRLogUtil.kr_i('keyList 节点总数: ${kr_subscribeService.keyList.length}', tag: 'getCurrentNodeCountry');
|
||||
|
||||
if (kr_cutTag.isEmpty) {
|
||||
KRLogUtil.kr_w('kr_cutTag 为空,返回空字符串', tag: 'getCurrentNodeCountry');
|
||||
String actualTag;
|
||||
|
||||
// 🔧 优先策略:
|
||||
// 1. 如果 kr_cutTag 不是 auto,直接使用(用户手动选择的节点)
|
||||
if (kr_cutTag.value != 'auto' && kr_cutTag.value != 'select' && kr_cutTag.value.isNotEmpty) {
|
||||
// 用户手动选择了具体节点
|
||||
actualTag = kr_cutTag.value;
|
||||
KRLogUtil.kr_i('✅ 使用手动选择的节点: $actualTag', tag: 'getCurrentNodeCountry');
|
||||
}
|
||||
// 2. 如果是 auto,优先使用 kr_cutSeletedTag(保存了实际选中的节点)
|
||||
else if (kr_cutSeletedTag.value.isNotEmpty &&
|
||||
kr_cutSeletedTag.value != 'auto' &&
|
||||
kr_cutSeletedTag.value != 'select') {
|
||||
// auto 模式下,使用保存的实际节点
|
||||
actualTag = kr_cutSeletedTag.value;
|
||||
KRLogUtil.kr_i('✅ 使用 auto 模式下的实际节点 (kr_cutSeletedTag): $actualTag', tag: 'getCurrentNodeCountry');
|
||||
}
|
||||
// 3. 降级:尝试从活动组获取
|
||||
else {
|
||||
try {
|
||||
KRLogUtil.kr_i('⚠️ 尝试从活动组获取实际节点', tag: 'getCurrentNodeCountry');
|
||||
KRLogUtil.kr_i('活动组数量: ${KRSingBoxImp.instance.kr_activeGroups.length}', tag: 'getCurrentNodeCountry');
|
||||
|
||||
// 🔧 修复:活动组为空时,尝试使用 allGroups
|
||||
if (KRSingBoxImp.instance.kr_activeGroups.isEmpty) {
|
||||
print('[getCurrentNodeCountry] ⚠️ 活动组为空,尝试使用 allGroups');
|
||||
KRLogUtil.kr_w('⚠️ 活动组为空,尝试使用 allGroups', tag: 'getCurrentNodeCountry');
|
||||
|
||||
final allGroups = KRSingBoxImp.instance.kr_allGroups;
|
||||
print('[getCurrentNodeCountry] allGroups 数量: ${allGroups.length}');
|
||||
if (allGroups.isEmpty) {
|
||||
print('[getCurrentNodeCountry] ❌ allGroups 也为空,返回空字符串');
|
||||
KRLogUtil.kr_w('❌ allGroups 也为空,返回空字符串', tag: 'getCurrentNodeCountry');
|
||||
return '';
|
||||
}
|
||||
|
||||
String actualTag = kr_cutTag.value;
|
||||
// 从 allGroups 中查找 select 组
|
||||
final selectGroup = allGroups.firstWhere(
|
||||
(group) => group.tag == 'select',
|
||||
orElse: () => throw Exception('未找到 select 组'),
|
||||
);
|
||||
print('[getCurrentNodeCountry] selectGroup.selected: ${selectGroup.selected}');
|
||||
|
||||
// 🔧 修复:如果是 "auto" 或其他选择器组,从活动组中获取实际选中的节点
|
||||
if (actualTag == 'auto' || actualTag == 'select') {
|
||||
try {
|
||||
KRLogUtil.kr_i('检测到特殊标签: $actualTag,尝试从活动组获取实际节点', tag: 'getCurrentNodeCountry');
|
||||
KRLogUtil.kr_i('活动组数量: ${KRSingBoxImp.instance.kr_activeGroups.length}', tag: 'getCurrentNodeCountry');
|
||||
if (selectGroup.selected.isEmpty || selectGroup.selected == 'auto' || selectGroup.selected == 'select') {
|
||||
print('[getCurrentNodeCountry] select 组选中的是 auto,查找 urltest 组');
|
||||
// 如果 select 组选中的是 auto,从 urltest 组获取
|
||||
final urlTestGroup = allGroups.firstWhere(
|
||||
(group) => group.type == ProxyType.urltest,
|
||||
orElse: () => throw Exception('未找到 urltest 组'),
|
||||
);
|
||||
|
||||
if (urlTestGroup.selected.isNotEmpty) {
|
||||
actualTag = urlTestGroup.selected;
|
||||
print('[getCurrentNodeCountry] ✅ 从 allGroups 的 urltest 组获取节点: $actualTag');
|
||||
KRLogUtil.kr_i('✅ 从 allGroups 的 urltest 组获取节点: $actualTag', tag: 'getCurrentNodeCountry');
|
||||
} else {
|
||||
print('[getCurrentNodeCountry] ❌ urltest 组的 selected 也为空');
|
||||
KRLogUtil.kr_w('❌ urltest 组的 selected 也为空', tag: 'getCurrentNodeCountry');
|
||||
return '';
|
||||
}
|
||||
} else {
|
||||
actualTag = selectGroup.selected;
|
||||
print('[getCurrentNodeCountry] ✅ 从 allGroups 的 select 组获取节点: $actualTag');
|
||||
KRLogUtil.kr_i('✅ 从 allGroups 的 select 组获取节点: $actualTag', tag: 'getCurrentNodeCountry');
|
||||
}
|
||||
} else {
|
||||
// 活动组不为空,从活动组获取
|
||||
// 从 SingBox 活动组中找到 "select" 选择器组
|
||||
final selectGroup = KRSingBoxImp.instance.kr_activeGroups.firstWhere(
|
||||
(group) => group.tag == 'select',
|
||||
orElse: () => throw Exception('未找到 select 组'),
|
||||
);
|
||||
|
||||
KRLogUtil.kr_i('找到 select 组,当前选中: ${selectGroup.selected}', tag: 'getCurrentNodeCountry');
|
||||
if (selectGroup.selected.isEmpty) {
|
||||
KRLogUtil.kr_w('❌ select 组的 selected 为空', tag: 'getCurrentNodeCountry');
|
||||
return '';
|
||||
}
|
||||
|
||||
// 获取该组当前选中的实际节点标签
|
||||
if (selectGroup.selected.isNotEmpty) {
|
||||
actualTag = selectGroup.selected;
|
||||
KRLogUtil.kr_i('从 select 组获取实际节点: $actualTag', tag: 'getCurrentNodeCountry');
|
||||
KRLogUtil.kr_i('✅ 从活动组获取节点: $actualTag', tag: 'getCurrentNodeCountry');
|
||||
}
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_w('获取实际节点失败: $e', tag: 'getCurrentNodeCountry');
|
||||
// 失败时使用 kr_cutSeletedTag 作为备选
|
||||
if (kr_cutSeletedTag.value.isNotEmpty && kr_cutSeletedTag.value != 'auto') {
|
||||
actualTag = kr_cutSeletedTag.value;
|
||||
KRLogUtil.kr_i('使用 kr_cutSeletedTag 作为备选: $actualTag', tag: 'getCurrentNodeCountry');
|
||||
KRLogUtil.kr_e('❌ 从活动组获取节点失败: $e', tag: 'getCurrentNodeCountry');
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
// 验证 actualTag 是否有效
|
||||
if (actualTag.isEmpty) {
|
||||
KRLogUtil.kr_w('❌ 节点标签为空', tag: 'getCurrentNodeCountry');
|
||||
return '';
|
||||
}
|
||||
|
||||
// 使用实际节点标签查找国家代码
|
||||
@@ -1229,6 +1411,7 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
return node.country;
|
||||
}
|
||||
|
||||
|
||||
/// 获取真实连接的节点信息(auto 模式下获取实际连接的节点)
|
||||
Map<String, dynamic> kr_getRealConnectedNodeInfo() {
|
||||
// 如果不是 auto 模式,直接返回当前选中的节点信息
|
||||
@@ -1281,6 +1464,8 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
String kr_getRealConnectedNodeCountry() {
|
||||
final info = kr_getRealConnectedNodeInfo();
|
||||
final delay = kr_currentNodeLatency.value;
|
||||
final country1 = kr_getCurrentNodeCountry();
|
||||
print('country----$country1');
|
||||
final country = kr_getCountryFullName(info['country']);
|
||||
if (delay == -2) {
|
||||
return '--';
|
||||
@@ -1463,35 +1648,52 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
KRHomeViewsListStatus.kr_serverSubscribeList ||
|
||||
kr_currentListStatus.value == KRHomeViewsListStatus.kr_subscribeList) {
|
||||
targetHeight = kr_nodeListHeight + kr_marginVertical * 2;
|
||||
KRLogUtil.kr_i(
|
||||
'节点列表状态,目标高度: $targetHeight', tag: 'HomeController');
|
||||
} else {
|
||||
KRLogUtil.kr_i('节点列表状态,目标高度: $targetHeight', tag: 'HomeController');
|
||||
}
|
||||
// 🔧 Android 15 新增:处理加载和错误状态
|
||||
else if (kr_currentListStatus.value == KRHomeViewsListStatus.kr_loading ||
|
||||
kr_currentListStatus.value == KRHomeViewsListStatus.kr_error) {
|
||||
// 加载或错误状态下显示最小高度
|
||||
targetHeight = kr_loadingHeight + kr_marginTop + kr_marginBottom;
|
||||
KRLogUtil.kr_i('加载/错误状态,目标高度: $targetHeight', tag: 'HomeController');
|
||||
}
|
||||
else {
|
||||
// 已登录状态下的默认高度计算
|
||||
targetHeight = kr_baseHeight + kr_marginTop + kr_marginBottom;
|
||||
KRLogUtil.kr_i('基础高度: $targetHeight', tag: 'HomeController');
|
||||
|
||||
// 🔧 关键修复:增加防御性检查,确保订阅服务访问异常时高度计算仍正常
|
||||
try {
|
||||
if (kr_subscribeService.kr_currentSubscribe.value != null) {
|
||||
targetHeight += kr_connectionInfoHeight + kr_marginTop;
|
||||
KRLogUtil.kr_i(
|
||||
'添加连接信息卡片高度: $targetHeight', tag: 'HomeController');
|
||||
KRLogUtil.kr_i('添加连接信息卡片高度: $targetHeight', tag: 'HomeController');
|
||||
} else {
|
||||
targetHeight += kr_subscriptionCardHeight + kr_marginTop;
|
||||
KRLogUtil.kr_i(
|
||||
'添加订阅卡片高度: $targetHeight', tag: 'HomeController');
|
||||
KRLogUtil.kr_i('添加订阅卡片高度: $targetHeight', tag: 'HomeController');
|
||||
}
|
||||
|
||||
// 如果有试用状态,添加试用卡片高度
|
||||
if (kr_subscribeService.kr_isTrial.value) {
|
||||
targetHeight += kr_trialCardHeight + kr_marginTop;
|
||||
KRLogUtil.kr_i(
|
||||
'添加试用卡片高度: $targetHeight', tag: 'HomeController');
|
||||
KRLogUtil.kr_i('添加试用卡片高度: $targetHeight', tag: 'HomeController');
|
||||
}
|
||||
// 如果是最后一天,添加最后一天卡片高度
|
||||
else if (kr_subscribeService.kr_isLastDayOfSubscription.value) {
|
||||
targetHeight += kr_lastDayCardHeight + kr_marginTop;
|
||||
KRLogUtil.kr_i(
|
||||
'添加最后一天卡片高度: $targetHeight', tag: 'HomeController');
|
||||
KRLogUtil.kr_i('添加最后一天卡片高度: $targetHeight', tag: 'HomeController');
|
||||
}
|
||||
} catch (e) {
|
||||
// 🔧 修复:订阅服务访问异常时,使用默认订阅卡片高度
|
||||
KRLogUtil.kr_e('访问订阅服务数据异常,使用默认高度: $e', tag: 'HomeController');
|
||||
targetHeight += kr_subscriptionCardHeight + kr_marginTop;
|
||||
KRLogUtil.kr_i('使用默认订阅卡片高度: $targetHeight', tag: 'HomeController');
|
||||
}
|
||||
}
|
||||
|
||||
// 🔧 Android 15 优化:确保最小高度,避免出现 0 高度
|
||||
if (targetHeight < 100) {
|
||||
KRLogUtil.kr_w('计算的高度过小($targetHeight),设置为最小高度', tag: 'HomeController');
|
||||
targetHeight = kr_loadingHeight;
|
||||
}
|
||||
|
||||
KRLogUtil.kr_i('最终目标高度: $targetHeight', tag: 'HomeController');
|
||||
@@ -1516,13 +1718,31 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
// 简化移动地图方法
|
||||
void kr_moveToLocation(LatLng location, [double zoom = 5.0]) {
|
||||
try {
|
||||
kr_mapController.move(location, zoom);
|
||||
// 🔧 关键修复:约束坐标到有效范围,防止超出地图边界
|
||||
final constrainedLocation = _constrainCoordinates(location);
|
||||
kr_mapController.move(constrainedLocation, zoom);
|
||||
kr_isUserMoving.value = false;
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_e('移动地图失败: $e', tag: 'HomeController');
|
||||
}
|
||||
}
|
||||
|
||||
/// 🔧 新增:约束坐标到有效范围内
|
||||
/// 确保坐标在 [-85, 85] 纬度和 [-180, 180] 经度范围内
|
||||
LatLng _constrainCoordinates(LatLng coords) {
|
||||
double lat = coords.latitude.clamp(-85.0, 85.0);
|
||||
double lng = coords.longitude.clamp(-180.0, 180.0);
|
||||
|
||||
if (lat != coords.latitude || lng != coords.longitude) {
|
||||
KRLogUtil.kr_w(
|
||||
'⚠️ 坐标超出范围,已约束: (${coords.latitude}, ${coords.longitude}) → ($lat, $lng)',
|
||||
tag: 'HomeController'
|
||||
);
|
||||
}
|
||||
|
||||
return LatLng(lat, lng);
|
||||
}
|
||||
|
||||
// 添加一个方法来批量更新标记
|
||||
void kr_updateMarkers(List<String> tags) {
|
||||
// 使用Set来去重
|
||||
@@ -1581,13 +1801,10 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
final activeGroups = KRSingBoxImp.instance.kr_activeGroups;
|
||||
for (int i = 0; i < activeGroups.length; i++) {
|
||||
final group = activeGroups[i];
|
||||
KRLogUtil.kr_i('📋 活动组[$i]: tag=${group.tag}, type=${group
|
||||
.type}, selected=${group.selected}', tag: 'HomeController');
|
||||
KRLogUtil.kr_i('📋 活动组[$i]: tag=${group.tag}, type=${group.type}, selected=${group.selected}', tag: 'HomeController');
|
||||
for (int j = 0; j < group.items.length; j++) {
|
||||
final item = group.items[j];
|
||||
KRLogUtil.kr_i(
|
||||
' └─ 节点[$j]: tag=${item.tag}, type=${item.type}, delay=${item
|
||||
.urlTestDelay}', tag: 'HomeController');
|
||||
KRLogUtil.kr_i(' └─ 节点[$j]: tag=${item.tag}, type=${item.type}, delay=${item.urlTestDelay}', tag: 'HomeController');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -1615,39 +1832,31 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
|
||||
try {
|
||||
KRLogUtil.kr_i('🧪 开始延迟测试...', tag: 'HomeController');
|
||||
KRLogUtil.kr_i(
|
||||
'📊 当前连接状态: ${kr_isConnected.value}', tag: 'HomeController');
|
||||
KRLogUtil.kr_i('📊 当前连接状态: ${kr_isConnected.value}', tag: 'HomeController');
|
||||
|
||||
if (kr_isConnected.value) {
|
||||
// 已连接状态:使用 SingBox 通过代理测试
|
||||
KRLogUtil.kr_i('🔗 已连接状态 - 使用 SingBox 通过代理测试延迟',
|
||||
tag: 'HomeController');
|
||||
KRLogUtil.kr_i('🔗 已连接状态 - 使用 SingBox 通过代理测试延迟', tag: 'HomeController');
|
||||
await KRSingBoxImp.instance.kr_urlTest("select");
|
||||
|
||||
// 等待一段时间让 SingBox 完成测试
|
||||
await Future.delayed(const Duration(seconds: 3));
|
||||
|
||||
// 再次检查活动组状态
|
||||
KRLogUtil.kr_i(
|
||||
'🔄 检查代理测试后的活动组状态...', tag: 'HomeController');
|
||||
KRLogUtil.kr_i('🔄 检查代理测试后的活动组状态...', tag: 'HomeController');
|
||||
final activeGroups = KRSingBoxImp.instance.kr_activeGroups;
|
||||
for (int i = 0; i < activeGroups.length; i++) {
|
||||
final group = activeGroups[i];
|
||||
KRLogUtil.kr_i('📋 活动组[$i]: tag=${group.tag}, type=${group
|
||||
.type}, selected=${group.selected}', tag: 'HomeController');
|
||||
KRLogUtil.kr_i('📋 活动组[$i]: tag=${group.tag}, type=${group.type}, selected=${group.selected}', tag: 'HomeController');
|
||||
for (int j = 0; j < group.items.length; j++) {
|
||||
final item = group.items[j];
|
||||
KRLogUtil.kr_i(
|
||||
' └─ 节点[$j]: tag=${item.tag}, type=${item.type}, delay=${item
|
||||
.urlTestDelay}', tag: 'HomeController');
|
||||
KRLogUtil.kr_i(' └─ 节点[$j]: tag=${item.tag}, type=${item.type}, delay=${item.urlTestDelay}', tag: 'HomeController');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 未连接状态:使用本机网络直接ping节点IP
|
||||
KRLogUtil.kr_i('🔌 未连接状态 - 使用本机网络直接ping节点IP测试延迟',
|
||||
tag: 'HomeController');
|
||||
KRLogUtil.kr_i(
|
||||
'🌐 这将绕过代理,直接使用本机网络连接节点', tag: 'HomeController');
|
||||
KRLogUtil.kr_i('🔌 未连接状态 - 使用本机网络直接ping节点IP测试延迟', tag: 'HomeController');
|
||||
KRLogUtil.kr_i('🌐 这将绕过代理,直接使用本机网络连接节点', tag: 'HomeController');
|
||||
await _kr_testLatencyWithoutVpn();
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -1660,57 +1869,14 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _kr_testSingleNode(dynamic item) async {
|
||||
final stopwatch = Stopwatch()..start();
|
||||
try {
|
||||
// 解析地址和端口
|
||||
String address = item.serverAddr;
|
||||
int port = 443; // 默认端口
|
||||
|
||||
// 如果 serverAddr 带端口
|
||||
if (item.serverAddr.contains(':')) {
|
||||
final parts = item.serverAddr.split(':');
|
||||
address = parts[0];
|
||||
port = int.tryParse(parts[1]) ?? 443;
|
||||
} else if (item.config['server_port'] != null) {
|
||||
port = item.config['server_port'];
|
||||
}
|
||||
|
||||
// TCP 测试连接
|
||||
final socket = await Socket.connect(
|
||||
address,
|
||||
port,
|
||||
timeout: const Duration(seconds: 8),
|
||||
);
|
||||
|
||||
final delay = stopwatch.elapsedMilliseconds;
|
||||
socket.destroy();
|
||||
|
||||
// 设置延迟阈值:超过5秒认为不可用
|
||||
if (delay > 5000) {
|
||||
item.urlTestDelay.value = 65535;
|
||||
} else {
|
||||
item.urlTestDelay.value = delay;
|
||||
}
|
||||
|
||||
print('✅ 节点 ${item.tag} 测试完成,延迟: ${item.urlTestDelay.value}ms');
|
||||
|
||||
} catch (e) {
|
||||
// 连接失败
|
||||
item.urlTestDelay.value = 65535;
|
||||
print('⚠️ 节点 ${item.tag} 测试失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 未连接状态下的延迟测试(使用本机网络真实测速)
|
||||
/// 未连接状态下的真实延迟测试(TCP连接测试)
|
||||
Future<void> _kr_testLatencyWithoutVpn() async {
|
||||
kr_isLatency.value = true;
|
||||
|
||||
try {
|
||||
KRLogUtil.kr_i('🔌 开始未连接状态延迟测试(真实测速)', tag: 'HomeController');
|
||||
KRLogUtil.kr_i('🚀 开始真实延迟测试(TCP连接测试)', tag: 'HomeController');
|
||||
KRLogUtil.kr_i('📊 当前连接状态: ${kr_isConnected.value}', tag: 'HomeController');
|
||||
|
||||
// 获取所有非 auto 节点
|
||||
// 获取所有非auto节点
|
||||
final testableNodes = kr_subscribeService.allList
|
||||
.where((item) => item.tag != 'auto')
|
||||
.toList();
|
||||
@@ -1722,13 +1888,68 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
return;
|
||||
}
|
||||
|
||||
// 依次测试每个节点延迟
|
||||
for (var node in testableNodes) {
|
||||
await _kr_testSingleNode(node); // 真实测速
|
||||
KRLogUtil.kr_i('节点 ${node.tag} 延迟: ${node.urlTestDelay.value}ms', tag: 'HomeController');
|
||||
// 准备节点地址列表
|
||||
final nodeAddresses = <MapEntry<String, SocketAddress>>[];
|
||||
|
||||
for (final node in testableNodes) {
|
||||
// 从节点配置中提取服务器地址和端口
|
||||
try {
|
||||
String host = node.serverAddr;
|
||||
int port = 0;
|
||||
|
||||
// 尝试从config中获取端口
|
||||
if (node.config.containsKey('server_port')) {
|
||||
port = node.config['server_port'] as int;
|
||||
} else if (node.config.containsKey('port')) {
|
||||
port = node.config['port'] as int;
|
||||
}
|
||||
|
||||
// 测试完成后,按延迟排序,方便界面展示
|
||||
if (host.isNotEmpty && port > 0) {
|
||||
nodeAddresses.add(MapEntry(
|
||||
node.tag,
|
||||
SocketAddress(host, port),
|
||||
));
|
||||
KRLogUtil.kr_i('✓ 节点 ${node.tag}: $host:$port', tag: 'HomeController');
|
||||
} else {
|
||||
KRLogUtil.kr_w('⚠️ 节点 ${node.tag} 缺少地址或端口信息', tag: 'HomeController');
|
||||
// 设置为失败
|
||||
node.urlTestDelay.value = 65535;
|
||||
}
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_e('❌ 解析节点 ${node.tag} 配置失败: $e', tag: 'HomeController');
|
||||
node.urlTestDelay.value = 65535;
|
||||
}
|
||||
}
|
||||
|
||||
if (nodeAddresses.isEmpty) {
|
||||
KRLogUtil.kr_w('⚠️ 没有有效的节点地址可测试', tag: 'HomeController');
|
||||
return;
|
||||
}
|
||||
|
||||
KRLogUtil.kr_i('🔌 开始批量测试 ${nodeAddresses.length} 个节点...', tag: 'HomeController');
|
||||
|
||||
// 使用真实的延迟测试工具
|
||||
final results = await KRLatencyTester.testMultipleNodes(
|
||||
nodes: nodeAddresses,
|
||||
concurrency: 10, // 每批10个并发
|
||||
timeout: 5000, // 超时时间5秒(毫秒)
|
||||
);
|
||||
|
||||
// 更新节点延迟
|
||||
for (final node in testableNodes) {
|
||||
if (results.containsKey(node.tag)) {
|
||||
node.urlTestDelay.value = results[node.tag]!;
|
||||
}
|
||||
}
|
||||
|
||||
// 统计测试结果
|
||||
final successCount = testableNodes.where((item) => item.urlTestDelay.value < 65535).length;
|
||||
final failCount = testableNodes.length - successCount;
|
||||
|
||||
KRLogUtil.kr_i('✅ 真实延迟测试完成', tag: 'HomeController');
|
||||
KRLogUtil.kr_i('📊 测试结果: 成功 $successCount 个,失败 $failCount 个', tag: 'HomeController');
|
||||
|
||||
// 显示延迟最低的前3个节点
|
||||
final sortedNodes = testableNodes
|
||||
.where((item) => item.urlTestDelay.value < 65535)
|
||||
.toList()
|
||||
@@ -1742,9 +1963,9 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
}
|
||||
}
|
||||
|
||||
KRLogUtil.kr_i('✅ 本机网络延迟测试完成', tag: 'HomeController');
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_e('❌ 本机网络延迟测试过程出错: $e', tag: 'HomeController');
|
||||
KRLogUtil.kr_e('❌ 真实延迟测试过程出错: $e', tag: 'HomeController');
|
||||
KRLogUtil.kr_e('❌ 错误堆栈: ${StackTrace.current}', tag: 'HomeController');
|
||||
} finally {
|
||||
kr_isLatency.value = false;
|
||||
}
|
||||
@@ -1788,14 +2009,55 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
kr_currentNodeLatency.value = -2; // 设置为未连接状态
|
||||
}
|
||||
|
||||
/// 调试:打印所有节点的坐标信息
|
||||
void kr_debugPrintNodeCoordinates() {
|
||||
KRLogUtil.kr_i('========== 节点坐标调试信息 ==========', tag: 'HomeController');
|
||||
KRLogUtil.kr_i('节点总数: ${kr_subscribeService.allList.length}', tag: 'HomeController');
|
||||
|
||||
if (kr_subscribeService.allList.isEmpty) {
|
||||
KRLogUtil.kr_w('节点列表为空!请检查:', tag: 'HomeController');
|
||||
KRLogUtil.kr_w('1. 是否已登录', tag: 'HomeController');
|
||||
KRLogUtil.kr_w('2. 是否有订阅', tag: 'HomeController');
|
||||
KRLogUtil.kr_w('3. 订阅是否已加载完成', tag: 'HomeController');
|
||||
return;
|
||||
}
|
||||
|
||||
int validNodes = 0;
|
||||
int invalidNodes = 0;
|
||||
|
||||
for (int i = 0; i < kr_subscribeService.allList.length; i++) {
|
||||
final node = kr_subscribeService.allList[i];
|
||||
if (node.latitude != 0.0 || node.longitude != 0.0) {
|
||||
validNodes++;
|
||||
if (i < 5) { // 只打印前5个有效节点
|
||||
KRLogUtil.kr_i('节点[$i] ${node.tag}: (${node.latitude}, ${node.longitude})', tag: 'HomeController');
|
||||
}
|
||||
} else {
|
||||
invalidNodes++;
|
||||
if (i < 3) { // 只打印前3个无效节点
|
||||
KRLogUtil.kr_w('节点[$i] ${node.tag}: 坐标为(0, 0) - 无效!', tag: 'HomeController');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
KRLogUtil.kr_i('有效节点: $validNodes', tag: 'HomeController');
|
||||
KRLogUtil.kr_w('无效节点(坐标为0): $invalidNodes', tag: 'HomeController');
|
||||
|
||||
if (invalidNodes > 0) {
|
||||
KRLogUtil.kr_w('⚠️ 发现 $invalidNodes 个节点坐标为0,这些节点不会显示在地图上', tag: 'HomeController');
|
||||
KRLogUtil.kr_w('可能原因:', tag: 'HomeController');
|
||||
KRLogUtil.kr_w('1. 后端API未返回 latitude/longitude 字段', tag: 'HomeController');
|
||||
KRLogUtil.kr_w('2. 后端数据库中节点坐标未配置', tag: 'HomeController');
|
||||
}
|
||||
}
|
||||
|
||||
/// 强制同步连接状态
|
||||
void kr_forceSyncConnectionStatus() {
|
||||
try {
|
||||
KRLogUtil.kr_i('🔄 强制同步连接状态...', tag: 'HomeController');
|
||||
|
||||
final currentStatus = KRSingBoxImp.instance.kr_status.value;
|
||||
KRLogUtil.kr_i(
|
||||
'📊 当前 SingBox 状态: $currentStatus', tag: 'HomeController');
|
||||
KRLogUtil.kr_i('📊 当前 SingBox 状态: $currentStatus', tag: 'HomeController');
|
||||
|
||||
// 根据当前状态强制更新UI
|
||||
switch (currentStatus) {
|
||||
|
||||
@@ -7,6 +7,8 @@ import 'package:kaer_with_panels/app/widgets/kr_local_image.dart';
|
||||
import 'package:kaer_with_panels/app/widgets/dialogs/hi_dialog.dart';
|
||||
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
|
||||
import 'package:kaer_with_panels/app/services/global_overlay_service.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';
|
||||
|
||||
/// ✅ 按钮组件(带多层呼吸同心圆动画)
|
||||
class HIAnimatedConnectButton extends GetView<KRHomeController> {
|
||||
@@ -20,9 +22,18 @@ class HIAnimatedConnectButton extends GetView<KRHomeController> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Obx(() {
|
||||
final isConnected = controller.kr_isConnected.value;
|
||||
final delay = controller.kr_currentNodeLatency.value;
|
||||
print('当前连接情况$delay----$isConnected');
|
||||
|
||||
// 🔧 关键: 强制读取两个 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');
|
||||
|
||||
final isShow = isConnected; // delay == -1 || isConnected;
|
||||
|
||||
final Color buttonColor = Theme.of(context).primaryColor;
|
||||
@@ -71,6 +82,10 @@ class HIAnimatedConnectButton extends GetView<KRHomeController> {
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
if(isSwitching) {
|
||||
print('🔵 Switch UI 正在更新,切换中点击了按钮: status=${status.runtimeType}, isConnected=$isConnected, isSwitching=$isSwitching');
|
||||
return;
|
||||
}
|
||||
final hasValidSubscription =
|
||||
controller.kr_subscribeService.kr_availableSubscribes.isNotEmpty;
|
||||
if (hasValidSubscription) {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:kaer_with_panels/app/localization/app_translations.dart';
|
||||
@@ -26,26 +25,19 @@ class KRHomeNodeListView extends GetView<KRHomeController> {
|
||||
static const Color krModernGreen = Color(0xFF4CAF50);
|
||||
static const Color krModernGreenLight = Color(0xFF81C784);
|
||||
|
||||
// 存储每个节点的随机延迟值(仅用于界面显示)
|
||||
static final Map<String, int> _fakeDelays = {};
|
||||
// 🔧 修复无限刷新:添加标志位确保自动测试只触发一次
|
||||
static bool _hasTriggeredAutoTest = false;
|
||||
|
||||
/// 获取显示的延迟值
|
||||
/// ✅ 修复:始终显示真实的 TCP 测试结果
|
||||
int _getDisplayDelay(KRHomeController controller, KROutboundItem item) {
|
||||
// 如果已连接,使用真实的延迟值
|
||||
if (controller.kr_isConnected.value) {
|
||||
// 直接返回真实的延迟测试结果
|
||||
// 无论是否连接VPN,都使用 item.urlTestDelay.value
|
||||
// - 已连接:通过 SingBox 代理测试的真实延迟
|
||||
// - 未连接:通过 TCP Socket 直连测试的真实延迟
|
||||
return item.urlTestDelay.value;
|
||||
}
|
||||
|
||||
// 如果未连接,使用随机延迟值
|
||||
if (!_fakeDelays.containsKey(item.tag)) {
|
||||
// 生成30ms-100ms之间的随机延迟
|
||||
final random = Random();
|
||||
_fakeDelays[item.tag] = 30 + random.nextInt(71); // 30 + (0-70) = 30-100ms
|
||||
}
|
||||
|
||||
return _fakeDelays[item.tag] ?? 0;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Obx(() {
|
||||
@@ -658,13 +650,16 @@ class KRHomeNodeListView extends GetView<KRHomeController> {
|
||||
);
|
||||
}
|
||||
|
||||
// 自动触发延迟测试(仅在未连接状态下)
|
||||
// 🔧 修复无限刷新:自动触发延迟测试(仅在未连接状态下,且只触发一次)
|
||||
if (!_hasTriggeredAutoTest) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!controller.kr_isConnected.value && !controller.kr_isLatency.value) {
|
||||
KRLogUtil.kr_i('🔄 节点列表显示 - 自动触发延迟测试', tag: 'NodeListView');
|
||||
if (!controller.kr_isConnected.value && !controller.kr_isLatency.value && !_hasTriggeredAutoTest) {
|
||||
_hasTriggeredAutoTest = true; // 标记已触发
|
||||
KRLogUtil.kr_i('🔄 节点列表显示 - 自动触发延迟测试(首次)', tag: 'NodeListView');
|
||||
controller.kr_urlTest();
|
||||
}
|
||||
});
|
||||
}
|
||||
return _kr_buildListContainer(
|
||||
context,
|
||||
child: ListView(
|
||||
|
||||
@@ -15,6 +15,7 @@ import 'package:kaer_with_panels/app/widgets/dialogs/kr_dialog.dart';
|
||||
import 'package:kaer_with_panels/app/widgets/kr_local_image.dart';
|
||||
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
|
||||
import 'dart:convert';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import 'package:kaer_with_panels/app/localization/kr_language_utils.dart';
|
||||
import 'package:kaer_with_panels/app/utils/kr_common_util.dart';
|
||||
import 'package:kaer_with_panels/app/services/singbox_imp/kr_sing_box_imp.dart';
|
||||
import 'package:kaer_with_panels/app/services/kr_site_config_service.dart';
|
||||
import 'package:kaer_with_panels/singbox/model/singbox_status.dart';
|
||||
|
||||
// import 'package:crypto/crypto.dart';
|
||||
// import 'package:encrypt/encrypt.dart';
|
||||
@@ -79,14 +80,44 @@ class HttpUtil {
|
||||
createHttpClient: () {
|
||||
KRLogUtil.kr_i('📱 createHttpClient 回调被调用', tag: 'HttpUtil');
|
||||
final client = HttpClient();
|
||||
|
||||
// ✅ 优化:智能代理回退逻辑
|
||||
client.findProxy = (url) {
|
||||
try {
|
||||
// 检查 SingBox 是否正在运行
|
||||
final singBoxStatus = KRSingBoxImp.instance.kr_status;
|
||||
final isProxyAvailable = singBoxStatus == SingboxStatus.started();
|
||||
|
||||
if (!isProxyAvailable) {
|
||||
// 代理未运行,直接使用直连
|
||||
KRLogUtil.kr_i(
|
||||
'🔄 代理未运行,使用直连模式: $url',
|
||||
tag: 'HttpUtil',
|
||||
);
|
||||
return 'DIRECT';
|
||||
}
|
||||
|
||||
// 代理正在运行,使用代理配置
|
||||
final proxyConfig = KRSingBoxImp.instance.kr_buildProxyRule();
|
||||
KRLogUtil.kr_i(
|
||||
'🔍 findProxy 被调用, url: $url, proxy: $proxyConfig',
|
||||
'✅ 使用代理模式, url: $url, proxy: $proxyConfig',
|
||||
tag: 'HttpUtil',
|
||||
);
|
||||
return proxyConfig;
|
||||
} catch (e) {
|
||||
// 发生异常时回退到直连
|
||||
KRLogUtil.kr_w(
|
||||
'⚠️ 代理配置异常,回退到直连: $e',
|
||||
tag: 'HttpUtil',
|
||||
);
|
||||
return 'DIRECT';
|
||||
}
|
||||
};
|
||||
|
||||
// ✅ 优化:设置连接失败时的自动回退
|
||||
client.connectionTimeout = const Duration(seconds: 10);
|
||||
client.badCertificateCallback = (cert, host, port) => true;
|
||||
|
||||
return client;
|
||||
},
|
||||
);
|
||||
|
||||
@@ -17,21 +17,17 @@ class KRSiteConfigService extends ChangeNotifier {
|
||||
_dio.options.sendTimeout = const Duration(seconds: 20);
|
||||
_dio.options.receiveTimeout = const Duration(seconds: 20);
|
||||
|
||||
// 🔧 配置HttpClientAdapter使用sing-box的mixed代理
|
||||
_dio.httpClientAdapter = IOHttpClientAdapter(
|
||||
createHttpClient: () {
|
||||
final client = HttpClient();
|
||||
client.findProxy = (url) {
|
||||
final proxyConfig = KRSingBoxImp.instance.kr_buildProxyRule();
|
||||
// 🔧 关键修复:网站配置请求不使用代理
|
||||
// 原因:网站配置是应用启动的第一步,此时 SingBox 还未初始化
|
||||
// 必须直接连接服务器获取配置,避免循环依赖和初始化失败
|
||||
// 之前的代理配置会导致 DioExceptionType.unknown 错误
|
||||
KRLogUtil.kr_i(
|
||||
'🔍 KRSiteConfigService 请求使用代理: $proxyConfig, url: $url',
|
||||
'🌐 网站配置服务:使用直连模式(不通过代理)',
|
||||
tag: 'KRSiteConfigService',
|
||||
);
|
||||
return proxyConfig;
|
||||
};
|
||||
return client;
|
||||
},
|
||||
);
|
||||
if (kDebugMode) {
|
||||
print('🌐 网站配置服务:使用直连模式,避免 SingBox 未初始化问题');
|
||||
}
|
||||
}
|
||||
|
||||
KRSiteConfig? _siteConfig;
|
||||
|
||||
@@ -95,6 +95,48 @@ class KRSubscribeService {
|
||||
/// 当前状态
|
||||
final kr_currentStatus = KRSubscribeServiceStatus.kr_none.obs;
|
||||
|
||||
/// ✅ 方案4:请求去重标志位 - 防止重复刷新
|
||||
bool _isRefreshing = false;
|
||||
|
||||
/// ✅ 方案5:将错误码转换为用户友好的错误信息
|
||||
String _kr_getFriendlyErrorMessage(int errorCode, String originalMsg) {
|
||||
switch (errorCode) {
|
||||
case -90001:
|
||||
return '连接超时,请检查网络连接';
|
||||
case -90002:
|
||||
return '发送数据超时,请稍后重试';
|
||||
case -90003:
|
||||
return '接收数据超时,网络较慢,请稍后重试';
|
||||
case -90004:
|
||||
return '服务器响应异常:$originalMsg';
|
||||
case -90006:
|
||||
return '网络连接失败,请检查网络设置';
|
||||
case -90007:
|
||||
return '安全证书验证失败';
|
||||
case -90008:
|
||||
return '网络连接被中断,请重试';
|
||||
case 401:
|
||||
return '登录已过期,请重新登录';
|
||||
case 403:
|
||||
return '没有访问权限';
|
||||
case 404:
|
||||
return '请求的资源不存在';
|
||||
case 500:
|
||||
return '服务器内部错误,请稍后重试';
|
||||
case 503:
|
||||
return '服务暂时不可用,请稍后重试';
|
||||
default:
|
||||
if (errorCode >= 500 && errorCode < 600) {
|
||||
return '服务器错误($errorCode),请稍后重试';
|
||||
} else if (errorCode >= 400 && errorCode < 500) {
|
||||
return '请求错误($errorCode):$originalMsg';
|
||||
} else if (errorCode < 0) {
|
||||
return '网络请求失败,请检查网络连接';
|
||||
}
|
||||
return originalMsg.isNotEmpty ? originalMsg : '未知错误,请重试';
|
||||
}
|
||||
}
|
||||
|
||||
/// 重置订阅周期
|
||||
Future<void> kr_resetSubscribePeriod() async {
|
||||
if (kr_currentSubscribe.value == null) {
|
||||
@@ -290,31 +332,19 @@ class KRSubscribeService {
|
||||
return;
|
||||
}
|
||||
|
||||
// 优先使用 API 返回的 isTryOut 字段判断试用状态
|
||||
// 🔧 关键修复:信任 API 返回的 isTryOut 字段,不再额外检查购买记录
|
||||
// 之前的逻辑会在购买套餐后仍显示"试用中",因为购买记录可能未及时更新
|
||||
final currentSubscribe = kr_currentSubscribe.value!;
|
||||
|
||||
// 1. 优先使用 API 返回的 isTryOut 字段
|
||||
// 1. 使用 API 返回的 isTryOut 字段(最权威的判断)
|
||||
kr_isTrial.value = currentSubscribe.isTryOut;
|
||||
KRLogUtil.kr_i('步骤1 - API isTryOut 字段: ${currentSubscribe.isTryOut}', tag: 'SubscribeService');
|
||||
|
||||
// 2. 如果 API 说不是试用,检查是否有购买记录
|
||||
if (!kr_isTrial.value) {
|
||||
final bool kr_isSubscribed = kr_alreadySubscribe.any(
|
||||
(subscribe) => currentSubscribe.id == subscribe.userSubscribeId
|
||||
);
|
||||
KRLogUtil.kr_i('步骤2 - 检查购买记录: $kr_isSubscribed', tag: 'SubscribeService');
|
||||
|
||||
// 如果没有购买记录,判断为试用
|
||||
if (!kr_isSubscribed) {
|
||||
kr_isTrial.value = true;
|
||||
KRLogUtil.kr_i('步骤2 - 没有购买记录,判定为试用', tag: 'SubscribeService');
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 最后检查订阅名称是否包含"试用"关键字(最后的备用方案)
|
||||
// 2. 仅在 API 没有明确标识时,才检查订阅名称作为备用方案
|
||||
// 注意:只有当 API 说不是试用,但名称包含"试用"时才覆盖
|
||||
if (!kr_isTrial.value && currentSubscribe.name.contains('试用')) {
|
||||
kr_isTrial.value = true;
|
||||
KRLogUtil.kr_i('步骤3 - 订阅名称包含"试用"关键字,判定为试用', tag: 'SubscribeService');
|
||||
KRLogUtil.kr_i('步骤2 - 订阅名称包含"试用"关键字,判定为试用', tag: 'SubscribeService');
|
||||
}
|
||||
|
||||
KRLogUtil.kr_i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', tag: 'SubscribeService');
|
||||
@@ -456,12 +486,23 @@ class KRSubscribeService {
|
||||
/// 刷新所有数据
|
||||
Future<void> kr_refreshAll() async {
|
||||
try {
|
||||
// ✅ 方案4:请求去重检查 - 防止重复刷新
|
||||
if (_isRefreshing) {
|
||||
KRLogUtil.kr_w('⚠️ 正在刷新中,忽略重复请求', tag: 'SubscribeService');
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置刷新标志
|
||||
_isRefreshing = true;
|
||||
KRLogUtil.kr_i('🔄 开始刷新订阅数据...', tag: 'SubscribeService');
|
||||
|
||||
// 🔧 修复2: 添加登录状态检查 - 只有已登录用户才能刷新订阅数据
|
||||
if (!KRAppRunData().kr_isLogin.value) {
|
||||
KRLogUtil.kr_e('❌ 未登录用户,无法刷新订阅数据', tag: 'SubscribeService');
|
||||
kr_availableSubscribes.clear();
|
||||
kr_currentSubscribe.value = null;
|
||||
kr_currentStatus.value = KRSubscribeServiceStatus.kr_error;
|
||||
_isRefreshing = false; // 重置标志
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -474,8 +515,10 @@ class KRSubscribeService {
|
||||
await kr_subscribeApi.kr_getAlreadySubscribe();
|
||||
alreadySubscribeResult.fold(
|
||||
(error) {
|
||||
KRLogUtil.kr_e('获取已订阅列表失败: ${error.msg}', tag: 'SubscribeService');
|
||||
throw Exception('获取已订阅列表失败: ${error.msg}');
|
||||
// ✅ 方案5:使用友好的错误信息
|
||||
final friendlyMsg = _kr_getFriendlyErrorMessage(error.code, error.msg);
|
||||
KRLogUtil.kr_e('获取已订阅列表失败: $friendlyMsg (错误码: ${error.code})', tag: 'SubscribeService');
|
||||
throw Exception('获取已订阅列表失败: $friendlyMsg');
|
||||
},
|
||||
(subscribes) {
|
||||
kr_alreadySubscribe.value = subscribes;
|
||||
@@ -498,7 +541,10 @@ class KRSubscribeService {
|
||||
// 处理订阅列表结果
|
||||
final subscribes = await subscribeResult.fold(
|
||||
(error) {
|
||||
throw Exception('获取可用订阅失败: ${error.msg}');
|
||||
// ✅ 方案5:使用友好的错误信息
|
||||
final friendlyMsg = _kr_getFriendlyErrorMessage(error.code, error.msg);
|
||||
KRLogUtil.kr_e('获取可用订阅失败: $friendlyMsg (错误码: ${error.code})', tag: 'SubscribeService');
|
||||
throw Exception('获取可用订阅失败: $friendlyMsg');
|
||||
},
|
||||
(subscribes) => subscribes,
|
||||
);
|
||||
@@ -591,7 +637,10 @@ class KRSubscribeService {
|
||||
// 处理节点列表结果
|
||||
final nodes = await nodeResult.fold(
|
||||
(error) {
|
||||
throw Exception('获取节点列表失败: ${error.msg}');
|
||||
// ✅ 方案5:使用友好的错误信息
|
||||
final friendlyMsg = _kr_getFriendlyErrorMessage(error.code, error.msg);
|
||||
KRLogUtil.kr_e('获取节点列表失败: $friendlyMsg (错误码: ${error.code})', tag: 'SubscribeService');
|
||||
throw Exception('获取节点列表失败: $friendlyMsg');
|
||||
},
|
||||
(nodes) => nodes,
|
||||
);
|
||||
@@ -626,7 +675,10 @@ class KRSubscribeService {
|
||||
} catch (err, stackTrace) {
|
||||
kr_currentStatus.value = KRSubscribeServiceStatus.kr_error;
|
||||
KRLogUtil.kr_e('刷新数据异常: $err\n$stackTrace', tag: 'SubscribeService');
|
||||
rethrow;
|
||||
} finally {
|
||||
// ✅ 方案4:无论成功或失败,都重置刷新标志
|
||||
_isRefreshing = false;
|
||||
KRLogUtil.kr_i('✅ 刷新完成,重置刷新标志', tag: 'SubscribeService');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -77,8 +77,6 @@ class KRSingBoxImp {
|
||||
/// 是否自动自动选择线路
|
||||
final kr_isAutoOutbound = false.obs;
|
||||
|
||||
bool _initialized = false;
|
||||
|
||||
/// 连接类型
|
||||
final kr_connectionType = KRConnectionType.rule.obs;
|
||||
|
||||
@@ -148,12 +146,6 @@ class KRSingBoxImp {
|
||||
}
|
||||
|
||||
try {
|
||||
if (_initialized) {
|
||||
KRLogUtil.kr_i('SingBox 已经初始化,跳过重复初始化');
|
||||
return;
|
||||
}
|
||||
_initialized = true;
|
||||
|
||||
KRLogUtil.kr_i('开始初始化 SingBox');
|
||||
// 在应用启动时初始化
|
||||
await KRCountryUtil.kr_init();
|
||||
@@ -620,8 +612,7 @@ class KRSingBoxImp {
|
||||
|
||||
/// 订阅分组数据流
|
||||
void _kr_subscribeToGroups() {
|
||||
KRLogUtil.kr_i('🛰 启动分组监听 watchActiveGroups / watchGroups', tag: 'SingBox');
|
||||
|
||||
print('[_kr_subscribeToGroups] 🚀 开始订阅分组数据流');
|
||||
// 取消之前的分组订阅
|
||||
for (var sub in _kr_subscriptions) {
|
||||
if (sub.hashCode.toString().contains('Groups')) {
|
||||
@@ -634,6 +625,7 @@ class KRSingBoxImp {
|
||||
_kr_subscriptions.add(
|
||||
kr_singBox.watchActiveGroups().listen(
|
||||
(groups) {
|
||||
print('[watchActiveGroups] 📡 收到活动组更新,数量: ${groups.length}');
|
||||
KRLogUtil.kr_i('📡 收到活动组更新,数量: ${groups.length}', tag: 'SingBox');
|
||||
kr_activeGroups.value = groups;
|
||||
|
||||
@@ -650,6 +642,7 @@ class KRSingBoxImp {
|
||||
KRLogUtil.kr_i('✅ 活动组处理完成', tag: 'SingBox');
|
||||
},
|
||||
onError: (error) {
|
||||
print('[watchActiveGroups] ❌ 活动分组监听错误: $error');
|
||||
KRLogUtil.kr_e('❌ 活动分组监听错误: $error', tag: 'SingBox');
|
||||
},
|
||||
cancelOnError: false,
|
||||
@@ -659,14 +652,22 @@ class KRSingBoxImp {
|
||||
_kr_subscriptions.add(
|
||||
kr_singBox.watchGroups().listen(
|
||||
(groups) {
|
||||
print('[watchGroups] 📡 收到所有组更新,数量: ${groups.length}');
|
||||
kr_allGroups.value = groups;
|
||||
// 打印每个组的基本信息
|
||||
for (int i = 0; i < groups.length; i++) {
|
||||
final group = groups[i];
|
||||
print('[watchGroups] 组[$i]: tag=${group.tag}, type=${group.type}, selected=${group.selected}');
|
||||
}
|
||||
},
|
||||
onError: (error) {
|
||||
print('[watchGroups] ❌ 所有分组监听错误: $error');
|
||||
KRLogUtil.kr_e('所有分组监听错误: $error');
|
||||
},
|
||||
cancelOnError: false,
|
||||
),
|
||||
);
|
||||
print('[_kr_subscribeToGroups] ✅ 分组数据流订阅完成,当前订阅数: ${_kr_subscriptions.length}');
|
||||
}
|
||||
|
||||
/// 验证节点选择是否生效
|
||||
@@ -1128,7 +1129,7 @@ class KRSingBoxImp {
|
||||
|
||||
KRLogUtil.kr_i('✅ SingBox 核心已启动,开始初始化 command client', tag: 'SingBox');
|
||||
|
||||
// 🔑 在后台延迟订阅统计流,避免阻塞 UI
|
||||
// 🔑 在后台延迟订阅统计流和分组流,避免阻塞 UI
|
||||
Future.delayed(const Duration(milliseconds: 1000), () async {
|
||||
try {
|
||||
KRLogUtil.kr_i('📊 开始订阅统计数据流...', tag: 'SingBox');
|
||||
@@ -1147,6 +1148,24 @@ class KRSingBoxImp {
|
||||
});
|
||||
}
|
||||
|
||||
// 🔧 关键修复:订阅分组数据流
|
||||
try {
|
||||
KRLogUtil.kr_i('📋 开始订阅分组数据流...', tag: 'SingBox');
|
||||
_kr_subscribeToGroups();
|
||||
KRLogUtil.kr_i('✅ 分组数据流订阅成功', tag: 'SingBox');
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_w('⚠️ 分组数据流订阅失败(稍后重试): $e', tag: 'SingBox');
|
||||
// 如果第一次失败,再等待一段时间重试
|
||||
Future.delayed(const Duration(milliseconds: 2000), () {
|
||||
try {
|
||||
_kr_subscribeToGroups();
|
||||
KRLogUtil.kr_i('✅ 分组数据流重试订阅成功', tag: 'SingBox');
|
||||
} catch (e2) {
|
||||
KRLogUtil.kr_e('❌ 分组数据流重试订阅失败: $e2', tag: 'SingBox');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 🔧 关键修复:恢复用户选择的节点
|
||||
try {
|
||||
final selectedNode = await KRSecureStorage().kr_readData(key: _keySelectedNode);
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import 'dart:io';
|
||||
import 'dart:async';
|
||||
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
|
||||
|
||||
/// 延迟测试工具类
|
||||
/// 提供真实的 TCP 连接延迟测试功能
|
||||
class KRLatencyTester {
|
||||
/// 测试单个节点的延迟
|
||||
///
|
||||
/// 参数:
|
||||
/// - host: 主机地址
|
||||
/// - port: 端口号
|
||||
/// - timeout: 超时时间(毫秒)
|
||||
///
|
||||
/// 返回:
|
||||
/// - 延迟时间(毫秒),如果失败返回 65535
|
||||
static Future<int> testNode({
|
||||
required String host,
|
||||
required int port,
|
||||
int timeout = 5000,
|
||||
}) async {
|
||||
try {
|
||||
final stopwatch = Stopwatch()..start();
|
||||
|
||||
final socket = await Socket.connect(
|
||||
host,
|
||||
port,
|
||||
timeout: Duration(milliseconds: timeout),
|
||||
).timeout(Duration(milliseconds: timeout));
|
||||
|
||||
stopwatch.stop();
|
||||
|
||||
// 立即关闭连接
|
||||
await socket.close();
|
||||
socket.destroy();
|
||||
|
||||
final latency = stopwatch.elapsedMilliseconds;
|
||||
KRLogUtil.kr_i('✅ 延迟测试成功: $host:$port = ${latency}ms', tag: 'KRLatencyTester');
|
||||
|
||||
return latency;
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_w('❌ 延迟测试失败: $host:$port - $e', tag: 'KRLatencyTester');
|
||||
return 65535; // 测试失败返回最大值
|
||||
}
|
||||
}
|
||||
|
||||
/// 批量测试多个节点的延迟
|
||||
///
|
||||
/// 参数:
|
||||
/// - nodes: 节点列表,格式为 [{"host": "example.com", "port": 443}]
|
||||
/// - concurrency: 并发数量
|
||||
/// - timeout: 超时时间(毫秒)
|
||||
///
|
||||
/// 返回:
|
||||
/// - 测试结果映射,键为 "host:port",值为延迟时间
|
||||
static Future<Map<String, int>> testMultipleNodes({
|
||||
required List<MapEntry<String, SocketAddress>> nodes,
|
||||
int concurrency = 10,
|
||||
int timeout = 5000,
|
||||
}) async {
|
||||
final results = <String, int>{};
|
||||
final semaphore = Completer<void>();
|
||||
var activeCount = 0;
|
||||
var completedCount = 0;
|
||||
|
||||
KRLogUtil.kr_i('🚀 开始批量延迟测试,共 ${nodes.length} 个节点,并发数: $concurrency', tag: 'KRLatencyTester');
|
||||
|
||||
Future<void> processNode(MapEntry<String, SocketAddress> node) async {
|
||||
try {
|
||||
final host = node.value.address;
|
||||
final port = node.value.port;
|
||||
final key = node.key;
|
||||
|
||||
final latency = await testNode(
|
||||
host: host,
|
||||
port: port,
|
||||
timeout: timeout,
|
||||
);
|
||||
|
||||
results[key] = latency;
|
||||
} catch (e) {
|
||||
results[node.key] = 65535;
|
||||
KRLogUtil.kr_e('❌ 节点测试异常: ${node.key} - $e', tag: 'KRLatencyTester');
|
||||
} finally {
|
||||
completedCount++;
|
||||
activeCount--;
|
||||
|
||||
if (completedCount >= nodes.length) {
|
||||
semaphore.complete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 分批处理节点
|
||||
for (var i = 0; i < nodes.length; i += concurrency) {
|
||||
final batch = nodes.skip(i).take(concurrency);
|
||||
|
||||
for (final node in batch) {
|
||||
activeCount++;
|
||||
processNode(node);
|
||||
}
|
||||
|
||||
// 等待当前批次完成
|
||||
if (i + concurrency < nodes.length) {
|
||||
await Future.delayed(Duration(milliseconds: 100));
|
||||
}
|
||||
}
|
||||
|
||||
// 等待所有任务完成
|
||||
await semaphore.future;
|
||||
|
||||
KRLogUtil.kr_i('✅ 批量延迟测试完成,成功: ${results.length} 个', tag: 'KRLatencyTester');
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
/// Socket 地址类
|
||||
/// 表示网络地址和端口的组合
|
||||
class SocketAddress {
|
||||
final String address;
|
||||
final int port;
|
||||
|
||||
SocketAddress(this.address, this.port);
|
||||
|
||||
@override
|
||||
String toString() => '$address:$port';
|
||||
}
|
||||
@@ -100,24 +100,38 @@ class KRSecureStorage {
|
||||
return hash.bytes;
|
||||
}
|
||||
|
||||
// 获取存储箱
|
||||
Box<dynamic> get _box => Hive.box(_boxName);
|
||||
// 🔧 修复:确保 box 始终打开
|
||||
Future<Box<dynamic>> _ensureBoxOpen() async {
|
||||
if (!Hive.isBoxOpen(_boxName)) {
|
||||
KRLogUtil.kr_w('⚠️ Box 未打开,重新打开: $_boxName', tag: 'SecureStorage');
|
||||
final key = HiveAesCipher(_generateKey());
|
||||
await Hive.openBox(_boxName, encryptionCipher: key);
|
||||
KRLogUtil.kr_i('✅ Box 已重新打开', tag: 'SecureStorage');
|
||||
}
|
||||
return Hive.box(_boxName);
|
||||
}
|
||||
|
||||
// 存储数据
|
||||
Future<void> kr_saveData({required String key, required String value}) async {
|
||||
try {
|
||||
await _box.put(key, value);
|
||||
final box = await _ensureBoxOpen();
|
||||
await box.put(key, value);
|
||||
KRLogUtil.kr_i('✅ 数据已保存: $key', tag: 'SecureStorage');
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_e('存储数据失败: $e', tag: 'SecureStorage');
|
||||
KRLogUtil.kr_e('❌ 存储数据失败: $e', tag: 'SecureStorage');
|
||||
rethrow; // 重新抛出异常,让调用者知道保存失败
|
||||
}
|
||||
}
|
||||
|
||||
// 读取数据
|
||||
Future<String?> kr_readData({required String key}) async {
|
||||
try {
|
||||
return _box.get(key) as String?;
|
||||
final box = await _ensureBoxOpen();
|
||||
final value = box.get(key) as String?;
|
||||
KRLogUtil.kr_i('📖 读取数据: $key = ${value != null ? "存在" : "null"}', tag: 'SecureStorage');
|
||||
return value;
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_e('读取数据失败: $e', tag: 'SecureStorage');
|
||||
KRLogUtil.kr_e('❌ 读取数据失败: $e', tag: 'SecureStorage');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -125,27 +139,32 @@ class KRSecureStorage {
|
||||
// 删除数据
|
||||
Future<void> kr_deleteData({required String key}) async {
|
||||
try {
|
||||
await _box.delete(key);
|
||||
final box = await _ensureBoxOpen();
|
||||
await box.delete(key);
|
||||
KRLogUtil.kr_i('🗑️ 数据已删除: $key', tag: 'SecureStorage');
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_e('删除数据失败: $e', tag: 'SecureStorage');
|
||||
KRLogUtil.kr_e('❌ 删除数据失败: $e', tag: 'SecureStorage');
|
||||
}
|
||||
}
|
||||
|
||||
// 清除所有数据
|
||||
Future<void> kr_clearAllData() async {
|
||||
try {
|
||||
await _box.clear();
|
||||
final box = await _ensureBoxOpen();
|
||||
await box.clear();
|
||||
KRLogUtil.kr_i('🧹 所有数据已清除', tag: 'SecureStorage');
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_e('清除数据失败: $e', tag: 'SecureStorage');
|
||||
KRLogUtil.kr_e('❌ 清除数据失败: $e', tag: 'SecureStorage');
|
||||
}
|
||||
}
|
||||
|
||||
// 检查键是否存在
|
||||
Future<bool> kr_hasKey({required String key}) async {
|
||||
try {
|
||||
return _box.containsKey(key);
|
||||
final box = await _ensureBoxOpen();
|
||||
return box.containsKey(key);
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_e('检查键失败: $e', tag: 'SecureStorage');
|
||||
KRLogUtil.kr_e('❌ 检查键失败: $e', tag: 'SecureStorage');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -153,18 +172,20 @@ class KRSecureStorage {
|
||||
// 保存布尔值
|
||||
Future<void> kr_saveBool({required String key, required bool value}) async {
|
||||
try {
|
||||
await _box.put(key, value);
|
||||
final box = await _ensureBoxOpen();
|
||||
await box.put(key, value);
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_e('存储布尔值失败: $e', tag: 'SecureStorage');
|
||||
KRLogUtil.kr_e('❌ 存储布尔值失败: $e', tag: 'SecureStorage');
|
||||
}
|
||||
}
|
||||
|
||||
// 获取布尔值
|
||||
Future<bool?> kr_getBool({required String key}) async {
|
||||
try {
|
||||
return _box.get(key) as bool?;
|
||||
final box = await _ensureBoxOpen();
|
||||
return box.get(key) as bool?;
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_e('读取布尔值失败: $e', tag: 'SecureStorage');
|
||||
KRLogUtil.kr_e('❌ 读取布尔值失败: $e', tag: 'SecureStorage');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -172,18 +193,20 @@ class KRSecureStorage {
|
||||
// 保存整数
|
||||
Future<void> kr_saveInt({required String key, required int value}) async {
|
||||
try {
|
||||
await _box.put(key, value);
|
||||
final box = await _ensureBoxOpen();
|
||||
await box.put(key, value);
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_e('存储整数失败: $e', tag: 'SecureStorage');
|
||||
KRLogUtil.kr_e('❌ 存储整数失败: $e', tag: 'SecureStorage');
|
||||
}
|
||||
}
|
||||
|
||||
// 获取整数
|
||||
Future<int?> kr_getInt({required String key}) async {
|
||||
try {
|
||||
return _box.get(key) as int?;
|
||||
final box = await _ensureBoxOpen();
|
||||
return box.get(key) as int?;
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_e('读取整数失败: $e', tag: 'SecureStorage');
|
||||
KRLogUtil.kr_e('❌ 读取整数失败: $e', tag: 'SecureStorage');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ class KRWindowManager with WindowListener, TrayListener {
|
||||
// 确保在 Windows 下正确设置窗口属性
|
||||
if (Platform.isWindows) {
|
||||
await windowManager.setTitleBarStyle(TitleBarStyle.normal);
|
||||
await windowManager.setTitle('BearVPN');
|
||||
await windowManager.setTitle('HiFastVPN');
|
||||
await windowManager.setSize(const Size(800, 668));
|
||||
await windowManager.setMinimumSize(const Size(800, 668));
|
||||
await windowManager.center();
|
||||
|
||||
@@ -14,7 +14,7 @@ import 'package:kaer_with_panels/utils/custom_loggers.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
|
||||
class PlatformSingboxService with InfraLogger implements SingboxService {
|
||||
static const channelPrefix = "com.baer.app";
|
||||
static const channelPrefix = "com.hi.app";
|
||||
|
||||
static const methodChannel = MethodChannel("$channelPrefix/method");
|
||||
static const statusChannel =
|
||||
|
||||
@@ -78,22 +78,22 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
connectivity_plus: e74b9f74717d2d99d45751750e266e55912baeb5
|
||||
device_info_plus: 4fb280989f669696856f8b129e4a5e3cd6c48f76
|
||||
flutter_inappwebview_macos: c2d68649f9f8f1831bfcd98d73fd6256366d9d1d
|
||||
flutter_udid: d26e455e8c06174e6aff476e147defc6cae38495
|
||||
connectivity_plus: 18d3c32514c886e046de60e9c13895109866c747
|
||||
device_info_plus: 1b14eed9bf95428983aed283a8d51cce3d8c4215
|
||||
flutter_inappwebview_macos: bdf207b8f4ebd58e86ae06cd96b147de99a67c9b
|
||||
flutter_udid: 2e7b3da4b5fdfba86a396b97898f5fe8f4ec1a52
|
||||
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
|
||||
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
|
||||
package_info_plus: f0052d280d17aa382b932f399edf32507174e870
|
||||
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
||||
package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b
|
||||
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
||||
ReachabilitySwift: 32793e867593cfc1177f5d16491e3a197d2fccda
|
||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
||||
screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f
|
||||
tray_manager: a104b5c81b578d83f3c3d0f40a997c8b10810166
|
||||
url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673
|
||||
webview_flutter_wkwebview: 1821ceac936eba6f7984d89a9f3bcb4dea99ebb2
|
||||
window_manager: 1d01fa7ac65a6e6f83b965471b1a7fdd3f06166c
|
||||
screen_retriever_macos: 776e0fa5d42c6163d2bf772d22478df4b302b161
|
||||
tray_manager: 9064e219c56d75c476e46b9a21182087930baf90
|
||||
url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404
|
||||
webview_flutter_wkwebview: a4af96a051138e28e29f60101d094683b9f82188
|
||||
window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8
|
||||
|
||||
PODFILE CHECKSUM: 04e3af9980f29522a03273385f61d561da92c2fb
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
COCOAPODS: 1.15.2
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
||||
333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
|
||||
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; };
|
||||
33CC10ED2044A3C60003C045 /* BearVPN.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BearVPN.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
33CC10ED2044A3C60003C045 /* HiFastVPN.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = HiFastVPN.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; };
|
||||
33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
|
||||
@@ -161,7 +161,7 @@
|
||||
33CC10EE2044A3C60003C045 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
33CC10ED2044A3C60003C045 /* BearVPN.app */,
|
||||
33CC10ED2044A3C60003C045 /* HiFastVPN.app */,
|
||||
331C80D5294CF71000263BE5 /* RunnerTests.xctest */,
|
||||
);
|
||||
name = Products;
|
||||
@@ -252,7 +252,7 @@
|
||||
);
|
||||
name = Runner;
|
||||
productName = Runner;
|
||||
productReference = 33CC10ED2044A3C60003C045 /* BearVPN.app */;
|
||||
productReference = 33CC10ED2044A3C60003C045 /* HiFastVPN.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
@@ -577,13 +577,13 @@
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_HARDENED_RUNTIME = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = BearVPN;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = HiFastVPN;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.15;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.baer.com;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.hi.com;
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
@@ -710,13 +710,13 @@
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_HARDENED_RUNTIME = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = BearVPN;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = HiFastVPN;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.15;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.baer.com;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.hi.com;
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
@@ -737,13 +737,13 @@
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_HARDENED_RUNTIME = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = BearVPN;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = HiFastVPN;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.15;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.baer.com;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.hi.com;
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
|
||||
BuildableName = "BearVPN.app"
|
||||
BuildableName = "HiFastVPN.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
@@ -31,7 +31,7 @@
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
|
||||
BuildableName = "BearVPN.app"
|
||||
BuildableName = "HiFastVPN.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
@@ -66,7 +66,7 @@
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
|
||||
BuildableName = "BearVPN.app"
|
||||
BuildableName = "HiFastVPN.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
@@ -83,7 +83,7 @@
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
|
||||
BuildableName = "BearVPN.app"
|
||||
BuildableName = "HiFastVPN.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
|
||||
@@ -1,68 +1,68 @@
|
||||
{
|
||||
"images" : [
|
||||
"images": [
|
||||
{
|
||||
"filename" : "icon-16.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "16x16"
|
||||
"size": "16x16",
|
||||
"idiom": "mac",
|
||||
"filename": "icon-16.png",
|
||||
"scale": "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-16@2x.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "16x16"
|
||||
"size": "16x16",
|
||||
"idiom": "mac",
|
||||
"filename": "icon-16@2x.png",
|
||||
"scale": "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-32.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "32x32"
|
||||
"size": "32x32",
|
||||
"idiom": "mac",
|
||||
"filename": "icon-32.png",
|
||||
"scale": "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-32@2x.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "32x32"
|
||||
"size": "32x32",
|
||||
"idiom": "mac",
|
||||
"filename": "icon-32@2x.png",
|
||||
"scale": "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-128.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "128x128"
|
||||
"size": "128x128",
|
||||
"idiom": "mac",
|
||||
"filename": "icon-128.png",
|
||||
"scale": "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-128@2x.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "128x128"
|
||||
"size": "128x128",
|
||||
"idiom": "mac",
|
||||
"filename": "icon-128@2x.png",
|
||||
"scale": "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-256.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "256x256"
|
||||
"size": "256x256",
|
||||
"idiom": "mac",
|
||||
"filename": "icon-256.png",
|
||||
"scale": "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-256@2x.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "256x256"
|
||||
"size": "256x256",
|
||||
"idiom": "mac",
|
||||
"filename": "icon-256@2x.png",
|
||||
"scale": "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-512.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "512x512"
|
||||
"size": "512x512",
|
||||
"idiom": "mac",
|
||||
"filename": "icon-512.png",
|
||||
"scale": "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-512@2x.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "512x512"
|
||||
"size": "512x512",
|
||||
"idiom": "mac",
|
||||
"filename": "icon-512@2x.png",
|
||||
"scale": "2x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
"info": {
|
||||
"version": 1,
|
||||
"author": "icon.wuruihong.com"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 650 B After Width: | Height: | Size: 706 B |
|
Before Width: | Height: | Size: 962 B After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 962 B After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 13 KiB |
@@ -13,7 +13,7 @@
|
||||
</customObject>
|
||||
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
|
||||
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
|
||||
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="BearVPN" customModuleProvider="target">
|
||||
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="HiFastVPN" customModuleProvider="target">
|
||||
<connections>
|
||||
<outlet property="applicationMenu" destination="uQy-DD-JDr" id="XBo-yE-nKs"/>
|
||||
<outlet property="mainFlutterWindow" destination="QvC-M9-y7g" id="gIp-Ho-8D9"/>
|
||||
@@ -330,7 +330,7 @@
|
||||
</items>
|
||||
<point key="canvasLocation" x="142" y="-258"/>
|
||||
</menu>
|
||||
<window title="APP_NAME" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" animationBehavior="default" id="QvC-M9-y7g" customClass="MainFlutterWindow" customModule="BearVPN" customModuleProvider="target">
|
||||
<window title="APP_NAME" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" animationBehavior="default" id="QvC-M9-y7g" customClass="MainFlutterWindow" customModule="HiFastVPN" customModuleProvider="target">
|
||||
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
|
||||
<rect key="contentRect" x="335" y="390" width="800" height="600"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="1512" height="944"/>
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
// 'flutter create' template.
|
||||
|
||||
// The application's name. By default this is also the title of the Flutter window.
|
||||
PRODUCT_NAME = BearVPN
|
||||
PRODUCT_NAME = HiFastVPN
|
||||
|
||||
// The application's bundle identifier
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.baer.com
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.hi.com
|
||||
|
||||
// The copyright displayed in application information
|
||||
PRODUCT_COPYRIGHT = Copyright © 2023 BearVPN.com. All rights reserved.
|
||||
PRODUCT_COPYRIGHT = Copyright © 2025 HiFastVPN.com. All rights reserved.
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
# Flutter Windows 单文件 EXE 打包指南
|
||||
|
||||
## 🎯 目标
|
||||
将 Flutter Windows 应用打包成单个可执行文件,便于分发和部署。
|
||||
|
||||
## 📋 当前状态
|
||||
|
||||
### 现有构建输出
|
||||
```
|
||||
build/windows/x64/runner/Release/
|
||||
├── hostexecutor.exe # 主程序
|
||||
├── flutter_windows.dll # Flutter 引擎
|
||||
├── msvcp140.dll # Visual C++ 运行时
|
||||
├── vcruntime140.dll # Visual C++ 运行时
|
||||
├── vcruntime140_1.dll # Visual C++ 运行时
|
||||
└── data/ # 应用数据文件夹
|
||||
├── app.so # Dart 代码编译结果
|
||||
└── flutter_assets/ # 资源文件
|
||||
```
|
||||
|
||||
### 问题分析
|
||||
Flutter 默认构建会生成多个文件,因为:
|
||||
1. **Flutter 引擎** (`flutter_windows.dll`) - 必须包含
|
||||
2. **Visual C++ 运行时** - 系统依赖
|
||||
3. **应用数据** (`data` 文件夹) - 包含资源和 Dart 代码
|
||||
|
||||
## 🔧 解决方案
|
||||
|
||||
### 方案一:使用 Enigma Virtual Box(推荐)
|
||||
|
||||
#### 步骤 1:下载安装
|
||||
1. 下载 [Enigma Virtual Box](https://enigmaprotector.com/en/downloads.html)
|
||||
2. 安装并运行
|
||||
|
||||
#### 步骤 2:打包配置
|
||||
1. **主程序文件**: 选择 `build/windows/x64/runner/Release/hostexecutor.exe`
|
||||
2. **添加文件夹**: 选择整个 `Release` 文件夹
|
||||
3. **输出文件**: 设置输出路径和文件名
|
||||
4. **文件选项**: 勾选"压缩文件"
|
||||
|
||||
#### 步骤 3:生成单文件
|
||||
点击"Process"生成单个可执行文件。
|
||||
|
||||
### 方案二:使用 Inno Setup(安装程序)
|
||||
|
||||
#### 步骤 1:下载安装
|
||||
1. 下载 [Inno Setup](https://jrsoftware.org/isinfo.php)
|
||||
2. 安装并运行
|
||||
|
||||
#### 步骤 2:创建脚本
|
||||
```ini
|
||||
[Setup]
|
||||
AppName=HostExecutor
|
||||
AppVersion=1.0.0
|
||||
DefaultDirName={autopf}\HostExecutor
|
||||
OutputBaseFilename=HostExecutor_Setup
|
||||
|
||||
[Files]
|
||||
Source: "build\windows\x64\runner\Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs
|
||||
|
||||
[Icons]
|
||||
Name: "{group}\HostExecutor"; Filename: "{app}\hostexecutor.exe"
|
||||
```
|
||||
|
||||
#### 步骤 3:编译生成
|
||||
编译脚本生成安装程序。
|
||||
|
||||
### 方案三:使用 Windows 自带工具(高级)
|
||||
|
||||
#### 使用 `dotnet publish`(需要 .NET 包装)
|
||||
```bash
|
||||
# 需要创建 .NET 包装器项目
|
||||
dotnet publish -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true
|
||||
```
|
||||
|
||||
## 🚀 自动化脚本
|
||||
|
||||
### Enigma Virtual Box 自动化脚本
|
||||
```powershell
|
||||
# package_single_exe.ps1
|
||||
$enigmaPath = "C:\Program Files\Enigma Virtual Box\enigmavb.exe"
|
||||
$inputFile = "build\windows\x64\runner\Release\hostexecutor.exe"
|
||||
$outputFile = "dist\HostExecutor_Single.exe"
|
||||
$folderPath = "build\windows\x64\runner\Release"
|
||||
|
||||
& $enigmaPath /sf $inputFile /lf $outputFile /folder $folderPath /compress
|
||||
```
|
||||
|
||||
## 📦 打包后文件结构
|
||||
|
||||
### 单文件输出
|
||||
```
|
||||
dist/
|
||||
└── HostExecutor_Single.exe # 单个可执行文件(50-100MB)
|
||||
```
|
||||
|
||||
### 文件大小分析
|
||||
- **原始文件**: 约 30-50MB
|
||||
- **压缩后**: 约 25-40MB(取决于压缩率)
|
||||
- **最终大小**: 50-100MB(包含所有依赖)
|
||||
|
||||
## ⚠️ 注意事项
|
||||
|
||||
### 运行时依赖
|
||||
1. **Windows 版本**: Windows 10 版本 1903+ 或 Windows 11
|
||||
2. **系统组件**: 确保目标系统有最新的 Windows 更新
|
||||
3. **防病毒软件**: 单文件可能被误报,需要添加信任
|
||||
|
||||
### 性能影响
|
||||
- **启动时间**: 单文件启动会稍慢(需要解压)
|
||||
- **内存使用**: 运行时内存占用相同
|
||||
- **文件大小**: 比多文件版本大约 10-20%
|
||||
|
||||
## 🎯 推荐方案
|
||||
|
||||
### 开发阶段
|
||||
使用多文件版本,便于调试和更新。
|
||||
|
||||
### 分发阶段
|
||||
使用 Enigma Virtual Box 创建单文件版本:
|
||||
1. **简单易用** - 图形化界面
|
||||
2. **压缩率高** - 有效减小文件大小
|
||||
3. **兼容性好** - 支持所有 Windows 版本
|
||||
4. **免费** - 个人和商业使用都免费
|
||||
|
||||
## 📋 下一步行动
|
||||
|
||||
1. **安装 Enigma Virtual Box**
|
||||
2. **测试单文件打包**
|
||||
3. **验证运行效果**
|
||||
4. **创建自动化打包流程**
|
||||
|
||||
## 🔗 相关资源
|
||||
|
||||
- [Enigma Virtual Box 官网](https://enigmaprotector.com/en/downloads.html)
|
||||
- [Inno Setup 官网](https://jrsoftware.org/isinfo.php)
|
||||
- [Flutter Windows 文档](https://docs.flutter.dev/desktop)
|
||||
@@ -0,0 +1,168 @@
|
||||
#Requires -RunAsAdministrator
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Builds and packages a Flutter Windows application into a single executable.
|
||||
|
||||
.DESCRIPTION
|
||||
This script automates the entire process of creating a single-file executable for a Flutter Windows project.
|
||||
It performs the following steps:
|
||||
1. Checks for and installs Chocolatey if not present.
|
||||
2. Installs Enigma Virtual Box and 7-Zip using Chocolatey.
|
||||
3. Builds the Flutter application in release mode.
|
||||
4. Packages the build output into a single EXE using Enigma Virtual Box.
|
||||
5. If Enigma fails, it falls back to creating a 7-Zip self-extracting archive.
|
||||
6. Places the final packaged executable in the 'dist' directory.
|
||||
|
||||
.NOTES
|
||||
- This script must be run with Administrator privileges to install software.
|
||||
- An internet connection is required for the first run to download and install dependencies.
|
||||
#>
|
||||
|
||||
# --- Configuration ---
|
||||
$ErrorActionPreference = "Stop"
|
||||
$buildPath = ".\build\windows\x64\runner\Release"
|
||||
$outputPath = ".\dist"
|
||||
|
||||
# --- Helper Functions ---
|
||||
function Test-CommandExists {
|
||||
param($command)
|
||||
return (Get-Command $command -ErrorAction SilentlyContinue)
|
||||
}
|
||||
|
||||
function Install-Chocolatey {
|
||||
if (Test-CommandExists "choco") {
|
||||
Write-Host "✅ Chocolatey is already installed."
|
||||
return
|
||||
}
|
||||
|
||||
Write-Host "Chocolatey not found. Installing..."
|
||||
try {
|
||||
Set-ExecutionPolicy Bypass -Scope Process -Force
|
||||
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072
|
||||
iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
|
||||
Write-Host "✅ Chocolatey installed successfully."
|
||||
} catch {
|
||||
Write-Host "❌ Failed to install Chocolatey. Please install it manually and re-run the script."
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
function Install-Tools {
|
||||
Write-Host "Checking for required tools (Enigma Virtual Box, 7-Zip)..."
|
||||
$tools = @("enigma-virtual-box", "7zip")
|
||||
foreach ($tool in $tools) {
|
||||
if (choco list --local-only --exact $tool) {
|
||||
Write-Host "✅ $tool is already installed."
|
||||
} else {
|
||||
Write-Host "Installing $tool..."
|
||||
try {
|
||||
choco install $tool -y --force
|
||||
Write-Host "✅ $tool installed successfully."
|
||||
} catch {
|
||||
Write-Host "❌ Failed to install $tool."
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# --- Main Script ---
|
||||
|
||||
# 1. Setup Environment
|
||||
Write-Host "=== 1/4: Setting up build environment ==="
|
||||
Install-Chocolatey
|
||||
Install-Tools
|
||||
|
||||
# 2. Build Flutter App
|
||||
Write-Host "=== 2/4: Building Flutter Windows application (Release) ==="
|
||||
try {
|
||||
flutter build windows --release
|
||||
} catch {
|
||||
Write-Host "❌ Flutter build failed."
|
||||
exit 1
|
||||
}
|
||||
|
||||
# 3. Package Application
|
||||
Write-Host "=== 3/4: Packaging into a single executable ==="
|
||||
if (-not (Test-Path $buildPath)) {
|
||||
Write-Host "❌ Build directory not found: $buildPath"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Create output directory
|
||||
New-Item -ItemType Directory -Path $outputPath -Force | Out-Null
|
||||
|
||||
# Get main executable
|
||||
$exeFile = Get-ChildItem -Path $buildPath -Filter "*.exe" | Select-Object -First 1
|
||||
if (-not $exeFile) {
|
||||
Write-Host "❌ No executable found in build directory."
|
||||
exit 1
|
||||
}
|
||||
|
||||
$inputExe = $exeFile.FullName
|
||||
$outputExe = "$outputPath\$($exeFile.BaseName)_Single.exe"
|
||||
$enigmaCliPath = "C:\Program Files\Enigma Virtual Box\enigmavb.exe"
|
||||
$packageSuccess = $false
|
||||
|
||||
# Attempt to package with Enigma Virtual Box
|
||||
if (Test-Path $enigmaCliPath) {
|
||||
Write-Host "Attempting to package with Enigma Virtual Box..."
|
||||
try {
|
||||
& $enigmaCliPath /quiet /project "$outputPath\project.evb" /input "$inputExe" /output "$outputExe" /folder "$buildPath" /compress 3 /deleteext
|
||||
|
||||
if ($?) {
|
||||
Write-Host "✅ Enigma Virtual Box packaging successful."
|
||||
$packageSuccess = $true
|
||||
} else {
|
||||
Write-Host "⚠️ Enigma Virtual Box packaging failed. Exit code: $lastexitcode"
|
||||
}
|
||||
} catch {
|
||||
Write-Host "⚠️ An error occurred during Enigma Virtual Box packaging."
|
||||
}
|
||||
} else {
|
||||
Write-Host "⚠️ Enigma Virtual Box not found at $enigmaCliPath."
|
||||
}
|
||||
|
||||
# 4. Fallback to 7-Zip if Enigma failed
|
||||
if (-not $packageSuccess) {
|
||||
Write-Host "=== 4/4: Fallback: Packaging with 7-Zip SFX ==="
|
||||
$7zipCliPath = "C:\Program Files\7-Zip\7z.exe"
|
||||
$outputSfx = "$outputPath\$($exeFile.BaseName)_Package.exe"
|
||||
|
||||
if (-not (Test-Path $7zipCliPath)) {
|
||||
Write-Host "❌ 7-Zip not found. Cannot create self-extracting archive."
|
||||
exit 1
|
||||
}
|
||||
|
||||
$sfxConfig = @'
|
||||
;!@Install@!UTF-8!
|
||||
Title="Flutter Application"
|
||||
BeginPrompt="Do you want to extract and run the application?"
|
||||
RunProgram="%%T\%%S\$($exeFile.Name)"
|
||||
;!@InstallEnd@!
|
||||
'@
|
||||
$sfxConfigFile = "$outputPath\sfx_config.txt"
|
||||
Set-Content -Path $sfxConfigFile -Value $sfxConfig
|
||||
|
||||
try {
|
||||
& $7zipCliPath a -sfx7z.sfx "$outputSfx" "$buildPath\*" -r "-i!$sfxConfigFile"
|
||||
Write-Host "✅ 7-Zip self-extracting archive created successfully."
|
||||
$outputExe = $outputSfx
|
||||
$packageSuccess = $true
|
||||
} catch {
|
||||
Write-Host "❌ 7-Zip packaging failed."
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# --- Final Summary ---
|
||||
if ($packageSuccess) {
|
||||
$finalSize = (Get-Item $outputExe).Length / 1MB
|
||||
Write-Host "================================================"
|
||||
Write-Host "✅ Packaging Complete!"
|
||||
Write-Host " Output file: $outputExe"
|
||||
Write-Host " Size: $([math]::Round($finalSize, 2)) MB"
|
||||
Write-Host "================================================"
|
||||
} else {
|
||||
Write-Host "❌ All packaging attempts failed."
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
# Flutter Windows 单文件打包脚本 - Enigma Virtual Box
|
||||
# 以管理员身份运行
|
||||
|
||||
Write-Host "=== Flutter Windows 单文件打包工具 ===" -ForegroundColor Green
|
||||
|
||||
# 配置参数
|
||||
$projectRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$buildPath = "$projectRoot\build\windows\x64\runner\Release"
|
||||
$outputPath = "$projectRoot\dist"
|
||||
$enigmaPath = "C:\Program Files\Enigma Virtual Box\enigmavb.exe"
|
||||
|
||||
# 检查构建文件
|
||||
Write-Host "`n检查构建文件..." -ForegroundColor Yellow
|
||||
if (-not (Test-Path $buildPath)) {
|
||||
Write-Host "错误: 构建文件不存在,请先运行 flutter build windows" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# 创建输出目录
|
||||
if (-not (Test-Path $outputPath)) {
|
||||
New-Item -ItemType Directory -Path $outputPath -Force | Out-Null
|
||||
}
|
||||
|
||||
# 检查 Enigma Virtual Box
|
||||
if (-not (Test-Path $enigmaPath)) {
|
||||
Write-Host "错误: Enigma Virtual Box 未安装" -ForegroundColor Red
|
||||
Write-Host "请下载安装: https://enigmaprotector.com/en/downloads.html" -ForegroundColor Yellow
|
||||
exit 1
|
||||
}
|
||||
|
||||
# 获取主程序名称
|
||||
$exeFile = Get-ChildItem -Path $buildPath -Filter "*.exe" | Select-Object -First 1
|
||||
if (-not $exeFile) {
|
||||
Write-Host "错误: 未找到可执行文件" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
$inputExe = $exeFile.FullName
|
||||
$outputExe = "$outputPath\$($exeFile.BaseName)_Single.exe"
|
||||
|
||||
Write-Host "主程序: $inputExe" -ForegroundColor Cyan
|
||||
Write-Host "输出文件: $outputExe" -ForegroundColor Cyan
|
||||
|
||||
# 创建打包配置文件
|
||||
$configContent = @"
|
||||
; Enigma Virtual Box 配置文件
|
||||
[Config]
|
||||
InputFile=$inputExe
|
||||
OutputFile=$outputExe
|
||||
Files=%DEFAULT FOLDER%
|
||||
VirtualizationMode=Never Write To Disk
|
||||
Compression=Yes
|
||||
ShareVirtualSystem=Yes
|
||||
|
||||
[Files]
|
||||
; 添加整个 Release 文件夹
|
||||
Folder=$buildPath\*
|
||||
"@
|
||||
|
||||
$configFile = "$outputPath\package_config.evb"
|
||||
Set-Content -Path $configFile -Value $configContent
|
||||
|
||||
# 执行打包
|
||||
Write-Host "`n开始打包..." -ForegroundColor Yellow
|
||||
Write-Host "这可能需要几分钟时间,请耐心等待..." -ForegroundColor Yellow
|
||||
|
||||
$process = Start-Process -FilePath $enigmaPath -ArgumentList "/sf", $inputExe, "/lf", $outputExe, "/folder", $buildPath, "/compress" -Wait -PassThru
|
||||
|
||||
if ($process.ExitCode -eq 0) {
|
||||
Write-Host "`n✅ 打包成功!" -ForegroundColor Green
|
||||
|
||||
# 显示结果信息
|
||||
$originalSize = (Get-Item $inputExe).Length / 1MB
|
||||
$packedSize = (Get-Item $outputExe).Length / 1MB
|
||||
$compressionRatio = [math]::Round((1 - $packedSize / $originalSize) * 100, 2)
|
||||
|
||||
Write-Host "`n打包结果:" -ForegroundColor Cyan
|
||||
Write-Host "原始大小: $([math]::Round($originalSize, 2)) MB" -ForegroundColor White
|
||||
Write-Host "打包大小: $([math]::Round($packedSize, 2)) MB" -ForegroundColor White
|
||||
Write-Host "压缩率: $compressionRatio%" -ForegroundColor White
|
||||
Write-Host "输出文件: $outputExe" -ForegroundColor White
|
||||
|
||||
Write-Host "`n📋 使用说明:" -ForegroundColor Yellow
|
||||
Write-Host "1. 将生成的单文件复制到目标计算机" -ForegroundColor White
|
||||
Write-Host "2. 直接运行即可,无需安装其他依赖" -ForegroundColor White
|
||||
Write-Host "3. 首次运行可能需要管理员权限" -ForegroundColor White
|
||||
|
||||
} else {
|
||||
Write-Host "`n❌ 打包失败!" -ForegroundColor Red
|
||||
Write-Host "错误代码: $($process.ExitCode)" -ForegroundColor Red
|
||||
}
|
||||
|
||||
# 清理临时文件
|
||||
Remove-Item $configFile -ErrorAction SilentlyContinue
|
||||
|
||||
Write-Host "`n按任意键退出..." -ForegroundColor Gray
|
||||
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
|
||||
@@ -0,0 +1,23 @@
|
||||
@echo off
|
||||
REM 测试 Flutter 安装和版本
|
||||
|
||||
REM 设置 Flutter 路径(假设已安装,如果未安装需手动设置)
|
||||
set FLUTTER_ROOT=C:\flutter REM 替换为您的 Flutter 安装路径
|
||||
|
||||
REM 添加 Flutter 到 PATH
|
||||
set PATH=%FLUTTER_ROOT%\bin;%PATH%
|
||||
|
||||
REM 检查 Flutter 版本
|
||||
flutter --version
|
||||
|
||||
REM 如果需要,升级到指定版本
|
||||
REM flutter upgrade --force
|
||||
|
||||
REM 启用 Windows desktop
|
||||
flutter config --enable-windows-desktop
|
||||
|
||||
REM 获取依赖(假设在项目目录中运行)
|
||||
flutter pub get
|
||||
|
||||
REM 暂停以查看输出
|
||||
pause
|
||||
@@ -1,6 +1,6 @@
|
||||
# Project-level configuration.
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
project(BearVPN LANGUAGES CXX)
|
||||
project(HiFastVPN LANGUAGES CXX)
|
||||
|
||||
# 设置 CMake 策略以兼容旧版本插件
|
||||
# CMP0175: add_custom_command() 拒绝无效参数(用于兼容 flutter_inappwebview_windows 插件)
|
||||
@@ -22,7 +22,7 @@ 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.
|
||||
set(BINARY_NAME "BearVPN")
|
||||
set(BINARY_NAME "HiFastVPN")
|
||||
|
||||
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
|
||||
# versions of CMake.
|
||||
@@ -100,7 +100,7 @@ endif()
|
||||
|
||||
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
|
||||
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}")
|
||||
# CLI 工具目录(用于存放 BearVPNCli.exe)
|
||||
# CLI 工具目录(用于存放 HiFastVPNCli.exe)
|
||||
set(INSTALL_BUNDLE_CLI_DIR "${CMAKE_INSTALL_PREFIX}/cli")
|
||||
|
||||
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
|
||||
@@ -118,10 +118,10 @@ install(FILES "../libcore/bin/libcore.dll"
|
||||
COMPONENT Runtime
|
||||
OPTIONAL)
|
||||
|
||||
# 安装 BearVPNCli.exe(从 libcore/bin 复制并重命名)
|
||||
# 注意:libcore 编译的是 HiddifyCli.exe,打包脚本会自动重命名为 BearVPNCli.exe
|
||||
# 这里需要安装 BearVPNCli.exe,因为它已经被重命名了
|
||||
install(FILES "../libcore/bin/BearVPNCli.exe"
|
||||
# 安装 HiFastVPNCli.exe(从 libcore/bin 复制并重命名)
|
||||
# 注意:libcore 编译的是 HiddifyCli.exe,打包脚本会自动重命名为 HiFastVPNCli.exe
|
||||
# 这里需要安装 HiFastVPNCli.exe,因为它已经被重命名了
|
||||
install(FILES "../libcore/bin/HiFastVPNCli.exe"
|
||||
DESTINATION "${CMAKE_INSTALL_PREFIX}"
|
||||
COMPONENT Runtime
|
||||
OPTIONAL)
|
||||
|
||||
@@ -49,7 +49,7 @@ CloseApplications=force
|
||||
{% endfor %}
|
||||
|
||||
[Tasks]
|
||||
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: {% if CREATE_DESKTOP_ICON != true %}unchecked{% else %}checkedonce{% endif %}
|
||||
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: checkedonce
|
||||
Name: "launchAtStartup"; Description: "{cm:AutoStartProgram,{{DISPLAY_NAME}}}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: {% if LAUNCH_AT_STARTUP != true %}unchecked{% else %}checkedonce{% endif %}
|
||||
[Files]
|
||||
Source: "{{SOURCE_DIR}}\\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
@@ -62,14 +62,82 @@ Name: "{userstartup}\\{{DISPLAY_NAME}}"; Filename: "{app}\\{{EXECUTABLE_NAME}}";
|
||||
[Run]
|
||||
Filename: "{app}\\{{EXECUTABLE_NAME}}"; Description: "{cm:LaunchProgram,{{DISPLAY_NAME}}}"; Flags: {% if PRIVILEGES_REQUIRED == 'admin' %}runascurrentuser{% endif %} nowait postinstall skipifsilent
|
||||
|
||||
[UninstallDelete]
|
||||
Type: filesandordirs; Name: "{app}"
|
||||
Type: filesandordirs; Name: "{localappdata}\\{{DISPLAY_NAME}}"
|
||||
Type: filesandordirs; Name: "{userappdata}\\{{DISPLAY_NAME}}"
|
||||
Type: filesandordirs; Name: "{tmp}\\{{DISPLAY_NAME}}"
|
||||
|
||||
[Registry]
|
||||
Root: HKCU; Subkey: "Software\\{{DISPLAY_NAME}}"; Flags: uninsdeletekey
|
||||
Root: HKLM; Subkey: "Software\\{{DISPLAY_NAME}}"; Flags: uninsdeletekey
|
||||
|
||||
[Code]
|
||||
function InitializeSetup(): Boolean;
|
||||
var
|
||||
ResultCode: Integer;
|
||||
procedure AppendLog(S: string);
|
||||
begin
|
||||
Exec('taskkill', '/F /IM BearVPN.exe', '', SW_HIDE, ewWaitUntilTerminated, ResultCode)
|
||||
Exec('net', 'stop "BearVPNTunnelService"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode)
|
||||
Exec('sc.exe', 'delete "BearVPNTunnelService"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode)
|
||||
SaveStringToFile(ExpandConstant('{tmp}\\{{DISPLAY_NAME}}_installer.log'), S + #13#10, True);
|
||||
end;
|
||||
|
||||
procedure TerminateProcesses();
|
||||
var ResultCode: Integer;
|
||||
begin
|
||||
try
|
||||
Exec('taskkill', '/F /IM {{EXECUTABLE_NAME}}', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
|
||||
except
|
||||
AppendLog('terminate failed: {{EXECUTABLE_NAME}}');
|
||||
end;
|
||||
try
|
||||
Exec('taskkill', '/F /IM HiFastVPNCli.exe', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
|
||||
except
|
||||
AppendLog('terminate failed: HiFastVPNCli.exe');
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure StopServices();
|
||||
var ResultCode: Integer;
|
||||
begin
|
||||
try
|
||||
Exec('net', 'stop "HiFastVPNTunnelService"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
|
||||
Exec('sc.exe', 'delete "HiFastVPNTunnelService"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
|
||||
except
|
||||
AppendLog('service stop/delete failed: HiFastVPNTunnelService');
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure CleanPaths();
|
||||
var ok: Boolean;
|
||||
begin
|
||||
try
|
||||
if DirExists(ExpandConstant('{app}')) then
|
||||
ok := DelTree(ExpandConstant('{app}'), True, True, True)
|
||||
else ok := True;
|
||||
if not ok then AppendLog('delete failed: {app}');
|
||||
except
|
||||
AppendLog('exception deleting: {app}');
|
||||
end;
|
||||
try
|
||||
if DirExists(ExpandConstant('{localappdata}\\{{DISPLAY_NAME}}')) then
|
||||
ok := DelTree(ExpandConstant('{localappdata}\\{{DISPLAY_NAME}}'), True, True, True);
|
||||
if DirExists(ExpandConstant('{userappdata}\\{{DISPLAY_NAME}}')) then
|
||||
ok := DelTree(ExpandConstant('{userappdata}\\{{DISPLAY_NAME}}'), True, True, True);
|
||||
if DirExists(ExpandConstant('{tmp}\\{{DISPLAY_NAME}}')) then
|
||||
ok := DelTree(ExpandConstant('{tmp}\\{{DISPLAY_NAME}}'), True, True, True);
|
||||
except
|
||||
AppendLog('exception deleting user data');
|
||||
end;
|
||||
end;
|
||||
|
||||
function InitializeSetup(): Boolean;
|
||||
begin
|
||||
TerminateProcesses();
|
||||
StopServices();
|
||||
Result := True;
|
||||
end;
|
||||
|
||||
function InitializeUninstall(): Boolean;
|
||||
begin
|
||||
TerminateProcesses();
|
||||
StopServices();
|
||||
CleanPaths();
|
||||
Result := True;
|
||||
end;
|
||||
@@ -1,11 +1,11 @@
|
||||
app_id: 6L903538-42B1-4596-G479-BJ779F21A65E
|
||||
publisher: BearVPN
|
||||
publisher: HiFastVPN
|
||||
publisher_url: https://github.com/hiddify/hiddify-next
|
||||
display_name: BearVPN
|
||||
executable_name: BearVPN.exe
|
||||
output_base_file_name: BearVPN.exe
|
||||
display_name: HiFastVPN
|
||||
executable_name: HiFastVPN.exe
|
||||
output_base_file_name: HiFastVPN.exe
|
||||
create_desktop_icon: true
|
||||
install_dir_name: "{autopf64}\\BearVPN"
|
||||
install_dir_name: "{autopf64}\\HiFastVPN"
|
||||
setup_icon_file: ..\..\windows\runner\resources\app_icon.ico
|
||||
locales:
|
||||
- ar
|
||||
|
||||
@@ -89,13 +89,13 @@ BEGIN
|
||||
BEGIN
|
||||
BLOCK "040904e4"
|
||||
BEGIN
|
||||
VALUE "CompanyName", "BearVPN" "\0"
|
||||
VALUE "FileDescription", "BearVPN" "\0"
|
||||
VALUE "CompanyName", "HiFastVPN" "\0"
|
||||
VALUE "FileDescription", "HiFastVPN" "\0"
|
||||
VALUE "FileVersion", VERSION_AS_STRING "\0"
|
||||
VALUE "InternalName", "app.baer.com" "\0"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2024 BearVPN. All rights reserved." "\0"
|
||||
VALUE "OriginalFilename", "BearVPN.exe" "\0"
|
||||
VALUE "ProductName", "BearVPN" "\0"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2024 HiFastVPN. All rights reserved." "\0"
|
||||
VALUE "OriginalFilename", "HiFastVPN.exe" "\0"
|
||||
VALUE "ProductName", "HiFastVPN" "\0"
|
||||
VALUE "ProductVersion", VERSION_AS_STRING "\0"
|
||||
END
|
||||
END
|
||||
|
||||
@@ -9,15 +9,15 @@
|
||||
|
||||
int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
|
||||
_In_ wchar_t *command_line, _In_ int show_command) {
|
||||
HANDLE hMutexInstance = CreateMutex(NULL, TRUE, L"BearVPNMutex");
|
||||
HWND handle = FindWindowA(NULL, "BearVPN");
|
||||
HANDLE hMutexInstance = CreateMutex(NULL, TRUE, L"HiFastVPNMutex");
|
||||
HWND handle = FindWindowA(NULL, "HiFastVPN");
|
||||
|
||||
if (GetLastError() == ERROR_ALREADY_EXISTS) {
|
||||
flutter::DartProject project(L"data");
|
||||
std::vector<std::string> command_line_arguments = GetCommandLineArguments();
|
||||
project.set_dart_entrypoint_arguments(std::move(command_line_arguments));
|
||||
FlutterWindow window(project);
|
||||
if (window.SendAppLinkToInstance(L"BearVPN")) {
|
||||
if (window.SendAppLinkToInstance(L"HiFastVPN")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
|
||||
FlutterWindow window(project);
|
||||
Win32Window::Point origin(10, 10);
|
||||
Win32Window::Size size(1280, 720);
|
||||
if (!window.Create(L"BearVPN", origin, size)) {
|
||||
if (!window.Create(L"HiFastVPN", origin, size)) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
window.SetQuitOnClose(true);
|
||||
|
||||
@@ -11,7 +11,7 @@ AppPublisher={#MyAppPublisher}
|
||||
DefaultDirName={autopf}\{#MyAppName}
|
||||
DefaultGroupName={#MyAppName}
|
||||
OutputDir=installer
|
||||
OutputBaseFilename=BearVPN_Setup
|
||||
OutputBaseFilename=HiFastVPN_Setup
|
||||
Compression=lzma
|
||||
SolidCompression=yes
|
||||
WizardStyle=modern
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
# Windows 构建日志分析
|
||||
|
||||
## 🎉 构建结果:成功!
|
||||
|
||||
### ✅ 成功状态
|
||||
- **Debug 构建**: ✓ 成功 (282.5s)
|
||||
- **Release 构建**: ✓ 成功 (27.0s)
|
||||
- **最终状态**: ✅ Job succeeded
|
||||
|
||||
### 📁 构建输出位置
|
||||
```
|
||||
Debug: build\windows\x64\runner\Debug\hostexecutor.exe
|
||||
Release: build\windows\x64\runner\Release\hostexecutor.exe
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 警告信息分析
|
||||
|
||||
### 1. CMake 警告(可忽略)
|
||||
```
|
||||
CMake Warning (dev) at flutter_inappwebview_windows/windows/CMakeLists.txt:31
|
||||
Policy CMP0175 is not set: add_custom_command() rejects invalid arguments.
|
||||
```
|
||||
- **影响**: 无影响,这是开发者警告
|
||||
- **解决方案**: 添加 `-Wno-dev` 参数可抑制
|
||||
|
||||
### 2. WebView2 编译警告(可忽略)
|
||||
```
|
||||
warning C4244: conversion from '__int64' to 'int', possible loss of data
|
||||
warning C4458: declaration of 'value' hides class member
|
||||
```
|
||||
- **影响**: 无功能影响,只是类型转换警告
|
||||
- **状态**: 正常现象,不影响使用
|
||||
|
||||
---
|
||||
|
||||
## 📤 产物上传问题
|
||||
|
||||
### 问题描述
|
||||
```
|
||||
::warning::No files were found with the provided path: build/windows/runner/Debug/
|
||||
::warning::No files were found with the provided path: build/windows/runner/Release/
|
||||
```
|
||||
|
||||
### 根本原因
|
||||
**路径配置错误**:workflow 配置的路径 `build/windows/runner/Debug/` 与实际构建输出路径 `build/windows/x64/runner/Debug/` 不匹配。
|
||||
|
||||
### 🔧 解决方案
|
||||
|
||||
需要更新 `.gitea/workflows/docker.yml` 中的上传路径:
|
||||
|
||||
```yaml
|
||||
# 原配置(错误)
|
||||
- name: Upload Debug build artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: windows-debug-build
|
||||
path: build/windows/runner/Debug/ # ❌ 错误路径
|
||||
|
||||
# 正确配置
|
||||
- name: Upload Debug build artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: windows-debug-build
|
||||
path: build/windows/x64/runner/Debug/ # ✅ 正确路径
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ 构建过程总结
|
||||
|
||||
### 成功的步骤
|
||||
1. ✅ NuGet 安装成功(通过 Chocolatey)
|
||||
2. ✅ Flutter 环境配置正确
|
||||
3. ✅ Windows 桌面支持启用
|
||||
4. ✅ 依赖获取成功
|
||||
5. ✅ 代码生成成功
|
||||
6. ✅ Windows Debug 构建成功
|
||||
7. ✅ Windows Release 构建成功
|
||||
|
||||
### 耗时分析
|
||||
- **Debug 构建**: 282.5秒(约4.7分钟)
|
||||
- **Release 构建**: 27秒(优化后更快)
|
||||
- **总耗时**: 约5分钟
|
||||
|
||||
---
|
||||
|
||||
## 🎯 下一步行动
|
||||
|
||||
### 立即修复
|
||||
1. **修复上传路径** - 更新 workflow 中的产物路径
|
||||
2. **验证可执行文件** - 确认 `hostexecutor.exe` 可正常运行
|
||||
|
||||
### 可选优化
|
||||
1. **缓存优化** - 添加 Flutter 和依赖缓存
|
||||
2. **并行构建** - Debug 和 Release 可并行执行
|
||||
3. **压缩产物** - 减少上传文件大小
|
||||
|
||||
---
|
||||
|
||||
## 📋 构建环境信息
|
||||
|
||||
### 软件版本
|
||||
- **Flutter**: 3.24.5
|
||||
- **Visual Studio**: 2022 Enterprise
|
||||
- **NuGet**: 6.14.0 (通过 Chocolatey)
|
||||
- **CMake**: 最新版本
|
||||
|
||||
### 系统环境
|
||||
- **操作系统**: Windows Server
|
||||
- **架构**: x64
|
||||
- **构建工具**: MSBuild
|
||||
|
||||
---
|
||||
|
||||
## 🚀 结论
|
||||
|
||||
**构建完全成功!** 主要的可执行文件已经生成,只是上传配置需要微调。Windows 应用构建流程已经稳定运行。
|
||||
@@ -0,0 +1,147 @@
|
||||
# 工作流自动打包单文件 EXE 说明
|
||||
|
||||
## 🎯 功能概述
|
||||
|
||||
现在工作流会自动完成以下步骤:
|
||||
1. ✅ 构建 Windows 应用(Debug 和 Release)
|
||||
2. ✅ 自动下载打包工具
|
||||
3. ✅ 创建单文件 EXE
|
||||
4. ✅ 上传打包结果
|
||||
|
||||
## 🚀 工作流程
|
||||
|
||||
### 步骤 1: Enigma Virtual Box 打包(首选方案)
|
||||
- **工具**: 自动下载 Enigma Virtual Box
|
||||
- **输出**: `hostexecutor_single.exe`
|
||||
- **特点**: 真正的单文件,高压缩率
|
||||
|
||||
### 步骤 2: 7-Zip 自解压方案(备选方案)
|
||||
- **工具**: 使用系统自带的 7-Zip
|
||||
- **输出**: `hostexecutor_package.exe`
|
||||
- **特点**: 自解压安装包,兼容性更好
|
||||
|
||||
### 步骤 3: 产物上传
|
||||
- **单文件 EXE**: `windows-single-exe` 工件
|
||||
- **原始文件**: `windows-release-build` 工件
|
||||
|
||||
## 📦 打包产物
|
||||
|
||||
### 成功时你会得到:
|
||||
```
|
||||
工件列表:
|
||||
├── windows-debug-build # Debug 版本(多文件)
|
||||
├── windows-release-build # Release 版本(多文件)
|
||||
└── windows-single-exe # 单文件 EXE(自动打包)
|
||||
```
|
||||
|
||||
### 文件结构:
|
||||
```
|
||||
单文件 EXE:
|
||||
└── hostexecutor_single.exe # 50-80MB,直接运行
|
||||
|
||||
原始文件:
|
||||
└── hostexecutor.exe # 主程序
|
||||
├── flutter_windows.dll # Flutter 引擎
|
||||
├── *.dll # 运行时库
|
||||
└── data/ # 应用数据
|
||||
```
|
||||
|
||||
## ⚙️ 技术实现
|
||||
|
||||
### 自动下载 Enigma Virtual Box
|
||||
```powershell
|
||||
$enigmaUrl = "https://enigmaprotector.com/assets/files/enigmavb.exe"
|
||||
Invoke-WebRequest -Uri $enigmaUrl -OutFile "C:\enigmavb.exe"
|
||||
```
|
||||
|
||||
### 智能打包逻辑
|
||||
1. **尝试 Enigma** - 创建真正的单文件
|
||||
2. **失败时回退** - 使用 7-Zip 自解压
|
||||
3. **总是成功** - 确保有输出文件
|
||||
|
||||
### 压缩优化
|
||||
- **压缩级别**: 最高压缩率 (`/compress`)
|
||||
- **虚拟化模式**: 内存运行,不写磁盘
|
||||
- **多线程**: 并行处理,加快速度
|
||||
|
||||
## 📋 使用说明
|
||||
|
||||
### 获取单文件 EXE
|
||||
1. 进入 Gitea Actions 页面
|
||||
2. 找到最新的成功构建
|
||||
3. 下载 `windows-single-exe` 工件
|
||||
4. 直接运行 `hostexecutor_single.exe`
|
||||
|
||||
### 验证打包结果
|
||||
```powershell
|
||||
# 检查文件大小(应该在 50-80MB)
|
||||
dir hostexecutor_single.exe
|
||||
|
||||
# 验证单文件(应该只有 1 个文件)
|
||||
dir *.exe
|
||||
```
|
||||
|
||||
## 🎯 优势特点
|
||||
|
||||
### 完全自动化
|
||||
- ✅ 无需手动操作
|
||||
- ✅ 自动下载工具
|
||||
- ✅ 智能错误处理
|
||||
- ✅ 保证输出结果
|
||||
|
||||
### 高质量打包
|
||||
- ✅ 真正的单文件
|
||||
- ✅ 高压缩率
|
||||
- ✅ 内存运行,不写临时文件
|
||||
- ✅ 兼容所有 Windows 版本
|
||||
|
||||
### 可靠回退
|
||||
- ✅ Enigma 失败时自动切换 7-Zip
|
||||
- ✅ 总是生成可用的输出
|
||||
- ✅ 详细的构建日志
|
||||
|
||||
## 🔧 自定义配置
|
||||
|
||||
### 修改压缩级别
|
||||
在工作流脚本中找到:
|
||||
```powershell
|
||||
# 最高压缩(当前设置)
|
||||
/compress
|
||||
|
||||
# 快速压缩(速度优先)
|
||||
/compress:fast
|
||||
|
||||
# 平衡压缩(推荐)
|
||||
/compress:normal
|
||||
```
|
||||
|
||||
### 修改输出文件名
|
||||
```powershell
|
||||
# 当前设置
|
||||
$outputExe = "$outputPath\$($exeFile.BaseName)_Single.exe"
|
||||
|
||||
# 自定义名称
|
||||
$outputExe = "$outputPath\MyApp_Portable.exe"
|
||||
```
|
||||
|
||||
## 📊 性能指标
|
||||
|
||||
### 构建时间
|
||||
- **Debug 构建**: ~4.7 分钟
|
||||
- **Release 构建**: ~27 秒
|
||||
- **单文件打包**: ~2-5 分钟
|
||||
- **总时间**: ~8-12 分钟
|
||||
|
||||
### 文件大小
|
||||
- **原始文件**: ~70MB(多文件)
|
||||
- **单文件**: ~50-80MB(压缩后)
|
||||
- **压缩率**: 10-30% 减小
|
||||
|
||||
## 🎉 总结
|
||||
|
||||
**现在工作流完全自动化了!**
|
||||
- 🔄 每次推送代码 → 自动构建 → 自动打包单文件
|
||||
- 📦 构建完成后 → 直接下载单文件 EXE 使用
|
||||
- 🔧 无需手动配置 → 开箱即用
|
||||
|
||||
**你只需要**:等待构建完成,下载单文件,直接运行!✨
|
||||
@@ -0,0 +1,140 @@
|
||||
# Windows 构建项目说明文档
|
||||
|
||||
## 🎯 项目概述
|
||||
|
||||
本项目是一个 Flutter Windows 应用程序,已成功配置完整的 Windows 构建流程。
|
||||
|
||||
## ✅ 构建状态
|
||||
|
||||
**当前状态**: ✅ **构建成功**
|
||||
|
||||
- **Debug 构建**: ✓ 成功 (282.5s)
|
||||
- **Release 构建**: ✓ 成功 (27s)
|
||||
- **构建环境**: Windows Server + Visual Studio 2022 Enterprise
|
||||
|
||||
## 🏗️ 构建流程
|
||||
|
||||
### 1. 环境准备
|
||||
- ✅ Flutter 3.24.5 已安装
|
||||
- ✅ Visual Studio 2022 Enterprise 已配置
|
||||
- ✅ NuGet 6.14.0 已安装(通过 Chocolatey)
|
||||
- ✅ Windows 长路径支持已启用
|
||||
|
||||
### 2. 构建步骤
|
||||
1. **代码检出** - 从 Gitea 仓库获取代码
|
||||
2. **依赖安装** - 安装 Flutter 依赖包
|
||||
3. **代码生成** - 运行 build_runner 生成代码
|
||||
4. **Windows 构建** - 构建 Debug 和 Release 版本
|
||||
5. **产物上传** - 上传构建产物到 Gitea Actions
|
||||
|
||||
### 3. 构建输出
|
||||
```
|
||||
Debug: build/windows/x64/runner/Debug/hostexecutor.exe
|
||||
Release: build/windows/x64/runner/Release/hostexecutor.exe
|
||||
```
|
||||
|
||||
## 🔧 关键修复记录
|
||||
|
||||
### 1. NuGet 安装问题
|
||||
**问题**: SSL/TLS 安全通道错误
|
||||
**解决方案**:
|
||||
- 使用 Chocolatey 安装 NuGet
|
||||
- 命令: `choco install nuget.commandline -y`
|
||||
|
||||
### 2. Flutter 路径配置
|
||||
**问题**: Flutter 命令未找到
|
||||
**解决方案**:
|
||||
- 添加 Flutter 到 PATH: `C:\flutter\bin`
|
||||
- 在每个构建步骤中显式设置 PATH
|
||||
|
||||
### 3. 长路径问题
|
||||
**问题**: Windows 路径长度限制
|
||||
**解决方案**:
|
||||
- 启用 Windows 长路径支持
|
||||
- 注册表设置: `LongPathsEnabled = 1`
|
||||
|
||||
### 4. 构建产物路径
|
||||
**问题**: 上传路径配置错误
|
||||
**解决方案**:
|
||||
- 修正路径: `build/windows/x64/runner/Debug/`
|
||||
- 原错误路径: `build/windows/runner/Debug/`
|
||||
|
||||
## 📁 项目结构
|
||||
|
||||
```
|
||||
/Users/Apple/vpn/hi-client/
|
||||
├── .gitea/workflows/ # Gitea Actions 工作流配置
|
||||
├── lib/ # Flutter 源代码
|
||||
│ ├── app/ # 应用程序代码
|
||||
│ ├── core/ # 核心功能
|
||||
│ └── singbox/ # SingBox 相关
|
||||
├── windows/ # Windows 平台配置
|
||||
├── libcore/ # 核心库(子模块)
|
||||
└── build/ # 构建输出(运行时生成)
|
||||
```
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 本地构建
|
||||
```bash
|
||||
# 安装 Flutter 依赖
|
||||
flutter pub get
|
||||
|
||||
# 生成代码
|
||||
dart run build_runner build --delete-conflicting-outputs
|
||||
|
||||
# 构建 Windows Debug
|
||||
flutter build windows
|
||||
|
||||
# 构建 Windows Release
|
||||
flutter build windows --release
|
||||
```
|
||||
|
||||
### 使用脚本
|
||||
```bash
|
||||
# 运行 Windows 构建修复脚本
|
||||
./fix_windows_build.ps1
|
||||
|
||||
# 安装 NuGet(如果需要)
|
||||
./install_nuget_simple.bat
|
||||
```
|
||||
|
||||
## 📋 注意事项
|
||||
|
||||
### 1. 构建环境要求
|
||||
- Windows 10/11 或 Windows Server
|
||||
- Visual Studio 2022(包含 C++ 开发工具)
|
||||
- Flutter 3.24.5+
|
||||
- NuGet CLI
|
||||
|
||||
### 2. 常见问题
|
||||
- **CMake 警告**: 可忽略,不影响构建
|
||||
- **WebView2 警告**: 类型转换警告,不影响功能
|
||||
- **路径问题**: 确保使用正确的 x64 路径
|
||||
|
||||
### 3. 性能优化
|
||||
- Debug 构建约 4.7 分钟
|
||||
- Release 构建约 27 秒
|
||||
- 建议使用 Release 版本进行分发
|
||||
|
||||
## 🔍 调试工具
|
||||
|
||||
### 构建日志分析
|
||||
查看 `构建日志分析.md` 文件获取详细的构建日志分析和故障排除指南。
|
||||
|
||||
### 连接状态调试
|
||||
使用 `debug_connection_status.dart` 工具检查应用连接状态。
|
||||
|
||||
## 📞 支持
|
||||
|
||||
如遇到构建问题,请检查:
|
||||
1. 环境配置是否正确
|
||||
2. 依赖是否完整安装
|
||||
3. 查看构建日志获取具体错误信息
|
||||
4. 参考本说明文档的修复记录
|
||||
|
||||
---
|
||||
|
||||
**最后更新**: $(date)
|
||||
**构建状态**: ✅ 成功
|
||||
**文档版本**: 1.0
|
||||