deploy/windows/start.ps1
Rama master · réplica de lectura del repositorio original.
# WindsurfAPI 首次引导 + 前台监督启动。
# 幂等生成项目根 .env(UTF-8 无 BOM),填缺失键,打印面板地址,进入监督循环。
# 前台运行(可见日志 + Ctrl-C 优雅停),最利首次调试。开机自启后台见 install-task.bat。
$ErrorActionPreference = 'Stop'
# 控制台按 UTF-8 输出,否则中文文案在默认 GBK/936 码页的 cmd 窗口里显示为乱码
# (脚本本身是 UTF-8 带 BOM,PowerShell 能正确读中文字面量,问题只在输出编码)。
try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch { }
$Root = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
Set-Location $Root
$EnvPath = Join-Path $Root '.env'
# ── 1. 预检 Node ≥ 22.13 ───────────────────────────────────
function Stop-WithError([string]$msg, [string]$hint) {
Write-Host ''
Write-Host '============================================================' -ForegroundColor Red
Write-Host " $msg" -ForegroundColor Red
if ($hint) { Write-Host " $hint" -ForegroundColor Yellow }
Write-Host '============================================================' -ForegroundColor Red
Write-Host '按任意键退出...'
$null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown')
exit 1
}
$node = Get-Command node -ErrorAction SilentlyContinue
if (-not $node) {
Stop-WithError 'Node.js 未安装或不在 PATH。' '请从 https://nodejs.org 安装 Node 22.13+ 后重试。'
}
$verRaw = (& node -v) -replace '^v', ''
$nodeVer = [version]$verRaw
if ($nodeVer -lt [version]'22.13.0') {
Stop-WithError "Node 版本过低(当前 v$verRaw,需 ≥ 22.13)。" '请升级 Node.js 到 22.13 或更高。'
}
# ── 2. 强随机密钥(RandomNumberGenerator,非 Get-Random)──────
function New-Secret([int]$bytes = 24) {
$buf = New-Object 'System.Byte[]' $bytes
$rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()
$rng.GetBytes($buf)
$rng.Dispose()
# base64url:去掉 +/= 让密钥能安全放进 .env / URL / header
return ([Convert]::ToBase64String($buf)) -replace '\+', '-' -replace '/', '_' -replace '=', ''
}
# ── 3. 幂等读取现有 .env(仅填缺失/占位键,保留用户已设值)──
$existing = @{}
$existingLines = @()
if (Test-Path $EnvPath) {
try {
$existingLines = Get-Content -Path $EnvPath -Encoding UTF8
foreach ($line in $existingLines) {
$t = $line.Trim()
if (-not $t -or $t.StartsWith('#')) { continue }
$eq = $t.IndexOf('=')
if ($eq -lt 1) { continue }
$k = $t.Substring(0, $eq).Trim()
$v = $t.Substring($eq + 1).Trim()
$existing[$k] = $v
}
} catch {
# 无法解析:备份后重写(不丢用户数据)。
$bak = "$EnvPath.bak.$(Get-Date -Format 'yyyyMMddHHmmss')"
Copy-Item $EnvPath $bak -Force
Write-Host "现有 .env 无法解析,已备份到 $bak" -ForegroundColor Yellow
$existing = @{}
}
}
function Need-Key([string]$key) {
return (-not $existing.ContainsKey($key)) -or [string]::IsNullOrWhiteSpace($existing[$key])
}
$generated = @{}
$genApiKey = $null
$genPassword = $null
if (Need-Key 'PORT') { $generated['PORT'] = '3003' }
# HOST=127.0.0.1 必需:默认 0.0.0.0 + 空 API_KEY = fail-closed 锁死。
if (Need-Key 'HOST') { $generated['HOST'] = '127.0.0.1' }
if (Need-Key 'API_KEY') { $genApiKey = 'sk-windsurf-' + (New-Secret 24); $generated['API_KEY'] = $genApiKey }
if (Need-Key 'DASHBOARD_PASSWORD') { $genPassword = New-Secret 18; $generated['DASHBOARD_PASSWORD'] = $genPassword }
# DEVIN_CONNECT=1 必需:language_server 仅 Linux/macOS。
if ((Need-Key 'DEVIN_CONNECT') -and (Need-Key 'DEVIN_ONLY')) { $generated['DEVIN_CONNECT'] = '1' }
if ($generated.Count -gt 0) {
# 合并:保留现有行,追加缺失键。
$outLines = New-Object System.Collections.Generic.List[string]
if ($existingLines.Count -gt 0) { foreach ($l in $existingLines) { $outLines.Add($l) } }
if ($outLines.Count -gt 0 -and $outLines[$outLines.Count - 1].Trim() -ne '') { $outLines.Add('') }
$outLines.Add("# --- generated by deploy/windows/start.ps1 $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') ---")
foreach ($k in $generated.Keys) { $outLines.Add("$k=$($generated[$k])") }
# ⚠️ 必须无 BOM 写(UTF8Encoding($false))。虽然 config.js 的 .trim() 恰好能
# 剥掉首键 BOM,仍以无 BOM 为规范,避免任何边缘键被 BOM 污染。
# try/catch:写盘失败(.env 被占用 / 目录只读)必须友好报错 + 暂停,绝不让
# 双击窗口静默闪退(对齐 KiroStudio start.ps1 的 Stop-WithError 保护)。
try {
$text = ($outLines -join "`r`n") + "`r`n"
[System.IO.File]::WriteAllText($EnvPath, $text, (New-Object System.Text.UTF8Encoding($false)))
# Windows 上 .env 默认继承目录 ACL(Users / Authenticated Users 可读),而它
# 存着 API_KEY 与 DASHBOARD_PASSWORD。best-effort 收紧为仅当前用户:失败只
# 警告 —— 密钥已经写成功,不能因此让启动失败。
$aclUser = if ($env:USERDOMAIN) { "$env:USERDOMAIN\$env:USERNAME" } else { $env:USERNAME }
try {
& icacls $EnvPath /inheritance:r /grant:r "${aclUser}:(F)" *> $null
if ($LASTEXITCODE -ne 0) { Write-Warning "icacls 未能收紧 .env 权限(退出码 $LASTEXITCODE);其他本机用户可能仍可读取。" }
} catch {
Write-Warning "icacls 未能收紧 .env 权限:$($_.Exception.Message)"
}
Write-Host ".env 已更新(填入 $($generated.Count) 个缺失键)。" -ForegroundColor Green
} catch {
Stop-WithError "写入 .env 失败:$($_.Exception.Message)" `
'常见原因:.env 被其它程序占用(已在运行的网关/编辑器),或目录只读。请关闭占用程序后重试。'
}
} else {
Write-Host '.env 已存在且完整,未改动。' -ForegroundColor Green
}
# ── 4. 端口预检(更快的友好提示;server.js 也会自重试 10x3s)──
$port = if ($generated.ContainsKey('PORT')) { $generated['PORT'] } elseif ($existing.ContainsKey('PORT')) { $existing['PORT'] } else { '3003' }
try {
$listeners = [System.Net.NetworkInformation.IPGlobalProperties]::GetIPGlobalProperties().GetActiveTcpListeners()
if ($listeners | Where-Object { $_.Port -eq [int]$port }) {
Write-Host "警告:端口 $port 已被占用,启动可能失败(server.js 会重试 ~30s)。" -ForegroundColor Yellow
}
} catch { }
# git 缺失告警(自更新需要)
if (-not (Get-Command git -ErrorAction SilentlyContinue)) {
Write-Host '提示:未检测到 git,面板 Update / update.bat 自更新将不可用。' -ForegroundColor Yellow
}
# ── 5. 打印面板地址 + 密钥 ─────────────────────────────────
$hostVal = if ($generated.ContainsKey('HOST')) { $generated['HOST'] } elseif ($existing.ContainsKey('HOST')) { $existing['HOST'] } else { '127.0.0.1' }
$displayHost = if ($hostVal -eq '0.0.0.0') { '127.0.0.1' } else { $hostVal }
Write-Host ''
if ($genPassword -or $genApiKey) {
# 大字框:首次生成的密钥务必醒目,提示用户截图保存(对齐 KiroStudio 模板)。
Write-Host '################################################################' -ForegroundColor Green
Write-Host '# #' -ForegroundColor Green
Write-Host '# 已为你自动生成密钥(请截图妥善保存) #' -ForegroundColor Green
Write-Host '# #' -ForegroundColor Green
Write-Host '################################################################' -ForegroundColor Green
Write-Host ''
if ($genPassword) {
Write-Host ' 面板登录密码 (DASHBOARD_PASSWORD):' -ForegroundColor Yellow
Write-Host " $genPassword" -ForegroundColor White
Write-Host ''
}
if ($genApiKey) {
Write-Host ' 网关调用密钥 (API_KEY,给 Claude Code / SDK 用):' -ForegroundColor Yellow
Write-Host " $genApiKey" -ForegroundColor White
Write-Host ''
}
Write-Host ' 这些密钥已写入项目根 .env,下次启动无需重设。' -ForegroundColor DarkGray
Write-Host ''
}
Write-Host '============================================================' -ForegroundColor Cyan
Write-Host ' WindsurfAPI 已引导完成' -ForegroundColor Cyan
Write-Host " >> 面板地址(浏览器打开,用 DASHBOARD_PASSWORD 登录):" -ForegroundColor Cyan
Write-Host " http://${displayHost}:$port/dashboard" -ForegroundColor White
if (-not $genPassword -and -not $genApiKey) {
Write-Host ' (密钥沿用现有 .env,如需查看请打开项目根 .env)' -ForegroundColor DarkGray
}
if ($hostVal -eq '0.0.0.0') {
Write-Host ' [提示] HOST=0.0.0.0:网关对局域网开放,请确保 API_KEY 为强随机值。' -ForegroundColor Yellow
}
Write-Host ' >> 登录后到 accounts 页添加一个 Windsurf/Devin session token 即可使用。' -ForegroundColor DarkGray
Write-Host '============================================================' -ForegroundColor Cyan
Write-Host ''
# ── 6. 进入前台监督循环 ────────────────────────────────────
& (Join-Path $PSScriptRoot 'run.ps1')