feat: bootstrap portable Node runtimes
CI / Client installer (macos-latest) (push) Canceled after 0s
CI / Client installer (ubuntu-latest) (push) Canceled after 0s
CI / Client installer (windows-latest) (push) Canceled after 0s
CI / test (22) (push) Canceled after 0s
CI / test (24) (push) Canceled after 0s
CI / release-smoke (push) Canceled after 0s
CI / Client installer (macos-latest) (push) Canceled after 0s
CI / Client installer (ubuntu-latest) (push) Canceled after 0s
CI / Client installer (windows-latest) (push) Canceled after 0s
CI / test (22) (push) Canceled after 0s
CI / test (24) (push) Canceled after 0s
CI / release-smoke (push) Canceled after 0s
This commit is contained in:
@@ -15,7 +15,11 @@
|
||||
- 签名清单:`{{MANIFEST_URL}}`
|
||||
- 清单签名:`{{MANIFEST_SIGNATURE_URL}}`
|
||||
- 统一网关:`{{GATEWAY_URL}}`
|
||||
- Node.js:`>=22.13.0`
|
||||
- Windows 安装引导:`{{BOOTSTRAP_WINDOWS_URL}}`
|
||||
- Windows 安装引导 SHA-256:`{{BOOTSTRAP_WINDOWS_SHA256}}`
|
||||
- Linux/macOS 安装引导:`{{BOOTSTRAP_POSIX_URL}}`
|
||||
- Linux/macOS 安装引导 SHA-256:`{{BOOTSTRAP_POSIX_SHA256}}`
|
||||
- 私有 Node.js:`{{NODE_VERSION}}`(最低兼容版本 `{{NODE_MINIMUM_VERSION}}`)
|
||||
|
||||
首次安装通过上述 HTTPS 地址和固定 SHA-256 建立信任;安装器随后必须使用固定公钥验证 Ed25519 清单签名和全部发布物哈希。URL、TLS、签名或哈希任一不匹配都必须停止。不得索取 AppKey、AppSecret、AuthoritySecret、CorpId、userId、员工号、Cookie 或 Token。
|
||||
|
||||
@@ -23,7 +27,7 @@
|
||||
|
||||
只有以下条件全部成立,才能告诉用户“安装完成,可以使用”:
|
||||
|
||||
1. Node.js 版本满足要求,安装器与公钥 SHA-256 均正确;
|
||||
1. 对应平台安装引导的 SHA-256 正确,并已选用兼容的系统 Node 或经过官方哈希校验的私有 Node;
|
||||
2. 安装器 JSON 返回 `ok: true`;
|
||||
3. 已按当前 Agent 的官方方式安装返回的 `skillPath`;
|
||||
4. 已原样使用返回的 `mcpConfig` 注册并信任本地 stdio MCP;
|
||||
@@ -31,73 +35,79 @@
|
||||
6. `approval_list_todos` 的双来源验证中,钉钉 OA 与薪福通都为 `ok`;
|
||||
7. 未执行任何审批写操作。
|
||||
|
||||
## 步骤 1:检查运行环境
|
||||
## 步骤 1:确认操作系统与架构
|
||||
|
||||
在当前用户权限下运行:
|
||||
支持以下当前用户环境:
|
||||
|
||||
```text
|
||||
node --version
|
||||
npm --version
|
||||
```
|
||||
- Windows 10/11:x64 或 ARM64;
|
||||
- Linux:x64 或 ARM64;
|
||||
- macOS:Intel x64 或 Apple Silicon ARM64。
|
||||
|
||||
Node.js 必须为 `22.13.0` 或更高版本,npm 必须可用。不要使用管理员权限、`sudo`、全局 npm 安装、DWS 或浏览器自动化。若刚安装或升级 Node.js,先打开一个新的终端,再重新执行版本检查。
|
||||
系统无需预装 Node.js。安装引导会优先复用版本不低于 `{{NODE_MINIMUM_VERSION}}` 且 npm 可用的系统 Node;否则自动准备只属于本插件的 Node `{{NODE_VERSION}}`。不要让用户安装全局 Node,不要使用管理员权限、`sudo`、全局 npm 安装、DWS 或浏览器自动化。
|
||||
|
||||
## 步骤 2:下载并校验首次安装文件
|
||||
## 步骤 2:下载、校验并执行平台安装引导
|
||||
|
||||
不要在浏览器中复制下载内容,不要对 URL 解码、重新编码或手工拼接。原样使用上面的 HTTPS URL。
|
||||
不要在浏览器中复制脚本内容,不要对 URL 解码、重新编码或手工拼接。必须先校验安装引导 SHA-256,再执行它。
|
||||
|
||||
### Windows(PowerShell)
|
||||
|
||||
在普通 PowerShell 中逐行运行:
|
||||
|
||||
```powershell
|
||||
$dingInstallDir = Join-Path ([System.IO.Path]::GetTempPath()) ("dingmcp-install-" + [guid]::NewGuid().ToString("N"))
|
||||
New-Item -ItemType Directory -Path $dingInstallDir -Force | Out-Null
|
||||
$dingInstaller = Join-Path $dingInstallDir "installer.mjs"
|
||||
$dingPublicKey = Join-Path $dingInstallDir "release-signing-public.pem"
|
||||
Invoke-WebRequest -Uri "{{INSTALLER_URL}}" -OutFile $dingInstaller
|
||||
Invoke-WebRequest -Uri "{{PUBLIC_KEY_URL}}" -OutFile $dingPublicKey
|
||||
$dingInstallerHash = (Get-FileHash -Algorithm SHA256 $dingInstaller).Hash.ToLowerInvariant()
|
||||
$dingPublicKeyHash = (Get-FileHash -Algorithm SHA256 $dingPublicKey).Hash.ToLowerInvariant()
|
||||
if ($dingInstallerHash -ne "{{INSTALLER_SHA256}}") { throw "安装器 SHA-256 不匹配,停止安装" }
|
||||
if ($dingPublicKeyHash -ne "{{PUBLIC_KEY_SHA256}}") { throw "发布公钥 SHA-256 不匹配,停止安装" }
|
||||
```
|
||||
|
||||
任一 `Invoke-WebRequest` 或哈希检查失败都必须停止,不能忽略错误继续。
|
||||
|
||||
### Linux / macOS(POSIX shell)
|
||||
|
||||
```bash
|
||||
ding_install_dir="$(mktemp -d)"
|
||||
curl --fail --location --output "$ding_install_dir/installer.mjs" "{{INSTALLER_URL}}"
|
||||
curl --fail --location --output "$ding_install_dir/release-signing-public.pem" "{{PUBLIC_KEY_URL}}"
|
||||
printf '%s %s\n' '{{INSTALLER_SHA256}}' "$ding_install_dir/installer.mjs" | sha256sum --check --strict
|
||||
printf '%s %s\n' '{{PUBLIC_KEY_SHA256}}' "$ding_install_dir/release-signing-public.pem" | sha256sum --check --strict
|
||||
```
|
||||
|
||||
macOS 没有 `sha256sum` 时,使用 `shasum -a 256` 分别计算并精确比较,不得跳过校验。
|
||||
|
||||
## 步骤 3:执行唯一通用安装入口
|
||||
|
||||
### Windows(PowerShell)
|
||||
|
||||
继续使用步骤 2 的同一个 PowerShell:
|
||||
|
||||
```powershell
|
||||
$dingInstallJson = & node $dingInstaller install --manifest-url "{{MANIFEST_URL}}" --public-key $dingPublicKey --json
|
||||
if ($LASTEXITCODE -ne 0) { throw "钉钉审批安装器执行失败:$dingInstallJson" }
|
||||
$dingBootstrapDir = Join-Path ([System.IO.Path]::GetTempPath()) ("dingmcp-bootstrap-" + [guid]::NewGuid().ToString("N"))
|
||||
New-Item -ItemType Directory -Path $dingBootstrapDir -Force | Out-Null
|
||||
$dingBootstrap = Join-Path $dingBootstrapDir "dingtalk-approval-bootstrap.ps1"
|
||||
Invoke-WebRequest -UseBasicParsing -Uri "{{BOOTSTRAP_WINDOWS_URL}}" -OutFile $dingBootstrap
|
||||
$dingBootstrapHash = (Get-FileHash -Algorithm SHA256 $dingBootstrap).Hash.ToLowerInvariant()
|
||||
if ($dingBootstrapHash -ne "{{BOOTSTRAP_WINDOWS_SHA256}}") { throw "Windows 安装引导 SHA-256 不匹配,停止安装" }
|
||||
$dingInstallJson = & powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File $dingBootstrap
|
||||
if ($LASTEXITCODE -ne 0) { throw "钉钉审批安装引导执行失败:$dingInstallJson" }
|
||||
$dingInstall = $dingInstallJson | ConvertFrom-Json
|
||||
if ($dingInstall.ok -ne $true) { throw "钉钉审批安装失败:$($dingInstall.error)" }
|
||||
$dingInstall | ConvertTo-Json -Depth 20
|
||||
```
|
||||
|
||||
### Linux / macOS
|
||||
完成后可删除 `$dingBootstrapDir`。任一下载或哈希检查失败都不能忽略。
|
||||
|
||||
### Linux / macOS(POSIX shell)
|
||||
|
||||
```bash
|
||||
node "$ding_install_dir/installer.mjs" install --manifest-url "{{MANIFEST_URL}}" --public-key "$ding_install_dir/release-signing-public.pem" --json
|
||||
ding_bootstrap_dir="$(mktemp -d)"
|
||||
ding_bootstrap="$ding_bootstrap_dir/dingtalk-approval-bootstrap.sh"
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
curl --fail --location --output "$ding_bootstrap" "{{BOOTSTRAP_POSIX_URL}}"
|
||||
elif command -v wget >/dev/null 2>&1; then
|
||||
wget --https-only --output-document="$ding_bootstrap" "{{BOOTSTRAP_POSIX_URL}}"
|
||||
else
|
||||
echo '缺少 curl 或 wget,无法下载安装引导' >&2
|
||||
exit 1
|
||||
fi
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
ding_bootstrap_hash="$(sha256sum "$ding_bootstrap" | awk '{print $1}')"
|
||||
else
|
||||
ding_bootstrap_hash="$(shasum -a 256 "$ding_bootstrap" | awk '{print $1}')"
|
||||
fi
|
||||
if [ "$ding_bootstrap_hash" != '{{BOOTSTRAP_POSIX_SHA256}}' ]; then
|
||||
echo 'Linux/macOS 安装引导 SHA-256 不匹配,停止安装' >&2
|
||||
exit 1
|
||||
fi
|
||||
ding_install_json="$(sh "$ding_bootstrap")"
|
||||
printf '%s\n' "$ding_install_json"
|
||||
```
|
||||
|
||||
这是唯一通用安装入口。安装器会在稳定的当前用户数据目录安装客户端与 Skill,不会全局安装 npm 包,也不会修改 Agent 私有配置。保留完整 JSON 结果,后续只使用其中的真实字段,不要猜路径。
|
||||
完成后可删除 `$ding_bootstrap_dir`。如果 `sha256sum` 与 `shasum` 都不存在,必须停止,不能跳过校验。
|
||||
|
||||
## 步骤 3:验收安装结果
|
||||
|
||||
步骤 2 的平台引导是唯一通用安装入口。它会:
|
||||
|
||||
1. 检查系统 Node;缺失或版本不兼容时,根据当前操作系统和 CPU 架构选择私有 Node;
|
||||
2. 按“阿里云精确版本 → npmmirror 精确版本 → Node 官方精确版本”的顺序下载。只有网络失败、超时或 404 才切换来源;任一已下载文件的 SHA-256 不匹配都会立即终止;
|
||||
3. 把私有 Node 放在插件自己的当前用户数据目录,不修改系统 PATH,不要求管理员权限;
|
||||
4. 下载并校验首次安装器和发布公钥,再由安装器验证 Ed25519 清单签名与全部客户端发布物;
|
||||
5. 返回一份 JSON 安装结果。
|
||||
|
||||
安装器会在稳定的当前用户数据目录安装客户端与 Skill,不会全局安装 npm 包,也不会修改 Agent 私有配置。保留完整 JSON 结果,后续只使用其中的真实字段,不要猜路径。
|
||||
|
||||
安装结果至少必须包含:
|
||||
|
||||
@@ -106,6 +116,7 @@ node "$ding_install_dir/installer.mjs" install --manifest-url "{{MANIFEST_URL}}"
|
||||
- `loginCommand`;
|
||||
- `mcpConfig.mcpServers.dingtalk-approval`;
|
||||
- `postInstall`。
|
||||
- `nodeRuntime`,其中 `managed: true` 表示正在使用插件私有 Node。
|
||||
|
||||
Windows 的 `bridgePath` 和 `mcpConfig...command` 正常应以 `dingtalk-approval-bridge.cmd` 结尾。路径中可能包含空格;保留完整字符串,不要拆分,不要去掉 `.cmd`,不要改为 `node`、`npx` 或远程 URL。
|
||||
|
||||
@@ -155,7 +166,7 @@ $dingLogin | ConvertTo-Json -Depth 20
|
||||
1. 如果当前 Agent 要求信任本地连接器,按其官方界面或命令确认名称为 `dingtalk-approval` 的连接器。
|
||||
2. 如果不能热加载,完整退出并重新启动 Agent,或新建一个确定会重新读取 MCP 配置的会话;只刷新聊天内容通常不够。
|
||||
3. 重启后确认能发现 `approval_login`、`approval_list_todos` 等钉钉审批工具。当前版本应发现 13 个工具。
|
||||
4. 若连接器进程退出、显示 302 或无法启动,先执行步骤 9 的 Windows 排障,不要把连接器改成远程 HTTP MCP。
|
||||
4. 若连接器进程退出、显示 302 或无法启动,先执行步骤 9 的故障排查,不要把连接器改成远程 HTTP MCP。
|
||||
|
||||
## 步骤 7:确认登录并验证两个审批来源
|
||||
|
||||
@@ -173,17 +184,18 @@ $dingLogin | ConvertTo-Json -Depth 20
|
||||
|
||||
安装阶段不得处理审批,也不得在登录和双来源验证完成前声称“已可使用”。
|
||||
|
||||
## 步骤 9:Windows 常见故障排查
|
||||
## 步骤 9:常见故障排查
|
||||
|
||||
按顺序排查,不要跳到重装:
|
||||
|
||||
1. **显示 302 或启动后立即退出**:检查连接器是否被错误配置成 `{{GATEWAY_URL}}` 或其他 HTTP URL。正确配置必须是 `type: "stdio"`,`command` 为本机 `dingtalk-approval-bridge.cmd`,`args: []`。重新复制安装结果中的 `mcpConfig`。
|
||||
2. **路径不存在或 Node 升级后失效**:不要引用 Node/NVM 的版本目录。执行安装结果中的 `cliPath`,或运行 `dingtalk-approval print-config --format mcp-json` 重新取得稳定配置。
|
||||
3. **Bridge 仍无法启动**:执行 `<cliPath> doctor --json`。只有所有 `checks` 都为 `ok: true` 才继续;把失败检查和错误原文报告给维护者,不要提供 Token 或会话文件。
|
||||
4. **提示找不到 npm-cli.js**:当前 Node 安装不完整。安装带 npm 的 Node.js 22.13+,打开新 PowerShell,重新检查 `node --version` 和 `npm --version` 后再运行安装器。
|
||||
4. **提示找不到 npm-cli.js 或 Node 运行时损坏**:重新执行步骤 2 中已经过哈希校验的平台引导,让它修复插件私有 Node;不要安装全局 npm。
|
||||
5. **路径包含空格**:不要手写命令行字符串。把安装器返回的整个 `mcpConfig` 作为 JSON 对象交给 Agent;在 PowerShell 中执行 CLI 时用调用运算符 `&` 和独立参数。
|
||||
6. **连接器一直未出现**:确认已信任本地连接器并完整重启 Agent。不要通过反复重装刷新连接器。
|
||||
7. **授权链接过期**:MCP 已加载就再次调用 `approval_login`;否则执行保存的 `loginCommand`。不要重装。
|
||||
8. **Linux/macOS 缺少下载或校验命令**:必须先具备 `curl` 或 `wget`,以及 `sha256sum` 或 `shasum`;不能绕过 SHA-256 校验。系统 `tar` 必须支持 gzip 归档。
|
||||
|
||||
## 更新
|
||||
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
ding_source_bases='{{NODE_SOURCE_BASES_POSIX}}'
|
||||
ding_installer_url='{{INSTALLER_URL}}'
|
||||
ding_installer_sha256='{{INSTALLER_SHA256}}'
|
||||
ding_public_key_url='{{PUBLIC_KEY_URL}}'
|
||||
ding_public_key_sha256='{{PUBLIC_KEY_SHA256}}'
|
||||
ding_manifest_url='{{MANIFEST_URL}}'
|
||||
|
||||
ding_temp_dir="$(mktemp -d "${TMPDIR:-/tmp}/dingmcp-install.XXXXXX")"
|
||||
trap 'rm -rf "$ding_temp_dir"' EXIT HUP INT TERM
|
||||
|
||||
ding_hash_file() {
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
sha256sum "$1" | awk '{print $1}'
|
||||
elif command -v shasum >/dev/null 2>&1; then
|
||||
shasum -a 256 "$1" | awk '{print $1}'
|
||||
else
|
||||
echo '缺少 sha256sum 或 shasum,无法安全安装' >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
ding_fetch() {
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
if ding_fetch_http="$(curl --fail --location --silent --show-error --connect-timeout 10 --max-time 300 \
|
||||
--write-out '%{http_code}' --output "$2" "$1")"; then
|
||||
return 0
|
||||
else
|
||||
ding_fetch_status=$?
|
||||
fi
|
||||
if { [ "$ding_fetch_status" -eq 22 ] && [ "$ding_fetch_http" = '404' ]; } \
|
||||
|| [ "$ding_fetch_status" -eq 5 ] || [ "$ding_fetch_status" -eq 6 ] \
|
||||
|| [ "$ding_fetch_status" -eq 7 ] || [ "$ding_fetch_status" -eq 18 ] \
|
||||
|| [ "$ding_fetch_status" -eq 28 ] || [ "$ding_fetch_status" -eq 35 ] \
|
||||
|| [ "$ding_fetch_status" -eq 52 ] || [ "$ding_fetch_status" -eq 55 ] \
|
||||
|| [ "$ding_fetch_status" -eq 56 ]; then
|
||||
return 75
|
||||
fi
|
||||
echo "下载失败且不允许换源(HTTP ${ding_fetch_http:-unknown} / curl $ding_fetch_status):$1" >&2
|
||||
return 1
|
||||
elif command -v wget >/dev/null 2>&1; then
|
||||
ding_wget_log="${2}.wget.log"
|
||||
if wget --https-only --timeout=300 --server-response --output-document="$2" "$1" 2>"$ding_wget_log"; then
|
||||
rm -f "$ding_wget_log"
|
||||
return 0
|
||||
else
|
||||
ding_fetch_status=$?
|
||||
fi
|
||||
ding_fetch_http="$(awk '/^ HTTP\/[0-9.]+ [0-9][0-9][0-9]/{code=$2} END{print code}' "$ding_wget_log")"
|
||||
cat "$ding_wget_log" >&2
|
||||
rm -f "$ding_wget_log"
|
||||
if [ "$ding_fetch_http" = '404' ] || [ "$ding_fetch_status" -eq 4 ] || [ "$ding_fetch_status" -eq 5 ]; then
|
||||
return 75
|
||||
fi
|
||||
echo "下载失败且不允许换源(HTTP ${ding_fetch_http:-unknown} / wget $ding_fetch_status):$1" >&2
|
||||
return 1
|
||||
else
|
||||
echo '缺少 curl 或 wget,无法下载安装文件' >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
ding_download_verified() {
|
||||
ding_download_target="$1"
|
||||
ding_download_expected="$2"
|
||||
shift 2
|
||||
ding_download_succeeded=0
|
||||
for ding_download_url in "$@"; do
|
||||
rm -f "$ding_download_target"
|
||||
if ding_fetch "$ding_download_url" "$ding_download_target"; then
|
||||
ding_download_succeeded=1
|
||||
ding_download_actual="$(ding_hash_file "$ding_download_target")"
|
||||
if [ "$ding_download_actual" != "$ding_download_expected" ]; then
|
||||
echo "下载文件 SHA-256 不匹配,停止安装:$ding_download_url" >&2
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
else
|
||||
ding_download_status=$?
|
||||
if [ "$ding_download_status" -ne 75 ]; then return 1; fi
|
||||
fi
|
||||
done
|
||||
if [ "$ding_download_succeeded" -eq 0 ]; then
|
||||
echo '所有下载源均不可用' >&2
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
ding_test_node() {
|
||||
[ -x "$1" ] || return 1
|
||||
"$1" -e 'const [a,b]=process.versions.node.split(".").map(Number);process.exit(a>22||(a===22&&b>=13)?0:1)' >/dev/null 2>&1
|
||||
}
|
||||
|
||||
case "$(uname -s)" in
|
||||
Linux) ding_os='linux' ;;
|
||||
Darwin) ding_os='darwin' ;;
|
||||
*) echo "不支持的操作系统:$(uname -s)" >&2; exit 1 ;;
|
||||
esac
|
||||
case "$(uname -m)" in
|
||||
x86_64|amd64) ding_arch='x64' ;;
|
||||
arm64|aarch64) ding_arch='arm64' ;;
|
||||
*) echo "不支持的 CPU 架构:$(uname -m)" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
case "$ding_os-$ding_arch" in
|
||||
linux-x64) ding_node_file='{{NODE_LINUX_X64_FILE}}'; ding_node_sha256='{{NODE_LINUX_X64_SHA256}}' ;;
|
||||
linux-arm64) ding_node_file='{{NODE_LINUX_ARM64_FILE}}'; ding_node_sha256='{{NODE_LINUX_ARM64_SHA256}}' ;;
|
||||
darwin-x64) ding_node_file='{{NODE_DARWIN_X64_FILE}}'; ding_node_sha256='{{NODE_DARWIN_X64_SHA256}}' ;;
|
||||
darwin-arm64) ding_node_file='{{NODE_DARWIN_ARM64_FILE}}'; ding_node_sha256='{{NODE_DARWIN_ARM64_SHA256}}' ;;
|
||||
esac
|
||||
|
||||
ding_node_path=''
|
||||
if command -v node >/dev/null 2>&1 && command -v npm >/dev/null 2>&1 && ding_test_node "$(command -v node)"; then
|
||||
ding_node_path="$(command -v node)"
|
||||
else
|
||||
if [ -n "${DINGTALK_APPROVAL_CLIENT_HOME:-}" ]; then
|
||||
ding_client_root="$DINGTALK_APPROVAL_CLIENT_HOME"
|
||||
elif [ "$ding_os" = 'darwin' ]; then
|
||||
ding_client_root="$HOME/Library/Application Support/dingtalk-approval"
|
||||
else
|
||||
ding_client_root="${XDG_DATA_HOME:-$HOME/.local/share}/dingtalk-approval"
|
||||
fi
|
||||
ding_runtime_name="${ding_node_file%.tar.gz}"
|
||||
ding_runtime_parent="$ding_client_root/node"
|
||||
ding_runtime_dir="$ding_runtime_parent/$ding_runtime_name"
|
||||
ding_node_path="$ding_runtime_dir/bin/node"
|
||||
ding_npm_cli="$ding_runtime_dir/lib/node_modules/npm/bin/npm-cli.js"
|
||||
if ! ding_test_node "$ding_node_path" || [ ! -f "$ding_npm_cli" ]; then
|
||||
mkdir -p "$ding_runtime_parent"
|
||||
chmod 700 "$ding_client_root" "$ding_runtime_parent" 2>/dev/null || true
|
||||
ding_runtime_archive="$ding_temp_dir/$ding_node_file"
|
||||
ding_runtime_urls=''
|
||||
for ding_source_base in $ding_source_bases; do
|
||||
ding_runtime_urls="$ding_runtime_urls $ding_source_base$ding_node_file"
|
||||
done
|
||||
# The URLs are fixed by the signed release process and contain no whitespace.
|
||||
# shellcheck disable=SC2086
|
||||
ding_download_verified "$ding_runtime_archive" "$ding_node_sha256" $ding_runtime_urls
|
||||
ding_extract_root="$ding_temp_dir/node-extract"
|
||||
mkdir -p "$ding_extract_root"
|
||||
tar -xzf "$ding_runtime_archive" -C "$ding_extract_root"
|
||||
ding_extracted_dir="$ding_extract_root/$ding_runtime_name"
|
||||
if ! ding_test_node "$ding_extracted_dir/bin/node" || [ ! -f "$ding_extracted_dir/lib/node_modules/npm/bin/npm-cli.js" ]; then
|
||||
echo '便携 Node 归档缺少可用的 node 或 npm-cli.js' >&2
|
||||
exit 1
|
||||
fi
|
||||
rm -rf "$ding_runtime_dir"
|
||||
mv "$ding_extracted_dir" "$ding_runtime_dir"
|
||||
fi
|
||||
fi
|
||||
|
||||
ding_installer="$ding_temp_dir/installer.mjs"
|
||||
ding_public_key="$ding_temp_dir/release-signing-public.pem"
|
||||
ding_download_verified "$ding_installer" "$ding_installer_sha256" "$ding_installer_url"
|
||||
ding_download_verified "$ding_public_key" "$ding_public_key_sha256" "$ding_public_key_url"
|
||||
|
||||
"$ding_node_path" "$ding_installer" install --manifest-url "$ding_manifest_url" --public-key "$ding_public_key" --json
|
||||
@@ -0,0 +1,111 @@
|
||||
$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 }
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"version": "22.23.2",
|
||||
"minimumVersion": "22.13.0",
|
||||
"sourceBases": [
|
||||
"https://mirrors.aliyun.com/nodejs-release/v22.23.2/",
|
||||
"https://npmmirror.com/mirrors/node/v22.23.2/",
|
||||
"https://nodejs.org/dist/v22.23.2/"
|
||||
],
|
||||
"platforms": {
|
||||
"win32-x64": {
|
||||
"file": "node-v22.23.2-win-x64.zip",
|
||||
"sha256": "1177b4137ba5adaa56354ae40f1080c7450e8ae09cecb47da459d1c52ac99f97"
|
||||
},
|
||||
"win32-arm64": {
|
||||
"file": "node-v22.23.2-win-arm64.zip",
|
||||
"sha256": "fec025a6da31757e3b6af84c5a1628e9d38442ca99a2161091d78f2fcfa35ef3"
|
||||
},
|
||||
"linux-x64": {
|
||||
"file": "node-v22.23.2-linux-x64.tar.gz",
|
||||
"sha256": "b294a556e639d64338823920e5866c21c02741742d2e1529ee1a225c1ec9252a"
|
||||
},
|
||||
"linux-arm64": {
|
||||
"file": "node-v22.23.2-linux-arm64.tar.gz",
|
||||
"sha256": "013b59cfd2819703a6f4a14ab891fc46fc2a4e3f5bcd92de3fb4929b43e35b30"
|
||||
},
|
||||
"darwin-x64": {
|
||||
"file": "node-v22.23.2-darwin-x64.tar.gz",
|
||||
"sha256": "58e99022c2ff89395576cc7fd4d98cea24bb68081475d5f88b801ee8729fb026"
|
||||
},
|
||||
"darwin-arm64": {
|
||||
"file": "node-v22.23.2-darwin-arm64.tar.gz",
|
||||
"sha256": "61130f394c1630d211dd50aecc4353d379480f36d3ac913cd85dbba1aed585c6"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user