Spaces:
Running
Running
File size: 8,011 Bytes
51ec4bc 969345a bcbf1ad 51ec4bc 529ca98 51ec4bc bcbf1ad 51ec4bc 969345a e0ab924 969345a bcbf1ad 51ec4bc 969345a e0ab924 6a8bffa 544bf0f bcbf1ad 544bf0f 51ec4bc 969345a bcbf1ad 969345a bcbf1ad 969345a 51ec4bc 529ca98 51ec4bc bcbf1ad 529ca98 bcbf1ad 51ec4bc 544bf0f 51ec4bc bcbf1ad 51ec4bc 969345a 51ec4bc bcbf1ad 51ec4bc 6a8bffa 51ec4bc bcbf1ad 51ec4bc e0ab924 bcbf1ad e0ab924 bcbf1ad e0ab924 51ec4bc bcbf1ad 51ec4bc bcbf1ad 51ec4bc bcbf1ad 51ec4bc 969345a 51ec4bc 969345a 51ec4bc e0ab924 bcbf1ad 969345a bcbf1ad 969345a 51ec4bc 969345a bcbf1ad 51ec4bc 6a8bffa bcbf1ad 969345a 51ec4bc bcbf1ad 51ec4bc 43f221d bcbf1ad 43f221d bcbf1ad 43f221d bcbf1ad 529ca98 51ec4bc bcbf1ad 51ec4bc 529ca98 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 | /**
* HuggingClaw WhatsApp Guardian
*
* Automates the WhatsApp pairing process on HuggingFace Spaces.
* Handles the "515 Restart" by monitoring the channel status and
* re-applying the configuration after a successful scan.
*/
"use strict";
const fs = require("fs");
const path = require("path");
const { WebSocket } = require('/home/node/.openclaw/openclaw-app/node_modules/ws');
const { randomUUID } = require('node:crypto');
const GATEWAY_URL = "ws://127.0.0.1:7860";
const GATEWAY_TOKEN = process.env.GATEWAY_TOKEN || "huggingclaw";
const WHATSAPP_ENABLED = /^true$/i.test(process.env.WHATSAPP_ENABLED || "");
const CHECK_INTERVAL = 5000;
const WAIT_TIMEOUT = 120000;
const POST_515_NO_LOGOUT_MS = 90 * 1000;
const SUCCESS_COOLDOWN_MS = 60 * 1000;
const RESET_MARKER_PATH = path.join(
process.env.HOME || "/home/node",
".openclaw",
"workspace",
".reset_credentials",
);
const STATUS_FILE_PATH = "/tmp/huggingclaw-wa-status.json";
let isWaiting = false;
let hasShownWaitMessage = false;
let last515At = 0;
let lastConnectedAt = 0;
let shouldStop = false;
function extractErrorMessage(msg) {
if (!msg || typeof msg !== "object") return "Unknown error";
if (typeof msg.error === "string") return msg.error;
if (msg.error && typeof msg.error.message === "string") return msg.error.message;
if (typeof msg.message === "string") return msg.message;
return "Unknown error";
}
function writeResetMarker() {
try {
fs.mkdirSync(path.dirname(RESET_MARKER_PATH), { recursive: true });
fs.writeFileSync(RESET_MARKER_PATH, "reset\n");
console.log(`[guardian] Created backup reset marker at ${RESET_MARKER_PATH}`);
} catch (error) {
console.log(`[guardian] Failed to write backup reset marker: ${error.message}`);
}
}
function writeStatus(partial) {
try {
const current = fs.existsSync(STATUS_FILE_PATH)
? JSON.parse(fs.readFileSync(STATUS_FILE_PATH, "utf8"))
: {};
const next = {
configured: true,
connected: false,
pairing: false,
updatedAt: new Date().toISOString(),
...current,
...partial,
};
fs.writeFileSync(STATUS_FILE_PATH, JSON.stringify(next));
} catch (error) {
console.log(`[guardian] Failed to write status file: ${error.message}`);
}
}
async 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: GATEWAY_TOKEN },
role: "operator",
scopes: ["operator.read", "operator.write", "operator.admin", "operator.pairing"],
},
}));
return;
}
if (!resolved && msg.type === "res" && msg.ok === false) {
resolved = true;
ws.close();
reject(new Error(extractErrorMessage(msg)));
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("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);
if (msg.ok === false) {
reject(new Error(extractErrorMessage(msg)));
return;
}
resolve(msg);
}
};
ws.on("message", handler);
ws.send(JSON.stringify({ type: "req", id, method, params }));
setTimeout(() => { ws.removeListener("message", handler); reject(new Error("RPC Timeout")); }, WAIT_TIMEOUT + 5000);
});
}
async function checkStatus() {
if (shouldStop) return;
if (isWaiting) return;
if (lastConnectedAt && Date.now() - lastConnectedAt < SUCCESS_COOLDOWN_MS) return;
let ws;
try {
ws = await createConnection();
const statusRes = await callRpc(ws, "channels.status", {});
const channels = (statusRes.payload || statusRes.result)?.channels || {};
const wa = channels.whatsapp;
if (!wa) {
hasShownWaitMessage = false;
writeStatus({ configured: true, connected: false, pairing: false });
return;
}
if (wa.connected) {
hasShownWaitMessage = false;
lastConnectedAt = Date.now();
writeStatus({ configured: true, connected: true, pairing: false });
shouldStop = true;
setTimeout(() => process.exit(0), 1000);
return;
}
isWaiting = true;
writeStatus({ configured: true, connected: false, pairing: true });
if (!hasShownWaitMessage) {
console.log("\n[guardian] 📱 WhatsApp pairing in progress. Please scan the QR code in the Control UI.");
hasShownWaitMessage = true;
}
console.log("[guardian] Waiting for pairing completion...");
const waitRes = await callRpc(ws, "web.login.wait", { timeoutMs: WAIT_TIMEOUT });
const result = waitRes.payload || waitRes.result;
const message = result?.message || "";
const linkedAfter515 = !result?.connected && message.includes("515");
if (linkedAfter515) {
last515At = Date.now();
}
if (result && (result.connected || linkedAfter515)) {
hasShownWaitMessage = false;
lastConnectedAt = Date.now();
writeStatus({ configured: true, connected: true, pairing: false });
if (linkedAfter515) {
console.log("[guardian] 515 after scan: credentials saved, reloading config to start WhatsApp...");
} else {
console.log("[guardian] ✅ Pairing completed! Reloading config...");
}
const getRes = await callRpc(ws, "config.get", {});
if (getRes.payload?.raw && getRes.payload?.hash) {
await callRpc(ws, "config.apply", { raw: getRes.payload.raw, baseHash: getRes.payload.hash });
console.log("[guardian] Configuration re-applied.");
}
shouldStop = true;
setTimeout(() => process.exit(0), 1000);
} else if (!message.includes("No active") && !message.includes("Still waiting")) {
console.log(`[guardian] Wait result: ${message}`);
}
} catch (e) {
const message = e && e.message ? e.message : "";
if (
/401|unauthorized|logged out|440|conflict/i.test(message) &&
Date.now() - last515At >= POST_515_NO_LOGOUT_MS
) {
console.log("[guardian] Clearing invalid WhatsApp session so a fresh QR can be used...");
try {
if (ws) {
await callRpc(ws, "channels.logout", { channel: "whatsapp" });
writeResetMarker();
hasShownWaitMessage = false;
console.log("[guardian] Logged out invalid WhatsApp session.");
}
} catch (error) {
console.log(`[guardian] Failed to log out invalid session: ${error.message}`);
}
}
if (!/RPC Timeout/i.test(message)) {
writeStatus({ configured: true, connected: false, pairing: false });
}
// Normal timeout or gateway starting up; retry on the next interval.
} finally {
isWaiting = false;
if (ws) ws.close();
}
}
if (!WHATSAPP_ENABLED) {
writeStatus({ configured: false, connected: false, pairing: false });
process.exit(0);
}
writeStatus({ configured: true, connected: false, pairing: false });
console.log("[guardian] ⚔️ WhatsApp Guardian active. Monitoring pairing status...");
setInterval(checkStatus, CHECK_INTERVAL);
setTimeout(checkStatus, 15000);
|