################################################################################ # 闪电API - Claude Code 一键安装脚本 (Windows PowerShell) # # 功能: 安装 Claude Code CLI / Claude 桌面版 + 配置 API # 官网: flashapi.top # 模型调用: ai.flashapi.top (国内直连) / cdn.flashapi.top (海外CDN) # # 用法 (PowerShell): irm https://sh.flashapi.top/claudecode.ps1 | iex # # 兼容 Windows PowerShell 5.1 与 PowerShell 7+;不需要管理员权限,所有操作在用户目录下完成 ################################################################################ & { $ErrorActionPreference = 'Stop' $ProgressPreference = 'SilentlyContinue' try { [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 } catch {} try { [Console]::OutputEncoding = [Text.Encoding]::UTF8 } catch {} ############################################################################## # 常量定义 ############################################################################## $OnWindows = ([Environment]::OSVersion.Platform -eq [PlatformID]::Win32NT) $UserHome = $env:USERPROFILE if (-not $UserHome) { $UserHome = $HOME } $LocalAppData = $env:LOCALAPPDATA if (-not $LocalAppData) { $LocalAppData = Join-Path (Join-Path $UserHome 'AppData') 'Local' } $ClaudeDir = Join-Path $UserHome '.claude' $ClaudeConfigFile = Join-Path $ClaudeDir 'settings.json' $ClaudeJsonFile = Join-Path $UserHome '.claude.json' $NpmMirror = 'https://registry.npmmirror.com' $DefaultModel = 'claude-opus-5-5' $DefaultEffort = 'medium' # Claude 桌面版第三方推理 (3P gateway) 配置 # 目录结构参考 CC Switch: Claude\ 与 Claude-3p\ 下的 claude_desktop_config.json, # 以及 Claude-3p\configLibrary\_meta.json + .json $DesktopProfileId = '00000000-0000-4000-8000-00f1a5ba0001' $DesktopProfileName = '闪电API' $DesktopPackageFamily = 'Claude_pzs8sxrjxfjjc' # 可变状态放在哈希表里 (子函数可直接修改,且不污染用户会话) $S = @{ UsageTarget = '' UsageTargetLabel = '' DesktopNormalDir = '' DesktopThreepDir = '' } $Line = '───────────────────────────────────────────────────────' ############################################################################## # 工具函数 ############################################################################## # 输出带颜色的一行: Say '文本' '颜色' '文本' '颜色' ... (颜色为空 = 默认色) function Write-Parts { param([object[]]$Parts, [bool]$NoNewline) if ($null -ne $Parts) { for ($i = 0; $i -lt $Parts.Count; $i += 2) { $text = [string]$Parts[$i] $color = '' if ($i + 1 -lt $Parts.Count) { $color = [string]$Parts[$i + 1] } if ($color) { Write-Host $text -NoNewline -ForegroundColor $color } else { Write-Host $text -NoNewline } } } if (-not $NoNewline) { Write-Host '' } } function Say { Write-Parts -Parts $args -NoNewline $false } function SayN { Write-Parts -Parts $args -NoNewline $true } function Read-Line { # 读取一行输入 (与 bash 的 read -r 一样去掉首尾空白);输入流结束时返回 $null $v = Read-Host if ($null -eq $v) { return $null } return ([string]$v).Trim() } function Read-YesNo { param([string]$Prompt, [bool]$Default = $false) SayN " $Prompt" 'Yellow' $a = Read-Line if ($Default -and -not $a) { return $true } return ($a -eq 'y' -or $a -eq 'Y') } function Print-Banner { Say '' Say ' ' '' ' ⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡' 'Yellow' Say ' ' '' ' ███████╗██╗ █████╗ ███████╗██╗ ██╗' 'Yellow' Say ' ' '' ' ██╔════╝██║ ██╔══██╗██╔════╝██║ ██║' 'Yellow' Say ' ' '' ' █████╗ ██║ ███████║███████╗███████║' 'Yellow' Say ' ' '' ' ██╔══╝ ██║ ██╔══██║╚════██║██╔══██║' 'Yellow' Say ' ' '' ' ██║ ███████╗██║ ██║███████║██║ ██║' 'Yellow' Say ' ' '' ' ╚═╝ ╚══════╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝' 'Yellow' Say ' ' '' ' ⚡ A P I ⚡' 'Blue' Say ' ' '' ' ⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡' 'Yellow' Say '' Say " $Line" 'DarkGray' Say ' ' '' '⚡ 闪电API' 'White' ' ' '' '|' 'DarkGray' ' ' '' '官网' 'Cyan' ' ' '' 'flashapi.top' 'Yellow' Say ' ' '' 'Claude Code CLI / 桌面版 一键安装' 'White' ' ' '' '|' 'DarkGray' ' ' '' '让顶尖模型触手可及' 'Cyan' Say " $Line" 'DarkGray' Say ' ' '' '国内直连:' 'DarkGray' ' ' '' 'ai.flashapi.top' 'Green' ' ' '' '海外CDN:' 'DarkGray' ' ' '' 'cdn.flashapi.top' 'Green' Say " $Line" 'DarkGray' Say '' } function Print-Status { param([string]$Type, [string]$Message) switch ($Type) { 'ok' { Say ' ' '' '[⚡]' 'Green' " $Message" '' } 'info' { Say ' ' '' '[*]' 'Cyan' " $Message" '' } 'warn' { Say ' ' '' '[!]' 'Yellow' " $Message" '' } 'error' { Say ' ' '' '[x]' 'Red' " $Message" '' } default { Say " $Message" } } } # 读取密码输入 (星号遮盖) function Read-Secret { param([string]$Prompt) SayN $Prompt 'White' $sec = Read-Host -AsSecureString if ($null -eq $sec -or $sec.Length -eq 0) { return '' } $plain = '' $bstr = [IntPtr]::Zero try { $bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($sec) $plain = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr) } catch { $plain = (New-Object System.Management.Automation.PSCredential('x', $sec)).GetNetworkCredential().Password } finally { if ($bstr -ne [IntPtr]::Zero) { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) } } if ($null -eq $plain) { return '' } return $plain.Trim() } function Mask-Key { param([string]$Value) if (-not $Value) { return '' } if ($Value.Length -le 12) { return $Value } return $Value.Substring(0, 8) + '...' + $Value.Substring($Value.Length - 4) } function Backup-IfExists { param([string]$File) if (Test-Path -LiteralPath $File -PathType Leaf) { $backup = $File + '.bak.' + (Get-Date -Format 'yyyyMMddHHmmss') Copy-Item -LiteralPath $File -Destination $backup -Force Print-Status 'info' "已备份现有文件: $backup" } } function Ensure-Dir { param([string]$Dir) if (-not (Test-Path -LiteralPath $Dir -PathType Container)) { New-Item -ItemType Directory -Path $Dir -Force | Out-Null } } # 写文件: UTF-8 无 BOM,LF 换行 function Write-Utf8File { param([string]$Path, [string]$Text) [IO.File]::WriteAllText($Path, $Text, (New-Object Text.UTF8Encoding $false)) } # 在 PATH 中查找可执行文件 (优先 .exe/.cmd,避开受执行策略限制的 .ps1 shim) function Find-Exe { param([string[]]$Names) foreach ($n in $Names) { $c = Get-Command $n -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1 if ($null -ne $c) { return $c.Path } } return $null } function Get-ExeVersion { param([string]$Exe) $ErrorActionPreference = 'Continue' $out = $null try { $out = & $Exe --version 2>$null | Select-Object -First 1 } catch { $out = $null } if ($out) { return ([string]$out).Trim() } return 'unknown' } function Add-SessionPath { param([string]$Dir) if (-not (Test-Path -LiteralPath $Dir -PathType Container)) { return } $sep = [IO.Path]::PathSeparator $parts = @($env:PATH -split [regex]::Escape([string]$sep)) if ($parts -notcontains $Dir) { $env:PATH = $Dir + $sep + $env:PATH } } ############################################################################## # JSON 工具 (PSCustomObject 读写;输出格式与 node 的 JSON.stringify(x, null, 2) 一致) ############################################################################## $JsonDateKind = $false try { $JsonDateKind = (Get-Command ConvertFrom-Json).Parameters.ContainsKey('DateKind') } catch {} function Test-JsonObject { param($Value) return ($null -ne $Value -and $Value.GetType().FullName -eq 'System.Management.Automation.PSCustomObject') } # 读取 JSON 文件;不存在/解析失败/顶层不是对象时返回 $null function Read-JsonObject { param([string]$Path) if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { return $null } try { $text = [IO.File]::ReadAllText($Path) if (-not $text.Trim()) { return $null } $opts = @{} if ($JsonDateKind) { $opts['DateKind'] = 'String' } $obj = ConvertFrom-Json -InputObject $text @opts if (Test-JsonObject $obj) { return $obj } } catch {} return $null } function New-JsonObject { return (New-Object -TypeName PSObject) } function Get-Prop { param($Obj, [string]$Name) if ($null -eq $Obj) { return $null } $p = $Obj.PSObject.Properties[$Name] if ($null -eq $p) { return $null } return $p.Value } function Has-Prop { param($Obj, [string]$Name) return ($null -ne $Obj.PSObject.Properties[$Name]) } function Set-Prop { param($Obj, [string]$Name, $Value) $p = $Obj.PSObject.Properties[$Name] if ($null -ne $p) { $p.Value = $Value } else { Add-Member -InputObject $Obj -MemberType NoteProperty -Name $Name -Value $Value } } function Remove-Prop { param($Obj, [string]$Name) if ($null -ne $Obj.PSObject.Properties[$Name]) { $Obj.PSObject.Properties.Remove($Name) } } function ConvertTo-JsonString { param([string]$s) $sb = New-Object System.Text.StringBuilder [void]$sb.Append('"') foreach ($ch in $s.ToCharArray()) { $code = [int]$ch switch ($code) { 34 { [void]$sb.Append('\"') } 92 { [void]$sb.Append('\\') } 8 { [void]$sb.Append('\b') } 12 { [void]$sb.Append('\f') } 10 { [void]$sb.Append('\n') } 13 { [void]$sb.Append('\r') } 9 { [void]$sb.Append('\t') } default { if ($code -lt 32) { [void]$sb.Append('\u' + $code.ToString('x4')) } else { [void]$sb.Append($ch) } } } } [void]$sb.Append('"') return $sb.ToString() } function ConvertTo-JsonText { param($Value, [int]$Level = 0) $inv = [Globalization.CultureInfo]::InvariantCulture if ($null -eq $Value) { return 'null' } if ($Value -is [string]) { return (ConvertTo-JsonString $Value) } if ($Value -is [char]) { return (ConvertTo-JsonString ([string]$Value)) } if ($Value -is [bool]) { if ($Value) { return 'true' } else { return 'false' } } if ($Value -is [DateTime]) { return (ConvertTo-JsonString ($Value.ToString('yyyy-MM-ddTHH:mm:ss.FFFFFFFK', $inv))) } if ($Value -is [DateTimeOffset]) { return (ConvertTo-JsonString ($Value.ToString('yyyy-MM-ddTHH:mm:ss.FFFFFFFzzz', $inv))) } if ($Value -is [double] -or $Value -is [float]) { $d = [double]$Value if ([double]::IsNaN($d) -or [double]::IsInfinity($d)) { return 'null' } return $d.ToString('R', $inv) } if ($Value -is [decimal] -or $Value -is [int] -or $Value -is [long] -or $Value -is [int16] -or $Value -is [byte] -or $Value -is [sbyte] -or $Value -is [uint16] -or $Value -is [uint32] -or $Value -is [uint64]) { return ([IFormattable]$Value).ToString($null, $inv) } if ($Value.GetType().FullName -eq 'System.Numerics.BigInteger') { return $Value.ToString() } $pad = ' ' * ($Level + 1) $endPad = ' ' * $Level if ($Value -is [Collections.IDictionary]) { $items = New-Object System.Collections.Generic.List[string] foreach ($k in $Value.Keys) { $items.Add($pad + (ConvertTo-JsonString ([string]$k)) + ': ' + (ConvertTo-JsonText $Value[$k] ($Level + 1))) } if ($items.Count -eq 0) { return '{}' } return "{`n" + ($items -join ",`n") + "`n" + $endPad + '}' } if (Test-JsonObject $Value) { $items = New-Object System.Collections.Generic.List[string] foreach ($p in $Value.PSObject.Properties) { $items.Add($pad + (ConvertTo-JsonString $p.Name) + ': ' + (ConvertTo-JsonText $p.Value ($Level + 1))) } if ($items.Count -eq 0) { return '{}' } return "{`n" + ($items -join ",`n") + "`n" + $endPad + '}' } if ($Value -is [Collections.IEnumerable]) { $items = New-Object System.Collections.Generic.List[string] foreach ($e in $Value) { $items.Add($pad + (ConvertTo-JsonText $e ($Level + 1))) } if ($items.Count -eq 0) { return '[]' } return "[`n" + ($items -join ",`n") + "`n" + $endPad + ']' } return (ConvertTo-JsonString ([string]$Value)) } function Write-JsonFile { param([string]$Path, $Data) Write-Utf8File $Path ((ConvertTo-JsonText $Data 0) + "`n") } ############################################################################## # 功能 1: 安装 Claude Code ############################################################################## function Get-ClaudeExe { $exe = Find-Exe @('claude.exe', 'claude.cmd', 'claude') if ($exe) { return $exe } $local = Join-Path (Join-Path (Join-Path $UserHome '.local') 'bin') 'claude.exe' if (Test-Path -LiteralPath $local -PathType Leaf) { return $local } return $null } # 已安装返回版本号,未安装返回 $null function Check-ClaudeInstalled { $exe = Get-ClaudeExe if (-not $exe) { return $null } return (Get-ExeVersion $exe) } function Invoke-NpmInstall { param([string]$Npm, [string]$Registry) $ErrorActionPreference = 'Continue' try { if ($Registry) { & $Npm install -g '@anthropic-ai/claude-code' "--registry=$Registry" | Out-Host } else { & $Npm install -g '@anthropic-ai/claude-code' 2>$null | Out-Host } return ($LASTEXITCODE -eq 0) } catch { return $false } } function Install-WithMirror { param([string]$Npm) Print-Status 'info' '切换国内镜像 (npmmirror.com) 安装 ...' return (Invoke-NpmInstall $Npm $NpmMirror) } # 官方安装器 (irm https://claude.ai/install.ps1 | iex) 在子进程中运行: # 它内部失败时会调用 exit,放在子进程里不会关掉当前窗口 function Invoke-OfficialInstaller { $ErrorActionPreference = 'Continue' $psExe = $null try { $psExe = (Get-Process -Id $PID).Path } catch {} if (-not $psExe) { $psExe = 'powershell.exe' } $cmd = '[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12; irm https://claude.ai/install.ps1 | iex' try { & $psExe -NoProfile -ExecutionPolicy Bypass -Command $cmd | Out-Host return ($LASTEXITCODE -eq 0) } catch { return $false } } function Find-GitBash { $candidates = New-Object System.Collections.Generic.List[string] $git = Find-Exe @('git.exe') if ($git) { # ...\Git\cmd\git.exe 或 ...\Git\bin\git.exe 或 ...\Git\mingw64\bin\git.exe $dir = Split-Path -Parent $git $root = Split-Path -Parent $dir $candidates.Add((Join-Path (Join-Path $root 'bin') 'bash.exe')) $candidates.Add((Join-Path (Join-Path (Split-Path -Parent $root) 'bin') 'bash.exe')) } foreach ($base in @($env:ProgramFiles, ${env:ProgramFiles(x86)}, (Join-Path $LocalAppData 'Programs'))) { if ($base) { $candidates.Add((Join-Path (Join-Path (Join-Path $base 'Git') 'bin') 'bash.exe')) } } foreach ($c in $candidates) { if (Test-Path -LiteralPath $c -PathType Leaf) { return $c } } return $null } function Show-GitBashHint { if (-not $OnWindows) { return } $bash = Find-GitBash if ($bash) { Print-Status 'ok' "检测到 Git Bash: $bash" Print-Status 'info' "如 Claude Code 提示找不到 Git Bash,可在 settings.json 的 env 中设置 CLAUDE_CODE_GIT_BASH_PATH 为上面的路径" } else { Print-Status 'info' '未检测到 Git for Windows (可选):Claude Code 将使用 PowerShell 执行命令' Print-Status 'info' '如需 Bash 工具,可安装 Git for Windows: https://git-scm.com/downloads/win (或 winget install --id Git.Git -e)' } } function Install-ClaudeCode { Say '' Print-Status 'info' '⚡ 正在安装 Claude Code ...' Say '' if ($OnWindows) { Print-Status 'info' '检测到 Windows (PowerShell) 环境' } $ver = Check-ClaudeInstalled if ($null -ne $ver) { Print-Status 'ok' "Claude Code 已安装 ($ver)" Say '' if (-not (Read-YesNo '是否重新安装? (y/N): ')) { Print-Status 'info' '跳过安装'; return $true } } Print-Status 'info' '调用官方安装器 (irm https://claude.ai/install.ps1 | iex) ...' Say '' $installed = Invoke-OfficialInstaller if (-not $installed) { Print-Status 'warn' '官方安装器失败,尝试 npm 安装 ...' $npm = Find-Exe @('npm.cmd', 'npm') if (-not $npm) { Print-Status 'error' '未找到 npm,请先安装 Node.js' Say '' Say ' ' '' '安装方式:' 'White' Say ' ' '' '官网下载:' 'Cyan' ' https://nodejs.org/' '' Say ' ' '' 'winget:' 'Cyan' ' winget install --id OpenJS.NodeJS.LTS -e' '' return $false } $npmVer = Get-ExeVersion $npm Print-Status 'ok' "npm 已就绪 ($npmVer)" if (-not (Invoke-NpmInstall $npm '')) { Print-Status 'warn' '默认源也失败了,可能无法访问国外网络' if (-not (Install-WithMirror $npm)) { Print-Status 'error' '所有安装方式均失败,请检查网络连接' return $false } } } Say '' Add-SessionPath (Join-Path (Join-Path $UserHome '.local') 'bin') $ver = Check-ClaudeInstalled if ($null -ne $ver) { Print-Status 'ok' "⚡ Claude Code 安装成功 ($ver)" } else { Print-Status 'warn' '安装已完成,但未在 PATH 中找到 claude' Print-Status 'info' '请重新打开终端后再试' } Show-GitBashHint return $true } ############################################################################## # 功能 1b: 安装 Claude 桌面版 ############################################################################## # 与 CC Switch 的 pick_windows_claude_dir 一致: # 精确目录存在则用之;否则取 LOCALAPPDATA 下以 "Claude" 开头、是否含 "-3p" 匹配的第一个目录 (按名称排序) function Pick-ClaudeDir { param([bool]$Threep) $exactName = 'Claude' if ($Threep) { $exactName = 'Claude-3p' } $exact = Join-Path $LocalAppData $exactName if (Test-Path -LiteralPath $exact) { return $exact } if (-not (Test-Path -LiteralPath $LocalAppData -PathType Container)) { return $null } $names = New-Object System.Collections.Generic.List[string] foreach ($d in @(Get-ChildItem -LiteralPath $LocalAppData -Force -ErrorAction SilentlyContinue | Where-Object { $_.PSIsContainer })) { $n = $d.Name if ($n.StartsWith('Claude', [StringComparison]::Ordinal) -and ($n.Contains('-3p') -eq $Threep)) { $names.Add($n) } } if ($names.Count -eq 0) { return $null } $arr = $names.ToArray() [Array]::Sort($arr, [StringComparer]::Ordinal) return (Join-Path $LocalAppData $arr[0]) } # 解析 Claude 桌面版配置目录 (与 CC Switch 的 current_platform_paths 保持一致) function Resolve-DesktopDirs { if (-not $LocalAppData) { return $false } $normal = Pick-ClaudeDir $false if (-not $normal) { $normal = Join-Path $LocalAppData 'Claude' } $threep = Pick-ClaudeDir $true if (-not $threep) { $threep = Join-Path $LocalAppData 'Claude-3p' } $S.DesktopNormalDir = $normal $S.DesktopThreepDir = $threep return $true } # 桌面版安装形态: # MSIX — 新版安装器 (包名 Claude_pzs8sxrjxfjjc),Cowork 依赖它注册 CoworkVMService # Squirrel — 旧版安装器 (%LOCALAPPDATA%\AnthropicClaude),winget 的 Anthropic.Claude 装的也是这个; # 此形态下 Cowork 不可用,会一直提示 "Reinstall the desktop app to access Cowork" function Test-DesktopMsix { try { $pkgs = @(Get-AppxPackage -Name 'Claude' -ErrorAction Stop | Where-Object { $_.PackageFamilyName -eq $DesktopPackageFamily }) return ($pkgs.Count -gt 0) } catch { # PowerShell 7 等环境下 Appx 模块不可用时,退回检查包数据目录 return (Test-Path -LiteralPath (Join-Path (Join-Path $LocalAppData 'Packages') $DesktopPackageFamily) -PathType Container) } } function Test-DesktopSquirrel { return (Test-Path -LiteralPath (Join-Path $LocalAppData 'AnthropicClaude') -PathType Container) } function Check-DesktopInstalled { return ((Test-DesktopMsix) -or (Test-DesktopSquirrel)) } function Open-Url { param([string]$Url) try { Start-Process $Url | Out-Null; return $true } catch { return $false } } function Get-DesktopMsixUrl { $arch = $env:PROCESSOR_ARCHITEW6432 if (-not $arch) { $arch = $env:PROCESSOR_ARCHITECTURE } if ($arch -eq 'ARM64') { return 'https://claude.ai/api/desktop/win32/arm64/msix/latest/redirect' } return 'https://claude.ai/api/desktop/win32/x64/msix/latest/redirect' } function Install-DesktopManually { $url = Get-DesktopMsixUrl Print-Status 'info' '请在浏览器中下载 MSIX 安装包,双击安装 (需要管理员权限):' Print-Status 'info' $url Print-Status 'warn' '不要使用 winget / 旧的 .exe 安装包,它们装的是旧版 (Squirrel),Cowork 无法使用' Open-Url $url | Out-Null } # 卸载旧版 (Squirrel) 桌面版;配置目录 Claude\ 与 Claude-3p\ 不受影响 function Uninstall-DesktopSquirrel { $root = Join-Path $LocalAppData 'AnthropicClaude' $ErrorActionPreference = 'Continue' # 只结束旧版目录下的进程,避免误杀 Claude Code CLI (同名 claude.exe) foreach ($p in @(Get-Process -Name 'claude' -ErrorAction SilentlyContinue)) { try { if ($p.Path -and $p.Path.StartsWith($root, [StringComparison]::OrdinalIgnoreCase)) { $p.Kill() } } catch {} } Start-Sleep -Seconds 2 $update = Join-Path $root 'Update.exe' if (Test-Path -LiteralPath $update -PathType Leaf) { try { Start-Process -FilePath $update -ArgumentList '--uninstall' -Wait | Out-Null } catch {} } Remove-ItemProperty -Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Run' -Name 'Claude' -ErrorAction SilentlyContinue if (Test-Path -LiteralPath $root) { Remove-Item -LiteralPath $root -Recurse -Force -ErrorAction SilentlyContinue } $ErrorActionPreference = 'Stop' if (Test-DesktopSquirrel) { Print-Status 'warn' "旧版目录未能完全删除: $root (可在退出 Claude 后手动删除)" } else { Print-Status 'ok' '已卸载旧版 (Squirrel) 桌面版' } } function Install-DesktopWindows { $url = Get-DesktopMsixUrl $msix = Join-Path ([IO.Path]::GetTempPath()) 'Claude-flashapi.msix' Print-Status 'info' '正在下载 Claude 桌面版 MSIX 安装包 ...' try { Invoke-WebRequest -Uri $url -OutFile $msix -UseBasicParsing } catch { Print-Status 'warn' "下载失败: $($_.Exception.Message)" Install-DesktopManually return $false } if (Test-DesktopSquirrel) { Uninstall-DesktopSquirrel } # MSIX 包含 CoworkVMService 服务,必须以管理员身份安装 (否则 0x80073D28) Print-Status 'info' '正在安装 (将弹出管理员权限确认,请点击「是」) ...' $ps = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe' $cmd = "try { Add-AppxPackage -Path '" + ($msix -replace "'", "''") + "' -ForceApplicationShutdown; exit 0 } catch { Write-Host `$_; Start-Sleep -Seconds 8; exit 1 }" $code = 1 try { $proc = Start-Process -FilePath $ps -ArgumentList @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', $cmd) -Verb RunAs -Wait -PassThru $code = $proc.ExitCode } catch { Print-Status 'warn' '未获得管理员权限' } if ($code -eq 0 -and (Test-DesktopMsix)) { Remove-Item -LiteralPath $msix -Force -ErrorAction SilentlyContinue return $true } if ($code -eq 0) { Print-Status 'warn' '安装命令已完成,但当前用户下未检测到 Claude (MSIX),请确认管理员授权使用的是当前账户' } else { Print-Status 'warn' 'MSIX 安装失败' } Print-Status 'info' "安装包已保存在: $msix ,可双击手动安装" return $false } function Install-ClaudeDesktop { Say '' Print-Status 'info' '⚡ 正在安装 Claude 桌面版 ...' Say '' $hasMsix = Test-DesktopMsix $hasSquirrel = Test-DesktopSquirrel if ($hasSquirrel) { Print-Status 'warn' '检测到旧版安装器 (Squirrel) 安装的桌面版,Cowork 无法使用' Print-Status 'info' '(即提示 "Reinstall the desktop app to access Cowork"),需要换成新版 MSIX 安装' Print-Status 'info' '已写入的 API 配置会保留,聊天记录保存在云端不受影响' Say '' if (-not (Read-YesNo '是否现在迁移到新版? (Y/n): ' $true)) { Print-Status 'info' '跳过安装'; return $true } if ($hasMsix) { # 新版已装好,只需清掉旧版,避免开始菜单/开机自启仍然打开旧版 Uninstall-DesktopSquirrel Print-Status 'ok' '⚡ 已切换到新版 Claude 桌面版' return $true } } elseif ($hasMsix) { Print-Status 'ok' 'Claude 桌面版已安装' Say '' if (-not (Read-YesNo '是否重新安装? (y/N): ')) { Print-Status 'info' '跳过安装'; return $true } } $ok = Install-DesktopWindows Say '' if ($ok) { Print-Status 'ok' '⚡ Claude 桌面版安装完成' Print-Status 'info' '如 Cowork 仍不可用,请在「启用或关闭 Windows 功能」中开启「虚拟机平台」后重启电脑' } else { Print-Status 'info' '安装完成后,回到本脚本执行「配置」即可写入 API' } return $ok } function Install-Selected { $ok = $true if ($S.UsageTarget -eq 'cli' -or $S.UsageTarget -eq 'all') { if (-not (Install-ClaudeCode)) { $ok = $false } } if ($S.UsageTarget -eq 'all') { Say '' Say " $Line" 'DarkGray' } if ($S.UsageTarget -eq 'desktop' -or $S.UsageTarget -eq 'all') { if (-not (Install-ClaudeDesktop)) { $ok = $false } } return $ok } ############################################################################## # 功能 2: 配置 API ############################################################################## function Choose-UsageTarget { Say ' ' '' '请选择要使用的 Claude 客户端:' 'White' Say '' Say ' ' '' '⚡' 'Yellow' ' ' '' '1)' 'Cyan' ' Claude Code CLI' '' Say ' ' '' '⚡' 'Yellow' ' ' '' '2)' 'Cyan' ' Claude 桌面版 (Claude Desktop)' '' Say ' ' '' '⚡' 'Yellow' ' ' '' '回车' 'Cyan' ' CLI 和桌面版都使用' '' Say '' SayN ' ' '' '请输入选项 [回车=全部]: ' 'White' $choice = Read-Line switch ($choice) { '' { $S.UsageTarget = 'all'; $S.UsageTargetLabel = 'Claude Code CLI / 桌面版' } '1' { $S.UsageTarget = 'cli'; $S.UsageTargetLabel = 'Claude Code CLI' } '2' { $S.UsageTarget = 'desktop'; $S.UsageTargetLabel = 'Claude 桌面版' } default { Print-Status 'error' '无效选项,已取消' return $false } } Say '' Print-Status 'ok' "已选择: $($S.UsageTargetLabel)" return $true } # 返回 HTTP 状态码;网络错误返回 0 function Get-HttpStatus { param([string]$Uri, [hashtable]$Headers) try { $r = Invoke-WebRequest -Uri $Uri -Headers $Headers -Method Get -UseBasicParsing -TimeoutSec 10 return [int]$r.StatusCode } catch { $ex = $_.Exception $resp = $null if ($null -ne $ex -and $null -ne $ex.PSObject.Properties['Response']) { $resp = $ex.Response } if ($null -ne $resp) { try { return [int]$resp.StatusCode } catch { return 0 } } return 0 } } function Get-HttpBody { param([string]$Uri, [hashtable]$Headers) try { $r = Invoke-WebRequest -Uri $Uri -Headers $Headers -Method Get -UseBasicParsing -TimeoutSec 10 return [string]$r.Content } catch { return '' } } function Get-AnthropicHeaders { param([string]$ApiKey) return @{ 'x-api-key' = $ApiKey; 'anthropic-version' = '2023-06-01' } } function Validate-ApiKey { param([string]$BaseUrl, [string]$ApiKey) Print-Status 'info' '正在验证 API Key ...' $code = Get-HttpStatus "$BaseUrl/v1/models" (Get-AnthropicHeaders $ApiKey) if ($code -eq 200) { Print-Status 'ok' '⚡ API Key 验证通过,模型列表获取成功' return $true } elseif ($code -eq 401) { Print-Status 'error' 'API Key 无效 (401 Unauthorized)' return $false } elseif ($code -eq 0) { Print-Status 'warn' "无法连接到 $BaseUrl (网络错误)" return $false } else { Print-Status 'warn' "API 返回状态码: $code (可能仍可用)" return $true } } function Write-ClaudeConfig { param([string]$BaseUrl, [string]$ApiKey) Ensure-Dir $ClaudeDir Backup-IfExists $ClaudeConfigFile $data = Read-JsonObject $ClaudeConfigFile if ($null -eq $data) { $data = New-JsonObject } $envObj = Get-Prop $data 'env' if (-not (Test-JsonObject $envObj)) { $envObj = New-JsonObject } Set-Prop $envObj 'ANTHROPIC_BASE_URL' $BaseUrl Set-Prop $envObj 'ANTHROPIC_AUTH_TOKEN' $ApiKey Set-Prop $envObj 'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC' '1' Set-Prop $data 'env' $envObj Set-Prop $data 'model' $DefaultModel Set-Prop $data 'effortLevel' $DefaultEffort Write-JsonFile $ClaudeConfigFile $data } function Write-ClaudeOnboarding { if (-not (Test-Path -LiteralPath $ClaudeJsonFile -PathType Leaf)) { Write-Utf8File $ClaudeJsonFile "{`n `"hasCompletedOnboarding`": true`n}`n" Print-Status 'info' "已创建 $ClaudeJsonFile" return } $data = Read-JsonObject $ClaudeJsonFile if ($null -eq $data) { # 现有文件无法解析:先备份再重写,避免丢数据 Backup-IfExists $ClaudeJsonFile $data = New-JsonObject } Set-Prop $data 'hasCompletedOnboarding' $true Write-JsonFile $ClaudeJsonFile $data Print-Status 'info' "已更新 $ClaudeJsonFile" } # 从 /v1/models 中挑出桌面版可识别的模型 ID (claude-sonnet-/opus-/haiku-/fable-*),逗号分隔返回 # 桌面版对 inferenceModels 是整组校验,任何一个不合法都会整组拒收,所以必须过滤 function Fetch-DesktopModels { param([string]$BaseUrl, [string]$ApiKey) $body = Get-HttpBody "$BaseUrl/v1/models" (Get-AnthropicHeaders $ApiKey) # 拿不到列表时也写入默认模型,保证桌面版默认选中默认模型 if (-not $body) { return $DefaultModel } $ids = New-Object System.Collections.Generic.List[string] foreach ($m in [regex]::Matches($body, '"id"\s*:\s*"([^"]*)"')) { $id = $m.Groups[1].Value if ($id -cmatch '^(anthropic/)?claude-(sonnet|opus|haiku|fable)-\S+$' -and -not $id.ToLowerInvariant().Contains('[1m]') -and $id -cne $DefaultModel -and -not $ids.Contains($id)) { $ids.Add($id) } } $sorted = @($ids | Sort-Object) return (@($DefaultModel) + $sorted) -join ',' } function Write-ClaudeDesktopConfig { param([string]$BaseUrl, [string]$ApiKey, [string]$Models) $normalFile = Join-Path $S.DesktopNormalDir 'claude_desktop_config.json' $threepFile = Join-Path $S.DesktopThreepDir 'claude_desktop_config.json' $libraryDir = Join-Path $S.DesktopThreepDir 'configLibrary' $profileFile = Join-Path $libraryDir ($DesktopProfileId + '.json') $metaFile = Join-Path $libraryDir '_meta.json' Ensure-Dir $S.DesktopNormalDir Ensure-Dir $libraryDir Backup-IfExists $normalFile Backup-IfExists $threepFile Backup-IfExists $metaFile # 1. 两份主配置都切到第三方推理模式 foreach ($file in @($normalFile, $threepFile)) { $data = Read-JsonObject $file if ($null -eq $data) { $data = New-JsonObject } Set-Prop $data 'deploymentMode' '3p' Write-JsonFile $file $data } # 2. gateway profile: 连接字段覆盖,策略字段仅在缺失时写入 $prof = Read-JsonObject $profileFile if ($null -eq $prof) { $prof = New-JsonObject } Set-Prop $prof 'inferenceProvider' 'gateway' Set-Prop $prof 'inferenceGatewayBaseUrl' $BaseUrl Set-Prop $prof 'inferenceGatewayApiKey' $ApiKey Set-Prop $prof 'inferenceGatewayAuthScheme' 'bearer' if (-not (Has-Prop $prof 'disableDeploymentModeChooser')) { Set-Prop $prof 'disableDeploymentModeChooser' $true } if (-not (Has-Prop $prof 'coworkEgressAllowedHosts')) { Set-Prop $prof 'coworkEgressAllowedHosts' ([object[]]@('*')) } $modelList = New-Object System.Collections.Generic.List[object] foreach ($m in ($Models -split ',')) { if ($m) { $modelList.Add($m) } } if ($modelList.Count -gt 0) { Set-Prop $prof 'inferenceModels' ($modelList.ToArray()) } else { Remove-Prop $prof 'inferenceModels' } Write-JsonFile $profileFile $prof # 3. _meta.json: 登记 profile 并设为当前生效 $meta = Read-JsonObject $metaFile if ($null -eq $meta) { $meta = New-JsonObject } $entries = New-Object System.Collections.Generic.List[object] $oldEntries = Get-Prop $meta 'entries' if ($null -ne $oldEntries -and $oldEntries -is [Array]) { foreach ($e in $oldEntries) { $entries.Add($e) } } $found = $false foreach ($e in $entries) { if ((Test-JsonObject $e) -and ((Get-Prop $e 'id') -ceq $DesktopProfileId)) { Set-Prop $e 'name' $DesktopProfileName $found = $true break } } if (-not $found) { $entry = New-JsonObject Set-Prop $entry 'id' $DesktopProfileId Set-Prop $entry 'name' $DesktopProfileName $entries.Add($entry) } Set-Prop $meta 'entries' ($entries.ToArray()) Set-Prop $meta 'appliedId' $DesktopProfileId Write-JsonFile $metaFile $meta # 4. 旧一些的 MSIX 版本会把 %LOCALAPPDATA% 虚拟化到包的 LocalCache 下读取配置; # 新版 (2.31226+) 已将 Claude-3p 排除虚拟化直接读真实路径。包目录存在时两边都写一份 $cache = Join-Path (Join-Path (Join-Path $LocalAppData 'Packages') $DesktopPackageFamily) 'LocalCache\Local' if (Test-Path -LiteralPath (Split-Path -Parent $cache) -PathType Container) { $pairs = @( @($normalFile, (Join-Path (Join-Path $cache (Split-Path -Leaf $S.DesktopNormalDir)) 'claude_desktop_config.json')), @($threepFile, (Join-Path (Join-Path $cache (Split-Path -Leaf $S.DesktopThreepDir)) 'claude_desktop_config.json')), @($profileFile, (Join-Path (Join-Path (Join-Path $cache (Split-Path -Leaf $S.DesktopThreepDir)) 'configLibrary') ($DesktopProfileId + '.json'))), @($metaFile, (Join-Path (Join-Path (Join-Path $cache (Split-Path -Leaf $S.DesktopThreepDir)) 'configLibrary') '_meta.json')) ) foreach ($pair in $pairs) { Ensure-Dir (Split-Path -Parent $pair[1]) Backup-IfExists $pair[1] Copy-Item -LiteralPath $pair[0] -Destination $pair[1] -Force } } } function Read-DesktopProfileValue { param([string]$Key) if (-not $S.DesktopThreepDir) { return '' } $file = Join-Path (Join-Path $S.DesktopThreepDir 'configLibrary') ($DesktopProfileId + '.json') $data = Read-JsonObject $file $v = Get-Prop $data $Key if ($v -is [string]) { return $v } return '' } function Read-ClaudeConfigValue { param([string]$Key) $data = Read-JsonObject $ClaudeConfigFile $v = Get-Prop (Get-Prop $data 'env') $Key if ($v -is [string]) { return $v } return '' } function Configure-Api { Say '' Print-Status 'info' '⚡ 配置 API 密钥和线路' Say '' Resolve-DesktopDirs | Out-Null $existingKey = Read-ClaudeConfigValue 'ANTHROPIC_AUTH_TOKEN' $existingUrl = Read-ClaudeConfigValue 'ANTHROPIC_BASE_URL' if (-not $existingKey) { $existingKey = Read-DesktopProfileValue 'inferenceGatewayApiKey' } if (-not $existingUrl) { $existingUrl = Read-DesktopProfileValue 'inferenceGatewayBaseUrl' } if ($existingKey) { Print-Status 'info' "当前 API Key: $(Mask-Key $existingKey)" } if ($existingUrl) { Print-Status 'info' "当前 BASE URL: $existingUrl" } Say '' if (-not $S.UsageTarget) { if (-not (Choose-UsageTarget)) { return $false } Say '' } if (($S.UsageTarget -eq 'desktop' -or $S.UsageTarget -eq 'all') -and -not (Resolve-DesktopDirs)) { Print-Status 'error' '无法定位 Claude 桌面版配置目录 (未找到 LOCALAPPDATA)' if ($S.UsageTarget -eq 'desktop') { return $false } Print-Status 'warn' '将仅配置 Claude Code CLI' $S.UsageTarget = 'cli' } # ── 选择线路 ── Say ' ' '' '请选择 API 线路:' 'White' Say '' Say ' ' '' '⚡' 'Yellow' ' ' '' '1)' 'Cyan' ' ' '' '国内直连' 'Green' ' - https://ai.flashapi.top' '' Say ' ' '' '服务器位于香港,国内可直连,无需翻墙' 'DarkGray' Say '' Say ' ' '' '⚡' 'Yellow' ' ' '' '2)' 'Cyan' ' ' '' '海外CDN' 'Green' ' - https://cdn.flashapi.top' '' Say ' ' '' '全球CDN加速,适合海外用户' 'DarkGray' Say '' SayN ' ' '' '请输入选项 (1/2): ' 'White' $choice = Read-Line $baseUrl = '' switch ($choice) { '1' { $baseUrl = 'https://ai.flashapi.top' } '2' { $baseUrl = 'https://cdn.flashapi.top' } default { Print-Status 'error' '无效选项,已取消'; return $false } } Say '' Print-Status 'info' "已选择: $baseUrl" Say '' # ── 令牌分组提醒 ── Say ' ' '' '╔══════════════════════════════════════════════════════╗' 'Yellow' Say ' ' '' '║' 'Yellow' ' ' '' '⚡ 重要提示' 'White' ' ' '' '║' 'Yellow' Say ' ' '' '║' 'Yellow' ' ' '' '║' 'Yellow' Say ' ' '' '║' 'Yellow' ' ' '' '请先在官网' 'DarkGray' ' ' '' 'flashapi.top' 'Yellow' ' ' '' '创建' 'DarkGray' ' ' '' '令牌分组' 'Green' ' ' '' '║' 'Yellow' Say ' ' '' '║' 'Yellow' ' ' '' '未创建令牌分组将无法获取模型列表!' 'DarkGray' ' ' '' '║' 'Yellow' Say ' ' '' '╚══════════════════════════════════════════════════════╝' 'Yellow' Say '' # ── 输入 API Key (密码遮盖) ── $apiKey = Read-Secret ' 请输入 API Key: ' if (-not $apiKey) { Print-Status 'error' 'API Key 不能为空,已取消' return $false } Say '' # ── 验证 ── if (-not (Validate-ApiKey $baseUrl $apiKey)) { if (-not (Read-YesNo '验证未通过,是否仍要保存? (y/N): ')) { Print-Status 'info' '已取消配置'; return $false } } $wantCli = ($S.UsageTarget -eq 'cli' -or $S.UsageTarget -eq 'all') $wantDesktop = ($S.UsageTarget -eq 'desktop' -or $S.UsageTarget -eq 'all') if ($wantCli) { Write-ClaudeConfig $baseUrl $apiKey Write-ClaudeOnboarding } $desktopModels = '' if ($wantDesktop) { $desktopModels = Fetch-DesktopModels $baseUrl $apiKey # 桌面版会自行拼接 /v1/messages,这里写不带 /v1 的根地址 Write-ClaudeDesktopConfig $baseUrl $apiKey $desktopModels } Say '' Print-Status 'ok' "API Key 已保存: $(Mask-Key $apiKey)" Print-Status 'ok' "BASE URL 已保存: $baseUrl" if ($wantCli) { Print-Status 'ok' "默认模型: $DefaultModel (推理强度: $DefaultEffort)" Print-Status 'ok' "Claude Code 配置已写入: $ClaudeConfigFile" } if ($wantDesktop) { Print-Status 'ok' ("桌面版配置已写入: " + (Join-Path (Join-Path $S.DesktopThreepDir 'configLibrary') ($DesktopProfileId + '.json'))) Print-Status 'ok' ("桌面版可用模型: " + ($desktopModels -replace ',', ', ')) Print-Status 'info' '如果 Claude 桌面版正在运行,请彻底退出 (托盘/菜单栏也要退出) 后重新打开' } return $true } ############################################################################## # 状态检测与菜单 ############################################################################## function Show-StatusSummary { Say ' ' '' '当前状态:' 'White' $osLabel = 'windows' if (-not $OnWindows) { $osLabel = ([string][Environment]::OSVersion.Platform).ToLowerInvariant() } Say ' 系统环境: ' '' $osLabel 'Cyan' $claudeVer = Check-ClaudeInstalled if ($null -ne $claudeVer) { Say ' Claude Code: ' '' '已安装' 'Green' " ($claudeVer)" '' } else { Say ' Claude Code: ' '' '未安装' 'Red' } $configuredKey = Read-ClaudeConfigValue 'ANTHROPIC_AUTH_TOKEN' $configuredUrl = Read-ClaudeConfigValue 'ANTHROPIC_BASE_URL' if ($configuredKey) { Say ' API 配置: ' '' '已配置' 'Green' " ($(Mask-Key $configuredKey))" '' } else { Say ' API 配置: ' '' '未配置' 'Red' } if ($configuredUrl) { Say ' API 线路: ' '' $configuredUrl 'Green' } Say ' Claude 配置: ' '' $ClaudeConfigFile 'Cyan' if (Test-DesktopSquirrel) { Say ' 桌面版: ' '' '旧版安装器 (Cowork 不可用,请选 1 重新安装)' 'Yellow' } elseif (Test-DesktopMsix) { Say ' 桌面版: ' '' '已安装' 'Green' } else { Say ' 桌面版: ' '' '未检测到' 'Red' } if (Resolve-DesktopDirs) { $desktopKey = Read-DesktopProfileValue 'inferenceGatewayApiKey' $desktopUrl = Read-DesktopProfileValue 'inferenceGatewayBaseUrl' if ($desktopKey) { Say ' 桌面版 API: ' '' '已配置' 'Green' " ($(Mask-Key $desktopKey))" '' } else { Say ' 桌面版 API: ' '' '未配置' 'Red' } if ($desktopUrl) { Say ' 桌面版线路: ' '' $desktopUrl 'Green' } Say ' 桌面版配置: ' '' $S.DesktopThreepDir 'Cyan' } Say '' } function Show-Menu { Say ' ' '' '请选择操作:' 'White' Say '' Say ' ' '' '⚡' 'Yellow' ' ' '' '1)' 'Cyan' ' 安装 Claude Code CLI / 桌面版' '' Say ' ' '' '⚡' 'Yellow' ' ' '' '2)' 'Cyan' ' 配置 Claude Code CLI / 桌面版 API' '' Say ' ' '' '⚡' 'Yellow' ' ' '' '3)' 'Cyan' ' 一键全部安装 (安装 + 配置)' '' Say ' ' '' '⚡' 'Yellow' ' ' '' '0)' 'Cyan' ' 退出' '' Say '' SayN ' ' '' '请输入选项: ' 'White' } function Invoke-Safely { param([scriptblock]$Block) try { & $Block | Out-Null } catch { Print-Status 'error' "出错: $($_.Exception.Message)" } } ############################################################################## # 主流程 ############################################################################## function Main { Print-Banner Show-StatusSummary while ($true) { $S.UsageTarget = '' $S.UsageTargetLabel = '' Show-Menu $choice = Read-Line if ($null -eq $choice) { Say ''; return } switch ($choice) { '1' { Say '' Invoke-Safely { if (Choose-UsageTarget) { Install-Selected | Out-Null } } Say '' } '2' { Invoke-Safely { Configure-Api | Out-Null } Say '' } '3' { Say '' $picked = $false try { $picked = Choose-UsageTarget } catch { Print-Status 'error' "出错: $($_.Exception.Message)" } if (-not $picked) { Say ''; break } Invoke-Safely { Install-Selected | Out-Null } Say '' Say " $Line" 'DarkGray' Invoke-Safely { Configure-Api | Out-Null } Say '' Say " $Line" 'DarkGray' Say '' Print-Status 'ok' '⚡ 全部完成! Claude Code 请重新打开终端后运行 claude;桌面版请彻底退出后重新打开' Say '' } '0' { Say '' Say ' ' '' '⚡' 'Yellow' ' ' '' '感谢使用闪电API,祝你Vibe Coding愉快!' 'White' Say ' ' '' '官网: flashapi.top' 'DarkGray' Say '' return } default { Print-Status 'error' '无效选项,请重新选择' Say '' } } } } Main }