$ErrorActionPreference = "Stop" $ProgressPreference = "SilentlyContinue" $dingUtf8 = New-Object System.Text.UTF8Encoding($false) [Console]::InputEncoding = $dingUtf8 [Console]::OutputEncoding = $dingUtf8 $OutputEncoding = $dingUtf8 if (Get-Variable -Name PSNativeCommandUseErrorActionPreference -ErrorAction SilentlyContinue) { $PSNativeCommandUseErrorActionPreference = $false } [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 $dingSourceBases = @("https://mirrors.aliyun.com/nodejs-release/v22.23.2/", "https://npmmirror.com/mirrors/node/v22.23.2/", "https://nodejs.org/dist/v22.23.2/") $dingInstallerUrls = @("https://git.justconn.top/gitea-admin/dingMCP/releases/download/v1.1.0-rc.20/dingtalk-approval-installer.mjs", "https://approval.justconn.top/downloads/dingmcp/dingtalk-approval-installer.mjs") $dingInstallerSha256 = "d72e642f7296821ca8b25f0eda48c7104032f650dfe2f95582fb4a8991c10254" $dingPublicKeyUrls = @("https://git.justconn.top/gitea-admin/dingMCP/releases/download/v1.1.0-rc.20/release-signing-public.pem", "https://approval.justconn.top/downloads/dingmcp/release-signing-public.pem") $dingPublicKeySha256 = "1fb639eeb7d6ac9b112c7c6d3224ca7fb4f74db0c9cf84f28ad1d1cb89260adf" $dingManifestUrl = "https://git.justconn.top/gitea-admin/dingMCP/releases/download/v1.1.0-rc.20/manifest.json" $dingManifestMirrorUrls = @("https://approval.justconn.top/downloads/dingmcp/manifest.json") function Write-DingProgress([string] $dingMessage) { [Console]::Error.WriteLine("[dingtalk-approval] $dingMessage") } 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 $dingCurl = Get-Command curl.exe -ErrorAction SilentlyContinue foreach ($dingUrl in $dingUrls) { Write-DingProgress "正在下载并校验:$dingUrl" try { if (Test-Path -LiteralPath $dingTarget) { Remove-Item -LiteralPath $dingTarget -Force } if ($dingCurl) { $dingCurlHttp = & $dingCurl.Source --fail --location --silent --show-error --connect-timeout 10 --max-time 90 --speed-time 20 --speed-limit 1024 --write-out "%{http_code}" --output $dingTarget $dingUrl $dingCurlExit = $LASTEXITCODE if ($dingCurlExit -ne 0) { if ($dingCurlHttp -match '^[1-5][0-9][0-9]$' -and $dingCurlHttp -ne '000') { throw "HTTP $dingCurlHttp" } throw "curl.exe 退出码 $dingCurlExit" } } else { Invoke-WebRequest -UseBasicParsing -Uri $dingUrl -OutFile $dingTarget -TimeoutSec 90 } } 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 -and $dingLastError -match 'HTTP ([0-9][0-9][0-9])') { $dingStatusCode = [int]$Matches[1] } 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-v22.23.2-win-x64.zip"; $dingNodeSha256 = "1177b4137ba5adaa56354ae40f1080c7450e8ae09cecb47da459d1c52ac99f97" } "arm64" { $dingNodeFile = "node-v22.23.2-win-arm64.zip"; $dingNodeSha256 = "fec025a6da31757e3b6af84c5a1628e9d38442ca99a2161091d78f2fcfa35ef3" } default { throw "不支持的 Windows CPU 架构:$dingArchitecture" } } Write-DingProgress "1/4 检查 Windows-$dingArchitecture 的 Node/npm 运行环境" $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($dingNodeFile) $dingRuntimeDir = Join-Path $dingRuntimeParent $dingRuntimeName $dingNodePath = Join-Path $dingRuntimeDir "node.exe" if (-not (Test-DingNode $dingNodePath $true)) { Write-DingProgress "2/4 系统 Node 不可用,准备插件私有 Node;下载可能需要一些时间" 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 } } } else { Write-DingProgress "2/4 复用已安装的插件私有 Node" } } else { Write-DingProgress "2/4 复用兼容的系统 Node" } Write-DingProgress "3/4 下载并校验签名安装器与发布公钥" $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 $dingInstallerUrls $dingInstaller $dingInstallerSha256 Save-DingDownload $dingPublicKeyUrls $dingPublicKey $dingPublicKeySha256 Write-DingProgress "4/4 执行安装器;最终 JSON 输出到 stdout,阶段进度输出到 stderr" $dingInstallerArgs = @($dingInstaller, "install", "--manifest-url", $dingManifestUrl) foreach ($dingManifestMirrorUrl in $dingManifestMirrorUrls) { $dingInstallerArgs += @("--manifest-mirror-url", $dingManifestMirrorUrl) } $dingInstallerArgs += @("--public-key", $dingPublicKey, "--json") $dingNodeProxyArgs = @() $dingProxyConfigured = $env:HTTPS_PROXY -or $env:HTTP_PROXY -or $env:ALL_PROXY $dingProxyAlreadyEnabled = ($env:NODE_OPTIONS -match '(^|\s)--use-env-proxy($|\s)') -or ($env:NODE_USE_ENV_PROXY -match '^(1|true)$') if ($dingProxyConfigured -and -not $dingProxyAlreadyEnabled) { & $dingNodePath -e "process.exit(process.allowedNodeEnvironmentFlags.has('--use-env-proxy') ? 0 : 1)" if ($LASTEXITCODE -eq 0) { $dingNodeProxyArgs = @("--use-env-proxy") } } $dingInstallOutput = & $dingNodePath @dingNodeProxyArgs @dingInstallerArgs $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 } }