Files
Neo-ZQYY/scripts/ops/start-admin.ps1

211 lines
10 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"
try {
# CHANGE 2026-03-07 | 定位项目根目录:从 bat 启动目录推算,不穿透 junction
# 背景C:\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"
if (-not (Test-Path $backendDir)) { throw "后端目录不存在: $backendDir" }
if (-not (Test-Path $frontendDir)) { throw "前端目录不存在: $frontendDir" }
# ── 端口冲突检测:确保 8000/5173 未被旧进程占用 ──
foreach ($port in @(8000, 5173)) {
$listeners = Get-NetTCPConnection -LocalPort $port -State Listen -ErrorAction SilentlyContinue
if ($listeners) {
foreach ($l in $listeners) {
$pid = $l.OwningProcess
$proc = Get-Process -Id $pid -ErrorAction SilentlyContinue
$procPath = if ($proc) { $proc.Path } else { "未知" }
Write-Host "端口 $port 被占用: PID=$pid ($procPath)" -ForegroundColor Yellow
# 尝试多种方式终止Stop-Process → taskkill /T /F
Write-Host " 正在终止旧进程..." -ForegroundColor Yellow
Stop-Process -Id $pid -Force -ErrorAction SilentlyContinue
Start-Sleep -Seconds 1
# 如果 Stop-Process 不够,用 taskkill 连子进程一起杀
# taskkill 非零退出码不应终止脚本
$ErrorActionPreference = "Continue"
taskkill /PID $pid /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"
# 选择 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"
$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不受影响。
@(
"`$env:NEOZQYY_ROOT = ${q}${ProjectRoot}${q}"
""
"# pwsh 7+ 原生支持 ANSIPS 5.1 需要 Windows Terminal 才行"
"# 简单判断PSVersion.Major < 7 且不在 Windows Terminal 中 → 禁用颜色"
"if (`$PSVersionTable.PSVersion.Major -lt 7 -and -not `$env:WT_SESSION) {"
" `$env:NO_COLOR = ${q}1${q}"
" Write-Host ${q}[提示] PS 5.1 + 传统控制台,已禁用 ANSI 颜色${q} -ForegroundColor Yellow"
"}"
""
"Set-Location -LiteralPath ${q}${backendDir}${q}"
"Write-Host ${q}=== 后端 FastAPI ===${q} -ForegroundColor Green"
"Write-Host ${q}NEOZQYY_ROOT=`$env:NEOZQYY_ROOT${q}"
"& ${q}${venvPython}${q} -m uvicorn app.main:app --reload --port 8000"
"Write-Host ${q}后端已退出,按任意键关闭...${q} -ForegroundColor Red"
"`$null = `$Host.UI.RawUI.ReadKey(${q}NoEcho,IncludeKeyDown${q})"
) | Set-Content -Path $beTmp -Encoding UTF8
@(
"Set-Location -LiteralPath ${q}${frontendDir}${q}"
"Write-Host ${q}=== 前端 Vite ===${q} -ForegroundColor Green"
"pnpm dev 2>&1 | Tee-Object -FilePath ${q}${frontendLog}${q}"
"Write-Host ${q}前端已退出,按任意键关闭...${q} -ForegroundColor Red"
"`$null = `$Host.UI.RawUI.ReadKey(${q}NoEcho,IncludeKeyDown${q})"
) | Set-Content -Path $feTmp -Encoding UTF8
# ── 启动后端 ──
Write-Host "[1/2] 启动后端 FastAPI (http://localhost:8000) ..." -ForegroundColor Yellow
Start-Process $psExe -ArgumentList "-NoExit", "-ExecutionPolicy", "Bypass", "-File", $beTmp
Start-Sleep -Seconds 2
# ── 启动前端 ──
Write-Host "[2/2] 启动前端 Vite (http://localhost:5173) ..." -ForegroundColor Yellow
Start-Process $psExe -ArgumentList "-NoExit", "-ExecutionPolicy", "Bypass", "-File", $feTmp
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 "========================================" -ForegroundColor Green
Write-Host ""
# ── 检测前端就绪(匹配 Vite 输出中的 localhost:5173忽略 ANSI 转义码) ──
Write-Host "等待前端 Vite 就绪..." -ForegroundColor Yellow
$timeout = 45
$elapsed = 0
$ready = $false
while ($elapsed -lt $timeout) {
Start-Sleep -Seconds 1
$elapsed++
if (Test-Path $frontendLog) {
$raw = Get-Content $frontendLog -Raw -ErrorAction SilentlyContinue
if ($raw) {
# 去掉 ANSI 转义序列后再匹配
$clean = $raw -replace '\x1b\[[0-9;]*m', ''
if ($clean -match "localhost:5173" -or $clean -match "ready in") {
$ready = $true
break
}
}
}
}
if ($ready) {
Write-Host "前端已就绪(${elapsed}s打开浏览器..." -ForegroundColor Green
Start-Process "http://localhost:5173"
} else {
Write-Host "等待超时(${timeout}s请手动打开 http://localhost:5173" -ForegroundColor Red
}
} catch {
Write-Host ""
Write-Host "启动失败: $_" -ForegroundColor Red
Write-Host $_.ScriptStackTrace -ForegroundColor DarkRed
}
Write-Host ""
Write-Host "按任意键关闭此窗口..." -ForegroundColor DarkGray
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")