$ErrorActionPreference = "Stop" $ProgressPreference = "SilentlyContinue" [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 $dingSourceBases = @({{NODE_SOURCE_BASES_POWERSHELL}}) $dingInstallerUrl = "{{INSTALLER_URL}}" $dingInstallerSha256 = "{{INSTALLER_SHA256}}" $dingPublicKeyUrl = "{{PUBLIC_KEY_URL}}" $dingPublicKeySha256 = "{{PUBLIC_KEY_SHA256}}" $dingManifestUrl = "{{MANIFEST_URL}}" function Get-DingSha256([string] $dingPath) { return (Get-FileHash -Algorithm SHA256 -LiteralPath $dingPath).Hash.ToLowerInvariant() } function Save-DingDownload([string[]] $dingUrls, [string] $dingTarget, [string] $dingExpectedSha256) { $dingLastError = $null foreach ($dingUrl in $dingUrls) { try { if (Test-Path -LiteralPath $dingTarget) { Remove-Item -LiteralPath $dingTarget -Force } Invoke-WebRequest -UseBasicParsing -Uri $dingUrl -OutFile $dingTarget -TimeoutSec 300 } catch { $dingLastError = $_.Exception.Message $dingStatusCode = $null if ($_.Exception.Response -and $_.Exception.Response.StatusCode) { try { $dingStatusCode = [int]$_.Exception.Response.StatusCode } catch { $dingStatusCode = $null } } if ($null -eq $dingStatusCode -or $dingStatusCode -eq 404) { continue } throw "下载失败且不允许换源(HTTP $dingStatusCode):$dingUrl;$dingLastError" } $dingActualSha256 = Get-DingSha256 $dingTarget if ($dingActualSha256 -ne $dingExpectedSha256) { throw "下载文件 SHA-256 不匹配,停止安装:$dingUrl" } return } throw "所有下载源均不可用:$dingLastError" } function Test-DingNode([string] $dingNodePath, [bool] $dingRequireAdjacentNpm) { if (-not $dingNodePath -or -not (Test-Path -LiteralPath $dingNodePath -PathType Leaf)) { return $false } try { & $dingNodePath -e 'const [a,b]=process.versions.node.split(".").map(Number);process.exit(a>22||(a===22&&b>=13)?0:1)' if ($LASTEXITCODE -ne 0) { return $false } if ($dingRequireAdjacentNpm) { $dingNpmCli = Join-Path (Split-Path -Parent $dingNodePath) "node_modules\npm\bin\npm-cli.js" return Test-Path -LiteralPath $dingNpmCli -PathType Leaf } return $null -ne (Get-Command npm -ErrorAction SilentlyContinue) } catch { return $false } } $dingArchitecture = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString().ToLowerInvariant() switch ($dingArchitecture) { "x64" { $dingNodeFile = "{{NODE_WIN32_X64_FILE}}"; $dingNodeSha256 = "{{NODE_WIN32_X64_SHA256}}" } "arm64" { $dingNodeFile = "{{NODE_WIN32_ARM64_FILE}}"; $dingNodeSha256 = "{{NODE_WIN32_ARM64_SHA256}}" } default { throw "不支持的 Windows CPU 架构:$dingArchitecture" } } $dingSystemNode = Get-Command node -ErrorAction SilentlyContinue $dingNodePath = if ($dingSystemNode) { $dingSystemNode.Source } else { $null } if (-not (Test-DingNode $dingNodePath $false)) { if (-not $env:LOCALAPPDATA -and -not $env:DINGTALK_APPROVAL_CLIENT_HOME) { throw "无法确定当前用户数据目录:LOCALAPPDATA 为空" } $dingClientRoot = if ($env:DINGTALK_APPROVAL_CLIENT_HOME) { [IO.Path]::GetFullPath($env:DINGTALK_APPROVAL_CLIENT_HOME) } else { Join-Path $env:LOCALAPPDATA "DingTalkApproval" } $dingRuntimeParent = Join-Path $dingClientRoot "node" $dingRuntimeName = [IO.Path]::GetFileNameWithoutExtension([IO.Path]::GetFileNameWithoutExtension($dingNodeFile)) $dingRuntimeDir = Join-Path $dingRuntimeParent $dingRuntimeName $dingNodePath = Join-Path $dingRuntimeDir "node.exe" if (-not (Test-DingNode $dingNodePath $true)) { New-Item -ItemType Directory -Path $dingRuntimeParent -Force | Out-Null $dingRuntimeTemp = Join-Path ([IO.Path]::GetTempPath()) ("dingmcp-node-" + [guid]::NewGuid().ToString("N")) New-Item -ItemType Directory -Path $dingRuntimeTemp -Force | Out-Null try { $dingRuntimeArchive = Join-Path $dingRuntimeTemp $dingNodeFile $dingRuntimeUrls = @($dingSourceBases | ForEach-Object { $_ + $dingNodeFile }) Save-DingDownload $dingRuntimeUrls $dingRuntimeArchive $dingNodeSha256 $dingExtractRoot = Join-Path $dingRuntimeTemp "extract" Expand-Archive -LiteralPath $dingRuntimeArchive -DestinationPath $dingExtractRoot -Force $dingExtractedDir = Join-Path $dingExtractRoot $dingRuntimeName $dingExtractedNode = Join-Path $dingExtractedDir "node.exe" if (-not (Test-DingNode $dingExtractedNode $true)) { throw "便携 Node 归档缺少可用的 node.exe 或 npm-cli.js" } if (Test-Path -LiteralPath $dingRuntimeDir) { Remove-Item -LiteralPath $dingRuntimeDir -Recurse -Force } Move-Item -LiteralPath $dingExtractedDir -Destination $dingRuntimeDir } finally { if (Test-Path -LiteralPath $dingRuntimeTemp) { Remove-Item -LiteralPath $dingRuntimeTemp -Recurse -Force } } } } $dingInstallTemp = Join-Path ([IO.Path]::GetTempPath()) ("dingmcp-install-" + [guid]::NewGuid().ToString("N")) New-Item -ItemType Directory -Path $dingInstallTemp -Force | Out-Null try { $dingInstaller = Join-Path $dingInstallTemp "installer.mjs" $dingPublicKey = Join-Path $dingInstallTemp "release-signing-public.pem" Save-DingDownload @($dingInstallerUrl) $dingInstaller $dingInstallerSha256 Save-DingDownload @($dingPublicKeyUrl) $dingPublicKey $dingPublicKeySha256 $dingInstallOutput = & $dingNodePath $dingInstaller install --manifest-url $dingManifestUrl --public-key $dingPublicKey --json $dingInstallExit = $LASTEXITCODE if ($dingInstallExit -ne 0) { throw "钉钉审批安装器执行失败:$dingInstallOutput" } $dingResult = $dingInstallOutput | ConvertFrom-Json if ($dingResult.ok -ne $true) { throw "钉钉审批安装失败:$($dingResult.error)" } $dingResult | ConvertTo-Json -Depth 20 -Compress } finally { if (Test-Path -LiteralPath $dingInstallTemp) { Remove-Item -LiteralPath $dingInstallTemp -Recurse -Force } }