Spaces:
Running
Running
File size: 9,176 Bytes
dd7ada8 d439036 d43f799 dd7ada8 73b4eda dd7ada8 de4ed0d 64857cd dd7ada8 d439036 9aba9c1 d439036 de4ed0d dd7ada8 d439036 9aba9c1 ad768a0 2ab78d1 dd7ada8 d439036 de4ed0d dd7ada8 9f04df0 dd7ada8 d43f799 dd7ada8 ba7c710 d5203bf 9f04df0 dd7ada8 9f04df0 6ca12eb 9f04df0 dd7ada8 2ab78d1 dd7ada8 73b4eda dd7ada8 d43f799 dd7ada8 d439036 dd7ada8 f72a7b7 73b4eda dd7ada8 ad768a0 dd7ada8 9aba9c1 dd7ada8 9aba9c1 de4ed0d 9aba9c1 de4ed0d 9aba9c1 dd7ada8 de4ed0d dd7ada8 d43f799 dd7ada8 73b4eda dd7ada8 d439036 dd7ada8 d439036 dd7ada8 9aba9c1 de4ed0d d439036 73b4eda d439036 d43f799 d439036 73b4eda dd7ada8 ad768a0 d439036 dd7ada8 a0b9abe de4ed0d 9f04df0 dd7ada8 de4ed0d f72a7b7 de4ed0d d43f799 dd7ada8 9f04df0 | 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 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 | /**
* 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");
let WebSocket;
try {
({ WebSocket } = require('ws'));
} catch (_) {
({ WebSocket } = require('/home/node/.openclaw/openclaw-app/node_modules/ws'));
}
const { randomUUID } = require('node:crypto');
const GATEWAY_PORT = Number.parseInt(process.env.GATEWAY_PORT || "7860", 10);
const GATEWAY_URL = `ws://127.0.0.1:${GATEWAY_PORT}`;
const GATEWAY_TOKEN = process.env.GATEWAY_TOKEN || "huggingclaw";
const WHATSAPP_ENABLED = /^true$/i.test(process.env.WHATSAPP_ENABLED || "");
const CHECK_INTERVAL = 30000;
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) => {
let msg;
try { msg = JSON.parse(data.toString()); } catch { return; }
if (msg.type === "event" && msg.event === "connect.challenge") {
ws.send(JSON.stringify({
type: "req",
id: randomUUID(),
method: "connect",
params: {
minProtocol: 3,
maxProtocol: 4,
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, timeoutMs) {
const ms = timeoutMs !== undefined ? timeoutMs : 10000; // default 10s for normal calls
return new Promise((resolve, reject) => {
const id = randomUUID();
const handler = (data) => {
let msg;
try { msg = JSON.parse(data.toString()); } catch { return; }
if (msg.id === id) {
ws.removeListener("message", handler);
if (msg.ok === false) {
reject(new Error(extractErrorMessage(msg)));
return;
}
resolve(msg);
}
};
ws.on("message", handler);
try {
ws.send(JSON.stringify({ type: "req", id, method, params }));
} catch (sendErr) {
ws.removeListener("message", handler);
reject(sendErr);
return;
}
setTimeout(() => { ws.removeListener("message", handler); reject(new Error("RPC Timeout")); }, ms);
});
}
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 }, WAIT_TIMEOUT + 5000);
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 });
// Set shouldStop BEFORE config.apply — gateway restart during apply must not
// leave guardian running (it would then incorrectly wipe valid credentials).
shouldStop = true;
if (linkedAfter515) {
console.log("[guardian] 515 after scan: credentials saved, reloading config to start WhatsApp...");
} else {
console.log("[guardian] Pairing completed! Reloading config...");
}
try {
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.");
}
} catch (applyErr) {
// Gateway restarted during config.apply — that is expected and fine.
// shouldStop is already true so we will not retry or attempt logout.
console.log(`[guardian] Config re-apply interrupted (gateway restarting): ${applyErr.message}`);
}
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);
}
process.on("unhandledRejection", (reason) => {
const msg = reason && reason.message ? reason.message : String(reason);
if (!/RPC Timeout|Timeout/i.test(msg)) {
console.log(`[guardian] Unhandled rejection: ${msg}`);
}
});
writeStatus({ configured: true, connected: false, pairing: false });
console.log("[guardian] WhatsApp Guardian active. Monitoring pairing status...");
setInterval(checkStatus, CHECK_INTERVAL);
setTimeout(checkStatus, 15000);
|