Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 62 additions & 24 deletions deployer/windows_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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,
]
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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:]}"
Expand All @@ -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"
Expand Down Expand Up @@ -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."""
Expand Down
51 changes: 51 additions & 0 deletions tests/test_windows_setup_upgrade.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
MIRROR_NPMMIRROR,
MIRROR_OFFICIAL,
MIRRORS,
NPM_REGISTRY_MICROSOFT,
ActiveGateway,
ActiveInstallation,
NodeInstallBlocked,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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="")
)
Expand All @@ -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"
Expand Down
Loading