diff --git a/deployer/windows_setup.py b/deployer/windows_setup.py
index fa88ca0..82529a9 100644
--- a/deployer/windows_setup.py
+++ b/deployer/windows_setup.py
@@ -1307,6 +1307,53 @@ def _npm_registry_candidates(self) -> list[str]:
registries.append(registry)
return registries
+ def _reachable_npm_registries(self, candidates: list[str], timeout: float = 2.5) -> list[str]:
+ """Filter ``candidates`` down to registries that answer a quick probe.
+
+ Each candidate's ``/-/ping`` endpoint is probed in parallel with a
+ short per-registry ``timeout``. A registry counts as reachable if the
+ host responds at all — even a 4xx/5xx is fine, since that still proves
+ the TCP+TLS handshake completed. Registries that time out or fail to
+ connect (DNS, SSL, or a corporate ``NPM URL Block`` network policy)
+ are dropped so we never hand them to ``npm install``, which would
+ otherwise hang for up to 15 minutes retrying a blocked host.
+
+ Returns the reachable subset ordered fastest-first; unreachable
+ registries are omitted entirely.
+ """
+ import concurrent.futures as _cf
+
+ def _measure(registry: str) -> tuple[str, float]:
+ url = registry.rstrip("/") + "/-/ping"
+ req = urllib.request.Request(url, headers={"User-Agent": "OpenClawDeployer/1.0"})
+ start = time.monotonic()
+ try:
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
+ resp.read(64)
+ return registry, time.monotonic() - start
+ except urllib.error.HTTPError:
+ # A 404/403 still means the host is reachable.
+ return registry, time.monotonic() - start
+ except Exception:
+ return registry, float("inf")
+
+ if not candidates:
+ return []
+
+ results: list[tuple[str, float]] = []
+ try:
+ with _cf.ThreadPoolExecutor(max_workers=len(candidates)) as pool:
+ futures = [pool.submit(_measure, r) for r in candidates]
+ for f in _cf.as_completed(futures, timeout=timeout + 1.0):
+ results.append(f.result())
+ except Exception as e:
+ self.log.debug(f"npm registry reachability probe failed: {e}")
+
+ reachable = sorted((r for r in results if r[1] != float("inf")), key=lambda x: x[1])
+ for registry, latency in reachable:
+ self.log.debug(f" npm registry {registry}: {int(latency * 1000)} ms")
+ return [registry for registry, _ in reachable]
+
def _install_openclaw_from_registry(
self, install_prefix: Path, registry: str
) -> OpenClawInstallAttempt:
@@ -1366,7 +1413,18 @@ def _install_openclaw_with_registry_fallback(self, install_prefix: Path) -> bool
)
channel = self.cfg.get("openclaw.channel", "stable")
expected_version = OPENCLAW_TARGET_VERSION if channel == "stable" else None
- for registry in self._npm_registry_candidates():
+ candidates = self._npm_registry_candidates()
+ registries = self._reachable_npm_registries(candidates)
+ if not registries:
+ self.log.error(
+ "Cannot reach any npm registry to install OpenClaw. Every candidate "
+ "registry failed a quick reachability probe, which usually means they "
+ "are blocked by your network or IT policy (e.g. a corporate "
+ "'NPM URL Block'). Tried: " + ", ".join(candidates) + ". "
+ "Configure an allowed npm registry via 'npm.registry' and retry."
+ )
+ return False
+ for registry in registries:
self.log.info(f" npm registry attempt: {registry}")
attempt = self._install_openclaw_from_registry(install_prefix, registry)
installed = attempt.installed_version
diff --git a/desktop/renderer/src/views/SettingsView.vue b/desktop/renderer/src/views/SettingsView.vue
index 5060c7b..76f2159 100644
--- a/desktop/renderer/src/views/SettingsView.vue
+++ b/desktop/renderer/src/views/SettingsView.vue
@@ -418,11 +418,14 @@
{{ skill.description }}
-
-
- {{ t("settings.askAgent") }}
-
-
+
+ {{ t("settings.askAgent") }}
+
{{ skill.description }}
-
-
- {{ t("settings.askAgent") }}
-
-
+
+ {{ t("settings.askAgent") }}
+