$ErrorActionPreference = "Stop" $PrimaryBaseUrl = "https://code.aiserverai.online/v1" $SecondaryBaseUrl = "https://cx.aiserverai.online/v1" $XmumacBaseUrl = "https://code.xmumac.com/v1" $BaseUrl = $PrimaryBaseUrl $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, 230) $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) $endpointLabel = New-Object System.Windows.Forms.Label $endpointLabel.Text = "选择 API URL(三个入口连接同一服务)" $endpointLabel.AutoSize = $true $endpointLabel.Location = New-Object System.Drawing.Point(24, 92) $form.Controls.Add($endpointLabel) $endpoint = New-Object System.Windows.Forms.ComboBox $endpoint.Location = New-Object System.Drawing.Point(24, 118) $endpoint.Size = New-Object System.Drawing.Size(430, 28) $endpoint.DropDownStyle = [System.Windows.Forms.ComboBoxStyle]::DropDownList $endpoint.Items.Add($PrimaryBaseUrl) | Out-Null $endpoint.Items.Add($SecondaryBaseUrl) | Out-Null $endpoint.Items.Add($XmumacBaseUrl) | Out-Null $endpoint.SelectedIndex = 0 $form.Controls.Add($endpoint) $ok = New-Object System.Windows.Forms.Button $ok.Text = "继续" $ok.DialogResult = [System.Windows.Forms.DialogResult]::OK $ok.Location = New-Object System.Drawing.Point(286, 165) $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, 165) $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 不能为空" } $script:BaseUrl = $endpoint.SelectedItem.ToString() 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 $nativeArchitecture = if ($env:PROCESSOR_ARCHITEW6432) { $env:PROCESSOR_ARCHITEW6432 } else { $env:PROCESSOR_ARCHITECTURE } $arch = switch ($nativeArchitecture) { "ARM64" { "arm64"; break } "AMD64" { "x64"; break } "X64" { "x64"; break } default { throw "暂不支持此 Windows 架构:$nativeArchitecture(仅支持 x64 和 ARM64)" } } $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 $skipManaged = $line -match '^\[model_providers\.(aiserver|cch)(\.|\])' -or $line -match '^\[sandbox_workspace_write(\.|\])' } if ($skipManaged) { continue } if (-not $inTable -and $line -match '^\s*(model|model_provider|model_reasoning_effort|disable_response_storage|sandbox_mode)\s*=') { continue } $existingLines += $line } } $managedConfig = @' model = "gpt-5.6-sol" model_provider = "cch" model_reasoning_effort = "medium" disable_response_storage = true sandbox_mode = "workspace-write" '@ $providerConfig = @" [model_providers.cch] name = "cch" base_url = "$BaseUrl" wire_api = "responses" requires_openai_auth = true [sandbox_workspace_write] network_access = 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 Write-CodexAuth { param([string]$ApiKey) $stamp = Get-Date -Format "yyyyMMdd-HHmmss" $authFile = Join-Path $CodexHomeDir "auth.json" if (Test-Path $authFile) { Copy-Item $authFile "$authFile.backup-$stamp" } $authJson = @{ OPENAI_API_KEY = $ApiKey } | ConvertTo-Json $utf8 = New-Object System.Text.UTF8Encoding($false) [IO.File]::WriteAllText($authFile, $authJson + [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 Write-CodexAuth -ApiKey $ApiKey $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 } }