Spaces:
Runtime error
Runtime error
| /** | |
| * WhatsApp Login Guardian — background helper for HF Spaces. | |
| * | |
| * Problem: After QR scan, WhatsApp sends 515 (restart required). The | |
| * web.login.wait RPC handles this restart, but HF Spaces' proxy drops | |
| * WebSocket connections, so the UI's web.login.wait may not be active. | |
| * | |
| * Solution: This script connects to the local gateway and keeps calling | |
| * web.login.wait with long timeouts, ensuring the 515 restart is handled. | |
| * | |
| * Usage: Run as background process from entrypoint.sh | |
| */ | |
| ; | |
| const { WebSocket } = require("ws"); | |
| const { randomUUID } = require("node:crypto"); | |
| const { exec } = require('child_process'); | |
| const GATEWAY_URL = "ws://127.0.0.1:7860"; | |
| const TOKEN = "openclaw-space-default"; | |
| const CHECK_INTERVAL = 5000; // Check every 5s so we catch QR scan quickly | |
| const WAIT_TIMEOUT = 120000; // 2 minute wait timeout | |
| const POST_515_NO_LOGOUT_MS = 90000; // After 515, don't clear "401" for 90s (avoid wiping just-saved creds) | |
| let isWaiting = false; | |
| let last515At = 0; | |
| let hasShownWaitMessage = false; | |
| function createConnection() { | |
| return new Promise((resolve, reject) => { | |
| const ws = new WebSocket(GATEWAY_URL); | |
| let resolved = false; | |
| ws.on("message", (data) => { | |
| const msg = JSON.parse(data.toString()); | |
| if (msg.type === "event" && msg.event === "connect.challenge") { | |
| ws.send( | |
| JSON.stringify({ | |
| type: "req", | |
| id: randomUUID(), | |
| method: "connect", | |
| params: { | |
| minProtocol: 3, | |
| maxProtocol: 3, | |
| client: { | |
| id: "gateway-client", | |
| version: "1.0.0", | |
| platform: "linux", | |
| mode: "backend", | |
| }, | |
| caps: [], | |
| auth: { token: TOKEN }, | |
| role: "operator", | |
| scopes: ["operator.admin"], | |
| }, | |
| }) | |
| ); | |
| return; | |
| } | |
| if (!resolved && msg.type === "res" && msg.ok) { | |
| resolved = true; | |
| resolve(ws); | |
| } | |
| }); | |
| ws.on("error", (e) => { | |
| if (!resolved) reject(e); | |
| }); | |
| setTimeout(() => { | |
| if (!resolved) { | |
| ws.close(); | |
| reject(new Error("Connection timeout")); | |
| } | |
| }, 10000); | |
| }); | |
| } | |
| async function callRpc(ws, method, params) { | |
| return new Promise((resolve, reject) => { | |
| const id = randomUUID(); | |
| const handler = (data) => { | |
| const msg = JSON.parse(data.toString()); | |
| if (msg.id === id) { | |
| ws.removeListener("message", handler); | |
| resolve(msg); | |
| } | |
| }; | |
| ws.on("message", handler); | |
| ws.send(JSON.stringify({ type: "req", id, method, params })); | |
| // Long timeout for web.login.wait | |
| setTimeout(() => { | |
| ws.removeListener("message", handler); | |
| reject(new Error("RPC timeout")); | |
| }, WAIT_TIMEOUT + 5000); | |
| }); | |
| } | |
| async function checkAndWait() { | |
| if (isWaiting) return; | |
| let ws; | |
| try { | |
| ws = await createConnection(); | |
| } catch { | |
| return; // Gateway not ready yet | |
| } | |
| try { | |
| // Check channel status to see if WhatsApp needs attention | |
| const statusRes = await callRpc(ws, "channels.status", {}); | |
| const channels = (statusRes.payload || statusRes.result)?.channels || {}; | |
| const wa = channels.whatsapp; | |
| if (!wa) { | |
| ws.close(); | |
| return; | |
| } | |
| // If linked but got 401/logged out OR 440/conflict, clear invalid credentials so user can get a fresh QR — | |
| // but NOT within POST_515_NO_LOGOUT_MS of a 515 (channel may still report 401 and we'd wipe just-saved creds). | |
| const err = (wa.lastError || "").toLowerCase(); | |
| const recently515 = Date.now() - last515At < POST_515_NO_LOGOUT_MS; | |
| const needsLogout = wa.linked && !wa.connected && !recently515 && | |
| (err.includes("401") || err.includes("unauthorized") || err.includes("logged out") || err.includes("440") || err.includes("conflict")); | |
| if (needsLogout) { | |
| console.log("[wa-guardian] Clearing invalid session (401/440/conflict) so a fresh QR can be used..."); | |
| try { | |
| await callRpc(ws, "channels.logout", { channel: "whatsapp" }); | |
| console.log("[wa-guardian] Logged out; user can click Login again for a new QR."); | |
| // Signal sync_hf.py to delete remote credentials | |
| const fs = require('fs'); | |
| const path = require('path'); | |
| // Workspace is usually /home/node/.openclaw/workspace | |
| const markerPath = path.join(process.env.HOME || '/home/node', '.openclaw/workspace/.reset_credentials'); | |
| fs.writeFileSync(markerPath, 'reset'); | |
| console.log("[wa-guardian] Created .reset_credentials marker for sync script."); | |
| } catch (e) { | |
| console.log("[wa-guardian] channels.logout failed:", e.message); | |
| } | |
| ws.close(); | |
| return; | |
| } | |
| // If WhatsApp is already connected, nothing to do | |
| if (wa.connected) { | |
| ws.close(); | |
| return; | |
| } | |
| // Try web.login.wait — this will handle 515 restart if QR was scanned | |
| isWaiting = true; | |
| if (!hasShownWaitMessage) { | |
| console.log("⏳ Waiting for WhatsApp QR code scan..."); | |
| console.log("📱 Please scan the QR code with your phone to continue."); | |
| hasShownWaitMessage = true; | |
| } | |
| console.log("[wa-guardian] Calling web.login.wait..."); | |
| const waitRes = await callRpc(ws, "web.login.wait", { | |
| timeoutMs: WAIT_TIMEOUT, | |
| }); | |
| const result = waitRes.payload || waitRes.result; | |
| const msg = result?.message || ""; | |
| const linkedAfter515 = !result?.connected && msg.includes("515"); | |
| if (linkedAfter515) last515At = Date.now(); | |
| if (result?.connected || linkedAfter515) { | |
| hasShownWaitMessage = false; // Reset for next time | |
| if (linkedAfter515) { | |
| console.log("[wa-guardian] 515 after scan — credentials saved; triggering config reload to start channel..."); | |
| } else { | |
| console.log("[wa-guardian] WhatsApp connected successfully! Triggering config reload to start channel..."); | |
| } | |
| console.log("✅ QR code scanned successfully. Login completed."); | |
| // Persistence handled by sync_hf.py background loop | |
| try { | |
| const getRes = await callRpc(ws, "config.get", {}); | |
| const raw = getRes.payload?.raw; | |
| const hash = getRes.payload?.hash; | |
| if (raw && hash) { | |
| await callRpc(ws, "config.apply", { raw, baseHash: hash }); | |
| console.log("[wa-guardian] Config applied; gateway will restart with WhatsApp channel."); | |
| } | |
| } catch (e) { | |
| console.log("[wa-guardian] Config apply failed:", e.message); | |
| } | |
| } else { | |
| if (!msg.includes("No active") && !msg.includes("Still waiting")) { | |
| console.log("[wa-guardian] Wait result:", msg); | |
| } | |
| } | |
| } catch (e) { | |
| // Timeout or error — normal, just retry | |
| } finally { | |
| isWaiting = false; | |
| try { | |
| ws.close(); | |
| } catch {} | |
| } | |
| } | |
| // Start checking periodically | |
| console.log("[wa-guardian] WhatsApp login guardian started"); | |
| setInterval(checkAndWait, CHECK_INTERVAL); | |
| // Initial check after 15s (give gateway time to start) | |
| setTimeout(checkAndWait, 15000); | |