################################################################################ # 闪电API - Pi (pi coding agent) 一键安装脚本 (Windows PowerShell) # # 功能: 安装 Pi + 配置 闪电API (Claude + GPT 两个供应商) # 官网: flashapi.top # 模型调用: ai.flashapi.top (国内直连) / cdn.flashapi.top (海外CDN) # # 用法 (PowerShell): irm https://sh.flashapi.top/pi.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 {} ############################################################################## # 常量定义 ############################################################################## $OnWindows = ([Environment]::OSVersion.Platform -eq [PlatformID]::Win32NT) $UserHome = $env:USERPROFILE if (-not $UserHome) { $UserHome = $HOME } $NpmMirror = 'https://registry.npmmirror.com' $PiPackage = '@earendil-works/pi-coding-agent' $PiNodeMinMajor = 22 $PiNodeMinMinor = 19 # Pi 配置目录: $env:PI_CODING_AGENT_DIR,默认 ~\.pi\agent (各平台一致) $PiAgentDir = $env:PI_CODING_AGENT_DIR if (-not $PiAgentDir) { $PiAgentDir = Join-Path (Join-Path $UserHome '.pi') 'agent' } elseif ($PiAgentDir -eq '~') { $PiAgentDir = $UserHome } elseif ($PiAgentDir.StartsWith('~/') -or $PiAgentDir.StartsWith('~\')) { $PiAgentDir = Join-Path $UserHome $PiAgentDir.Substring(2) } $PiModelsFile = Join-Path $PiAgentDir 'models.json' $PiSettingsFile = Join-Path $PiAgentDir 'settings.json' $ProviderClaude = 'flashapi-claude' $ProviderGpt = 'flashapi-gpt' $DefaultClaudeModel = 'claude-opus-5-5' $DefaultGptModel = 'gpt-6.1-sol' $DefaultThinking = 'medium' # 同时配置 Claude + GPT 时默认使用哪一家 (claude / gpt) $DefaultFamilyWhenBoth = 'claude' $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 { $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 × Pi ⚡' 'Blue' Say ' ' '' ' ⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡' 'Yellow' Say '' Say " $Line" 'DarkGray' Say ' ' '' '⚡ 闪电API' 'White' ' ' '' '|' 'DarkGray' ' ' '' '官网' 'Cyan' ' ' '' 'flashapi.top' 'Yellow' Say ' ' '' 'Pi Coding Agent 一键配置' '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 Get-ExeVersion { param([string]$Exe) $ErrorActionPreference = 'Continue' $out = $null try { $out = & $Exe --version 2>$null | Select-Object -First 1 } catch { $out = $null } if ($out) { return ([string]$out).Trim() } return 'unknown' } 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') } # 与 Pi 自身 (dist/utils/json.js 的 stripJsonComments) 一致: 去掉 // 注释和尾逗号 function Remove-JsonComments { param([string]$Text) $e1 = [Text.RegularExpressions.MatchEvaluator] { param($m) if ($m.Value.StartsWith('"')) { return $m.Value } return '' } $e2 = [Text.RegularExpressions.MatchEvaluator] { param($m) if ($m.Groups[1].Success) { return $m.Groups[1].Value } if ($m.Value.StartsWith('"')) { return $m.Value } return '' } $t = [regex]::Replace($Text, '"(?:\\.|[^"\\])*"|//[^\n]*', $e1) $t = [regex]::Replace($t, '"(?:\\.|[^"\\])*"|,(\s*[}\]])', $e2) return $t } function ConvertFrom-JsonText { param([string]$Text) $opts = @{} if ($JsonDateKind) { $opts['DateKind'] = 'String' } return (ConvertFrom-Json -InputObject $Text @opts) } # 读取 JSON 对象文件。返回 @{ Ok; Data; Stripped; Error } # 不存在/空文件 -> Ok + 空对象;解析失败 -> Ok=$false (调用方不得覆盖原文件) function Read-JsonFile { param([string]$Path, [bool]$Tolerant) $r = @{ Ok = $true; Data = (New-Object -TypeName PSObject); Stripped = $false; Error = '' } if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { return $r } $text = [IO.File]::ReadAllText($Path) if ($text.Length -gt 0 -and [int]$text[0] -eq 0xFEFF) { $text = $text.Substring(1) } if (-not $text.Trim()) { return $r } $obj = $null $parsed = $false if ($Tolerant) { # 注释/尾逗号: 先去掉再解析 (正则跳过字符串字面量,去掉前后有差异即说明原文件含注释或尾逗号) $clean = Remove-JsonComments $text if ($clean -ne $text) { $r.Stripped = $true } try { $obj = ConvertFrom-JsonText $clean; $parsed = $true } catch {} } else { try { $obj = ConvertFrom-JsonText $text; $parsed = $true } catch {} } if (-not $parsed) { $r.Ok = $false; $r.Error = "$Path 不是合法 JSON"; return $r } if (-not (Test-JsonObject $obj)) { $r.Ok = $false; $r.Error = "$Path 根节点不是对象"; return $r } $r.Data = $obj return $r } function Get-Prop { param($Obj, [string]$Name) if ($null -eq $Obj) { 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 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)) } function Write-JsonFile { param([string]$Path, $Data) Write-Utf8File $Path ((ConvertTo-JsonText $Data 0) + "`n") } ############################################################################## # 功能 1: 安装 Pi ############################################################################## function Get-NpmExe { return (Find-Exe @('npm.cmd', 'npm.exe', 'npm')) } function Get-NodeExe { return (Find-Exe @('node.exe', 'node')) } function Get-PiExe { return (Find-Exe @('pi.cmd', 'pi.exe', 'pi')) } # 已安装返回版本号,未安装返回 $null function Check-PiInstalled { $exe = Get-PiExe if (-not $exe) { return $null } return (Get-ExeVersion $exe) } # 把 npm 全局 bin 目录加入当前会话 PATH (刚装完时 PATH 可能还没生效) function Add-NpmBinToPath { param([string]$Npm) $ErrorActionPreference = 'Continue' $prefix = $null try { $prefix = & $Npm prefix -g 2>$null | Select-Object -First 1 } catch { $prefix = $null } if (-not $prefix) { return } $prefix = ([string]$prefix).Trim() if ($OnWindows) { Add-SessionPath $prefix } else { Add-SessionPath (Join-Path $prefix 'bin') } } function Invoke-NpmInstall { param([string]$Npm, [string]$Registry) $ErrorActionPreference = 'Continue' try { if ($Registry) { & $Npm install -g --ignore-scripts "$PiPackage@latest" "--registry=$Registry" | Out-Host } else { & $Npm install -g --ignore-scripts "$PiPackage@latest" | 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 Show-NodeHint { Say '' Say ' ' '' '安装方式:' 'White' Say ' ' '' '官网下载:' 'Cyan' ' https://nodejs.org/' '' Say ' ' '' 'winget:' 'Cyan' ' winget install --id OpenJS.NodeJS.LTS -e' '' } function Install-Pi { Say '' Print-Status 'info' '⚡ 正在安装 Pi ...' Say '' if ($OnWindows) { Print-Status 'info' '检测到 Windows (PowerShell) 环境' } $ver = Check-PiInstalled if ($null -ne $ver) { Print-Status 'ok' "Pi 已安装 ($ver)" Say '' if (-not (Read-YesNo '是否重新安装/升级到最新版?')) { Print-Status 'info' '跳过安装'; return $true } } $npm = Get-NpmExe $node = Get-NodeExe if (-not $npm -or -not $node) { Print-Status 'error' "未找到 Node.js / npm,请先安装 Node.js (需要 v$PiNodeMinMajor.$PiNodeMinMinor+)" Show-NodeHint return $false } # 检查 Node 版本 (Pi 要求 >= 22.19.0) $nodeVer = (Get-ExeVersion $node) -replace '^v', '' $m = [regex]::Match($nodeVer, '^(\d+)\.(\d+)') if ($m.Success) { $major = [int]$m.Groups[1].Value $minor = [int]$m.Groups[2].Value if ($major -lt $PiNodeMinMajor -or ($major -eq $PiNodeMinMajor -and $minor -lt $PiNodeMinMinor)) { Print-Status 'warn' "当前 Node.js 版本 v$nodeVer,Pi 需要 v$PiNodeMinMajor.$PiNodeMinMinor+" Print-Status 'info' '建议升级: https://nodejs.org/ (或 winget upgrade OpenJS.NodeJS.LTS)' Say '' if (-not (Read-YesNo '是否仍然继续安装?')) { Print-Status 'info' '已取消'; return $false } } } Print-Status 'ok' "npm 已就绪 ($(Get-ExeVersion $npm)), Node v$nodeVer" Print-Status 'info' "使用 npm 安装 $PiPackage ..." Say '' if (-not (Invoke-NpmInstall $npm '')) { Print-Status 'warn' '默认源安装失败,可能是网络问题' if (-not (Install-WithMirror $npm)) { Print-Status 'error' '所有安装方式均失败,请检查网络连接' return $false } } Say '' Add-NpmBinToPath $npm $ver = Check-PiInstalled if ($null -ne $ver) { Print-Status 'ok' "⚡ Pi 安装成功 ($ver)" } else { Print-Status 'warn' '安装已完成,但未在 PATH 中找到 pi' Print-Status 'info' '请重新打开终端后再试' } Show-GitBashHint return $true } # Pi 在原生 Windows 上默认用 Git Bash 执行 bash 工具 function Show-GitBashHint { if (-not $OnWindows) { return } $found = $false foreach ($base in @($env:ProgramFiles, ${env:ProgramFiles(x86)})) { if ($base -and (Test-Path -LiteralPath (Join-Path (Join-Path (Join-Path $base 'Git') 'bin') 'bash.exe') -PathType Leaf)) { $found = $true } } if (-not $found -and (Find-Exe @('bash.exe'))) { $found = $true } if ($found) { Print-Status 'ok' '检测到 Git Bash (Pi 的 bash 工具将使用它)' } else { Print-Status 'warn' '未检测到 Git Bash:Pi 在 Windows 上需要 Bash 执行命令' Print-Status 'info' '请安装 Git for Windows: https://git-scm.com/downloads/win (或 winget install --id Git.Git -e)' } } ############################################################################## # 功能 2: 配置 API ############################################################################## # 读取我们管理的 provider 字段 (apiKey / baseUrl) function Read-ProviderField { param([string]$Provider, [string]$Field) try { $r = Read-JsonFile $PiModelsFile $true if (-not $r.Ok) { return '' } $v = Get-Prop (Get-Prop (Get-Prop $r.Data 'providers') $Provider) $Field if ($v -is [string]) { return $v } } catch {} return '' } function Read-SettingsField { param([string]$Field) try { $r = Read-JsonFile $PiSettingsFile $false if (-not $r.Ok) { return '' } $v = Get-Prop $r.Data $Field if ($v -is [string]) { return $v } } catch {} return '' } # 用 /v1/models 验证 Key。返回 @{ Code = 200/401/0(网络错误)/其他; Ids = string[] } function Fetch-Models { param([string]$BaseUrl, [string]$ApiKey) $res = @{ Code = 0; Ids = @() } $content = $null try { $r = Invoke-WebRequest -Uri "$BaseUrl/v1/models" -Headers @{ 'Authorization' = "Bearer $ApiKey" } -Method Get -UseBasicParsing -TimeoutSec 15 $res.Code = [int]$r.StatusCode $content = [string]$r.Content } 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 } if ($res.Code -eq 200 -and $content) { try { $j = ConvertFrom-JsonText $content $list = @() $data = Get-Prop $j 'data' if ($null -ne $data) { $list = @($data) } elseif ($j -is [Array]) { $list = @($j) } $ids = New-Object System.Collections.Generic.List[string] foreach ($mm in $list) { $id = Get-Prop $mm 'id' if ($id -is [string] -and $id) { $ids.Add($id) } } $res.Ids = $ids.ToArray() } catch {} } return $res } function Select-FamilyIds { param([string]$Family, [string[]]$Ids) $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 = $false 表示用户取消; 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 ..." $f = Fetch-Models $BaseUrl $ApiKey $ids = @() if ($f.Code -eq 200) { $ids = Select-FamilyIds $Family $f.Ids 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 ($f.Code -eq 401) { Print-Status 'error' "$label API Key 无效 (401 Unauthorized)" if (-not (Read-YesNo '验证未通过,是否仍要保存?')) { Print-Status 'info' '已取消配置'; return @{ Ok = $false; Ids = @() } } } elseif ($f.Code -eq 0) { Print-Status 'warn' "无法连接到 $BaseUrl (网络错误)" if (-not (Read-YesNo '验证未通过,是否仍要保存?')) { Print-Status 'info' '已取消配置'; return @{ Ok = $false; Ids = @() } } } else { Print-Status 'warn' "API 返回状态码: $($f.Code) (可能仍可用),将只写入默认模型" } return @{ Ok = $true; Ids = $ids } } # ── 模型元数据 ── $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') # 思考档位映射: 取自 Pi 1.1.0 内置模型目录 (pi-ai providers/data/*.json) # 规格字符串: 档位=值 ,值 'null' 表示该档位不支持 $MapFull = 'off=null,minimal=null,low=low,medium=medium,high=high,xhigh=xhigh,max=max' $MapAdaptive = 'off=null,xhigh=xhigh,max=max' $MapXhighMax = 'xhigh=xhigh,max=max' $MapMaxOnly = 'max=max' $MapGpt56 = 'off=none,minimal=null,low=low,medium=medium,high=high,xhigh=xhigh,max=max' $MapGpt55 = 'off=none,minimal=null,low=low,medium=medium,high=high,xhigh=xhigh,max=null' function Get-ClaudeThinkingSpec { param([string]$Id) switch ($Id) { 'claude-opus-5-5' { return $MapFull } 'claude-sonnet-5-5' { return $MapFull } 'claude-haiku-5-5' { return $MapFull } 'claude-fable-5-1' { return $MapAdaptive } 'claude-fable-5' { return $MapAdaptive } 'claude-opus-5' { return $MapAdaptive } 'claude-opus-4-8' { return $MapXhighMax } 'claude-opus-4-7' { return $MapXhighMax } 'claude-sonnet-5' { return $MapXhighMax } 'claude-opus-4-6' { return $MapMaxOnly } 'claude-sonnet-4-6' { return $MapMaxOnly } } # 未知的新一代 Claude (版本 >= 5) 按自适应思考处理 $m = [regex]::Match($Id, '^claude-[a-z]+-(\d+)') if ($m.Success) { $v = [int]$m.Groups[1].Value if ($v -ge 5 -and $v -lt 100) { return $MapAdaptive } } return '' } function Get-GptThinkingSpec { param([string]$Id) switch ($Id) { 'gpt-6.1-sol' { return $MapFull } 'gpt-6-astra' { return $MapFull } 'gpt-6-sol' { return $MapGpt56 } 'gpt-6-luna' { return $MapGpt56 } 'gpt-5.6-sol' { return $MapGpt56 } 'gpt-5.6-luna' { return $MapGpt56 } 'gpt-5.6-terra' { return $MapGpt56 } 'gpt-5.5' { return $MapGpt55 } } if ($Id -match '^gpt-[6-9]') { return $MapFull } if ($Id -match '^gpt-5\.6') { return $MapGpt56 } return '' } function New-ThinkingMap { param([string]$Spec) $map = [ordered]@{} foreach ($pair in $Spec.Split(',')) { $kv = $pair.Split('=') if ($kv[1] -eq 'null') { $map[$kv[0]] = $null } else { $map[$kv[0]] = $kv[1] } } return $map } function Get-TitleCase { param([string]$s) if (-not $s) { return '' } return $s.Substring(0, 1).ToUpperInvariant() + $s.Substring(1) } # claude-opus-5-5 -> Claude Opus 5.5 ;gpt-6.1-sol -> GPT-6.1 Sol (与 sh 版一致) function Get-PrettyName { param([string]$Id) $parts = $Id.Split('-') if ($parts[0] -eq 'gpt') { $ver = '' if ($parts.Count -gt 1) { $ver = $parts[1] } $words = New-Object System.Collections.Generic.List[string] $words.Add("GPT-$ver") for ($i = 2; $i -lt $parts.Count; $i++) { $words.Add((Get-TitleCase $parts[$i])) } return ($words -join ' ').Trim() } $words = 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) { $words.Add(($nums -join '.')); $nums.Clear() } if ($p -match '^\d{8}$') { $words.Add("($p)") } else { $words.Add((Get-TitleCase $p)) } } if ($nums.Count -gt 0) { $words.Add(($nums -join '.')) } return ($words -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 } } $model = [ordered]@{ id = $Id name = (Get-PrettyName $Id) reasoning = $true input = @('text', 'image') contextWindow = $ctx maxTokens = $max } $spec = '' if ($Family -eq 'claude') { $spec = Get-ClaudeThinkingSpec $Id } else { $spec = Get-GptThinkingSpec $Id } if ($spec) { $model['thinkingLevelMap'] = (New-ThinkingMap $spec) if ($Family -eq 'claude') { $model['compat'] = [ordered]@{ forceAdaptiveThinking = $true } } } return $model } function New-ModelList { param([string]$Family, [string[]]$Ids, [string]$DefaultId) $all = New-Object System.Collections.Generic.List[string] $all.Add($DefaultId) foreach ($id in $Ids) { $t = ([string]$id).Trim() if ($t -and -not $all.Contains($t)) { $all.Add($t) } } $list = New-Object System.Collections.Generic.List[object] foreach ($id in $all) { $list.Add((New-ModelEntry $Family $id)) } return ,($list.ToArray()) } # Pi 会把 apiKey 中的 $NAME / ${NAME} 当环境变量,开头的 ! 当命令,这里做转义 function Protect-ConfigValue { param([string]$Value) $s = $Value.Replace('$', '$$') if ($s.StartsWith('!')) { $s = '$' + $s } return $s } # 合并写入 models.json + settings.json。成功返回 $true function Write-PiConfig { param([string]$BaseUrl, [string]$ClaudeKey, [string[]]$ClaudeIds, [string]$GptKey, [string[]]$GptIds, [string]$DefaultProvider, [string]$DefaultModel) $mr = Read-JsonFile $PiModelsFile $true if (-not $mr.Ok) { Print-Status 'error' "写入配置失败: $($mr.Error)"; Print-Status 'info' '原文件未被修改'; return $false } $sr = Read-JsonFile $PiSettingsFile $false if (-not $sr.Ok) { Print-Status 'error' "写入配置失败: $($sr.Error)"; Print-Status 'info' '原文件未被修改'; return $false } Ensure-Dir $PiAgentDir Backup-IfExists $PiModelsFile Backup-IfExists $PiSettingsFile # ── models.json ── $models = $mr.Data $providers = Get-Prop $models 'providers' if (-not (Test-JsonObject $providers)) { $providers = New-Object -TypeName PSObject Set-Prop $models 'providers' $providers } if ($ClaudeKey) { Set-Prop $providers $ProviderClaude ([ordered]@{ name = '闪电API Claude' baseUrl = $BaseUrl api = 'anthropic-messages' apiKey = (Protect-ConfigValue $ClaudeKey) models = (New-ModelList 'claude' $ClaudeIds $DefaultClaudeModel) }) } if ($GptKey) { Set-Prop $providers $ProviderGpt ([ordered]@{ name = '闪电API GPT' baseUrl = "$BaseUrl/v1" api = 'openai-responses' apiKey = (Protect-ConfigValue $GptKey) models = (New-ModelList 'gpt' $GptIds $DefaultGptModel) }) } # ── settings.json (Pi 用严格 JSON 解析) ── $settings = $sr.Data Set-Prop $settings 'defaultProvider' $DefaultProvider Set-Prop $settings 'defaultModel' $DefaultModel Set-Prop $settings 'defaultThinkingLevel' $DefaultThinking # 用户若设置了 enabledModels (启动/切换范围),Pi 只会在其中选默认模型;追加我们的供应商通配 $added = New-Object System.Collections.Generic.List[string] # 直接取属性值 (经函数 return 会把单元素数组拆包) $enabled = $null $ep = $settings.PSObject.Properties['enabledModels'] if ($null -ne $ep) { $enabled = $ep.Value } if ($null -ne $enabled -and ($enabled -is [Array]) -and $enabled.Count -gt 0) { $newList = New-Object System.Collections.Generic.List[object] foreach ($e in $enabled) { $newList.Add($e) } $want = @() if ($ClaudeKey) { $want += "$ProviderClaude/*" } if ($GptKey) { $want += "$ProviderGpt/*" } foreach ($p in $want) { if (-not ($newList -contains $p)) { $newList.Add($p); $added.Add($p) } } Set-Prop $settings 'enabledModels' ($newList.ToArray()) } Write-JsonFile $PiModelsFile $models Write-JsonFile $PiSettingsFile $settings if ($mr.Stripped) { Print-Status 'warn' 'models.json 中原有的注释/尾逗号已在重写时移除 (原文件已备份)' } if ($added.Count -gt 0) { Print-Status 'info' ("检测到 settings.json 设置了 enabledModels,已追加: " + ($added -join ' ')) } 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 $r = $null switch ($choice) { '' { $r = @{ Claude = $true; Gpt = $true; Label = 'Claude + GPT' } } '1' { $r = @{ Claude = $true; Gpt = $true; Label = 'Claude + GPT' } } '2' { $r = @{ Claude = $true; Gpt = $false; Label = '仅 Claude' } } '3' { $r = @{ Claude = $false; Gpt = $true; Label = '仅 GPT' } } default { Print-Status 'error' '无效选项,已取消'; return $null } } Say '' Print-Status 'ok' "已选择: $($r.Label)" return $r } function Configure-Api { Say '' Print-Status 'info' '⚡ 配置 闪电API (Claude + GPT)' Say '' $existing = Read-ProviderField $ProviderClaude 'apiKey' if ($existing) { Print-Status 'info' "当前 Claude Key: $(Mask-Key $existing)" } $existing = Read-ProviderField $ProviderGpt 'apiKey' if ($existing) { Print-Status 'info' "当前 GPT Key: $(Mask-Key $existing)" } $existing = Read-ProviderField $ProviderClaude 'baseUrl' if (-not $existing) { $existing = Read-ProviderField $ProviderGpt 'baseUrl' } if ($existing) { Print-Status 'info' "当前 BASE URL: $existing" } Say '' $fam = Choose-Families if ($null -eq $fam) { return $false } $doClaude = [bool]$fam.Claude $doGpt = [bool]$fam.Gpt 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 ($doClaude) { $claudeKey = Read-Secret ' 请输入 Claude 分组 API Key: ' if (-not $claudeKey) { Print-Status 'error' 'API Key 不能为空,已取消'; return $false } } if ($doGpt) { if ($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 ($doClaude) { $v = Validate-Key 'claude' $baseUrl $claudeKey if (-not $v.Ok) { return $false } $claudeIds = @($v.Ids) } if ($doGpt) { $v = Validate-Key 'gpt' $baseUrl $gptKey if (-not $v.Ok) { return $false } $gptIds = @($v.Ids) } Say '' $defaultFamily = 'gpt' if ($doClaude -and $doGpt) { $defaultFamily = $DefaultFamilyWhenBoth } elseif ($doClaude) { $defaultFamily = 'claude' } $defaultProvider = $ProviderGpt $defaultModel = $DefaultGptModel if ($defaultFamily -eq 'claude') { $defaultProvider = $ProviderClaude; $defaultModel = $DefaultClaudeModel } # ── 写入 Pi 配置 ── $wk = '' if ($doClaude) { $wk = $claudeKey } $wg = '' if ($doGpt) { $wg = $gptKey } if (-not (Write-PiConfig $baseUrl $wk $claudeIds $wg $gptIds $defaultProvider $defaultModel)) { return $false } Say '' if ($doClaude) { Say ' ' '' '[⚡]' 'Green' " Claude Key 已保存: $(Mask-Key $claudeKey) " '' "(供应商 $ProviderClaude)" 'DarkGray' Say ' ' '' '[⚡]' 'Green' " Claude BASE URL: $baseUrl " '' '(anthropic-messages)' 'DarkGray' } if ($doGpt) { Say ' ' '' '[⚡]' 'Green' " GPT Key 已保存: $(Mask-Key $gptKey) " '' "(供应商 $ProviderGpt)" 'DarkGray' Say ' ' '' '[⚡]' 'Green' " GPT BASE URL: $baseUrl/v1 " '' '(openai-responses)' 'DarkGray' } Print-Status 'ok' "默认模型: $defaultProvider/$defaultModel" Print-Status 'ok' "推理强度: $DefaultThinking (defaultThinkingLevel)" Print-Status 'ok' "模型配置已写入: $PiModelsFile" Print-Status 'ok' "默认设置已写入: $PiSettingsFile" Say '' Say ' ' '' '使用方法:' 'White' Say ' ' '' '启动:' 'Cyan' ' pi' '' Say ' ' '' '快速验证:' 'Cyan' ' pi -p "Reply with exactly: OK"' '' Say ' ' '' '查看模型:' 'Cyan' ' pi --list-models flashapi' '' if ($doGpt) { Say ' ' '' '用 GPT:' 'Cyan' " pi --provider $ProviderGpt --model $DefaultGptModel" '' } if ($doClaude) { Say ' ' '' '用 Claude:' 'Cyan' " pi --provider $ProviderClaude --model $DefaultClaudeModel" '' } Say ' ' '' '会话中切换:' 'Cyan' ' 输入 /model 选择 (Ctrl+S 设为默认),/thinking 调整推理强度' '' if ($doClaude) { Say '' Print-Status 'info' '提示: 若 Claude 请求报错 "Requests from the ... framework are not supported",' Say ' ' '' '说明该 Claude Key 所在分组仅限官方客户端;请到 flashapi.top 在允许第三方工具的分组下' 'DarkGray' Say ' ' '' '新建令牌,然后重新运行本脚本选择「2) 配置 闪电API」' 'DarkGray' } return $true } ############################################################################## # 状态检测与菜单 ############################################################################## function Show-StatusSummary { Say ' ' '' '当前状态:' 'White' $osLabel = 'windows' if (-not $OnWindows) { $osLabel = ([string][Environment]::OSVersion.Platform).ToLowerInvariant() } Say ' 系统环境: ' '' $osLabel 'Cyan' $piVer = Check-PiInstalled if ($null -ne $piVer) { Say ' Pi: ' '' '已安装' 'Green' " ($piVer)" '' } else { Say ' Pi: ' '' '未安装' 'Red' } $key = Read-ProviderField $ProviderClaude 'apiKey' if ($key) { Say ' Claude 配置: ' '' '已配置' 'Green' " ($(Mask-Key $key))" '' } else { Say ' Claude 配置: ' '' '未配置' 'Red' } $key = Read-ProviderField $ProviderGpt 'apiKey' if ($key) { Say ' GPT 配置: ' '' '已配置' 'Green' " ($(Mask-Key $key))" '' } else { Say ' GPT 配置: ' '' '未配置' 'Red' } $url = Read-ProviderField $ProviderClaude 'baseUrl' if (-not $url) { $url = Read-ProviderField $ProviderGpt 'baseUrl' } if ($url) { Say ' API 线路: ' '' ($url -replace '/v1$', '') 'Green' } $dp = Read-SettingsField 'defaultProvider' $dm = Read-SettingsField 'defaultModel' if ($dp -or $dm) { Say ' 默认模型: ' '' "$dp/$dm" 'Cyan' } Say ' Pi 配置目录: ' '' $PiAgentDir 'Cyan' Say '' } function Show-Menu { Say ' ' '' '请选择操作:' 'White' Say '' Say ' ' '' '⚡' 'Yellow' ' ' '' '1)' 'Cyan' ' 安装 Pi' '' 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 } if ($choice -eq '1') { try { Install-Pi | Out-Null } catch { Print-Status 'error' "出错: $($_.Exception.Message)" } Say '' } elseif ($choice -eq '2') { try { Configure-Api | Out-Null } catch { Print-Status 'error' "出错: $($_.Exception.Message)" } Say '' } elseif ($choice -eq '3') { try { Install-Pi | Out-Null } catch { Print-Status 'error' "出错: $($_.Exception.Message)" } Say '' Say " $Line" 'DarkGray' $ok = $false try { $ok = Configure-Api } catch { Print-Status 'error' "出错: $($_.Exception.Message)" } if ($ok) { Say '' Say " $Line" 'DarkGray' Say '' Print-Status 'ok' '⚡ 全部完成! 请重新打开终端后运行 pi' } Say '' } elseif ($choice -eq '0') { Say '' Say ' ' '' '⚡' 'Yellow' ' ' '' '感谢使用闪电API,祝你Vibe Coding愉快!' 'White' Say ' ' '' '官网: flashapi.top' 'DarkGray' Say '' return } else { Print-Status 'error' '无效选项,请重新选择' Say '' } } } Main }