493 lines
19 KiB
PowerShell
493 lines
19 KiB
PowerShell
<#
|
||
.SYNOPSIS
|
||
本地一键启动开发环境:Go 后端 + Astro 前端(可选 CLIP 视觉服务)。
|
||
|
||
.DESCRIPTION
|
||
- 后端:编译后直接运行二进制(不走 go run,进程可精确管理),监听 8090/8091/8092
|
||
- 前端:node 直接拉起 astro dev,端口固定 4321(与后端 CORS 白名单一致)
|
||
- 可选:-Vision 同时拉起 fashion-siglip 向量服务,并给后端注入 VISION_ENABLED=true
|
||
- 日志写到 .logs\,PID 登记到 .run\,随时可用 dev\stop.ps1 一键关停
|
||
- 启动完成后自检 CORS:前端端口不在白名单里会直接告警(这是「页面无数据」的头号原因)
|
||
|
||
.EXAMPLE
|
||
.\dev\dev.ps1
|
||
起后端 + 前端
|
||
|
||
.EXAMPLE
|
||
.\dev\dev.ps1 -Vision -Open
|
||
连视觉服务一起起,并自动打开浏览器
|
||
|
||
.EXAMPLE
|
||
.\dev\dev.ps1 frontend
|
||
只起前端(后端已经单独在跑)
|
||
|
||
.EXAMPLE
|
||
.\dev\dev.ps1 -Force
|
||
端口被占时先杀掉占用进程再启动
|
||
|
||
.EXAMPLE
|
||
.\dev\dev.ps1 backend -Foreground
|
||
前台运行后端:日志(含 gin 路由、8092 管理后台请求)直接打在终端,Ctrl+C 结束
|
||
|
||
.EXAMPLE
|
||
.\dev\dev.ps1 frontend -Foreground
|
||
前台运行前端:astro 的请求日志、编译告警直接打在终端,Ctrl+C 结束
|
||
|
||
.EXAMPLE
|
||
.\dev\dev.ps1 -Foreground
|
||
推荐日常调试:后端 + 前端都前台跑,两边输出一起打在当前终端
|
||
#>
|
||
param(
|
||
[Parameter(Position = 0)]
|
||
[ValidateSet('all', 'backend', 'frontend', 'vision')]
|
||
[string]$Target = 'all',
|
||
|
||
[switch]$Vision,
|
||
[switch]$SkipBuild,
|
||
[switch]$Open,
|
||
[switch]$Force,
|
||
|
||
# 前台模式:不重定向、不隐藏窗口,服务输出直接打在当前终端(Ctrl+C 结束)。
|
||
# 不带服务名(all)时,前后端输出会一起打在当前终端,适合日常调试。
|
||
[switch]$Foreground
|
||
)
|
||
|
||
$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
|
||
|
||
if ($Foreground -and $Target -eq 'vision') {
|
||
Write-Fail '视觉服务不支持 -Foreground(用 .\vision\vision.ps1 logs 查看输出)'
|
||
exit 1
|
||
}
|
||
|
||
# 前台模式下正在运行的进程(all 模式会有多个,输出共用当前控制台)
|
||
$script:fgProcs = @()
|
||
|
||
$backendPort = [int](Get-Setting $envMap 'BACKEND_PORT' '8090')
|
||
$frontendPort = [int](Get-Setting $envMap 'FRONTEND_PORT' '4321')
|
||
$visionPort = [int](Get-Setting $envMap 'VISION_PORT' '8900')
|
||
|
||
if ($Open.IsPresent -eq $false -and (Get-Setting $envMap 'OPEN_BROWSER' '0') -eq '1') {
|
||
$Open = $true
|
||
}
|
||
|
||
# ---------------------------------------------------------------- 通用小工具
|
||
|
||
# Assert-CanStart:'start' 该起;'skip' 已在跑;'abort' 有冲突且未授权强杀。
|
||
function Assert-CanStart {
|
||
param([int]$Port, [string]$Name)
|
||
|
||
if (-not (Test-PortInUse $Port)) { return 'start' }
|
||
|
||
$owner = Get-PortOwnerPid $Port
|
||
$saved = Get-ServicePid $Name
|
||
if ($saved -gt 0 -and $saved -eq $owner) {
|
||
# 前台模式的语义就是「把输出占在我的终端」,已在后台跑的那份必须让位。
|
||
# 否则会出现「全部跳过 → 前台循环没进程 → 脚本直接退出」的假象。
|
||
if ($Foreground) {
|
||
Write-Note "$Name 已在后台运行(PID $owner),前台接管:先停止再前台启动"
|
||
Stop-NamedService $Name | Out-Null
|
||
Start-Sleep -Milliseconds 600
|
||
if (Test-PortInUse $Port) {
|
||
Write-Fail "端口 $Port 仍未释放,无法前台接管"
|
||
return 'abort'
|
||
}
|
||
return 'start'
|
||
}
|
||
Write-Note "$Name 已在运行(PID $owner,端口 $Port),跳过"
|
||
return 'skip'
|
||
}
|
||
|
||
if ($Force) {
|
||
Write-Warn "端口 $Port 被 PID $owner 占用,按 -Force 终止"
|
||
Stop-PidTree $owner | Out-Null
|
||
if (Test-PortInUse $Port) {
|
||
Write-Fail "端口 $Port 仍被占用,可能是系统服务或无权限"
|
||
return 'abort'
|
||
}
|
||
return 'start'
|
||
}
|
||
|
||
Write-Fail "端口 $Port 已被 PID $owner 占用(不是本脚本启动的)"
|
||
Write-Note "释放后再启动:.\dev\dev.ps1 -Force 或 手动结束该进程"
|
||
return 'abort'
|
||
}
|
||
|
||
function Test-ToolVersion {
|
||
param([string]$Name, [string]$Hint)
|
||
$cmd = Get-Command $Name -ErrorAction SilentlyContinue
|
||
if (-not $cmd) {
|
||
Write-Fail "找不到 $Name —— $Hint"
|
||
return $false
|
||
}
|
||
return $true
|
||
}
|
||
|
||
# ---------------------------------------------------------------- 后端
|
||
|
||
function Start-Backend {
|
||
$action = Assert-CanStart -Port $backendPort -Name 'backend'
|
||
if ($action -eq 'skip') { return $true }
|
||
if ($action -eq 'abort') { return $false }
|
||
|
||
$exe = Join-Path $BinDir 'fashionapi-dev.exe'
|
||
|
||
if (-not $SkipBuild) {
|
||
Write-Step '编译后端(go build ./cmd/server)'
|
||
Push-Location $svc.Backend.Path
|
||
try {
|
||
& go build -o $exe ./cmd/server
|
||
$code = $LASTEXITCODE
|
||
} finally {
|
||
Pop-Location
|
||
}
|
||
if ($code -ne 0) {
|
||
Write-Fail 'Go 编译失败,见上方错误'
|
||
return $false
|
||
}
|
||
Write-Ok "编译完成:$exe"
|
||
} elseif (-not (Test-Path $exe)) {
|
||
Write-Fail "指定了 -SkipBuild 但没有编译产物:$exe"
|
||
return $false
|
||
}
|
||
|
||
# 视觉服务开关通过环境变量传给后端(config.yml 里默认 false)
|
||
if ($Vision) {
|
||
$env:VISION_ENABLED = 'true'
|
||
$env:VISION_ENDPOINT = "http://127.0.0.1:$visionPort"
|
||
} else {
|
||
Remove-Item Env:\VISION_ENABLED -ErrorAction SilentlyContinue
|
||
}
|
||
|
||
if ($Foreground) {
|
||
Write-Step "前台运行后端(公开 $backendPort / SSG 8091 / 后台 8092)"
|
||
|
||
# 只起后端:直接阻塞在这个进程上,Ctrl+C 就是结束进程,最干净。
|
||
if ($Target -eq 'backend') {
|
||
Write-Note '输出直接打在下面;Ctrl+C 结束'
|
||
Push-Location $svc.Backend.Path
|
||
try {
|
||
& $exe
|
||
} finally {
|
||
Pop-Location
|
||
Remove-Item Env:\VISION_ENABLED -ErrorAction SilentlyContinue
|
||
}
|
||
exit 0
|
||
}
|
||
|
||
# all 模式:-NoNewWindow 让子进程继承当前控制台,输出实时可见(不重定向)
|
||
$p = Start-Process -FilePath $exe -WorkingDirectory $svc.Backend.Path -NoNewWindow -PassThru
|
||
$script:fgProcs += [PSCustomObject]@{ Name = 'backend'; Proc = $p; Reported = $false }
|
||
Remove-Item Env:\VISION_ENABLED -ErrorAction SilentlyContinue
|
||
Write-Ok "后端已在前台启动(PID $($p.Id))"
|
||
return $true
|
||
}
|
||
|
||
Write-Step "启动后端(公开 $backendPort / SSG 8091 / 后台 8092)"
|
||
Start-LoggedProcess -Name 'backend' -FilePath $exe -ArgumentList @() -WorkingDirectory $svc.Backend.Path | Out-Null
|
||
Remove-Item Env:\VISION_ENABLED -ErrorAction SilentlyContinue
|
||
|
||
$health = "http://127.0.0.1:$backendPort/api/health"
|
||
if (Wait-HttpOk -Url $health -TimeoutSec 60) {
|
||
Write-Ok '后端就绪'
|
||
return $true
|
||
}
|
||
Write-Fail '后端 60 秒内未就绪(常见原因:数据库连不上、config.local.yml 缺密码)'
|
||
Show-LogTail -Name 'backend' -Lines 20
|
||
return $false
|
||
}
|
||
|
||
# ---------------------------------------------------------------- 前端
|
||
|
||
# Get-AstroDevLock:读 astro 自己的 dev 锁(<frontend>/.astro/dev.json)。
|
||
# astro 7 起 dev server 有单实例锁:已有实例在跑时直接报错退出,
|
||
# 即使端口没冲突也会起不来——所以必须显式处理,不能只查端口。
|
||
function Get-AstroDevLock {
|
||
param([string]$FrontendPath)
|
||
$f = Join-Path $FrontendPath '.astro\dev.json'
|
||
if (-not (Test-Path $f)) { return $null }
|
||
try {
|
||
$j = Get-Content -Path $f -Raw -ErrorAction Stop | ConvertFrom-Json
|
||
$lockPid = 0
|
||
if ($null -eq $j.pid) { return $null }
|
||
$lockPid = [int]$j.pid
|
||
$alive = [bool](Get-Process -Id $lockPid -ErrorAction SilentlyContinue)
|
||
$lockPort = 0
|
||
if ($null -ne $j.port) { $lockPort = [int]$j.port }
|
||
$lockUrl = ''
|
||
if ($null -ne $j.url) { $lockUrl = [string]$j.url }
|
||
return [PSCustomObject]@{ Pid = $lockPid; Port = $lockPort; Url = $lockUrl; Alive = $alive }
|
||
} catch {
|
||
return $null
|
||
}
|
||
}
|
||
|
||
function Remove-AstroDevLock {
|
||
param([string]$FrontendPath)
|
||
$f = Join-Path $FrontendPath '.astro\dev.json'
|
||
Remove-Item $f -Force -ErrorAction SilentlyContinue
|
||
}
|
||
|
||
function Start-Frontend {
|
||
$action = Assert-CanStart -Port $frontendPort -Name 'frontend'
|
||
if ($action -eq 'skip') { return $true }
|
||
if ($action -eq 'abort') { return $false }
|
||
|
||
# 前台接管时旧实例刚被停掉,锁文件可能残留(PID 被复用会误判「已在运行」)
|
||
if ($Foreground) { Remove-AstroDevLock $svc.Frontend.Path }
|
||
|
||
# astro 单实例锁:端口没冲突也可能起不来
|
||
$lock = Get-AstroDevLock $svc.Frontend.Path
|
||
if ($lock -and $lock.Alive) {
|
||
if ($lock.Port -eq $frontendPort) {
|
||
Write-Note "前端已在运行(PID $($lock.Pid),端口 $($lock.Port)),跳过"
|
||
return $true
|
||
}
|
||
if (-not $Force) {
|
||
Write-Fail "已有一个 astro dev 在端口 $($lock.Port) 运行(PID $($lock.Pid),$($lock.Url))"
|
||
Write-Note "它不是本脚本启动的,且端口不在 CORS 白名单里 → 浏览器调接口会被拦。"
|
||
Write-Note "接管并重起在 $frontendPort:.\dev\dev.ps1 frontend -Force"
|
||
return $false
|
||
}
|
||
Write-Warn "接管:终止旧的 astro dev(PID $($lock.Pid),端口 $($lock.Port))"
|
||
Stop-PidTree $lock.Pid | Out-Null
|
||
Start-Sleep -Milliseconds 500
|
||
Remove-AstroDevLock $svc.Frontend.Path
|
||
}
|
||
|
||
$nodeModules = Join-Path $svc.Frontend.Path 'node_modules'
|
||
if (-not (Test-Path $nodeModules)) {
|
||
Write-Step '安装前端依赖(npm ci)'
|
||
Push-Location $svc.Frontend.Path
|
||
try {
|
||
& npm.cmd ci
|
||
$code = $LASTEXITCODE
|
||
} finally {
|
||
Pop-Location
|
||
}
|
||
if ($code -ne 0) {
|
||
Write-Fail 'npm ci 失败'
|
||
return $false
|
||
}
|
||
}
|
||
|
||
$astro = Join-Path $svc.Frontend.Path 'node_modules\astro\bin\astro.mjs'
|
||
if (-not (Test-Path $astro)) {
|
||
Write-Fail "找不到 astro CLI:$astro(依赖可能损坏,删掉 node_modules 重装)"
|
||
return $false
|
||
}
|
||
|
||
if ($Foreground) {
|
||
Write-Step "前台运行前端(astro dev,端口 $frontendPort)"
|
||
|
||
if ($Target -eq 'frontend') {
|
||
Write-Note '输出直接打在下面;Ctrl+C 结束'
|
||
Push-Location $svc.Frontend.Path
|
||
try {
|
||
& node.exe $astro dev --port "$frontendPort"
|
||
} finally {
|
||
Pop-Location
|
||
}
|
||
exit 0
|
||
}
|
||
|
||
$p = Start-Process -FilePath 'node.exe' `
|
||
-ArgumentList @($astro, 'dev', '--port', "$frontendPort") `
|
||
-WorkingDirectory $svc.Frontend.Path -NoNewWindow -PassThru
|
||
$script:fgProcs += [PSCustomObject]@{ Name = 'frontend'; Proc = $p; Reported = $false }
|
||
Write-Ok "前端已在前台启动(PID $($p.Id))"
|
||
return $true
|
||
}
|
||
|
||
Write-Step "启动前端(astro dev,端口 $frontendPort)"
|
||
Start-LoggedProcess -Name 'frontend' -FilePath 'node.exe' `
|
||
-ArgumentList @($astro, 'dev', '--port', "$frontendPort") `
|
||
-WorkingDirectory $svc.Frontend.Path | Out-Null
|
||
|
||
$probe = "http://127.0.0.1:$frontendPort/cn/runway-looks/"
|
||
if (Wait-HttpOk -Url $probe -TimeoutSec 120) {
|
||
Write-Ok '前端就绪'
|
||
return $true
|
||
}
|
||
Write-Fail '前端 120 秒内未就绪'
|
||
Show-LogTail -Name 'frontend' -Lines 20
|
||
return $false
|
||
}
|
||
|
||
# ---------------------------------------------------------------- 视觉服务
|
||
|
||
function Start-Vision {
|
||
$action = Assert-CanStart -Port $visionPort -Name 'vision'
|
||
if ($action -eq 'skip') { return $true }
|
||
if ($action -eq 'abort') { 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) 建虚拟环境并装依赖(torch / open_clip_torch / fastapi / uvicorn)"
|
||
return $false
|
||
}
|
||
|
||
Write-Step "启动视觉服务(CLIP,端口 $visionPort,首次加载模型较慢)"
|
||
Start-LoggedProcess -Name 'vision' -FilePath $uvicorn `
|
||
-ArgumentList @('app:app', '--host', '127.0.0.1', '--port', "$visionPort", '--workers', '1') `
|
||
-WorkingDirectory $svc.Vision.Path | Out-Null
|
||
|
||
$health = "http://127.0.0.1:$visionPort/health"
|
||
if (Wait-HttpOk -Url $health -TimeoutSec 180) {
|
||
Write-Ok '视觉服务就绪'
|
||
return $true
|
||
}
|
||
Write-Fail '视觉服务 180 秒内未就绪'
|
||
Show-LogTail -Name 'vision' -Lines 20
|
||
return $false
|
||
}
|
||
|
||
# ---------------------------------------------------------------- 主流程
|
||
|
||
Write-Host ''
|
||
Write-Host ' app-cli / 本地开发环境' -ForegroundColor White
|
||
Write-Host ' ----------------------------------------' -ForegroundColor DarkGray
|
||
|
||
Write-Step '检查依赖'
|
||
$ready = $true
|
||
if ($Target -eq 'all' -or $Target -eq 'backend') {
|
||
if (-not (Test-ToolVersion 'go' '安装 Go 1.22+')) { $ready = $false }
|
||
}
|
||
if ($Target -eq 'all' -or $Target -eq 'frontend') {
|
||
if (-not (Test-ToolVersion 'node' '安装 Node 22.12+')) { $ready = $false }
|
||
if (-not (Test-ToolVersion 'npm' '随 Node 一起安装')) { $ready = $false }
|
||
}
|
||
if (-not $ready) { exit 1 }
|
||
|
||
Write-Step '检查仓库路径'
|
||
foreach ($item in @(
|
||
@{ S = $svc.Backend; Need = ($Target -eq 'all' -or $Target -eq 'backend') },
|
||
@{ S = $svc.Frontend; Need = ($Target -eq 'all' -or $Target -eq 'frontend') },
|
||
@{ S = $svc.Vision; Need = ($Vision -or $Target -eq 'vision') }
|
||
)) {
|
||
if (-not $item.Need) { continue }
|
||
if ($item.S.IsValid) {
|
||
Write-Ok "$($item.S.Name): $($item.S.Path)"
|
||
} else {
|
||
Write-Fail "$($item.S.Name) 路径无效:$($item.S.Path)(缺标记文件 $($item.S.Marker))"
|
||
$ready = $false
|
||
}
|
||
}
|
||
if (-not $ready) {
|
||
Write-Note '在 config/local.env 里显式写 BACKEND_DIR / FRONTEND_DIR / VISION_DIR 纠正路径'
|
||
exit 1
|
||
}
|
||
|
||
$failed = @()
|
||
|
||
if ($Target -eq 'all' -or $Target -eq 'backend') {
|
||
if (-not (Start-Backend)) { $failed += 'backend' }
|
||
}
|
||
if ($Target -eq 'all' -or $Target -eq 'frontend') {
|
||
if (-not (Start-Frontend)) { $failed += 'frontend' }
|
||
}
|
||
if ($Vision -or $Target -eq 'vision') {
|
||
if (-not (Start-Vision)) { $failed += 'vision' }
|
||
}
|
||
|
||
# ---------------------------------------------------------------- 前台聚合输出
|
||
# 前台模式下半套启动没有意义(端口冲突等):直接报错退出,不留孤儿进程。
|
||
if ($Foreground -and $failed.Count -gt 0) {
|
||
Write-Fail ("前台启动失败:" + ($failed -join ', '))
|
||
Write-Note '常见原因:8090/4323 已被之前的进程占用,先 .\dev\stop.ps1 再重试'
|
||
exit 1
|
||
}
|
||
|
||
# 兜底:前台模式必须有进程在跑,否则静默退出会让人误以为脚本自己崩了。
|
||
if ($Foreground -and $script:fgProcs.Count -eq 0) {
|
||
Write-Fail '前台模式没有启动任何进程,请查看上面的输出'
|
||
exit 1
|
||
}
|
||
|
||
# all + -Foreground:子进程共用当前控制台,输出由它们直接写终端(无重定向、无延迟)。
|
||
# 这里只负责等待与收尾:任一进程退出就报告,Ctrl+C 时一起停掉。
|
||
if ($Foreground -and $script:fgProcs.Count -gt 0) {
|
||
$names = ($script:fgProcs | ForEach-Object { $_.Name }) -join ' + '
|
||
Write-Host ''
|
||
Write-Host ' ----------------------------------------' -ForegroundColor DarkGray
|
||
Write-Host " $names 的输出直接打在下面,Ctrl+C 一起停止" -ForegroundColor Yellow
|
||
Write-Host ' ----------------------------------------' -ForegroundColor DarkGray
|
||
Write-Host ''
|
||
try {
|
||
while ($true) {
|
||
Start-Sleep -Milliseconds 400
|
||
$alive = @($script:fgProcs | Where-Object { -not $_.Proc.HasExited })
|
||
if ($alive.Count -eq 0) { break }
|
||
foreach ($e in $script:fgProcs) {
|
||
if (-not $e.Reported -and $e.Proc.HasExited) {
|
||
Write-Fail "$($e.Name) 已退出(退出码 $($e.Proc.ExitCode)),另一个仍在运行"
|
||
$e.Reported = $true
|
||
}
|
||
}
|
||
}
|
||
} finally {
|
||
foreach ($e in $script:fgProcs) {
|
||
if (-not $e.Proc.HasExited) {
|
||
Write-Note "停止 $($e.Name)(PID $($e.Proc.Id))"
|
||
Stop-PidTree $e.Proc.Id | Out-Null
|
||
}
|
||
}
|
||
}
|
||
Write-Host ''
|
||
Write-Note '前台模式已退出(这些进程不在 .run\ 登记,stop.ps1 不管它们)'
|
||
exit 0
|
||
}
|
||
|
||
# ---------------------------------------------------------------- 结果汇总
|
||
|
||
Write-Host ''
|
||
if ($failed.Count -gt 0) {
|
||
Write-Fail ("启动失败:" + ($failed -join ', '))
|
||
Write-Note '日志在 .logs\ 目录;排查完用 .\dev\dev.ps1 -Force 重来'
|
||
exit 1
|
||
}
|
||
|
||
Write-Host ' 已就绪' -ForegroundColor White
|
||
Write-Host ' ----------------------------------------' -ForegroundColor DarkGray
|
||
if ($Target -eq 'all' -or $Target -eq 'backend') {
|
||
Write-Host (" 前台页面 http://localhost:" + $frontendPort + "/cn/runway-looks/")
|
||
Write-Host (" 公开接口 http://localhost:" + $backendPort + "/api/v1/public/runway-looks?page=1")
|
||
Write-Host (" 管理后台 http://localhost:8092/admin")
|
||
Write-Host (" 健康检查 http://localhost:" + $backendPort + "/api/health")
|
||
}
|
||
if ($Vision -or $Target -eq 'vision') {
|
||
Write-Host (" 视觉服务 http://127.0.0.1:" + $visionPort + "/health")
|
||
}
|
||
Write-Host ''
|
||
|
||
# CORS 自检:前端端口没被后端放行时,浏览器会整条拦掉接口响应
|
||
if (Test-PortInUse $backendPort) {
|
||
$origin = "http://localhost:$frontendPort"
|
||
$url = "http://127.0.0.1:$backendPort/api/v1/public/runway-looks?page=1"
|
||
$acao = Get-CorsOrigin -Url $url -Origin $origin
|
||
if ($acao -eq $origin) {
|
||
Write-Ok "CORS 已放行 $origin"
|
||
} else {
|
||
Write-Warn "CORS 未放行 $origin(后端回显:'$acao')"
|
||
Write-Note "浏览器会拦截接口 → 页面显示「无数据」。"
|
||
Write-Note "修:在 backend/configs/config.local.yml 的 cors.allow_origins 里加一行 $origin"
|
||
}
|
||
}
|
||
|
||
Write-Host ''
|
||
Write-Note '本窗口只显示启动过程;服务输出写在 .logs\ 下,不会打到终端(这是后台模式的正常表现)'
|
||
Write-Note '要实时看输出:.\dev\logs.ps1 backend -Follow 或 .\dev\logs.ps1 frontend -Follow'
|
||
Write-Note '要实时看全部输出:.\dev\dev.ps1 -Foreground(前后端一起前台跑,Ctrl+C 停)'
|
||
Write-Note '停止:.\dev\stop.ps1 状态:.\dev\status.ps1'
|
||
|
||
if ($Open) {
|
||
$target = "http://localhost:$frontendPort/cn/runway-looks/"
|
||
Write-Note "打开浏览器:$target"
|
||
Start-Process $target | Out-Null
|
||
}
|