# dev/doctor.ps1 — 本地环境体检 # # 只读检查,不改任何东西。把「启动后才发现」的问题提前暴露: # 工具链版本 / 仓库路径 / 配置文件 / 依赖安装 / 数据库连通 / 端口占用 / CORS 白名单 # # 退出码:0 = 可以开工;1 = 有必须先处理的问题。 $ErrorActionPreference = 'Stop' $CliRoot = Split-Path -Parent $PSScriptRoot . (Join-Path $CliRoot 'lib\common.ps1') $envMap = Import-LocalEnv (Join-Path $CliRoot 'config\local.env') $svc = Resolve-Services -EnvMap $envMap -Root $CliRoot $backendPort = [int](Get-Setting $envMap 'BACKEND_PORT' '8090') $frontendPort = [int](Get-Setting $envMap 'FRONTEND_PORT' '4321') $visionPort = [int](Get-Setting $envMap 'VISION_PORT' '8900') $script:Fail = 0 $script:Warn = 0 # Check:$Value 只在通过时显示(版本号、路径等);$Hint 只在未通过时显示(怎么修)。 function Check { param( [string]$Name, [bool]$Ok, [string]$Value = '', [string]$Hint = '', [bool]$Fatal = $true ) if ($Ok) { if ($Value -ne '') { Write-Ok "$Name($Value)" } else { Write-Ok $Name } return } if ($Fatal) { Write-Fail $Name $script:Fail++ } else { Write-Warn $Name $script:Warn++ } if ($Hint -ne '') { Write-Note " $Hint" } } Write-Host '' Write-Host ' app-cli / 本地环境体检' -ForegroundColor White Write-Host ' ----------------------------------------' -ForegroundColor DarkGray # ---------------------------------------------------------------- 1. 工具链 Write-Step '工具链' $goCmd = Get-Command go -ErrorAction SilentlyContinue if ($goCmd) { $goVer = (& go version) -replace 'go version ', '' -replace ' windows.*', '' Check -Name 'Go' -Ok $true -Value $goVer } else { Check -Name 'Go' -Ok $false -Hint '未安装,后端无法编译' } $nodeCmd = Get-Command node -ErrorAction SilentlyContinue if ($nodeCmd) { $nodeVer = (& node -v).Trim() $major = 0 if ($nodeVer -match 'v(\d+)') { $major = [int]$Matches[1] } # 前端 package.json engines 要求 >=22.12,只做提示不拦 Check -Name 'Node' -Ok ($major -ge 22) -Value $nodeVer -Hint "当前 $nodeVer,前端要求 >=22.12" -Fatal $false } else { Check -Name 'Node' -Ok $false -Hint '未安装,前端无法启动' } $pythonCmd = Get-Command python -ErrorAction SilentlyContinue if ($pythonCmd) { $pyVer = (& python --version 2>&1).ToString().Trim() Check -Name 'Python' -Ok $true -Value $pyVer } else { Check -Name 'Python' -Ok $false -Hint '未安装(只有跑视觉服务才需要)' -Fatal $false } # ---------------------------------------------------------------- 2. 仓库路径 Write-Step '仓库路径' Check -Name "backend $($svc.Backend.Path)" -Ok $svc.Backend.IsValid -Hint "缺标记文件 $($svc.Backend.Marker)" Check -Name "frontend $($svc.Frontend.Path)" -Ok $svc.Frontend.IsValid -Hint "缺标记文件 $($svc.Frontend.Marker)" Check -Name "vision $($svc.Vision.Path)" -Ok $svc.Vision.IsValid -Hint "缺标记文件 $($svc.Vision.Marker),仅影响视觉服务" -Fatal $false # ---------------------------------------------------------------- 3. 配置与依赖 Write-Step '配置与依赖' if ($svc.Backend.IsValid) { $cfgYml = Join-Path $svc.Backend.Path 'configs\config.yml' $cfgLocal = Join-Path $svc.Backend.Path 'configs\config.local.yml' Check -Name 'backend configs/config.yml' -Ok (Test-Path $cfgYml) -Hint '唯一配置源,缺失则全走默认值' Check -Name 'backend configs/config.local.yml' -Ok (Test-Path $cfgLocal) -Hint '本机密钥(DB 密码 / JWT / S4),缺失会在启动时交互提示' -Fatal $false } if ($svc.Frontend.IsValid) { $nm = Join-Path $svc.Frontend.Path 'node_modules' $astro = Join-Path $svc.Frontend.Path 'node_modules\astro\bin\astro.mjs' Check -Name 'frontend node_modules' -Ok (Test-Path $nm) -Hint '缺失时 dev.ps1 会自动 npm ci' -Fatal $false Check -Name 'frontend astro CLI' -Ok (Test-Path $astro) -Hint "缺 $astro(依赖损坏,删掉 node_modules 重装)" # 前端 dev 端口的两处来源必须一致,否则手动 npm run dev 会起在别的端口 → CORS 拦截 $astroCfg = Join-Path $svc.Frontend.Path 'astro.config.mjs' if (Test-Path $astroCfg) { $raw = Get-Content $astroCfg -Raw if ($raw -match 'port:\s*(\d+)') { $cfgPort = [int]$Matches[1] Check -Name '前端端口一致性' -Ok ($cfgPort -eq $frontendPort) -Value "$cfgPort" ` -Hint "astro.config.mjs 的 server.port=$cfgPort,而 FRONTEND_PORT=$frontendPort;dev.ps1 会用 --port 覆盖,但你手动 npm run dev 会起在 $cfgPort 并被 CORS 拦截" ` -Fatal $false } } } if ($svc.Vision.IsValid) { $uvicorn = Join-Path $svc.Vision.Path '.venv\Scripts\uvicorn.exe' Check -Name 'vision .venv/uvicorn' -Ok (Test-Path $uvicorn) -Hint '缺失则无法启动 CLIP 服务(python -m venv .venv 后装依赖)' -Fatal $false } # ---------------------------------------------------------------- 4. 数据库连通 Write-Step '数据库' if ($svc.Backend.IsValid) { $cfgYml = Join-Path $svc.Backend.Path 'configs\config.yml' $cfgLocal = Join-Path $svc.Backend.Path 'configs\config.local.yml' $dbHost = Get-YamlScalar -Path $cfgYml -Section 'database' -Key 'host' $dbPortRaw = Get-YamlScalar -Path $cfgYml -Section 'database' -Key 'port' $dbName = Get-YamlScalar -Path $cfgYml -Section 'database' -Key 'name' # 本机覆盖优先 $localHost = Get-YamlScalar -Path $cfgLocal -Section 'database' -Key 'host' if ($localHost -ne '') { $dbHost = $localHost } $localPort = Get-YamlScalar -Path $cfgLocal -Section 'database' -Key 'port' if ($localPort -ne '') { $dbPortRaw = $localPort } $dbPort = 5432 if ($dbPortRaw -ne '') { $dbPort = [int]$dbPortRaw } if ($dbHost -eq '') { Check -Name '数据库配置' -Ok $false -Hint '配置里没读到 database.host' -Fatal $false } else { $reachable = Test-TcpConnect -ComputerName $dbHost -Port $dbPort -TimeoutMs 4000 Check -Name "PostgreSQL $dbHost`:$dbPort/$dbName" -Ok $reachable ` -Hint 'TCP 连不上:后端启动会直接退出(查网络 / 密码 / 远端是否放行本机 IP)' } } # ---------------------------------------------------------------- 5. 端口 Write-Step '端口' $ports = @( @{ Port = $backendPort; Name = 'backend 公开' }, @{ Port = 8091; Name = 'backend SSG' }, @{ Port = 8092; Name = 'backend 后台' }, @{ Port = $frontendPort; Name = 'frontend dev' }, @{ Port = $visionPort; Name = 'vision' } ) foreach ($p in $ports) { if (Test-PortInUse $p.Port) { $owner = Get-PortOwnerPid $p.Port Write-Note ($p.Port.ToString() + ' ' + $p.Name + ' 已被 PID ' + $owner + ' 占用(若已在跑属正常)') } else { Write-Note ($p.Port.ToString() + ' ' + $p.Name + ' 空闲') } } # ---------------------------------------------------------------- 6. 运行时自检 Write-Step '运行时自检' if (Test-PortInUse $backendPort) { $healthUrl = "http://127.0.0.1:$backendPort/api/health" Check -Name '后端 /api/health' -Ok (Wait-HttpOk -Url $healthUrl -TimeoutSec 3) -Value $healthUrl -Fatal $false $origin = "http://localhost:$frontendPort" $acao = Get-CorsOrigin -Url "http://127.0.0.1:$backendPort/api/v1/public/runway-looks?page=1" -Origin $origin if ($acao -eq $origin) { Check -Name "CORS 放行 $origin" -Ok $true -Value $acao } else { Check -Name "CORS 放行 $origin" -Ok $false -Fatal $false ` -Hint "后端回显 '$acao';不在白名单时浏览器会拦掉接口,页面显示「无数据」。改 backend/configs/config.local.yml → cors.allow_origins" } } else { Write-Note '后端未运行,跳过健康与 CORS 检查' } # ---------------------------------------------------------------- 汇总 Write-Host '' if ($script:Fail -gt 0) { Write-Fail ("体检未通过:$($script:Fail) 项必处理,$($script:Warn) 项提醒") exit 1 } if ($script:Warn -gt 0) { Write-Warn ("可以开工,但有 $($script:Warn) 项提醒") exit 0 } Write-Ok '体检全部通过'