$ErrorActionPreference = "Stop" $BaseUrl = "https://code.aiserverai.online/v1" $NodeMirror = "https://npmmirror.com/mirrors/node" $NpmMirror = "https://registry.npmmirror.com" $SupportEmail = "alexisty233@gmail.com" $InstallRoot = Join-Path $env:LOCALAPPDATA "Programs" $NodeRoot = Join-Path $InstallRoot "nodejs-codex" $CodexPrefix = Join-Path $InstallRoot "codex-npm" $CodexHomeDir = Join-Path $HOME ".codex" $TempDir = Join-Path ([System.IO.Path]::GetTempPath()) ("codex-install-" + [Guid]::NewGuid().ToString("N")) Add-Type -AssemblyName System.Windows.Forms Add-Type -AssemblyName System.Drawing function Show-Result { param( [string]$Title, [string]$Message, [System.Windows.Forms.MessageBoxIcon]$Icon ) [System.Windows.Forms.MessageBox]::Show( $Message, $Title, [System.Windows.Forms.MessageBoxButtons]::OK, $Icon ) | Out-Null } function Read-ApiKey { $form = New-Object System.Windows.Forms.Form $form.Text = "Codex 一键配置" $form.StartPosition = "CenterScreen" $form.ClientSize = New-Object System.Drawing.Size(480, 170) $form.FormBorderStyle = "FixedDialog" $form.MaximizeBox = $false $form.MinimizeBox = $false $label = New-Object System.Windows.Forms.Label $label.Text = "请输入你的 Sub2API Key" $label.AutoSize = $true $label.Location = New-Object System.Drawing.Point(24, 22) $form.Controls.Add($label) $input = New-Object System.Windows.Forms.TextBox $input.Location = New-Object System.Drawing.Point(24, 52) $input.Size = New-Object System.Drawing.Size(430, 28) $input.UseSystemPasswordChar = $true $form.Controls.Add($input) $ok = New-Object System.Windows.Forms.Button $ok.Text = "继续" $ok.DialogResult = [System.Windows.Forms.DialogResult]::OK $ok.Location = New-Object System.Drawing.Point(286, 105) $form.Controls.Add($ok) $cancel = New-Object System.Windows.Forms.Button $cancel.Text = "取消" $cancel.DialogResult = [System.Windows.Forms.DialogResult]::Cancel $cancel.Location = New-Object System.Drawing.Point(379, 105) $form.Controls.Add($cancel) $form.AcceptButton = $ok $form.CancelButton = $cancel $form.Add_Shown({ $input.Select() }) if ($form.ShowDialog() -ne [System.Windows.Forms.DialogResult]::OK) { throw "用户取消了 Key 输入" } if ([string]::IsNullOrWhiteSpace($input.Text)) { throw "API Key 不能为空" } return $input.Text.Trim() } function Add-UserPath { param([string]$Directory) $current = [Environment]::GetEnvironmentVariable("Path", "User") $parts = @($current -split ";" | Where-Object { $_ }) if ($parts -notcontains $Directory) { $updated = (@($Directory) + $parts) -join ";" [Environment]::SetEnvironmentVariable("Path", $updated, "User") } if (($env:Path -split ";") -notcontains $Directory) { $env:Path = "$Directory;$env:Path" } } function Ensure-Node { $npm = Get-Command npm.cmd -ErrorAction SilentlyContinue $node = Get-Command node.exe -ErrorAction SilentlyContinue if ($npm -and $node) { $nodeMajor = [int](& node.exe -p "process.versions.node.split('.')[0]") if ($nodeMajor -ge 20) { return } } Write-Host "正在安装 Node.js LTS..." try { $releases = Invoke-RestMethod -Uri "$NodeMirror/index.json" } catch { $releases = Invoke-RestMethod -Uri "https://nodejs.org/dist/index.json" } $release = $releases | Where-Object { $_.lts } | Select-Object -First 1 if (-not $release) { throw "无法获取 Node.js LTS 版本" } $version = $release.version $arch = if ($env:PROCESSOR_ARCHITECTURE -eq "ARM64") { "arm64" } else { "x64" } $archiveName = "node-$version-win-$arch.zip" $archivePath = Join-Path $TempDir $archiveName $extractPath = Join-Path $TempDir "node" $versionHome = Join-Path $NodeRoot "$version-$arch" New-Item -ItemType Directory -Force -Path $TempDir, $NodeRoot | Out-Null try { Invoke-WebRequest -UseBasicParsing -Uri "$NodeMirror/$version/$archiveName" -OutFile $archivePath } catch { Invoke-WebRequest -UseBasicParsing -Uri "https://nodejs.org/dist/$version/$archiveName" -OutFile $archivePath } try { $checksumText = (Invoke-WebRequest -UseBasicParsing -Uri "$NodeMirror/$version/SHASUMS256.txt").Content } catch { $checksumText = (Invoke-WebRequest -UseBasicParsing -Uri "https://nodejs.org/dist/$version/SHASUMS256.txt").Content } $checksumLine = $checksumText -split "[\r\n]+" | Where-Object { $_ -match ("^[a-fA-F0-9]{64}[ ]+" + [regex]::Escape($archiveName) + "$") } | Select-Object -First 1 if (-not $checksumLine) { throw "无法校验 Node.js 安装包" } $expectedHash = ($checksumLine -split "[ ]+")[0].ToLowerInvariant() $actualHash = (Get-FileHash -Algorithm SHA256 -Path $archivePath).Hash.ToLowerInvariant() if ($expectedHash -ne $actualHash) { throw "Node.js 安装包校验失败" } Expand-Archive -Path $archivePath -DestinationPath $extractPath -Force if (-not (Test-Path $versionHome)) { $expanded = Get-ChildItem -Path $extractPath -Directory | Select-Object -First 1 Move-Item -Path $expanded.FullName -Destination $versionHome } Add-UserPath $versionHome } function Write-CodexConfig { New-Item -ItemType Directory -Force -Path $CodexHomeDir | Out-Null $stamp = Get-Date -Format "yyyyMMdd-HHmmss" $mainConfig = Join-Path $CodexHomeDir "config.toml" if (Test-Path $mainConfig) { Copy-Item $mainConfig "$mainConfig.backup-$stamp" } $existingLines = @() if (Test-Path $mainConfig) { $skipProvider = $false $inTable = $false foreach ($line in Get-Content $mainConfig) { if ($line -match '^\[') { $inTable = $true $skipProvider = $line -match '^\[model_providers\.aiserver(\.|\])' } if ($skipProvider) { continue } if (-not $inTable -and $line -match '^\s*(profile|model|model_provider|model_reasoning_effort)\s*=') { continue } $existingLines += $line } } $managedConfig = @' model = "gpt-5.6-sol" model_provider = "aiserver" model_reasoning_effort = "medium" '@ $providerConfig = @' [model_providers.aiserver] name = "AIServer" base_url = "https://code.aiserverai.online/v1" wire_api = "responses" requires_openai_auth = true '@ $mainText = (@($managedConfig.TrimEnd()) + $existingLines + @($providerConfig.TrimEnd())) -join [Environment]::NewLine $utf8 = New-Object System.Text.UTF8Encoding($false) [IO.File]::WriteAllText($mainConfig, $mainText + [Environment]::NewLine, $utf8) } function Test-CodexCli { param([string]$CodexBin) if (-not (Test-Path $CodexBin)) { return $false } & $CodexBin --version *> $null return $LASTEXITCODE -eq 0 } try { New-Item -ItemType Directory -Force -Path $TempDir, $CodexPrefix | Out-Null Ensure-Node Add-UserPath $CodexPrefix Write-Host "正在安装 Codex CLI..." $NpmCache = Join-Path $CodexPrefix "cache" $CodexBin = Join-Path $CodexPrefix "codex.cmd" New-Item -ItemType Directory -Force -Path $NpmCache | Out-Null & npm.cmd install -g --prefix $CodexPrefix --cache=$NpmCache --registry=$NpmMirror --include=optional --no-audit --no-fund "@openai/codex@latest" if ($LASTEXITCODE -ne 0 -or -not (Test-CodexCli $CodexBin)) { Write-Host "中国 npm 镜像安装不完整,正在使用官方源修复..." & npm.cmd install -g --force --prefix $CodexPrefix --cache=$NpmCache --registry="https://registry.npmjs.org" --include=optional --no-audit --no-fund "@openai/codex@latest" } if ($LASTEXITCODE -ne 0 -or -not (Test-CodexCli $CodexBin)) { throw "Codex CLI 或当前 Windows 架构对应的原生组件安装未完成" } $ApiKey = Read-ApiKey Invoke-WebRequest -UseBasicParsing -Uri "$BaseUrl/models" -Headers @{ Authorization = "Bearer $ApiKey" } -Method Get | Out-Null Write-CodexConfig $ApiKey | & $CodexBin login --with-api-key | Out-Null $ApiKey = $null Show-Result "配置完成" "Codex 已安装并完成 AIServer 配置。打开新的 PowerShell,进入项目目录运行 codex 即可。" ([System.Windows.Forms.MessageBoxIcon]::Information) } catch { $supportMessage = $_.Exception.Message + [Environment]::NewLine + [Environment]::NewLine + "请附完整失败截图联系管理员:$SupportEmail" + [Environment]::NewLine + "我们会在 72 小时内更新脚本,请更新后重试。" Show-Result "Codex 安装失败" $supportMessage ([System.Windows.Forms.MessageBoxIcon]::Error) Write-Error $_ exit 1 } finally { $ApiKey = $null if (Test-Path $TempDir) { Remove-Item -Recurse -Force $TempDir } }