chore: 初始化 app-cli,本地开发环境脚本

- 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: 路径与端口覆盖模板
This commit is contained in:
toom1996
2026-10-22 10:37:59 +08:00
commit e366ee7498
10 changed files with 1269 additions and 0 deletions

190
dev/doctor.ps1 Normal file
View File

@ -0,0 +1,190 @@
# 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 '体检全部通过'