Add Windows start and update script

This commit is contained in:
mid
2026-08-08 00:06:18 +08:00
parent fa14e9e5b9
commit 023e77dbd4
3 changed files with 224 additions and 0 deletions
+7
View File
@@ -2,6 +2,13 @@
本文档记录面向使用者和维护者的重要变更。版本号以 `package.json` 为准。 本文档记录面向使用者和维护者的重要变更。版本号以 `package.json` 为准。
## Unreleased
### Added
- 增加统一的 Windows PowerShell 启动与更新脚本,提供固定的生产配置、Git 快进更新、锁定依赖重装和构建后启动流程。
- 增加 Windows Server 非 Docker 部署、启动和更新说明。
## 0.0.5 - 2026-08-07 ## 0.0.5 - 2026-08-07
### Changed ### Changed
+37
View File
@@ -109,6 +109,43 @@ docker compose down
健康检查地址:`http://服务器地址:8080/healthz` 健康检查地址:`http://服务器地址:8080/healthz`
## Windows Server 部署(不使用 Docker)
要求安装 Git、Node.js 22.12.0 或更高版本,并使用 PowerShell 5.1 或更高版本。以下示例假设项目位于 `C:\Services\WebTools`。
首次克隆后安装依赖并构建:
```powershell
Set-Location C:\Services\WebTools
npm.cmd ci --include=dev
npm.cmd run build
```
生产构建成功后,使用项目根目录的统一脚本启动:
```powershell
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\web-tools.ps1 start
```
默认监听 `0.0.0.0:9000`,导航配置保存在 `C:\ProgramData\WebTools\data\navigation.json`。可以通过参数覆盖:
```powershell
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\web-tools.ps1 start `
-ListenAddress 127.0.0.1 `
-Port 8080 `
-DataDir C:\ProgramData\WebTools\data
```
从 Git `main` 分支更新时,先在运行窗口按 `Ctrl+C` 停止服务,再执行:
```powershell
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\web-tools.ps1 update
```
更新脚本只接受干净的 Git 工作区和当前 `main` 分支,使用 `git pull --ff-only origin main` 避免隐式合并。随后执行 `npm ci --include=dev`、生产构建并自动重新启动服务。`npm ci` 会先删除现有 `node_modules` 再按 `package-lock.json` 完整重建,不需要手工删除旧依赖。只更新和构建、不立即启动时增加 `-NoStart`。
脚本不会读取、覆盖或删除 `C:\ProgramData\WebTools\data`。PowerShell 窗口关闭后前台服务会停止;如使用 Windows 计划任务,应让任务执行 `web-tools.ps1 start`,更新时停止任务、运行 `web-tools.ps1 update -NoStart`,然后重新启动任务。
## 工具管理 ## 工具管理
管理入口不显示在页面中,直接访问: 管理入口不显示在页面中,直接访问:
+180
View File
@@ -0,0 +1,180 @@
#Requires -Version 5.1
[CmdletBinding()]
param(
[Parameter(Position = 0)]
[ValidateSet('start', 'update')]
[string]$Action = 'start',
[ValidateNotNullOrEmpty()]
[string]$ListenAddress = '0.0.0.0',
[ValidateRange(1, 65535)]
[int]$Port = 9000,
[ValidateNotNullOrEmpty()]
[string]$DataDir = 'C:\ProgramData\WebTools\data',
[ValidateNotNullOrEmpty()]
[string]$Remote = 'origin',
[ValidateNotNullOrEmpty()]
[string]$Branch = 'main',
[switch]$NoStart
)
Set-StrictMode -Version 3.0
$ErrorActionPreference = 'Stop'
$projectRoot = $PSScriptRoot
$serverEntry = Join-Path $projectRoot 'server\index.mjs'
$distIndex = Join-Path $projectRoot 'dist\index.html'
function Resolve-Application {
param(
[Parameter(Mandatory = $true)]
[string]$Name
)
$command = Get-Command $Name -CommandType Application -ErrorAction SilentlyContinue |
Select-Object -First 1
if ($null -eq $command) {
throw "Required command '$Name' was not found in PATH."
}
return $command.Source
}
function Invoke-NativeCommand {
param(
[Parameter(Mandatory = $true)]
[string]$Executable,
[Parameter(Mandatory = $true)]
[string[]]$Arguments
)
& $Executable @Arguments
$exitCode = $LASTEXITCODE
if ($exitCode -ne 0) {
throw "Command '$Executable $($Arguments -join ' ')' failed with exit code $exitCode."
}
}
function Assert-ProductionBuild {
if (-not (Test-Path -LiteralPath $serverEntry -PathType Leaf)) {
throw "Server entry was not found: $serverEntry"
}
if (-not (Test-Path -LiteralPath $distIndex -PathType Leaf)) {
throw "Production build was not found: $distIndex. Run '.\web-tools.ps1 update' first."
}
}
function Update-WebTools {
$git = Resolve-Application 'git.exe'
$npm = Resolve-Application 'npm.cmd'
Push-Location $projectRoot
try {
$insideWorkTree = @(& $git rev-parse --is-inside-work-tree)
if ($LASTEXITCODE -ne 0 -or ($insideWorkTree -join '').Trim() -ne 'true') {
throw "Project directory is not a Git work tree: $projectRoot"
}
$statusLines = @(& $git status --porcelain --untracked-files=normal)
if ($LASTEXITCODE -ne 0) {
throw "Unable to inspect the Git work tree."
}
if ($statusLines.Count -gt 0) {
throw "The Git work tree contains local changes. Update was stopped to avoid overwriting them:`n$($statusLines -join "`n")"
}
$currentBranchOutput = @(& $git branch --show-current)
if ($LASTEXITCODE -ne 0) {
throw "Unable to determine the current Git branch."
}
$currentBranch = ($currentBranchOutput -join '').Trim()
if ($currentBranch -ne $Branch) {
throw "Current branch is '$currentBranch'; expected '$Branch'."
}
Write-Host "Updating $Remote/$Branch..."
Invoke-NativeCommand $git @('pull', '--ff-only', $Remote, $Branch)
$previousNodeEnv = [Environment]::GetEnvironmentVariable('NODE_ENV', 'Process')
try {
[Environment]::SetEnvironmentVariable('NODE_ENV', $null, 'Process')
Write-Host 'Installing locked dependencies (npm ci replaces node_modules automatically)...'
Invoke-NativeCommand $npm @('ci', '--include=dev')
Write-Host 'Building the production application...'
Invoke-NativeCommand $npm @('run', 'build')
}
finally {
[Environment]::SetEnvironmentVariable('NODE_ENV', $previousNodeEnv, 'Process')
}
Assert-ProductionBuild
Write-Host 'Update and production build completed successfully.'
}
finally {
Pop-Location
}
}
function Start-WebTools {
Assert-ProductionBuild
$node = Resolve-Application 'node.exe'
$dataDirectory = (New-Item -ItemType Directory -Force -Path $DataDir).FullName
$previousEnvironment = @{
NODE_ENV = [Environment]::GetEnvironmentVariable('NODE_ENV', 'Process')
HOST = [Environment]::GetEnvironmentVariable('HOST', 'Process')
PORT = [Environment]::GetEnvironmentVariable('PORT', 'Process')
DATA_DIR = [Environment]::GetEnvironmentVariable('DATA_DIR', 'Process')
}
try {
[Environment]::SetEnvironmentVariable('NODE_ENV', 'production', 'Process')
[Environment]::SetEnvironmentVariable('HOST', $ListenAddress, 'Process')
[Environment]::SetEnvironmentVariable('PORT', $Port.ToString(), 'Process')
[Environment]::SetEnvironmentVariable('DATA_DIR', $dataDirectory, 'Process')
Write-Host "Starting Web Tools on http://${ListenAddress}:$Port"
Write-Host "Navigation config: $(Join-Path $dataDirectory 'navigation.json')"
Write-Host 'Press Ctrl+C to stop.'
Push-Location $projectRoot
try {
& $node $serverEntry
$exitCode = $LASTEXITCODE
if ($exitCode -ne 0) {
throw "Web Tools exited with code $exitCode."
}
}
finally {
Pop-Location
}
}
finally {
foreach ($name in $previousEnvironment.Keys) {
[Environment]::SetEnvironmentVariable($name, $previousEnvironment[$name], 'Process')
}
}
}
if ($Action -eq 'start' -and $NoStart) {
throw "-NoStart can only be used with the 'update' action."
}
if ($Action -eq 'update') {
Update-WebTools
if ($NoStart) {
return
}
}
Start-WebTools