| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205 |
- [CmdletBinding()]
- param(
- [string]$BackendBindAddress = "0.0.0.0",
- [int]$BackendPort = 8000,
- [string]$FrontendBindAddress = "0.0.0.0",
- [int]$FrontendPort = 5173,
- [string]$ApiBaseUrl = "",
- [switch]$SkipFrontend,
- [switch]$NoBrowser
- )
- $ErrorActionPreference = "Stop"
- $ProjectRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
- $RuntimeDir = Join-Path $ProjectRoot ".runtime"
- $LogDir = Join-Path $RuntimeDir "logs"
- $StatePath = Join-Path $RuntimeDir "processes.json"
- $PythonPath = Join-Path $ProjectRoot ".venv\Scripts\python.exe"
- $BackendDir = Join-Path $ProjectRoot "backend"
- $FrontendDir = Join-Path $ProjectRoot "frontend"
- $NodeModulesDir = Join-Path $FrontendDir "node_modules"
- function Get-ListeningProcessId {
- param([int]$Port)
- $connection = Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue |
- Select-Object -First 1
- if ($null -eq $connection) {
- return $null
- }
- return [int]$connection.OwningProcess
- }
- function Wait-ForPort {
- param(
- [int]$Port,
- [System.Diagnostics.Process]$Launcher,
- [int]$TimeoutSeconds = 30
- )
- $deadline = (Get-Date).AddSeconds($TimeoutSeconds)
- while ((Get-Date) -lt $deadline) {
- $listenerPid = Get-ListeningProcessId -Port $Port
- if ($null -ne $listenerPid) {
- return $listenerPid
- }
- if ($Launcher.HasExited) {
- throw "启动进程已退出,退出码:$($Launcher.ExitCode)"
- }
- Start-Sleep -Milliseconds 300
- $Launcher.Refresh()
- }
- throw "等待端口 $Port 监听超时(${TimeoutSeconds} 秒)"
- }
- function Stop-ProcessTree {
- param([int]$RootProcessId)
- if ($RootProcessId -le 0) {
- return
- }
- # 先结束子进程,避免 npm/cmd 退出后留下独立的 Vite 进程。
- $pending = [System.Collections.Generic.List[int]]::new()
- $pending.Add($RootProcessId)
- $allProcessIds = [System.Collections.Generic.List[int]]::new()
- while ($pending.Count -gt 0) {
- $currentProcessId = $pending[0]
- $pending.RemoveAt(0)
- $allProcessIds.Add($currentProcessId)
- Get-CimInstance Win32_Process -Filter "ParentProcessId = $currentProcessId" -ErrorAction SilentlyContinue |
- ForEach-Object { $pending.Add([int]$_.ProcessId) }
- }
- $orderedProcessIds = @($allProcessIds)
- [array]::Reverse($orderedProcessIds)
- foreach ($processId in $orderedProcessIds) {
- Stop-Process -Id $processId -Force -ErrorAction SilentlyContinue
- }
- }
- if (-not (Test-Path -LiteralPath $PythonPath -PathType Leaf)) {
- throw "未找到 Python 虚拟环境:$PythonPath。请先执行 python -m venv .venv 并安装后端依赖。"
- }
- if (-not (Test-Path -LiteralPath $BackendDir -PathType Container)) {
- throw "未找到后端目录:$BackendDir"
- }
- if (-not $SkipFrontend) {
- if (-not (Get-Command npm.cmd -ErrorAction SilentlyContinue)) {
- throw "未找到 npm.cmd,请先安装 Node.js 并确认 npm 已加入 PATH。"
- }
- if (-not (Test-Path -LiteralPath $NodeModulesDir -PathType Container)) {
- throw "未找到前端依赖目录:$NodeModulesDir。请先在 frontend 目录执行 npm install。"
- }
- }
- $portsToCheck = @($BackendPort)
- if (-not $SkipFrontend) {
- $portsToCheck += $FrontendPort
- }
- foreach ($port in $portsToCheck) {
- if ($null -ne (Get-ListeningProcessId -Port $port)) {
- throw "端口 $port 已被占用。请先关闭占用进程,或通过参数指定其他端口。"
- }
- }
- New-Item -ItemType Directory -Path $LogDir -Force | Out-Null
- if (Test-Path -LiteralPath $StatePath) {
- Remove-Item -LiteralPath $StatePath -Force
- }
- $backendProcess = $null
- $frontendProcess = $null
- try {
- Write-Host "正在启动后端:http://127.0.0.1:$BackendPort" -ForegroundColor Cyan
- $backendProcess = Start-Process `
- -FilePath $PythonPath `
- -ArgumentList @("-m", "uvicorn", "app.main:app", "--host", $BackendBindAddress, "--port", $BackendPort) `
- -WorkingDirectory $BackendDir `
- -WindowStyle Hidden `
- -RedirectStandardOutput (Join-Path $LogDir "backend.out.log") `
- -RedirectStandardError (Join-Path $LogDir "backend.err.log") `
- -PassThru
- $backendListenerPid = Wait-ForPort -Port $BackendPort -Launcher $backendProcess
- $frontendListenerPid = $null
- if (-not $SkipFrontend) {
- Write-Host "正在启动前端:http://127.0.0.1:$FrontendPort" -ForegroundColor Cyan
- $originalApiBaseUrl = $env:VITE_API_BASE
- try {
- if (-not [string]::IsNullOrWhiteSpace($ApiBaseUrl)) {
- $env:VITE_API_BASE = $ApiBaseUrl.TrimEnd("/")
- } elseif ($BackendPort -ne 8000) {
- # 自定义后端端口时,为本机访问自动同步前端 API 地址。
- $env:VITE_API_BASE = "http://127.0.0.1:$BackendPort"
- }
- $frontendProcess = Start-Process `
- -FilePath "npm.cmd" `
- -ArgumentList @("run", "dev", "--", "--host", $FrontendBindAddress, "--port", $FrontendPort) `
- -WorkingDirectory $FrontendDir `
- -WindowStyle Hidden `
- -RedirectStandardOutput (Join-Path $LogDir "frontend.out.log") `
- -RedirectStandardError (Join-Path $LogDir "frontend.err.log") `
- -PassThru
- } finally {
- if ($null -eq $originalApiBaseUrl) {
- Remove-Item Env:VITE_API_BASE -ErrorAction SilentlyContinue
- } else {
- $env:VITE_API_BASE = $originalApiBaseUrl
- }
- }
- $frontendListenerPid = Wait-ForPort -Port $FrontendPort -Launcher $frontendProcess
- }
- $state = [ordered]@{
- started_at = (Get-Date).ToString("o")
- project_root = $ProjectRoot
- backend = [ordered]@{
- launcher_pid = $backendProcess.Id
- listener_pid = $backendListenerPid
- bind_address = $BackendBindAddress
- port = $BackendPort
- }
- frontend = if ($SkipFrontend) {
- $null
- } else {
- [ordered]@{
- launcher_pid = $frontendProcess.Id
- listener_pid = $frontendListenerPid
- bind_address = $FrontendBindAddress
- port = $FrontendPort
- api_base_url = if (-not [string]::IsNullOrWhiteSpace($ApiBaseUrl)) {
- $ApiBaseUrl.TrimEnd("/")
- } elseif ($BackendPort -ne 8000) {
- "http://127.0.0.1:$BackendPort"
- } else {
- $null
- }
- }
- }
- }
- $state | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath $StatePath -Encoding UTF8
- Write-Host "项目启动成功。" -ForegroundColor Green
- Write-Host "后端:http://127.0.0.1:$BackendPort"
- if (-not $SkipFrontend) {
- $frontendUrl = "http://127.0.0.1:$FrontendPort"
- Write-Host "前端:$frontendUrl"
- if (-not $NoBrowser) {
- Start-Process $frontendUrl
- }
- }
- Write-Host "日志目录:$LogDir"
- Write-Host "关闭项目:.\stop.ps1"
- } catch {
- Write-Host "启动失败:$($_.Exception.Message)" -ForegroundColor Red
- if ($null -ne $frontendProcess) {
- Stop-ProcessTree -RootProcessId $frontendProcess.Id
- }
- if ($null -ne $backendProcess) {
- Stop-ProcessTree -RootProcessId $backendProcess.Id
- }
- Remove-Item -LiteralPath $StatePath -Force -ErrorAction SilentlyContinue
- throw
- }
|