Ctrl K
ComunidadGuías…

deploy/windows/start.ps1

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# 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')