################################################################################ # 闪电API - OpenAI Codex CLI 一键安装脚本 (Windows PowerShell) # # 功能: 安装 Codex CLI + 配置 Codex CLI / 桌面版 App API # 官网: flashapi.top # 模型调用: ai.flashapi.top (国内直连) / cdn.flashapi.top (海外CDN) # # 用法 (PowerShell): irm https://sh.flashapi.top/codex.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 } $CodexConfigDir = $env:CODEX_HOME if (-not $CodexConfigDir) { $CodexConfigDir = Join-Path $UserHome '.codex' } $CodexConfigFile = Join-Path $CodexConfigDir 'config.toml' $CodexAuthFile = Join-Path $CodexConfigDir 'auth.json' $NpmMirror = 'https://registry.npmmirror.com' $DefaultModel = 'gpt-6.1-sol' $DefaultEffort = 'medium' # 可变状态放在哈希表里 (子函数可直接修改,且不污染用户会话) $S = @{ UsageTarget = '' UsageTargetLabel = '' } $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" 'Yellow' $a = Read-Line return ($a -eq 'y' -or $a -eq 'Y') } function Print-Banner { Say '' Say ' ' '' ' ⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡' 'Green' Say ' ' '' ' ██████╗ ██████╗ ██████╗ ███████╗██╗ ██╗' 'Green' Say ' ' '' ' ██╔════╝██╔═══██╗██╔══██╗██╔════╝╚██╗██╔╝' 'Green' Say ' ' '' ' ██║ ██║ ██║██║ ██║█████╗ ╚███╔╝' 'Green' Say ' ' '' ' ██║ ██║ ██║██║ ██║██╔══╝ ██╔██╗' 'Green' Say ' ' '' ' ╚██████╗╚██████╔╝██████╔╝███████╗██╔╝ ██╗' 'Green' Say ' ' '' ' ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝' 'Green' Say ' ' '' ' ⚡ 闪电API × Codex CLI ⚡' 'Yellow' Say ' ' '' ' ⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡' 'Green' Say '' Say " $Line" 'DarkGray' Say ' ' '' '⚡ 闪电API' 'White' ' ' '' '|' 'DarkGray' ' ' '' '官网' 'Cyan' ' ' '' 'flashapi.top' 'Yellow' Say ' ' '' 'Codex CLI / App 一键配置' '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 (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') } # 读取 JSON 文件;不存在/解析失败/顶层不是对象时返回 $null function Read-JsonObject { param([string]$Path) if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { return $null } try { $text = [IO.File]::ReadAllText($Path) if (-not $text.Trim()) { return $null } $opts = @{} if ($JsonDateKind) { $opts['DateKind'] = 'String' } $obj = ConvertFrom-Json -InputObject $text @opts if (Test-JsonObject $obj) { return $obj } } catch {} return $null } function New-JsonObject { return (New-Object -TypeName PSObject) } function Get-Prop { param($Obj, [string]$Name) if ($null -eq $Obj) { return $null } $p = $Obj.PSObject.Properties[$Name] if ($null -eq $p) { return $null } return $p.Value } function Has-Prop { param($Obj, [string]$Name) return ($null -ne $Obj.PSObject.Properties[$Name]) } 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)) } function Write-JsonFile { param([string]$Path, $Data) Write-Utf8File $Path ((ConvertTo-JsonText $Data 0) + "`n") } ############################################################################## # 功能 1: 安装 Codex CLI ############################################################################## function Get-NpmExe { return (Find-Exe @('npm.cmd', 'npm')) } function Get-CodexExe { return (Find-Exe @('codex.cmd', 'codex.exe', 'codex')) } # 已安装返回版本号,未安装返回 $null function Check-CodexInstalled { $exe = Get-CodexExe if (-not $exe) { return $null } return (Get-ExeVersion $exe) } function Invoke-NpmInstall { param([string]$Npm, [string]$Registry) $ErrorActionPreference = 'Continue' try { if ($Registry) { & $Npm install -g '@openai/codex' "--registry=$Registry" | Out-Host } else { & $Npm install -g '@openai/codex' 2>$null | 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 Get-NodeMajor { $node = Find-Exe @('node.exe', 'node') if (-not $node) { return $null } $v = Get-ExeVersion $node $m = [regex]::Match($v, '^v?(\d+)') if ($m.Success) { return [int]$m.Groups[1].Value } return $null } function Install-CodexCli { Say '' Print-Status 'info' '⚡ 正在安装 Codex CLI ...' Say '' $ver = Check-CodexInstalled if ($null -ne $ver) { Print-Status 'ok' "Codex CLI 已安装 ($ver)" Say '' if (-not (Read-YesNo '是否重新安装? (y/N): ')) { Print-Status 'info' '跳过安装'; return $true } } $npm = Get-NpmExe if (-not $npm) { Print-Status 'error' '未找到 npm,请先安装 Node.js (需要 v22+)' Say '' Say ' ' '' '安装方式:' 'White' Say ' ' '' '官网下载:' 'Cyan' ' https://nodejs.org/' '' Say ' ' '' 'winget:' 'Cyan' ' winget install OpenJS.NodeJS.LTS' '' return $false } # 检查 Node 版本 (Codex 需要 v22+) $major = Get-NodeMajor if ($null -ne $major -and $major -lt 22) { Print-Status 'warn' "当前 Node.js 主版本 v$major,Codex CLI 建议 v22+" Print-Status 'info' '建议升级: https://nodejs.org/' Say '' if (-not (Read-YesNo '是否仍然继续安装? (y/N): ')) { Print-Status 'info' '已取消'; return $false } } $npmVer = Get-ExeVersion $npm Print-Status 'ok' "npm 已就绪 ($npmVer)" Print-Status 'info' '使用 npm 安装 @openai/codex ...' Say '' if (-not (Invoke-NpmInstall $npm '')) { Print-Status 'warn' '默认源安装失败,可能是网络问题' if (-not (Install-WithMirror $npm)) { Print-Status 'error' '所有安装方式均失败,请检查网络连接' return $false } } Say '' $ver = Check-CodexInstalled if ($null -ne $ver) { Print-Status 'ok' "⚡ Codex CLI 安装成功 ($ver)" } else { Print-Status 'warn' '安装已完成,但未在 PATH 中找到 codex' Print-Status 'info' '请重新打开 PowerShell 后再试' } return $true } ############################################################################## # 功能 2: 配置 API ############################################################################## function Choose-UsageTarget { Say ' ' '' '请选择要在哪个 Codex 客户端中使用:' 'White' Say '' Say ' ' '' '⚡' 'Yellow' ' ' '' '1)' 'Cyan' ' Codex CLI' '' Say ' ' '' '⚡' 'Yellow' ' ' '' '2)' 'Cyan' ' Codex 桌面版 App' '' Say ' ' '' '⚡' 'Yellow' ' ' '' '回车' 'Cyan' ' CLI 和桌面版 App 都使用' '' Say '' SayN ' ' '' '请输入选项 [回车=全部]: ' 'White' $choice = Read-Line switch ($choice) { '' { $S.UsageTarget = 'all'; $S.UsageTargetLabel = 'Codex CLI / 桌面版 App' } '1' { $S.UsageTarget = 'cli'; $S.UsageTargetLabel = 'Codex CLI' } '2' { $S.UsageTarget = 'app'; $S.UsageTargetLabel = 'Codex 桌面版 App' } default { Print-Status 'error' '无效选项,已取消'; return $false } } Say '' Print-Status 'ok' "已选择: $($S.UsageTargetLabel)" return $true } # 返回 HTTP 状态码;网络错误返回 0 function Get-HttpStatus { param([string]$Uri, [hashtable]$Headers) try { $r = Invoke-WebRequest -Uri $Uri -Headers $Headers -Method Get -UseBasicParsing -TimeoutSec 10 return [int]$r.StatusCode } catch { $ex = $_.Exception $resp = $null if ($null -ne $ex -and $null -ne $ex.PSObject.Properties['Response']) { $resp = $ex.Response } if ($null -ne $resp) { try { return [int]$resp.StatusCode } catch { return 0 } } return 0 } } function Validate-ApiKey { param([string]$BaseUrl, [string]$ApiKey) Print-Status 'info' '正在验证 API Key ...' $code = Get-HttpStatus "$BaseUrl/v1/models" @{ 'Authorization' = "Bearer $ApiKey" } if ($code -eq 200) { Print-Status 'ok' '⚡ API Key 验证通过,模型列表获取成功' return $true } elseif ($code -eq 401) { Print-Status 'error' 'API Key 无效 (401 Unauthorized)' return $false } elseif ($code -eq 0) { Print-Status 'warn' "无法连接到 $BaseUrl (网络错误)" return $false } else { Print-Status 'warn' "API 返回状态码: $code (可能仍可用)" return $true } } # 与 codex.sh 的 awk 过滤一致: 去掉 [model_providers.flashapi] 整个表, # 以及第一个表之前由本脚本管理的顶层键;其余内容原样保留 function Write-CodexConfig { param([string]$BaseUrl) Ensure-Dir $CodexConfigDir Backup-IfExists $CodexConfigFile $managed = '^(model_provider|model|review_model|model_reasoning_effort|openai_base_url|disable_response_storage|network_access|windows_wsl_setup_acknowledged|model_context_window|model_auto_compact_token_limit|effective_context_window_percent)[ \t]*=' $kept = New-Object System.Collections.Generic.List[string] if (Test-Path -LiteralPath $CodexConfigFile -PathType Leaf) { $text = [IO.File]::ReadAllText($CodexConfigFile) if ($text.Length -gt 0 -and [int]$text[0] -eq 0xFEFF) { $text = $text.Substring(1) } $lines = $text -split "`r?`n" $skipProvider = $false $inTable = $false foreach ($l in $lines) { if ($l -eq '[model_providers.flashapi]') { $skipProvider = $true; $inTable = $true; continue } if ($l.StartsWith('[')) { $skipProvider = $false; $inTable = $true } if ($skipProvider) { continue } if (-not $inTable -and $l -match $managed) { continue } if ($l -match '^[ \t]*$') { continue } $kept.Add($l) } } $out = New-Object System.Collections.Generic.List[string] $out.Add('# 基础配置') $out.Add('model_provider = "flashapi"') $out.Add("model = `"$DefaultModel`"") $out.Add("review_model = `"$DefaultModel`"") $out.Add("model_reasoning_effort = `"$DefaultEffort`"") $out.Add('disable_response_storage = true') $out.Add('network_access = "enabled"') $out.Add('windows_wsl_setup_acknowledged = true') $out.Add('model_context_window = 272000') $out.Add('model_auto_compact_token_limit = 272000') $out.Add('effective_context_window_percent = 95') $out.Add('') foreach ($l in $kept) { $out.Add($l) } $out.Add('') $out.Add('[model_providers.flashapi]') $out.Add('name = "flashapi"') $out.Add("base_url = `"$BaseUrl`"") $out.Add('wire_api = "responses"') $out.Add('requires_openai_auth = true') $out.Add('supports_websockets = false') $out.Add('http_headers = { "x-openai-actor-authorization" = "flashapi.top" }') Write-Utf8File $CodexConfigFile (($out -join "`n") + "`n") } function Write-CodexAuth { param([string]$ApiKey) Ensure-Dir $CodexConfigDir Backup-IfExists $CodexAuthFile $data = Read-JsonObject $CodexAuthFile if ($null -eq $data) { $data = New-JsonObject } # 存在 ChatGPT 官方登录时 Codex 会优先使用它 (请求中转会 401),因此移除登录凭据 (原文件已备份) if ($null -ne (Get-Prop $data 'tokens')) { Remove-Prop $data 'tokens' Remove-Prop $data 'last_refresh' Print-Status 'warn' '检测到 ChatGPT 官方登录凭据,Codex 会优先使用它导致中转 401,已从 auth.json 移除 (原文件已备份,恢复官方登录请把备份文件改回 auth.json 或重新 codex login)' } Remove-Prop $data 'auth_mode' Set-Prop $data 'OPENAI_API_KEY' $ApiKey Write-JsonFile $CodexAuthFile $data } function Read-AuthKey { $data = Read-JsonObject $CodexAuthFile $v = Get-Prop $data 'OPENAI_API_KEY' if ($v -is [string] -and $v) { return $v } return '' } function Read-ConfigBaseUrl { if (-not (Test-Path -LiteralPath $CodexConfigFile -PathType Leaf)) { return '' } $inProvider = $false foreach ($l in ([IO.File]::ReadAllText($CodexConfigFile) -split "`r?`n")) { if ($l -eq '[model_providers.flashapi]') { $inProvider = $true; continue } if ($inProvider -and $l.StartsWith('[')) { break } if ($inProvider) { $m = [regex]::Match($l, '^[ \t]*base_url[ \t]*=[^"]*"([^"]*)"') if ($m.Success) { return $m.Groups[1].Value } } } return '' } function Configure-Api { Say '' Print-Status 'info' '⚡ 配置 Codex CLI / 桌面版 App API' Say '' $existingKey = Read-AuthKey $existingUrl = Read-ConfigBaseUrl if ($existingKey) { Print-Status 'info' "当前 API Key: $(Mask-Key $existingKey)" } if ($existingUrl) { Print-Status 'info' "当前 BASE URL: $existingUrl" } Say '' if (-not $S.UsageTarget) { if (-not (Choose-UsageTarget)) { 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 (密码遮盖) ── $apiKey = Read-Secret ' 请输入 API Key: ' if (-not $apiKey) { Print-Status 'error' 'API Key 不能为空,已取消' return $false } Say '' # ── 验证 ── if (-not (Validate-ApiKey $baseUrl $apiKey)) { if (-not (Read-YesNo '验证未通过,是否仍要保存? (y/N): ')) { Print-Status 'info' '已取消配置'; return $false } } # ── 写入 Codex 配置文件 ── Write-CodexConfig "$baseUrl/v1" Write-CodexAuth $apiKey Say '' Print-Status 'ok' "API Key 已保存: $(Mask-Key $apiKey)" Print-Status 'ok' "BASE URL 已保存: $baseUrl/v1" Print-Status 'ok' "默认模型: $DefaultModel (推理强度: $DefaultEffort)" Print-Status 'ok' "配置文件已写入: $CodexConfigFile" Print-Status 'ok' "认证文件已写入: $CodexAuthFile" if ($S.UsageTarget -eq 'app' -or $S.UsageTarget -eq 'all') { Print-Status 'info' '如果 Codex 桌面版 App 正在运行,请彻底退出后重新打开' } return $true } ############################################################################## # 状态检测与菜单 ############################################################################## function Show-StatusSummary { Say ' ' '' '当前状态:' 'White' $osLabel = 'windows' if (-not $OnWindows) { $osLabel = ([string][Environment]::OSVersion.Platform).ToLowerInvariant() } Say ' 系统环境: ' '' $osLabel 'Cyan' $ver = Check-CodexInstalled if ($null -ne $ver) { Say ' Codex CLI: ' '' '已安装' 'Green' " ($ver)" '' } else { Say ' Codex CLI: ' '' '未安装' 'Red' } $configuredKey = Read-AuthKey $configuredUrl = Read-ConfigBaseUrl if ($configuredKey) { Say ' API 配置: ' '' '已配置' 'Green' " ($(Mask-Key $configuredKey))" '' } else { Say ' API 配置: ' '' '未配置' 'Red' } if ($configuredUrl) { Say ' API 线路: ' '' $configuredUrl 'Green' } Say ' Codex 配置: ' '' $CodexConfigFile 'Cyan' Say '' } function Show-Menu { Say ' ' '' '请选择操作:' 'White' Say '' Say ' ' '' '⚡' 'Yellow' ' ' '' '1)' 'Cyan' ' 安装 Codex CLI' '' Say ' ' '' '⚡' 'Yellow' ' ' '' '2)' 'Cyan' ' 配置 Codex CLI / 桌面版 App' '' Say ' ' '' '⚡' 'Yellow' ' ' '' '3)' 'Cyan' ' 一键全部安装 (安装 + 配置)' '' Say ' ' '' '⚡' 'Yellow' ' ' '' '0)' 'Cyan' ' 退出' '' Say '' SayN ' ' '' '请输入选项: ' 'White' } function Invoke-Safely { param([scriptblock]$Block) try { & $Block | Out-Null } catch { Print-Status 'error' "出错: $($_.Exception.Message)" } } ############################################################################## # 主流程 ############################################################################## function Main { Print-Banner Show-StatusSummary while ($true) { $S.UsageTarget = '' $S.UsageTargetLabel = '' Show-Menu $choice = Read-Line if ($null -eq $choice) { Say ''; return } switch ($choice) { '1' { Invoke-Safely { Install-CodexCli | Out-Null } Say '' } '2' { Invoke-Safely { Configure-Api | Out-Null } Say '' } '3' { Invoke-Safely { Install-CodexCli | Out-Null } Say '' Say " $Line" 'DarkGray' Invoke-Safely { Configure-Api | Out-Null } Say '' Say " $Line" 'DarkGray' Say '' Print-Status 'ok' '⚡ 全部完成! Codex CLI 请重新打开 PowerShell 后运行 codex;桌面版 App 请重启后使用' Say '' } '0' { Say '' Say ' ' '' '⚡' 'Yellow' ' ' '' '感谢使用闪电API,祝你Vibe Coding愉快!' 'White' Say ' ' '' '官网: flashapi.top' 'DarkGray' Say '' return } default { Print-Status 'error' '无效选项,请重新选择' Say '' } } } } Main }