chore: retire PowerShell update.ps1 and install_local.ps1
Superseded by src/launcher.py (Launcher.exe) + the portable layout from build.ps1. The PS prototype validated the flow; removed to avoid confusion with the old %LOCALAPPDATA% installer. docs/10 §16 and tasks 17.19 updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -253,8 +253,8 @@ HTTP 在线更新比内网共享面临更高风险,按以下层次防护:
|
||||
|
||||
1. **地基**:数据目录分离(第 5 节)。任何更新方案的前提。✅ 已实现。
|
||||
2. **只读通知**:启动时读取 `manifest.json` 比对版本,有新版仅提示。✅ 已实现——`services/update_service.py`(`check_for_update` / 版本比较,纯逻辑可测,支持 **`http(s)://` 源 + Basic Auth** 及本地路径;manifest 用 `utf-8-sig` 解码以容忍 BOM)+ 主窗口顶部通知横幅,后台线程检查(不可达不阻塞启动),更新源 / 凭据由 `update_source` / `update_user` / `update_pass` 配置。
|
||||
3. **自动安装(PowerShell 原型)**:启动器完整流程(下载 → 校验 → `app` 目录切换 → 启动 → `app.old` 回滚)。✅ 以 `scripts/update.ps1` 实现并端到端验证(HTTP 下载 zip + SHA-256 + 解压 + `app/app.old` 切换);`scripts/build.ps1` 产出 `version.txt` + zip + 写无 BOM `manifest.json`。**此为流程验证原型。**
|
||||
4. **改用 `Launcher.exe` + `~/.cmbot`(当前方向)**:🚧 将启动器改为 **Python + PyInstaller onefile** 编译的 `Launcher.exe`(复用 `services/update_service.py`),采用**便携布局**(解压任意可写目录即用,安装根 = `Launcher.exe` 所在目录),用户数据移到 **`~/.cmbot`**(`get_data_dir()` 三级回退,见第 5 节)。退休 `scripts/update.ps1` 与 `scripts/install_local.ps1`(`%LOCALAPPDATA%` 安装器)。待做:`src/launcher.py`、`build.ps1` 增产 `Launcher.exe`、安装根可写性检测、首次播种默认模板、真实环境端到端实测。
|
||||
3. **自动安装(PowerShell 原型,已退休)**:曾以 `scripts/update.ps1` + `scripts/install_local.ps1` 跑通整条流程(HTTP 下载 zip + SHA-256 + 解压 + `app/app.old` 切换),验证了设计可行。该原型已被阶段④取代并删除(git 历史可查)。
|
||||
4. **`Launcher.exe` + `~/.cmbot`(当前方向)**:✅ 已实现——`src/launcher.py`(Python + PyInstaller onefile,复用 `services/update_service.py`:下载 zip + SHA-256 + 解压 + `app/app.old` 切换 + 安装根可写性检测 + 首次播种默认模板到 `~/.cmbot`);`scripts/build.ps1` 产出 `Launcher.exe` + 便携布局 + 两个 zip(自更新载荷 / 便携安装包)+ 无 BOM `manifest.json`;`get_data_dir()` 三级回退(见第 5 节)。已用本地 HTTP server 真实端到端验证(下载→校验→切换→播种)。**待做**:在 Windows 上真实 PyInstaller 构建 + 解压到 D 盘真机端到端实测。
|
||||
5. **强制更新与保留策略**:补全 `mandatory` / `min_supported` 与 `app.old` 回滚策略。⛔ 未做。
|
||||
|
||||
启动器(`Launcher.exe`,目标)要点:
|
||||
|
||||
@@ -1,166 +0,0 @@
|
||||
<#
|
||||
.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,321 +0,0 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
HTTP auto-update launcher for CMBot (docs/10-lan-update.md, stage 3).
|
||||
|
||||
.DESCRIPTION
|
||||
Run instead of launching CMBot.exe directly. On each start it:
|
||||
1. reads the local version from app\version.txt;
|
||||
2. reads update_source/update_user/update_pass from data\config\app_config.json;
|
||||
3. downloads manifest.json and, when newer, downloads the release zip;
|
||||
4. verifies SHA-256, extracts to staging\app.new, and switches app/app.old;
|
||||
5. launches app\CMBot.exe with CMBOT_DATA_DIR pointed at data\.
|
||||
|
||||
Degrades safely for normal updates: network errors, malformed manifests,
|
||||
failed downloads, bad hashes, or bad zips leave the existing app\ version
|
||||
in place and launch it.
|
||||
|
||||
.PARAMETER InstallRoot
|
||||
Install directory holding Launcher.exe, app\, app.old\, data\, staging\.
|
||||
Defaults to the launcher's own folder.
|
||||
|
||||
.PARAMETER NoLaunch
|
||||
Do everything except start the app. For testing the update flow.
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$InstallRoot = $PSScriptRoot,
|
||||
[switch]$NoLaunch
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$AppExe = "CMBot.exe"
|
||||
$ManifestName = "manifest.json"
|
||||
$VersionFileName = "version.txt"
|
||||
|
||||
$AppDir = Join-Path $InstallRoot "app"
|
||||
$OldAppDir = Join-Path $InstallRoot "app.old"
|
||||
$DataDir = Join-Path $InstallRoot "data"
|
||||
$StagingDir = Join-Path $InstallRoot "staging"
|
||||
$ConfigFile = Join-Path $DataDir "config\app_config.json"
|
||||
$LogFile = Join-Path $InstallRoot "launcher.log"
|
||||
|
||||
function Write-Log {
|
||||
param([string]$Message)
|
||||
$line = "{0} {1}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss"), $Message
|
||||
Write-Host $line
|
||||
try { Add-Content -LiteralPath $LogFile -Value $line -Encoding UTF8 } catch { }
|
||||
}
|
||||
|
||||
function Get-LocalVersion {
|
||||
$versionFile = Join-Path $AppDir $VersionFileName
|
||||
if (Test-Path -LiteralPath $versionFile) {
|
||||
return (Get-Content -LiteralPath $versionFile -Raw).TrimStart([char]0xFEFF).Trim()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
function Get-UpdateConfig {
|
||||
$result = [ordered]@{
|
||||
Source = ""
|
||||
User = ""
|
||||
Pass = ""
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $ConfigFile)) {
|
||||
return $result
|
||||
}
|
||||
try {
|
||||
$cfg = Get-Content -LiteralPath $ConfigFile -Raw | ConvertFrom-Json
|
||||
$result.Source = [string]$cfg.update_source
|
||||
$result.User = [string]$cfg.update_user
|
||||
$result.Pass = [string]$cfg.update_pass
|
||||
} catch {
|
||||
Write-Log "Cannot read update config: $_"
|
||||
}
|
||||
return $result
|
||||
}
|
||||
|
||||
function ConvertTo-VersionParts {
|
||||
param([string]$Text)
|
||||
$parts = @(0, 0, 0)
|
||||
if ($Text) {
|
||||
$chunks = $Text.Trim().Split(".")
|
||||
for ($i = 0; $i -lt 3 -and $i -lt $chunks.Count; $i++) {
|
||||
$digits = ($chunks[$i] -replace '^(\d*).*$', '$1')
|
||||
if ($digits) { $parts[$i] = [int]$digits }
|
||||
}
|
||||
}
|
||||
return ,$parts
|
||||
}
|
||||
|
||||
function Test-IsNewer {
|
||||
param([string]$Remote, [string]$Local)
|
||||
$r = ConvertTo-VersionParts $Remote
|
||||
$l = ConvertTo-VersionParts $Local
|
||||
for ($i = 0; $i -lt 3; $i++) {
|
||||
if ($r[$i] -gt $l[$i]) { return $true }
|
||||
if ($r[$i] -lt $l[$i]) { return $false }
|
||||
}
|
||||
return $false
|
||||
}
|
||||
|
||||
function Get-AuthHeaders {
|
||||
param(
|
||||
[string]$User,
|
||||
[string]$Pass
|
||||
)
|
||||
$headers = @{}
|
||||
if ($User -or $Pass) {
|
||||
$bytes = [System.Text.Encoding]::UTF8.GetBytes(("{0}:{1}" -f $User, $Pass))
|
||||
$headers["Authorization"] = "Basic " + [Convert]::ToBase64String($bytes)
|
||||
}
|
||||
return $headers
|
||||
}
|
||||
|
||||
function Resolve-ManifestUrl {
|
||||
param([string]$Source)
|
||||
$uri = [System.Uri]$Source
|
||||
if ([System.IO.Path]::GetFileName($uri.AbsolutePath).ToLowerInvariant() -eq $ManifestName) {
|
||||
return $uri.AbsoluteUri
|
||||
}
|
||||
if (-not $Source.EndsWith("/")) {
|
||||
$Source = "$Source/"
|
||||
$uri = [System.Uri]$Source
|
||||
}
|
||||
return (New-Object -TypeName System.Uri -ArgumentList $uri, $ManifestName).AbsoluteUri
|
||||
}
|
||||
|
||||
function Resolve-ReleaseUrl {
|
||||
param(
|
||||
[string]$ManifestUrl,
|
||||
[string]$ReleaseUrl
|
||||
)
|
||||
$uri = $null
|
||||
if ([System.Uri]::TryCreate($ReleaseUrl, [System.UriKind]::Absolute, [ref]$uri)) {
|
||||
return $uri.AbsoluteUri
|
||||
}
|
||||
return (New-Object -TypeName System.Uri -ArgumentList ([System.Uri]$ManifestUrl), $ReleaseUrl).AbsoluteUri
|
||||
}
|
||||
|
||||
function Invoke-JsonGet {
|
||||
param(
|
||||
[string]$Url,
|
||||
[hashtable]$Headers
|
||||
)
|
||||
$response = Invoke-WebRequest -Uri $Url -Headers $Headers -UseBasicParsing -TimeoutSec 15
|
||||
return $response.Content | ConvertFrom-Json
|
||||
}
|
||||
|
||||
function Save-HttpFile {
|
||||
param(
|
||||
[string]$Url,
|
||||
[string]$Destination,
|
||||
[hashtable]$Headers
|
||||
)
|
||||
Invoke-WebRequest -Uri $Url -Headers $Headers -UseBasicParsing -TimeoutSec 120 -OutFile $Destination
|
||||
}
|
||||
|
||||
function Remove-PathIfExists {
|
||||
param([string]$Path)
|
||||
if (Test-Path -LiteralPath $Path) {
|
||||
Remove-Item -LiteralPath $Path -Recurse -Force
|
||||
}
|
||||
}
|
||||
|
||||
function Get-PackageRoot {
|
||||
param([string]$ExtractDir)
|
||||
if (Test-Path -LiteralPath (Join-Path $ExtractDir $AppExe)) {
|
||||
return $ExtractDir
|
||||
}
|
||||
$children = @(Get-ChildItem -LiteralPath $ExtractDir -Directory)
|
||||
if ($children.Count -eq 1) {
|
||||
$candidate = $children[0].FullName
|
||||
if (Test-Path -LiteralPath (Join-Path $candidate $AppExe)) {
|
||||
return $candidate
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
function Assert-PackageValid {
|
||||
param(
|
||||
[string]$PackageRoot,
|
||||
[string]$ExpectedVersion
|
||||
)
|
||||
if (-not $PackageRoot -or -not (Test-Path -LiteralPath (Join-Path $PackageRoot $AppExe))) {
|
||||
throw "downloaded package does not contain $AppExe"
|
||||
}
|
||||
$versionFile = Join-Path $PackageRoot $VersionFileName
|
||||
if (-not (Test-Path -LiteralPath $versionFile)) {
|
||||
throw "downloaded package does not contain $VersionFileName"
|
||||
}
|
||||
$actualVersion = (Get-Content -LiteralPath $versionFile -Raw).TrimStart([char]0xFEFF).Trim()
|
||||
if ($actualVersion -ne $ExpectedVersion) {
|
||||
throw "package version '$actualVersion' does not match manifest '$ExpectedVersion'"
|
||||
}
|
||||
}
|
||||
|
||||
function Test-AppRunning {
|
||||
$name = [System.IO.Path]::GetFileNameWithoutExtension($AppExe)
|
||||
$exePath = Join-Path $AppDir $AppExe
|
||||
$running = Get-Process -Name $name -ErrorAction SilentlyContinue | Where-Object {
|
||||
try { $_.Path -eq $exePath } catch { $false }
|
||||
}
|
||||
return [bool]$running
|
||||
}
|
||||
|
||||
function Switch-AppDirectory {
|
||||
param([string]$PackageRoot)
|
||||
if (Test-AppRunning) {
|
||||
throw "$AppExe is already running"
|
||||
}
|
||||
if (Test-Path -LiteralPath $OldAppDir) {
|
||||
Remove-Item -LiteralPath $OldAppDir -Recurse -Force
|
||||
}
|
||||
if (Test-Path -LiteralPath $AppDir) {
|
||||
Move-Item -LiteralPath $AppDir -Destination $OldAppDir
|
||||
}
|
||||
try {
|
||||
Move-Item -LiteralPath $PackageRoot -Destination $AppDir
|
||||
} catch {
|
||||
if ((-not (Test-Path -LiteralPath $AppDir)) -and (Test-Path -LiteralPath $OldAppDir)) {
|
||||
Move-Item -LiteralPath $OldAppDir -Destination $AppDir
|
||||
}
|
||||
throw
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-Update {
|
||||
param(
|
||||
[string]$Source,
|
||||
[string]$User,
|
||||
[string]$Pass,
|
||||
[string]$LocalVersion
|
||||
)
|
||||
|
||||
$manifestUrl = Resolve-ManifestUrl $Source
|
||||
$headers = Get-AuthHeaders -User $User -Pass $Pass
|
||||
|
||||
Write-Log "Checking update manifest: $manifestUrl"
|
||||
$m = Invoke-JsonGet -Url $manifestUrl -Headers $headers
|
||||
|
||||
$remoteVer = [string]$m.version
|
||||
if (-not $remoteVer) {
|
||||
Write-Log "Manifest has no version - skip update."
|
||||
return
|
||||
}
|
||||
if (-not (Test-IsNewer $remoteVer $LocalVersion)) {
|
||||
Write-Log "Up to date (local '$LocalVersion', remote '$remoteVer')."
|
||||
return
|
||||
}
|
||||
|
||||
$releaseField = [string]$m.url
|
||||
$expectedHash = ([string]$m.sha256).Trim().ToLowerInvariant()
|
||||
if (-not $releaseField -or -not $expectedHash) {
|
||||
throw "manifest must contain url and sha256"
|
||||
}
|
||||
$releaseUrl = Resolve-ReleaseUrl -ManifestUrl $manifestUrl -ReleaseUrl $releaseField
|
||||
|
||||
$zipPath = Join-Path $StagingDir ("{0}.zip" -f $remoteVer)
|
||||
$extractDir = Join-Path $StagingDir "app.new"
|
||||
Remove-PathIfExists -Path $zipPath
|
||||
Remove-PathIfExists -Path $extractDir
|
||||
|
||||
Write-Log "Downloading v$remoteVer from $releaseUrl ..."
|
||||
Save-HttpFile -Url $releaseUrl -Destination $zipPath -Headers $headers
|
||||
|
||||
if ($m.size) {
|
||||
$actualSize = (Get-Item -LiteralPath $zipPath).Length
|
||||
if ($actualSize -ne [int64]$m.size) {
|
||||
throw "download size mismatch: got $actualSize, expected $($m.size)"
|
||||
}
|
||||
}
|
||||
|
||||
$actualHash = (Get-FileHash -LiteralPath $zipPath -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
if ($actualHash -ne $expectedHash) {
|
||||
throw "sha256 mismatch"
|
||||
}
|
||||
|
||||
Expand-Archive -LiteralPath $zipPath -DestinationPath $extractDir -Force
|
||||
$packageRoot = Get-PackageRoot -ExtractDir $extractDir
|
||||
Assert-PackageValid -PackageRoot $packageRoot -ExpectedVersion $remoteVer
|
||||
|
||||
Switch-AppDirectory -PackageRoot $packageRoot
|
||||
Remove-PathIfExists -Path $zipPath
|
||||
Remove-PathIfExists -Path $extractDir
|
||||
Write-Log "Installed and switched to v$remoteVer."
|
||||
}
|
||||
|
||||
# -- main --------------------------------------------------------------------
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $StagingDir | Out-Null
|
||||
New-Item -ItemType Directory -Force -Path $DataDir | Out-Null
|
||||
|
||||
$local = Get-LocalVersion
|
||||
$cfg = Get-UpdateConfig
|
||||
|
||||
if ($cfg.Source) {
|
||||
try {
|
||||
Invoke-Update -Source $cfg.Source -User $cfg.User -Pass $cfg.Pass -LocalVersion $local
|
||||
} catch {
|
||||
Write-Log "Update skipped (degraded to local version): $_"
|
||||
}
|
||||
} else {
|
||||
Write-Log "No update_source configured - skip update check."
|
||||
}
|
||||
|
||||
$current = Get-LocalVersion
|
||||
$exe = Join-Path $AppDir $AppExe
|
||||
if (-not (Test-Path -LiteralPath $exe)) {
|
||||
Write-Log "ERROR: executable not found: $exe"
|
||||
exit 1
|
||||
}
|
||||
|
||||
if ($NoLaunch) {
|
||||
Write-Log "NoLaunch: would start $exe v$current (CMBOT_DATA_DIR=$DataDir)"
|
||||
exit 0
|
||||
}
|
||||
|
||||
Write-Log "Launching v$current ..."
|
||||
$env:CMBOT_DATA_DIR = $DataDir
|
||||
Start-Process -FilePath $exe
|
||||
@@ -935,7 +935,7 @@
|
||||
- [x] `get_data_dir()` 三级回退:`CMBOT_DATA_DIR` → 打包态 `~/.cmbot` → 开发态项目根;`tests/test_file_service.py` 5 个单测
|
||||
- [x] `src/launcher.py`:复用 `update_service`,下载 zip→SHA-256→解压→`app/app.old` 切换→启动;安装根可写性检测;首次把 `app\config\` 默认模板播种到 `~/.cmbot`;`update_service` 扩展(`UpdateInfo.sha256/size/min_supported`、绝对 url 解析、`make_auth_header`/`download`);`tests/test_launcher.py` 11 个单测 + 本地 HTTP server 真实端到端验证
|
||||
- [x] `build.ps1` 增产 `Launcher.exe`(PyInstaller onefile,console);发布目录改为便携布局 `Launcher.exe` + `app\`;产出两个 zip——自更新载荷 `CMBot-<ver>.zip`(app\ 内容,manifest.url 指向它)与便携安装包 `CMBot-<ver>-portable.zip`;`launcher.py` 日志初始化健壮化(--windowed/只读根不崩)。语法校验通过;实际 PyInstaller 构建需在 Windows 跑
|
||||
- [ ] 退休 `scripts/update.ps1` 与 `scripts/install_local.ps1`
|
||||
- [x] 退休 `scripts/update.ps1` 与 `scripts/install_local.ps1`(已删除,git 历史可查)
|
||||
- [ ] 端到端实测(解压到 D 盘运行、自更新、回滚)
|
||||
|
||||
### 17.20 设置对话框(更新配置入口)
|
||||
|
||||
Reference in New Issue
Block a user