# ============================================================= # DFCode installer (Windows) -- launched by install_win.bat # 必须保存为 UTF-8 with BOM,PowerShell 5.1 才能正确解码中文。 # (.editorconfig 的 [*.ps1] 段与 .gitattributes 一起守住 BOM + CRLF,别删) # 暂停由 install_win.bat 负责,本脚本只输出并以退出码结束。 # # ★ 维护者必读:本脚本跑在 install_win.bat 给的 -ExecutionPolicy Bypass 下。 # 该值属于 Process 作用域,优先级高于 CurrentUser/LocalMachine,且被子进程继承。 # 因此在本文件里: # · 裸 Get-ExecutionPolicy 恒返回 Bypass —— 不要用它做任何判断。 # · 直接跑 dfcode 恒成功 —— 不要用它当校验。 # 这两条正是 2026-07 那个「装完显示环境就绪、用户敲 dfcode 却报 # 无法加载 dfcode.ps1」的 bug 能出厂的原因。判断用 Get-RealEffectivePolicy, # 校验用 Invoke-CleanProbe,两者都刻意绕开被本进程污染的环境。 # ============================================================= $ScriptVersion = '2026.09.23.1' $ErrorActionPreference = 'Continue' $ProgressPreference = 'SilentlyContinue' # 不只是美观:5.1 的 IWR 进度条会拖慢下载一个数量级 try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {} try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 } catch {} # 固定 Node 版本:用于 winget 不可用时的镜像兜底。 # 维护提示:Node 26 预计 2026-10-28 转 LTS,届时更新此处并重新核对 URL 是否 200。 $NodePinned = 'v24.20.0' $NodeMirror = 'https://cdn.npmmirror.com/binaries/node' # npmmirror.com/mirrors/node 会 302 到这里 $NodeMinMajor = 18 $installedNodeZip = $false function Note($m){ Write-Host "[*] $m" -ForegroundColor Cyan } function Good($m){ Write-Host "[OK] $m" -ForegroundColor Green } function Warn($m){ Write-Host "[!] $m" -ForegroundColor Yellow } function Bad($m){ Write-Host "[X] $m" -ForegroundColor Red } function RefreshPath { # 仅刷新本进程 PATH,让后续步骤能用上刚装好的 node/npm。 # ★ 绝不能用它来"让校验通过" —— 校验必须走 Invoke-CleanProbe 读注册表的真实 PATH。 $mp = [Environment]::GetEnvironmentVariable('Path','Machine') $up = [Environment]::GetEnvironmentVariable('Path','User') $env:Path = (@($mp, $up) | Where-Object { $_ }) -join ';' } function Get-RealEffectivePolicy { # 按引擎真实顺序求有效策略,并显式剔除 Process 行(那是 bat 给的 Bypass,是我们自己的噪音)。 # 注意:微软文档 about_Execution_Policies 的 precedence 代码块把 LocalMachine 排在 # CurrentUser 前面,那是文档 bug(同页 -List 示例与 Set-ExecutionPolicy Example 4 都 # 证明 CurrentUser 赢)。照文档写会静默拆掉 IT 用 cmdlet 设的 LocalMachine 基线。 $rows = Get-ExecutionPolicy -List foreach ($s in 'MachinePolicy','UserPolicy','CurrentUser','LocalMachine') { $r = $rows | Where-Object { [string]$_.Scope -eq $s } if ($r -and ([string]$r.ExecutionPolicy) -ne 'Undefined') { return [string]$r.ExecutionPolicy } } return 'Restricted' # 全 Undefined ⇒ 客户端为 Restricted(Server 是 RemoteSigned, # 那种情况下我们会去设一个同值,无害 no-op,故不必区分 ProductType) } function Get-PolicyLock { # 只有「限制性」GPO 才是拦路虎。GPO=RemoteSigned 是良性配置—— # 若按「存在即报警」写,一台完全正常的机器会被告知"IT 锁死了",用户白开一张工单。 Get-ExecutionPolicy -List | Where-Object { ([string]$_.Scope -eq 'MachinePolicy' -or [string]$_.Scope -eq 'UserPolicy') -and (([string]$_.ExecutionPolicy) -eq 'Restricted' -or ([string]$_.ExecutionPolicy) -eq 'AllSigned') } } function Invoke-CleanProbe { # Child environment matches a fresh terminal; a hung binary is a failed probe. param([string]$HostExe = 'powershell.exe', [string]$Cmd = 'dfcode --version', [int]$TimeoutMs = 30000) $exe = Get-Command $HostExe -ErrorAction SilentlyContinue if (-not $exe) { return $null } $out = ''; $errorText = ''; $code = -1 $process = New-Object Diagnostics.Process try { $start = New-Object Diagnostics.ProcessStartInfo $start.FileName = $exe.Source $payload = '$ErrorActionPreference = "Stop"; $ProgressPreference = "SilentlyContinue"; ' + $Cmd + '; if ($null -ne $LASTEXITCODE) { exit $LASTEXITCODE }' $encoded = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($payload)) $start.Arguments = '-NoProfile -NonInteractive -OutputFormat Text -EncodedCommand ' + $encoded $start.UseShellExecute = $false $start.CreateNoWindow = $true $start.RedirectStandardOutput = $true $start.RedirectStandardError = $true $start.EnvironmentVariables.Remove('PSExecutionPolicyPreference') $start.EnvironmentVariables['Path'] = (@([Environment]::GetEnvironmentVariable('Path','Machine'), [Environment]::GetEnvironmentVariable('Path','User')) | Where-Object { $_ }) -join ';' $process.StartInfo = $start [void]$process.Start() $stdout = $process.StandardOutput.ReadToEndAsync() $stderr = $process.StandardError.ReadToEndAsync() if (-not $process.WaitForExit($TimeoutMs)) { $code = 124 $out = "DFCode verification timed out after $TimeoutMs ms." $killInfo = New-Object Diagnostics.ProcessStartInfo $killInfo.FileName = Join-Path $env:SystemRoot 'System32\taskkill.exe' $killInfo.Arguments = '/PID ' + $process.Id + ' /T /F' $killInfo.UseShellExecute = $false $killInfo.CreateNoWindow = $true $killInfo.RedirectStandardOutput = $true $killInfo.RedirectStandardError = $true $killer = New-Object Diagnostics.Process try { $killer.StartInfo = $killInfo [void]$killer.Start() [void]$killer.StandardOutput.ReadToEndAsync() [void]$killer.StandardError.ReadToEndAsync() if (-not $killer.WaitForExit(10000)) { $killer.Kill() } if (-not $process.HasExited) { $process.Kill() } } finally { $killer.Dispose() } } else { $code = $process.ExitCode # Native commands can leave inherited pipe handles open after their host # exits; do not wait indefinitely for those handles either. $readers = [Threading.Tasks.Task]::WhenAll([Threading.Tasks.Task[]]@($stdout, $stderr)) if (-not $readers.Wait(1000)) { $code = 124 $out = 'DFCode verification output did not close after the process exited.' } else { $out = $stdout.GetAwaiter().GetResult().Trim() $errorText = $stderr.GetAwaiter().GetResult().Trim() if ($code -ne 0) { $out = ($out + "`n" + $errorText).Trim() } } } } catch { $code = -1 $out = $_.Exception.Message } finally { $process.Dispose() } $diagnostic = $out + "`n" + $errorText $blocked = $diagnostic -match 'PSSecurityException|UnauthorizedAccess|无法加载文件|cannot be loaded|因为在此系统上禁止运行脚本' $missing = $diagnostic -match 'CommandNotFoundException|不是内部或外部命令|无法将|is not recognized' [pscustomobject]@{ HostExe = $HostExe Code = $code Text = $out StandardError = $errorText Blocked = [bool]$blocked Missing = [bool]$missing Ok = ($code -eq 0 -and $out -and -not $blocked -and -not $missing) } } function Repair-ExecutionPolicy { # 返回 'ok' | 'locked' | 'failed' param([string]$HostExe) $exe = Get-Command $HostExe -ErrorAction SilentlyContinue if (-not $exe) { return 'absent' } # 用 -Command(不是脚本文件 ⇒ 免执行策略),所以即使在 Restricted 机器上也能跑。 $ps = @' $rows = Get-ExecutionPolicy -List $eff = '' foreach ($s in 'MachinePolicy','UserPolicy','CurrentUser','LocalMachine') { if ($eff -eq '') { $r = $rows | Where-Object { [string]$_.Scope -eq $s } if ($r -and ([string]$r.ExecutionPolicy) -ne 'Undefined') { $eff = [string]$r.ExecutionPolicy } } } if ($eff -eq '') { $eff = 'Restricted' } if (@('RemoteSigned','Unrestricted','Bypass') -contains $eff) { exit 0 } $lock = $rows | Where-Object { ([string]$_.Scope -eq 'MachinePolicy' -or [string]$_.Scope -eq 'UserPolicy') -and (([string]$_.ExecutionPolicy) -eq 'Restricted' -or ([string]$_.ExecutionPolicy) -eq 'AllSigned') } if ($lock) { exit 3 } try { Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned -Force -ErrorAction Stop } catch { Write-Host $_.FullyQualifiedErrorId exit 2 } exit 0 '@ $global:LASTEXITCODE = 0 & $exe.Source -NoProfile -NonInteractive -Command $ps | Out-Null switch ($LASTEXITCODE) { 0 { return 'ok' } 3 { return 'locked' } default { return 'failed' } } } function Get-OSArch { try { return [string][Runtime.InteropServices.RuntimeInformation]::OSArchitecture } # X64 / Arm64 / X86 catch { # 32 位 powershell 跑在 64 位 OS 上时 PROCESSOR_ARCHITECTURE 会返回 x86(WOW64), # PROCESSOR_ARCHITEW6432 才是真值。 $a = $env:PROCESSOR_ARCHITEW6432 if (-not $a) { $a = $env:PROCESSOR_ARCHITECTURE } switch -Wildcard ($a) { 'AMD64' { 'X64' } 'ARM64' { 'Arm64' } default { 'X86' } } } } function Test-IsAdmin { try { $id = [Security.Principal.WindowsIdentity]::GetCurrent() return (New-Object Security.Principal.WindowsPrincipal($id)).IsInRole( [Security.Principal.WindowsBuiltInRole]::Administrator) } catch { return $false } } function Get-NodeMajor { # 不要用 [version]'v24.20.0' —— 前缀 v 会抛异常;nightly 还带 - 后缀。 $v = (& node -v 2>$null | Select-Object -Last 1) if ($v -and ($v -match '^v?(\d+)\.')) { return [int]$Matches[1] } return 0 } function Resolve-NpmBinDir { # ★ npm.cmd,不是裸 npm ★ 裸 npm 在 PowerShell 里解析到 npm.ps1,正是我们在诊断的 # 那个条件;npm.cmd 完全绕开执行策略。 # ★ 永远带 -g ★ 不带 -g 的 npm prefix 返回 cwd 的 local prefix,是个静默的错答案。 # ★ Select-Object -Last 1 ★ 镜像/代理告警会污染 stdout。 $p = & npm.cmd config get prefix 2>$null | Select-Object -Last 1 if ($p) { $p = $p.Trim() } if ([string]::IsNullOrWhiteSpace($p) -or $p -eq 'undefined' -or -not (Test-Path -LiteralPath $p)) { $root = & npm.cmd root -g 2>$null | Select-Object -Last 1 if ($root) { $p = Split-Path -Parent $root.Trim() } } if ([string]::IsNullOrWhiteSpace($p)) { return $null } return $p # Windows 上 binDir == prefix } function Add-ToUserPath { # 只在"不写就没法用"的场景调用(非管理员的 Node ZIP 兜底)。 # 普通 npm prefix 缺 PATH 的情况请如实报错 + 给命令,不要在这里自作主张。 param([Parameter(Mandatory=$true)][string]$Dir) function Norm([string]$p) { if ([string]::IsNullOrWhiteSpace($p)) { return '' } try { $e = [Environment]::ExpandEnvironmentVariables($p) } catch { $e = $p } return $e.Trim().TrimEnd('\','/').ToLowerInvariant() } # 严禁 setx:1024 字符处静默截断并应用截断后的文本。 # 严禁 [Environment]::SetEnvironmentVariable(...,'User'):写 REG_SZ 把 REG_EXPAND_SZ # 永久降级,配对 Getter 又展开 %VAR% ⇒ 读-改-写一轮就把 %JAVA_HOME%\bin 冻成字面量。 $key = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey('Environment', $true) # HKCU ⇒ 免管理员 if (-not $key) { throw '无法打开 HKCU\Environment' } try { $raw = $key.GetValue('Path', '', [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames) $kind = [Microsoft.Win32.RegistryValueKind]::ExpandString try { $kind = $key.GetValueKind('Path') } catch {} if ($kind -ne [Microsoft.Win32.RegistryValueKind]::String -and $kind -ne [Microsoft.Win32.RegistryValueKind]::ExpandString) { throw "PATH 类型异常:$kind,拒绝写入" } if ($null -eq $raw) { $raw = '' } $entries = @($raw -split ';' | Where-Object { $_.Trim() }) foreach ($e in $entries) { if ((Norm $e) -eq (Norm $Dir)) { return $false } } # 幂等 $new = (($entries + $Dir) -join ';') if ($new.Length -gt 32000) { throw "User PATH 将达 $($new.Length) 字符,拒绝写入" } # 备份写 HKCU\Software\dfcode —— 绝不能写 HKCU\Environment: # 那个键下的每个值都会变成该用户的一个真实环境变量。 $bk = [Microsoft.Win32.Registry]::CurrentUser.CreateSubKey('Software\dfcode') try { $bk.SetValue('PathBackup', $raw, $kind) $bk.SetValue('PathBackupTime', (Get-Date -Format o)) } finally { $bk.Close() } $key.SetValue('Path', $new, $kind) # 保型 ⇒ REG_EXPAND_SZ 不降级 return $true } finally { $key.Close() } } Write-Host '============================================================' Write-Host " DFCode 一键安装 (Windows) 脚本版本 $ScriptVersion" Write-Host ' 自动检查/安装 Node.js,修复执行策略,再安装并升级 DFCode' Write-Host '============================================================' Write-Host '' # ---------- 第 1 步:修复 PowerShell 执行策略 ---------- # ★ 刻意放在最前面 ★ 它与安装成败无关:一个「已装好、只是启动不了」的用户重跑本脚本时, # 若 npm 恰好被内网挡住而提前 exit,他就永远等不到这次修复——而他离修好只差一次 HKCU 写入。 Note '第 1 步:检查 PowerShell 执行策略' $realPolicy = Get-RealEffectivePolicy $policyLock = Get-PolicyLock $script:PolicyLocked = $false if (@('RemoteSigned','Unrestricted','Bypass') -contains $realPolicy) { Good "执行策略为 $realPolicy,无需改动" } elseif ($policyLock) { # ★ 必须在写入之前判定 ★ Set-ExecutionPolicy 是「先写注册表、再抛终止性错误」, # 在 GPO 机器上硬试 = 白白触发 EDR 注册表告警,而用户一点好处都拿不到。 $script:PolicyLocked = $true $desc = ($policyLock | ForEach-Object { "$($_.Scope)=$($_.ExecutionPolicy)" }) -join ', ' Warn "执行策略被组策略锁定($desc),本机无法自行放开。" Warn '装好之后请在 cmd.exe 或 Git Bash 里运行 dfcode(它们不经 PowerShell 的脚本检查)。' } else { Warn "当前执行策略为 $realPolicy —— 这会导致你在 PowerShell 里敲 dfcode 时报" Warn '「无法加载文件 ...\npm\dfcode.ps1,因为在此系统上禁止运行脚本」。正在修复……' $r51 = Repair-ExecutionPolicy -HostExe 'powershell.exe' if ($r51 -eq 'ok') { Good '已将当前用户的执行策略放开为 RemoteSigned(仅影响你的账户,无需管理员)' Write-Host ' 这是运行 npm 全局命令(dfcode / npm 都是 .ps1 包装)的必需设置;' -ForegroundColor DarkGray Write-Host ' 从网上下载的脚本仍需签名,安全档位没有放宽到 Bypass。' -ForegroundColor DarkGray } elseif ($r51 -eq 'locked') { $script:PolicyLocked = $true Warn '执行策略被组策略锁定,本机无法自行放开;装好后请在 cmd.exe 或 Git Bash 里运行 dfcode。' } else { Warn '执行策略修复失败,稍后的校验会告诉你 dfcode 能不能正常启动。' } } # PS7 单独处理:PS7 出厂默认就是 RemoteSigned,且策略存在 powershell.config.json(与 5.1 不通), # 所以 5.1 的修复对它无效。Repair-ExecutionPolicy 自己会先判断、已经合规就直接返回不写, # 因此这里直接调用即可 —— 它天然是「探针驱动」的,不会盲写第二个存储。 if (Get-Command pwsh -ErrorAction SilentlyContinue) { $r7 = Repair-ExecutionPolicy -HostExe 'pwsh.exe' if ($r7 -eq 'locked') { $script:PolicyLocked = $true } elseif ($r7 -eq 'failed') { Warn 'PowerShell 7 的执行策略未能放开;若你平时用 Windows Terminal / VS Code,dfcode 可能仍无法启动。' } } # ---------- 第 2 步:检查 / 自动安装 Node.js ---------- Note '第 2 步:检查 Node.js' $arch = Get-OSArch if ($arch -eq 'X86') { Bad 'DFCode Windows requires a 64-bit operating system and x64 Node.js.' exit 1 } if ($arch -eq 'Arm64') { Warn 'DFCode Windows uses the x64 binary through Windows emulation. x64 Node.js is required.' } $vmgr = @() if ($env:NVM_HOME) { $vmgr += 'nvm-windows' } if ($env:FNM_DIR) { $vmgr += 'fnm' } if ($env:VOLTA_HOME) { $vmgr += 'volta' } if (-not (Get-Command node -ErrorAction SilentlyContinue)) { if ($vmgr.Count -gt 0) { # 别用 winget 再装第二个 Node:PATH 顺序会打架,npm prefix 也会跟着变。 Bad "检测到 $($vmgr -join '/') 但当前没有可用的 Node。请先用它安装并切换一个 Node 20+ 版本后重跑本脚本。" exit 1 } Warn '未检测到 Node.js,尝试自动安装(可能弹出 UAC 授权,请点"是")' $wingetOk = $false # Get-Command winget 不够:它可能是个零字节的 AppExecLink(Store 未初始化时存在但不可用)。 try { & winget --version > $null 2>&1; $wingetOk = ($LASTEXITCODE -eq 0) } catch { $wingetOk = $false } if ($wingetOk) { $global:LASTEXITCODE = 0 winget install -e --id OpenJS.NodeJS.LTS --architecture x64 --accept-source-agreements --accept-package-agreements $wrc = $LASTEXITCODE RefreshPath # winget 的「已安装」「无可用升级」「需重启」都是非零码,但 Node 其实是好的。 # 现象上曾表现为:Node 明明在那,脚本却 exit 1。⇒ 一律以「重新探测 node」为准。 if ($wrc -ne 0 -and -not (Get-Command node -ErrorAction SilentlyContinue)) { Warn "winget 返回 $wrc 且仍未检测到 node,改用国内镜像安装 Node $NodePinned" $wingetOk = $false } } else { Warn '本机没有可用的 winget(旧版 Win10 / LTSC / 组策略禁用都可能),改用国内镜像安装' } if (-not (Get-Command node -ErrorAction SilentlyContinue)) { # 镜像兜底。注意 npmmirror 只有「带版本号目录」可用,latest-vXX.x/*.msi 是 404。 $tag = 'x64' $base = "$NodeMirror/$NodePinned" $tmp = Join-Path $env:TEMP ("dfcode-node-{0}" -f [Guid]::NewGuid()) New-Item -ItemType Directory -Force -Path $tmp | Out-Null # 用 HEAD 探测资产是否真实存在:nodejs.org 的 index.json files[] 里没有 win-arm64-msi, # 但该文件其实是存在的 —— 信 index.json 会静默丢掉 ARM64。 function Test-Url($u) { try { $r = Invoke-WebRequest -Uri $u -Method Head -UseBasicParsing -TimeoutSec 20; return $r.StatusCode -eq 200 } catch { return $false } } function Get-Verified($url, $dest, $shaUrl, $name) { Note "下载 $name" Invoke-WebRequest -Uri $url -OutFile $dest -UseBasicParsing -TimeoutSec 600 -ErrorAction Stop # 未校验就 msiexec 一个第三方镜像的二进制 = 供应链缺口(企业 MITM 代理下尤甚)。 $sums = (Invoke-WebRequest -Uri $shaUrl -UseBasicParsing -TimeoutSec 60 -ErrorAction Stop).Content $want = ($sums -split "`n" | Where-Object { $_ -match ('^[a-fA-F0-9]{64}\s+' + [regex]::Escape($name) + '\r?$') } | Select-Object -First 1) -replace '\s.*$','' $got = (Get-FileHash -Algorithm SHA256 -LiteralPath $dest).Hash if (-not $want) { throw "SHASUMS256.txt 里找不到 $name" } if ($got -ne $want.ToUpper()) { throw "$name 校验和不匹配(期望 $want,实际 $got)" } Good "$name 校验通过" } try { $isAdmin = Test-IsAdmin $msiName = "node-$NodePinned-$tag.msi" $shaUrl = "https://nodejs.org/dist/$NodePinned/SHASUMS256.txt" if ($isAdmin -and (Test-Url "$base/$msiName")) { $msi = Join-Path $tmp $msiName Get-Verified "$base/$msiName" $msi $shaUrl $msiName Note '静默安装 Node.js(msiexec)' $p = Start-Process msiexec.exe -ArgumentList @('/i', "`"$msi`"", '/qn', '/norestart') -Wait -PassThru if ($p.ExitCode -ne 0 -and $p.ExitCode -ne 3010) { throw "msiexec 返回 $($p.ExitCode)" } } else { # 非管理员:per-machine 的 Node MSI 用 /qn 无法弹 UAC,会直接失败。 # 而「没有 winget」的人群和「非管理员」高度重合 ⇒ 走免管理员的 ZIP 路径。 $zipName = "node-$NodePinned-win-$tag.zip" if (-not (Test-Url "$base/$zipName")) { throw "镜像上找不到 $zipName(本机架构 $arch)" } $zip = Join-Path $tmp $zipName Get-Verified "$base/$zipName" $zip $shaUrl $zipName $target = Join-Path $env:LOCALAPPDATA "Programs\dfcode-nodejs-$NodePinned-$tag" Note "解压到 $target(免管理员)" if (Test-Path -LiteralPath $target) { throw "$target already exists. Check its PATH or remove it explicitly before reinstalling." } Expand-Archive -LiteralPath $zip -DestinationPath $tmp -ErrorAction Stop $inner = Join-Path $tmp "node-$NodePinned-win-$tag" New-Item -ItemType Directory -Force -Path (Split-Path -Parent $target) -ErrorAction Stop | Out-Null Move-Item -LiteralPath $inner -Destination $target -ErrorAction Stop $installedNodeZip = $true # 这条路径下不写 PATH,Node 一出这个窗口就没了 ⇒ 这里的持久化是必需的,不是顺手。 if (Add-ToUserPath -Dir $target) { Good "已把 $target 加入你的用户 PATH" } $env:Path = "$target;$env:Path" } RefreshPath } catch { Bad "自动安装 Node.js 失败:$($_.Exception.Message)" Bad '请到 https://nodejs.org 手动安装 LTS 版后,重新双击本脚本。' exit 1 } finally { Remove-Item -LiteralPath $tmp -Recurse -Force -ErrorAction SilentlyContinue } } } RefreshPath if (-not (Get-Command node -ErrorAction SilentlyContinue)) { Bad 'Node.js 已装好但本窗口 PATH 未刷新。请关闭本窗口,重新双击本脚本;仍不行请重启电脑。' exit 1 } $nodeMajor = Get-NodeMajor if ($nodeMajor -lt $NodeMinMajor) { Bad "Node.js 版本过低($(node -v)),DFCode 需要 $NodeMinMajor 以上,推荐 20+。请升级后重跑。" exit 1 } Good ("Node.js " + (node -v)) if (-not (Get-Command npm.cmd -ErrorAction SilentlyContinue)) { Bad '未检测到 npm,请检查 Node.js 安装。'; exit 1 } if ($installedNodeZip) { $npmBin = Resolve-NpmBinDir if (-not $npmBin) { Bad 'Cannot determine the npm command directory.'; exit 1 } try { [void](Add-ToUserPath -Dir $npmBin) } catch { Bad $_.Exception.Message; exit 1 } RefreshPath } # ---------- 第 3 步:安装并校验精确版本 ---------- Note '第 3 步:安装 DFCode' $work = Join-Path ([IO.Path]::GetTempPath()) ("dfcode-install-{0}" -f [Guid]::NewGuid()) New-Item -ItemType Directory -Path $work -ErrorAction Stop | Out-Null $contractFile = Join-Path $work 'contract.cjs' $expectedFile = Join-Path $work 'expected.txt' $logFile = Join-Path ([IO.Path]::GetTempPath()) ("dfcode-install-{0}.log" -f [Guid]::NewGuid()) $installContract = @' /* Embedded in both Windows installers. Keep the copies in sync; tests check them. */ "use strict"; /* eslint-disable @typescript-eslint/no-require-imports -- Standalone Node 18 CommonJS embedded in Windows installers. */ const fs = require("node:fs"); const path = require("node:path"); const cp = require("node:child_process"); const { performance } = require("node:perf_hooks"); const OFFICIAL = "https://registry.npmjs.org"; const MIRRORS = [ "https://repo.huaweicloud.com/repository/npm", "https://mirrors.cloud.tencent.com/npm", ]; const VERSION = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/; function validVersion(value) { if (typeof value !== "string") return false; const match = value.match(VERSION); return Boolean( match && (!match[4] || match[4] .split(".") .every((x) => !/^\d+$/.test(x) || x === "0" || !x.startsWith("0"))), ); } function compareVersion(left, right) { const a = left.match(VERSION), b = right.match(VERSION); for (let i = 1; i <= 3; i++) { const x = BigInt(a[i]), y = BigInt(b[i]); if (x !== y) return x > y ? 1 : -1; } if (!a[4] || !b[4]) return a[4] ? -1 : b[4] ? 1 : 0; const x = a[4].split("."), y = b[4].split("."); for (let i = 0; i < Math.max(x.length, y.length); i++) { if (x[i] === undefined || y[i] === undefined) return x[i] === undefined ? -1 : 1; if (x[i] === y[i]) continue; const xn = /^\d+$/.test(x[i]), yn = /^\d+$/.test(y[i]); if (xn && yn) return BigInt(x[i]) > BigInt(y[i]) ? 1 : -1; if (xn !== yn) return xn ? -1 : 1; return x[i] > y[i] ? 1 : -1; } return 0; } function log(text) { if (process.env.DFCODE_INSTALL_LOG) fs.appendFileSync(process.env.DFCODE_INSTALL_LOG, text); } function run(command, args, timeout = 180000, phase = command) { if (timeout <= 0) return Promise.resolve({ ok: false, stdout: "", text: "Official registry budget exhausted.", timedOut: true, }); if (process.env.DFCODE_INSTALL_KEEP_LOCK === "1") return Promise.resolve({ ok: false, stdout: "", text: "A timed-out process could not be stopped.", }); // Command names are internal constants. Quoting a bare PATH command makes // cmd.exe resolve npm.cmd's %~dp0 against cwd instead of its installation. // Keep the command token plain; every argument remains independently quoted. const line = [command, ...args.map((x) => '"' + x + '"')].join(" "); const started = performance.now(); log( `\n[phase] ${phase} timeout_ms=${Math.ceil(timeout)}\n${command} ${args.join(" ")}\n`, ); return new Promise((resolve) => { const child = cp.spawn( process.env.ComSpec || "cmd.exe", ["/d", "/s", "/c", line], { windowsHide: true, windowsVerbatimArguments: true, stdio: ["ignore", "pipe", "pipe"], }, ); let stdout = "", stderr = "", failed = false, finished = false, timedOut = false; const progress = setInterval(() => { const message = `[*] ${phase}\uff1a\u5df2\u7b49\u5f85 ${Math.floor((performance.now() - started) / 1000)} \u79d2\uff0c\u4ecd\u5728\u5904\u7406\u3002`; console.log(message); log(message + "\n"); }, 15000); const finish = (code) => { if (finished) return; finished = true; clearTimeout(timer); clearInterval(progress); const text = stdout + stderr; const elapsedMs = Math.round(performance.now() - started); log( `\n[exit] code=${code} elapsed_ms=${elapsedMs} timed_out=${timedOut}\n`, ); resolve({ ok: !failed && code === 0, stdout, text, timedOut, elapsedMs, exitCode: code, }); }; const stop = (reason) => { if (failed) return; failed = true; stderr += reason + "\n"; log(reason + "\n"); // Kill the cmd/npm/binary tree before releasing the install lock. if (child.pid) { const killed = cp.spawnSync( "taskkill.exe", ["/PID", String(child.pid), "/T", "/F"], { windowsHide: true, timeout: 10000, stdio: "ignore" }, ); if (killed.status !== 0) { process.env.DFCODE_INSTALL_KEEP_LOCK = "1"; child.kill(); child.stdout.destroy(); child.stderr.destroy(); finish(1); } } }; const timer = setTimeout(() => { timedOut = true; stop(phase + " timed out."); }, timeout); child.stdout.setEncoding("utf8"); child.stderr.setEncoding("utf8"); child.stdout.on("data", (data) => { if (failed) return; stdout += data; log(data); if (stdout.length + stderr.length > 4 * 1024 * 1024) stop("Command output exceeded 4 MiB."); }); child.stderr.on("data", (data) => { if (failed) return; stderr += data; log(data); if (stdout.length + stderr.length > 4 * 1024 * 1024) stop("Command output exceeded 4 MiB."); }); child.on("error", (error) => { failed = true; stderr += error.message; log(error.message + "\n"); finish(1); }); child.on("close", (code, signal) => { // Ctrl+C can be a signal or Windows STATUS_CONTROL_C_EXIT. Never treat // cancellation as permission to launch another installer on a mirror. if ( !failed && (signal || [130, 143, 3221225786, -1073741510].includes(code)) ) { process.env.DFCODE_INSTALL_KEEP_LOCK = "1"; failed = true; stderr += "Installation was interrupted.\n"; log("Installation was interrupted.\n"); } finish(code); }); }); } function fetchOptions(officialPhase, timeout) { return officialPhase ? [ "--fetch-retries=2", "--fetch-retry-factor=10", "--fetch-retry-mintimeout=10000", "--fetch-retry-maxtimeout=60000", "--fetch-timeout=" + Math.max(1, Math.min(300000, Math.ceil(timeout))), ] : [ "--fetch-retries=1", "--fetch-retry-mintimeout=1000", "--fetch-retry-maxtimeout=3000", "--fetch-timeout=20000", ]; } async function metadata( spec, registry, timeout = 45000, officialPhase = false, ) { const r = await run( "npm.cmd", [ "view", spec, "--json", "--registry=" + registry, ...fetchOptions(officialPhase, timeout), ], timeout, (officialPhase ? "\u5b98\u65b9\u6e90\u7248\u672c\u67e5\u8be2" : "\u955c\u50cf\u5b89\u5168\u6821\u9a8c") + " " + spec, ); if (!r.ok) { checkLocalInstallFailure(r); const error = new Error( `\u65e0\u6cd5\u8bfb\u53d6 ${spec} \u7684\u7248\u672c\u4fe1\u606f\uff08${registry}\uff0c${r.timedOut ? "\u7b49\u5f85\u8d85\u65f6" : "\u9000\u51fa\u7801 " + r.exitCode}\uff09\u3002\u8bf7\u67e5\u770b\u65e5\u5fd7\u4e2d\u7684 npm \u9519\u8bef\u3002`, ); const code = npmErrorCode(r.text); if (code && !networkFailure(r) && !["E404", "ETARGET"].includes(code)) error.code = "DFCODE_LOCAL_INSTALL_ERROR"; throw error; } let data; try { data = JSON.parse(r.stdout); } catch { throw new Error("Registry returned invalid JSON for " + spec + "."); } if ( !data || typeof data !== "object" || Array.isArray(data) || !validVersion(data.version) ) throw new Error("Registry returned invalid metadata for " + spec + "."); return data; } async function binaryVersion() { const r = await run("dfcode", ["--version"], 20000); const text = r.stdout.replace(/\x1b\[[0-9;]*[A-Za-z]/g, "").trim(); return r.ok && validVersion(text) ? text : null; } function checkLocalInstallFailure(result) { if (result.ok) return; const text = result.text.replace(/\x1b\[[0-9;]*[A-Za-z]/g, ""); const code = npmErrorCode(text); if (!["EBUSY", "EPERM", "EACCES", "ENOSPC"].includes(code)) return; const advice = code === "EBUSY" ? "\u672c\u5730\u6587\u4ef6\u6b63\u5728\u88ab\u4f7f\u7528\uff0cWindows \u65e0\u6cd5\u66ff\u6362\u5b83\u3002" + recoveryAdvice() : code === "ENOSPC" ? "\u5b89\u88c5\u76d8\u6216\u4e34\u65f6\u76ee\u5f55\u6240\u5728\u78c1\u76d8\u7a7a\u95f4\u4e0d\u8db3\uff0c\u8bf7\u91ca\u653e\u7a7a\u95f4\u540e\u91cd\u8bd5\u3002" : "\u672c\u5730\u6587\u4ef6\u8bbf\u95ee\u5931\u8d25\uff0c\u53ef\u80fd\u662f\u5360\u7528\u3001\u76ee\u5f55\u6743\u9650\u6216\u5b89\u5168\u8f6f\u4ef6\u62e6\u622a\u3002\u8bf7\u68c0\u67e5\u9519\u8bef\u8def\u5f84\u3002" + recoveryAdvice(); const location = text.match(/^\s*npm (?:error|ERR!) path (.+)$/m)?.[1]; const error = new Error( `\u5347\u7ea7\u672a\u5b8c\u6210\uff08${code}\uff09\uff1a${advice}${location ? "\n\u8def\u5f84\uff1a" + location.trim() : ""}\n\u8fd9\u662f\u672c\u5730\u6587\u4ef6\u95ee\u9898\uff0c\u4e0d\u4f1a\u5207\u6362\u955c\u50cf\u91cd\u8bd5\u3002`, ); error.code = "DFCODE_LOCAL_INSTALL_ERROR"; throw error; } function recoveryAdvice() { return ( "\n1. \u4fdd\u5b58\u5de5\u4f5c\uff0c\u6b63\u5e38\u9000\u51fa\u4f7f\u7528\u8be5\u5b89\u88c5\u76ee\u5f55\u7684 DFCode \u7ec8\u7aef\u3001Studio \u6216 IDE \u4efb\u52a1\uff1b\u5176\u4ed6\u5b89\u88c5\u76ee\u5f55\u7684\u4f1a\u8bdd\u4e0d\u5fc5\u5173\u95ed\u3002" + "\n2. \u91cd\u65b0\u8fd0\u884c\u540c\u4e00\u6761\u5b89\u88c5\u547d\u4ee4\uff0c\u65e0\u9700\u5148\u5378\u8f7d\u6216\u6e05\u7a7a\u7f13\u5b58\u3002" + "\n3. \u56de\u5230\u539f\u9879\u76ee\u76ee\u5f55\uff0c\u6267\u884c dfcode -c \u7ee7\u7eed\u6700\u8fd1\u7684\u4f1a\u8bdd\uff1b\u6216\u542f\u52a8 dfcode \u540e\u8f93\u5165 /sessions \u9009\u62e9\u5386\u53f2\u4f1a\u8bdd\u3002" + "\n\u5b89\u88c5\u5668\u4e0d\u4f1a\u5220\u9664\u5386\u53f2\u4f1a\u8bdd\uff0c\u4e5f\u4e0d\u4f1a\u81ea\u52a8\u7ed3\u675f\u4f60\u7684\u8fdb\u7a0b\u3002\u6b63\u5728\u6267\u884c\u7684\u4efb\u52a1\u8bf7\u5148\u59a5\u5584\u505c\u6b62\u3002" ); } function canonicalWindowsPath(filename) { const win = path.win32; let resolved; try { resolved = fs.realpathSync.native(filename); } catch (error) { if (error.code !== "ENOENT" && error.code !== "ENOTDIR") throw error; const parent = win.dirname(filename); resolved = parent === filename ? filename : win.join(canonicalWindowsPath(parent), win.basename(filename)); } return win .normalize( resolved.replace(/^\\\\\?\\UNC\\/i, "\\\\").replace(/^\\\\\?\\/, ""), ) .replace(/[\\/]+$/, ""); } async function checkRunningDfcode() { const rootResult = await run( "npm.cmd", ["root", "-g"], 20000, "\u68c0\u67e5\u5b89\u88c5\u76ee\u5f55", ); const root = rootResult.stdout.trim(); if (!rootResult.ok || !path.win32.isAbsolute(root) || /[\r\n]/.test(root)) throw new Error( "\u65e0\u6cd5\u786e\u5b9a npm \u5168\u5c40\u5b89\u88c5\u76ee\u5f55\uff0c\u5347\u7ea7\u5c1a\u672a\u5f00\u59cb\u3002\u8bf7\u8fd0\u884c npm root -g \u68c0\u67e5\u5f53\u524d Node.js \u73af\u5883\u3002", ); const packageRoot = path.win32.join(root, "dfcode"); const directories = [packageRoot]; for (const name of ["dfcode-windows-x64", "dfcode-windows-x64-baseline"]) { directories.push( path.win32.join(packageRoot, "node_modules", name), path.win32.join(root, name), ); } const scopes = directories.map((dir) => canonicalWindowsPath(dir).toLowerCase(), ); // Optional diagnostic only: file probes below remain available when CIM or // PowerShell is restricted. No process-name-only veto and no user process kill. const script = "$ErrorActionPreference='Stop'; [Console]::OutputEncoding=New-Object Text.UTF8Encoding($false); ConvertTo-Json -Compress -InputObject @(Get-CimInstance Win32_Process -Filter \"Name = 'dfcode.exe'\" | Select-Object ProcessId,ExecutablePath)"; const result = await run( "powershell.exe", [ "-NoProfile", "-NonInteractive", "-EncodedCommand", Buffer.from(script, "utf16le").toString("base64"), ], 20000, "\u68c0\u67e5\u76f8\u5173 DFCode \u8fdb\u7a0b", ); let processes = []; try { if (result.ok) { const data = JSON.parse(result.stdout.replace(/^\uFEFF/, "")); if (Array.isArray(data)) processes = data; } } catch { /* File probes are the fallback, not a blanket installation veto. */ } const affected = processes.filter((item) => { if ( typeof item?.ExecutablePath !== "string" || !path.win32.isAbsolute(item.ExecutablePath) ) return false; try { const filename = canonicalWindowsPath(item.ExecutablePath).toLowerCase(); return scopes.some((dir) => filename.startsWith(dir + "\\")); } catch { return false; } }); if (affected.length) { throw new Error( "\u5347\u7ea7\u5c1a\u672a\u5f00\u59cb\uff1a\u5f53\u524d\u5b89\u88c5\u76ee\u5f55\u4e2d\u7684 DFCode \u6b63\u5728\u8fd0\u884c\uff0cWindows \u65e0\u6cd5\u66ff\u6362\u76f8\u5173\u7a0b\u5e8f\u6587\u4ef6\u3002\n" + affected .map((item) => `PID: ${item.ProcessId} ${item.ExecutablePath}`) .join("\n") + recoveryAdvice(), ); } for (const dir of directories) { const filename = path.win32.join(dir, "bin", "dfcode.exe"); // r+ requests write access without truncating, writing or renaming anything. // Opening is also the existence check: existsSync would hide EACCES. let handle; try { handle = fs.openSync(filename, "r+"); } catch (error) { if (error.code === "ENOENT" || error.code === "ENOTDIR") continue; throw new Error( `\u5347\u7ea7\u5c1a\u672a\u5f00\u59cb\uff1a\u5b89\u88c5\u6587\u4ef6\u88ab\u5360\u7528\u6216\u4e0d\u53ef\u5199\uff08${error.code}\uff09\u3002\n\u8def\u5f84\uff1a${filename}` + recoveryAdvice(), ); } finally { if (handle !== undefined) fs.closeSync(handle); } } } function npmErrorCode(text) { return [ ...text .replace(/\x1b\[[0-9;]*[A-Za-z]/g, "") .matchAll(/^\s*npm (?:error|ERR!) code (\S+)\s*$/gm), ].at(-1)?.[1]; } function networkFailure(result) { const codes = "ECONNRESET|ECONNREFUSED|ECONNABORTED|ETIMEDOUT|ESOCKETTIMEDOUT|ERR_SOCKET_TIMEOUT|EAI_AGAIN|EAI_FAIL|ENETUNREACH|EHOSTUNREACH|ENOTFOUND|FETCH_ERROR|E408|E429|E50[0-9]"; const code = npmErrorCode(result.text); if (code) return new RegExp("^(?:" + codes + ")$").test(code); return ( result.timedOut || new RegExp("^(?:" + codes + ")\\b", "m").test(result.text) ); } async function mirrorContract(officialRoot, target, mirrorRegistry) { const mirrorRoot = await metadata("dfcode@" + target, mirrorRegistry); if ( mirrorRoot.version !== target || !officialRoot.dist?.integrity || mirrorRoot.dist?.integrity !== officialRoot.dist.integrity ) throw new Error( "Mirror root package does not match the official " + target + " release.", ); const platformNames = ["dfcode-windows-x64", "dfcode-windows-x64-baseline"]; for (const name of platformNames) { const version = officialRoot.optionalDependencies?.[name]; if ( version !== target || mirrorRoot.optionalDependencies?.[name] !== version ) throw new Error( "Release is missing the exact Windows platform dependency " + name + "@" + target + ".", ); const official = await metadata(name + "@" + version, OFFICIAL); const mirror = await metadata(name + "@" + version, mirrorRegistry); if ( official.version !== version || mirror.version !== version || !official.dist?.integrity || mirror.dist?.integrity !== official.dist.integrity ) throw new Error( "Mirror platform package does not match " + name + "@" + version + ".", ); } } function acquireLock() { const base = path.join( process.env.LOCALAPPDATA || process.env.TEMP, "dfcode", ); fs.mkdirSync(base, { recursive: true }); const filename = path.join(base, "npm-installer.lock"); // Do not remove an existing lock: the other installer may still be running. // A leftover lock after a killed process requires an explicit operator check. let handle; try { handle = fs.openSync(filename, "wx"); } catch (error) { if (error.code === "EEXIST") throw new Error( "Another DFCode installation may be running. Close it before retrying. If it was interrupted, check and remove " + filename + ".", ); throw error; } fs.writeFileSync( handle, JSON.stringify({ pid: process.pid, started: new Date().toISOString() }) + "\n", ); return () => { fs.closeSync(handle); if (process.env.DFCODE_INSTALL_KEEP_LOCK !== "1") fs.unlinkSync(filename); }; } async function main() { const checkInterrupted = () => { if (process.env.DFCODE_INSTALL_KEEP_LOCK === "1") throw new Error( "Installation was interrupted or its process tree could not be stopped. No further installation will be started; check the retained installer lock before retrying.", ); }; checkInterrupted(); const pin = process.env.DFCODE_VERSION || ""; const tag = process.env.DFCODE_DIST_TAG || "latest"; if (pin && !validVersion(pin)) throw new Error( "DFCODE_VERSION must be an exact semantic version, for example 3.3.17 or 3.3.18-beta.1.", ); if (!pin && !["latest", "stable", "beta"].includes(tag)) throw new Error("DFCODE_DIST_TAG must be latest, stable, or beta."); if (process.arch !== "x64") throw new Error( "DFCode Windows currently requires x64 Node.js. Native ARM64 and x86 packages are not published.", ); const officialDeadline = performance.now() + 600000; const remaining = () => Math.max(0, officialDeadline - performance.now()); console.log( "[*] \u5b98\u65b9\u6e90\u4f18\u5148\uff1a\u7248\u672c\u67e5\u8be2\u3001\u5b89\u88c5\u548c\u91cd\u8bd5\u5408\u8ba1\u6700\u591a\u7b49\u5f85 10 \u5206\u949f\u3002", ); const release = await metadata( "dfcode@" + (pin || tag), OFFICIAL, remaining(), true, ); if (pin && release.version !== pin) throw new Error( "Official registry did not return the requested version " + pin + ".", ); const target = release.version; console.log("[OK] Target DFCode version: " + target); const unlock = acquireLock(); try { const before = await binaryVersion(); checkInterrupted(); if (!pin && before && compareVersion(before, target) > 0) { console.log("[OK] Keeping newer working DFCode " + before + "."); fs.writeFileSync(process.env.DFCODE_INSTALL_EXPECTED, before); return; } if (before !== target) { await checkRunningDfcode(); checkInterrupted(); const installOfficial = () => { const timeout = remaining(); return run( "npm.cmd", [ "install", "-g", "dfcode@" + target, "--include=optional", "--registry=" + OFFICIAL, ...fetchOptions(true, timeout), ], timeout, "\u5b98\u65b9\u6e90\u5b89\u88c5", ); }; let installed = await installOfficial(); checkInterrupted(); checkLocalInstallFailure(installed); if ( !installed.ok && networkFailure(installed) && !installed.timedOut && remaining() > 0 ) { console.log( "[!] \u5b98\u65b9\u6e90\u6682\u65f6\u51fa\u73b0\u7f51\u7edc\u9519\u8bef\uff0c\u5c06\u5728\u5269\u4f59\u65f6\u95f4\u5185\u91cd\u8bd5\u4e00\u6b21\uff1b\u4e0d\u4f1a\u91cd\u65b0\u8ba1\u7b97 10 \u5206\u949f\u3002", ); installed = await installOfficial(); checkInterrupted(); checkLocalInstallFailure(installed); } let actual = installed.ok ? await binaryVersion() : null; checkInterrupted(); if (installed.ok && actual !== target) throw new Error( `npm \u5b89\u88c5\u5df2\u5b8c\u6210\uff0c\u4f46\u547d\u4ee4\u8fd0\u884c\u7248\u672c\u4e0d\u7b26\uff1a\u9884\u671f ${target}\uff0c\u5b9e\u9645 ${actual || "\u65e0\u6cd5\u542f\u52a8"}\u3002\u8bf7\u68c0\u67e5 PATH \u4e2d\u7684\u65e7\u5b89\u88c5\u53ca\u65e5\u5fd7\uff1b\u4e0d\u4f1a\u901a\u8fc7\u5207\u6362\u955c\u50cf\u4fee\u590d\u672c\u5730\u542f\u52a8\u95ee\u9898\u3002`, ); if (!installed.ok && !networkFailure(installed)) throw new Error( `\u5b98\u65b9\u6e90\u5b89\u88c5\u672a\u5b8c\u6210\uff08\u9000\u51fa\u7801 ${installed.exitCode}\uff09\u3002\u65e5\u5fd7\u672a\u663e\u793a\u53ef\u91cd\u8bd5\u7684\u7f51\u7edc\u9519\u8bef\uff0c\u8bf7\u68c0\u67e5 npm \u9519\u8bef\uff1b\u672a\u5207\u6362\u955c\u50cf\u3002`, ); if (!installed.ok) { console.log( `[!] \u5b98\u65b9\u6e90${installed.timedOut ? "\u7b49\u5f85\u9884\u7b97\u5df2\u8017\u5c3d" : "\u7f51\u7edc\u91cd\u8bd5\u5931\u8d25"}\uff0c\u5f00\u59cb\u6309\u534e\u4e3a\u4e91\u3001\u817e\u8baf\u4e91\u987a\u5e8f\u9a8c\u8bc1\u540c\u4e00\u7248\u672c\u3002`, ); let lastMirrorError = null; for (const mirror of MIRRORS) { try { console.log( "[!] Checking " + mirror + " for the same release and its Windows binaries.", ); await mirrorContract(release, target, mirror); checkInterrupted(); installed = await run( "npm.cmd", [ "install", "-g", "dfcode@" + target, "--include=optional", "--registry=" + mirror, "--fetch-retries=1", "--fetch-retry-mintimeout=1000", "--fetch-retry-maxtimeout=3000", "--fetch-timeout=30000", ], 180000, "\u955c\u50cf\u5b89\u88c5 " + mirror, ); checkInterrupted(); checkLocalInstallFailure(installed); actual = installed.ok ? await binaryVersion() : null; checkInterrupted(); if (installed.ok && actual !== target) { const error = new Error( `\u955c\u50cf\u5b89\u88c5\u540e\u8fd0\u884c\u7248\u672c\u4e0d\u7b26\uff1a\u9884\u671f ${target}\uff0c\u5b9e\u9645 ${actual || "\u65e0\u6cd5\u542f\u52a8"}\u3002\u8bf7\u68c0\u67e5 PATH \u548c\u65e5\u5fd7\u3002`, ); error.code = "DFCODE_LOCAL_INSTALL_ERROR"; throw error; } if (!installed.ok && !networkFailure(installed)) { const error = new Error( `\u955c\u50cf\u5b89\u88c5\u9047\u5230\u975e\u7f51\u7edc\u9519\u8bef\uff08\u9000\u51fa\u7801 ${installed.exitCode}\uff09\uff0c\u8bf7\u67e5\u770b\u65e5\u5fd7\u3002`, ); error.code = "DFCODE_LOCAL_INSTALL_ERROR"; throw error; } if (installed.ok && actual === target) break; } catch (error) { checkInterrupted(); if (error.code === "DFCODE_LOCAL_INSTALL_ERROR") throw error; lastMirrorError = error; console.log( "[!] " + mirror + " is unavailable for this release; continuing to the next mirror.", ); installed = { ok: false }; } } if ((!installed.ok || actual !== target) && lastMirrorError) throw lastMirrorError; } if (!installed.ok) throw new Error( "Installation failed. The requested version was not installed.", ); } const actual = await binaryVersion(); if (actual !== target) throw new Error( "DFCode binary verification failed: expected " + target + ", got " + (actual || "no working binary") + ".", ); fs.writeFileSync(process.env.DFCODE_INSTALL_EXPECTED, target); console.log("[OK] Verified DFCode binary: " + actual); } finally { unlock(); } } module.exports = { validVersion, compareVersion, mirrorContract, main }; if (require.main === module) { main().catch((error) => { console.error("[X] " + error.message); if (process.env.DFCODE_INSTALL_LOG) console.error("[X] Log: " + process.env.DFCODE_INSTALL_LOG); process.exitCode = 1; }); } '@ $savedExpected = $env:DFCODE_INSTALL_EXPECTED $savedLog = $env:DFCODE_INSTALL_LOG try { [IO.File]::WriteAllText($contractFile, $installContract, (New-Object Text.UTF8Encoding($false))) $env:DFCODE_INSTALL_EXPECTED = $expectedFile $env:DFCODE_INSTALL_LOG = $logFile & node $contractFile if ($LASTEXITCODE -ne 0) { exit 1 } $expectedVersion = [IO.File]::ReadAllText($expectedFile).Trim() } finally { $env:DFCODE_INSTALL_EXPECTED = $savedExpected $env:DFCODE_INSTALL_LOG = $savedLog Remove-Item -LiteralPath $work -Recurse -Force -ErrorAction SilentlyContinue } RefreshPath # ---------- 第 5 步:诚实校验 ---------- # ★ 这一步是本次改动的核心 ★ # 旧版这里是 Get-Command dfcode + dfcode --version —— 两者都跑在被 bat 的 Bypass 污染的 # 环境里,在报错用户的机器上它们今天就是绿的。那正是「装完显示环境就绪、用户却用不了」的成因。 Note '第 5 步:校验(模拟你新开一个窗口的真实环境)' $probe = Invoke-CleanProbe $binDir = Resolve-NpmBinDir if ($probe -and $probe.Ok) { if ($probe.Text.Trim() -cne $expectedVersion) { Bad "DFCode binary version mismatch: expected $expectedVersion, got $($probe.Text.Trim())." exit 1 } Good ('DFCode 版本:' + $expectedVersion) } elseif ($probe -and $probe.Blocked) { Bad 'DFCode 已装好,但 PowerShell 仍然拒绝启动它(执行策略未放开)。' if ($script:PolicyLocked) { Bad '原因:执行策略被组策略锁定。请改用 cmd.exe 或 Git Bash 运行 dfcode,或联系 IT 放开 RemoteSigned。' } else { Bad '请在 PowerShell 里手动执行这一行后重试:' Bad ' Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned -Force' } exit 1 } elseif ($probe -and $probe.Missing) { Bad 'DFCode 已装好,但它所在的目录不在你的 PATH 里,所以新开的窗口找不到它。' if ($binDir) { Bad " 需要加入 PATH 的目录:$binDir" Bad ' 可在「设置 → 搜索"环境变量" → 编辑用户环境变量 Path」里新增该目录,然后重开终端。' } exit 1 } else { Bad 'DFCode 装好了但无法正常运行。请把本窗口完整截图发给支持人员。' if ($probe) { Write-Host $probe.Text -ForegroundColor DarkGray } exit 1 } if ($binDir -and $binDir.StartsWith('\\')) { # RemoteSigned 对 UNC 路径上的脚本仍可能拒绝(微软明文)。漫游配置/文件夹重定向会踩到。 Warn "你的 npm 目录在网络路径上($binDir)。若之后 dfcode 仍报无法加载,请改用 cmd.exe 运行。" } Write-Host '' Write-Host '============================================================' Good '环境就绪!下一步:' Write-Host ' 1) 进入你的项目目录:cd D:\projects\my-app' Write-Host ' 2) 启动: dfcode' Write-Host ' 3) 登录:输入 /login,在企业门户完成登录和设备绑定' Write-Host ' (企业统一分发模型,无需配置 API Key)' Write-Host ' 4) 选择模型:/models' Write-Host ' 图文详解见官网文档中心《登录部署》' Write-Host '============================================================' Good "安装日志:$logFile" exit 0