Files
Neo-ZQYY/scripts/ops/start-admin.ps1
Neo 95a4500c75 chore(ops): reload 卡死三层预防 + F1-5a 完整走查报告
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>
2026-05-05 11:53:08 +08:00

487 lines
25 KiB
PowerShell
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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")