# Code2AI 终端助手(c2a)- Windows PowerShell # 由 install.ps1 安装到 %USERPROFILE%\.local\bin\c2a.ps1(并在 PATH 上放一个 c2a.cmd shim) # 自更新源:https://console.code2ai.codes/c2a.ps1 # 用法: # c2a 交互菜单 # c2a doctor 环境自检(CLI / 环境变量 / 密钥 / 网络路径 / 连通性) # c2a fix-ipv6 IPv6 线路有问题时强制走 IPv4(c2a fix-ipv6 undo 撤销,需管理员) # c2a login 重新浏览器授权(更换账号或密钥失效时用) # c2a update 更新 Claude CLI + c2a 自身 # c2a uninstall 移除 c2a 与环境变量配置(保留 Claude CLI 本体) # c2a version 显示版本 param( [Parameter(Position=0)] [string]$Command = '', # 用 ValueFromRemainingArguments 收尾参数:直接声明成 [string]$Arg1 的话, # PowerShell 会把 "--undo" 当成参数名去绑定并报错,而不是当成值 [Parameter(Position=1, ValueFromRemainingArguments=$true)] [string[]]$Rest = @() ) $ErrorActionPreference = 'Stop' # 强制控制台按 UTF-8 输出,避免中文乱码(现代 Windows Terminal 默认支持) try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {} # 强制 TLS 1.2:Windows PowerShell 5.1 默认 TLS 1.0,CDN/服务器只接受 1.2+,不设会握手失败 try { [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 } catch {} $C2aVersion = '1.0.11' $BaseUrl = 'https://code2ai.codes' $ApiHost = 'code2ai.codes' $ConsoleUrl = 'https://console.code2ai.codes' $InstallDir = Join-Path $HOME '.local\bin' function Write-Info { param($msg) Write-Host "==> $msg" -ForegroundColor Cyan } function Write-Ok { param($msg) Write-Host "[OK] $msg" -ForegroundColor Green } function Write-Warn { param($msg) Write-Host "[!] $msg" -ForegroundColor Yellow } function Write-Err { param($msg) Write-Host "[X] $msg" -ForegroundColor Red } # ---------- 公共工具 ---------- function Copy-ToClipboard { param([string]$Text) try { if (Get-Command Set-Clipboard -ErrorAction SilentlyContinue) { Set-Clipboard -Value $Text; return $true } $Text | clip.exe; return $true } catch { return $false } } # 当前生效的 Key:优先当前会话,其次用户级环境变量 function Get-CurrentKey { if (-not [string]::IsNullOrWhiteSpace($env:ANTHROPIC_AUTH_TOKEN)) { return $env:ANTHROPIC_AUTH_TOKEN } $u = [System.Environment]::GetEnvironmentVariable('ANTHROPIC_AUTH_TOKEN', 'User') if ($u) { return $u } return '' } function Get-MaskedKey { param([string]$Key) if ([string]::IsNullOrEmpty($Key)) { return '(未配置)' } if ($Key.Length -le 12) { return $Key } return $Key.Substring(0,11) + '...' + $Key.Substring($Key.Length-4) } # 返回 HTTP 状态码字符串(网络失败返回 "000") # # 优先用 curl.exe:Claude Code 是 Node 程序,只认 HTTP_PROXY/HTTPS_PROXY 环境变量, # curl.exe 的代理行为与它一致;而 Invoke-WebRequest 走的是 .NET 那套(读系统代理、 # 不读环境变量)。用 IWR 会出现「代理挂了导致 CC 用不了,doctor 却报 API 正常」的误判。 function Get-ApiHttpStatus { param([string]$Key) $body = @{ model='claude-sonnet-4-20250514'; max_tokens=10; messages=@(@{role='user';content='hi'}) } | ConvertTo-Json -Depth 5 $curl = Get-CurlExe if ($curl) { try { $tmp = Join-Path $env:TEMP "c2a-api-$PID.json" # 必须写成无 BOM:PowerShell 5.1 的 -Encoding UTF8 会加 BOM, # 服务端 JSON 解析会因此失败(实测返回 503),看起来就像是服务挂了 [System.IO.File]::WriteAllText($tmp, $body, (New-Object System.Text.UTF8Encoding($false))) $cargs = @('-sS','-o','NUL','-w','%{http_code}','-m','20','-X','POST', '-H','Content-Type: application/json', '-H',"x-api-key: $Key", '-H','anthropic-version: 2023-06-01', '--data-binary',"@$tmp","$BaseUrl/v1/messages") $out = & $curl @cargs 2>$null Remove-Item $tmp -Force -ErrorAction SilentlyContinue if ([string]::IsNullOrWhiteSpace($out)) { return '000' } return ($out | Select-Object -First 1).ToString().Trim() } catch { return '000' } } $headers = @{ 'Content-Type'='application/json'; 'x-api-key'=$Key; 'anthropic-version'='2023-06-01' } try { $resp = Invoke-WebRequest -Uri "$BaseUrl/v1/messages" -Method Post -Headers $headers -Body $body -TimeoutSec 20 -UseBasicParsing return [string][int]$resp.StatusCode } catch { # PS 5.1 抛 WebException、PS 7 抛 HttpResponseException,两者 .Response.StatusCode # 都是 HttpStatusCode 枚举,[int] 转换在两版本都可用(不用 .value__,PS7 无该属性) if ($_.Exception.Response -and ($null -ne $_.Exception.Response.StatusCode)) { return [string][int]$_.Exception.Response.StatusCode } return '000' } } # ---------- 网络路径探测 ---------- # /health 是公开端点,不需要 Key。用它先把「网络层不通」和「Key 有问题」分开。 # 协议族要能指定,Invoke-WebRequest 做不到,所以优先用系统自带的 curl.exe # (Windows 10 1803+ 均内置);没有时回退到 .NET 直接测 TCP 443。 function Get-CurlExe { $c = Get-Command curl.exe -ErrorAction SilentlyContinue if ($c) { return $c.Source } return $null } # $Family: '' | '-4' | '-6';返回 "http_code|remote_ip",连不上为 "000|" function Test-Health { param([string]$Family = '') $curl = Get-CurlExe if ($curl) { # 注意:不能用 $args 当变量名,那是 PowerShell 的自动变量 $cargs = @('-sS','-o','NUL','-w','%{http_code}|%{remote_ip}','-m','12') if ($Family) { $cargs = ,$Family + $cargs } $cargs += "$BaseUrl/health" try { $out = & $curl @cargs 2>$null if ([string]::IsNullOrWhiteSpace($out)) { return '000|' } return ($out | Select-Object -First 1).ToString().Trim() } catch { return '000|' } } # 回退:只能测 TCP 可达性,测不到 HTTP 码 $af = [System.Net.Sockets.AddressFamily]::Unspecified if ($Family -eq '-4') { $af = [System.Net.Sockets.AddressFamily]::InterNetwork } if ($Family -eq '-6') { $af = [System.Net.Sockets.AddressFamily]::InterNetworkV6 } try { $ips = [System.Net.Dns]::GetHostAddresses($ApiHost) if ($af -ne [System.Net.Sockets.AddressFamily]::Unspecified) { $ips = $ips | Where-Object { $_.AddressFamily -eq $af } } if (-not $ips) { return '000|' } $ip = $ips[0] $c = New-Object System.Net.Sockets.TcpClient $iar = $c.BeginConnect($ip, 443, $null, $null) $okc = $iar.AsyncWaitHandle.WaitOne(12000, $false) if ($okc -and $c.Connected) { $c.Close(); return "200|$($ip.ToString())" } $c.Close(); return '000|' } catch { return '000|' } } # 大包探测:小包通、大包挂 = MTU 黑洞(IPv6 隧道常见)。 # 症状是「刚开始能聊,上下文一长就断」,只测小 GET 会完全漏判。 # 返回什么状态码都不重要 —— 能拿到响应就说明大包出得去。 function Test-LargePacket { $curl = Get-CurlExe $payload = 'x' * 24000 if ($curl) { try { $tmp = Join-Path $env:TEMP "c2a-mtu-$PID.txt" Set-Content -Path $tmp -Value $payload -NoNewline -Encoding ASCII $cargs = @('-sS','-o','NUL','-w','%{http_code}','-m','20','-X','POST', '-H','Content-Type: text/plain','--data-binary',"@$tmp","$BaseUrl/health") $out = & $curl @cargs 2>$null Remove-Item $tmp -Force -ErrorAction SilentlyContinue if ([string]::IsNullOrWhiteSpace($out)) { return '000' } return ($out | Select-Object -First 1).ToString().Trim() } catch { return '000' } } try { $r = Invoke-WebRequest -Uri "$BaseUrl/health" -Method Post -Body $payload ` -ContentType 'text/plain' -TimeoutSec 20 -UseBasicParsing return [string][int]$r.StatusCode } catch { if ($_.Exception.Response -and ($null -ne $_.Exception.Response.StatusCode)) { return [string][int]$_.Exception.Response.StatusCode } return '000' } } # 代理来源在 Windows 上有三处,只看环境变量会漏: # 1) 环境变量(Node/Claude Code 只认这个) # 2) 系统代理(设置→网络→代理,写在注册表,VS Code 等会读) # 3) VS Code / Cursor 自己的 http.proxy 设置(只影响编辑器内的插件) function Get-ProxyFindings { $found = @() foreach ($n in 'HTTP_PROXY','HTTPS_PROXY','ALL_PROXY') { $v = [System.Environment]::GetEnvironmentVariable($n) if (-not [string]::IsNullOrWhiteSpace($v)) { $found += [pscustomobject]@{ Kind='env'; Name=$n; Value=$v } } } try { $reg = Get-ItemProperty -Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings' -ErrorAction Stop if ($reg.ProxyEnable -eq 1 -and -not [string]::IsNullOrWhiteSpace($reg.ProxyServer)) { $found += [pscustomobject]@{ Kind='system'; Name='系统代理'; Value=$reg.ProxyServer } } } catch {} foreach ($ed in @(@{n='VS Code';d='Code'}, @{n='Cursor';d='Cursor'})) { $sf = Join-Path $env:APPDATA "$($ed.d)\User\settings.json" if (Test-Path $sf) { try { # VS Code 的 settings.json 允许注释,ConvertFrom-Json 会炸,所以直接正则取值 $raw = Get-Content $sf -Raw -ErrorAction Stop if ($raw -match '"http\.proxy"\s*:\s*"([^"]+)"') { $found += [pscustomobject]@{ Kind='editor'; Name="$($ed.n) 的 http.proxy"; Value=$Matches[1] } } } catch {} } } return $found } function Test-IsAdmin { try { $id = [Security.Principal.WindowsIdentity]::GetCurrent() return (New-Object Security.Principal.WindowsPrincipal($id)).IsInRole( [Security.Principal.WindowsBuiltInRole]::Administrator) } catch { return $false } } # 设备码授权;成功时设置 $script:ApiKey 并返回 $true function Invoke-DeviceAuthFlow { Write-Info "进入配对授权..." $start = $null try { $start = Invoke-RestMethod -Method Post -Uri "$ConsoleUrl/api/v1/device/start" -ContentType 'application/json' -Body '{}' -TimeoutSec 15 } catch { Write-Err "无法连接授权服务:$($_.Exception.Message)" return $false } Write-Host "" Write-Host " +-----------------------------------------+" -ForegroundColor Green Write-Host " | 配对码: $($start.user_code) |" -ForegroundColor Green Write-Host " +-----------------------------------------+" -ForegroundColor Green Write-Host "" Write-Host " 请在任意浏览器(电脑/手机均可)打开下面的地址,登录后输入上面的配对码:" Write-Host " $($start.verification_url)" -ForegroundColor Cyan Write-Host "" if (Copy-ToClipboard -Text $start.user_code) { Write-Ok "配对码已复制到剪贴板,打开网页后直接粘贴即可。" Write-Host "" } Write-Info "等待网页确认(10 分钟内有效)..." $pollInterval = [Math]::Max(1, [int]$start.interval) $deadline = (Get-Date).AddSeconds(600) while ((Get-Date) -lt $deadline) { Start-Sleep -Seconds $pollInterval $r = $null try { $r = Invoke-RestMethod -Method Post -Uri "$ConsoleUrl/api/v1/device/token" -ContentType 'application/json' ` -Body (@{ device_code = $start.device_code } | ConvertTo-Json) -TimeoutSec 10 } catch { Write-Host "." -NoNewline; continue } switch ($r.status) { 'success' { Write-Host "" $script:ApiKey = $r.api_key Write-Ok "授权成功(账号:$($r.username))。" return $true } 'no_subscription' { Write-Host "" Write-Err "该账号暂无有效订阅。" Write-Host " 请先购买套餐:$($r.pricing_url)" return $false } 'key_unavailable' { Write-Host "" Write-Err "账号密钥需要重新生成。" Write-Host " 请到控制台「API 密钥管理」重新生成:$($r.dashboard_url)" return $false } 'expired' { Write-Host "" Write-Err "授权已过期,请重新运行本命令。" return $false } default { Write-Host "." -NoNewline } } } Write-Host "" Write-Err "等待授权超时(10 分钟),请重新运行本命令。" return $false } # 写环境变量(用户级 + 当前会话),带字符集校验 function Write-EnvConfig { param([string]$Key) if ([string]::IsNullOrWhiteSpace($Key) -or ($Key -notmatch '^[A-Za-z0-9_-]+$')) { Write-Err "API Key 含非法字符或为空,已中止(未写入配置)。" return $false } [System.Environment]::SetEnvironmentVariable('ANTHROPIC_BASE_URL', $BaseUrl, 'User') [System.Environment]::SetEnvironmentVariable('ANTHROPIC_AUTH_TOKEN', $Key, 'User') [System.Environment]::SetEnvironmentVariable('CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC', '1', 'User') [System.Environment]::SetEnvironmentVariable('ENABLE_TOOL_SEARCH', 'true', 'User') $env:ANTHROPIC_BASE_URL = $BaseUrl $env:ANTHROPIC_AUTH_TOKEN = $Key $env:CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = '1' $env:ENABLE_TOOL_SEARCH = 'true' Write-Ok "环境变量已保存(用户级)。" return $true } # ---------- 子命令 ---------- function Invoke-Doctor { $fails = 0 # 1. Claude CLI $sCli = 'none' $claudeCmd = Get-Command claude -ErrorAction SilentlyContinue if ($claudeCmd) { $ver = & claude --version 2>$null | Select-Object -First 1 if ($ver) { $sCli = ($ver.ToString().Trim() -split '\s+')[0] } Write-Ok "Claude CLI 已安装($ver)。" } else { Write-Err "Claude CLI 未安装 —— 运行:irm $ConsoleUrl/install.ps1 | iex" $fails++ } # 2. 用户级环境变量是否已配置 $userKey = [System.Environment]::GetEnvironmentVariable('ANTHROPIC_AUTH_TOKEN', 'User') if ($userKey) { Write-Ok "用户级环境变量已配置。" } else { Write-Err "未找到 code2ai 环境变量 —— 运行:c2a login" $fails++ } # 3. 当前会话与用户级一致性 if (-not [string]::IsNullOrWhiteSpace($env:ANTHROPIC_AUTH_TOKEN)) { if ($userKey -and $userKey -ne $env:ANTHROPIC_AUTH_TOKEN) { Write-Warn "当前会话的 Key 与保存的不一致 —— 请重开终端。" $fails++ } else { Write-Ok "当前会话环境变量已生效。" } } else { Write-Warn "当前会话未加载环境变量 —— 请重开终端。" $fails++ } # 4. 网络路径 —— 用公开的 /health 探测,不需要 Key, # 先把「网络层不通」和「Key 有问题」分开,避免都报成一句笼统的连不上 $sNet = '?'; $sProxy = 'none'; $sApi = '?'; $sConsole = '?' $proxies = Get-ProxyFindings $envProxy = @($proxies | Where-Object { $_.Kind -eq 'env' }) if ($proxies.Count -gt 0) { $sProxy = 'set' Write-Warn "检测到代理配置(三类的影响范围不一样):" foreach ($p in $proxies) { $note = switch ($p.Kind) { 'env' { '← Claude Code 只认这个,代理挂了就直接 ConnectionRefused' } 'system' { '← Claude Code 不读它,浏览器和编辑器会读,仅供参考' } 'editor' { '← 只影响编辑器内的 Claude Code,命令行里测不出来' } default { '' } } Write-Host " - $($p.Name) = $($p.Value) $note" } } else { Write-Ok "未检测到代理配置。" } Write-Info "检测网络路径..." $auto = Test-Health '' $autoHttp = ($auto -split '\|')[0] $autoIp = ($auto -split '\|')[1] if ($autoHttp -eq '200') { if ($autoIp -like '*:*') { $sNet = '200/v6'; Write-Ok "网络可达(当前走 IPv6:$autoIp)。" } else { $sNet = '200/v4'; Write-Ok "网络可达(当前走 IPv4:$autoIp)。" } # 小包通不等于大包通:MTU 黑洞下小请求正常、大请求被静默丢弃 $big = Test-LargePacket if ($big -eq '000') { $sNet = "$sNet/big:fail" Write-Err "小数据包正常,但大数据包发不出去 —— 典型的 MTU 黑洞。" Write-Host " 症状:Claude Code 刚开始能用,对话一长就断(ConnectionRefused 或卡死)" if ($autoIp -like '*:*') { Write-Warn "当前走的是 IPv6,运行 c2a fix-ipv6 改走 IPv4 通常可解决。" } else { Write-Warn "当前已经是 IPv4,请检查本机 VPN / 路由器的 MTU 设置。" } $fails++ } else { Write-Ok "大数据包传输正常(长对话不会断)。" } } else { $sNet = 'fail' Write-Err "无法连接 $BaseUrl —— 网络层不通(不是 Key 的问题)。" $fails++ $v4 = (Test-Health '-4') -split '\|' $v6 = (Test-Health '-6') -split '\|' $sNet = "v4:$($v4[0])/v6:$($v6[0])" Write-Host " IPv4 通道 = $($v4[0]) IPv6 通道 = $($v6[0]) (200 为正常,000 为连不上)" if ($v4[0] -eq '200' -and $v6[0] -ne '200') { Write-Warn "IPv4 通、IPv6 不通 —— 运行 c2a fix-ipv6 强制走 IPv4 即可解决。" } elseif ($v4[0] -ne '200' -and $v6[0] -eq '200') { Write-Warn "IPv6 通、IPv4 不通 —— 多为本机代理或防火墙拦了 IPv4 出口。" } elseif ($envProxy.Count -gt 0) { $sProxy = 'culprit' Write-Err "两条通道都不通,且设置了代理环境变量 —— 大概率是代理的问题。" Write-Host " 在当前窗口执行下面三行后重试(只影响本窗口):" Write-Host ' $env:HTTP_PROXY=""; $env:HTTPS_PROXY=""; $env:ALL_PROXY=""' } else { Write-Warn "两条通道都不通 —— 检查本机网络 / VPN / 防火墙;公司或校园网请换网络重试。" } } # 5. Key 格式 + API 连通 $key = Get-CurrentKey if ([string]::IsNullOrEmpty($key)) { Write-Err "未找到 API Key —— 运行:c2a login" $fails++ } else { if ($key.StartsWith('sk-c2a-') -or $key.StartsWith('sk-tm-') -or $key.StartsWith('sk-br-')) { Write-Ok "API Key 格式正确($(Get-MaskedKey $key))。" } else { Write-Warn "API Key 格式异常($(Get-MaskedKey $key),应以 sk-c2a- 开头)。" } Write-Info "测试 API 连通..." $http = Get-ApiHttpStatus -Key $key $sApi = $http switch ($http) { '200' { Write-Ok "API 连通正常。" } '401' { Write-Err "Key 已失效(HTTP 401)—— 运行:c2a login"; $fails++ } '403' { Write-Err "Key 已失效(HTTP 403)—— 运行:c2a login"; $fails++ } '429' { Write-Warn "当前限流中(HTTP 429),配置本身正常,稍后再试。" } '000' { if ($autoHttp -eq '200') { Write-Err "网络可达但 API 请求失败 —— 可能是安全软件拦截了 POST 请求,或请求超时。" } else { Write-Err "无法连接 $BaseUrl(见上面的网络路径检测)。" } $fails++ } default { Write-Warn "API 返回 HTTP $http;如持续异常请联系 support@code2ai.codes"; $fails++ } } } # 6. 控制台可达 try { $r = Invoke-WebRequest -Uri "$ConsoleUrl/c2a.version" -TimeoutSec 10 -UseBasicParsing $sConsole = [string][int]$r.StatusCode if ([int]$r.StatusCode -eq 200) { Write-Ok "控制台可达($ConsoleUrl)。" } else { Write-Warn "控制台返回 HTTP $([int]$r.StatusCode);自更新可能失败。" } } catch { $sConsole = '000'; Write-Warn "控制台不可达;c2a 自更新可能失败。" } Write-Host "" if ($fails -eq 0) { Write-Ok "自检通过,一切正常。" } else { Write-Err "自检发现 $fails 个问题,请按上面提示处理;仍有疑问联系 support@code2ai.codes" Write-Host "" Write-Info "如果你是在 VS Code / Cursor 里用 Claude Code,请再到编辑器的「集成终端」里跑一次 c2a doctor:" Write-Host " 两边结果不同 = 编辑器自己的代理设置在捣乱(设置里搜 proxy,清空 Http: Proxy 后重启编辑器)" } Write-Host "" Write-Host "诊断摘要(可直接发给客服):" Write-Host " c2a=$C2aVersion os=Windows cli=$sCli net=$sNet proxy=$sProxy api=$sApi console=$sConsole" return $fails } # Windows 上强制走 IPv4 用前缀策略,比改 hosts 好:不写死 IP,服务商换 IP 也不失效。 # 默认策略里 IPv4-mapped(::ffff:0:0/96) 优先级 35,低于 IPv6 的 40;抬到 60 即 IPv4 优先。 function Invoke-FixIpv6 { param([string]$Mode = '') # undo / --undo / -undo 都接受:跨平台文案统一用 undo,避免 PowerShell 的参数绑定坑 if ($Mode -and ($Mode.TrimStart('-') -eq 'undo')) { if (-not (Test-IsAdmin)) { Write-Err "撤销需要管理员权限 —— 请右键「以管理员身份运行」PowerShell 后重试。" return } & netsh interface ipv6 set prefixpolicy ::ffff:0:0/96 35 4 | Out-Null if ($LASTEXITCODE -eq 0) { Write-Ok "已恢复默认策略(IPv6 优先)。重启 Claude Code 后生效。" } else { Write-Err "恢复失败,可手动执行:netsh interface ipv6 set prefixpolicy ::ffff:0:0/96 35 4" } return } Write-Info "先确认两条通道各自的状态..." $v4 = (Test-Health '-4') -split '\|' $v6 = (Test-Health '-6') -split '\|' Write-Host " IPv4 通道 = $($v4[0]) IPv6 通道 = $($v6[0])" if ($v4[0] -ne '200') { Write-Err "IPv4 通道本身就不通(HTTP $($v4[0]))—— 强制走 IPv4 解决不了问题。" Write-Host " 请先运行 c2a doctor 看完整诊断。" return } # IPv6 小包通、大包不通(MTU 黑洞)同样需要切 IPv4,所以这里不因 v6 通就直接劝退 if ($v6[0] -eq '200') { Write-Warn "IPv6 小包测试也是通的。" Write-Host " 但如果你的症状是「刚开始能用、对话一长就断」,那多半是 IPv6 的 MTU 问题,切 IPv4 仍然有效。" $a = Read-Host "继续切换到 IPv4 优先?(y/N)" if ($a -notmatch '^(y|Y)') { Write-Info "已取消。"; return } } if (-not (Test-IsAdmin)) { Write-Err "修改网络策略需要管理员权限。" Write-Host " 请右键点「Windows PowerShell」→「以管理员身份运行」,然后执行:c2a fix-ipv6" Write-Host " 或者直接运行这一条命令:" Write-Host " netsh interface ipv6 set prefixpolicy ::ffff:0:0/96 60 4" return } & netsh interface ipv6 set prefixpolicy ::ffff:0:0/96 60 4 | Out-Null if ($LASTEXITCODE -ne 0) { Write-Err "设置失败,可手动执行:netsh interface ipv6 set prefixpolicy ::ffff:0:0/96 60 4" return } Write-Ok "已设置为 IPv4 优先。" & ipconfig /flushdns | Out-Null Write-Ok "已清空 DNS 缓存。" $after = Test-Health '' $afterIp = ($after -split '\|')[1] if ($afterIp -and $afterIp -notlike '*:*') { Write-Ok "验证通过,现在走 IPv4($afterIp)。" } elseif ($afterIp) { Write-Warn "当前仍解析到 $afterIp,重启终端和 Claude Code 后再看。" } Write-Host "" Write-Info "务必完全退出 Claude Code 再打开(在 VS Code / Cursor 里用的话要整个编辑器退出重开)。" Write-Info "撤销用:c2a fix-ipv6 undo" } function Invoke-Login { $script:ApiKey = '' if (-not (Invoke-DeviceAuthFlow)) { return } if (-not (Write-EnvConfig -Key $script:ApiKey)) { return } Write-Info "验证新配置..." $http = Get-ApiHttpStatus -Key $script:ApiKey if ($http -eq '200') { Write-Ok "API 连通验证成功。" } else { Write-Warn "API 验证返回 HTTP=$http;配置已写入,可稍后运行 c2a doctor 复查。" } Write-Host "" Write-Ok "完成!新配置对新终端自动生效;重开本终端即可加载。" } function Update-ClaudeCli { if (-not (Get-Command claude -ErrorAction SilentlyContinue)) { Write-Warn "Claude CLI 未安装,跳过。" return } Write-Info "更新 Claude CLI..." $npm = Get-Command npm -ErrorAction SilentlyContinue $viaNpm = $false if ($npm) { try { & npm ls -g '@anthropic-ai/claude-code' *>$null; if ($LASTEXITCODE -eq 0) { $viaNpm = $true } } catch {} } if ($viaNpm) { try { & npm update -g '@anthropic-ai/claude-code' 2>&1 | Out-Host; Write-Ok "Claude CLI 已更新。" } catch { Write-Warn "npm 更新失败;可手动执行:npm update -g @anthropic-ai/claude-code" } } else { try { & claude update *>$null; Write-Ok "Claude CLI 已更新。" } catch { Write-Warn "Claude CLI 自更新未成功;可重跑安装命令覆盖安装。" } } } function Update-Self { Write-Info "检查 c2a 更新..." $remote = $null try { $remote = (Invoke-RestMethod -Uri "$ConsoleUrl/c2a.version" -TimeoutSec 10).ToString().Trim() } catch {} if ([string]::IsNullOrWhiteSpace($remote)) { Write-Warn "无法获取版本信息;跳过自更新。"; return } if ($remote -eq $C2aVersion) { Write-Ok "c2a 已是最新($C2aVersion)。"; return } $dest = Join-Path $InstallDir 'c2a.ps1' $tmp = Join-Path $InstallDir ".c2a.ps1.new.$PID" try { $wr = Invoke-WebRequest -Uri "$ConsoleUrl/c2a.ps1" -TimeoutSec 30 -UseBasicParsing $content = $wr.Content # 校验:拒绝空/过短内容和 HTML 错误页 if ([string]::IsNullOrWhiteSpace($content) -or $content.Length -lt 500 -or $content -match '(?i)<\s*(!doctype|html|head|body)\b') { Write-Warn "下载的更新校验失败;跳过。" return } Set-Content -Path $tmp -Value $content -Encoding UTF8 Move-Item -Path $tmp -Destination $dest -Force Write-Ok "c2a 已更新 $C2aVersion -> $remote(下次运行生效)。" } catch { if (Test-Path $tmp) { Remove-Item $tmp -Force -ErrorAction SilentlyContinue } Write-Warn "自更新失败:$($_.Exception.Message)" } } function Invoke-Update { Update-ClaudeCli Update-Self } function Invoke-Uninstall { Write-Host "将执行以下操作:" Write-Host " 1) 移除 code2ai 用户级环境变量" Write-Host " 2) 删除 $InstallDir 下的 c2a" Write-Host " * Claude CLI 本体保留" $answer = Read-Host "确认卸载?(y/N)" if ($answer -notmatch '^(y|Y)') { Write-Info "已取消。"; return } foreach ($v in 'ANTHROPIC_BASE_URL','ANTHROPIC_AUTH_TOKEN','CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC','ENABLE_TOOL_SEARCH') { [System.Environment]::SetEnvironmentVariable($v, $null, 'User') } Write-Ok "环境变量已移除。" foreach ($f in 'c2a.cmd','c2a.ps1') { $p = Join-Path $InstallDir $f if (Test-Path $p) { Remove-Item $p -Force -ErrorAction SilentlyContinue } } Write-Ok "c2a 已卸载。如需卸载 Claude CLI 本体:npm uninstall -g @anthropic-ai/claude-code" } function Show-Menu { $key = Get-CurrentKey $claudeVer = '未安装' if (Get-Command claude -ErrorAction SilentlyContinue) { $claudeVer = (& claude --version 2>$null | Select-Object -First 1) } Write-Host "" Write-Host " +-------------------------------------+" -ForegroundColor Cyan Write-Host " | Code2AI 终端助手 v$C2aVersion |" -ForegroundColor Cyan Write-Host " +-------------------------------------+" -ForegroundColor Cyan Write-Host "" Write-Host " Claude CLI : $claudeVer" Write-Host " API Key : $(Get-MaskedKey $key)" Write-Host " 服务地址 : $BaseUrl" Write-Host "" Write-Host " 1) 环境自检(排查连不上/报错)" Write-Host " 2) 重新登录授权(更换账号 / 密钥失效)" Write-Host " 3) 强制走 IPv4(连不上或对话一长就断时用)" Write-Host " 4) 检查更新(Claude CLI + c2a)" Write-Host " 5) 卸载 c2a" Write-Host " 0) 退出" Write-Host "" $choice = Read-Host "请选择" Write-Host "" switch ($choice) { '1' { Invoke-Doctor | Out-Null } '2' { Invoke-Login } '3' { Invoke-FixIpv6 } '4' { Invoke-Update } '5' { Invoke-Uninstall } default { } } } # ---------- 命令分发 ---------- $Arg1 = if ($Rest -and $Rest.Count -gt 0) { [string]$Rest[0] } else { '' } switch ($Command.ToLower()) { 'doctor' { Invoke-Doctor | Out-Null } 'fix-ipv6' { Invoke-FixIpv6 -Mode $Arg1 } 'login' { Invoke-Login } 'update' { Invoke-Update } 'uninstall' { Invoke-Uninstall } 'version' { Write-Host "c2a $C2aVersion" } '-v' { Write-Host "c2a $C2aVersion" } '--version' { Write-Host "c2a $C2aVersion" } '' { Show-Menu } default { Write-Err "未知命令:$Command" Write-Host "可用命令:c2a [doctor|fix-ipv6|login|update|uninstall|version]" } }