feat: add local launcher installer

This commit is contained in:
2026-06-18 10:04:29 +08:00
parent 757056b462
commit ba8daaccdd
2 changed files with 167 additions and 1 deletions
+166
View File
@@ -0,0 +1,166 @@
<#
.SYNOPSIS
Install a built CMBot release into the local launcher layout.
.DESCRIPTION
Creates or updates this structure:
%LOCALAPPDATA%\CMBot\
update.ps1
app\
app.old\
data\
config\
logs\
output\
staging\
The script copies a built release directory (release\CMBot-x.y.z by default)
into app\, copies scripts\update.ps1 to the install root, and migrates
config files into data\config without overwriting existing user config.
.PARAMETER ReleaseDir
Built release directory to install. Defaults to release\CMBot-APP_VERSION.
.PARAMETER InstallRoot
Local install root. Defaults to %LOCALAPPDATA%\CMBot.
.PARAMETER LegacyDataDir
Optional old flat install/data root. When provided, config files are copied
from LegacyDataDir\config before falling back to app.old\config or release
defaults.
.PARAMETER Force
Allows replacing an existing app.old directory.
#>
[CmdletBinding()]
param(
[string]$ReleaseDir = "",
[string]$InstallRoot = (Join-Path $env:LOCALAPPDATA "CMBot"),
[string]$LegacyDataDir = "",
[switch]$Force
)
$ErrorActionPreference = "Stop"
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$RootDir = Split-Path -Parent $ScriptDir
$VersionFile = Join-Path $RootDir "src\version.py"
$ReleaseRoot = Join-Path $RootDir "release"
$LauncherScript = Join-Path $ScriptDir "update.ps1"
$AppExeName = "CMBot"
$AppExe = "$AppExeName.exe"
function Read-VersionValue {
param(
[string]$Path,
[string]$Name
)
$pattern = "^\s*$Name\s*=\s*`"([^`"]+)`""
foreach ($line in Get-Content -LiteralPath $Path -Encoding UTF8) {
if ($line -match $pattern) {
return $Matches[1]
}
}
throw "Cannot find $Name in $Path"
}
function Resolve-ExistingPath {
param(
[string]$Path,
[string]$Name
)
if (-not (Test-Path -LiteralPath $Path)) {
throw "$Name not found: $Path"
}
return (Resolve-Path -LiteralPath $Path).Path
}
function Assert-Release {
param([string]$Path)
$exe = Join-Path $Path $AppExe
$version = Join-Path $Path "version.txt"
if (-not (Test-Path -LiteralPath $exe)) {
throw "Release missing executable: $exe"
}
if (-not (Test-Path -LiteralPath $version)) {
throw "Release missing version.txt: $version"
}
}
function Copy-ConfigIfMissing {
param(
[string]$SourceConfigDir,
[string]$TargetConfigDir,
[string]$FileName
)
if (-not $SourceConfigDir) {
return
}
$source = Join-Path $SourceConfigDir $FileName
$target = Join-Path $TargetConfigDir $FileName
if ((Test-Path -LiteralPath $source) -and -not (Test-Path -LiteralPath $target)) {
Copy-Item -LiteralPath $source -Destination $target
Write-Host "Migrated config: $FileName"
}
}
if (-not $ReleaseDir) {
$appVersion = Read-VersionValue -Path $VersionFile -Name "APP_VERSION"
$ReleaseDir = Join-Path $ReleaseRoot "$AppExeName-$appVersion"
}
$ReleaseDir = Resolve-ExistingPath -Path $ReleaseDir -Name "Release directory"
$LauncherScript = Resolve-ExistingPath -Path $LauncherScript -Name "Launcher script"
Assert-Release -Path $ReleaseDir
$InstallRoot = [System.IO.Path]::GetFullPath($InstallRoot)
$AppDir = Join-Path $InstallRoot "app"
$OldAppDir = Join-Path $InstallRoot "app.old"
$DataDir = Join-Path $InstallRoot "data"
$ConfigDir = Join-Path $DataDir "config"
$LogsDir = Join-Path $DataDir "logs"
$OutputDir = Join-Path $DataDir "output"
$StagingDir = Join-Path $InstallRoot "staging"
New-Item -ItemType Directory -Force -Path $InstallRoot | Out-Null
New-Item -ItemType Directory -Force -Path $ConfigDir | Out-Null
New-Item -ItemType Directory -Force -Path $LogsDir | Out-Null
New-Item -ItemType Directory -Force -Path $OutputDir | Out-Null
New-Item -ItemType Directory -Force -Path $StagingDir | Out-Null
if (Test-Path -LiteralPath $AppDir) {
if (Test-Path -LiteralPath $OldAppDir) {
if (-not $Force) {
throw "app.old already exists: $OldAppDir. Re-run with -Force to replace it."
}
Remove-Item -LiteralPath $OldAppDir -Recurse -Force
}
Move-Item -LiteralPath $AppDir -Destination $OldAppDir
}
Copy-Item -LiteralPath $ReleaseDir -Destination $AppDir -Recurse
Copy-Item -LiteralPath $LauncherScript -Destination (Join-Path $InstallRoot "update.ps1") -Force
$configSources = @()
if ($LegacyDataDir) {
$configSources += Join-Path ([System.IO.Path]::GetFullPath($LegacyDataDir)) "config"
}
if (Test-Path -LiteralPath $OldAppDir) {
$configSources += Join-Path $OldAppDir "config"
}
$configSources += Join-Path $AppDir "config"
foreach ($sourceConfig in $configSources) {
Copy-ConfigIfMissing -SourceConfigDir $sourceConfig -TargetConfigDir $ConfigDir -FileName "app_config.json"
Copy-ConfigIfMissing -SourceConfigDir $sourceConfig -TargetConfigDir $ConfigDir -FileName "templates.json"
}
Write-Host "Installed CMBot launcher layout:"
Write-Host " Root: $InstallRoot"
Write-Host " App: $AppDir"
Write-Host " Data: $DataDir"
Write-Host ""
Write-Host "Start with:"
Write-Host " powershell -ExecutionPolicy Bypass -File `"$InstallRoot\update.ps1`""
+1 -1
View File
@@ -890,7 +890,7 @@
- [x] 启动 `versions\<current>\CMBot.exe` 并设 `CMBOT_DATA_DIR` - [x] 启动 `versions\<current>\CMBot.exe` 并设 `CMBOT_DATA_DIR`
- [x] 降级:源不可达 / robocopy 失败 / marker 缺失 / 坏 manifest 一律启动本地现版本 - [x] 降级:源不可达 / robocopy 失败 / marker 缺失 / 坏 manifest 一律启动本地现版本
- [x] 假版本目录验证 6 个用例(更新、幂等、源不可达、未配置源、下载损坏、坏 manifest)全过 - [x] 假版本目录验证 6 个用例(更新、幂等、源不可达、未配置源、下载损坏、坏 manifest)全过
- [ ] 接入真实安装结构:`build.ps1` 或新增安装步骤产出 `%LOCALAPPDATA%\CMBot\versions\<ver>\` 版本并排布局 - [x] 接入真实安装结构:新增 `scripts/install_local.ps1` 产出 `%LOCALAPPDATA%\CMBot\app` / `data` / `staging` 启动器布局
- [ ] 真实双机实测 - [ ] 真实双机实测
### 17.18 更新传输统一改为 HTTP ### 17.18 更新传输统一改为 HTTP