fix: make Windows portable Node bootstrap reliable

This commit is contained in:
2026-08-19 01:12:13 +00:00
parent 0d5e192e95
commit 7e2a01c913
6 changed files with 17 additions and 3 deletions
@@ -68,6 +68,10 @@
在普通 PowerShell 中逐行运行: 在普通 PowerShell 中逐行运行:
```powershell ```powershell
$dingUtf8 = New-Object System.Text.UTF8Encoding($false)
[Console]::InputEncoding = $dingUtf8
[Console]::OutputEncoding = $dingUtf8
$OutputEncoding = $dingUtf8
$dingBootstrapDir = Join-Path ([System.IO.Path]::GetTempPath()) ("dingmcp-bootstrap-" + [guid]::NewGuid().ToString("N")) $dingBootstrapDir = Join-Path ([System.IO.Path]::GetTempPath()) ("dingmcp-bootstrap-" + [guid]::NewGuid().ToString("N"))
New-Item -ItemType Directory -Path $dingBootstrapDir -Force | Out-Null New-Item -ItemType Directory -Path $dingBootstrapDir -Force | Out-Null
$dingBootstrap = Join-Path $dingBootstrapDir "dingtalk-approval-bootstrap.ps1" $dingBootstrap = Join-Path $dingBootstrapDir "dingtalk-approval-bootstrap.ps1"
+5 -1
View File
@@ -1,5 +1,9 @@
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue" $ProgressPreference = "SilentlyContinue"
$dingUtf8 = New-Object System.Text.UTF8Encoding($false)
[Console]::InputEncoding = $dingUtf8
[Console]::OutputEncoding = $dingUtf8
$OutputEncoding = $dingUtf8
if (Get-Variable -Name PSNativeCommandUseErrorActionPreference -ErrorAction SilentlyContinue) { if (Get-Variable -Name PSNativeCommandUseErrorActionPreference -ErrorAction SilentlyContinue) {
$PSNativeCommandUseErrorActionPreference = $false $PSNativeCommandUseErrorActionPreference = $false
} }
@@ -92,7 +96,7 @@ if (-not (Test-DingNode $dingNodePath $false)) {
Join-Path $env:LOCALAPPDATA "DingTalkApproval" Join-Path $env:LOCALAPPDATA "DingTalkApproval"
} }
$dingRuntimeParent = Join-Path $dingClientRoot "node" $dingRuntimeParent = Join-Path $dingClientRoot "node"
$dingRuntimeName = [IO.Path]::GetFileNameWithoutExtension([IO.Path]::GetFileNameWithoutExtension($dingNodeFile)) $dingRuntimeName = [IO.Path]::GetFileNameWithoutExtension($dingNodeFile)
$dingRuntimeDir = Join-Path $dingRuntimeParent $dingRuntimeName $dingRuntimeDir = Join-Path $dingRuntimeParent $dingRuntimeName
$dingNodePath = Join-Path $dingRuntimeDir "node.exe" $dingNodePath = Join-Path $dingRuntimeDir "node.exe"
if (-not (Test-DingNode $dingNodePath $true)) { if (-not (Test-DingNode $dingNodePath $true)) {
+1 -1
View File
@@ -16,7 +16,7 @@
兼容发行物只为不支持远程 MCP/OAuth 的宿主保留。它不使用全局 npm prefix:首次安装器以固定 Ed25519 公钥验证 schema v3 清单和 artifact SHA-256,并把内置依赖展开到稳定的用户目录。Windows/Linux/macOS 引导可复用系统 Node 或安装经官方哈希校验的固定私有 Node,不改 PATH、不要求管理员权限。发布物优先使用国内 Gitea 和 Node 固定版本源,自建 HTTPS/官方源作为受同一签名或哈希保护的备用。 兼容发行物只为不支持远程 MCP/OAuth 的宿主保留。它不使用全局 npm prefix:首次安装器以固定 Ed25519 公钥验证 schema v3 清单和 artifact SHA-256,并把内置依赖展开到稳定的用户目录。Windows/Linux/macOS 引导可复用系统 Node 或安装经官方哈希校验的固定私有 Node,不改 PATH、不要求管理员权限。发布物优先使用国内 Gitea 和 Node 固定版本源,自建 HTTPS/官方源作为受同一签名或哈希保护的备用。
兼容 Windows 引导以 UTF-8 BOM 发布,确保 Windows PowerShell 5.1 正确解析中文;传给 `node -e` 的脚本使用 PowerShell 不会吞掉的引号组合。下载优先调用 `curl.exe` 复用代理,并设置 10 秒连接、20 秒低速和 90 秒总时限;`Invoke-WebRequest` 只在没有 curl 时兜底。安装器仍输出稳定 `bridgePath`、`skillPath`、`mcpConfig` 和 `postInstall`,保留原子安装、跨进程锁、明确诊断、更新与卸载能力。 兼容 Windows 引导以 UTF-8 BOM 发布,确保 Windows PowerShell 5.1 正确解析中文,并显式把控制台和原生程序管道固定为 UTF-8,避免 Node 返回的 JSON/诊断被按系统代码页解码。传给 `node -e` 的脚本使用 PowerShell 不会吞掉的引号组合。Windows ZIP 目录名只去掉一次 `.zip`,避免把 Node 语义版本中的 `.2-win-x64` 误当作第二层扩展名。下载优先调用 `curl.exe` 复用代理,并设置 10 秒连接、20 秒低速和 90 秒总时限;`Invoke-WebRequest` 只在没有 curl 时兜底。安装器仍输出稳定 `bridgePath`、`skillPath`、`mcpConfig` 和 `postInstall`,保留原子安装、跨进程锁、明确诊断、更新与卸载能力。
## Agent 领域接口 ## Agent 领域接口
+1 -1
View File
@@ -12,7 +12,7 @@
- 远程 OAuth 后可发现全部 12 个业务工具,`approval_list_todos(source=all, limit=1)` 中钉钉 OA 与薪福通均为 `ok`;网关重启后宿主能刷新或重新授权,用户身份仍由服务端 Token 决定。 - 远程 OAuth 后可发现全部 12 个业务工具,`approval_list_todos(source=all, limit=1)` 中钉钉 OA 与薪福通均为 `ok`;网关重启后宿主能刷新或重新授权,用户身份仍由服务端 Token 决定。
- `install.md` 只有在能力缺失时才链接 `compat-install.md`。用至少一个只支持 stdio 的宿主验证兼容安装可发现 `approval_login` 加 12 个业务工具,且不产生第二套用户身份。 - `install.md` 只有在能力缺失时才链接 `compat-install.md`。用至少一个只支持 stdio 的宿主验证兼容安装可发现 `approval_login` 加 12 个业务工具,且不产生第二套用户身份。
- 在未安装 Node 的干净 Windows x64/ARM64、Linux x64/ARM64、macOS Intel/Apple Silicon 环境验证兼容引导。确认固定版本、国内镜像回退、哈希失败即停止、私有 Node 不改 PATH;至少每类操作系统一次端到端,其余架构由 CI/制品校验覆盖。 - 在未安装 Node 的干净 Windows x64/ARM64、Linux x64/ARM64、macOS Intel/Apple Silicon 环境验证兼容引导。确认固定版本、国内镜像回退、哈希失败即停止、私有 Node 不改 PATH;至少每类操作系统一次端到端,其余架构由 CI/制品校验覆盖。
- 在 Windows PowerShell 5.1 验证生成的 `.ps1` 具有 UTF-8 BOM,中文可解析,Node 版本和代理能力探测不会因原生程序参数引号被吞而失败。Windows 下载优先 `curl.exe` 并继承 `HTTPS_PROXY`;连接 10 秒、低速 20 秒或总计 90 秒触发明确失败换源,不得出现 300 秒静默等待。 - 在 Windows PowerShell 5.1 验证生成的 `.ps1` 具有 UTF-8 BOM,中文可解析,Node 版本和代理能力探测不会因原生程序参数引号被吞而失败;确认 `.zip` 仅去除一次扩展名,解压目录与 `node-v<version>-win-<arch>` 完全一致。Windows 下载优先 `curl.exe` 并继承 `HTTPS_PROXY`;连接 10 秒、低速 20 秒或总计 90 秒触发明确失败换源,不得出现 300 秒静默等待。
- 兼容安装继续验证 stdout 只有最终 JSON、stderr 有阶段进度;模拟离线 npm 超时、外部终止、死亡锁、staging 和并发安装,确认明确失败、安全恢复和零并发写入。父进程含 Node 代理选项时,npm 子进程不得继承 `--use-env-proxy`。 - 兼容安装继续验证 stdout 只有最终 JSON、stderr 有阶段进度;模拟离线 npm 超时、外部终止、死亡锁、staging 和并发安装,确认明确失败、安全恢复和零并发写入。父进程含 Node 代理选项时,npm 子进程不得继承 `--use-env-proxy`。
- 兼容安装结果的 `loginCommand` 能在 MCP 未加载时刷新登录链接;MCP 已加载时 `approval_login` 完成同一操作。不得因链接过期而重装,首次成功响应只展示一次 onboarding 示例。 - 兼容安装结果的 `loginCommand` 能在 MCP 未加载时刷新登录链接;MCP 已加载时 `approval_login` 完成同一操作。不得因链接过期而重装,首次成功响应只展示一次 onboarding 示例。
- 只读验证待办、本人已处理历史、本人发起历史、历史默认最近 30 天及显式时间窗口、搜索、详情、费用结构化字段、动作、模板 Schema、部门/城市选项和越权拒绝;历史引用必须被动作查询和批处理拒绝。 - 只读验证待办、本人已处理历史、本人发起历史、历史默认最近 30 天及显式时间窗口、搜索、详情、费用结构化字段、动作、模板 Schema、部门/城市选项和越权拒绝;历史引用必须被动作查询和批处理拒绝。
+3
View File
@@ -110,8 +110,11 @@ try {
const posixBootstrap = readFileSync(posixBootstrapPath, "utf8"); const posixBootstrap = readFileSync(posixBootstrapPath, "utf8");
if (windowsBootstrap.includes("{{") || posixBootstrap.includes("{{") if (windowsBootstrap.includes("{{") || posixBootstrap.includes("{{")
|| !windowsBootstrap.includes("Save-DingDownload") || !windowsBootstrap.includes("Expand-Archive") || !windowsBootstrap.includes("Save-DingDownload") || !windowsBootstrap.includes("Expand-Archive")
|| !windowsBootstrap.includes("[Console]::OutputEncoding = $dingUtf8") || !windowsBootstrap.includes("$OutputEncoding = $dingUtf8")
|| !windowsBootstrap.includes("curl.exe") || !windowsBootstrap.includes("--max-time 90") || !windowsBootstrap.includes("curl.exe") || !windowsBootstrap.includes("--max-time 90")
|| !windowsBootstrap.includes(`-e "const [a,b]=process.versions.node.split('.').map(Number)`) || !windowsBootstrap.includes(`-e "const [a,b]=process.versions.node.split('.').map(Number)`)
|| !windowsBootstrap.includes("$dingRuntimeName = [IO.Path]::GetFileNameWithoutExtension($dingNodeFile)")
|| windowsBootstrap.includes("GetFileNameWithoutExtension([IO.Path]::GetFileNameWithoutExtension")
|| windowsBootstrap.includes("TimeoutSec 300") || windowsBootstrap.includes("TimeoutSec 300")
|| !windowsBootstrap.includes("Write-DingProgress") || !posixBootstrap.includes("ding_progress") || !windowsBootstrap.includes("Write-DingProgress") || !posixBootstrap.includes("ding_progress")
|| !posixBootstrap.includes("--max-time 90") || /--max-time 300|--timeout=300/.test(posixBootstrap) || !posixBootstrap.includes("--max-time 90") || /--max-time 300|--timeout=300/.test(posixBootstrap)
+3
View File
@@ -49,10 +49,13 @@ test("customer distribution prefers remote MCP OAuth and keeps a generic compati
assert.match(windowsBootstrap, /OSArchitecture[\s\S]*Save-DingDownload[\s\S]*Expand-Archive/); assert.match(windowsBootstrap, /OSArchitecture[\s\S]*Save-DingDownload[\s\S]*Expand-Archive/);
assert.match(windowsBootstrap, /SHA-256 不匹配,停止安装/); assert.match(windowsBootstrap, /SHA-256 不匹配,停止安装/);
assert.match(windowsBootstrap, /Write-DingProgress[\s\S]*\[dingtalk-approval\]/); assert.match(windowsBootstrap, /Write-DingProgress[\s\S]*\[dingtalk-approval\]/);
assert.match(windowsBootstrap, /UTF8Encoding\(\$false\)[\s\S]*Console\]::InputEncoding[\s\S]*Console\]::OutputEncoding[\s\S]*\$OutputEncoding/);
assert.match(windowsBootstrap, /dingInstallerUrls[\s\S]*dingManifestMirrorUrls[\s\S]*use-env-proxy/s); assert.match(windowsBootstrap, /dingInstallerUrls[\s\S]*dingManifestMirrorUrls[\s\S]*use-env-proxy/s);
assert.match(windowsBootstrap, /curl\.exe[\s\S]*--connect-timeout 10[\s\S]*--max-time 90/); assert.match(windowsBootstrap, /curl\.exe[\s\S]*--connect-timeout 10[\s\S]*--max-time 90/);
assert.match(windowsBootstrap, /-e "const \[a,b\]=process\.versions\.node\.split\('\.'\)/); assert.match(windowsBootstrap, /-e "const \[a,b\]=process\.versions\.node\.split\('\.'\)/);
assert.match(windowsBootstrap, /-e "process\.exit\(process\.allowedNodeEnvironmentFlags\.has\('--use-env-proxy'\)/); assert.match(windowsBootstrap, /-e "process\.exit\(process\.allowedNodeEnvironmentFlags\.has\('--use-env-proxy'\)/);
assert.match(windowsBootstrap, /dingRuntimeName = \[IO\.Path\]::GetFileNameWithoutExtension\(\$dingNodeFile\)/);
assert.doesNotMatch(windowsBootstrap, /GetFileNameWithoutExtension\(\[IO\.Path\]::GetFileNameWithoutExtension/);
assert.doesNotMatch(windowsBootstrap, /TimeoutSec 300|-e '.*split\("\."\)/); assert.doesNotMatch(windowsBootstrap, /TimeoutSec 300|-e '.*split\("\."\)/);
assert.match(posixBootstrap, /^#!\/bin\/sh\nset -eu/); assert.match(posixBootstrap, /^#!\/bin\/sh\nset -eu/);
assert.match(posixBootstrap, /uname -s[\s\S]*uname -m[\s\S]*tar -xzf/); assert.match(posixBootstrap, /uname -s[\s\S]*uname -m[\s\S]*tar -xzf/);