From ba8daaccddaae3baf6d5d691e6d8d690b284ac62 Mon Sep 17 00:00:00 2001 From: ila Date: Thu, 18 Jun 2026 10:04:29 +0800 Subject: [PATCH] feat: add local launcher installer --- scripts/install_local.ps1 | 166 ++++++++++++++++++++++++++++++++++++++ tasks.md | 2 +- 2 files changed, 167 insertions(+), 1 deletion(-) create mode 100644 scripts/install_local.ps1 diff --git a/scripts/install_local.ps1 b/scripts/install_local.ps1 new file mode 100644 index 0000000..f5798ac --- /dev/null +++ b/scripts/install_local.ps1 @@ -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`"" diff --git a/tasks.md b/tasks.md index 578fa7f..6377932 100644 --- a/tasks.md +++ b/tasks.md @@ -890,7 +890,7 @@ - [x] 启动 `versions\\CMBot.exe` 并设 `CMBOT_DATA_DIR` - [x] 降级:源不可达 / robocopy 失败 / marker 缺失 / 坏 manifest 一律启动本地现版本 - [x] 假版本目录验证 6 个用例(更新、幂等、源不可达、未配置源、下载损坏、坏 manifest)全过 -- [ ] 接入真实安装结构:`build.ps1` 或新增安装步骤产出 `%LOCALAPPDATA%\CMBot\versions\\` 版本并排布局 +- [x] 接入真实安装结构:新增 `scripts/install_local.ps1` 产出 `%LOCALAPPDATA%\CMBot\app` / `data` / `staging` 启动器布局 - [ ] 真实双机实测 ### 17.18 更新传输统一改为 HTTP