<# .SYNOPSIS CLIP 向量服务(fashion-siglip)的独立起停脚本。 .DESCRIPTION 该服务把图片编码成归一化向量,供后台「疑似同款」提示使用。 - 只监听 127.0.0.1,不对外暴露 - --workers 必须为 1(模型常驻进程内,多 worker 会重复加载模型) - 后端需要 VISION_ENABLED=true 才会调用它;服务不可用时后端自动降级,不影响入库 .EXAMPLE .\vision\vision.ps1 start .\vision\vision.ps1 status .\vision\vision.ps1 logs .\vision\vision.ps1 stop #> param( [Parameter(Position = 0)] [ValidateSet('start', 'stop', 'restart', 'status', 'logs')] [string]$Action = 'start', [switch]$Force ) $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 $port = [int](Get-Setting $envMap 'VISION_PORT' '8900') $healthUrl = "http://127.0.0.1:$port/health" function Start-CLIP { if (Test-PortInUse $port) { $owner = Get-PortOwnerPid $port $saved = Get-ServicePid 'vision' if ($saved -gt 0 -and $saved -eq $owner) { Write-Note "视觉服务已在运行(PID $owner)" return $true } if (-not $Force) { Write-Fail "端口 $port 被 PID $owner 占用" Write-Note '加 -Force 强制释放:.\vision\vision.ps1 start -Force' return $false } Stop-PidTree $owner | Out-Null } if (-not $svc.Vision.IsValid) { Write-Fail "视觉服务目录无效:$($svc.Vision.Path)" return $false } $uvicorn = Join-Path $svc.Vision.Path '.venv\Scripts\uvicorn.exe' if (-not (Test-Path $uvicorn)) { Write-Fail "找不到 $uvicorn" Write-Note "在 $($svc.Vision.Path) 建虚拟环境:python -m venv .venv && .venv\Scripts\pip install -r requirements.txt" return $false } Write-Step "启动视觉服务(端口 $port,首次加载模型较慢)" Start-LoggedProcess -Name 'vision' -FilePath $uvicorn ` -ArgumentList @('app:app', '--host', '127.0.0.1', '--port', "$port", '--workers', '1') ` -WorkingDirectory $svc.Vision.Path | Out-Null if (Wait-HttpOk -Url $healthUrl -TimeoutSec 180) { Write-Ok '视觉服务就绪' Show-Health return $true } Write-Fail '视觉服务 180 秒内未就绪' Show-LogTail -Name 'vision' -Lines 25 return $false } function Show-Health { try { $r = Invoke-WebRequest -Uri $healthUrl -UseBasicParsing -TimeoutSec 5 -ErrorAction Stop Write-Note ("health: " + $r.Content.Trim()) } catch { } } Write-Host '' Write-Host ' app-cli / CLIP 视觉服务' -ForegroundColor White Write-Host ' ----------------------------------------' -ForegroundColor DarkGray switch ($Action) { 'start' { if (-not (Start-CLIP)) { exit 1 } } 'stop' { if (Stop-NamedService 'vision') { Write-Ok '视觉服务已停止' } else { Write-Note '视觉服务未在本脚本登记(可能没启动过)' } if (Test-PortInUse $port) { $owner = Get-PortOwnerPid $port Write-Warn "端口 $port 仍被 PID $owner 占用" if ($Force) { Stop-PidTree $owner | Out-Null; Write-Ok "已终止 PID $owner" } else { Write-Note '加 -Force 强制终止:.\vision\vision.ps1 stop -Force' } } } 'restart' { Stop-NamedService 'vision' | Out-Null Start-Sleep -Milliseconds 500 if (-not (Start-CLIP)) { exit 1 } } 'status' { $st = Get-ServiceStatus -Name 'vision' -Port $port -HealthUrl $healthUrl if ($st.Healthy) { Write-Ok "运行中(PID $($st.OwnerPid),端口 $port)" Show-Health } elseif ($st.Listening) { Write-Warn "端口 $port 已监听但 /health 未通过(模型可能仍在加载)" Show-LogTail -Name 'vision' -Lines 10 } else { Write-Note '未运行' } } 'logs' { Show-LogTail -Name 'vision' -Lines 40 } }