Spaces:
Sleeping
Sleeping
feat: implement persistent WhatsApp credential syncing and automatic session recovery in guardian and health-server
Browse files- README.md +7 -4
- health-server.js +22 -7
- start.sh +19 -2
- wa-guardian.js +67 -5
- workspace-sync.py +50 -0
README.md
CHANGED
|
@@ -44,12 +44,12 @@ license: mit
|
|
| 44 |
- π **Any LLM:** Use Claude, OpenAI GPT, Google Gemini, Grok, DeepSeek, Qwen, and 40+ providers (set `LLM_API_KEY` and `LLM_MODEL` accordingly).
|
| 45 |
- β‘ **Zero Config:** Duplicate this Space and set **just three** secrets (LLM_API_KEY, LLM_MODEL, GATEWAY_TOKEN) β no other setup needed.
|
| 46 |
- π³ **Fast Builds:** Uses a pre-built OpenClaw Docker image to deploy in minutes.
|
| 47 |
-
- πΎ **Workspace Backup:** Chats and
|
| 48 |
- π **Always-On:** Built-in keep-alive pings prevent the HF Space from sleeping, so the assistant is always online.
|
| 49 |
- π₯ **Multi-User Messaging:** Support for Telegram (multi-user) and WhatsApp (pairing).
|
| 50 |
- π **Visual Dashboard:** Beautiful Web UI to monitor uptime, sync status, and active models.
|
| 51 |
- π **Webhooks:** Get notified on restarts or backup failures via standard webhooks.
|
| 52 |
-
- π **Flexible Auth:** Secure the Control UI with either a gateway token or password.
|
| 53 |
- π **100% HF-Native:** Runs entirely on HuggingFaceβs free infrastructure (2 vCPU, 16GB RAM).
|
| 54 |
|
| 55 |
## π₯ Video Tutorial
|
|
@@ -102,6 +102,8 @@ To use WhatsApp:
|
|
| 102 |
2. In the Control UI, go to **Channels** β **WhatsApp** β **Login**.
|
| 103 |
3. Scan the QR code with your phone. π±
|
| 104 |
|
|
|
|
|
|
|
| 105 |
## πΎ Workspace Backup *(Optional)*
|
| 106 |
|
| 107 |
For persistent chat history and configuration:
|
|
@@ -109,7 +111,7 @@ For persistent chat history and configuration:
|
|
| 109 |
- Set `HF_USERNAME` to your HuggingFace username.
|
| 110 |
- Set `HF_TOKEN` to a HuggingFace token with write access.
|
| 111 |
|
| 112 |
-
Optionally set `BACKUP_DATASET_NAME` (default: `huggingclaw-backup`) to choose the HF Dataset name. On first run, HuggingClaw will create (or use) the private Dataset repo `HF_USERNAME/SPACE-backup`, then restore your workspace on startup and sync changes every 10 minutes. The workspace is also saved on graceful shutdown.
|
| 113 |
|
| 114 |
## π Dashboard & Monitoring
|
| 115 |
|
|
@@ -289,7 +291,8 @@ HuggingClaw keeps the Space awake without external cron tools:
|
|
| 289 |
- **Backup restore failing:** Make sure `HF_USERNAME` and `HF_TOKEN` are correct (token needs write access to your Dataset).
|
| 290 |
- **Space keeps sleeping:** Check logs for `Keep-alive` messages. Ensure `KEEP_ALIVE_INTERVAL` isnβt set to `0`.
|
| 291 |
- **Auth errors / proxy:** If you see reverse-proxy auth errors, add the logged IPs under `TRUSTED_PROXIES` (from logs `remote=x.x.x.x`).
|
| 292 |
-
- **Control UI says too many failed authentication attempts:** Wait for the retry window to expire, then open the Space in an incognito window or clear site storage for your Space
|
|
|
|
| 293 |
- **UI blocked (CORS):** Set `ALLOWED_ORIGINS=https://your-space-name.hf.space`.
|
| 294 |
- **Version mismatches:** Pin a specific OpenClaw build with the `OPENCLAW_VERSION` Variable in HF Spaces, or `--build-arg OPENCLAW_VERSION=...` locally.
|
| 295 |
|
|
|
|
| 44 |
- π **Any LLM:** Use Claude, OpenAI GPT, Google Gemini, Grok, DeepSeek, Qwen, and 40+ providers (set `LLM_API_KEY` and `LLM_MODEL` accordingly).
|
| 45 |
- β‘ **Zero Config:** Duplicate this Space and set **just three** secrets (LLM_API_KEY, LLM_MODEL, GATEWAY_TOKEN) β no other setup needed.
|
| 46 |
- π³ **Fast Builds:** Uses a pre-built OpenClaw Docker image to deploy in minutes.
|
| 47 |
+
- πΎ **Workspace Backup:** Chats, settings, and WhatsApp session state sync to a private HF Dataset via the `huggingface_hub` (Git fallback), preserving data automatically.
|
| 48 |
- π **Always-On:** Built-in keep-alive pings prevent the HF Space from sleeping, so the assistant is always online.
|
| 49 |
- π₯ **Multi-User Messaging:** Support for Telegram (multi-user) and WhatsApp (pairing).
|
| 50 |
- π **Visual Dashboard:** Beautiful Web UI to monitor uptime, sync status, and active models.
|
| 51 |
- π **Webhooks:** Get notified on restarts or backup failures via standard webhooks.
|
| 52 |
+
- π **Flexible Auth:** Secure the Control UI with either a gateway token or password, with automatic token redirect for the web UI.
|
| 53 |
- π **100% HF-Native:** Runs entirely on HuggingFaceβs free infrastructure (2 vCPU, 16GB RAM).
|
| 54 |
|
| 55 |
## π₯ Video Tutorial
|
|
|
|
| 102 |
2. In the Control UI, go to **Channels** β **WhatsApp** β **Login**.
|
| 103 |
3. Scan the QR code with your phone. π±
|
| 104 |
|
| 105 |
+
The Control UI now redirects to `/?token=...` automatically, so you usually won't need to paste `GATEWAY_TOKEN` manually in the browser.
|
| 106 |
+
|
| 107 |
## πΎ Workspace Backup *(Optional)*
|
| 108 |
|
| 109 |
For persistent chat history and configuration:
|
|
|
|
| 111 |
- Set `HF_USERNAME` to your HuggingFace username.
|
| 112 |
- Set `HF_TOKEN` to a HuggingFace token with write access.
|
| 113 |
|
| 114 |
+
Optionally set `BACKUP_DATASET_NAME` (default: `huggingclaw-backup`) to choose the HF Dataset name. On first run, HuggingClaw will create (or use) the private Dataset repo `HF_USERNAME/SPACE-backup`, then restore your workspace on startup and sync changes every 10 minutes. The workspace is also saved on graceful shutdown. If you use WhatsApp, HuggingClaw also stores a hidden backup of the WhatsApp session credentials so paired logins can survive restarts.
|
| 115 |
|
| 116 |
## π Dashboard & Monitoring
|
| 117 |
|
|
|
|
| 291 |
- **Backup restore failing:** Make sure `HF_USERNAME` and `HF_TOKEN` are correct (token needs write access to your Dataset).
|
| 292 |
- **Space keeps sleeping:** Check logs for `Keep-alive` messages. Ensure `KEEP_ALIVE_INTERVAL` isnβt set to `0`.
|
| 293 |
- **Auth errors / proxy:** If you see reverse-proxy auth errors, add the logged IPs under `TRUSTED_PROXIES` (from logs `remote=x.x.x.x`).
|
| 294 |
+
- **Control UI says too many failed authentication attempts:** Wait for the retry window to expire, then open the Space in an incognito window or clear site storage for your Space. The root UI now auto-injects the current `GATEWAY_TOKEN`, so a fresh browser session usually fixes stale-token lockouts.
|
| 295 |
+
- **WhatsApp lost its session after restart:** Make sure `HF_USERNAME` and `HF_TOKEN` are configured so the hidden session backup can be restored on boot.
|
| 296 |
- **UI blocked (CORS):** Set `ALLOWED_ORIGINS=https://your-space-name.hf.space`.
|
| 297 |
- **Version mismatches:** Pin a specific OpenClaw build with the `OPENCLAW_VERSION` Variable in HF Spaces, or `--build-arg OPENCLAW_VERSION=...` locally.
|
| 298 |
|
health-server.js
CHANGED
|
@@ -21,11 +21,11 @@ let gatewayStatusCache = {
|
|
| 21 |
},
|
| 22 |
};
|
| 23 |
|
| 24 |
-
function
|
| 25 |
try {
|
| 26 |
-
return new URL(url, "http://localhost")
|
| 27 |
} catch {
|
| 28 |
-
return "/";
|
| 29 |
}
|
| 30 |
}
|
| 31 |
|
|
@@ -33,11 +33,16 @@ function isDashboardRoute(pathname) {
|
|
| 33 |
return pathname === "/dashboard" || pathname === "/dashboard/";
|
| 34 |
}
|
| 35 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
function isLocalRoute(pathname) {
|
| 37 |
return (
|
| 38 |
pathname === "/health" ||
|
| 39 |
pathname === "/status" ||
|
| 40 |
-
isDashboardRoute(pathname)
|
|
|
|
| 41 |
);
|
| 42 |
}
|
| 43 |
|
|
@@ -382,7 +387,7 @@ function renderDashboard() {
|
|
| 382 |
<span class="stat-label">Telegram</span>
|
| 383 |
<span id="tg-status">Loading...</span>
|
| 384 |
</div>
|
| 385 |
-
<a href="/" class="stat-btn">Open Control UI</a>
|
| 386 |
</div>
|
| 387 |
|
| 388 |
<div class="stat-card" style="width: 100%;">
|
|
@@ -545,7 +550,8 @@ function proxyUpgrade(req, socket, head) {
|
|
| 545 |
}
|
| 546 |
|
| 547 |
const server = http.createServer((req, res) => {
|
| 548 |
-
const
|
|
|
|
| 549 |
const uptime = Math.floor((Date.now() - startTime) / 1000);
|
| 550 |
const uptimeHuman = `${Math.floor(uptime / 3600)}h ${Math.floor((uptime % 3600) / 60)}m`;
|
| 551 |
|
|
@@ -585,11 +591,20 @@ const server = http.createServer((req, res) => {
|
|
| 585 |
return;
|
| 586 |
}
|
| 587 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 588 |
proxyHttp(req, res);
|
| 589 |
});
|
| 590 |
|
| 591 |
server.on("upgrade", (req, socket, head) => {
|
| 592 |
-
const pathname =
|
| 593 |
if (isLocalRoute(pathname)) {
|
| 594 |
socket.destroy();
|
| 595 |
return;
|
|
|
|
| 21 |
},
|
| 22 |
};
|
| 23 |
|
| 24 |
+
function parseRequestUrl(url) {
|
| 25 |
try {
|
| 26 |
+
return new URL(url, "http://localhost");
|
| 27 |
} catch {
|
| 28 |
+
return new URL("http://localhost/");
|
| 29 |
}
|
| 30 |
}
|
| 31 |
|
|
|
|
| 33 |
return pathname === "/dashboard" || pathname === "/dashboard/";
|
| 34 |
}
|
| 35 |
|
| 36 |
+
function isControlRoute(pathname) {
|
| 37 |
+
return pathname === "/control" || pathname === "/control/";
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
function isLocalRoute(pathname) {
|
| 41 |
return (
|
| 42 |
pathname === "/health" ||
|
| 43 |
pathname === "/status" ||
|
| 44 |
+
isDashboardRoute(pathname) ||
|
| 45 |
+
isControlRoute(pathname)
|
| 46 |
);
|
| 47 |
}
|
| 48 |
|
|
|
|
| 387 |
<span class="stat-label">Telegram</span>
|
| 388 |
<span id="tg-status">Loading...</span>
|
| 389 |
</div>
|
| 390 |
+
<a href="/control" class="stat-btn">Open Control UI</a>
|
| 391 |
</div>
|
| 392 |
|
| 393 |
<div class="stat-card" style="width: 100%;">
|
|
|
|
| 550 |
}
|
| 551 |
|
| 552 |
const server = http.createServer((req, res) => {
|
| 553 |
+
const parsedUrl = parseRequestUrl(req.url || "/");
|
| 554 |
+
const pathname = parsedUrl.pathname;
|
| 555 |
const uptime = Math.floor((Date.now() - startTime) / 1000);
|
| 556 |
const uptimeHuman = `${Math.floor(uptime / 3600)}h ${Math.floor((uptime % 3600) / 60)}m`;
|
| 557 |
|
|
|
|
| 591 |
return;
|
| 592 |
}
|
| 593 |
|
| 594 |
+
if ((pathname === "/" || isControlRoute(pathname)) && req.method === "GET" && !parsedUrl.searchParams.get("token") && GATEWAY_TOKEN) {
|
| 595 |
+
res.writeHead(302, {
|
| 596 |
+
Location: `/?token=${encodeURIComponent(GATEWAY_TOKEN)}`,
|
| 597 |
+
"Cache-Control": "no-store",
|
| 598 |
+
});
|
| 599 |
+
res.end();
|
| 600 |
+
return;
|
| 601 |
+
}
|
| 602 |
+
|
| 603 |
proxyHttp(req, res);
|
| 604 |
});
|
| 605 |
|
| 606 |
server.on("upgrade", (req, socket, head) => {
|
| 607 |
+
const pathname = parseRequestUrl(req.url || "/").pathname;
|
| 608 |
if (isLocalRoute(pathname)) {
|
| 609 |
socket.destroy();
|
| 610 |
return;
|
start.sh
CHANGED
|
@@ -163,6 +163,23 @@ if [ -n "$HF_USERNAME" ] && [ -n "$HF_TOKEN" ]; then
|
|
| 163 |
cd /
|
| 164 |
fi
|
| 165 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
# ββ Build config ββ
|
| 167 |
CONFIG_JSON=$(cat <<'CONFIGEOF'
|
| 168 |
{
|
|
@@ -243,8 +260,8 @@ CONFIG_JSON=$(echo "$CONFIG_JSON" | jq '.channels.whatsapp = {"dmPolicy": "pairi
|
|
| 243 |
echo "$CONFIG_JSON" > "/home/node/.openclaw/openclaw.json"
|
| 244 |
chmod 600 /home/node/.openclaw/openclaw.json
|
| 245 |
|
| 246 |
-
# ββ Enable
|
| 247 |
-
# This
|
| 248 |
export NODE_OPTIONS="${NODE_OPTIONS:+$NODE_OPTIONS }--require /home/node/app/iframe-fix.cjs"
|
| 249 |
|
| 250 |
# ββ Startup Summary ββ
|
|
|
|
| 163 |
cd /
|
| 164 |
fi
|
| 165 |
|
| 166 |
+
# ββ Restore persisted WhatsApp credentials (if present) ββ
|
| 167 |
+
WA_BACKUP_DIR="/home/node/.openclaw/workspace/.huggingclaw-state/credentials/whatsapp/default"
|
| 168 |
+
WA_CREDS_DIR="/home/node/.openclaw/credentials/whatsapp/default"
|
| 169 |
+
if [ -d "$WA_BACKUP_DIR" ]; then
|
| 170 |
+
WA_FILE_COUNT=$(find "$WA_BACKUP_DIR" -type f | wc -l | tr -d ' ')
|
| 171 |
+
if [ "$WA_FILE_COUNT" -ge 2 ]; then
|
| 172 |
+
echo "π± Restoring WhatsApp credentials..."
|
| 173 |
+
rm -rf "$WA_CREDS_DIR"
|
| 174 |
+
mkdir -p "$(dirname "$WA_CREDS_DIR")"
|
| 175 |
+
cp -R "$WA_BACKUP_DIR" "$WA_CREDS_DIR"
|
| 176 |
+
chmod -R go-rwx /home/node/.openclaw/credentials/whatsapp 2>/dev/null || true
|
| 177 |
+
echo " β
WhatsApp credentials restored"
|
| 178 |
+
else
|
| 179 |
+
echo " β οΈ Saved WhatsApp credentials look incomplete (${WA_FILE_COUNT} files), skipping restore."
|
| 180 |
+
fi
|
| 181 |
+
fi
|
| 182 |
+
|
| 183 |
# ββ Build config ββ
|
| 184 |
CONFIG_JSON=$(cat <<'CONFIGEOF'
|
| 185 |
{
|
|
|
|
| 260 |
echo "$CONFIG_JSON" > "/home/node/.openclaw/openclaw.json"
|
| 261 |
chmod 600 /home/node/.openclaw/openclaw.json
|
| 262 |
|
| 263 |
+
# ββ Enable Gateway Preload Fixes ββ
|
| 264 |
+
# This preload script keeps iframe embedding working on HF Spaces.
|
| 265 |
export NODE_OPTIONS="${NODE_OPTIONS:+$NODE_OPTIONS }--require /home/node/app/iframe-fix.cjs"
|
| 266 |
|
| 267 |
# ββ Startup Summary ββ
|
wa-guardian.js
CHANGED
|
@@ -7,6 +7,8 @@
|
|
| 7 |
*/
|
| 8 |
"use strict";
|
| 9 |
|
|
|
|
|
|
|
| 10 |
const { WebSocket } = require('/home/node/.openclaw/openclaw-app/node_modules/ws');
|
| 11 |
const { randomUUID } = require('node:crypto');
|
| 12 |
|
|
@@ -15,11 +17,19 @@ const GATEWAY_TOKEN = process.env.GATEWAY_TOKEN || "huggingclaw";
|
|
| 15 |
const CHECK_INTERVAL = 5000;
|
| 16 |
const WAIT_TIMEOUT = 120000;
|
| 17 |
const AUTH_FAILURE_COOLDOWN = 5 * 60 * 1000;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
let isWaiting = false;
|
| 20 |
let hasShownWaitMessage = false;
|
| 21 |
let authFailureUntil = 0;
|
| 22 |
let authFailureLogged = false;
|
|
|
|
| 23 |
|
| 24 |
function extractErrorMessage(msg) {
|
| 25 |
if (!msg || typeof msg !== "object") return "Unknown error";
|
|
@@ -29,6 +39,16 @@ function extractErrorMessage(msg) {
|
|
| 29 |
return "Unknown error";
|
| 30 |
}
|
| 31 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
async function createConnection() {
|
| 33 |
return new Promise((resolve, reject) => {
|
| 34 |
const ws = new WebSocket(GATEWAY_URL);
|
|
@@ -76,6 +96,10 @@ async function callRpc(ws, method, params) {
|
|
| 76 |
const msg = JSON.parse(data.toString());
|
| 77 |
if (msg.id === id) {
|
| 78 |
ws.removeListener("message", handler);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
resolve(msg);
|
| 80 |
}
|
| 81 |
};
|
|
@@ -105,8 +129,34 @@ async function checkStatus() {
|
|
| 105 |
return;
|
| 106 |
}
|
| 107 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
// If connected, we are good
|
| 109 |
if (wa.connected) {
|
|
|
|
| 110 |
ws.close();
|
| 111 |
return;
|
| 112 |
}
|
|
@@ -121,17 +171,29 @@ async function checkStatus() {
|
|
| 121 |
console.log("[guardian] Waiting for pairing completion...");
|
| 122 |
const waitRes = await callRpc(ws, "web.login.wait", { timeoutMs: WAIT_TIMEOUT });
|
| 123 |
const result = waitRes.payload || waitRes.result;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
|
| 125 |
-
if (result && (result.connected ||
|
| 126 |
-
console.log("[guardian] β
Pairing completed! Saving session and restarting gateway...");
|
| 127 |
hasShownWaitMessage = false;
|
| 128 |
-
|
| 129 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
const getRes = await callRpc(ws, "config.get", {});
|
| 131 |
-
if (getRes.
|
| 132 |
await callRpc(ws, "config.apply", { raw: getRes.payload.raw, baseHash: getRes.payload.hash });
|
| 133 |
console.log("[guardian] Configuration re-applied.");
|
| 134 |
}
|
|
|
|
|
|
|
| 135 |
}
|
| 136 |
|
| 137 |
} catch (e) {
|
|
|
|
| 7 |
*/
|
| 8 |
"use strict";
|
| 9 |
|
| 10 |
+
const fs = require("fs");
|
| 11 |
+
const path = require("path");
|
| 12 |
const { WebSocket } = require('/home/node/.openclaw/openclaw-app/node_modules/ws');
|
| 13 |
const { randomUUID } = require('node:crypto');
|
| 14 |
|
|
|
|
| 17 |
const CHECK_INTERVAL = 5000;
|
| 18 |
const WAIT_TIMEOUT = 120000;
|
| 19 |
const AUTH_FAILURE_COOLDOWN = 5 * 60 * 1000;
|
| 20 |
+
const POST_515_NO_LOGOUT_MS = 90 * 1000;
|
| 21 |
+
const RESET_MARKER_PATH = path.join(
|
| 22 |
+
process.env.HOME || "/home/node",
|
| 23 |
+
".openclaw",
|
| 24 |
+
"workspace",
|
| 25 |
+
".reset_credentials",
|
| 26 |
+
);
|
| 27 |
|
| 28 |
let isWaiting = false;
|
| 29 |
let hasShownWaitMessage = false;
|
| 30 |
let authFailureUntil = 0;
|
| 31 |
let authFailureLogged = false;
|
| 32 |
+
let last515At = 0;
|
| 33 |
|
| 34 |
function extractErrorMessage(msg) {
|
| 35 |
if (!msg || typeof msg !== "object") return "Unknown error";
|
|
|
|
| 39 |
return "Unknown error";
|
| 40 |
}
|
| 41 |
|
| 42 |
+
function writeResetMarker() {
|
| 43 |
+
try {
|
| 44 |
+
fs.mkdirSync(path.dirname(RESET_MARKER_PATH), { recursive: true });
|
| 45 |
+
fs.writeFileSync(RESET_MARKER_PATH, "reset\n");
|
| 46 |
+
console.log(`[guardian] Created backup reset marker at ${RESET_MARKER_PATH}`);
|
| 47 |
+
} catch (error) {
|
| 48 |
+
console.log(`[guardian] Failed to write backup reset marker: ${error.message}`);
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
async function createConnection() {
|
| 53 |
return new Promise((resolve, reject) => {
|
| 54 |
const ws = new WebSocket(GATEWAY_URL);
|
|
|
|
| 96 |
const msg = JSON.parse(data.toString());
|
| 97 |
if (msg.id === id) {
|
| 98 |
ws.removeListener("message", handler);
|
| 99 |
+
if (msg.ok === false) {
|
| 100 |
+
reject(new Error(extractErrorMessage(msg)));
|
| 101 |
+
return;
|
| 102 |
+
}
|
| 103 |
resolve(msg);
|
| 104 |
}
|
| 105 |
};
|
|
|
|
| 129 |
return;
|
| 130 |
}
|
| 131 |
|
| 132 |
+
const lastError = String(wa.lastError || "").toLowerCase();
|
| 133 |
+
const recentlySaw515 = Date.now() - last515At < POST_515_NO_LOGOUT_MS;
|
| 134 |
+
const needsLogout = wa.linked && !wa.connected && !recentlySaw515 &&
|
| 135 |
+
(
|
| 136 |
+
lastError.includes("401") ||
|
| 137 |
+
lastError.includes("unauthorized") ||
|
| 138 |
+
lastError.includes("logged out") ||
|
| 139 |
+
lastError.includes("440") ||
|
| 140 |
+
lastError.includes("conflict")
|
| 141 |
+
);
|
| 142 |
+
|
| 143 |
+
if (needsLogout) {
|
| 144 |
+
console.log("[guardian] Clearing invalid WhatsApp session so a fresh QR can be used...");
|
| 145 |
+
try {
|
| 146 |
+
await callRpc(ws, "channels.logout", { channel: "whatsapp" });
|
| 147 |
+
writeResetMarker();
|
| 148 |
+
hasShownWaitMessage = false;
|
| 149 |
+
console.log("[guardian] Logged out invalid WhatsApp session.");
|
| 150 |
+
} catch (error) {
|
| 151 |
+
console.log(`[guardian] Failed to log out invalid session: ${error.message}`);
|
| 152 |
+
}
|
| 153 |
+
ws.close();
|
| 154 |
+
return;
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
// If connected, we are good
|
| 158 |
if (wa.connected) {
|
| 159 |
+
hasShownWaitMessage = false;
|
| 160 |
ws.close();
|
| 161 |
return;
|
| 162 |
}
|
|
|
|
| 171 |
console.log("[guardian] Waiting for pairing completion...");
|
| 172 |
const waitRes = await callRpc(ws, "web.login.wait", { timeoutMs: WAIT_TIMEOUT });
|
| 173 |
const result = waitRes.payload || waitRes.result;
|
| 174 |
+
const message = result?.message || "";
|
| 175 |
+
const linkedAfter515 = !result?.connected && message.includes("515");
|
| 176 |
+
|
| 177 |
+
if (linkedAfter515) {
|
| 178 |
+
last515At = Date.now();
|
| 179 |
+
}
|
| 180 |
|
| 181 |
+
if (result && (result.connected || linkedAfter515)) {
|
|
|
|
| 182 |
hasShownWaitMessage = false;
|
| 183 |
+
|
| 184 |
+
if (linkedAfter515) {
|
| 185 |
+
console.log("[guardian] 515 after scan: credentials saved, reloading config to start WhatsApp...");
|
| 186 |
+
} else {
|
| 187 |
+
console.log("[guardian] β
Pairing completed! Reloading config...");
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
const getRes = await callRpc(ws, "config.get", {});
|
| 191 |
+
if (getRes.payload?.raw && getRes.payload?.hash) {
|
| 192 |
await callRpc(ws, "config.apply", { raw: getRes.payload.raw, baseHash: getRes.payload.hash });
|
| 193 |
console.log("[guardian] Configuration re-applied.");
|
| 194 |
}
|
| 195 |
+
} else if (!message.includes("No active") && !message.includes("Still waiting")) {
|
| 196 |
+
console.log(`[guardian] Wait result: ${message}`);
|
| 197 |
}
|
| 198 |
|
| 199 |
} catch (e) {
|
workspace-sync.py
CHANGED
|
@@ -11,10 +11,15 @@ import os
|
|
| 11 |
import sys
|
| 12 |
import time
|
| 13 |
import signal
|
|
|
|
| 14 |
import subprocess
|
| 15 |
from pathlib import Path
|
| 16 |
|
| 17 |
WORKSPACE = Path("/home/node/.openclaw/workspace")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
INTERVAL = int(os.environ.get("SYNC_INTERVAL", "600"))
|
| 19 |
HF_TOKEN = os.environ.get("HF_TOKEN", "")
|
| 20 |
HF_USERNAME = os.environ.get("HF_USERNAME", "")
|
|
@@ -31,6 +36,47 @@ signal.signal(signal.SIGTERM, signal_handler)
|
|
| 31 |
signal.signal(signal.SIGINT, signal_handler)
|
| 32 |
|
| 33 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
def has_changes():
|
| 35 |
"""Check if workspace has uncommitted changes (git-based check)."""
|
| 36 |
try:
|
|
@@ -142,6 +188,8 @@ def main():
|
|
| 142 |
|
| 143 |
use_hf_hub = bool(HF_TOKEN and HF_USERNAME)
|
| 144 |
|
|
|
|
|
|
|
| 145 |
if use_hf_hub:
|
| 146 |
print(f"π Workspace sync started (huggingface_hub): every {INTERVAL}s β {HF_USERNAME}/{BACKUP_DATASET}")
|
| 147 |
else:
|
|
@@ -156,6 +204,8 @@ def main():
|
|
| 156 |
if not running:
|
| 157 |
break
|
| 158 |
|
|
|
|
|
|
|
| 159 |
if not has_changes():
|
| 160 |
continue
|
| 161 |
|
|
|
|
| 11 |
import sys
|
| 12 |
import time
|
| 13 |
import signal
|
| 14 |
+
import shutil
|
| 15 |
import subprocess
|
| 16 |
from pathlib import Path
|
| 17 |
|
| 18 |
WORKSPACE = Path("/home/node/.openclaw/workspace")
|
| 19 |
+
STATE_DIR = WORKSPACE / ".huggingclaw-state"
|
| 20 |
+
WHATSAPP_CREDS_DIR = Path("/home/node/.openclaw/credentials/whatsapp/default")
|
| 21 |
+
WHATSAPP_BACKUP_DIR = STATE_DIR / "credentials" / "whatsapp" / "default"
|
| 22 |
+
RESET_MARKER = WORKSPACE / ".reset_credentials"
|
| 23 |
INTERVAL = int(os.environ.get("SYNC_INTERVAL", "600"))
|
| 24 |
HF_TOKEN = os.environ.get("HF_TOKEN", "")
|
| 25 |
HF_USERNAME = os.environ.get("HF_USERNAME", "")
|
|
|
|
| 36 |
signal.signal(signal.SIGINT, signal_handler)
|
| 37 |
|
| 38 |
|
| 39 |
+
def count_files(path: Path) -> int:
|
| 40 |
+
"""Count regular files recursively under a path."""
|
| 41 |
+
if not path.exists():
|
| 42 |
+
return 0
|
| 43 |
+
return sum(1 for child in path.rglob("*") if child.is_file())
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def snapshot_state_into_workspace() -> None:
|
| 47 |
+
"""
|
| 48 |
+
Mirror persistent state into the workspace-backed dataset repo.
|
| 49 |
+
|
| 50 |
+
This keeps WhatsApp credentials in a hidden folder that is synced together
|
| 51 |
+
with the workspace, without changing the live credentials location.
|
| 52 |
+
"""
|
| 53 |
+
try:
|
| 54 |
+
STATE_DIR.mkdir(parents=True, exist_ok=True)
|
| 55 |
+
|
| 56 |
+
if RESET_MARKER.exists():
|
| 57 |
+
if WHATSAPP_BACKUP_DIR.exists():
|
| 58 |
+
shutil.rmtree(WHATSAPP_BACKUP_DIR, ignore_errors=True)
|
| 59 |
+
print("π§Ή Removed backed-up WhatsApp credentials after reset request.")
|
| 60 |
+
RESET_MARKER.unlink(missing_ok=True)
|
| 61 |
+
return
|
| 62 |
+
|
| 63 |
+
if not WHATSAPP_CREDS_DIR.exists():
|
| 64 |
+
return
|
| 65 |
+
|
| 66 |
+
file_count = count_files(WHATSAPP_CREDS_DIR)
|
| 67 |
+
if file_count < 2:
|
| 68 |
+
if file_count > 0:
|
| 69 |
+
print(f"π¦ WhatsApp backup skipped: credentials incomplete ({file_count} files).")
|
| 70 |
+
return
|
| 71 |
+
|
| 72 |
+
WHATSAPP_BACKUP_DIR.parent.mkdir(parents=True, exist_ok=True)
|
| 73 |
+
if WHATSAPP_BACKUP_DIR.exists():
|
| 74 |
+
shutil.rmtree(WHATSAPP_BACKUP_DIR, ignore_errors=True)
|
| 75 |
+
shutil.copytree(WHATSAPP_CREDS_DIR, WHATSAPP_BACKUP_DIR)
|
| 76 |
+
except Exception as e:
|
| 77 |
+
print(f" β οΈ Could not snapshot WhatsApp state: {e}")
|
| 78 |
+
|
| 79 |
+
|
| 80 |
def has_changes():
|
| 81 |
"""Check if workspace has uncommitted changes (git-based check)."""
|
| 82 |
try:
|
|
|
|
| 188 |
|
| 189 |
use_hf_hub = bool(HF_TOKEN and HF_USERNAME)
|
| 190 |
|
| 191 |
+
snapshot_state_into_workspace()
|
| 192 |
+
|
| 193 |
if use_hf_hub:
|
| 194 |
print(f"π Workspace sync started (huggingface_hub): every {INTERVAL}s β {HF_USERNAME}/{BACKUP_DATASET}")
|
| 195 |
else:
|
|
|
|
| 204 |
if not running:
|
| 205 |
break
|
| 206 |
|
| 207 |
+
snapshot_state_into_workspace()
|
| 208 |
+
|
| 209 |
if not has_changes():
|
| 210 |
continue
|
| 211 |
|