Spaces:
Runtime error
Runtime error
fix: revert auth to password mode, add Telegram API auto-probe, background DNS
Browse files1. Revert auth from token back to password (fixes token_mismatch on Control UI)
2. Add Telegram API base auto-probe: probes api.telegram.org, mirrors, and
Cloudflare Pages proxy at startup; selects first working endpoint
3. Revert DNS resolution from blocking (141s delay) to background
4. Add TELEGRAM_BOT_TOKEN and TELEGRAM_API_BASE env vars to README
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- README.md +4 -0
- openclaw.json +1 -1
- scripts/entrypoint.sh +7 -6
- scripts/sync_hf.py +102 -5
README.md
CHANGED
|
@@ -116,6 +116,8 @@ Fine-tune persistence and performance. Set these as **Repository Secrets** in HF
|
|
| 116 |
| `AUTO_CREATE_DATASET` | `false` | **Auto-create the Dataset repo.** Default is `false` for security. Set to `true` to let HuggingClaw automatically create a **private** Dataset repo on first startup (and auto-derive the repo name from your `HF_TOKEN` if `OPENCLAW_DATASET_REPO` is not set). Accepted values: `true`, `1`, `yes` / `false`, `0`, `no`. |
|
| 117 |
| `SYNC_INTERVAL` | `60` | **Backup interval in seconds.** How often HuggingClaw syncs `~/.openclaw` to the Dataset repo. Lower = safer but more API calls. Recommended: `60`β`300`. |
|
| 118 |
| `NODE_MEMORY_LIMIT` | `512` | **Node.js heap memory limit in MB.** HF free tier provides 16 GB RAM; 512 MB is enough for most cases. Increase for complex agent workflows. |
|
|
|
|
|
|
|
| 119 |
| `TZ` | `UTC` | **Timezone** for log timestamps. Example: `Asia/Shanghai`, `America/New_York`. |
|
| 120 |
|
| 121 |
> For the full list (including `OPENAI_BASE_URL`, `OLLAMA_HOST`, proxy settings, etc.), see [`.env.example`](.env.example).
|
|
@@ -126,6 +128,8 @@ Visit your Space URL. Click the settings icon, enter your password, and connect.
|
|
| 126 |
|
| 127 |
Messaging integrations (Telegram, WhatsApp) can be configured directly inside the Control UI after connecting.
|
| 128 |
|
|
|
|
|
|
|
| 129 |
## Configuration
|
| 130 |
|
| 131 |
HuggingClaw supports **all OpenClaw environment variables** β it passes the entire environment to the OpenClaw process (`env=os.environ.copy()`), so any variable from the [OpenClaw docs](https://openclawdoc.com/docs/reference/environment-variables) works out of the box in HF Spaces. This includes:
|
|
|
|
| 116 |
| `AUTO_CREATE_DATASET` | `false` | **Auto-create the Dataset repo.** Default is `false` for security. Set to `true` to let HuggingClaw automatically create a **private** Dataset repo on first startup (and auto-derive the repo name from your `HF_TOKEN` if `OPENCLAW_DATASET_REPO` is not set). Accepted values: `true`, `1`, `yes` / `false`, `0`, `no`. |
|
| 117 |
| `SYNC_INTERVAL` | `60` | **Backup interval in seconds.** How often HuggingClaw syncs `~/.openclaw` to the Dataset repo. Lower = safer but more API calls. Recommended: `60`β`300`. |
|
| 118 |
| `NODE_MEMORY_LIMIT` | `512` | **Node.js heap memory limit in MB.** HF free tier provides 16 GB RAM; 512 MB is enough for most cases. Increase for complex agent workflows. |
|
| 119 |
+
| `TELEGRAM_BOT_TOKEN` | β | **Telegram bot token.** If set, HuggingClaw auto-probes Telegram API endpoints at startup and selects a working one (HF Spaces blocks `api.telegram.org`). You can also configure the bot token in the Control UI. |
|
| 120 |
+
| `TELEGRAM_API_BASE` | β | **Force a specific Telegram API base URL.** Skips auto-probe. Example: `https://telegram-api.mykdigi.com`. Only needed if auto-probe doesn't find a working endpoint. |
|
| 121 |
| `TZ` | `UTC` | **Timezone** for log timestamps. Example: `Asia/Shanghai`, `America/New_York`. |
|
| 122 |
|
| 123 |
> For the full list (including `OPENAI_BASE_URL`, `OLLAMA_HOST`, proxy settings, etc.), see [`.env.example`](.env.example).
|
|
|
|
| 128 |
|
| 129 |
Messaging integrations (Telegram, WhatsApp) can be configured directly inside the Control UI after connecting.
|
| 130 |
|
| 131 |
+
> **Telegram note:** HF Spaces blocks `api.telegram.org` DNS. HuggingClaw automatically probes alternative API endpoints at startup and selects one that works β no manual configuration needed.
|
| 132 |
+
|
| 133 |
## Configuration
|
| 134 |
|
| 135 |
HuggingClaw supports **all OpenClaw environment variables** β it passes the entire environment to the OpenClaw process (`env=os.environ.copy()`), so any variable from the [OpenClaw docs](https://openclawdoc.com/docs/reference/environment-variables) works out of the box in HF Spaces. This includes:
|
openclaw.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
| 3 |
"mode": "local",
|
| 4 |
"bind": "lan",
|
| 5 |
"port": 7860,
|
| 6 |
-
"auth": { "
|
| 7 |
"trustedProxies": [
|
| 8 |
"0.0.0.0/0"
|
| 9 |
],
|
|
|
|
| 3 |
"mode": "local",
|
| 4 |
"bind": "lan",
|
| 5 |
"port": 7860,
|
| 6 |
+
"auth": { "password": "__OPENCLAW_PASSWORD__" },
|
| 7 |
"trustedProxies": [
|
| 8 |
"0.0.0.0/0"
|
| 9 |
],
|
scripts/entrypoint.sh
CHANGED
|
@@ -6,12 +6,13 @@ BOOT_START=$(date +%s)
|
|
| 6 |
echo "[entrypoint] OpenClaw HuggingFace Spaces Entrypoint"
|
| 7 |
echo "[entrypoint] ======================================="
|
| 8 |
|
| 9 |
-
# ββ DNS pre-resolution (
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
|
|
|
| 15 |
|
| 16 |
# ββ Node.js memory limit (only if explicitly set) βββββββββββββββββββββββββ
|
| 17 |
if [ -n "$NODE_MEMORY_LIMIT" ]; then
|
|
|
|
| 6 |
echo "[entrypoint] OpenClaw HuggingFace Spaces Entrypoint"
|
| 7 |
echo "[entrypoint] ======================================="
|
| 8 |
|
| 9 |
+
# ββ DNS pre-resolution (background β non-blocking) βββββββββββββββββββββββ
|
| 10 |
+
# Resolves WhatsApp domains via DoH for dns-fix.cjs to consume.
|
| 11 |
+
# Telegram connectivity is handled by API base auto-probe in sync_hf.py.
|
| 12 |
+
echo "[entrypoint] Starting DNS resolution in background..."
|
| 13 |
+
python3 /home/node/scripts/dns-resolve.py /tmp/dns-resolved.json 2>&1 &
|
| 14 |
+
DNS_PID=$!
|
| 15 |
+
echo "[entrypoint] DNS resolver PID: $DNS_PID"
|
| 16 |
|
| 17 |
# ββ Node.js memory limit (only if explicitly set) βββββββββββββββββββββββββ
|
| 18 |
if [ -n "$NODE_MEMORY_LIMIT" ]; then
|
scripts/sync_hf.py
CHANGED
|
@@ -22,6 +22,8 @@ import shutil
|
|
| 22 |
import tempfile
|
| 23 |
import traceback
|
| 24 |
import re
|
|
|
|
|
|
|
| 25 |
from pathlib import Path
|
| 26 |
from datetime import datetime
|
| 27 |
# Set timeout BEFORE importing huggingface_hub
|
|
@@ -100,6 +102,73 @@ log_dir.mkdir(parents=True, exist_ok=True)
|
|
| 100 |
sys.stdout = TeeLogger(log_dir / "sync.log", sys.stdout)
|
| 101 |
sys.stderr = sys.stdout
|
| 102 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
# ββ Sync Manager ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 104 |
|
| 105 |
class OpenClawFullSync:
|
|
@@ -292,8 +361,8 @@ class OpenClawFullSync:
|
|
| 292 |
cfg = json.load(f)
|
| 293 |
# Replace token placeholder
|
| 294 |
if "gateway" in cfg and "auth" in cfg["gateway"]:
|
| 295 |
-
if cfg["gateway"]["auth"].get("
|
| 296 |
-
cfg["gateway"]["auth"]["
|
| 297 |
if OPENAI_API_KEY and "models" in cfg and "providers" in cfg["models"] and "openai" in cfg["models"]["providers"]:
|
| 298 |
cfg["models"]["providers"]["openai"]["apiKey"] = OPENAI_API_KEY
|
| 299 |
if OPENAI_BASE_URL:
|
|
@@ -364,10 +433,9 @@ class OpenClawFullSync:
|
|
| 364 |
data["plugins"]["locations"] = [l for l in locs if l != "/dev/null"]
|
| 365 |
|
| 366 |
# Force full gateway config for HF Spaces
|
| 367 |
-
# Token auth: persisted in browser localStorage after first entry
|
| 368 |
if not OPENCLAW_PASSWORD:
|
| 369 |
print("[SYNC] WARNING: OPENCLAW_PASSWORD not set! Gateway will have no auth.")
|
| 370 |
-
auth = {"
|
| 371 |
# Dynamic allowedOrigins from SPACE_HOST (auto-set by HF runtime)
|
| 372 |
allowed_origins = [
|
| 373 |
"https://huggingface.co",
|
|
@@ -388,7 +456,7 @@ class OpenClawFullSync:
|
|
| 388 |
"allowedOrigins": allowed_origins
|
| 389 |
}
|
| 390 |
}
|
| 391 |
-
print(f"[SYNC] Set gateway config (auth={'
|
| 392 |
|
| 393 |
# Ensure agents defaults
|
| 394 |
data.setdefault("agents", {}).setdefault("defaults", {}).setdefault("model", {})
|
|
@@ -428,6 +496,35 @@ class OpenClawFullSync:
|
|
| 428 |
elif isinstance(data["plugins"]["entries"]["telegram"], dict):
|
| 429 |
data["plugins"]["entries"]["telegram"]["enabled"] = True
|
| 430 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 431 |
with open(config_path, "w") as f:
|
| 432 |
json.dump(data, f, indent=2)
|
| 433 |
print("[SYNC] Config patched and saved.")
|
|
|
|
| 22 |
import tempfile
|
| 23 |
import traceback
|
| 24 |
import re
|
| 25 |
+
import urllib.request
|
| 26 |
+
import ssl
|
| 27 |
from pathlib import Path
|
| 28 |
from datetime import datetime
|
| 29 |
# Set timeout BEFORE importing huggingface_hub
|
|
|
|
| 102 |
sys.stdout = TeeLogger(log_dir / "sync.log", sys.stdout)
|
| 103 |
sys.stderr = sys.stdout
|
| 104 |
|
| 105 |
+
# ββ Telegram API Base Auto-Probe ββββββββββββββββββββββββββββββββββββββββββββ
|
| 106 |
+
|
| 107 |
+
# HF Spaces blocks DNS for api.telegram.org. Probe multiple API bases at
|
| 108 |
+
# startup and pick the first one that responds to getMe. The working base
|
| 109 |
+
# is then injected into the OpenClaw Telegram channel config.
|
| 110 |
+
|
| 111 |
+
# User can force a specific base via env var (skip auto-probe)
|
| 112 |
+
TELEGRAM_API_BASE = os.environ.get("TELEGRAM_API_BASE", "")
|
| 113 |
+
|
| 114 |
+
TELEGRAM_API_BASES = [
|
| 115 |
+
"https://api.telegram.org", # official
|
| 116 |
+
"https://telegram-api.mykdigi.com", # known mirror
|
| 117 |
+
"https://telegram-api-proxy-anonymous.pages.dev/api", # Cloudflare Pages proxy
|
| 118 |
+
]
|
| 119 |
+
|
| 120 |
+
def probe_telegram_api(bot_token: str, timeout: int = 10) -> str:
|
| 121 |
+
"""Probe multiple Telegram API base URLs and return the first working one.
|
| 122 |
+
Returns the working base URL (without trailing slash), or empty string if none work.
|
| 123 |
+
"""
|
| 124 |
+
if not bot_token:
|
| 125 |
+
return ""
|
| 126 |
+
|
| 127 |
+
ctx = ssl.create_default_context()
|
| 128 |
+
for base in TELEGRAM_API_BASES:
|
| 129 |
+
url = f"{base}/bot{bot_token}/getMe"
|
| 130 |
+
try:
|
| 131 |
+
req = urllib.request.Request(url, method="GET")
|
| 132 |
+
resp = urllib.request.urlopen(req, timeout=timeout, context=ctx)
|
| 133 |
+
data = json.loads(resp.read().decode())
|
| 134 |
+
if data.get("ok"):
|
| 135 |
+
bot_name = data.get("result", {}).get("username", "unknown")
|
| 136 |
+
print(f"[TELEGRAM] β API base works: {base} (bot: @{bot_name})")
|
| 137 |
+
return base.rstrip("/")
|
| 138 |
+
except Exception as e:
|
| 139 |
+
reason = str(e)[:80]
|
| 140 |
+
print(f"[TELEGRAM] β API base failed: {base} ({reason})")
|
| 141 |
+
continue
|
| 142 |
+
|
| 143 |
+
print("[TELEGRAM] WARNING: All API bases failed! Telegram will not work.")
|
| 144 |
+
return ""
|
| 145 |
+
|
| 146 |
+
def get_telegram_bot_token() -> str:
|
| 147 |
+
"""Extract Telegram bot token from OpenClaw config or environment."""
|
| 148 |
+
# 1. Environment variable
|
| 149 |
+
token = os.environ.get("TELEGRAM_BOT_TOKEN", "")
|
| 150 |
+
if token:
|
| 151 |
+
return token
|
| 152 |
+
|
| 153 |
+
# 2. From openclaw.json channels config
|
| 154 |
+
config_path = OPENCLAW_HOME / "openclaw.json"
|
| 155 |
+
if config_path.exists():
|
| 156 |
+
try:
|
| 157 |
+
with open(config_path) as f:
|
| 158 |
+
cfg = json.load(f)
|
| 159 |
+
# Check channels.telegram.botToken (single account)
|
| 160 |
+
tg = cfg.get("channels", {}).get("telegram", {})
|
| 161 |
+
if tg.get("botToken"):
|
| 162 |
+
return tg["botToken"]
|
| 163 |
+
# Check channels.telegram.accounts.*.botToken (multi account)
|
| 164 |
+
for acc in tg.get("accounts", {}).values():
|
| 165 |
+
if isinstance(acc, dict) and acc.get("botToken"):
|
| 166 |
+
return acc["botToken"]
|
| 167 |
+
except Exception:
|
| 168 |
+
pass
|
| 169 |
+
return ""
|
| 170 |
+
|
| 171 |
+
|
| 172 |
# ββ Sync Manager ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 173 |
|
| 174 |
class OpenClawFullSync:
|
|
|
|
| 361 |
cfg = json.load(f)
|
| 362 |
# Replace token placeholder
|
| 363 |
if "gateway" in cfg and "auth" in cfg["gateway"]:
|
| 364 |
+
if cfg["gateway"]["auth"].get("password") == "__OPENCLAW_PASSWORD__":
|
| 365 |
+
cfg["gateway"]["auth"]["password"] = OPENCLAW_PASSWORD
|
| 366 |
if OPENAI_API_KEY and "models" in cfg and "providers" in cfg["models"] and "openai" in cfg["models"]["providers"]:
|
| 367 |
cfg["models"]["providers"]["openai"]["apiKey"] = OPENAI_API_KEY
|
| 368 |
if OPENAI_BASE_URL:
|
|
|
|
| 433 |
data["plugins"]["locations"] = [l for l in locs if l != "/dev/null"]
|
| 434 |
|
| 435 |
# Force full gateway config for HF Spaces
|
|
|
|
| 436 |
if not OPENCLAW_PASSWORD:
|
| 437 |
print("[SYNC] WARNING: OPENCLAW_PASSWORD not set! Gateway will have no auth.")
|
| 438 |
+
auth = {"password": OPENCLAW_PASSWORD} if OPENCLAW_PASSWORD else {}
|
| 439 |
# Dynamic allowedOrigins from SPACE_HOST (auto-set by HF runtime)
|
| 440 |
allowed_origins = [
|
| 441 |
"https://huggingface.co",
|
|
|
|
| 456 |
"allowedOrigins": allowed_origins
|
| 457 |
}
|
| 458 |
}
|
| 459 |
+
print(f"[SYNC] Set gateway config (auth={'password' if OPENCLAW_PASSWORD else 'none'}, origins={len(allowed_origins)})")
|
| 460 |
|
| 461 |
# Ensure agents defaults
|
| 462 |
data.setdefault("agents", {}).setdefault("defaults", {}).setdefault("model", {})
|
|
|
|
| 496 |
elif isinstance(data["plugins"]["entries"]["telegram"], dict):
|
| 497 |
data["plugins"]["entries"]["telegram"]["enabled"] = True
|
| 498 |
|
| 499 |
+
# ββ Telegram API base auto-probe ββββββββββββββββββββββββββββββ
|
| 500 |
+
# HF Spaces blocks api.telegram.org DNS. Probe mirrors and set
|
| 501 |
+
# the working base URL in the channel config so grammY uses it.
|
| 502 |
+
if TELEGRAM_API_BASE:
|
| 503 |
+
# User explicitly set a base via env var β use it directly
|
| 504 |
+
data.setdefault("channels", {}).setdefault("telegram", {})
|
| 505 |
+
data["channels"]["telegram"]["apiRoot"] = TELEGRAM_API_BASE.rstrip("/")
|
| 506 |
+
print(f"[TELEGRAM] Using user-specified API base: {TELEGRAM_API_BASE}")
|
| 507 |
+
else:
|
| 508 |
+
bot_token = get_telegram_bot_token()
|
| 509 |
+
if bot_token:
|
| 510 |
+
print("[TELEGRAM] Probing Telegram API bases...")
|
| 511 |
+
working_base = probe_telegram_api(bot_token)
|
| 512 |
+
if working_base and working_base != "https://api.telegram.org":
|
| 513 |
+
# Set apiRoot in channels.telegram for OpenClaw/grammY
|
| 514 |
+
data.setdefault("channels", {}).setdefault("telegram", {})
|
| 515 |
+
data["channels"]["telegram"]["apiRoot"] = working_base
|
| 516 |
+
print(f"[TELEGRAM] Set channels.telegram.apiRoot = {working_base}")
|
| 517 |
+
elif working_base:
|
| 518 |
+
# Official API works β remove any previously set mirror
|
| 519 |
+
tg_ch = data.get("channels", {}).get("telegram", {})
|
| 520 |
+
if tg_ch.get("apiRoot"):
|
| 521 |
+
del tg_ch["apiRoot"]
|
| 522 |
+
print("[TELEGRAM] Official API works β cleared apiRoot override")
|
| 523 |
+
else:
|
| 524 |
+
print("[TELEGRAM] Official API works β no override needed")
|
| 525 |
+
else:
|
| 526 |
+
print("[TELEGRAM] No bot token found β skipping API probe")
|
| 527 |
+
|
| 528 |
with open(config_path, "w") as f:
|
| 529 |
json.dump(data, f, indent=2)
|
| 530 |
print("[SYNC] Config patched and saved.")
|