reload 卡死三层预防(走查中遭遇 uvicorn graceful shutdown 死等触发): - Layer 1 (apps/backend/start_uvicorn.py 新): 把 reload-excludes 封装在 Python 字符串内,ps1 命令行只有字面路径,根治 PowerShell PSNativeCommandArgumentPassing 在不同 profile 下 wildcard 展开 行为差异(数组 splatting 和 --% 都不稳)。同时显式设 timeout-graceful-shutdown=5,5 秒强杀防死等 - Layer 2 (scripts/ops/backend-watchdog.ps1 新): 自主 socket 探针 (TcpClient + 手写 HTTP/1.1 GET,Connection: close)规避 .NET HttpClient pool 复用 + 系统代理误报;3s × 3 = 9s 触发重启; 进程链 kill 至 pwsh 后端窗口(关闭原窗口);3 次/小时上限自停 - Layer 3 (scripts/ops/start-admin.ps1): 启动时拉起 watchdog, 菜单 [4] 仅重启后端选项,主菜单退出时一并 kill 看门狗 CLAUDE.md: 新增"后端 reload 卡死预防(强制)"章节, 分级文件风险表 + SOP + 启动菜单速查 走查报告(应查尽查严肃版): - 后端 6 个改造点 PASS(P1-P4 + GUC + ai_run_logs runtime 字段) - admin-web 7 页 Playwright 实地走查 → 5 项 UI 不完整登记 F1-5b - 小程序看板 tab 7 页 weixin-devtools-mcp 实地 + DB 数据核对 → board-finance 5/6 项上界裁剪吻合;board-customer 业务日生效; board-coach 月度聚合表设计盲区;5 项 sandbox 覆盖盲区登记 F1-5b - 8 张走查截图归档 docs/audit/changes/screenshots/2026-05-05_f1_5a_walkthrough/ audit_dashboard 刷新到 153 条审计 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
487 lines
25 KiB
PowerShell
487 lines
25 KiB
PowerShell
# -*- coding: utf-8 -*-
|
||
# 启动管理后台(后端 + 前端)
|
||
# 服务成功启动后自动打开浏览器
|
||
|
||
$ErrorActionPreference = "Stop"
|
||
# CHANGE 2026-05-05 | $watchdogProc 提前到 try 块外,确保 catch / 退出兜底能访问
|
||
$watchdogProc = $null
|
||
|
||
try {
|
||
# CHANGE 2026-03-07 | 定位项目根目录:从 bat 启动目录推算,不穿透 junction
|
||
# 背景:C:\Project\NeoZQYY 是 junction → D:\NeoZQYY\...\repo,
|
||
# $MyInvocation.MyCommand.Path 和 Split-Path 都会穿透 junction 解析到 D 盘,
|
||
# 导致后端 CWD、.venv python、.env 全部指向 D 盘副本。
|
||
# 解决:优先用环境变量 NEOZQYY_ROOT;其次用 bat 传入的 %~dp0(不穿透 junction);
|
||
# 最后才回退到脚本路径推算。
|
||
if ($env:NEOZQYY_ROOT -and (Test-Path $env:NEOZQYY_ROOT)) {
|
||
$ProjectRoot = $env:NEOZQYY_ROOT
|
||
} elseif ($env:NEOZQYY_LAUNCH_DIR -and (Test-Path $env:NEOZQYY_LAUNCH_DIR)) {
|
||
# 由 start-admin.bat 通过 %~dp0 注入,不穿透 junction
|
||
$ProjectRoot = $env:NEOZQYY_LAUNCH_DIR.TrimEnd('\')
|
||
} elseif ($MyInvocation.MyCommand.Path) {
|
||
$ProjectRoot = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path))
|
||
} else {
|
||
$ProjectRoot = $PWD.Path
|
||
}
|
||
|
||
# CHANGE 2026-03-07 | 将 ProjectRoot 注入环境变量 NEOZQYY_ROOT,
|
||
# 使 config.py 的 _find_project_root() 策略 1 直接命中,
|
||
# 不再依赖 __file__ 推算(__file__ 会穿透 junction 到 D 盘)。
|
||
# 这个环境变量会被子进程(uvicorn)继承。
|
||
$env:NEOZQYY_ROOT = $ProjectRoot
|
||
|
||
Write-Host "========================================" -ForegroundColor Cyan
|
||
Write-Host " NeoZQYY 管理后台启动脚本" -ForegroundColor Cyan
|
||
Write-Host "========================================" -ForegroundColor Cyan
|
||
Write-Host ""
|
||
Write-Host "项目根目录: $ProjectRoot"
|
||
Write-Host "NEOZQYY_ROOT=$env:NEOZQYY_ROOT"
|
||
Write-Host ""
|
||
|
||
$backendDir = Join-Path $ProjectRoot "apps\backend"
|
||
$frontendDir = Join-Path $ProjectRoot "apps\admin-web"
|
||
$tenantDir = Join-Path $ProjectRoot "apps\tenant-admin"
|
||
if (-not (Test-Path $backendDir)) { throw "后端目录不存在: $backendDir" }
|
||
if (-not (Test-Path $frontendDir)) { throw "前端目录不存在: $frontendDir" }
|
||
if (-not (Test-Path $tenantDir)) { throw "租户管理后台目录不存在: $tenantDir" }
|
||
|
||
# ── 端口冲突检测:确保 8000/5173/5174 未被旧进程占用 ──
|
||
foreach ($port in @(8000, 5173, 5174)) {
|
||
$listeners = Get-NetTCPConnection -LocalPort $port -State Listen -ErrorAction SilentlyContinue
|
||
if ($listeners) {
|
||
foreach ($l in $listeners) {
|
||
$procId = $l.OwningProcess
|
||
$proc = Get-Process -Id $procId -ErrorAction SilentlyContinue
|
||
$procPath = if ($proc) { $proc.Path } else { "未知" }
|
||
Write-Host "端口 $port 被占用: PID=$procId ($procPath)" -ForegroundColor Yellow
|
||
|
||
# 尝试多种方式终止:Stop-Process → taskkill /T /F
|
||
Write-Host " 正在终止旧进程..." -ForegroundColor Yellow
|
||
Stop-Process -Id $procId -Force -ErrorAction SilentlyContinue
|
||
Start-Sleep -Seconds 1
|
||
# 如果 Stop-Process 不够,用 taskkill 连子进程一起杀
|
||
# taskkill 非零退出码不应终止脚本
|
||
$ErrorActionPreference = "Continue"
|
||
taskkill /PID $procId /T /F 2>$null | Out-Null
|
||
$ErrorActionPreference = "Stop"
|
||
}
|
||
|
||
# 等待端口释放(最多 30 秒)
|
||
$waitMax = 30
|
||
$waited = 0
|
||
while ($waited -lt $waitMax) {
|
||
Start-Sleep -Seconds 2
|
||
$waited += 2
|
||
$still = Get-NetTCPConnection -LocalPort $port -State Listen -ErrorAction SilentlyContinue
|
||
if (-not $still) {
|
||
Write-Host " 端口 $port 已释放(等待 ${waited}s)" -ForegroundColor Green
|
||
break
|
||
}
|
||
# 父进程可能已死但子进程仍占端口,扫描并杀子进程
|
||
$zombiePid = $still.OwningProcess
|
||
$children = Get-CimInstance Win32_Process | Where-Object { $_.ParentProcessId -eq $zombiePid }
|
||
if ($children) {
|
||
foreach ($child in $children) {
|
||
Write-Host " 发现残留子进程: PID=$($child.ProcessId) ($($child.ExecutablePath))" -ForegroundColor Yellow
|
||
# taskkill 非零退出码不应终止脚本
|
||
$ErrorActionPreference = "Continue"
|
||
taskkill /PID $child.ProcessId /T /F 2>$null | Out-Null
|
||
$ErrorActionPreference = "Stop"
|
||
}
|
||
}
|
||
Write-Host " 端口 $port 仍被占用,继续等待... (${waited}/${waitMax}s)" -ForegroundColor Yellow
|
||
}
|
||
if ($waited -ge $waitMax) {
|
||
$still2 = Get-NetTCPConnection -LocalPort $port -State Listen -ErrorAction SilentlyContinue
|
||
if ($still2) {
|
||
Write-Host ""
|
||
Write-Host " !! 端口 $port 无法释放(僵尸进程 PID=$($still2.OwningProcess))" -ForegroundColor Red
|
||
Write-Host " !! 请重启电脑或手动执行: netsh int ipv4 reset" -ForegroundColor Red
|
||
Write-Host " !! 或等待几分钟后重试" -ForegroundColor Red
|
||
throw "端口 $port 被僵尸进程占用,无法启动"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
# 前端日志文件(每次用唯一文件名,避免上次进程锁定旧文件)
|
||
$ts = Get-Date -Format "yyyyMMdd_HHmmss"
|
||
$frontendLog = Join-Path $env:TEMP "neozqyy_fe_${ts}.log"
|
||
$tenantLog = Join-Path $env:TEMP "neozqyy_ta_${ts}.log"
|
||
|
||
# 选择 PowerShell 可执行文件:优先 pwsh (7+),回退 powershell (5.1)
|
||
$psExe = if (Get-Command pwsh -ErrorAction SilentlyContinue) { "pwsh" } else { "powershell" }
|
||
|
||
# ── 生成临时启动脚本(带时间戳,避免旧脚本残留) ──
|
||
$beTmp = Join-Path $env:TEMP "neozqyy_start_be_${ts}.ps1"
|
||
$feTmp = Join-Path $env:TEMP "neozqyy_start_fe_${ts}.ps1"
|
||
$taTmp = Join-Path $env:TEMP "neozqyy_start_ta_${ts}.ps1"
|
||
|
||
$q = [char]39
|
||
# CHANGE 2026-03-07 | 显式使用 .venv 的 python,避免 uv run 解析到 miniconda
|
||
# 背景:uv run uvicorn 在此机器上解析到 C:\ProgramData\miniconda3\python.exe,
|
||
# 导致 config.py 的 Path(__file__).resolve() 指向错误的代码副本(D 盘)
|
||
$venvPython = Join-Path $ProjectRoot ".venv\Scripts\python.exe"
|
||
if (-not (Test-Path $venvPython)) {
|
||
throw ".venv Python 不存在: $venvPython — 请先运行 uv sync"
|
||
}
|
||
|
||
# CHANGE 2026-03-07 | 临时脚本中显式注入 NEOZQYY_ROOT,确保 uvicorn 子进程
|
||
# 无论通过何种方式启动都能拿到正确的项目根目录
|
||
# CHANGE 2026-03-07 | 解决 ANSI 转义码乱码:
|
||
# PowerShell 5.1 + 旧版 conhost 不解析 ANSI 转义序列,uvicorn 彩色日志
|
||
# 显示为 [32m、[0m 等乱码。设置 NO_COLOR=1 禁用颜色输出。
|
||
# pwsh 7+ 和 Windows Terminal 原生支持 VT,不受影响。
|
||
# CHANGE 2026-04-01 | 修复三个显示问题:
|
||
# 1. Write-Host 用单引号包裹导致 $env:NEOZQYY_ROOT 不展开 → 改用双引号
|
||
# 2. NO_COLOR 对 uvicorn 不生效 → 加 --no-color 参数
|
||
# 3. PS 5.1 传统控制台 UTF-8 箭头乱码 → 所有临时脚本开头 chcp 65001
|
||
# 并统一设 NO_COLOR=1 禁用 ANSI 转义码
|
||
# CHANGE 2026-05-05 | F1-5a 走查发现 reload 卡死问题:
|
||
# - 加 --timeout-graceful-shutdown 5:5 秒后 graceful 失败强杀,reload 不再死等
|
||
# - 配合 backend-watchdog.ps1:连续 30 秒探针失败自动重启
|
||
# CHANGE 2026-05-05 v3 | 根治 wildcard 展开:
|
||
# 用 apps/backend/start_uvicorn.py 启动脚本,所有 wildcard 字符串封装在 Python 内部,
|
||
# PowerShell shell 完全不接触 wildcard,根本上规避 PSNativeCommandArgumentPassing 行为
|
||
$beLines = @(
|
||
"`$env:NEOZQYY_ROOT = ${q}${ProjectRoot}${q}"
|
||
""
|
||
"# PS 5.1 + 传统控制台:启用 UTF-8 代码页 + 禁用 ANSI 颜色"
|
||
"if (`$PSVersionTable.PSVersion.Major -lt 7 -and -not `$env:WT_SESSION) {"
|
||
" chcp 65001 | Out-Null"
|
||
" `$env:NO_COLOR = ${q}1${q}"
|
||
" [Console]::OutputEncoding = [System.Text.Encoding]::UTF8"
|
||
" Write-Host ${q}[提示] PS 5.1 + 传统控制台,已启用 UTF-8 并禁用 ANSI 颜色${q} -ForegroundColor Yellow"
|
||
"}"
|
||
""
|
||
"Set-Location -LiteralPath ${q}${backendDir}${q}"
|
||
"Write-Host ${q}=== 后端 FastAPI ===${q} -ForegroundColor Green"
|
||
"Write-Host `"NEOZQYY_ROOT=`$env:NEOZQYY_ROOT`""
|
||
""
|
||
"# 调用 start_uvicorn.py(reload-excludes/timeout-graceful-shutdown 等参数硬编码在 Python 内)"
|
||
"& ${q}${venvPython}${q} ${q}${backendDir}\start_uvicorn.py${q} --port 8000"
|
||
"Write-Host ${q}后端已退出,按任意键关闭...${q} -ForegroundColor Red"
|
||
"`$null = `$Host.UI.RawUI.ReadKey(${q}NoEcho,IncludeKeyDown${q})"
|
||
)
|
||
$beLines | Set-Content -Path $beTmp -Encoding UTF8
|
||
|
||
$feLines = @(
|
||
"# PS 5.1 + 传统控制台:启用 UTF-8 + 禁用 ANSI"
|
||
"if (`$PSVersionTable.PSVersion.Major -lt 7 -and -not `$env:WT_SESSION) {"
|
||
" chcp 65001 | Out-Null"
|
||
" `$env:NO_COLOR = ${q}1${q}"
|
||
" [Console]::OutputEncoding = [System.Text.Encoding]::UTF8"
|
||
"}"
|
||
""
|
||
"Set-Location -LiteralPath ${q}${frontendDir}${q}"
|
||
"Write-Host ${q}=== 前端 Vite (admin-web) ===${q} -ForegroundColor Green"
|
||
"cmd /c ${q}pnpm dev 2>&1${q} | Tee-Object -FilePath ${q}${frontendLog}${q}"
|
||
"Write-Host ${q}前端已退出,按任意键关闭...${q} -ForegroundColor Red"
|
||
"`$null = `$Host.UI.RawUI.ReadKey(${q}NoEcho,IncludeKeyDown${q})"
|
||
)
|
||
$feLines | Set-Content -Path $feTmp -Encoding UTF8
|
||
|
||
$taLines = @(
|
||
"# PS 5.1 + 传统控制台:启用 UTF-8 + 禁用 ANSI"
|
||
"if (`$PSVersionTable.PSVersion.Major -lt 7 -and -not `$env:WT_SESSION) {"
|
||
" chcp 65001 | Out-Null"
|
||
" `$env:NO_COLOR = ${q}1${q}"
|
||
" [Console]::OutputEncoding = [System.Text.Encoding]::UTF8"
|
||
"}"
|
||
""
|
||
"Set-Location -LiteralPath ${q}${tenantDir}${q}"
|
||
"Write-Host ${q}=== 租户管理后台 Vite (tenant-admin) ===${q} -ForegroundColor Green"
|
||
"cmd /c ${q}pnpm dev 2>&1${q} | Tee-Object -FilePath ${q}${tenantLog}${q}"
|
||
"Write-Host ${q}租户管理后台已退出,按任意键关闭...${q} -ForegroundColor Red"
|
||
"`$null = `$Host.UI.RawUI.ReadKey(${q}NoEcho,IncludeKeyDown${q})"
|
||
)
|
||
$taLines | Set-Content -Path $taTmp -Encoding UTF8
|
||
|
||
# ── 启动服务函数 ──
|
||
function Start-AllServices {
|
||
param([ref]$BeProc, [ref]$FeProc, [ref]$TaProc)
|
||
|
||
Write-Host "[1/3] 启动后端 FastAPI (http://localhost:8000) ..." -ForegroundColor Yellow
|
||
$BeProc.Value = Start-Process $psExe -ArgumentList "-NoExit", "-ExecutionPolicy", "Bypass", "-File", $beTmp -PassThru
|
||
|
||
Start-Sleep -Seconds 2
|
||
|
||
Write-Host "[2/3] 启动前端 Vite admin-web (http://localhost:5173) ..." -ForegroundColor Yellow
|
||
$FeProc.Value = Start-Process $psExe -ArgumentList "-NoExit", "-ExecutionPolicy", "Bypass", "-File", $feTmp -PassThru
|
||
|
||
Start-Sleep -Seconds 1
|
||
|
||
Write-Host "[3/3] 启动租户管理后台 Vite tenant-admin (http://localhost:5174) ..." -ForegroundColor Yellow
|
||
$TaProc.Value = Start-Process $psExe -ArgumentList "-NoExit", "-ExecutionPolicy", "Bypass", "-File", $taTmp -PassThru
|
||
|
||
Write-Host ""
|
||
Write-Host "========================================" -ForegroundColor Green
|
||
Write-Host " 三个服务已在新窗口中启动" -ForegroundColor Green
|
||
Write-Host " 后端: http://localhost:8000" -ForegroundColor Green
|
||
Write-Host " 管理后台: http://localhost:5173" -ForegroundColor Green
|
||
Write-Host " 租户管理后台: http://localhost:5174" -ForegroundColor Green
|
||
Write-Host "========================================" -ForegroundColor Green
|
||
Write-Host ""
|
||
}
|
||
|
||
# ── 终止服务函数 ──
|
||
function Stop-AllServices {
|
||
param($BeProc, $FeProc, $TaProc)
|
||
|
||
Write-Host ""
|
||
Write-Host "正在终止所有服务..." -ForegroundColor Yellow
|
||
$ErrorActionPreference = "Continue"
|
||
foreach ($info in @(
|
||
@{ Name = "后端 FastAPI"; Proc = $BeProc; Port = 8000 },
|
||
@{ Name = "前端 admin-web"; Proc = $FeProc; Port = 5173 },
|
||
@{ Name = "租户 tenant-admin"; Proc = $TaProc; Port = 5174 }
|
||
)) {
|
||
$p = $info.Proc
|
||
if ($p -and -not $p.HasExited) {
|
||
Write-Host " 终止 $($info.Name) (PID=$($p.Id))..." -ForegroundColor Yellow
|
||
taskkill /PID $p.Id /T /F 2>$null | Out-Null
|
||
}
|
||
# 同时按端口清理(防止子进程残留)
|
||
$listeners = Get-NetTCPConnection -LocalPort $info.Port -State Listen -ErrorAction SilentlyContinue
|
||
foreach ($l in $listeners) {
|
||
taskkill /PID $l.OwningProcess /T /F 2>$null | Out-Null
|
||
}
|
||
}
|
||
$ErrorActionPreference = "Stop"
|
||
Start-Sleep -Seconds 1
|
||
Write-Host " 所有服务已终止" -ForegroundColor Green
|
||
}
|
||
|
||
# ── 等待前端就绪 + 打开浏览器 ──
|
||
function Wait-AndOpenBrowser {
|
||
Write-Host "等待前端 Vite 就绪..." -ForegroundColor Yellow
|
||
$timeout = 45
|
||
$elapsed = 0
|
||
$feReady = $false
|
||
$taReady = $false
|
||
|
||
while ($elapsed -lt $timeout -and (-not $feReady -or -not $taReady)) {
|
||
Start-Sleep -Seconds 1
|
||
$elapsed++
|
||
if (-not $feReady -and (Test-Path $frontendLog)) {
|
||
$raw = Get-Content $frontendLog -Raw -ErrorAction SilentlyContinue
|
||
if ($raw) {
|
||
$clean = $raw -replace '\x1b\[[0-9;]*m', ''
|
||
if ($clean -match "localhost:5173" -or $clean -match "ready in") {
|
||
$feReady = $true
|
||
Write-Host " admin-web 已就绪(${elapsed}s)" -ForegroundColor Green
|
||
}
|
||
}
|
||
}
|
||
if (-not $taReady -and (Test-Path $tenantLog)) {
|
||
$raw = Get-Content $tenantLog -Raw -ErrorAction SilentlyContinue
|
||
if ($raw) {
|
||
$clean = $raw -replace '\x1b\[[0-9;]*m', ''
|
||
if ($clean -match "localhost:5174" -or $clean -match "ready in") {
|
||
$taReady = $true
|
||
Write-Host " tenant-admin 已就绪(${elapsed}s)" -ForegroundColor Green
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if ($feReady) { Start-Process "http://localhost:5173" }
|
||
else { Write-Host "admin-web 等待超时,请手动打开 http://localhost:5173" -ForegroundColor Red }
|
||
if ($taReady) { Start-Process "http://localhost:5174" }
|
||
else { Write-Host "tenant-admin 等待超时,请手动打开 http://localhost:5174" -ForegroundColor Red }
|
||
}
|
||
|
||
# ── 看门狗启动/停止函数 ──
|
||
# CHANGE 2026-05-05 | F1-5a 走查发现 reload 卡死问题:
|
||
# 看门狗在独立窗口运行,周期探针 /health,卡死自动强杀+重启 backend。
|
||
function Start-Watchdog {
|
||
$watchdogScript = Join-Path $ProjectRoot "scripts\ops\backend-watchdog.ps1"
|
||
if (-not (Test-Path $watchdogScript)) {
|
||
Write-Host " !! 看门狗脚本不存在: $watchdogScript" -ForegroundColor Yellow
|
||
return $null
|
||
}
|
||
Write-Host "[守护] 启动后端看门狗 (后台监控,卡死自动重启) ..." -ForegroundColor DarkCyan
|
||
# WindowStyle Minimized:看门狗窗口最小化,不打扰主操作
|
||
$proc = Start-Process $psExe `
|
||
-ArgumentList "-NoExit", "-ExecutionPolicy", "Bypass", "-File", $watchdogScript, "-ProjectRoot", $ProjectRoot `
|
||
-WindowStyle Minimized -PassThru
|
||
return $proc
|
||
}
|
||
|
||
function Stop-Watchdog {
|
||
param($WdProc)
|
||
if ($WdProc -and -not $WdProc.HasExited) {
|
||
Write-Host " 终止看门狗 (PID=$($WdProc.Id))..." -ForegroundColor Yellow
|
||
taskkill /PID $WdProc.Id /T /F 2>$null | Out-Null
|
||
}
|
||
}
|
||
|
||
# ── 仅重启后端(保留前端) ──
|
||
# CHANGE 2026-05-05 | 测试场景下大多数改动只在后端,前端不需要重启;
|
||
# 此选项跳过前端避免 WS 重连 + 浏览器刷新成本
|
||
function Restart-BackendOnly {
|
||
param([ref]$BeProc)
|
||
$ErrorActionPreference = "Continue"
|
||
Write-Host ""
|
||
Write-Host "仅重启后端 (前端 admin-web/tenant-admin 保留)..." -ForegroundColor Yellow
|
||
if ($BeProc.Value -and -not $BeProc.Value.HasExited) {
|
||
Write-Host " 终止旧后端 (PID=$($BeProc.Value.Id))..." -ForegroundColor Yellow
|
||
taskkill /PID $BeProc.Value.Id /T /F 2>$null | Out-Null
|
||
}
|
||
# 兜底按端口清理
|
||
$listeners = Get-NetTCPConnection -LocalPort 8000 -State Listen -ErrorAction SilentlyContinue
|
||
foreach ($l in $listeners) {
|
||
taskkill /PID $l.OwningProcess /T /F 2>$null | Out-Null
|
||
}
|
||
# 等端口释放
|
||
$waited = 0
|
||
while ($waited -lt 15) {
|
||
Start-Sleep -Seconds 1
|
||
$waited++
|
||
$still = Get-NetTCPConnection -LocalPort 8000 -State Listen -ErrorAction SilentlyContinue
|
||
if (-not $still) { break }
|
||
}
|
||
$ErrorActionPreference = "Stop"
|
||
Write-Host " 端口 8000 已释放,启动新后端..." -ForegroundColor Green
|
||
$BeProc.Value = Start-Process $psExe -ArgumentList "-NoExit", "-ExecutionPolicy", "Bypass", "-File", $beTmp -PassThru
|
||
Write-Host " 新后端已启动 (PID=$($BeProc.Value.Id))" -ForegroundColor Green
|
||
}
|
||
|
||
# ── 倒计时函数 ──
|
||
function Show-Countdown {
|
||
param([int]$Seconds)
|
||
for ($i = $Seconds; $i -ge 1; $i--) {
|
||
Write-Host "`r 重启倒计时: ${i}s ... " -NoNewline -ForegroundColor Cyan
|
||
Start-Sleep -Seconds 1
|
||
}
|
||
Write-Host "`r 重启倒计时: 0s — 开始启动! " -ForegroundColor Green
|
||
Write-Host ""
|
||
}
|
||
|
||
# ── 首次启动 ──
|
||
$beProc = $null; $feProc = $null; $taProc = $null
|
||
$watchdogProc = $null
|
||
Start-AllServices -BeProc ([ref]$beProc) -FeProc ([ref]$feProc) -TaProc ([ref]$taProc)
|
||
# CHANGE 2026-05-05 | 启动看门狗(独立窗口,最小化运行)
|
||
$watchdogProc = Start-Watchdog
|
||
Wait-AndOpenBrowser
|
||
|
||
# ── 交互菜单循环 ──
|
||
$running = $true
|
||
while ($running) {
|
||
Write-Host ""
|
||
Write-Host "========================================" -ForegroundColor Cyan
|
||
Write-Host " 操作菜单" -ForegroundColor Cyan
|
||
Write-Host "========================================" -ForegroundColor Cyan
|
||
Write-Host " [1] 终止所有服务" -ForegroundColor Yellow
|
||
Write-Host " [2] 重启所有服务(间隔 5 秒倒计时)" -ForegroundColor Yellow
|
||
Write-Host " [3] 退出(终止服务并关闭窗口)" -ForegroundColor Yellow
|
||
Write-Host " [4] 仅重启后端(保留前端,推荐:测试时 reload 卡死/Python 改动)" -ForegroundColor Yellow
|
||
Write-Host "========================================" -ForegroundColor Cyan
|
||
$choice = Read-Host "请输入选项 (1/2/3/4)"
|
||
|
||
switch ($choice) {
|
||
"1" {
|
||
# CHANGE 2026-05-05 | [1] 全停 = 服务 + 看门狗都停(否则看门狗会自动拉起后端)
|
||
Stop-Watchdog -WdProc $watchdogProc
|
||
$watchdogProc = $null
|
||
Stop-AllServices -BeProc $beProc -FeProc $feProc -TaProc $taProc
|
||
$beProc = $null; $feProc = $null; $taProc = $null
|
||
}
|
||
"2" {
|
||
# CHANGE 2026-05-05 | 重启时先停看门狗,避免它在停-启间隙误判卡死
|
||
Stop-Watchdog -WdProc $watchdogProc
|
||
$watchdogProc = $null
|
||
Stop-AllServices -BeProc $beProc -FeProc $feProc -TaProc $taProc
|
||
Show-Countdown -Seconds 5
|
||
# 重新生成日志文件名(避免旧文件锁定)
|
||
$ts = Get-Date -Format "yyyyMMdd_HHmmss"
|
||
$frontendLog = Join-Path $env:TEMP "neozqyy_fe_${ts}.log"
|
||
$tenantLog = Join-Path $env:TEMP "neozqyy_ta_${ts}.log"
|
||
# 重新生成临时启动脚本
|
||
$beLines = @(
|
||
"`$env:NEOZQYY_ROOT = ${q}${ProjectRoot}${q}"
|
||
""
|
||
"if (`$PSVersionTable.PSVersion.Major -lt 7 -and -not `$env:WT_SESSION) {"
|
||
" chcp 65001 | Out-Null"
|
||
" `$env:NO_COLOR = ${q}1${q}"
|
||
" [Console]::OutputEncoding = [System.Text.Encoding]::UTF8"
|
||
" Write-Host ${q}[提示] PS 5.1 + 传统控制台,已启用 UTF-8 并禁用 ANSI 颜色${q} -ForegroundColor Yellow"
|
||
"}"
|
||
""
|
||
"Set-Location -LiteralPath ${q}${backendDir}${q}"
|
||
"Write-Host ${q}=== 后端 FastAPI ===${q} -ForegroundColor Green"
|
||
"Write-Host `"NEOZQYY_ROOT=`$env:NEOZQYY_ROOT`""
|
||
""
|
||
"& ${q}${venvPython}${q} ${q}${backendDir}\start_uvicorn.py${q} --port 8000"
|
||
"Write-Host ${q}后端已退出,按任意键关闭...${q} -ForegroundColor Red"
|
||
"`$null = `$Host.UI.RawUI.ReadKey(${q}NoEcho,IncludeKeyDown${q})"
|
||
)
|
||
$beLines | Set-Content -Path $beTmp -Encoding UTF8
|
||
$feLines = @(
|
||
"if (`$PSVersionTable.PSVersion.Major -lt 7 -and -not `$env:WT_SESSION) {"
|
||
" chcp 65001 | Out-Null"
|
||
" `$env:NO_COLOR = ${q}1${q}"
|
||
" [Console]::OutputEncoding = [System.Text.Encoding]::UTF8"
|
||
"}"
|
||
""
|
||
"Set-Location -LiteralPath ${q}${frontendDir}${q}"
|
||
"Write-Host ${q}=== 前端 Vite (admin-web) ===${q} -ForegroundColor Green"
|
||
"cmd /c ${q}pnpm dev 2>&1${q} | Tee-Object -FilePath ${q}${frontendLog}${q}"
|
||
"Write-Host ${q}前端已退出,按任意键关闭...${q} -ForegroundColor Red"
|
||
"`$null = `$Host.UI.RawUI.ReadKey(${q}NoEcho,IncludeKeyDown${q})"
|
||
)
|
||
$feLines | Set-Content -Path $feTmp -Encoding UTF8
|
||
$taLines = @(
|
||
"if (`$PSVersionTable.PSVersion.Major -lt 7 -and -not `$env:WT_SESSION) {"
|
||
" chcp 65001 | Out-Null"
|
||
" `$env:NO_COLOR = ${q}1${q}"
|
||
" [Console]::OutputEncoding = [System.Text.Encoding]::UTF8"
|
||
"}"
|
||
""
|
||
"Set-Location -LiteralPath ${q}${tenantDir}${q}"
|
||
"Write-Host ${q}=== 租户管理后台 Vite (tenant-admin) ===${q} -ForegroundColor Green"
|
||
"cmd /c ${q}pnpm dev 2>&1${q} | Tee-Object -FilePath ${q}${tenantLog}${q}"
|
||
"Write-Host ${q}租户管理后台已退出,按任意键关闭...${q} -ForegroundColor Red"
|
||
"`$null = `$Host.UI.RawUI.ReadKey(${q}NoEcho,IncludeKeyDown${q})"
|
||
)
|
||
$taLines | Set-Content -Path $taTmp -Encoding UTF8
|
||
|
||
$beProc = $null; $feProc = $null; $taProc = $null
|
||
Start-AllServices -BeProc ([ref]$beProc) -FeProc ([ref]$feProc) -TaProc ([ref]$taProc)
|
||
# CHANGE 2026-05-05 | 重启完后重新启动看门狗
|
||
$watchdogProc = Start-Watchdog
|
||
Wait-AndOpenBrowser
|
||
}
|
||
"3" {
|
||
Stop-AllServices -BeProc $beProc -FeProc $feProc -TaProc $taProc
|
||
Stop-Watchdog -WdProc $watchdogProc
|
||
$watchdogProc = $null
|
||
$running = $false
|
||
}
|
||
"4" {
|
||
Restart-BackendOnly -BeProc ([ref]$beProc)
|
||
# 看门狗保持运行(它会在重启 grace 期间不探针)
|
||
}
|
||
default {
|
||
Write-Host " 无效选项,请输入 1、2、3 或 4" -ForegroundColor Red
|
||
}
|
||
}
|
||
}
|
||
|
||
} catch {
|
||
Write-Host ""
|
||
Write-Host "启动失败: $_" -ForegroundColor Red
|
||
Write-Host $_.ScriptStackTrace -ForegroundColor DarkRed
|
||
}
|
||
|
||
# CHANGE 2026-05-05 | 主窗口退出前兜底 kill 看门狗,防止主窗口被强关时看门狗成孤儿进程
|
||
if ($watchdogProc -and -not $watchdogProc.HasExited) {
|
||
Write-Host " 兜底终止看门狗 (PID=$($watchdogProc.Id))..." -ForegroundColor DarkGray
|
||
taskkill /PID $watchdogProc.Id /T /F 2>$null | Out-Null
|
||
}
|
||
|
||
Write-Host ""
|
||
Write-Host "按任意键关闭此窗口..." -ForegroundColor DarkGray
|
||
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
|