################################################################################ # 闪电API - OpenClaw 一键安装脚本 (Windows PowerShell) # # 功能: 安装 OpenClaw + 配置 闪电API (Claude + GPT 两个供应商) # 官网: flashapi.top # 模型调用: ai.flashapi.top (国内直连) / cdn.flashapi.top (海外CDN) # # 用法 (PowerShell): irm https://sh.flashapi.top/openclaw.ps1 | iex # # 兼容 Windows PowerShell 5.1 与 PowerShell 7+;不需要管理员权限,所有操作在用户目录下完成 ################################################################################ & { $ErrorActionPreference = 'Stop' $ProgressPreference = 'SilentlyContinue' try { [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 } catch {} try { [Console]::OutputEncoding = [Text.Encoding]::UTF8 } catch {} # 通过管道传给 node/openclaw 的文本使用 UTF-8 (无 BOM) $OutputEncoding = New-Object Text.UTF8Encoding $false ############################################################################## # 常量定义 ############################################################################## $OnWindows = ([Environment]::OSVersion.Platform -eq [PlatformID]::Win32NT) $UserHome = $env:USERPROFILE if (-not $UserHome) { $UserHome = $HOME } $NpmMirror = 'https://registry.npmmirror.com' $OpenClawPackage = 'openclaw' $OfficialInstallerPs1 = 'https://openclaw.ai/install.ps1' # OpenClaw 2026.9.x 要求 Node >=24.16.0 <25 或 >=26.1.0 (package.json engines) $NodeRequirementText = 'Node.js 24.16+ (24.x) 或 26.1+' # OpenClaw 配置文件: $env:OPENCLAW_CONFIG_PATH,否则 (OPENCLAW_STATE_DIR 或 %USERPROFILE%\.openclaw)\openclaw.json (JSON5) $OcBaseHome = $UserHome if ($env:OPENCLAW_HOME) { $OcBaseHome = $env:OPENCLAW_HOME } $OcStateDir = Join-Path $OcBaseHome '.openclaw' if ($env:OPENCLAW_STATE_DIR) { $OcStateDir = $env:OPENCLAW_STATE_DIR } $DefaultConfigFile = Join-Path $OcStateDir 'openclaw.json' if ($env:OPENCLAW_CONFIG_PATH) { $DefaultConfigFile = $env:OPENCLAW_CONFIG_PATH } $ProviderClaude = 'flashapi-claude' $ProviderGpt = 'flashapi-gpt' $DefaultClaudeModel = 'claude-opus-5-5' $DefaultGptModel = 'gpt-6.1-sol' $DefaultThinking = 'medium' $BigClaude = @('claude-opus-5-5', 'claude-sonnet-5-5', 'claude-fable-5-1', 'claude-fable-5', 'claude-opus-5', 'claude-sonnet-5', 'claude-opus-4-8') $BigGpt = @('gpt-6.1-sol', 'gpt-6-sol', 'gpt-6-astra', 'gpt-6-luna') # 可变状态放在哈希表里 (子函数可直接修改,且不污染用户会话) $S = @{ ConfigFile = $DefaultConfigFile DoClaude = $false DoGpt = $false FamilyLabel = '' Json5Module = '' } $Line = '───────────────────────────────────────────────────────' ############################################################################## # 工具函数 ############################################################################## # 输出带颜色的一行: Say '文本' '颜色' '文本' '颜色' ... (颜色为空 = 默认色) function Write-Parts { param([object[]]$Parts, [bool]$NoNewline) if ($null -ne $Parts) { for ($i = 0; $i -lt $Parts.Count; $i += 2) { $text = [string]$Parts[$i] $color = '' if ($i + 1 -lt $Parts.Count) { $color = [string]$Parts[$i + 1] } if ($color) { Write-Host $text -NoNewline -ForegroundColor $color } else { Write-Host $text -NoNewline } } } if (-not $NoNewline) { Write-Host '' } } function Say { Write-Parts -Parts $args -NoNewline $false } function SayN { Write-Parts -Parts $args -NoNewline $true } function Read-Line { # 读取一行输入 (与 bash 的 read -r 一样去掉首尾空白);输入流结束时返回 $null $v = Read-Host if ($null -eq $v) { return $null } return ([string]$v).Trim() } function Read-YesNo { param([string]$Prompt) SayN " $Prompt (y/N): " 'Yellow' $a = Read-Line return ($a -eq 'y' -or $a -eq 'Y') } function Print-Banner { Say '' Say ' ' '' ' ⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡' 'Yellow' Say ' ' '' ' ██████╗ ██████╗ ███████╗███╗ ██╗ ██████╗██╗ █████╗ ██╗ ██╗' 'Yellow' Say ' ' '' '██╔═══██╗██╔══██╗██╔════╝████╗ ██║██╔════╝██║ ██╔══██╗██║ ██║' 'Yellow' Say ' ' '' '██║ ██║██████╔╝█████╗ ██╔██╗ ██║██║ ██║ ███████║██║ █╗ ██║' 'Yellow' Say ' ' '' '██║ ██║██╔═══╝ ██╔══╝ ██║╚██╗██║██║ ██║ ██╔══██║██║███╗██║' 'Yellow' Say ' ' '' '╚██████╔╝██║ ███████╗██║ ╚████║╚██████╗███████╗██║ ██║╚███╔███╔╝' 'Yellow' Say ' ' '' ' ╚═════╝ ╚═╝ ╚══════╝╚═╝ ╚═══╝ ╚═════╝╚══════╝╚═╝ ╚═╝ ╚══╝╚══╝' 'Yellow' Say ' ' '' ' ⚡ 闪电API × OpenClaw ⚡' 'Blue' Say ' ' '' ' ⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡' 'Yellow' Say '' Say " $Line" 'DarkGray' Say ' ' '' '⚡ 闪电API' 'White' ' ' '' '|' 'DarkGray' ' ' '' '官网' 'Cyan' ' ' '' 'flashapi.top' 'Yellow' Say ' ' '' 'OpenClaw 一键配置' 'White' ' ' '' '|' 'DarkGray' ' ' '' '让顶尖模型触手可及' 'Cyan' Say " $Line" 'DarkGray' Say ' ' '' '国内直连:' 'DarkGray' ' ' '' 'ai.flashapi.top' 'Green' ' ' '' '海外CDN:' 'DarkGray' ' ' '' 'cdn.flashapi.top' 'Green' Say " $Line" 'DarkGray' Say '' } function Print-Status { param([string]$Type, [string]$Message) switch ($Type) { 'ok' { Say ' ' '' '[⚡]' 'Green' " $Message" '' } 'info' { Say ' ' '' '[*]' 'Cyan' " $Message" '' } 'warn' { Say ' ' '' '[!]' 'Yellow' " $Message" '' } 'error' { Say ' ' '' '[x]' 'Red' " $Message" '' } default { Say " $Message" } } } # 读取密码输入 (星号遮盖) function Read-Secret { param([string]$Prompt) SayN $Prompt 'White' $sec = Read-Host -AsSecureString if ($null -eq $sec -or $sec.Length -eq 0) { return '' } $plain = '' $bstr = [IntPtr]::Zero try { $bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($sec) $plain = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr) } catch { $plain = (New-Object System.Management.Automation.PSCredential('x', $sec)).GetNetworkCredential().Password } finally { if ($bstr -ne [IntPtr]::Zero) { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) } } if ($null -eq $plain) { return '' } return $plain.Trim() } function Mask-Key { param([string]$Value) if (-not $Value) { return '' } if ($Value.Length -le 12) { return $Value } return $Value.Substring(0, 8) + '...' + $Value.Substring($Value.Length - 4) } function Backup-IfExists { param([string]$File) if (Test-Path -LiteralPath $File -PathType Leaf) { $backup = $File + '.bak.' + (Get-Date -Format 'yyyyMMddHHmmss') Copy-Item -LiteralPath $File -Destination $backup -Force Print-Status 'info' "已备份现有文件: $backup" } } function Ensure-Dir { param([string]$Dir) if (-not (Test-Path -LiteralPath $Dir -PathType Container)) { New-Item -ItemType Directory -Path $Dir -Force | Out-Null } } # 写文件: UTF-8 无 BOM,LF 换行 function Write-Utf8File { param([string]$Path, [string]$Text) [IO.File]::WriteAllText($Path, $Text, (New-Object Text.UTF8Encoding $false)) } # 在 PATH 中查找可执行文件 (优先 .exe/.cmd,避开受执行策略限制的 .ps1 shim) function Find-Exe { param([string[]]$Names) foreach ($n in $Names) { $c = Get-Command $n -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1 if ($null -ne $c) { return $c.Path } } return $null } function Add-SessionPath { param([string]$Dir) if (-not $Dir) { return } if (-not (Test-Path -LiteralPath $Dir -PathType Container)) { return } $sep = [IO.Path]::PathSeparator $parts = @($env:PATH -split [regex]::Escape([string]$sep)) if ($parts -notcontains $Dir) { $env:PATH = $Dir + $sep + $env:PATH } } ############################################################################## # JSON 工具 (PSCustomObject 读写;输出格式与 node 的 JSON.stringify(x, null, 2) 一致) ############################################################################## $JsonDateKind = $false try { $JsonDateKind = (Get-Command ConvertFrom-Json).Parameters.ContainsKey('DateKind') } catch {} function Test-JsonObject { param($Value) return ($null -ne $Value -and $Value.GetType().FullName -eq 'System.Management.Automation.PSCustomObject') } function ConvertFrom-JsonText { param([string]$Text) $opts = @{} if ($JsonDateKind) { $opts['DateKind'] = 'String' } return (ConvertFrom-Json -InputObject $Text @opts) } function New-JsonObject { return (New-Object -TypeName PSObject) } function Get-Prop { param($Obj, [string]$Name) if ($null -eq $Obj) { return $null } if ($Obj -is [Collections.IDictionary]) { if ($Obj.Contains($Name)) { return $Obj[$Name] } return $null } if (-not (Test-JsonObject $Obj)) { return $null } $p = $Obj.PSObject.Properties[$Name] if ($null -eq $p) { return $null } return $p.Value } function Set-Prop { param($Obj, [string]$Name, $Value) $p = $Obj.PSObject.Properties[$Name] if ($null -ne $p) { $p.Value = $Value } else { Add-Member -InputObject $Obj -MemberType NoteProperty -Name $Name -Value $Value } } function Remove-Prop { param($Obj, [string]$Name) if ($null -ne $Obj.PSObject.Properties[$Name]) { $Obj.PSObject.Properties.Remove($Name) } } function ConvertTo-JsonString { param([string]$s) $sb = New-Object System.Text.StringBuilder [void]$sb.Append('"') foreach ($ch in $s.ToCharArray()) { $code = [int]$ch switch ($code) { 34 { [void]$sb.Append('\"') } 92 { [void]$sb.Append('\\') } 8 { [void]$sb.Append('\b') } 12 { [void]$sb.Append('\f') } 10 { [void]$sb.Append('\n') } 13 { [void]$sb.Append('\r') } 9 { [void]$sb.Append('\t') } default { if ($code -lt 32) { [void]$sb.Append('\u' + $code.ToString('x4')) } else { [void]$sb.Append($ch) } } } } [void]$sb.Append('"') return $sb.ToString() } function ConvertTo-JsonText { param($Value, [int]$Level = 0) $inv = [Globalization.CultureInfo]::InvariantCulture if ($null -eq $Value) { return 'null' } if ($Value -is [string]) { return (ConvertTo-JsonString $Value) } if ($Value -is [char]) { return (ConvertTo-JsonString ([string]$Value)) } if ($Value -is [bool]) { if ($Value) { return 'true' } else { return 'false' } } if ($Value -is [DateTime]) { return (ConvertTo-JsonString ($Value.ToString('yyyy-MM-ddTHH:mm:ss.FFFFFFFK', $inv))) } if ($Value -is [DateTimeOffset]) { return (ConvertTo-JsonString ($Value.ToString('yyyy-MM-ddTHH:mm:ss.FFFFFFFzzz', $inv))) } if ($Value -is [double] -or $Value -is [float]) { $d = [double]$Value if ([double]::IsNaN($d) -or [double]::IsInfinity($d)) { return 'null' } return $d.ToString('R', $inv) } if ($Value -is [decimal] -or $Value -is [int] -or $Value -is [long] -or $Value -is [int16] -or $Value -is [byte] -or $Value -is [sbyte] -or $Value -is [uint16] -or $Value -is [uint32] -or $Value -is [uint64]) { return ([IFormattable]$Value).ToString($null, $inv) } if ($Value.GetType().FullName -eq 'System.Numerics.BigInteger') { return $Value.ToString() } $pad = ' ' * ($Level + 1) $endPad = ' ' * $Level if ($Value -is [Collections.IDictionary]) { $items = New-Object System.Collections.Generic.List[string] foreach ($k in $Value.Keys) { $items.Add($pad + (ConvertTo-JsonString ([string]$k)) + ': ' + (ConvertTo-JsonText $Value[$k] ($Level + 1))) } if ($items.Count -eq 0) { return '{}' } return "{`n" + ($items -join ",`n") + "`n" + $endPad + '}' } if (Test-JsonObject $Value) { $items = New-Object System.Collections.Generic.List[string] foreach ($p in $Value.PSObject.Properties) { $items.Add($pad + (ConvertTo-JsonString $p.Name) + ': ' + (ConvertTo-JsonText $p.Value ($Level + 1))) } if ($items.Count -eq 0) { return '{}' } return "{`n" + ($items -join ",`n") + "`n" + $endPad + '}' } if ($Value -is [Collections.IEnumerable]) { $items = New-Object System.Collections.Generic.List[string] foreach ($e in $Value) { $items.Add($pad + (ConvertTo-JsonText $e ($Level + 1))) } if ($items.Count -eq 0) { return '[]' } return "[`n" + ($items -join ",`n") + "`n" + $endPad + ']' } return (ConvertTo-JsonString ([string]$Value)) } ############################################################################## # 功能 1: 安装 OpenClaw ############################################################################## function Get-NodeExe { return (Find-Exe @('node.exe', 'node')) } function Get-NpmExe { return (Find-Exe @('npm.cmd', 'npm.exe', 'npm')) } function Get-OpenClawExe { return (Find-Exe @('openclaw.cmd', 'openclaw.exe', 'openclaw')) } function Get-NodeVersion { $node = Get-NodeExe if (-not $node) { return '' } $ErrorActionPreference = 'Continue' $v = '' try { $v = [string](& $node -v 2>$null | Select-Object -First 1) } catch { $v = '' } if ($null -eq $v) { return '' } return $v.Trim() } # Node 版本是否满足 OpenClaw 要求: >=24.16 <25 或 >=26.1 function Test-NodeVersionOk { $v = Get-NodeVersion if ($v -notmatch '^v?(\d+)\.(\d+)\.') { return $false } $major = [int]$Matches[1] $minor = [int]$Matches[2] if ($major -eq 24 -and $minor -ge 16) { return $true } if ($major -eq 26 -and $minor -ge 1) { return $true } if ($major -ge 27) { return $true } return $false } # 已安装返回版本号,未安装返回 $null function Check-OpenClawInstalled { $exe = Get-OpenClawExe if (-not $exe) { return $null } $ErrorActionPreference = 'Continue' $out = $null try { $out = & $exe --version 2>$null | Select-Object -First 1 } catch { $out = $null } if ($out) { return ([string]$out).Trim() } return '版本未知 (可能 Node.js 版本过低)' } # openclaw 命令可用 (已安装且 Node 版本满足) 时,优先用官方 `openclaw config patch` 写配置 function Test-OpenClawUsable { $exe = Get-OpenClawExe if (-not $exe) { return $false } $ErrorActionPreference = 'Continue' try { & $exe --version 2>$null | Out-Null return ($LASTEXITCODE -eq 0) } catch { return $false } } function Get-NpmAllowScriptsArg { param([string]$Npm) $ErrorActionPreference = 'Continue' $v = '' try { $v = [string](& $Npm --version 2>$null | Select-Object -First 1) } catch { $v = '' } if ($v -match '^(\d+)\.(\d+)') { $major = [int]$Matches[1] $minor = [int]$Matches[2] if ($major -gt 11 -or ($major -eq 11 -and $minor -ge 16)) { return "--allow-scripts=$OpenClawPackage" } } return '' } function Invoke-NpmInstall { param([string]$Npm, [string]$Registry) $ErrorActionPreference = 'Continue' $npmArgs = New-Object System.Collections.Generic.List[string] foreach ($a in @('install', '-g', '--no-fund', '--no-audit')) { $npmArgs.Add($a) } $allow = Get-NpmAllowScriptsArg $Npm if ($allow) { $npmArgs.Add($allow) } $npmArgs.Add("$OpenClawPackage@latest") if ($Registry) { $npmArgs.Add("--registry=$Registry") } try { & $Npm $npmArgs.ToArray() | Out-Host return ($LASTEXITCODE -eq 0) } catch { return $false } } function Install-WithMirror { param([string]$Npm) Print-Status 'info' '切换国内镜像 (npmmirror.com) 安装 ...' return (Invoke-NpmInstall $Npm $NpmMirror) } function Add-NpmBinToPath { $npm = Get-NpmExe if (-not $npm) { return } $ErrorActionPreference = 'Continue' $prefix = '' try { $prefix = [string](& $npm prefix -g 2>$null | Select-Object -First 1) } catch { $prefix = '' } if (-not $prefix) { return } $prefix = $prefix.Trim() if ($OnWindows) { Add-SessionPath $prefix } else { Add-SessionPath (Join-Path $prefix 'bin') } } # 官方安装器 (install.ps1,会自动安装 Node.js) 在子进程中运行: # 它内部失败时会调用 exit,放在子进程里不会关掉当前窗口 function Invoke-OfficialInstaller { $ErrorActionPreference = 'Continue' $psExe = $null try { $psExe = (Get-Process -Id $PID).Path } catch {} if (-not $psExe) { $psExe = 'powershell.exe' } $cmd = '[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12; & ([scriptblock]::Create((irm ' + $OfficialInstallerPs1 + '))) -NoOnboard' try { & $psExe -NoProfile -ExecutionPolicy Bypass -Command $cmd | Out-Host return ($LASTEXITCODE -eq 0) } catch { return $false } } function Show-NodeHelp { Say '' Say ' ' '' "OpenClaw 需要 $NodeRequirementText,安装方式:" 'White' Say ' ' '' '官网下载:' 'Cyan' ' https://nodejs.org/ (选择 24 LTS)' '' Say ' ' '' 'winget:' 'Cyan' ' winget install OpenJS.NodeJS.LTS' '' Say ' ' '' '官方脚本:' 'Cyan' " & ([scriptblock]::Create((irm $OfficialInstallerPs1))) -NoOnboard" '' } function Install-OpenClaw { Say '' Print-Status 'info' '⚡ 正在安装 OpenClaw ...' Say '' if ($OnWindows) { Print-Status 'info' '检测到 Windows (PowerShell) 环境' } $ver = Check-OpenClawInstalled if ($null -ne $ver) { Print-Status 'ok' "OpenClaw 已安装 ($ver)" Say '' if (-not (Read-YesNo '是否重新安装/升级到最新版?')) { Print-Status 'info' '跳过安装'; return $true } } $npm = Get-NpmExe if (-not $npm -or -not (Test-NodeVersionOk)) { $nv = Get-NodeVersion if ($nv) { Print-Status 'warn' "当前 Node.js 版本 $nv,OpenClaw 需要 $NodeRequirementText" } else { Print-Status 'warn' "未找到 Node.js / npm,OpenClaw 需要 $NodeRequirementText" } Show-NodeHelp Say '' if (Read-YesNo '是否使用 OpenClaw 官方安装脚本自动安装 (含 Node.js)?') { Print-Status 'info' '运行 OpenClaw 官方安装脚本 (会自动安装合适版本的 Node.js) ...' Say '' if (Invoke-OfficialInstaller) { Say '' Add-NpmBinToPath $ver = Check-OpenClawInstalled if ($null -ne $ver) { Print-Status 'ok' "⚡ OpenClaw 安装成功 ($ver)" } else { Print-Status 'warn' '安装已完成,但当前窗口未找到 openclaw,请重新打开 PowerShell 后再试' } return $true } Print-Status 'error' '官方安装脚本执行失败,请按上面的方式手动安装 Node.js 后重新运行本脚本' return $false } Print-Status 'info' '已取消,请升级 Node.js 后重新运行本脚本' return $false } $ErrorActionPreference = 'Continue' $npmVer = '' try { $npmVer = [string](& $npm --version 2>$null | Select-Object -First 1) } catch {} Print-Status 'ok' "npm 已就绪 ($npmVer), Node $(Get-NodeVersion)" Print-Status 'info' "使用 npm 安装 $OpenClawPackage@latest (文件较多,可能需要几分钟) ..." Say '' if (-not (Invoke-NpmInstall $npm '')) { Print-Status 'warn' '默认源安装失败,可能是网络问题' if (-not (Install-WithMirror $npm)) { Print-Status 'error' '所有安装方式均失败,请检查网络连接' Print-Status 'info' "也可以使用官方安装脚本: & ([scriptblock]::Create((irm $OfficialInstallerPs1))) -NoOnboard" return $false } } Say '' Add-NpmBinToPath $ver = Check-OpenClawInstalled if ($null -ne $ver) { Print-Status 'ok' "⚡ OpenClaw 安装成功 ($ver)" } else { Print-Status 'warn' '安装已完成,但未在 PATH 中找到 openclaw' Print-Status 'info' '请重新打开 PowerShell 后再试' } return $true } ############################################################################## # 功能 2: 配置 API ############################################################################## # 找到 OpenClaw 自带的 json5 解析器 (用于读取带注释的 openclaw.json) function Find-Json5Module { $S.Json5Module = '' $npm = Get-NpmExe if (-not $npm) { return } $ErrorActionPreference = 'Continue' $root = '' try { $root = [string](& $npm root -g 2>$null | Select-Object -First 1) } catch { $root = '' } if (-not $root) { return } $dir = Join-Path (Join-Path (Join-Path $root.Trim() 'openclaw') 'node_modules') 'json5' if (Test-Path -LiteralPath (Join-Path $dir 'package.json') -PathType Leaf) { $S.Json5Module = $dir } } # 读取 openclaw.json (JSON5)。返回 @{ Data = PSCustomObject 或 $null; Strict = 是否严格 JSON; Exists } function Read-OpenClawConfig { param([string]$Path) $r = @{ Data = $null; Strict = $true; Exists = $false; Ok = $true } if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { return $r } $r.Exists = $true $text = [IO.File]::ReadAllText($Path) if ($text.Length -gt 0 -and [int]$text[0] -eq 0xFEFF) { $text = $text.Substring(1) } if (-not $text.Trim()) { $r.Data = New-JsonObject; return $r } # 是否包含注释/尾逗号等非严格 JSON 写法 (粗略判断,仅用于提示) if ($text -match '(^|\s)//|/\*|,\s*[\}\]]') { $r.Strict = $false } $obj = $null # 先尝试用 node + OpenClaw 自带 json5 解析 (与 OpenClaw 行为一致) $node = Get-NodeExe if ($node -and $S.Json5Module) { $js = "const fs=require('fs');const J=require(process.argv[2]);const d=J.parse(fs.readFileSync(process.argv[1],'utf8').replace(/^/,''));process.stdout.write(JSON.stringify(d));" $ErrorActionPreference = 'Continue' try { $json = (($js | & $node - $Path $S.Json5Module 2>$null) -join "`n") if ($LASTEXITCODE -eq 0 -and $json) { $obj = ConvertFrom-JsonText $json } } catch { $obj = $null } $ErrorActionPreference = 'Stop' } if ($null -eq $obj) { try { $obj = ConvertFrom-JsonText $text } catch { $obj = $null } } if (Test-JsonObject $obj) { $r.Data = $obj } else { $r.Ok = $false } return $r } function Get-ProviderField { param($Cfg, [string]$Provider, [string]$Field) $p = Get-Prop (Get-Prop (Get-Prop $Cfg 'models') 'providers') $Provider $v = Get-Prop $p $Field if ($v -is [string]) { return $v } return '' } function Get-PrimaryModel { param($Cfg) $m = Get-Prop (Get-Prop (Get-Prop $Cfg 'agents') 'defaults') 'model' if ($m -is [string]) { return $m } $p = Get-Prop $m 'primary' if ($p -is [string]) { return $p } return '' } # GET /v1/models,返回 @{ Code = 状态码 (网络错误为 0); Ids = 模型 ID 数组 } function Get-ModelList { param([string]$BaseUrl, [string]$ApiKey) $res = @{ Code = 0; Ids = @() } try { $r = Invoke-WebRequest -Uri "$BaseUrl/v1/models" -Headers @{ Authorization = "Bearer $ApiKey" } -Method Get -UseBasicParsing -TimeoutSec 15 $res.Code = [int]$r.StatusCode try { $j = ConvertFrom-JsonText ([string]$r.Content) $list = Get-Prop $j 'data' $ids = New-Object System.Collections.Generic.List[string] if ($null -ne $list) { foreach ($m in $list) { $id = Get-Prop $m 'id' if ($id -is [string] -and $id) { $ids.Add($id) } } } $res.Ids = $ids.ToArray() } catch {} } catch { $ex = $_.Exception $resp = $null if ($null -ne $ex -and $null -ne $ex.PSObject.Properties['Response']) { $resp = $ex.Response } if ($null -ne $resp) { try { $res.Code = [int]$resp.StatusCode } catch { $res.Code = 0 } } } return $res } function Select-FamilyIds { param([string[]]$Ids, [string]$Family) $out = New-Object System.Collections.Generic.List[string] foreach ($id in $Ids) { if (-not $id) { continue } if ($Family -eq 'claude') { if (-not $id.StartsWith('claude-')) { continue } } else { if (-not $id.StartsWith('gpt-')) { continue } if ($id.Contains('compact')) { continue } } $out.Add($id) } return , $out.ToArray() } # 验证一个 Key;返回 @{ Ok = 是否继续; Ids = 该家族模型 ID } function Validate-Key { param([string]$Family, [string]$BaseUrl, [string]$ApiKey) $label = 'Claude' if ($Family -eq 'gpt') { $label = 'GPT' } Print-Status 'info' "正在验证 $label 分组 API Key ..." $r = Get-ModelList $BaseUrl $ApiKey $ids = @() if ($r.Code -eq 200) { $ids = Select-FamilyIds $r.Ids $Family if ($ids.Count -gt 0) { Print-Status 'ok' "⚡ $label Key 验证通过,获取到 $($ids.Count) 个 $label 模型" } else { Print-Status 'warn' "Key 有效,但模型列表里没有 $label 模型 —— 这个 Key 可能不是 $label 分组" Print-Status 'info' '请到 flashapi.top 确认该令牌所属分组,将只写入默认模型' } } elseif ($r.Code -eq 401) { Print-Status 'error' "$label API Key 无效 (401 Unauthorized)" if (-not (Read-YesNo '验证未通过,是否仍要保存?')) { Print-Status 'info' '已取消配置' return @{ Ok = $false; Ids = @() } } } elseif ($r.Code -eq 0) { Print-Status 'warn' "无法连接到 $BaseUrl (网络错误)" if (-not (Read-YesNo '验证未通过,是否仍要保存?')) { Print-Status 'info' '已取消配置' return @{ Ok = $false; Ids = @() } } } else { Print-Status 'warn' "API 返回状态码: $($r.Code) (可能仍可用),将只写入默认模型" } return @{ Ok = $true; Ids = $ids } } # 模型显示名: claude-opus-5-5 -> Claude Opus 5.5;gpt-6.1-sol -> GPT-6.1 Sol function Get-PrettyName { param([string]$Id) $parts = $Id.Split('-') if ($parts[0] -eq 'gpt') { $words = New-Object System.Collections.Generic.List[string] $second = '' if ($parts.Count -gt 1) { $second = $parts[1] } $words.Add("GPT-$second") for ($i = 2; $i -lt $parts.Count; $i++) { $p = $parts[$i] if ($p) { $words.Add($p.Substring(0, 1).ToUpper() + $p.Substring(1)) } } return (($words -join ' ').Trim()) } $out = New-Object System.Collections.Generic.List[string] $nums = New-Object System.Collections.Generic.List[string] foreach ($p in $parts) { if ($p -match '^\d{1,2}$') { $nums.Add($p); continue } if ($nums.Count -gt 0) { $out.Add(($nums -join '.')); $nums.Clear() } if ($p -match '^\d{8}$') { $out.Add("($p)") } elseif ($p) { $out.Add($p.Substring(0, 1).ToUpper() + $p.Substring(1)) } } if ($nums.Count -gt 0) { $out.Add(($nums -join '.')) } return ($out -join ' ') } function New-ModelEntry { param([string]$Family, [string]$Id) $big = $false if ($Family -eq 'claude') { $big = ($BigClaude -contains $Id) } else { $big = ($BigGpt -contains $Id) } $ctx = 272000 $max = 128000 if ($Family -eq 'claude') { if ($big) { $ctx = 1000000; $max = 128000 } else { $ctx = 200000; $max = 64000 } } else { if ($big) { $ctx = 1050000 } } $m = [ordered]@{} $m['id'] = $Id $m['name'] = (Get-PrettyName $Id) $m['reasoning'] = $true $m['input'] = @('text', 'image') $m['contextWindow'] = $ctx $m['maxTokens'] = $max return $m } function Get-ModelIdList { param([string[]]$Ids, [string]$Default) $list = New-Object System.Collections.Generic.List[string] $list.Add($Default) foreach ($id in $Ids) { if ($id -and -not $list.Contains($id)) { $list.Add($id) } } return , $list.ToArray() } function Get-ReplacePaths { $r = New-Object System.Collections.Generic.List[string] $r.Add('agents.defaults.model') if ($S.DoClaude) { $r.Add("models.providers.$ProviderClaude") } if ($S.DoGpt) { $r.Add("models.providers.$ProviderGpt") } return , $r.ToArray() } # 构造配置补丁 (与 sh 版 node 逻辑一致) function New-ConfigPatch { param($Existing, [string]$BaseUrl, [string]$ClaudeKey, [string[]]$ClaudeIds, [string]$GptKey, [string[]]$GptIds, [int[]]$DropFallbacks = @()) $exProviders = Get-Prop (Get-Prop $Existing 'models') 'providers' $exDefaults = Get-Prop (Get-Prop $Existing 'agents') 'defaults' $exCatalog = Get-Prop $exDefaults 'models' $exCatalogKeys = New-Object System.Collections.Generic.List[string] $usedAliases = New-Object System.Collections.Generic.List[string] if (Test-JsonObject $exCatalog) { foreach ($p in $exCatalog.PSObject.Properties) { $exCatalogKeys.Add($p.Name) if ($p.Name.StartsWith("$ProviderClaude/") -or $p.Name.StartsWith("$ProviderGpt/")) { continue } $al = Get-Prop $p.Value 'alias' if ($al -is [string]) { $usedAliases.Add($al.ToLower()) } } } $families = New-Object System.Collections.Generic.List[object] if ($S.DoClaude) { $families.Add(@{ P = $ProviderClaude; Family = 'claude'; Def = $DefaultClaudeModel; Key = $ClaudeKey; Ids = (Get-ModelIdList $ClaudeIds $DefaultClaudeModel); BaseUrl = $BaseUrl; Api = 'anthropic-messages' }) } if ($S.DoGpt) { $families.Add(@{ P = $ProviderGpt; Family = 'gpt'; Def = $DefaultGptModel; Key = $GptKey; Ids = (Get-ModelIdList $GptIds $DefaultGptModel); BaseUrl = "$BaseUrl/v1"; Api = 'openai-responses' }) } $providers = [ordered]@{} $catalog = [ordered]@{} foreach ($f in $families) { $models = New-Object System.Collections.Generic.List[object] foreach ($id in $f.Ids) { $models.Add((New-ModelEntry $f.Family $id)) } $prov = [ordered]@{} $prov['baseUrl'] = $f.BaseUrl $prov['apiKey'] = $f.Key $prov['api'] = $f.Api $prov['models'] = $models.ToArray() $providers[$f.P] = $prov foreach ($id in $f.Ids) { $entry = [ordered]@{} if ($id -eq $f.Def) { $alias = Get-PrettyName $id if (-not $usedAliases.Contains($alias.ToLower())) { $entry['alias'] = $alias } } $catalog["$($f.P)/$id"] = $entry } # 移除上次写入、但本次已不在模型列表里的条目 foreach ($ref in $exCatalogKeys) { if ($ref.StartsWith("$($f.P)/")) { $mid = $ref.Substring($f.P.Length + 1) if (-not ($f.Ids -contains $mid)) { $catalog[$ref] = $null } } } } $hasGpt = $S.DoGpt -or (Test-JsonObject (Get-Prop $exProviders $ProviderGpt)) $hasClaude = $S.DoClaude -or (Test-JsonObject (Get-Prop $exProviders $ProviderClaude)) $primary = "$ProviderGpt/$DefaultGptModel" if ($S.DoClaude) { $primary = "$ProviderClaude/$DefaultClaudeModel" } $fallbacks = New-Object System.Collections.Generic.List[string] if ($S.DoClaude -and $hasGpt) { $fallbacks.Add("$ProviderGpt/$DefaultGptModel") } if ((-not $S.DoClaude) -and $hasClaude) { $fallbacks.Add("$ProviderClaude/$DefaultClaudeModel") } # 保留用户自己的其他备用模型 $exModel = Get-Prop $exDefaults 'model' $exFallbacks = Get-Prop $exModel 'fallbacks' if ($null -ne $exFallbacks -and -not ($exFallbacks -is [string])) { foreach ($fb in $exFallbacks) { if (-not ($fb -is [string]) -or -not $fb) { continue } if ($fb.StartsWith("$ProviderClaude/") -or $fb.StartsWith("$ProviderGpt/") -or $fb -eq $primary -or $fallbacks.Contains($fb)) { continue } $fallbacks.Add($fb) } } # OpenClaw 校验时无法解析的备用模型: 按报错下标依次去掉 if ($null -ne $DropFallbacks) { foreach ($i in $DropFallbacks) { if ($i -ge 0 -and $i -lt $fallbacks.Count) { $fallbacks.RemoveAt($i) } } } $model = [ordered]@{} $model['primary'] = $primary $model['fallbacks'] = $fallbacks.ToArray() $defaults = [ordered]@{} $defaults['model'] = $model $defaults['thinkingDefault'] = $DefaultThinking $defaults['models'] = $catalog $agents = [ordered]@{} $agents['defaults'] = $defaults $modelsSec = [ordered]@{} $modelsSec['providers'] = $providers $patch = [ordered]@{} $patch['models'] = $modelsSec $patch['agents'] = $agents return $patch } # 非空 modelPolicy.allow 白名单会挡住未列出的模型: 返回追加我们 provider 通配符的补丁 (无需修改返回 $null) function New-PolicyPatch { param($Cfg) $pol = Get-Prop (Get-Prop (Get-Prop $Cfg 'agents') 'defaults') 'modelPolicy' $allow = Get-Prop $pol 'allow' if ($null -eq $allow -or $allow -is [string]) { return $null } $list = New-Object System.Collections.Generic.List[string] foreach ($a in $allow) { $list.Add([string]$a) } if ($list.Count -eq 0) { return $null } $changed = $false if ($S.DoClaude -and -not $list.Contains("$ProviderClaude/*")) { $list.Add("$ProviderClaude/*"); $changed = $true } if ($S.DoGpt -and -not $list.Contains("$ProviderGpt/*")) { $list.Add("$ProviderGpt/*"); $changed = $true } if (-not $changed) { return $null } $mp = [ordered]@{} $mp['allow'] = $list.ToArray() $d = [ordered]@{} $d['modelPolicy'] = $mp $ag = [ordered]@{} $ag['defaults'] = $d $p = [ordered]@{} $p['agents'] = $ag return $p } # 与 `openclaw config patch` 相同的语义: 对象递归合并,数组/标量替换,null 删除 function Apply-Patch { param($Target, $Patch, [string[]]$Replaces, [string]$Prefix) foreach ($k in @($Patch.Keys)) { $v = $Patch[$k] $path = $k if ($Prefix) { $path = "$Prefix.$k" } if ($null -eq $v) { Remove-Prop $Target $k; continue } if (($v -is [Collections.IDictionary]) -and -not ($Replaces -contains $path)) { $cur = Get-Prop $Target $k if (-not (Test-JsonObject $cur)) { $cur = New-JsonObject Set-Prop $Target $k $cur } Apply-Patch $cur $v $Replaces $path } else { Set-Prop $Target $k $v } } } # 调用 openclaw (stdin 传入 JSON);返回 @{ Ok; Output } function Invoke-OpenClawPatch { param([string]$Exe, [string]$Json, [string[]]$Replaces) $ocArgs = New-Object System.Collections.Generic.List[string] foreach ($a in @('config', 'patch', '--stdin')) { $ocArgs.Add($a) } foreach ($r in $Replaces) { $ocArgs.Add('--replace-path'); $ocArgs.Add($r) } $ErrorActionPreference = 'Continue' $lines = @() $ok = $false try { $lines = @($Json | & $Exe $ocArgs.ToArray() 2>&1 | ForEach-Object { [string]$_ }) $ok = ($LASTEXITCODE -eq 0) } catch { $lines = @([string]$_.Exception.Message) $ok = $false } return @{ Ok = $ok; Output = $lines } } function Write-OpenClawConfig { param([string]$BaseUrl, [string]$ClaudeKey, [string[]]$ClaudeIds, [string]$GptKey, [string[]]$GptIds) $oc = $null if (Test-OpenClawUsable) { $oc = Get-OpenClawExe } if ($oc) { # 以 openclaw 实际使用的配置文件为准 $ErrorActionPreference = 'Continue' $active = '' try { $active = [string](& $oc config file 2>$null | Where-Object { ([string]$_) -match '^(/|[A-Za-z]:|~)' } | Select-Object -Last 1) } catch { $active = '' } $ErrorActionPreference = 'Stop' if ($active) { $active = $active.Trim() if ($active.StartsWith('~')) { $active = $UserHome + $active.Substring(1) } $S.ConfigFile = $active } } $file = $S.ConfigFile Ensure-Dir (Split-Path -Parent $file) $cur = Read-OpenClawConfig $file $existing = $cur.Data if ($null -eq $existing) { $existing = New-JsonObject } $patch = New-ConfigPatch $existing $BaseUrl $ClaudeKey $ClaudeIds $GptKey $GptIds $replaces = Get-ReplacePaths if ($oc) { Backup-IfExists $file Print-Status 'info' '正在通过 openclaw config patch 写入配置 (官方命令,带格式校验) ...' $drops = New-Object System.Collections.Generic.List[int] $tries = 0 while ($true) { $json = ConvertTo-JsonText $patch 0 $r = Invoke-OpenClawPatch $oc $json $replaces if ($r.Ok) { break } # 用户原有的备用模型里有 OpenClaw 无法解析的引用: 去掉报错的那一条后重试 $m = [regex]::Match((($r.Output) -join "`n"), 'agents\.defaults\.model\.fallbacks\.(\d+)') if (-not $m.Success -or $tries -ge 8) { break } if ($tries -eq 0) { Print-Status 'warn' '原有的备用模型 (fallbacks) 中有 OpenClaw 无法识别的模型,已跳过该条' } $drops.Add([int]$m.Groups[1].Value) $tries++ $patch = New-ConfigPatch $existing $BaseUrl $ClaudeKey $ClaudeIds $GptKey $GptIds $drops.ToArray() } $json = '' if (-not $r.Ok) { Print-Status 'error' 'openclaw config patch 执行失败:' $tail = @($r.Output | Where-Object { $_ -and $_.Trim() } | Select-Object -Last 8) foreach ($l in $tail) { Say " $l" } Print-Status 'info' '原配置未被修改 (如已备份,可在同目录找到 .bak 文件);可运行 openclaw doctor 检查现有配置' return $false } if ((($r.Output) -join "`n") -match 'strip JSON5 comments') { Print-Status 'warn' 'openclaw.json 中原有的注释已在 OpenClaw 重写时移除 (原文件已备份)' } # 模型白名单 (agents.defaults.modelPolicy.allow) 非空时追加我们的 provider $after = Read-OpenClawConfig $file $pp = $null if ($null -ne $after.Data) { $pp = New-PolicyPatch $after.Data } if ($null -ne $pp) { $r2 = Invoke-OpenClawPatch $oc (ConvertTo-JsonText $pp 0) @() if ($r2.Ok) { Print-Status 'info' "检测到模型白名单 (modelPolicy.allow),已加入 $ProviderClaude/* 与 $ProviderGpt/*" } else { Print-Status 'warn' "检测到模型白名单 (modelPolicy.allow),自动追加失败,请手动加入 `"$ProviderClaude/*`"、`"$ProviderGpt/*`"" } } return $true } Print-Status 'warn' '未检测到可用的 openclaw 命令,将直接合并写入配置文件' if ($cur.Exists -and -not $cur.Ok) { Print-Status 'error' '无法解析现有的 openclaw.json (可能使用了 JSON5 语法),请先执行菜单 1 安装 OpenClaw 再配置' return $false } Backup-IfExists $file $data = $cur.Data if ($null -eq $data) { $data = New-JsonObject } $modelsObj = Get-Prop $data 'models' if (-not (Test-JsonObject $modelsObj)) { $modelsObj = New-JsonObject; Set-Prop $data 'models' $modelsObj } if ($null -eq (Get-Prop $modelsObj 'mode')) { Set-Prop $modelsObj 'mode' 'merge' } Apply-Patch $data $patch $replaces '' $pp = New-PolicyPatch $data if ($null -ne $pp) { Apply-Patch $data $pp @() '' } Write-Utf8File $file ((ConvertTo-JsonText $data 0) + "`n") if ($cur.Exists -and -not $cur.Strict) { Print-Status 'warn' 'openclaw.json 中原有的注释/尾逗号已在重写时移除 (原文件已备份)' } return $true } function Choose-Families { Say ' ' '' '请选择要配置的模型家族:' 'White' Say '' Say ' ' '' '⚡' 'Yellow' ' ' '' '1)' 'Cyan' ' Claude + GPT ' '' '(回车默认)' 'DarkGray' Say ' ' '' '⚡' 'Yellow' ' ' '' '2)' 'Cyan' ' 仅 Claude' '' Say ' ' '' '⚡' 'Yellow' ' ' '' '3)' 'Cyan' ' 仅 GPT' '' Say '' SayN ' ' '' '请输入选项 [回车=1]: ' 'White' $choice = Read-Line switch ($choice) { '' { $S.DoClaude = $true; $S.DoGpt = $true; $S.FamilyLabel = 'Claude + GPT' } '1' { $S.DoClaude = $true; $S.DoGpt = $true; $S.FamilyLabel = 'Claude + GPT' } '2' { $S.DoClaude = $true; $S.DoGpt = $false; $S.FamilyLabel = '仅 Claude' } '3' { $S.DoClaude = $false; $S.DoGpt = $true; $S.FamilyLabel = '仅 GPT' } default { Print-Status 'error' '无效选项,已取消'; return $false } } Say '' Print-Status 'ok' "已选择: $($S.FamilyLabel)" return $true } function Configure-Api { Say '' Print-Status 'info' '⚡ 配置 闪电API (Claude + GPT)' Say '' Find-Json5Module $cur = Read-OpenClawConfig $S.ConfigFile $k = Get-ProviderField $cur.Data $ProviderClaude 'apiKey' if ($k) { Print-Status 'info' "当前 Claude Key: $(Mask-Key $k)" } $k = Get-ProviderField $cur.Data $ProviderGpt 'apiKey' if ($k) { Print-Status 'info' "当前 GPT Key: $(Mask-Key $k)" } $u = Get-ProviderField $cur.Data $ProviderClaude 'baseUrl' if (-not $u) { $u = Get-ProviderField $cur.Data $ProviderGpt 'baseUrl' } if ($u) { Print-Status 'info' "当前 BASE URL: $($u -replace '/v1$', '')" } $k = '' Say '' $S.DoClaude = $false $S.DoGpt = $false $S.FamilyLabel = '' if (-not (Choose-Families)) { return $false } Say '' # ── 选择线路 ── Say ' ' '' '请选择 API 线路:' 'White' Say '' Say ' ' '' '⚡' 'Yellow' ' ' '' '1)' 'Cyan' ' ' '' '国内直连' 'Green' ' - https://ai.flashapi.top' '' Say ' ' '' '服务器位于香港,国内可直连,无需翻墙' 'DarkGray' Say '' Say ' ' '' '⚡' 'Yellow' ' ' '' '2)' 'Cyan' ' ' '' '海外CDN' 'Green' ' - https://cdn.flashapi.top' '' Say ' ' '' '全球CDN加速,适合海外用户' 'DarkGray' Say '' SayN ' ' '' '请输入选项 (1/2): ' 'White' $choice = Read-Line $baseUrl = '' switch ($choice) { '1' { $baseUrl = 'https://ai.flashapi.top' } '2' { $baseUrl = 'https://cdn.flashapi.top' } default { Print-Status 'error' '无效选项,已取消'; return $false } } Say '' Print-Status 'info' "已选择: $baseUrl" Say '' # ── 令牌分组提醒 ── Say ' ' '' '╔══════════════════════════════════════════════════════╗' 'Yellow' Say ' ' '' '║' 'Yellow' ' ' '' '⚡ 重要提示' 'White' ' ' '' '║' 'Yellow' Say ' ' '' '║' 'Yellow' ' ' '' '║' 'Yellow' Say ' ' '' '║' 'Yellow' ' ' '' '请先在官网' 'DarkGray' ' ' '' 'flashapi.top' 'Yellow' ' ' '' '创建' 'DarkGray' ' ' '' '令牌分组' 'Green' ' ' '' '║' 'Yellow' Say ' ' '' '║' 'Yellow' ' ' '' '未创建令牌分组将无法获取模型列表!' 'DarkGray' ' ' '' '║' 'Yellow' Say ' ' '' '╚══════════════════════════════════════════════════════╝' 'Yellow' Say '' # ── 输入 API Key (密码遮盖) ── $claudeKey = '' $gptKey = '' if ($S.DoClaude) { $claudeKey = Read-Secret ' 请输入 Claude 分组 API Key: ' if (-not $claudeKey) { Print-Status 'error' 'API Key 不能为空,已取消'; return $false } } if ($S.DoGpt) { if ($S.DoClaude) { $gptKey = Read-Secret ' 请输入 GPT 分组 API Key (回车 = 与 Claude Key 相同): ' if (-not $gptKey) { $gptKey = $claudeKey } } else { $gptKey = Read-Secret ' 请输入 GPT 分组 API Key: ' } if (-not $gptKey) { Print-Status 'error' 'API Key 不能为空,已取消'; return $false } } Say '' # ── 验证 ── $claudeIds = @() $gptIds = @() if ($S.DoClaude) { $v = Validate-Key 'claude' $baseUrl $claudeKey if (-not $v.Ok) { return $false } $claudeIds = $v.Ids } if ($S.DoGpt) { $v = Validate-Key 'gpt' $baseUrl $gptKey if (-not $v.Ok) { return $false } $gptIds = $v.Ids } Say '' $primary = "$ProviderGpt/$DefaultGptModel" $fallback = '' if ($S.DoClaude) { $primary = "$ProviderClaude/$DefaultClaudeModel" if ($S.DoGpt) { $fallback = "$ProviderGpt/$DefaultGptModel" } } # ── 写入 OpenClaw 配置 ── if (-not (Write-OpenClawConfig $baseUrl $claudeKey $claudeIds $gptKey $gptIds)) { return $false } Say '' if ($S.DoClaude) { Say ' ' '' '[⚡]' 'Green' " Claude Key 已保存: $(Mask-Key $claudeKey) " '' "(供应商 $ProviderClaude)" 'DarkGray' Say ' ' '' '[⚡]' 'Green' " Claude BASE URL: $baseUrl " '' '(anthropic-messages)' 'DarkGray' } if ($S.DoGpt) { Say ' ' '' '[⚡]' 'Green' " GPT Key 已保存: $(Mask-Key $gptKey) " '' "(供应商 $ProviderGpt)" 'DarkGray' Say ' ' '' '[⚡]' 'Green' " GPT BASE URL: $baseUrl/v1 " '' '(openai-responses)' 'DarkGray' } if ($fallback) { Say ' ' '' '[⚡]' 'Green' " 默认模型: $primary " '' "(备用: $fallback)" 'DarkGray' } else { Print-Status 'ok' "默认模型: $primary" } Print-Status 'ok' "推理强度: $DefaultThinking (agents.defaults.thinkingDefault)" Print-Status 'ok' "配置文件已写入: $($S.ConfigFile)" Say '' Say ' ' '' '使用方法:' 'White' Say ' ' '' '快速验证:' 'Cyan' ' openclaw agent --local -m "Reply with exactly: OK"' '' Say ' ' '' '终端聊天:' 'Cyan' ' openclaw chat' '' Say ' ' '' '完整功能:' 'Cyan' ' openclaw onboard --install-daemon ' '' '(网关/聊天渠道/控制台;模型与认证步骤选 Skip 以保留闪电API配置)' 'DarkGray' Say ' ' '' '查看模型:' 'Cyan' ' openclaw models list' '' if ($S.DoGpt) { Say ' ' '' '用 GPT:' 'Cyan' " openclaw agent --local --model $ProviderGpt/$DefaultGptModel -m `"你好`"" '' Say ' ' '' '默认改 GPT:' 'Cyan' " openclaw models set $ProviderGpt/$DefaultGptModel" '' } if ($S.DoClaude) { Say ' ' '' '默认改 Claude:' 'Cyan' " openclaw models set $ProviderClaude/$DefaultClaudeModel" '' } Say ' ' '' '会话中切换:' 'Cyan' ' 输入 /model 选择模型,/think 调整推理强度' '' Print-Status 'info' '网关 (gateway) 正在运行时会自动加载新配置,无需重启' if ($S.DoClaude) { Say '' Print-Status 'warn' '如果 Claude 请求报错 "Requests from the ... framework are not supported":' Say ' ' '' '说明你的 Claude Key 所在令牌分组只允许官方客户端。请到 flashapi.top 在允许第三方工具的分组' 'DarkGray' Say ' ' '' '下创建令牌,然后重新运行本脚本选择「配置」。' 'DarkGray' } $claudeKey = '' $gptKey = '' return $true } ############################################################################## # 状态检测与菜单 ############################################################################## function Show-StatusSummary { Say ' ' '' '当前状态:' 'White' $osLabel = 'windows' if (-not $OnWindows) { $osLabel = [string][Environment]::OSVersion.Platform } Say ' 系统环境: ' '' $osLabel 'Cyan' $nv = Get-NodeVersion if (-not $nv) { $nv = '未安装' } if (Test-NodeVersionOk) { Say ' Node.js: ' '' $nv 'Green' } else { Say ' Node.js: ' '' $nv 'Yellow' ' ' '' "(OpenClaw 需要 $NodeRequirementText)" 'DarkGray' } $ver = Check-OpenClawInstalled if ($null -ne $ver) { Say ' OpenClaw: ' '' '已安装' 'Green' " ($ver)" '' } else { Say ' OpenClaw: ' '' '未安装' 'Red' } Find-Json5Module $cur = Read-OpenClawConfig $S.ConfigFile $k = Get-ProviderField $cur.Data $ProviderClaude 'apiKey' if ($k) { Say ' Claude 配置: ' '' '已配置' 'Green' " ($(Mask-Key $k))" '' } else { Say ' Claude 配置: ' '' '未配置' 'Red' } $k = Get-ProviderField $cur.Data $ProviderGpt 'apiKey' if ($k) { Say ' GPT 配置: ' '' '已配置' 'Green' " ($(Mask-Key $k))" '' } else { Say ' GPT 配置: ' '' '未配置' 'Red' } $k = '' $u = Get-ProviderField $cur.Data $ProviderClaude 'baseUrl' if (-not $u) { $u = Get-ProviderField $cur.Data $ProviderGpt 'baseUrl' } if ($u) { Say ' API 线路: ' '' ($u -replace '/v1$', '') 'Green' } $pm = Get-PrimaryModel $cur.Data if ($pm) { Say ' 默认模型: ' '' $pm 'Cyan' } Say ' 配置文件: ' '' $S.ConfigFile 'Cyan' Say '' } function Show-Menu { Say ' ' '' '请选择操作:' 'White' Say '' Say ' ' '' '⚡' 'Yellow' ' ' '' '1)' 'Cyan' ' 安装 OpenClaw' '' Say ' ' '' '⚡' 'Yellow' ' ' '' '2)' 'Cyan' ' 配置 闪电API (Claude + GPT)' '' Say ' ' '' '⚡' 'Yellow' ' ' '' '3)' 'Cyan' ' 一键全部安装 (安装 + 配置)' '' Say ' ' '' '⚡' 'Yellow' ' ' '' '0)' 'Cyan' ' 退出' '' Say '' SayN ' ' '' '请输入选项: ' 'White' } ############################################################################## # 主流程 ############################################################################## function Main { Print-Banner Show-StatusSummary while ($true) { Show-Menu $choice = Read-Line if ($null -eq $choice) { Say ''; return } switch ($choice) { '1' { try { Install-OpenClaw | Out-Null } catch { Print-Status 'error' "出错: $($_.Exception.Message)" } Say '' } '2' { try { Configure-Api | Out-Null } catch { Print-Status 'error' "出错: $($_.Exception.Message)" } Say '' } '3' { try { Install-OpenClaw | Out-Null } catch { Print-Status 'error' "出错: $($_.Exception.Message)" } Say '' Say " $Line" 'DarkGray' $done = $false try { $done = Configure-Api } catch { Print-Status 'error' "出错: $($_.Exception.Message)" } if ($done) { Say '' Say " $Line" 'DarkGray' Say '' Print-Status 'ok' '⚡ 全部完成! 请重新打开 PowerShell 后运行 openclaw chat (或 openclaw onboard 开启完整功能)' } Say '' } '0' { Say '' Say ' ' '' '⚡' 'Yellow' ' ' '' '感谢使用闪电API,祝你Vibe Coding愉快!' 'White' Say ' ' '' '官网: flashapi.top' 'DarkGray' Say '' return } default { Print-Status 'error' '无效选项,请重新选择' Say '' } } } } Main }