- dev/dev.ps1: 一键启动后端(编译后运行)+前端(4321),校验 CORS 与 astro dev 单实例锁 - dev/stop.ps1: 按 PID 登记停止,-Force 清残留 - dev/status.ps1: 端口/PID/健康检查一览 - dev/doctor.ps1: 工具链/路径/依赖/数据库/端口/CORS 体检 - vision/vision.ps1: CLIP 向量服务起停 - lib/common.ps1: 路径探测、端口、进程登记、HTTP 探测 - config/local.env.example: 路径与端口覆盖模板
191 lines
8.2 KiB
PowerShell
191 lines
8.2 KiB
PowerShell
# 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 '体检全部通过'
|