param( [string]$NodeMajor = "24", [string]$CodexPackage = "@wepulse/codex", [string]$CodexVersion = "latest", [string]$NodeDistBaseUrl = "https://npmmirror.com/mirrors/node", [string]$NpmRegistry = "https://registry.npmmirror.com", [string]$WePulseCodexBaseUrl = "https://agent-api.wepulse.cn/v1", [string]$WePulseAuthBaseUrl = "https://auth.wepulse.cn", [string]$WePulseCodexReleaseBaseUrl = "https://codex.wepulse.cn" ) Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" $ProgressPreference = "SilentlyContinue" $script:Utf8NoBom = [System.Text.UTF8Encoding]::new($false) try { [Console]::InputEncoding = $script:Utf8NoBom [Console]::OutputEncoding = $script:Utf8NoBom $OutputEncoding = $script:Utf8NoBom } catch { # Older PowerShell hosts may reject console encoding changes. } if (-not [string]::IsNullOrWhiteSpace($env:WEPULSE_NODE_DIST_BASE_URL)) { $NodeDistBaseUrl = $env:WEPULSE_NODE_DIST_BASE_URL } if (-not [string]::IsNullOrWhiteSpace($env:WEPULSE_NPM_REGISTRY)) { $NpmRegistry = $env:WEPULSE_NPM_REGISTRY } if (-not [string]::IsNullOrWhiteSpace($env:WEPULSE_CODEX_BASE_URL)) { $WePulseCodexBaseUrl = $env:WEPULSE_CODEX_BASE_URL } if (-not [string]::IsNullOrWhiteSpace($env:WEPULSE_AUTH_BASE_URL)) { $WePulseAuthBaseUrl = $env:WEPULSE_AUTH_BASE_URL } if (-not [string]::IsNullOrWhiteSpace($env:WEPULSE_CODEX_RELEASE_BASE_URL)) { $WePulseCodexReleaseBaseUrl = $env:WEPULSE_CODEX_RELEASE_BASE_URL } $NodeDistBaseUrl = $NodeDistBaseUrl.TrimEnd("/") $NpmRegistry = $NpmRegistry.TrimEnd("/") $WePulseCodexReleaseBaseUrl = $WePulseCodexReleaseBaseUrl.TrimEnd("/") function Get-InstallLanguage { $cultureNames = @( [System.Globalization.CultureInfo]::CurrentUICulture.Name, [System.Globalization.CultureInfo]::CurrentCulture.Name, $env:LC_ALL, $env:LC_MESSAGES, $env:LANG ) foreach ($name in $cultureNames) { if (-not [string]::IsNullOrWhiteSpace($name) -and $name -match "^(?i:zh)(-|_|$)") { return "zh" } } return "en" } $script:InstallLanguage = Get-InstallLanguage function Select-InstallText { param( [string]$Zh, [string]$En ) if ($script:InstallLanguage -eq "zh") { return $Zh } return $En } function Write-Step { param( [string]$Message, [string]$EnglishMessage = $null ) if ($null -ne $EnglishMessage) { $Message = Select-InstallText -Zh $Message -En $EnglishMessage } Write-Host "==> $Message" } function Write-InstallWarning { param( [string]$Message, [string]$EnglishMessage = $null ) if ($null -ne $EnglishMessage) { $Message = Select-InstallText -Zh $Message -En $EnglishMessage } Write-Warning $Message } function New-InstallError { param( [string]$Message, [string]$EnglishMessage = $null ) if ($null -ne $EnglishMessage) { return (Select-InstallText -Zh $Message -En $EnglishMessage) } return $Message } function Initialize-NpmUserConfig { $wepulseRoot = Join-Path $env:LOCALAPPDATA "WePulse" New-Item -ItemType Directory -Force -Path $wepulseRoot | Out-Null $source = $env:npm_config_userconfig if ([string]::IsNullOrWhiteSpace($source)) { $source = Join-Path $HOME ".npmrc" } $target = Join-Path $wepulseRoot "npmrc" $obsoleteConfigPattern = "^\s*(msvs[-_]version|msbuild[-_]path|disturl|dist-url)\s*=" $lines = @() if ((Test-Path -LiteralPath $source -PathType Leaf) -and ($source -ine $target)) { foreach ($line in Get-Content -LiteralPath $source -ErrorAction Stop) { if ($line -notmatch $obsoleteConfigPattern) { $lines += $line } } } elseif (Test-Path -LiteralPath $target -PathType Leaf) { foreach ($line in Get-Content -LiteralPath $target -ErrorAction Stop) { if ($line -notmatch $obsoleteConfigPattern) { $lines += $line } } } [System.IO.File]::WriteAllText($target, ($lines -join [Environment]::NewLine) + [Environment]::NewLine, $script:Utf8NoBom) $env:npm_config_userconfig = $target Remove-Item Env:\npm_config_msvs_version -ErrorAction SilentlyContinue Remove-Item Env:\npm_config_msbuild_path -ErrorAction SilentlyContinue Remove-Item Env:\npm_config_disturl -ErrorAction SilentlyContinue Remove-Item Env:\npm_config_dist_url -ErrorAction SilentlyContinue } function Get-NodeMajor { try { $version = & node -p "process.versions.node.split('.')[0]" 2>$null if ($LASTEXITCODE -eq 0) { return [string]$version } } catch { return $null } return $null } function Add-UserPathEntry { param( [string]$Entry, [switch]$Prepend ) $current = [Environment]::GetEnvironmentVariable("Path", "User") $segments = @() if (-not [string]::IsNullOrWhiteSpace($current)) { $segments = $current.Split(";", [System.StringSplitOptions]::RemoveEmptyEntries) } $filtered = @() $alreadyFirst = $false $entryKey = $Entry.TrimEnd("\") for ($index = 0; $index -lt $segments.Count; $index++) { $segment = $segments[$index] if ($segment.TrimEnd("\") -ieq $entryKey) { if ($index -eq 0) { $alreadyFirst = $true } continue } $filtered += $segment } if ($Prepend) { if ($alreadyFirst -and $filtered.Count -eq ($segments.Count - 1)) { return } $updatedSegments = @($Entry) + $filtered } else { if ($filtered.Count -ne $segments.Count) { return } $updatedSegments = $filtered + @($Entry) } $updated = $updatedSegments -join ";" [Environment]::SetEnvironmentVariable("Path", $updated, "User") } function Add-CurrentPathEntryFirst { param([string]$Entry) $segments = @() if (-not [string]::IsNullOrWhiteSpace($env:Path)) { $segments = $env:Path.Split(";", [System.StringSplitOptions]::RemoveEmptyEntries) } $entryKey = $Entry.TrimEnd("\") $filtered = @() foreach ($segment in $segments) { if ($segment.TrimEnd("\") -ine $entryKey) { $filtered += $segment } } $env:Path = (@($Entry) + $filtered) -join ";" } function Prompt-YesNo { param( [string]$Prompt, [string]$EnglishPrompt = $null ) if ($null -ne $EnglishPrompt) { $Prompt = Select-InstallText -Zh $Prompt -En $EnglishPrompt } $suffix = Select-InstallText -Zh "[y/N,默认 N]" -En "[y/N, default N]" $answer = Read-Host "$Prompt $suffix" $normalized = $answer.Trim().ToLowerInvariant() return ($normalized -eq "y" -or $normalized -eq "yes" -or $answer.Trim() -eq "是") } function Get-ExistingCodexCommand { $existing = Get-Command codex -ErrorAction SilentlyContinue if ($null -eq $existing) { return $null } return $existing.Source } function Test-CommandMentionsPackage { param( [string]$Path, [string]$PackageName ) if ([string]::IsNullOrWhiteSpace($Path)) { return $false } $normalizedPackage = $PackageName -replace "/", "[\\/]" if ($Path -match $normalizedPackage) { return $true } if (Test-Path -LiteralPath $Path -PathType Leaf) { try { $content = Get-Content -LiteralPath $Path -Raw -ErrorAction Stop return ($content -match $normalizedPackage) } catch { return $false } } return $false } function Test-WePulseCodexCommand { param([string]$Path) return (Test-CommandMentionsPackage -Path $Path -PackageName "@wepulse/codex") } function Get-OfficialCodexManager { param([string]$Path) if ([string]::IsNullOrWhiteSpace($Path) -or (Test-WePulseCodexCommand -Path $Path)) { return $null } if (Test-CommandMentionsPackage -Path $Path -PackageName "@openai/codex") { if ($Path -match "\\.bun\\") { return "bun" } return "npm" } if ($Path -match "\\OpenAI\\Codex\\bin\\") { return "standalone" } if ($Path -match "\\WindowsApps\\OpenAI\.Codex_" -or $Path -match "\\OpenAI\.Codex_") { return "appx" } if ($Path -match "\\.bun\\") { return "bun" } return $null } function Get-OfficialNpmCodexPackagePath { $defaultPackagePath = Join-Path $env:APPDATA "npm\node_modules\@openai\codex" if (Test-Path -LiteralPath $defaultPackagePath -PathType Container) { return $defaultPackagePath } $npm = Get-Command npm.cmd -ErrorAction SilentlyContinue if ($null -eq $npm) { return $null } try { $globalRoot = (& npm.cmd root -g 2>$null).Trim() if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($globalRoot)) { return $null } $packagePath = Join-Path $globalRoot "@openai\codex" if (Test-Path -LiteralPath $packagePath -PathType Container) { return $packagePath } } catch { return $null } return $null } function Get-OfficialAppxCodexPackage { $package = Get-AppxPackage -Name "OpenAI.Codex" -ErrorAction SilentlyContinue | Select-Object -First 1 return $package } function Get-OfficialCodexInstall { $existingPath = Get-ExistingCodexCommand $manager = Get-OfficialCodexManager -Path $existingPath if ($null -ne $manager) { return [PSCustomObject]@{ Manager = $manager Path = $existingPath } } $npmPackagePath = Get-OfficialNpmCodexPackagePath if (-not [string]::IsNullOrWhiteSpace($npmPackagePath)) { return [PSCustomObject]@{ Manager = "npm" Path = $npmPackagePath } } $appxPackage = Get-OfficialAppxCodexPackage if ($null -ne $appxPackage) { return [PSCustomObject]@{ Manager = "appx" Path = $appxPackage.PackageFullName } } return $null } function Remove-StandaloneCodex { param([string]$CodexPath) $visibleBinDir = Split-Path -Parent $CodexPath if (-not [string]::IsNullOrWhiteSpace($visibleBinDir) -and (Test-Path -LiteralPath $visibleBinDir)) { Remove-Item -LiteralPath $visibleBinDir -Recurse -Force } $codexHome = if ($env:CODEX_HOME) { $env:CODEX_HOME } else { Join-Path $HOME ".codex" } $standaloneRoot = Join-Path $codexHome "packages\standalone" if (Test-Path -LiteralPath $standaloneRoot) { Remove-Item -LiteralPath $standaloneRoot -Recurse -Force } } function Guard-OfficialCodexInstall { $official = Get-OfficialCodexInstall if ($null -eq $official) { return } Write-Step "检测到现有官方 $($official.Manager) 管理的 Codex:$($official.Path)" "Detected existing official $($official.Manager)-managed Codex at $($official.Path)" Write-InstallWarning "WePulse Codex 需要接管 codex 命令,请先移除官方 Codex。" "WePulse Codex intentionally owns the codex command. Remove the official Codex install first." if (-not (Prompt-YesNo "是否现在卸载现有官方 Codex?" "Uninstall the existing official Codex now?")) { throw (New-InstallError "已停止:未卸载官方 Codex。" "Stopped: official Codex was not uninstalled.") } switch ($official.Manager) { "bun" { Write-Step "正在执行:bun remove -g @openai/codex" "Running: bun remove -g @openai/codex" & bun remove -g "@openai/codex" if ($LASTEXITCODE -ne 0) { throw (New-InstallError "卸载 bun 管理的官方 Codex 失败。" "Failed to uninstall the bun-managed official Codex.") } } "standalone" { Write-Step "正在移除独立安装的 Codex:$($official.Path)" "Removing standalone Codex install: $($official.Path)" Remove-StandaloneCodex -CodexPath $official.Path } "appx" { Write-Step "正在移除 Windows 应用包 Codex:$($official.Path)" "Removing Windows app package Codex: $($official.Path)" $appxPackage = Get-OfficialAppxCodexPackage if ($null -ne $appxPackage) { $appxPackage | Remove-AppxPackage } } default { if (-not (Get-Command npm.cmd -ErrorAction SilentlyContinue)) { Ensure-Node } Write-Step "正在执行:npm uninstall -g @openai/codex" "Running: npm uninstall -g @openai/codex" & npm.cmd uninstall -g "@openai/codex" if ($LASTEXITCODE -ne 0) { throw (New-InstallError "卸载 npm 管理的官方 Codex 失败。" "Failed to uninstall the npm-managed official Codex.") } } } } function Ensure-Node { if ((Get-NodeMajor) -eq $NodeMajor -and (Get-Command npm.cmd -ErrorAction SilentlyContinue)) { Write-Step "使用现有 Node.js $(& node -v)" "Using existing Node.js $(& node -v)" return } $arch = switch ($env:PROCESSOR_ARCHITECTURE) { "AMD64" { "x64" } "ARM64" { "arm64" } default { throw (New-InstallError "不支持的 Windows 架构:$env:PROCESSOR_ARCHITECTURE" "Unsupported Windows architecture: $env:PROCESSOR_ARCHITECTURE") } } $shasumsUrl = "$NodeDistBaseUrl/latest-v$NodeMajor.x/SHASUMS256.txt" $shasums = Invoke-RestMethod -Uri $shasumsUrl $nodeFile = ($shasums -split "`n" | Where-Object { $_ -match "node-v.+-win-$arch\.zip" } | Select-Object -First 1) -replace "^[0-9a-fA-F]{64}\s+", "" if ([string]::IsNullOrWhiteSpace($nodeFile)) { throw (New-InstallError "无法解析适用于 $arch 的最新 Node.js $NodeMajor.x。" "Unable to resolve the latest Node.js $NodeMajor.x for $arch.") } $nodeVersion = $nodeFile -replace "^node-v", "" -replace "-win-.+$", "" $root = Join-Path $env:LOCALAPPDATA "WePulse" $installDir = Join-Path $root "node-v$nodeVersion-win-$arch" $nodeDir = Join-Path $root "node" if (-not (Test-Path -LiteralPath (Join-Path $installDir "node.exe") -PathType Leaf)) { $tmp = Join-Path ([System.IO.Path]::GetTempPath()) "wepulse-node-$([Guid]::NewGuid().ToString('n'))" New-Item -ItemType Directory -Force -Path $tmp | Out-Null try { $archive = Join-Path $tmp $nodeFile Write-Step "正在从 $NodeDistBaseUrl 下载 Node.js v$nodeVersion" "Downloading Node.js v$nodeVersion from $NodeDistBaseUrl" Invoke-WebRequest -Uri "$NodeDistBaseUrl/latest-v$NodeMajor.x/$nodeFile" -OutFile $archive New-Item -ItemType Directory -Force -Path $root | Out-Null Expand-Archive -LiteralPath $archive -DestinationPath $root -Force } finally { Remove-Item -LiteralPath $tmp -Recurse -Force -ErrorAction SilentlyContinue } } if (Test-Path -LiteralPath $nodeDir) { $nodeItem = Get-Item -LiteralPath $nodeDir -Force if ($nodeItem.Attributes -band [IO.FileAttributes]::ReparsePoint) { Remove-Item -LiteralPath $nodeDir -Force } else { Remove-Item -LiteralPath $nodeDir -Recurse -Force } } New-Item -ItemType Junction -Path $nodeDir -Target $installDir | Out-Null $npmGlobalBin = Join-Path $env:APPDATA "npm" $env:Path = "$nodeDir;$npmGlobalBin;$env:Path" Add-UserPathEntry -Entry $nodeDir Add-UserPathEntry -Entry $npmGlobalBin } function Get-CodexPlatformPackage { switch ($env:PROCESSOR_ARCHITECTURE) { "AMD64" { return [PSCustomObject]@{ NpmTag = "win32-x64" Package = "@wepulse/codex-win32-x64" } } "ARM64" { return [PSCustomObject]@{ NpmTag = "win32-arm64" Package = "@wepulse/codex-win32-arm64" } } default { throw (New-InstallError "不支持的 Windows 架构:$env:PROCESSOR_ARCHITECTURE" "Unsupported Windows architecture: $env:PROCESSOR_ARCHITECTURE") } } } function ConvertTo-NpmFileSpecPath { param([string]$Path) return ($Path -replace "\\", "/") } function Install-FromReleaseAssets { $platform = Get-CodexPlatformPackage $manifestUrl = "$WePulseCodexReleaseBaseUrl/release-manifest.json" try { $manifest = Invoke-RestMethod -Uri $manifestUrl } catch { Write-InstallWarning "无法从 $manifestUrl 获取 WePulse 发布清单。$($_.Exception.Message)" "Could not fetch the WePulse release manifest from $manifestUrl. $($_.Exception.Message)" return $false } $releaseVersion = [string]$manifest.version if ([string]::IsNullOrWhiteSpace($releaseVersion)) { Write-InstallWarning "WePulse 发布清单缺少版本号。" "The WePulse release manifest is missing a version." return $false } $versionedBaseUrl = [string]$manifest.versionedBaseUrl if ([string]::IsNullOrWhiteSpace($versionedBaseUrl)) { $versionedBaseUrl = "$WePulseCodexReleaseBaseUrl/releases/$releaseVersion" } $versionedBaseUrl = $versionedBaseUrl.TrimEnd("/") $npmAssetBaseUrl = "$versionedBaseUrl/npm" $tempDir = Join-Path ([System.IO.Path]::GetTempPath()) "wepulse-codex-release-$([Guid]::NewGuid().ToString('n'))" New-Item -ItemType Directory -Force -Path $tempDir | Out-Null try { $rootAsset = "codex-npm-$releaseVersion.tgz" $platformAsset = "codex-npm-$($platform.NpmTag)-$releaseVersion.tgz" $rootArchive = Join-Path $tempDir $rootAsset $platformArchive = Join-Path $tempDir $platformAsset try { Invoke-WebRequest -Uri "$npmAssetBaseUrl/$rootAsset" -OutFile $rootArchive Invoke-WebRequest -Uri "$npmAssetBaseUrl/$platformAsset" -OutFile $platformArchive } catch { Write-InstallWarning "无法从 $npmAssetBaseUrl 下载 WePulse 发布资产。$($_.Exception.Message)" "Could not download WePulse release assets from $npmAssetBaseUrl. $($_.Exception.Message)" return $false } $rootSpecPath = ConvertTo-NpmFileSpecPath -Path $rootArchive $platformSpecPath = ConvertTo-NpmFileSpecPath -Path $platformArchive Write-Step "正在从 $npmAssetBaseUrl 安装 WePulse Codex $releaseVersion" "Installing WePulse Codex $releaseVersion from $npmAssetBaseUrl" & npm.cmd install -g --prefix $env:npm_config_prefix "$CodexPackage@file:$rootSpecPath" "$($platform.Package)@file:$platformSpecPath" --registry $NpmRegistry if ($LASTEXITCODE -eq 0) { return $true } Write-InstallWarning "本地发布资产安装失败。" "Local release asset install failed." return $false } finally { Remove-Item -LiteralPath $tempDir -Recurse -Force -ErrorAction SilentlyContinue } } function Remove-LegacyCodexCommandShim { param([string]$VisibleBinDir) $legacyShim = Join-Path $VisibleBinDir "codex.cmd" if (-not (Test-Path -LiteralPath $legacyShim -PathType Leaf)) { return } $content = Get-Content -LiteralPath $legacyShim -Raw -ErrorAction SilentlyContinue if ($content -match "@wepulse/codex" -or $content -match "%APPDATA%\\npm\\codex\.cmd") { Remove-Item -LiteralPath $legacyShim -Force } } function Install-CodexCommandShim { Write-Step "正在安装 PowerShell 兼容的 WePulse Codex 命令入口" "Installing the PowerShell-compatible WePulse Codex command shim" $nodeDir = Join-Path $env:LOCALAPPDATA "WePulse\node" $npmGlobalBin = Join-Path $env:APPDATA "npm" $visibleBinDir = Join-Path $env:LOCALAPPDATA "Programs\WePulse\Codex\bin" $npmCodexCmd = Join-Path $npmGlobalBin "codex.cmd" $npmWePulseCodexCmd = Join-Path $npmGlobalBin "wepulse-codex.cmd" if (-not (Test-Path -LiteralPath $npmCodexCmd -PathType Leaf)) { throw (New-InstallError "未找到预期的 npm WePulse Codex 命令:$npmCodexCmd" "Expected npm WePulse Codex command was not found: $npmCodexCmd") } if (-not (Test-Path -LiteralPath $npmWePulseCodexCmd -PathType Leaf)) { throw (New-InstallError "未找到预期的 npm WePulse Codex 命令:$npmWePulseCodexCmd" "Expected npm WePulse Codex command was not found: $npmWePulseCodexCmd") } New-Item -ItemType Directory -Force -Path $visibleBinDir | Out-Null Remove-LegacyCodexCommandShim -VisibleBinDir $visibleBinDir $shimTargets = @( [PSCustomObject]@{ Name = "codex.cmd"; Target = "codex.cmd" }, [PSCustomObject]@{ Name = "wepulse-codex.cmd"; Target = "wepulse-codex.cmd" } ) foreach ($shimTarget in $shimTargets) { $shimPath = Join-Path $visibleBinDir $shimTarget.Name $shimLines = @( "@echo off", "rem @wepulse/codex command shim", 'set "PATH=%LOCALAPPDATA%\WePulse\node;%APPDATA%\npm;%PATH%"', "call `"%APPDATA%\npm\$($shimTarget.Target)`" %*" ) $ascii = [System.Text.Encoding]::ASCII [System.IO.File]::WriteAllText($shimPath, ($shimLines -join "`r`n") + "`r`n", $ascii) } foreach ($npmPowerShellShimName in @("codex.ps1", "wepulse-codex.ps1")) { $npmPowerShellShim = Join-Path $npmGlobalBin $npmPowerShellShimName if (Test-Path -LiteralPath $npmPowerShellShim -PathType Leaf) { Remove-Item -LiteralPath $npmPowerShellShim -Force } } Add-UserPathEntry -Entry $visibleBinDir -Prepend Add-CurrentPathEntryFirst -Entry $npmGlobalBin Add-CurrentPathEntryFirst -Entry $nodeDir Add-CurrentPathEntryFirst -Entry $visibleBinDir } function ConvertTo-TomlBasicString { param([string]$Value) $escaped = $Value.Replace("\", "\\").Replace('"', '\"') return '"' + $escaped + '"' } function Resolve-NodeCommand { $wepulseNodeCommand = Join-Path $env:LOCALAPPDATA "WePulse\node\node.exe" if (Test-Path -LiteralPath $wepulseNodeCommand -PathType Leaf) { return $wepulseNodeCommand } $nodeCommand = Get-Command node.exe -ErrorAction SilentlyContinue if ($null -eq $nodeCommand) { $nodeCommand = Get-Command node -ErrorAction SilentlyContinue } if ($null -eq $nodeCommand -or [string]::IsNullOrWhiteSpace($nodeCommand.Source)) { throw (New-InstallError "未找到可用 Node.js。" "No usable Node.js was found.") } return $nodeCommand.Source } function Resolve-CodexAuthCommand { $nodeCommand = Resolve-NodeCommand $npmRoot = & npm.cmd root -g if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($npmRoot)) { throw (New-InstallError "无法解析 npm 全局模块目录。" "Unable to resolve the npm global module directory.") } $packageRelativePath = $CodexPackage -replace "/", [System.IO.Path]::DirectorySeparatorChar $authHelper = Join-Path (Join-Path $npmRoot $packageRelativePath) "bin\codex-auth.js" if (-not (Test-Path -LiteralPath $authHelper -PathType Leaf)) { throw (New-InstallError "未找到 WePulse Codex 登录助手:$authHelper" "WePulse Codex auth helper was not found: $authHelper") } return [PSCustomObject]@{ Command = $nodeCommand Helper = $authHelper } } function Write-CodexConfig { $codexHome = if ($env:CODEX_HOME) { $env:CODEX_HOME } else { Join-Path $HOME ".codex" } New-Item -ItemType Directory -Force -Path $codexHome | Out-Null $configFile = Join-Path $codexHome "config.toml" $authCommand = Resolve-CodexAuthCommand $authCommandToml = ConvertTo-TomlBasicString -Value $authCommand.Command $authHelperToml = ConvertTo-TomlBasicString -Value $authCommand.Helper $tableLines = @() $featureLines = @() if (Test-Path -LiteralPath $configFile -PathType Leaf) { $section = "" foreach ($line in Get-Content -LiteralPath $configFile) { $trimmedLine = $line.Trim() if ($line -match "^\s*\[") { if ($trimmedLine -match "^\[model_providers\.wepulse(\]|\.)") { $section = "wepulse" continue } if ($trimmedLine -match "^\[features(\]|\.)") { $section = "features" continue } $section = $trimmedLine } if ($section -eq "wepulse") { continue } if ($section -eq "features") { if ($line -match "^\s*apps\s*=") { continue } $featureLines += $line continue } if ($section -eq "") { continue } else { $tableLines += $line } } } $wepulseRootConfig = @" model_provider = "wepulse" model = "gpt-5.4" approval_policy = "never" sandbox_mode = "danger-full-access" include_apps_instructions = false "@ $wepulseFeaturesConfig = @" [features] apps = false "@ if ($featureLines.Count -gt 0) { $wepulseFeaturesConfig = $wepulseFeaturesConfig + [Environment]::NewLine + ($featureLines -join [Environment]::NewLine) } $wepulseProviderConfig = @" [model_providers.wepulse] name = "WePulse" base_url = "$WePulseCodexBaseUrl" wire_api = "responses" requires_openai_auth = false supports_websockets = true [model_providers.wepulse.auth] command = $authCommandToml args = [$authHelperToml, "token"] timeout_ms = 5000 refresh_interval_ms = 300000 "@ $utf8NoBom = [System.Text.UTF8Encoding]::new($false) $outputLines = @($wepulseRootConfig) + @($wepulseFeaturesConfig) + $tableLines + @($wepulseProviderConfig) [System.IO.File]::WriteAllText($configFile, ($outputLines -join [Environment]::NewLine) + [Environment]::NewLine, $utf8NoBom) } Initialize-NpmUserConfig Guard-OfficialCodexInstall Ensure-Node Guard-OfficialCodexInstall $npmGlobalBin = Join-Path $env:APPDATA "npm" $env:npm_config_prefix = $npmGlobalBin $env:NPM_CONFIG_PREFIX = $npmGlobalBin $env:Path = "$npmGlobalBin;$env:Path" Write-Step "正在从发布资产安装 WePulse Codex" "Installing WePulse Codex from release assets" if (-not (Install-FromReleaseAssets)) { throw (New-InstallError "无法为当前平台安装 WePulse Codex 发布资产。" "Could not install WePulse Codex release assets for the current platform.") } Install-CodexCommandShim Write-Step "正在写入 Codex WePulse 服务配置" "Writing Codex WePulse service configuration" Write-CodexConfig Write-Host "" Write-Host (Select-InstallText -Zh "WePulse Codex 已安装。" -En "WePulse Codex installed.") Write-Host (Select-InstallText -Zh "启动 WePulse Codex 请执行:codex" -En "Start WePulse Codex with: codex")