param([switch]$DetectOnly, [Alias('AssembleOnly')][switch]$DownloadOnly, [switch]$NoLaunch, [switch]$NoBrowser, [switch]$SkipFollowup, [string]$ManifestPath, [string]$ArchiveDirectory) $ErrorActionPreference = 'Stop' [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 $baseUrl = 'https://codex.xt6889.top' function Get-CodexInstallation { $ErrorActionPreference = 'Stop' $architecture = $env:PROCESSOR_ARCHITEW6432 if (-not $architecture) { $architecture = $env:PROCESSOR_ARCHITECTURE } $architecture = switch ($architecture.ToUpperInvariant()) { 'AMD64' { 'x64' } 'ARM64' { 'arm64' } default { 'unsupported' } } $candidates = [System.Collections.Generic.List[string]]::new() foreach ($base in @($env:LOCALAPPDATA, $env:ProgramFiles, ${env:ProgramFiles(x86)})) { if (-not $base) { continue } foreach ($relative in @('Programs\Codex\Codex.exe', 'Codex\Codex.exe')) { $path = Join-Path $base $relative if (Test-Path -LiteralPath $path -PathType Leaf) { $candidates.Add($path) } } } foreach ($root in @( 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall', 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall', 'HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall' )) { foreach ($key in @(Get-ChildItem -LiteralPath $root -ErrorAction SilentlyContinue)) { $entry = Get-ItemProperty -LiteralPath $key.PSPath -ErrorAction SilentlyContinue if ($entry.DisplayName -notmatch '^Codex(?:\s|$)') { continue } if ($entry.InstallLocation) { $path = Join-Path $entry.InstallLocation 'Codex.exe' if (Test-Path -LiteralPath $path -PathType Leaf) { $candidates.Add($path) } } } } $appx = @() if (Get-Command Get-AppxPackage -ErrorAction SilentlyContinue) { $appx = @(Get-AppxPackage -ErrorAction SilentlyContinue | Where-Object { $_.Name -match '(^|\.)Codex($|\.)' -and $_.Status -eq 'Ok' }) } $paths = @($candidates | Sort-Object -Unique) $installed = $paths.Count -gt 0 -or $appx.Count -gt 0 [ordered]@{ schema_version = 1 stage = 'detect_installation' os = 'windows' architecture = $architecture supported_architecture = ($architecture -eq 'x64') installation_state = $(if ($installed) { 'detected' } else { 'not_detected' }) executable_paths = $paths app_packages = @($appx | ForEach-Object { $_.Name }) email_login_state = 'not_checked' codex_authorization_state = 'not_checked' } | ConvertTo-Json -Depth 4 } function Get-Sha256([string]$Path) { $stream = [IO.File]::OpenRead($Path) $sha = [Security.Cryptography.SHA256]::Create() try { return ([BitConverter]::ToString($sha.ComputeHash($stream))).Replace('-', '').ToLowerInvariant() } finally { $sha.Dispose(); $stream.Dispose() } } function Test-VerifiedFile([string]$Path, [long]$Size, [string]$Sha256) { return ((Test-Path -LiteralPath $Path -PathType Leaf) -and (Get-Item -LiteralPath $Path).Length -eq $Size -and (Get-Sha256 $Path) -eq $Sha256) } function Assert-DirectManifest($Artifact) { if ($Artifact.delivery -ne 'server_direct' -or $Artifact.filename -ne 'codex-win.exe' -or $Artifact.download_url -ne ($baseUrl + '/downloads/codex-win.exe') -or $Artifact.sha256 -notmatch '^[a-f0-9]{64}$' -or [long]$Artifact.size -le 0) { throw '安装清单不正确,请联系服务提供方。' } } function Get-DirectInstaller($Artifact, [string]$Destination) { $temporary = $Destination + '.downloading' if (Test-Path -LiteralPath $temporary) { $length = (Get-Item -LiteralPath $temporary).Length if ($length -gt [long]$Artifact.size -or ($length -eq [long]$Artifact.size -and -not (Test-VerifiedFile $temporary $Artifact.size $Artifact.sha256))) { Remove-Item -LiteralPath $temporary } } for ($attempt = 1; $attempt -le 3; $attempt++) { if (Test-VerifiedFile $temporary $Artifact.size $Artifact.sha256) { break } [long]$offset = 0 if (Test-Path -LiteralPath $temporary) { $offset = (Get-Item -LiteralPath $temporary).Length } $response = $null; $inputStream = $null; $outputStream = $null try { $request = [Net.HttpWebRequest]::Create($Artifact.download_url) $request.AllowAutoRedirect = ([Uri]$Artifact.download_url).Host -eq 'github.com' $request.Timeout = 30000 $request.ReadWriteTimeout = 60000 if ($offset -gt 0) { $request.AddRange($offset) } $response = $request.GetResponse() if ([int]$response.StatusCode -eq 200) { $offset = 0 } elseif ([int]$response.StatusCode -eq 206 -and $offset -gt 0) { $expectedRange = 'bytes ' + $offset + '-' + ([long]$Artifact.size - 1) + '/' + $Artifact.size if ($response.Headers['Content-Range'] -ne $expectedRange) { throw '服务器返回了不正确的续传范围。' } } else { throw '服务器返回了不正确的下载响应。' } if ($response.ContentLength -ne ([long]$Artifact.size - $offset)) { throw '服务器安装包大小与清单不符。' } $inputStream = $response.GetResponseStream() $outputStream = [IO.File]::Open($temporary, [IO.FileMode]::OpenOrCreate, [IO.FileAccess]::Write, [IO.FileShare]::None) $outputStream.SetLength($offset) $outputStream.Position = $offset $buffer = New-Object byte[] 1048576 [long]$received = $offset $lastNotice = -10 while (($read = $inputStream.Read($buffer, 0, $buffer.Length)) -gt 0) { if ($received + $read -gt [long]$Artifact.size) { throw '下载内容超过预期大小。' } $outputStream.Write($buffer, 0, $read) $received += $read $percent = [int]([double]$received * 100 / [long]$Artifact.size) if ($percent -ge $lastNotice + 10) { Write-Host ('下载进度:{0}%({1:N0} / {2:N0} MB)' -f $percent, ($received / 1000000), ($Artifact.size / 1000000)) $lastNotice = $percent } } if ($received -ne [long]$Artifact.size) { throw '下载中断,文件尚未完整。' } } catch { if ($attempt -eq 3) { throw } Write-Host '下载中断,稍后自动续传;也可以重新运行命令继续。' Start-Sleep -Seconds 3 } finally { if ($outputStream) { $outputStream.Dispose() } if ($inputStream) { $inputStream.Dispose() } if ($response) { $response.Dispose() } } } Write-Host '正在校验安装包…' if (-not (Test-VerifiedFile $temporary $Artifact.size $Artifact.sha256)) { if (Test-Path -LiteralPath $temporary) { Remove-Item -LiteralPath $temporary } throw '安装包校验失败,已停止。请重新运行命令下载。' } if (Test-Path -LiteralPath $Destination) { Move-Item -LiteralPath $Destination -Destination ($Destination + '.previous-' + [Guid]::NewGuid().ToString('N')) } Move-Item -LiteralPath $temporary -Destination $Destination } function Open-Codex($Detection) { if ($NoLaunch) { return } if (@($Detection.executable_paths).Count -gt 0) { Start-Process -FilePath $Detection.executable_paths[0] return } if (@($Detection.app_packages).Count -gt 0) { $apps = @(Get-StartApps -ErrorAction SilentlyContinue | Where-Object { $_.Name -eq 'Codex' -or $_.AppID -like '*OpenAI.Codex*' }) if ($apps.Count -gt 0) { Start-Process explorer.exe -ArgumentList ('shell:AppsFolder\' + $apps[0].AppID) return } } Write-Host 'Open Codex from the Windows Start menu.' } $script:PhonePageConfig = ('{"schema_version": 1, "observed_title": "\u624b\u673a\u53f7\u662f\u5fc5\u586b\u9879", "title_source": "owner_description_not_live_observed", "recognition": "foreground_browser_visible_accessibility", "trusted_hosts": ["auth.openai.com", "chatgpt.com"], "poll_seconds": 5, "reader_timeout_seconds": 5, "requires_title_phone_field_sms_action": true, "candidate_is_not_email_login_proof": true, "automatic_purchase_from_page_match": false}' | ConvertFrom-Json) $script:PhoneReaderBase64 = '77u/IyBSZWFkIG9ubHkgdGhlIGZvcmVncm91bmQgYnJvd3NlcidzIHZpc2libGUgYWNjZXNzaWJpbGl0eSBsYWJlbHMuIE5ldmVyIGlucHV0IHZhbHVlcy4KJEVycm9yQWN0aW9uUHJlZmVyZW5jZSA9ICdTdG9wJwpbQ29uc29sZV06Ok91dHB1dEVuY29kaW5nID0gTmV3LU9iamVjdCBUZXh0LlVURjhFbmNvZGluZygkZmFsc2UpCiRyZXN1bHQgPSBbb3JkZXJlZF1Ae3JlYWRlcj0nd2luZG93c191aWEnOyBzdGF0dXM9J3VuYXZhaWxhYmxlJzsgdGl0bGVfbWF0Y2g9JGZhbHNlOyBwaG9uZV9maWVsZD0kZmFsc2U7IHNtc19hY3Rpb249JGZhbHNlOyBvcmlnaW49Jyd9CnRyeSB7CiAgICBBZGQtVHlwZSAtQXNzZW1ibHlOYW1lIFVJQXV0b21hdGlvbkNsaWVudAogICAgQWRkLVR5cGUgLUFzc2VtYmx5TmFtZSBVSUF1dG9tYXRpb25UeXBlcwogICAgQWRkLVR5cGUgLVR5cGVEZWZpbml0aW9uIEAnCnVzaW5nIFN5c3RlbTsKdXNpbmcgU3lzdGVtLlJ1bnRpbWUuSW50ZXJvcFNlcnZpY2VzOwpwdWJsaWMgc3RhdGljIGNsYXNzIENvZGV4Rm9yZWdyb3VuZEJyb3dzZXIgewogICAgW0RsbEltcG9ydCgidXNlcjMyLmRsbCIpXSBwdWJsaWMgc3RhdGljIGV4dGVybiBJbnRQdHIgR2V0Rm9yZWdyb3VuZFdpbmRvdygpOwogICAgW0RsbEltcG9ydCgidXNlcjMyLmRsbCIpXSBwdWJsaWMgc3RhdGljIGV4dGVybiB1aW50IEdldFdpbmRvd1RocmVhZFByb2Nlc3NJZChJbnRQdHIgaFduZCwgb3V0IHVpbnQgcHJvY2Vzc0lkKTsKfQonQAogICAgJGhhbmRsZSA9IFtDb2RleEZvcmVncm91bmRCcm93c2VyXTo6R2V0Rm9yZWdyb3VuZFdpbmRvdygpCiAgICBbdWludDMyXSRwcm9jZXNzSWQgPSAwCiAgICBbdm9pZF1bQ29kZXhGb3JlZ3JvdW5kQnJvd3Nlcl06OkdldFdpbmRvd1RocmVhZFByb2Nlc3NJZCgkaGFuZGxlLCBbcmVmXSRwcm9jZXNzSWQpCiAgICAkbmFtZSA9IChHZXQtUHJvY2VzcyAtSWQgJHByb2Nlc3NJZCkuUHJvY2Vzc05hbWUKICAgIGlmICgkbmFtZSAtbm90aW4gQCgnY2hyb21lJywnbXNlZGdlJywnZmlyZWZveCcsJ2JyYXZlJywndml2YWxkaScpKSB7CiAgICAgICAgJHJlc3VsdC5zdGF0dXM9J25vX2ZvcmVncm91bmRfYnJvd3NlcicKICAgIH0gZWxzZSB7CiAgICAgICAgJHdpbmRvdyA9IFtXaW5kb3dzLkF1dG9tYXRpb24uQXV0b21hdGlvbkVsZW1lbnRdOjpGcm9tSGFuZGxlKCRoYW5kbGUpCiAgICAgICAgJHdhbGtlciA9IFtXaW5kb3dzLkF1dG9tYXRpb24uVHJlZVdhbGtlcl06OkNvbnRyb2xWaWV3V2Fsa2VyCiAgICAgICAgJHF1ZXVlID0gW0NvbGxlY3Rpb25zLkdlbmVyaWMuUXVldWVbb2JqZWN0XV06Om5ldygpCiAgICAgICAgJHF1ZXVlLkVucXVldWUoQCgkd2luZG93LDAsJGZhbHNlKSkKICAgICAgICAkY291bnQgPSAwOyAkZG9jU2VlbiA9ICRmYWxzZTsgJHRydW5jYXRlZCA9ICRmYWxzZQogICAgICAgICR3YXRjaCA9IFtEaWFnbm9zdGljcy5TdG9wd2F0Y2hdOjpTdGFydE5ldygpCiAgICAgICAgJG9yaWdpbnMgPSBbQ29sbGVjdGlvbnMuR2VuZXJpYy5IYXNoU2V0W3N0cmluZ11dOjpuZXcoKQogICAgICAgIHdoaWxlICgkcXVldWUuQ291bnQgLWd0IDAgLWFuZCAkY291bnQgLWx0IDkwMCAtYW5kICR3YXRjaC5FbGFwc2VkTWlsbGlzZWNvbmRzIC1sdCAzMjAwKSB7CiAgICAgICAgICAgICRlbnRyeSA9ICRxdWV1ZS5EZXF1ZXVlKCk7ICRub2RlID0gJGVudHJ5WzBdOyAkZGVwdGggPSBbaW50XSRlbnRyeVsxXTsgJGluRG9jID0gW2Jvb2xdJGVudHJ5WzJdCiAgICAgICAgICAgICRjb3VudCsrCiAgICAgICAgICAgIHRyeSB7CiAgICAgICAgICAgICAgICAkY3VycmVudCA9ICRub2RlLkN1cnJlbnQKICAgICAgICAgICAgICAgIGlmICgkY3VycmVudC5Jc09mZnNjcmVlbiAtb3IgJGN1cnJlbnQuSXNQYXNzd29yZCkgeyBjb250aW51ZSB9CiAgICAgICAgICAgICAgICAkdHlwZSA9ICRjdXJyZW50LkNvbnRyb2xUeXBlLlByb2dyYW1tYXRpY05hbWUKICAgICAgICAgICAgICAgIGlmICgkdHlwZSAtZXEgJ0NvbnRyb2xUeXBlLkRvY3VtZW50JykgeyAkaW5Eb2MgPSAkdHJ1ZTsgJGRvY1NlZW4gPSAkdHJ1ZSB9CiAgICAgICAgICAgICAgICAkbGFiZWwgPSAkY3VycmVudC5OYW1lCiAgICAgICAgICAgICAgICBpZiAoJGluRG9jIC1hbmQgJHR5cGUgLWluIEAoJ0NvbnRyb2xUeXBlLlRleHQnLCdDb250cm9sVHlwZS5Eb2N1bWVudCcsJ0NvbnRyb2xUeXBlLkhlYWRlcicpKSB7CiAgICAgICAgICAgICAgICAgICAgJGNvbXBhY3QgPSBbcmVnZXhdOjpSZXBsYWNlKCRsYWJlbCwgJ1xzJywgJycpCiAgICAgICAgICAgICAgICAgICAgaWYgKCRjb21wYWN0IC1tYXRjaCAn5omL5py65Y+3KD8656CBKT/mmK/lv4XloavpobknKSB7ICRyZXN1bHQudGl0bGVfbWF0Y2ggPSAkdHJ1ZSB9CiAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICBpZiAoJGluRG9jIC1hbmQgJHR5cGUgLWVxICdDb250cm9sVHlwZS5FZGl0JyAtYW5kICRjdXJyZW50LklzRW5hYmxlZCkgewogICAgICAgICAgICAgICAgICAgIGlmICgoJGxhYmVsICsgJyAnICsgJGN1cnJlbnQuSGVscFRleHQpIC1tYXRjaCAnKD9pKSjmiYvmnLrlj7d85omL5py65Y+356CBfOeUteivneWPt+eggXxwaG9uZVxzKihudW1iZXIpP3xtb2JpbGUpJykgeyAkcmVzdWx0LnBob25lX2ZpZWxkID0gJHRydWUgfQogICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgaWYgKCRpbkRvYyAtYW5kICR0eXBlIC1lcSAnQ29udHJvbFR5cGUuQnV0dG9uJyAtYW5kICRjdXJyZW50LklzRW5hYmxlZCkgewogICAgICAgICAgICAgICAgICAgIGlmICgkbGFiZWwgLW1hdGNoICcoP2kpKOWPkemAgS4qKD866aqM6K+B56CBfOefreS/oSl86I635Y+WLirpqozor4HnoIF8KD86c2VuZHxnZXR8cmVxdWVzdCkuKig/OnNtc3xjb2RlKXznu6fnu618Y29udGludWV8bmV4dHzkuIvkuIDmraUpJykgeyAkcmVzdWx0LnNtc19hY3Rpb24gPSAkdHJ1ZSB9CiAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICAjIFJlYWQgb25seSB0aGUgYnJvd3NlciBjaHJvbWUgYWRkcmVzcyBmaWVsZCwgbmV2ZXIgYSBwYWdlIGlucHV0IHZhbHVlLgogICAgICAgICAgICAgICAgaWYgKC1ub3QgJGluRG9jIC1hbmQgJHR5cGUgLWVxICdDb250cm9sVHlwZS5FZGl0JyAtYW5kICRsYWJlbCAtbWF0Y2ggJyg/aSkoYWRkcmVzc3zlnLDlnYB85pCc57Si5oiW6L6T5YWl572R5Z2AfHNlYXJjaCBvciBlbnRlciknKSB7CiAgICAgICAgICAgICAgICAgICAgJHBhdHRlcm4gPSAkbnVsbAogICAgICAgICAgICAgICAgICAgIGlmICgkbm9kZS5UcnlHZXRDdXJyZW50UGF0dGVybihbV2luZG93cy5BdXRvbWF0aW9uLlZhbHVlUGF0dGVybl06OlBhdHRlcm4sIFtyZWZdJHBhdHRlcm4pKSB7CiAgICAgICAgICAgICAgICAgICAgICAgICRhZGRyZXNzID0gJHBhdHRlcm4uQ3VycmVudC5WYWx1ZQogICAgICAgICAgICAgICAgICAgICAgICAkdXJpID0gJG51bGwKICAgICAgICAgICAgICAgICAgICAgICAgaWYgKFtVcmldOjpUcnlDcmVhdGUoJGFkZHJlc3MsIFtVcmlLaW5kXTo6QWJzb2x1dGUsIFtyZWZdJHVyaSkgLWFuZCAkdXJpLlNjaGVtZSAtaW4gQCgnaHR0cCcsJ2h0dHBzJykpIHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgIFt2b2lkXSRvcmlnaW5zLkFkZCgkdXJpLkdldExlZnRQYXJ0KFtVcmlQYXJ0aWFsXTo6QXV0aG9yaXR5KSkKICAgICAgICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgICAgICAgICAkYWRkcmVzcyA9ICRudWxsCiAgICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgaWYgKCRkZXB0aCAtZ2UgMjgpIHsgJHRydW5jYXRlZCA9ICR0cnVlOyBjb250aW51ZSB9CiAgICAgICAgICAgICAgICAkY2hpbGQgPSAkd2Fsa2VyLkdldEZpcnN0Q2hpbGQoJG5vZGUpCiAgICAgICAgICAgICAgICB3aGlsZSAoJG51bGwgLW5lICRjaGlsZCAtYW5kICRxdWV1ZS5Db3VudCAtbHQgOTAwIC1hbmQgJHdhdGNoLkVsYXBzZWRNaWxsaXNlY29uZHMgLWx0IDMyMDApIHsKICAgICAgICAgICAgICAgICAgICAkcXVldWUuRW5xdWV1ZShAKCRjaGlsZCwoJGRlcHRoKzEpLCRpbkRvYykpCiAgICAgICAgICAgICAgICAgICAgJGNoaWxkID0gJHdhbGtlci5HZXROZXh0U2libGluZygkY2hpbGQpCiAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICBpZiAoJG51bGwgLW5lICRjaGlsZCkgeyAkdHJ1bmNhdGVkID0gJHRydWUgfQogICAgICAgICAgICB9IGNhdGNoIHsgJHRydW5jYXRlZCA9ICR0cnVlIH0KICAgICAgICB9CiAgICAgICAgaWYgKCRxdWV1ZS5Db3VudCAtZ3QgMCkgeyAkdHJ1bmNhdGVkID0gJHRydWUgfQogICAgICAgIGlmICgkb3JpZ2lucy5Db3VudCAtZXEgMSkgeyAkcmVzdWx0Lm9yaWdpbiA9IEAoJG9yaWdpbnMpWzBdIH0KICAgICAgICAkcmVzdWx0LnN0YXR1cyA9IGlmICgkdHJ1bmNhdGVkKSB7J3BhcnRpYWwnfSBlbHNlaWYgKCRkb2NTZWVuKSB7J29ic2VydmVkJ30gZWxzZSB7J2FjY2Vzc2liaWxpdHlfdW5hdmFpbGFibGUnfQogICAgfQp9IGNhdGNoIHsgJHJlc3VsdC5zdGF0dXM9J3VuYXZhaWxhYmxlJyB9CiMgT25seSBib29sZWFucyBhbmQgb3JpZ2luIChubyBwYXRoL3F1ZXJ5LCBwYWdlIHRleHQsIGNyZWRlbnRpYWxzIG9yIHNjcmVlbnNob3QpLgokcmVzdWx0IHwgQ29udmVydFRvLUpzb24gLUNvbXByZXNzCg==' function Get-PhonePageDecision($Observation) { if (-not $Observation -or $Observation.status -notin @('observed','partial')) { return 'not_observable' } if ($Observation.title_match -isnot [bool] -or $Observation.phone_field -isnot [bool] -or $Observation.sms_action -isnot [bool]) { return 'not_observable' } if (-not $Observation.title_match) { return 'no_match' } if (-not $Observation.phone_field -or -not $Observation.sms_action) { return 'candidate_incomplete' } $uri = $null if (-not [Uri]::TryCreate([string]$Observation.origin, [UriKind]::Absolute, [ref]$uri)) { return 'candidate_origin_unverified' } if ($uri.Scheme -ne 'https' -or $uri.Port -ne 443 -or $uri.UserInfo -or $uri.AbsolutePath -ne '/' -or $uri.Query -or $uri.Fragment -or $uri.Host -notin @($script:PhonePageConfig.trusted_hosts)) { return 'candidate_origin_unverified' } return 'phone_step_detected' } function Read-PhonePage { $process = $null try { $bytes = [Convert]::FromBase64String($script:PhoneReaderBase64) $sha = [Security.Cryptography.SHA256]::Create() try { $digest = ([BitConverter]::ToString($sha.ComputeHash($bytes))).Replace('-','').ToLowerInvariant() } finally { $sha.Dispose() } $folder = Join-Path $env:LOCALAPPDATA 'CodexSetup\Readers' [IO.Directory]::CreateDirectory($folder) | Out-Null $path = Join-Path $folder ($digest + '.ps1') if (-not (Test-Path -LiteralPath $path) -or (Get-Sha256 $path) -ne $digest) { [IO.File]::WriteAllBytes($path, $bytes) } $info = New-Object Diagnostics.ProcessStartInfo $info.FileName = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe' $info.Arguments = '-NoProfile -NonInteractive -STA -File "' + $path + '"' $info.UseShellExecute = $false; $info.CreateNoWindow = $true $info.RedirectStandardOutput = $true; $info.RedirectStandardError = $true $info.StandardOutputEncoding = [Text.Encoding]::UTF8 $process = New-Object Diagnostics.Process $process.StartInfo = $info [void]$process.Start() $stdout = $process.StandardOutput.ReadToEndAsync(); $stderr = $process.StandardError.ReadToEndAsync() if (-not $process.WaitForExit(5000)) { $process.Kill(); return 'not_observable' } if ($process.ExitCode -ne 0 -or $stdout.Result.Length -gt 4096) { return 'not_observable' } return Get-PhonePageDecision ($stdout.Result | ConvertFrom-Json) } catch { return 'not_observable' } finally { if ($process) { $process.Dispose() } } } function Update-PhonePageStep($State) { if ($State.phone_observation_enabled -ne 'yes') { return } $decision = Read-PhonePage # A terminal foreground or inaccessible browser cannot erase a previous detection. if ($decision -in @('not_observable','no_match')) { return } $State.phone_page_state = $decision Save-SetupState $State if ($State.phone_tier -ne 'not_selected') { return } if ($decision -ne 'phone_step_detected') { if ($State.phone_notice_shown -eq 'no') { Write-Host '看到了手机号步骤线索,但页面或来源尚未核对完整。请保持官方登录页在浏览器前台;暂不取号。' $State.phone_notice_shown='yes'; Save-SetupState $State } return } $State.stage='phone_tier_selection'; Save-SetupState $State Write-Host '已识别“手机号是必填项”、号码输入框和发送/继续按钮,来源为官方登录域名。' $choice = Read-Host '此时选择号码方案:免费输入 1,付费澳洲包月输入 2,稍后处理直接回车' switch ($choice) { '1' { $State.phone_tier='free'; $State.stage='waiting_free_sms_operator'; Write-Host '已选择免费号码,请在客户群联系服务提供方处理当前验证。短信自动填入尚未接通。' } '2' { $State.phone_tier='paid_manual'; $State.stage='waiting_paid_operator'; Write-Host '已选择付费澳洲包月,请在客户群联系服务提供方手动接号和验证码。' } default { $State.phone_tier='deferred'; $State.stage='waiting_phone_verification' } } Save-SetupState $State Write-Host '完成当前提示后请返回官方浏览器页面。程序继续等待授权;页面识别不代表邮箱登录或授权已成功。' } # Runs only on the customer's computer. No account or subscription is sent to our server. $script:ProxySubscriptionConfig = ('{"provider": "feituyun", "dashboard_url": "https://ft-test.tumaoyun.com/#/dashboard", "invite_url": "https://666.xn--h5qy75o.vip/#/register?code=dWt2QVPv", "api_base": "https://gtm20260412.apitutu.com/ft/gateway/nginx", "subscription_host": "ft.tutunode.com", "customer_registration": "official_browser", "customer_login": "local_hidden_password", "platform_relationship": "same_platform_confirmed_by_owner", "source": "public_frontend_config_and_runtime_2026-11-10", "poll_seconds": 10, "wait_seconds": 600}' | ConvertFrom-Json) function Invoke-ProxyAccountApi([string]$Path, [string]$Auth, [string]$Body) { $response = $null; $reader = $null try { $request = [Net.HttpWebRequest]::Create($script:ProxySubscriptionConfig.api_base + $Path) $request.AllowAutoRedirect = $false $request.Timeout = 20000; $request.ReadWriteTimeout = 20000 $request.Headers['Content-Language'] = 'zh-CN' if ($Auth) { $request.Headers['Authorization'] = $Auth } if ($Body) { $request.Method = 'POST' $request.ContentType = 'application/x-www-form-urlencoded' $bytes = [Text.Encoding]::UTF8.GetBytes($Body) $request.ContentLength = $bytes.Length $stream = $request.GetRequestStream() try { $stream.Write($bytes, 0, $bytes.Length) } finally { $stream.Dispose(); [Array]::Clear($bytes, 0, $bytes.Length) } } $response = $request.GetResponse() if ([int]$response.StatusCode -ne 200) { throw 'unexpected_http_status' } if ($response.ContentLength -gt 1048576) { throw 'unexpected_response_size' } $reader = New-Object IO.StreamReader($response.GetResponseStream(), [Text.Encoding]::UTF8) $raw = $reader.ReadToEnd() if ($raw.Length -gt 1048576) { throw 'unexpected_response_size' } return ($raw | ConvertFrom-Json).data } catch { throw '代理平台请求未成功,请确认账号或稍后重试。' } finally { if ($reader) { $reader.Dispose() }; if ($response) { $response.Dispose() } } } function Get-ProxySubscriptionStatus($Data) { try { # Null/missing values must not be silently converted to zero or unlimited. if ($null -eq $Data) { return 'invalid_response' } if ($Data.PSObject.Properties.Name -notcontains 'plan_id') { return 'invalid_response' } if (-not $Data.plan_id) { return 'waiting_purchase' } foreach ($key in @('transfer_enable', 'u', 'd', 'expired_at')) { if ($Data.PSObject.Properties.Name -notcontains $key) { return 'invalid_response' } } foreach ($key in @('transfer_enable', 'u', 'd')) { if ($null -eq $Data.$key -or [string]$Data.$key -notmatch '^\d+$') { return 'invalid_response' } } if ($null -ne $Data.expired_at) { if ([string]$Data.expired_at -notmatch '^\d+$') { return 'invalid_response' } if ([decimal]$Data.expired_at -le [DateTimeOffset]::UtcNow.ToUnixTimeSeconds()) { return 'plan_expired' } } if ([decimal]$Data.transfer_enable -le 0 -or ([decimal]$Data.u + [decimal]$Data.d) -ge [decimal]$Data.transfer_enable) { return 'quota_exhausted' } $uri = $null if (-not [Uri]::TryCreate([string]$Data.subscribe_url, [UriKind]::Absolute, [ref]$uri) -or $uri.Scheme -ne 'https' -or $uri.Host -ne $script:ProxySubscriptionConfig.subscription_host -or $uri.Port -ne 443 -or $uri.UserInfo -or $uri.Fragment) { return 'invalid_subscription' } return 'ready' } catch { return 'invalid_response' } } function Start-ProxySubscriptionSetup { Write-Host '请在刚打开的代理网站自行注册并购买套餐,完成后回到这里。' if ((Read-Host '已完成注册/购买输入 1,稍后处理直接回车') -ne '1') { return 'waiting_proxy_registration' } Write-Host '下面仅登录你的飞兔云代理账号,密码隐藏输入,直接发送给代理平台。' $email = Read-Host '刚注册的代理账号邮箱(不是 Codex 登录)' if (-not $email) { return 'waiting_proxy_registration' } $password = Read-Host '代理账号密码' -AsSecureString $pointer = [IntPtr]::Zero; $plain = $null; $body = $null; $auth = $null; $data = $null try { $pointer = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($password) $plain = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($pointer) $body = 'email=' + [Uri]::EscapeDataString($email) + '&password=' + [Uri]::EscapeDataString($plain) [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($pointer); $pointer = [IntPtr]::Zero $password.Dispose(); $password = $null; $plain = $null $login = Invoke-ProxyAccountApi '/passport/auth/login' '' $body $auth = [string]$login.auth_data $login = $null; $body = $null; $plain = $null if (-not $auth -or $auth -match '[\r\n]') { return 'proxy_login_failed' } $deadline = [DateTime]::UtcNow.AddSeconds($script:ProxySubscriptionConfig.wait_seconds) do { $data = Invoke-ProxyAccountApi '/user/getSubscribe' $auth '' $status = Get-ProxySubscriptionStatus $data if ($status -ne 'waiting_purchase') { break } Write-Host '尚未检测到已生效套餐。请完成购买,程序会继续检查…' Start-Sleep -Seconds $script:ProxySubscriptionConfig.poll_seconds } while ([DateTime]::UtcNow -lt $deadline) if ($status -ne 'ready') { switch ($status) { 'quota_exhausted' { Write-Host '套餐流量已用完,请在代理网站处理后重新运行。' } 'plan_expired' { Write-Host '套餐已过期,请在代理网站处理后重新运行。' } 'waiting_purchase' { Write-Host '等待套餐生效已超时,完成购买后可重新运行。' } default { Write-Host '订阅信息无法验证,请联系服务提供方。' } } return $status } Ensure-ClashVerge $importUri = 'clash://install-config?url=' + [Uri]::EscapeDataString([string]$data.subscribe_url) + '&name=Feituyun' Start-Process -FilePath $importUri | Out-Null Write-Host '已自动读取订阅并发起 Clash 导入。请在客户端确认导入、选用订阅并开启系统代理。' [void](Read-Host '完成后回车,程序将重新检查网络') return 'import_requested' } catch { Write-Host '代理账号登录、订阅读取或导入未完成,请确认账号及客户端后重新运行。'; return 'proxy_setup_failed' } finally { if ($pointer -ne [IntPtr]::Zero) { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($pointer) } if ($password) { $password.Dispose() } $plain = $null; $body = $null; $auth = $null; $data = $null; $importUri = $null } } # Embedded by build-public.py. State remains on the customer's computer. function Save-SetupState($State) { $directory = Join-Path $env:LOCALAPPDATA 'CodexSetup' [IO.Directory]::CreateDirectory($directory) | Out-Null $State.updated_at = [DateTime]::UtcNow.ToString('o') $path = Join-Path $directory 'workflow-state.json' $State | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath ($path + '.new') -Encoding UTF8 Move-Item -LiteralPath ($path + '.new') -Destination $path -Force } function Test-SetupNetwork { $results = @() foreach ($target in @('https://auth.openai.com/', 'https://chatgpt.com/')) { $response = $null try { $request = [Net.HttpWebRequest]::Create($target) $request.Method = 'HEAD'; $request.AllowAutoRedirect = $true $request.Timeout = 12000; $request.ReadWriteTimeout = 12000 $proxyValue = $env:HTTPS_PROXY if (-not $proxyValue) { $proxyValue = $env:ALL_PROXY } if ($proxyValue) { $proxyUri = $null if ([Uri]::TryCreate($proxyValue, [UriKind]::Absolute, [ref]$proxyUri) -and $proxyUri.Scheme -eq 'http') { $request.Proxy = New-Object Net.WebProxy($proxyUri) } } $response = $request.GetResponse() $results += [int]$response.StatusCode } catch [Net.WebException] { if ($_.Exception.Response) { $results += [int]$_.Exception.Response.StatusCode; $_.Exception.Response.Dispose() } else { $results += 0 } } finally { if ($response) { $response.Dispose() } } } if (@($results | Where-Object { $_ -eq 0 }).Count -gt 0) { return 'connection_failed' } if (@($results | Where-Object { $_ -ge 400 }).Count -gt 0) { return 'browser_check_required' } return 'reachable' } function Find-ClashVerge { foreach ($root in @($env:LOCALAPPDATA, $env:ProgramFiles, ${env:ProgramFiles(x86)})) { if (-not $root) { continue } foreach ($relative in @('Clash Verge\clash-verge.exe', 'Programs\Clash Verge\clash-verge.exe')) { $candidate = Join-Path $root $relative if (Test-Path -LiteralPath $candidate -PathType Leaf) { return $candidate } } } foreach ($root in @('HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall', 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall')) { foreach ($entry in @(Get-ChildItem -LiteralPath $root -ErrorAction SilentlyContinue)) { $value = Get-ItemProperty -LiteralPath $entry.PSPath -ErrorAction SilentlyContinue if ($value.DisplayName -match '^Clash Verge' -and $value.InstallLocation) { $candidate = Join-Path $value.InstallLocation 'clash-verge.exe' if (Test-Path -LiteralPath $candidate -PathType Leaf) { return $candidate } } } } return $null } function Ensure-ClashVerge { $clash = Find-ClashVerge if (-not $clash) { $artifact = [pscustomobject]@{filename='Clash.Verge_2.5.10_x64-setup.exe'; size=60045702; sha256='929da9303b643b80ce309ab29f618e37a5562f515d1fa55ebf5b86c822504450'; download_url='https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v2.5.10/Clash.Verge_2.5.10_x64-setup.exe'} $folder = Join-Path $env:LOCALAPPDATA ('CodexSetup\Proxy\' + $artifact.sha256) [IO.Directory]::CreateDirectory($folder) | Out-Null $installer = Join-Path $folder $artifact.filename if (-not (Test-VerifiedFile $installer $artifact.size $artifact.sha256)) { Write-Host '正在从 Clash Verge Rev 官方 GitHub 下载并校验安装包…' Get-DirectInstaller $artifact $installer } $process = Start-Process -FilePath $installer -Wait -PassThru if ($process.ExitCode -notin @(0,3010)) { throw 'Clash 安装尚未完成,请按安装程序提示处理。' } $clash = Find-ClashVerge } if ($clash) { Start-Process -FilePath $clash | Out-Null } else { Write-Host '请从开始菜单打开已安装的代理客户端。' } } function Find-CodexCli($Detection) { foreach ($name in @($Detection.app_packages)) { $package = Get-AppxPackage -Name $name -ErrorAction SilentlyContinue if ($package.InstallLocation) { $candidate = Join-Path $package.InstallLocation 'app\resources\codex.exe' if (Test-Path -LiteralPath $candidate -PathType Leaf) { return $candidate } } } foreach ($app in @($Detection.executable_paths)) { foreach ($relative in @('resources\codex.exe', 'resources\bin\codex.exe')) { $candidate = Join-Path (Split-Path $app -Parent) $relative if (Test-Path -LiteralPath $candidate -PathType Leaf) { return $candidate } } } $command = Get-Command codex.exe -ErrorAction SilentlyContinue if ($command) { return $command.Source } return $null } function Get-CodexAuthState([string]$Cli) { if (-not $Cli) { return 'unavailable' } $info = New-Object Diagnostics.ProcessStartInfo $info.FileName = $Cli; $info.Arguments = 'login status' $info.UseShellExecute = $false; $info.CreateNoWindow = $true $info.RedirectStandardOutput = $true; $info.RedirectStandardError = $true $process = New-Object Diagnostics.Process $process.StartInfo = $info try { [void]$process.Start() $stdout = $process.StandardOutput.ReadToEndAsync(); $stderr = $process.StandardError.ReadToEndAsync() if (-not $process.WaitForExit(15000)) { $process.Kill(); return 'check_failed' } $text = $stdout.Result + $stderr.Result if ($process.ExitCode -eq 0 -and $text -match 'Logged in using ChatGPT') { return 'chatgpt_authorized' } if ($process.ExitCode -eq 0) { return 'other_auth' } if ($text -match 'Not logged in') { return 'signed_out' } return 'check_failed' } catch { return 'check_failed' } finally { $process.Dispose() } } function Start-CodexFollowup($Detection) { if ($NoLaunch -or $SkipFollowup) { Open-Codex $Detection; return } $state = [ordered]@{schema_version=1; stage='network'; network_state='checking'; subscription_state='not_checked'; email_login_state='not_observable'; codex_cli_authorization_state='not_checked'; desktop_login_state='not_confirmed'; usability_state='not_checked'; phone_observation_enabled='no'; phone_page_state='not_observed'; phone_tier='not_selected'; phone_notice_shown='no'; updated_at=''} Save-SetupState $state Write-Host '第 2 步:检查官方服务连接…' for ($attempt = 0; $attempt -lt 4; $attempt++) { $state.network_state = Test-SetupNetwork Save-SetupState $state if ($state.network_state -ne 'connection_failed') { break } Write-Host '当前终端无法完成官方服务连接。' $answer = Read-Host '已有可用代理请选择 1,需要注册或获取订阅请选择 2,暂时退出请选择 0' if ($answer -eq '0') { $state.stage='waiting_network'; Save-SetupState $state; return } if ($answer -eq '2') { Start-Process -FilePath $script:ProxySubscriptionConfig.invite_url | Out-Null $state.subscription_state = Start-ProxySubscriptionSetup Save-SetupState $state if ($state.subscription_state -ne 'import_requested') { $state.stage='waiting_proxy_subscription'; Save-SetupState $state; return } } if ($answer -eq '1') { $clash = Find-ClashVerge if ($clash) { Start-Process -FilePath $clash | Out-Null } else { $install = Read-Host '需要安装 Clash Verge Rev 吗?输入 1 安装,直接回车使用自己的代理客户端' if ($install -eq '1') { Ensure-ClashVerge } } [void](Read-Host '请启用自己的代理及系统代理,完成后回车重新检查') } } if ($state.network_state -eq 'connection_failed') { $state.stage='waiting_network'; Save-SetupState $state Write-Host '连接仍未成功。处理网络后重新运行即可继续。'; return } if ($state.network_state -eq 'browser_check_required') { Write-Host '官方服务已响应,但可能要求浏览器验证;这不代表代理未开启。' } $state.stage='official_login'; Save-SetupState $state Write-Host '第 3 步:打开 Codex,请在应用内点击登录,再在官方浏览器页面完成 Google 或邮箱登录。' Write-Host '密码、邮箱验证码及安全验证均由你在官方页面完成。' Open-Codex $Detection $cli = Find-CodexCli $Detection $state.codex_cli_authorization_state = Get-CodexAuthState $cli Save-SetupState $state if ($state.codex_cli_authorization_state -ne 'chatgpt_authorized') { Write-Host '可在本机读取前台浏览器的可见控件文字,识别手机号步骤;不读取输入框内容或保存页面。' if ((Read-Host '启用本次页面识别请输入 1,否则回车继续手动处理') -eq '1') { $state.phone_observation_enabled='yes' } Save-SetupState $state } if ($state.codex_cli_authorization_state -eq 'signed_out' -or $state.phone_observation_enabled -eq 'yes') { Write-Host '正在等待授权和当前页面,最长 10 分钟。请把正在操作的登录页保持在浏览器前台。' $deadline = (Get-Date).AddMinutes(10) do { Start-Sleep -Seconds 5 Update-PhonePageStep $state $state.codex_cli_authorization_state = Get-CodexAuthState $cli Save-SetupState $state if ($state.codex_cli_authorization_state -eq 'chatgpt_authorized') { break } if ($state.phone_observation_enabled -ne 'yes' -and $state.codex_cli_authorization_state -ne 'signed_out') { break } } while ((Get-Date) -lt $deadline) } if ($state.codex_cli_authorization_state -ne 'chatgpt_authorized' -and $state.phone_page_state -eq 'phone_step_detected') { if ($state.phone_tier -eq 'not_selected') { $state.stage='waiting_phone_verification' } Save-SetupState $state; Write-Host '手机号步骤仍待处理,已保留当前方案。完成验证后重新运行以确认 Codex 登录。'; return } if ($state.codex_cli_authorization_state -ne 'chatgpt_authorized' -and $state.phone_page_state -like 'candidate_*') { $state.stage='waiting_phone_page_check'; Save-SetupState $state Write-Host '手机号页面识别线索尚不完整,请联系服务提供方核对当前页面。'; return } if ($state.codex_cli_authorization_state -eq 'chatgpt_authorized') { Write-Host '已检测到本机 Codex 命令行的 ChatGPT 授权。邮箱网页和桌面应用的当前状态仍需分别确认。' } else { Write-Host '暂未检测到命令行 ChatGPT 授权,可能登录尚未完成或此版本无法共享状态。' } $state.stage='desktop_confirmation'; Save-SetupState $state $answer = Read-Host '桌面 Codex 已进入工作界面请输入 1;仍停留在登录页面直接回车' if ($answer -ne '1') { $state.stage='waiting_desktop_login'; Save-SetupState $state; return } $state.desktop_login_state='customer_confirmed'; Save-SetupState $state Write-Host '请在 Codex 中新建一个测试会话,发送“只回复 OK”,确认收到正常回复。' $answer = Read-Host '收到正常回复请输入 1,否则直接回车保留待处理状态' $state.usability_state = if ($answer -eq '1') { 'customer_confirmed' } else { 'not_confirmed' } $state.stage = if ($state.usability_state -eq 'customer_confirmed') { 'customer_confirmed_complete' } else { 'waiting_usability' } Save-SetupState $state if ($state.stage -eq 'customer_confirmed_complete') { Write-Host '客户已确认桌面登录和实际使用成功。' } else { Write-Host '已保留当前进度,请联系服务提供方处理具体错误。' } } $detected = Get-CodexInstallation | ConvertFrom-Json if ($DetectOnly) { $detected | ConvertTo-Json -Depth 4; return } Write-Host '正在检测这台电脑是否已安装 Codex…' if ($detected.installation_state -eq 'detected' -and -not $DownloadOnly) { Write-Host '已检测到 Codex,无需下载。' -ForegroundColor Green Start-CodexFollowup $detected return } if (-not $detected.supported_architecture) { throw ('No owner-supplied installer is configured for Windows ' + $detected.architecture) } if ($ManifestPath) { if (-not $DownloadOnly) { throw 'A local test manifest is allowed only with -DownloadOnly.' } $manifest = Get-Content -LiteralPath $ManifestPath -Raw -Encoding UTF8 | ConvertFrom-Json } else { $manifest = Invoke-RestMethod -Uri ($baseUrl + '/releases.json') } $artifact = $manifest.artifacts.'windows-x64' if (-not $artifact.available) { throw 'The installer is not ready. Please try again later.' } Assert-DirectManifest $artifact $downloadDirectory = Join-Path $env:LOCALAPPDATA ('CodexSetup\Downloads\' + $artifact.sha256) [IO.Directory]::CreateDirectory($downloadDirectory) | Out-Null try { $cacheLock = [IO.File]::Open((Join-Path $downloadDirectory '.assembly.lock'), [IO.FileMode]::OpenOrCreate, [IO.FileAccess]::ReadWrite, [IO.FileShare]::None) } catch { throw '已有安装任务正在下载,请等待该任务完成或关闭后重试。' } try { $installer = Join-Path $downloadDirectory 'codex-win.exe' if (-not (Test-VerifiedFile $installer $artifact.size $artifact.sha256)) { Write-Host '正在从服务器下载 Codex 安装包,支持中断后续传…' Get-DirectInstaller $artifact $installer } if ($DownloadOnly) { Write-Host ('安装包已下载并校验:' + $installer); return } Write-Host '正在启动安装。请按安装程序和 Windows 的提示完成确认。' $process = Start-Process -FilePath $installer -PassThru -Wait if ($process.ExitCode -notin @(0, 3010)) { throw ('Installer exited with code ' + $process.ExitCode) } Write-Host '等待安装程序完成…' $deadline = (Get-Date).AddMinutes(10) do { $detected = Get-CodexInstallation | ConvertFrom-Json if ($detected.installation_state -eq 'detected') { break } Start-Sleep -Seconds 5 } while ((Get-Date) -lt $deadline) if ($detected.installation_state -ne 'detected') { throw 'Codex was not detected after setup. Complete the installer, then run this command again.' } Write-Host '已检测到 Codex 安装完成。' -ForegroundColor Green Start-CodexFollowup $detected } finally { $cacheLock.Dispose() }