feat: prefer remote MCP OAuth installation

This commit is contained in:
2026-08-18 18:24:47 +00:00
parent 55e43e3451
commit 0d5e192e95
20 changed files with 449 additions and 392 deletions
+45 -36
View File
@@ -3,41 +3,37 @@ import { spawnSync } from "node:child_process";
import { existsSync, readFileSync } from "node:fs";
import test from "node:test";
test("customer distribution is a generic single-login gateway bridge", () => {
test("customer distribution prefers remote MCP OAuth and keeps a generic compatibility bridge", () => {
const packageMetadata = JSON.parse(readFileSync("package.json", "utf8")) as { version: string };
assert.match(packageMetadata.version, /^\d+\.\d+\.\d+-rc\.\d+$/);
assert.equal(existsSync("plugins/dingtalk-approval"), false);
const installer = readFileSync("distribution/agent-install.template.md", "utf8");
assert.match(installer, /Ed25519/);
assert.match(installer, /唯一通用安装入口/);
assert.match(installer, /使用 `mcpConfig`/);
assert.match(installer, /安装器本身不会猜测或修改 Agent 的私有配置/);
assert.match(installer, /远程.*Streamable HTTP MCP/s);
assert.match(installer, /URL:`\{\{GATEWAY_URL\}\}`/);
assert.match(installer, /认证:OAuth/);
assert.match(installer, /动态客户端注册/);
assert.match(installer, /不要安装 Node\.js、npm、本地 Bridge/);
assert.match(installer, /只有.*不支持远程 Streamable HTTP MCP.*标准 OAuth.*兼容/s);
assert.match(installer, /COMPAT_INSTALL_URL/);
assert.match(installer, /approval_list_todos.*source="all".*limit=1/s);
assert.match(installer, /钉钉 OA 与薪福通来源都为 `ok`/);
assert.doesNotMatch(installer, /PowerShell|Git Bash|stdio|postInstall|loginCommand|npm install -g/);
assert.doesNotMatch(installer, /--host|WorkBuddy|Codex|Claude/);
assert.match(installer, /不会通过其他消息通道自动推送链接.*完整登录 URL.*当前回复/s);
assert.match(installer, /读取返回的 `postInstall`.*不得等到用户第一次查询审批/s);
assert.match(installer, /登录链接过期.*绝不能重新安装.*approval_login.*loginCommand/s);
assert.match(installer, /onboarding\.firstRun=true.*简短示例.*不要自动执行/s);
assert.match(installer, /Windows(PowerShell)/);
assert.match(installer, /Invoke-WebRequest[\s\S]*Get-FileHash[\s\S]*ConvertFrom-Json/);
assert.match(installer, /至少 10 分钟超时[\s\S]*stderr[\s\S]*stdout/);
assert.match(installer, /Git Bash[\s\S]*不得.*内部.*installer\.mjs/s);
assert.match(installer, /安装长时间无最终 JSON[\s\S]*残留 staging/s);
assert.match(installer, /系统无需预装 Node\.js/);
assert.match(installer, /阿里云精确版本.*npmmirror 精确版本.*Node 官方精确版本/s);
assert.match(installer, /备用发布源[\s\S]*优先使用国内 Gitea 静态发布源/s);
assert.match(installer, /优先使用系统 `curl`[\s\S]*Node `fetch` 仅在系统没有 `curl` 时兜底/s);
assert.match(installer, /不要设置全局 `NODE_OPTIONS=--use-env-proxy`[\s\S]*不要手工预装 `node_modules`/s);
assert.match(installer, /BOOTSTRAP_WINDOWS_URL|Windows 安装引导/);
assert.match(installer, /Linux\/macOS 安装引导/);
assert.match(installer, /dingtalk-approval-bridge\.cmd/);
assert.match(installer, /显示 302 或启动后立即退出[\s\S]*type: \"stdio\"[\s\S]*doctor --json/);
assert.match(installer, /sourceStatus[\s\S]*dingtalk\.status[\s\S]*xft\.status[\s\S]*都必须是 `ok`/);
assert.match(installer, /更新[\s\S]*update --json[\s\S]*卸载[\s\S]*uninstall --yes --json/);
assert.match(installer, /不能热加载[\s\S]*完整退出并重新启动 Agent/);
assert.doesNotMatch(installer, /npm install -g|npx\s/);
assert.doesNotMatch(installer, /通过当前 Messenger/);
assert.doesNotMatch(installer, /DWS_CLI_PATH|dingtalk-workspace-cli|APP_MODE=local/);
const compatibilityInstaller = readFileSync("distribution/agent-compat-install.template.md", "utf8");
assert.match(compatibilityInstaller, /仅当.*不支持远程 Streamable HTTP MCP.*标准 OAuth/s);
assert.match(compatibilityInstaller, /Ed25519/);
assert.match(compatibilityInstaller, /使用 `mcpConfig`/);
assert.match(compatibilityInstaller, /Windows(PowerShell)/);
assert.match(compatibilityInstaller, /curl\.exe[\s\S]*Invoke-WebRequest[\s\S]*Get-FileHash[\s\S]*ConvertFrom-Json/);
assert.match(compatibilityInstaller, /90 秒[\s\S]*stderr[\s\S]*stdout/);
assert.match(compatibilityInstaller, /登录链接过期.*绝不能重新安装.*approval_login.*loginCommand/s);
assert.match(compatibilityInstaller, /sourceStatus[\s\S]*dingtalk\.status[\s\S]*xft\.status[\s\S]*都必须是 `ok`/);
assert.match(compatibilityInstaller, /更新[\s\S]*update --json[\s\S]*卸载[\s\S]*uninstall --yes --json/);
assert.doesNotMatch(compatibilityInstaller, /TimeoutSec 300|--max-time 300|--timeout=300|至少 10 分钟超时/);
assert.doesNotMatch(compatibilityInstaller, /--host|WorkBuddy|Codex|Claude/);
assert.doesNotMatch(compatibilityInstaller, /DWS_CLI_PATH|dingtalk-workspace-cli|APP_MODE=local/);
const runtime = JSON.parse(readFileSync("distribution/node-runtime.json", "utf8"));
assert.equal(runtime.version, "22.23.2");
@@ -54,11 +50,17 @@ test("customer distribution is a generic single-login gateway bridge", () => {
assert.match(windowsBootstrap, /SHA-256 不匹配,停止安装/);
assert.match(windowsBootstrap, /Write-DingProgress[\s\S]*\[dingtalk-approval\]/);
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, /-e "const \[a,b\]=process\.versions\.node\.split\('\.'\)/);
assert.match(windowsBootstrap, /-e "process\.exit\(process\.allowedNodeEnvironmentFlags\.has\('--use-env-proxy'\)/);
assert.doesNotMatch(windowsBootstrap, /TimeoutSec 300|-e '.*split\("\."\)/);
assert.match(posixBootstrap, /^#!\/bin\/sh\nset -eu/);
assert.match(posixBootstrap, /uname -s[\s\S]*uname -m[\s\S]*tar -xzf/);
assert.match(posixBootstrap, /SHA-256 不匹配,停止安装/);
assert.match(posixBootstrap, /ding_progress[\s\S]*\[dingtalk-approval\]/);
assert.match(posixBootstrap, /ding_installer_urls[\s\S]*ding_manifest_mirror_urls[\s\S]*use-env-proxy/s);
assert.match(posixBootstrap, /--connect-timeout 10[\s\S]*--max-time 90[\s\S]*--speed-time 20/);
assert.doesNotMatch(posixBootstrap, /--max-time 300|--timeout=300/);
assert.equal(spawnSync("sh", ["-n", "distribution/bootstrap-posix.template.sh"]).status, 0);
assert.doesNotMatch(`${windowsBootstrap}\n${posixBootstrap}`, /latest-v|npm install -g/);
@@ -67,10 +69,12 @@ test("customer distribution is a generic single-login gateway bridge", () => {
assert.match(skill, /read `references\/create-approval\.md` completely/);
assert.doesNotMatch(skill, /XFT recall is experimental/);
assert.match(createApproval, /XFT recall is experimental.*XFTSYS9999/s);
assert.match(skill, /does not send or push.*current user-facing reply/s);
assert.match(skill, /handle the installer's `postInstall` result immediately.*waiting for the user's first approval request/s);
assert.match(skill, /host's native remote-MCP OAuth flow by default/);
assert.match(skill, /both DingTalk OA and XFT must be available/);
assert.match(skill, /Only hosts that cannot use remote Streamable HTTP MCP or standard OAuth.*local STDIO Bridge/s);
assert.match(skill, /authChallenge\.directUrl.*current reply/s);
assert.match(skill, /onboarding\.firstRun=true.*Do not execute an example automatically/s);
assert.match(skill, /expired login link.*approval_login.*never reinstall/s);
assert.match(skill, /link expires.*approval_login.*Never reinstall/s);
const builder = readFileSync("scripts/build-release.mjs", "utf8");
assert.match(builder, /schemaVersion: 3,\s+version,/s);
@@ -79,6 +83,9 @@ test("customer distribution is a generic single-login gateway bridge", () => {
assert.match(builder, /dingtalk-approval-client/);
assert.match(builder, /bootstrap-windows\.template\.ps1/);
assert.match(builder, /bootstrap-posix\.template\.sh/);
assert.match(builder, /agent-compat-install\.template\.md/);
assert.match(builder, /compat-install\.md/);
assert.match(builder, /utf8Bom: true/);
assert.match(builder, /validateNodeRuntime/);
assert.match(builder, /cpSync\(resolve\("skills\/dingtalk-approval"\).*recursive: true/s);
assert.doesNotMatch(builder, /codexPlugin|codex-plugin|\.codex-plugin|openai\.yaml/);
@@ -95,10 +102,12 @@ test("customer distribution is a generic single-login gateway bridge", () => {
test("public install page explicitly declares UTF-8 for browser rendering", () => {
const nginx = readFileSync("deploy/approval.justconn.top.nginx.conf", "utf8");
const installLocation = nginx.match(/location = \/downloads\/dingmcp\/install\.md \{([\s\S]*?)\n \}/)?.[1] ?? "";
assert.match(installLocation, /default_type text\/markdown;/);
assert.match(installLocation, /charset utf-8;/);
assert.match(installLocation, /charset_types text\/markdown;/);
for (const name of ["install", "compat-install"]) {
const location = nginx.match(new RegExp(`location = /downloads/dingmcp/${name}\\.md \\{([\\s\\S]*?)\\n \\}`))?.[1] ?? "";
assert.match(location, /default_type text\/markdown;/);
assert.match(location, /charset utf-8;/);
assert.match(location, /charset_types text\/markdown;/);
}
});
test("Gitea release lookup tolerates tea field-name corruption and fails closed", () => {