[CmdletBinding()] param( # 仅用于自动化验证;正常启动时保持 0,由 Ctrl+C 结束。 [ValidateRange(0, 3600)] [int]$RunForSeconds = 0, # 首次启动可能需要加载本地嵌入模型,因此预留充足的冷启动时间。 [ValidateRange(10, 600)] [int]$StartupTimeoutSeconds = 120 ) $ErrorActionPreference = "Stop" $ProjectRoot = Split-Path -Parent $PSScriptRoot $FrontendPath = Join-Path $ProjectRoot "frontend" $BackendPath = Join-Path $ProjectRoot "backend" $ManagedProcessIds = [System.Collections.Generic.HashSet[int]]::new() $StartedProcesses = @() function Get-DevPortListeners { param([int]$Port) return @( Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue ) } function Test-DevPortListening { param([int]$Port) $Endpoints = [System.Net.NetworkInformation.IPGlobalProperties]::GetIPGlobalProperties().GetActiveTcpListeners() return $null -ne ($Endpoints | Where-Object { $_.Port -eq $Port } | Select-Object -First 1) } function Assert-DevPortAvailable { param( [int]$Port, [string]$ApplicationName ) if (-not (Test-DevPortListening -Port $Port)) { return } # 只有端口确实被占用时才调用较慢的 Windows 连接查询,以便展示 PID。 $Listeners = Get-DevPortListeners -Port $Port $Owners = foreach ($Listener in $Listeners) { $Process = Get-CimInstance Win32_Process ` -Filter "ProcessId=$($Listener.OwningProcess)" ` -ErrorAction SilentlyContinue if ($null -ne $Process) { "PID $($Process.ProcessId):$($Process.CommandLine)" } else { "PID $($Listener.OwningProcess)" } } throw "$ApplicationName 端口 $Port 已被占用。请先结束以下进程后重试:`n$($Owners -join "`n")" } function Update-ManagedProcessTree { # Uvicorn reload、pnpm 和 Vite 都会派生子进程。持续记录完整进程树, # 即使某个中间父进程先退出,最终清理时仍能找到它已经创建的子进程。 # Win32_Process 单次查询在部分 Windows 机器上约需半秒,逐个父 PID # 查询会明显拖慢启动。因此每轮只取一次快照,在内存中展开进程树。 $AllProcesses = @(Get-CimInstance Win32_Process -ErrorAction SilentlyContinue) $AddedProcess = $true while ($AddedProcess) { $AddedProcess = $false foreach ($Child in $AllProcesses) { if (-not $ManagedProcessIds.Contains([int]$Child.ParentProcessId)) { continue } $ChildId = [int]$Child.ProcessId if ($ManagedProcessIds.Add($ChildId)) { $AddedProcess = $true } } } } function Test-ProcessRunning { param([System.Diagnostics.Process]$Process) try { $Process.Refresh() return -not $Process.HasExited } catch { return $false } } function Test-HttpEndpoint { param([string]$Uri) try { $Response = Invoke-WebRequest ` -Uri $Uri ` -UseBasicParsing ` -TimeoutSec 2 ` -ErrorAction Stop return $Response.StatusCode -ge 200 -and $Response.StatusCode -lt 500 } catch { return $false } } function Wait-ForDevelopmentServices { param( [System.Diagnostics.Process]$FrontendProcess, [System.Diagnostics.Process]$BackendProcess, [int]$TimeoutSeconds = 120 ) $Deadline = (Get-Date).AddSeconds($TimeoutSeconds) while ((Get-Date) -lt $Deadline) { Update-ManagedProcessTree if (-not (Test-ProcessRunning -Process $FrontendProcess)) { throw "前端开发服务器启动失败,请检查上方 pnpm 输出。" } if (-not (Test-ProcessRunning -Process $BackendProcess)) { throw "后端开发服务器启动失败,请检查上方 API 与退款 Worker 输出。" } $H5Ready = Test-HttpEndpoint -Uri "http://127.0.0.1:5173/" $AdminReady = Test-HttpEndpoint -Uri "http://127.0.0.1:5174/" $BackendReady = Test-HttpEndpoint -Uri "http://127.0.0.1:8000/docs" if ($H5Ready -and $AdminReady -and $BackendReady) { return } Start-Sleep -Milliseconds 300 } throw "开发服务在 $TimeoutSeconds 秒内未全部就绪,请检查上方启动日志。" } function Stop-ManagedProcesses { Write-Host "`n正在停止智保通开发服务..." -ForegroundColor Yellow # 多刷新几次,覆盖清理开始前恰好生成的 reload/Vite 子进程。 1..3 | ForEach-Object { Update-ManagedProcessTree Start-Sleep -Milliseconds 100 } $Processes = @( Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | Where-Object { $_.ProcessId -ne $PID -and $ManagedProcessIds.Contains([int]$_.ProcessId) } ) # 子进程通常晚于父进程创建;倒序停止可避免父进程退出后留下孤儿。 $Processes | Sort-Object CreationDate -Descending | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue } # 确认本次脚本启动的三个监听端口已经释放。 $Deadline = (Get-Date).AddSeconds(5) while ((Get-Date) -lt $Deadline) { $AnyPortListening = $false foreach ($Port in 5173, 5174, 8000) { if (Test-DevPortListening -Port $Port) { $AnyPortListening = $true break } } if (-not $AnyPortListening) { break } Start-Sleep -Milliseconds 200 } Write-Host "智保通开发服务已停止。" -ForegroundColor Green } foreach ($Service in @( @{ Port = 5173; Name = "H5" }, @{ Port = 5174; Name = "管理后台" }, @{ Port = 8000; Name = "后端 API" } )) { Assert-DevPortAvailable -Port $Service.Port -ApplicationName $Service.Name } $PnpmPath = (Get-Command pnpm.cmd -ErrorAction Stop).Source $UvPath = (Get-Command uv.exe -ErrorAction Stop).Source try { Write-Host "正在启动 H5、管理后台和后端 API..." -ForegroundColor Cyan $FrontendProcess = Start-Process ` -FilePath $PnpmPath ` -ArgumentList "dev" ` -WorkingDirectory $FrontendPath ` -NoNewWindow ` -PassThru $StartedProcesses += $FrontendProcess [void]$ManagedProcessIds.Add($FrontendProcess.Id) $BackendProcess = Start-Process ` -FilePath $UvPath ` -ArgumentList @( "run", "zbt-serve" ) ` -WorkingDirectory $BackendPath ` -NoNewWindow ` -PassThru $StartedProcesses += $BackendProcess [void]$ManagedProcessIds.Add($BackendProcess.Id) Wait-ForDevelopmentServices ` -FrontendProcess $FrontendProcess ` -BackendProcess $BackendProcess ` -TimeoutSeconds $StartupTimeoutSeconds Write-Host "" Write-Host "智保通开发服务已全部启动:" -ForegroundColor Green Write-Host "H5:http://127.0.0.1:5173" Write-Host "管理后台:http://127.0.0.1:5174" Write-Host "后端 API:http://127.0.0.1:8000/docs" Write-Host "按 Ctrl+C 可一次性停止全部服务。" -ForegroundColor DarkGray $ReadyAt = Get-Date while ($true) { Update-ManagedProcessTree foreach ($StartedProcess in $StartedProcesses) { if (-not (Test-ProcessRunning -Process $StartedProcess)) { throw "开发服务进程 PID $($StartedProcess.Id) 意外退出,请检查上方日志。" } } if ($RunForSeconds -gt 0 -and ((Get-Date) - $ReadyAt).TotalSeconds -ge $RunForSeconds) { break } Start-Sleep -Milliseconds 500 } } finally { Stop-ManagedProcesses }