param( [string]$NodeMsiUrl = "https://pub-644ad95b9d504aa8a79aeb428f44c923.r2.dev/prod/windows/x64/runtime/node/node-lts-x64.msi", [string]$OpenClawPackageUrl = "https://pub-644ad95b9d504aa8a79aeb428f44c923.r2.dev/prod/windows/x64/runtime/openclaw/openclaw-latest.tgz", [string]$Proxy = "", [switch]$UseMirror, [string]$ProviderBaseUrl = "", [string]$ProviderApiKey = "", [string]$ProviderModelId = "", [switch]$SkipProviderPrompt ) $ErrorActionPreference = "Stop" $ProgressPreference = "SilentlyContinue" $script:Stage = "init" $script:Workspace = Join-Path $env:USERPROFILE ".openclaw\workspace" $script:LogFile = Join-Path $env:TEMP ("openclaw-install-" + (Get-Date -Format "yyyyMMdd-HHmmss") + ".log") $script:ProviderBaseUrl = $ProviderBaseUrl $script:ProviderApiKey = $ProviderApiKey $script:ProviderModelId = $ProviderModelId $script:ProviderCompatibility = "openai" try { Start-Transcript -Path $script:LogFile -Force | Out-Null } catch {} function Log($msg) { Write-Host "`n==> $msg" -ForegroundColor Cyan } function Warn($msg) { Write-Warning $msg } function Fail($msg) { throw $msg } function Test-Command($name) { return [bool](Get-Command $name -ErrorAction SilentlyContinue) } function Invoke-Retry { param( [scriptblock]$Script, [string]$What, [int]$MaxAttempts = 4, [int]$DelaySeconds = 3 ) for ($i = 1; $i -le $MaxAttempts; $i++) { try { & $Script return } catch { if ($i -ge $MaxAttempts) { throw "$What failed after $MaxAttempts attempts: $($_.Exception.Message)" } Warn "$What attempt $i failed, retrying in ${DelaySeconds}s" Start-Sleep -Seconds $DelaySeconds } } } function Refresh-Path { $machine = [Environment]::GetEnvironmentVariable("Path", "Machine") $user = [Environment]::GetEnvironmentVariable("Path", "User") $paths = @() if ($machine) { $paths += $machine } if ($user) { $paths += $user } $paths += "C:\Program Files\nodejs" $paths += (Join-Path $env:APPDATA "npm") $env:Path = ($paths | Where-Object { $_ -and $_.Trim() } | Select-Object -Unique) -join ";" } function Set-ProxyIfNeeded { if ([string]::IsNullOrWhiteSpace($Proxy)) { return } Log "Setting proxy" $env:HTTP_PROXY = $Proxy $env:HTTPS_PROXY = $Proxy $env:http_proxy = $Proxy $env:https_proxy = $Proxy } function Invoke-WebRequestCompat { param( [Parameter(Mandatory = $true)] [string]$Uri, [string]$Method = "Get", [hashtable]$Headers, [string]$OutFile = "", [int]$TimeoutSec = 30 ) $params = @{ Uri = $Uri Method = $Method TimeoutSec = $TimeoutSec UseBasicParsing = $true } if ($Headers) { $params["Headers"] = $Headers } if (-not [string]::IsNullOrWhiteSpace($OutFile)) { $params["OutFile"] = $OutFile } return Invoke-WebRequest @params } function Test-Url($Url) { try { Invoke-WebRequestCompat -Uri $Url -Method Get -TimeoutSec 8 | Out-Null return $true } catch { return $false } } function Resolve-NpmRegistry { if ($UseMirror) { return "https://registry.npmmirror.com" } if (Test-Url "https://registry.npmjs.org/openclaw") { return "https://registry.npmjs.org" } if (Test-Url "https://registry.npmmirror.com/openclaw") { return "https://registry.npmmirror.com" } Fail "Neither the npm official registry nor the mirror is reachable. Pass -Proxy or -OpenClawPackageUrl." } function Test-NodeVersionOk { if (-not (Test-Command "node")) { return $false } try { $raw = (& node -v).Trim() $ver = $raw.TrimStart("v") return ([version]$ver -ge [version]"22.16.0") } catch { return $false } } function Get-NpmCmd { $cmd = Get-Command npm.cmd -ErrorAction SilentlyContinue if ($cmd) { return $cmd.Source } $fallback = "C:\Program Files\nodejs\npm.cmd" if (Test-Path $fallback) { return $fallback } Fail "npm.cmd not found" } function Get-OpenClawCmd { $cmd = Get-Command openclaw.cmd -ErrorAction SilentlyContinue if ($cmd) { return $cmd.Source } $candidate = Join-Path $env:APPDATA "npm\openclaw.cmd" if (Test-Path $candidate) { return $candidate } try { $npm = Get-NpmCmd $prefix = & $npm prefix -g $candidate2 = Join-Path $prefix "openclaw.cmd" if (Test-Path $candidate2) { return $candidate2 } } catch {} Fail "openclaw.cmd not found" } function Convert-SecureStringToPlainText($secureString) { if ($null -eq $secureString) { return "" } $bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($secureString) try { return [Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr) } finally { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) } } function Clear-ProviderSettings { $script:ProviderBaseUrl = "" $script:ProviderApiKey = "" $script:ProviderModelId = "" } function Prompt-ProviderSettings { if (-not [string]::IsNullOrWhiteSpace($script:ProviderBaseUrl)) { return } if ($SkipProviderPrompt) { return } Log "Model provider setup" $choice = Read-Host "Configure model provider now? (Y/N, default N)" if ([string]::IsNullOrWhiteSpace($choice)) { return } $normalized = $choice.Trim().ToLowerInvariant() if ($normalized -ne "y" -and $normalized -ne "yes") { return } while ([string]::IsNullOrWhiteSpace($script:ProviderBaseUrl)) { $enteredBaseUrl = Read-Host "Provider base URL" if (-not [string]::IsNullOrWhiteSpace($enteredBaseUrl)) { $script:ProviderBaseUrl = $enteredBaseUrl.Trim() break } Warn "Provider base URL cannot be empty" } Log "Provider compatibility is fixed to openai-compatible" $secureApiKey = Read-Host "Provider API key (input hidden)" -AsSecureString $script:ProviderApiKey = Convert-SecureStringToPlainText $secureApiKey if ([string]::IsNullOrWhiteSpace($script:ProviderBaseUrl)) { Fail "Provider base URL cannot be empty" } } function Get-ProviderModelsUrl { $baseUrl = $script:ProviderBaseUrl.Trim() if ($baseUrl.EndsWith("/")) { return $baseUrl + "models" } return $baseUrl + "/models" } function Get-ProviderHeaders { $headers = @{ "Accept" = "application/json" } if (-not [string]::IsNullOrWhiteSpace($script:ProviderApiKey)) { $headers["Authorization"] = "Bearer " + $script:ProviderApiKey } return $headers } function Get-ProviderModelPreferenceScore { param([string]$ModelId) if ([string]::IsNullOrWhiteSpace($ModelId)) { return -1000 } $id = $ModelId.ToLowerInvariant() $score = 0 if ($id -match 'embedding|rerank|moderation|tts|whisper|transcribe|speech|image|audio') { return -1000 } if ($id -match '^gpt-4o-mini($|-)') { $score = 1000 } elseif ($id -match '^gpt-4\.1-mini($|-)') { $score = 980 } elseif ($id -match '^gpt-5-mini($|-)') { $score = 960 } elseif ($id -match '^gpt-4\.1($|-)') { $score = 940 } elseif ($id -match '^gpt-5($|-)') { $score = 920 } elseif ($id -match 'haiku') { $score = 900 } elseif ($id -match 'sonnet') { $score = 880 } elseif ($id -match 'opus') { $score = 860 } elseif ($id -match '^o3-mini($|-)') { $score = 840 } elseif ($id -match '^o1-mini($|-)') { $score = 820 } elseif ($id -match '^o1($|-)') { $score = 800 } elseif ($id -match '^gpt-4o($|-)') { $score = 780 } elseif ($id -match '^claude') { $score = 760 } elseif ($id -match '^gpt-4($|-)') { $score = 740 } elseif ($id -match '^gpt-3\.5($|-)') { $score = 720 } elseif ($id -match 'gpt|claude|chat|o[0-9]') { $score = 700 } if ($id -match 'preview') { $score -= 120 } if ($id -match '\d{4}-\d{2}-\d{2}' -and $id -notmatch 'mini') { $score -= 80 } return $score } function Select-ProviderModelId($items) { if ($null -eq $items -or $items.Count -eq 0) { return "" } $normalized = @() foreach ($item in $items) { if ($item -is [string]) { $normalized += [pscustomobject]@{ id = $item; created = 0 } continue } $id = $item.id if ([string]::IsNullOrWhiteSpace($id)) { continue } $created = 0 try { if ($null -ne $item.created_at) { $created = [DateTimeOffset]::Parse([string]$item.created_at).ToUnixTimeSeconds() } elseif ($null -ne $item.created) { $created = [int64]$item.created } } catch {} $normalized += [pscustomobject]@{ id = [string]$id; created = $created } } if ($normalized.Count -eq 0) { return "" } $candidate = $normalized | Where-Object { $_.id -notmatch 'embedding|rerank|moderation|tts|whisper|transcribe|speech|image|audio' } if ($null -eq $candidate -or $candidate.Count -eq 0) { $candidate = $normalized } $ranked = $candidate | Select-Object *, @{ Name = 'preferenceScore'; Expression = { Get-ProviderModelPreferenceScore $_.id } } | Sort-Object -Property @( @{ Expression = 'preferenceScore'; Descending = $true }, @{ Expression = 'created'; Descending = $true }, @{ Expression = 'id'; Descending = $false } ) return ($ranked | Select-Object -First 1).id } function Try-AutoDetectProviderModelId { if (-not [string]::IsNullOrWhiteSpace($script:ProviderModelId)) { return } if ([string]::IsNullOrWhiteSpace($script:ProviderBaseUrl)) { return } Log "Trying to auto-detect model ID" try { $headers = Get-ProviderHeaders $modelsUrl = Get-ProviderModelsUrl $response = Invoke-WebRequestCompat -Uri $modelsUrl -Headers $headers -TimeoutSec 30 $payload = $response.Content | ConvertFrom-Json $items = @() if ($null -ne $payload.data) { $items = @($payload.data) } elseif ($null -ne $payload.models) { $items = @($payload.models) } $resolvedId = Select-ProviderModelId $items if (-not [string]::IsNullOrWhiteSpace($resolvedId)) { $script:ProviderModelId = $resolvedId Log "Auto-detected model ID: $resolvedId" return } } catch { Warn "Model auto-detect failed: $($_.Exception.Message)" } } function Ensure-ProviderModelId { if ([string]::IsNullOrWhiteSpace($script:ProviderBaseUrl)) { return } Try-AutoDetectProviderModelId if (-not [string]::IsNullOrWhiteSpace($script:ProviderModelId)) { return } if ($SkipProviderPrompt) { Warn "Model ID could not be auto-detected. Provider config will be skipped." Clear-ProviderSettings return } $manual = Read-Host "Model ID could not be auto-detected. Enter manually now, or press Enter to skip provider config" if ([string]::IsNullOrWhiteSpace($manual)) { Warn "Skipping provider config because model ID is still empty" Clear-ProviderSettings return } $script:ProviderModelId = $manual.Trim() } function Install-Node { $script:Stage = "install-node" if (Test-NodeVersionOk) { Log "Node already exists and the version is supported" & node -v return } if ($NodeMsiUrl) { Log "Installing Node from the provided MSI" $msi = Join-Path $env:TEMP "node-lts-x64.msi" Invoke-Retry -What "download node msi" -Script { Invoke-WebRequestCompat -Uri $NodeMsiUrl -OutFile $msi -TimeoutSec 180 | Out-Null } Invoke-Retry -What "install node msi" -Script { $p = Start-Process msiexec.exe -ArgumentList "/i `"$msi`" /qn /norestart" -Wait -PassThru -NoNewWindow if ($p.ExitCode -ne 0) { throw "msiexec exit code: $($p.ExitCode)" } } Refresh-Path if (-not (Test-NodeVersionOk)) { Fail "Node was not detected after MSI installation" } & node -v return } if (Test-Command "winget") { Log "Installing Node LTS via winget" Invoke-Retry -What "winget install node" -Script { & winget install --id OpenJS.NodeJS.LTS -e --accept-package-agreements --accept-source-agreements --silent if ($LASTEXITCODE -ne 0) { throw "winget exit code: $LASTEXITCODE" } } Refresh-Path if (-not (Test-NodeVersionOk)) { Fail "Node was not detected after winget installation" } & node -v return } Fail "Unable to install Node: winget is unavailable and -NodeMsiUrl was not provided" } function Install-OpenClaw { $script:Stage = "install-openclaw" Refresh-Path $npm = Get-NpmCmd if ($OpenClawPackageUrl) { Log "Installing OpenClaw from the provided tgz" $tgz = Join-Path $env:TEMP "openclaw.tgz" Invoke-Retry -What "download openclaw tgz" -Script { Invoke-WebRequestCompat -Uri $OpenClawPackageUrl -OutFile $tgz -TimeoutSec 180 | Out-Null } Invoke-Retry -What "npm install openclaw tgz" -Script { & $npm install -g $tgz --fetch-retries=5 --fetch-retry-mintimeout=2000 --fetch-retry-maxtimeout=20000 if ($LASTEXITCODE -ne 0) { throw "npm exit code: $LASTEXITCODE" } } } else { $registry = Resolve-NpmRegistry Log "Installing OpenClaw from npm: $registry" Invoke-Retry -What "npm install openclaw" -Script { & $npm install -g openclaw@latest ` --registry=$registry ` --fetch-retries=5 ` --fetch-retry-mintimeout=2000 ` --fetch-retry-maxtimeout=20000 if ($LASTEXITCODE -ne 0) { throw "npm exit code: $LASTEXITCODE" } } } Refresh-Path $openclaw = Get-OpenClawCmd & $openclaw --version } function Get-CustomProviderApi { param([string]$Compatibility) return "openai-completions" } function Get-CustomProviderId { param([string]$BaseUrl) $raw = "custom" try { $uri = [Uri]$BaseUrl $raw = "custom-" + $uri.Host if (-not $uri.IsDefaultPort -and $uri.Port -gt 0) { $raw += "-" + $uri.Port } } catch {} $normalized = ($raw.ToLowerInvariant() -replace '[^a-z0-9-]', '-') -replace '^-+|-+$', '' if ([string]::IsNullOrWhiteSpace($normalized)) { return "custom" } return $normalized } function Get-OpenClawPackageRoot { $candidates = New-Object System.Collections.Generic.List[string] try { $openclawCmd = Get-OpenClawCmd $cmdDir = Split-Path -Parent $openclawCmd if (-not [string]::IsNullOrWhiteSpace($cmdDir)) { $candidates.Add((Join-Path $cmdDir "node_modules\openclaw")) } } catch {} try { $npm = Get-NpmCmd $globalRoot = (& $npm root -g | Select-Object -First 1).Trim() if (-not [string]::IsNullOrWhiteSpace($globalRoot)) { $candidates.Add((Join-Path $globalRoot "openclaw")) } } catch {} foreach ($candidate in ($candidates | Select-Object -Unique)) { if (-not [string]::IsNullOrWhiteSpace($candidate) -and (Test-Path (Join-Path $candidate "package.json"))) { return $candidate } } Fail "Unable to locate the installed OpenClaw package root" } function Get-DocumentExtractPdfjsSpec { $packageRoot = Get-OpenClawPackageRoot $packageJsonPath = Join-Path $packageRoot "package.json" try { $packageJson = Get-Content -Raw -Path $packageJsonPath | ConvertFrom-Json $version = $packageJson.dependencies."pdfjs-dist" if (-not [string]::IsNullOrWhiteSpace($version)) { return "pdfjs-dist@$version" } } catch {} return "pdfjs-dist@^5.6.205" } function Set-ProviderConfig { param([string]$OpenClaw) $providerId = Get-CustomProviderId $script:ProviderBaseUrl $modelRef = "$providerId/$($script:ProviderModelId)" $providerConfig = @{ baseUrl = $script:ProviderBaseUrl api = Get-CustomProviderApi $script:ProviderCompatibility models = @( @{ id = $script:ProviderModelId name = "$($script:ProviderModelId) (Custom Provider)" contextWindow = 16000 maxTokens = 4096 input = @("text") cost = @{ input = 0 output = 0 cacheRead = 0 cacheWrite = 0 } reasoning = $false } ) } if (-not [string]::IsNullOrWhiteSpace($script:ProviderApiKey)) { $providerConfig["apiKey"] = $script:ProviderApiKey } $batch = @( @{ path = "models.mode"; value = "merge" }, @{ path = "models.providers.$providerId"; value = $providerConfig }, @{ path = "agents.defaults.model.primary"; value = $modelRef }, @{ path = "agents.defaults.models[$modelRef]"; value = @{} } ) $batchFile = Join-Path $env:TEMP "openclaw-config-set-batch.json" $batch | ConvertTo-Json -Depth 20 | Set-Content -Path $batchFile -Encoding UTF8 Log "Writing provider config: $modelRef" Invoke-Retry -What "openclaw config set provider batch" -Script { & $OpenClaw config set --batch-file $batchFile if ($LASTEXITCODE -ne 0) { throw "openclaw config set --batch-file exit code: $LASTEXITCODE" } } } function Initialize-OpenClaw { $script:Stage = "initialize-openclaw" $openclaw = Get-OpenClawCmd Log "Initializing OpenClaw workspace and config" Invoke-Retry -What "openclaw setup" -Script { & $openclaw setup --workspace $script:Workspace if ($LASTEXITCODE -ne 0) { throw "openclaw setup exit code: $LASTEXITCODE" } } Prompt-ProviderSettings Ensure-ProviderModelId if ($script:ProviderBaseUrl -or $script:ProviderModelId -or $script:ProviderApiKey) { if ([string]::IsNullOrWhiteSpace($script:ProviderBaseUrl)) { Fail "ProviderBaseUrl is required when model provider settings are used" } if ([string]::IsNullOrWhiteSpace($script:ProviderModelId)) { Fail "ProviderModelId is required when model provider settings are used" } Set-ProviderConfig -OpenClaw $openclaw return } Log "Skipping provider config" } function Prewarm-DocumentExtractRuntimeDeps { $script:Stage = "prewarm-document-extract" $npm = Get-NpmCmd $packageRoot = Get-OpenClawPackageRoot $pdfjsSpec = Get-DocumentExtractPdfjsSpec $pdfjsSentinel = Join-Path $packageRoot "node_modules\pdfjs-dist\package.json" if (Test-Path $pdfjsSentinel) { Log "document-extract runtime dependency already present: pdfjs-dist" return } Log "Prewarming document-extract runtime dependency: $pdfjsSpec" Invoke-Retry -What "npm install pdfjs-dist" -Script { & $npm install --prefix $packageRoot --no-save --omit=dev $pdfjsSpec if ($LASTEXITCODE -ne 0) { throw "npm install pdfjs-dist exit code: $LASTEXITCODE" } } if (-not (Test-Path $pdfjsSentinel)) { Fail "pdfjs-dist was not detected after prewarm" } } function Patch-ControlUiDeviceTokenMismatchRecovery { $script:Stage = "patch-control-ui-device-token-mismatch" $packageRoot = Get-OpenClawPackageRoot $assetsDir = Join-Path $packageRoot "dist\control-ui\assets" if (-not (Test-Path $assetsDir)) { Warn "control-ui assets directory not found; skipping device-token mismatch hotfix" return } $oldExpr = 't.selectedAuth.canFallbackToShared&&t.deviceIdentity&&n===L.AUTH_DEVICE_TOKEN_MISMATCH&&Lt({deviceId:t.deviceIdentity.deviceId,role:t.role}),this.ws?.close(dr,`connect failed`)' $newExpr = 't.deviceIdentity&&n===L.AUTH_DEVICE_TOKEN_MISMATCH&&(this.pendingDeviceTokenRetry=!1,this.deviceTokenRetryBudgetUsed=!1,Lt({deviceId:t.deviceIdentity.deviceId,role:t.role}),Gf(this.opts.url,``)),this.ws?.close(dr,`connect failed`)' $oldPattern = 't\.selectedAuth\.canFallbackToShared&&t\.deviceIdentity&&n===L\.AUTH_DEVICE_TOKEN_MISMATCH&&Lt\(\{deviceId:t\.deviceIdentity\.deviceId,role:t\.role\}\),this\.ws\?\.close\(dr,`connect failed`\)' $patched = 0 $alreadyPatched = 0 Get-ChildItem -Path $assetsDir -Filter "index-*.js" -File | ForEach-Object { $path = $_.FullName $content = Get-Content -Raw -Path $path if ($content.Contains($newExpr)) { $alreadyPatched += 1 return } $updated = $null if ($content.Contains($oldExpr)) { $updated = $content.Replace($oldExpr, $newExpr) } elseif ([regex]::IsMatch($content, $oldPattern)) { $updated = [regex]::Replace($content, $oldPattern, [System.Text.RegularExpressions.MatchEvaluator]{ param($match) $newExpr }, 1) } else { return } $utf8NoBom = New-Object System.Text.UTF8Encoding($false) [System.IO.File]::WriteAllText($path, $updated, $utf8NoBom) $patched += 1 } if ($patched -gt 0) { Log "Applied Control UI device-token mismatch hotfix to $patched asset file(s)" return } if ($alreadyPatched -gt 0) { Log "Control UI device-token mismatch hotfix already present" return } Warn "Control UI device-token mismatch hotfix signature not found; leaving installed bundle unchanged" } function Get-DashboardUrlFromOutput { param([string]$Text) if ([string]::IsNullOrWhiteSpace($Text)) { return "" } $match = [regex]::Match($Text, 'https?://\S+') if ($match.Success) { return $match.Value.Trim() } return "" } function Open-Url { param([string]$Url) if ([string]::IsNullOrWhiteSpace($Url)) { return $false } try { Start-Process $Url | Out-Null return $true } catch { Warn "opening URL failed: $($_.Exception.Message)" return $false } } function Wait-DashboardReady { param([string]$OpenClaw) $script:Stage = "wait-dashboard-ready" for ($attempt = 1; $attempt -le 30; $attempt++) { try { $output = & $OpenClaw dashboard --no-open 2>&1 if ($LASTEXITCODE -eq 0) { $text = ($output | Out-String).Trim() $url = Get-DashboardUrlFromOutput -Text $text if (-not [string]::IsNullOrWhiteSpace($url)) { return $url } } } catch {} Start-Sleep -Seconds 2 } return "" } function Install-Gateway { $script:Stage = "install-gateway" $openclaw = Get-OpenClawCmd Log "Setting gateway.mode=local" try { & $openclaw config set gateway.mode local if ($LASTEXITCODE -ne 0) { throw "openclaw config set gateway.mode local exit code: $LASTEXITCODE" } } catch { Warn "setting gateway.mode failed: $($_.Exception.Message)" } Log "Setting discovery.mdns.mode=off" try { & $openclaw config set discovery.mdns.mode off if ($LASTEXITCODE -ne 0) { throw "openclaw config set discovery.mdns.mode off exit code: $LASTEXITCODE" } } catch { Warn "setting discovery.mdns.mode failed: $($_.Exception.Message)" } Log "Installing Gateway auto-start" try { & $openclaw gateway install --runtime node --json if ($LASTEXITCODE -ne 0) { throw "openclaw gateway install exit code: $LASTEXITCODE" } } catch { Warn "gateway install failed: $($_.Exception.Message)" } Log "Starting Gateway now" try { & $openclaw gateway start if ($LASTEXITCODE -ne 0) { throw "openclaw gateway start exit code: $LASTEXITCODE" } } catch { Warn "gateway start failed: $($_.Exception.Message)" } Log "Checking Gateway status" try { & $openclaw gateway status --json } catch { Warn "gateway status check failed: $($_.Exception.Message)" } } function Show-DashboardAccess { $script:Stage = "dashboard-access" $openclaw = Get-OpenClawCmd Log "Waiting for Gateway dashboard readiness" $dashboardUrl = Wait-DashboardReady -OpenClaw $openclaw if ([string]::IsNullOrWhiteSpace($dashboardUrl)) { Warn "dashboard readiness check timed out" } Log "Dashboard access" Write-Host "Do not open http://127.0.0.1:18789 directly. It requires a tokenized dashboard URL." -ForegroundColor Yellow if (-not [string]::IsNullOrWhiteSpace($dashboardUrl)) { Write-Host "Dashboard URL:" -ForegroundColor Green Write-Host $dashboardUrl Write-Host "Trying to open the tokenized dashboard URL in your default browser..." -ForegroundColor Yellow if (-not (Open-Url -Url $dashboardUrl)) { Warn "dashboard browser open failed; use the printed URL manually" } return } Write-Host "Gateway is still warming up. Run this after a bit:" -ForegroundColor Yellow Write-Host "openclaw dashboard --no-open" -ForegroundColor Green } function Main { Log "Log file: $script:LogFile" Set-ProxyIfNeeded Refresh-Path Install-Node Install-OpenClaw Patch-ControlUiDeviceTokenMismatchRecovery Initialize-OpenClaw Prewarm-DocumentExtractRuntimeDeps Install-Gateway Show-DashboardAccess $script:Stage = "done" Log "Installation finished" Write-Host "OpenClaw installation is complete." -ForegroundColor Green } try { Main exit 0 } catch { Write-Error "[FAILED] stage=$script:Stage`n$($_.Exception.Message)" Write-Host "`nLog file: $script:LogFile" -ForegroundColor Yellow exit 1 } finally { try { Stop-Transcript | Out-Null } catch {} }