From 7c34ebba22c9a1af620330d7959ecfc19b47bb2b Mon Sep 17 00:00:00 2001 From: "Angnuo Li (from Dev Box)" Date: Thu, 13 Aug 2026 16:35:53 +0800 Subject: [PATCH] Fix npm fallback for search plugin install Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- deployer/windows_setup.py | 86 +++++++++++++++++++++-------- tests/test_windows_setup_upgrade.py | 51 +++++++++++++++++ 2 files changed, 113 insertions(+), 24 deletions(-) diff --git a/deployer/windows_setup.py b/deployer/windows_setup.py index 79410ef..9e1242c 100644 --- a/deployer/windows_setup.py +++ b/deployer/windows_setup.py @@ -66,6 +66,7 @@ MIRROR_HUAWEI = "huawei" MIRROR_FALLBACK = MIRROR_NPMMIRROR NPM_REGISTRY_HUAWEI = "https://repo.huaweicloud.com/repository/npm/" +NPM_REGISTRY_MICROSOFT = "https://packagefeedproxy.microsoft.io/npm/" _PARALLEL_PLUGIN_ID = "parallel" _PARALLEL_PLUGIN_PACKAGE = "@openclaw/parallel-plugin" _PARALLEL_FREE_PROVIDER = "parallel-free" @@ -2102,6 +2103,7 @@ def _npm_registry_candidates(self) -> list[str]: candidates = [ configured, MIRRORS[MIRROR_OFFICIAL]["npm_registry"], + NPM_REGISTRY_MICROSOFT, MIRRORS[MIRROR_NPMMIRROR]["npm_registry"], NPM_REGISTRY_HUAWEI, ] @@ -2211,12 +2213,6 @@ def _install_openclaw_from_registry( ) def _install_openclaw_with_registry_fallback(self, install_prefix: Path) -> bool: - retryable_registry_error = re.compile( - r"ERR_SSL|TLS|ECONNRESET|ECONNREFUSED|ETIMEDOUT|ENOTFOUND|EAI_AGAIN|" - r"\bE(?:401|403|404|408|429|5\d{2})\b|" - r"\b(?:401|403|404|408|429|5\d{2})\b", - re.IGNORECASE, - ) channel = self.cfg.get("openclaw.channel", "stable") expected_version = OPENCLAW_TARGET_VERSION if channel == "stable" else None candidates = self._npm_registry_candidates() @@ -2246,7 +2242,7 @@ def _install_openclaw_with_registry_fallback(self, install_prefix: Path) -> bool "OpenClaw package and entry were verified" ) return True - if not retryable_registry_error.search(attempt.output): + if not self._is_retryable_npm_registry_error(attempt.output): self.log.error( f"npm install failed (exit {attempt.returncode}) via {registry}:\n" f"{attempt.output[-1500:]}" @@ -2256,6 +2252,18 @@ def _install_openclaw_with_registry_fallback(self, install_prefix: Path) -> bool self.log.error("OpenClaw install failed through every configured npm registry") return False + @staticmethod + def _is_retryable_npm_registry_error(detail: str) -> bool: + return bool( + re.search( + r"ERR_SSL|TLS|ECONNRESET|ECONNREFUSED|ETIMEDOUT|ENOTFOUND|EAI_AGAIN|" + r"\bE(?:401|403|404|408|429|5\d{2})\b|" + r"\b(?:401|403|404|408|429|5\d{2})\b", + detail, + re.IGNORECASE, + ) + ) + def _load_openclaw_state_env(self, state_dir: Path) -> dict[str, str]: values: dict[str, str] = {} env_path = state_dir / ".env" @@ -3815,25 +3823,55 @@ def install_search_provider_plugin(self) -> bool: pass self.log.step("Installing Parallel web search plugin…") - try: - result = self._run( - openclaw_cmd + ["plugins", "install", _PARALLEL_PLUGIN_PACKAGE], - capture_output=True, - text=True, - encoding="utf-8", - errors="replace", - timeout=300, - env=env, + candidates = self._npm_registry_candidates() + registries = self._reachable_npm_registries(candidates) + if not registries: + self.log.error( + "Parallel web search plugin install failed: no npm registry is reachable. " + "Tried: " + ", ".join(candidates) ) - except (OSError, subprocess.TimeoutExpired) as error: - self.log.error(f"Parallel web search plugin install failed: {error}") - return False - if result.returncode != 0: - detail = result.stderr.strip() or result.stdout.strip() - self.log.error(f"Parallel web search plugin install failed: {detail}") return False - self.log.success("Parallel-free web search is ready") - return True + + last_detail = "" + for registry in registries: + attempt_env = env.copy() + for key in list(attempt_env): + if key.lower() == "npm_config_registry": + del attempt_env[key] + attempt_env["npm_config_registry"] = registry + self.log.info(f" Parallel plugin npm registry attempt: {registry}") + try: + result = self._run( + openclaw_cmd + ["plugins", "install", _PARALLEL_PLUGIN_PACKAGE], + capture_output=True, + text=True, + encoding="utf-8", + errors="replace", + timeout=300, + env=attempt_env, + ) + except subprocess.TimeoutExpired: + last_detail = f"timed out via {registry}" + self.log.warn(f"Parallel plugin install {last_detail}; trying next registry") + continue + except OSError as error: + self.log.error(f"Parallel web search plugin install failed: {error}") + return False + if result.returncode == 0: + self.log.success("Parallel-free web search is ready") + return True + + last_detail = result.stderr.strip() or result.stdout.strip() + if not self._is_retryable_npm_registry_error(last_detail): + self.log.error(f"Parallel web search plugin install failed: {last_detail}") + return False + self.log.warn(f"Parallel plugin registry failure via {registry}; trying next registry") + + self.log.error( + "Parallel web search plugin install failed through every npm registry" + + (f": {last_detail}" if last_detail else "") + ) + return False def install_weixin_plugin(self) -> bool: """Reconcile the bundled openclaw-weixin plugin through OpenClaw.""" diff --git a/tests/test_windows_setup_upgrade.py b/tests/test_windows_setup_upgrade.py index 1bfac45..a2a8116 100644 --- a/tests/test_windows_setup_upgrade.py +++ b/tests/test_windows_setup_upgrade.py @@ -18,6 +18,7 @@ MIRROR_NPMMIRROR, MIRROR_OFFICIAL, MIRRORS, + NPM_REGISTRY_MICROSOFT, ActiveGateway, ActiveInstallation, NodeInstallBlocked, @@ -844,6 +845,7 @@ def test_automatic_registry_fallbacks_are_https(self): self.assertTrue( all(registry.startswith("https://") for registry in self.ws._npm_registry_candidates()) ) + self.assertIn(NPM_REGISTRY_MICROSOFT, self.ws._npm_registry_candidates()) def test_node_download_bases_start_with_selected_and_are_deduped(self): self.ws._mirror_name = MIRROR_NPMMIRROR @@ -1705,6 +1707,9 @@ def test_install_search_provider_plugin_installs_parallel_package(self): return_value=(["openclaw.cmd"], {"OPENCLAW_STATE_DIR": str(config_path.parent)}) ) self.ws._run_openclaw_json = unittest.mock.Mock(side_effect=RuntimeError("not installed")) + self.ws._reachable_npm_registries = unittest.mock.Mock( + return_value=[NPM_REGISTRY_MICROSOFT] + ) self.ws._run = unittest.mock.Mock( return_value=SimpleNamespace(returncode=0, stdout="installed", stderr="") ) @@ -1716,6 +1721,52 @@ def test_install_search_provider_plugin_installs_parallel_package(self): self.ws._run.call_args.args[0], ["openclaw.cmd", "plugins", "install", "@openclaw/parallel-plugin"], ) + self.assertEqual( + self.ws._run.call_args.kwargs["env"]["npm_config_registry"], + NPM_REGISTRY_MICROSOFT, + ) + + def test_install_search_provider_plugin_retries_registry_tls_failure(self): + config_path = self.home / ".openclaw" / "openclaw.json" + config_path.parent.mkdir(parents=True) + config_path.write_text( + json.dumps({"tools": {"web": {"search": {"provider": "parallel-free"}}}}), + encoding="utf-8", + ) + self.ws._weixin_cli_context = unittest.mock.Mock( + return_value=(["openclaw.cmd"], {"OPENCLAW_STATE_DIR": str(config_path.parent)}) + ) + self.ws._run_openclaw_json = unittest.mock.Mock(side_effect=RuntimeError("not installed")) + self.ws._reachable_npm_registries = unittest.mock.Mock( + return_value=[ + MIRRORS[MIRROR_NPMMIRROR]["npm_registry"], + NPM_REGISTRY_MICROSOFT, + ] + ) + self.ws._run = unittest.mock.Mock( + side_effect=[ + SimpleNamespace( + returncode=1, + stdout="", + stderr="ERR_SSL_SSL/TLS_ALERT_HANDSHAKE_FAILURE", + ), + SimpleNamespace(returncode=0, stdout="installed", stderr=""), + ] + ) + + self.assertTrue(self.ws.install_search_provider_plugin()) + + self.assertEqual(self.ws._run.call_count, 2) + attempted_registries = [ + call.kwargs["env"]["npm_config_registry"] for call in self.ws._run.call_args_list + ] + self.assertEqual( + attempted_registries, + [ + MIRRORS[MIRROR_NPMMIRROR]["npm_registry"], + NPM_REGISTRY_MICROSOFT, + ], + ) def test_install_search_provider_plugin_uses_verified_local_fast_path(self): state_dir = self.home / ".openclaw"