Spaces:
Running
Running
feat: add optional WhatsApp integration with toggle support and status tracking
Browse files- .env.example +4 -0
- README.md +12 -4
- health-server.js +22 -123
- start.sh +17 -7
- wa-guardian.js +72 -78
- workspace-sync.py +4 -0
.env.example
CHANGED
|
@@ -133,6 +133,10 @@ GATEWAY_TOKEN=your_gateway_token_here
|
|
| 133 |
# OPENCLAW_PASSWORD=your_password_here
|
| 134 |
|
| 135 |
# ββ OPTIONAL: Chat Integrations ββ
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
# Get bot token from: https://t.me/BotFather
|
| 137 |
TELEGRAM_BOT_TOKEN=your_bot_token_here
|
| 138 |
|
|
|
|
| 133 |
# OPENCLAW_PASSWORD=your_password_here
|
| 134 |
|
| 135 |
# ββ OPTIONAL: Chat Integrations ββ
|
| 136 |
+
# Enable WhatsApp pairing flow
|
| 137 |
+
# Set to true only if you want WhatsApp enabled
|
| 138 |
+
WHATSAPP_ENABLED=false
|
| 139 |
+
|
| 140 |
# Get bot token from: https://t.me/BotFather
|
| 141 |
TELEGRAM_BOT_TOKEN=your_bot_token_here
|
| 142 |
|
README.md
CHANGED
|
@@ -98,10 +98,9 @@ After restarting, the bot should appear online on Telegram.
|
|
| 98 |
|
| 99 |
To use WhatsApp:
|
| 100 |
|
| 101 |
-
1.
|
| 102 |
-
2.
|
| 103 |
-
3.
|
| 104 |
-
4. Scan the QR code with your phone. π±
|
| 105 |
|
| 106 |
## πΎ Workspace Backup *(Optional)*
|
| 107 |
|
|
@@ -139,6 +138,15 @@ See `.env.example` for runtime settings. Key configuration values:
|
|
| 139 |
| `LLM_MODEL` | Model ID (prefix `<provider>/`, auto-detected from prefix) |
|
| 140 |
| `GATEWAY_TOKEN` | Gateway token for Control UI access (required) |
|
| 141 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
### Background Services
|
| 143 |
|
| 144 |
| Variable | Default | Description |
|
|
|
|
| 98 |
|
| 99 |
To use WhatsApp:
|
| 100 |
|
| 101 |
+
1. Add `WHATSAPP_ENABLED=true` in Hugging Face Space Variables or Secrets.
|
| 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 |
|
|
|
|
| 138 |
| `LLM_MODEL` | Model ID (prefix `<provider>/`, auto-detected from prefix) |
|
| 139 |
| `GATEWAY_TOKEN` | Gateway token for Control UI access (required) |
|
| 140 |
|
| 141 |
+
### Chat Integrations
|
| 142 |
+
|
| 143 |
+
| Variable | Default | Description |
|
| 144 |
+
|-----------------------|---------|----------------------------------------------------|
|
| 145 |
+
| `WHATSAPP_ENABLED` | `false` | Enable WhatsApp pairing/login support |
|
| 146 |
+
| `TELEGRAM_BOT_TOKEN` | β | Telegram bot token from BotFather |
|
| 147 |
+
| `TELEGRAM_USER_ID` | β | Single Telegram user ID allowlist |
|
| 148 |
+
| `TELEGRAM_USER_IDS` | β | Comma-separated Telegram user IDs for team access |
|
| 149 |
+
|
| 150 |
### Background Services
|
| 151 |
|
| 152 |
| Variable | Default | Description |
|
health-server.js
CHANGED
|
@@ -2,24 +2,15 @@
|
|
| 2 |
const http = require("http");
|
| 3 |
const fs = require("fs");
|
| 4 |
const net = require("net");
|
| 5 |
-
const { randomUUID } = require("node:crypto");
|
| 6 |
|
| 7 |
const PORT = 7861;
|
| 8 |
const GATEWAY_PORT = 7860;
|
| 9 |
const GATEWAY_HOST = "127.0.0.1";
|
| 10 |
const startTime = Date.now();
|
| 11 |
const LLM_MODEL = process.env.LLM_MODEL || "Not Set";
|
| 12 |
-
const GATEWAY_TOKEN = process.env.GATEWAY_TOKEN || "";
|
| 13 |
const TELEGRAM_ENABLED = !!process.env.TELEGRAM_BOT_TOKEN;
|
| 14 |
-
const
|
| 15 |
-
|
| 16 |
-
let gatewayStatusCache = {
|
| 17 |
-
expiresAt: 0,
|
| 18 |
-
value: {
|
| 19 |
-
whatsapp: { configured: true, connected: false },
|
| 20 |
-
telegram: { configured: TELEGRAM_ENABLED, connected: false },
|
| 21 |
-
},
|
| 22 |
-
};
|
| 23 |
|
| 24 |
function parseRequestUrl(url) {
|
| 25 |
try {
|
|
@@ -74,117 +65,21 @@ function normalizeChannelStatus(channel, configured) {
|
|
| 74 |
};
|
| 75 |
}
|
| 76 |
|
| 77 |
-
function
|
| 78 |
-
if (!
|
| 79 |
-
|
| 80 |
-
if (msg.error && typeof msg.error.message === "string") return msg.error.message;
|
| 81 |
-
if (typeof msg.message === "string") return msg.message;
|
| 82 |
-
return "Unknown error";
|
| 83 |
-
}
|
| 84 |
-
|
| 85 |
-
function createGatewayConnection() {
|
| 86 |
-
return new Promise((resolve, reject) => {
|
| 87 |
-
const { WebSocket } = require("/home/node/.openclaw/openclaw-app/node_modules/ws");
|
| 88 |
-
const ws = new WebSocket(`ws://${GATEWAY_HOST}:${GATEWAY_PORT}`);
|
| 89 |
-
let resolved = false;
|
| 90 |
-
|
| 91 |
-
ws.on("message", (data) => {
|
| 92 |
-
const msg = JSON.parse(data.toString());
|
| 93 |
-
|
| 94 |
-
if (msg.type === "event" && msg.event === "connect.challenge") {
|
| 95 |
-
ws.send(JSON.stringify({
|
| 96 |
-
type: "req",
|
| 97 |
-
id: randomUUID(),
|
| 98 |
-
method: "connect",
|
| 99 |
-
params: {
|
| 100 |
-
minProtocol: 3,
|
| 101 |
-
maxProtocol: 3,
|
| 102 |
-
client: {
|
| 103 |
-
id: "health-server",
|
| 104 |
-
version: "1.0.0",
|
| 105 |
-
platform: "linux",
|
| 106 |
-
mode: "backend",
|
| 107 |
-
},
|
| 108 |
-
caps: [],
|
| 109 |
-
auth: { token: GATEWAY_TOKEN },
|
| 110 |
-
role: "operator",
|
| 111 |
-
scopes: ["operator.read"],
|
| 112 |
-
},
|
| 113 |
-
}));
|
| 114 |
-
return;
|
| 115 |
-
}
|
| 116 |
-
|
| 117 |
-
if (!resolved && msg.type === "res" && msg.ok === false) {
|
| 118 |
-
resolved = true;
|
| 119 |
-
ws.close();
|
| 120 |
-
reject(new Error(extractErrorMessage(msg)));
|
| 121 |
-
return;
|
| 122 |
-
}
|
| 123 |
-
|
| 124 |
-
if (!resolved && msg.type === "res" && msg.ok) {
|
| 125 |
-
resolved = true;
|
| 126 |
-
resolve(ws);
|
| 127 |
-
}
|
| 128 |
-
});
|
| 129 |
-
|
| 130 |
-
ws.on("error", (error) => {
|
| 131 |
-
if (!resolved) reject(error);
|
| 132 |
-
});
|
| 133 |
-
|
| 134 |
-
setTimeout(() => {
|
| 135 |
-
if (!resolved) {
|
| 136 |
-
ws.close();
|
| 137 |
-
reject(new Error("Timeout"));
|
| 138 |
-
}
|
| 139 |
-
}, 10000);
|
| 140 |
-
});
|
| 141 |
-
}
|
| 142 |
-
|
| 143 |
-
function callGatewayRpc(ws, method, params) {
|
| 144 |
-
return new Promise((resolve, reject) => {
|
| 145 |
-
const id = randomUUID();
|
| 146 |
-
const handler = (data) => {
|
| 147 |
-
const msg = JSON.parse(data.toString());
|
| 148 |
-
if (msg.id === id) {
|
| 149 |
-
ws.removeListener("message", handler);
|
| 150 |
-
resolve(msg);
|
| 151 |
-
}
|
| 152 |
-
};
|
| 153 |
-
|
| 154 |
-
ws.on("message", handler);
|
| 155 |
-
ws.send(JSON.stringify({ type: "req", id, method, params }));
|
| 156 |
-
|
| 157 |
-
setTimeout(() => {
|
| 158 |
-
ws.removeListener("message", handler);
|
| 159 |
-
reject(new Error("RPC Timeout"));
|
| 160 |
-
}, 15000);
|
| 161 |
-
});
|
| 162 |
-
}
|
| 163 |
-
|
| 164 |
-
async function getGatewayChannelStatus() {
|
| 165 |
-
if (Date.now() < gatewayStatusCache.expiresAt) {
|
| 166 |
-
return gatewayStatusCache.value;
|
| 167 |
}
|
| 168 |
-
|
| 169 |
-
let ws;
|
| 170 |
try {
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
};
|
| 182 |
-
return value;
|
| 183 |
-
} catch {
|
| 184 |
-
return gatewayStatusCache.value;
|
| 185 |
-
} finally {
|
| 186 |
-
if (ws) ws.close();
|
| 187 |
-
}
|
| 188 |
}
|
| 189 |
|
| 190 |
function renderDashboard() {
|
|
@@ -570,13 +465,17 @@ const server = http.createServer((req, res) => {
|
|
| 570 |
|
| 571 |
if (pathname === "/status") {
|
| 572 |
void (async () => {
|
| 573 |
-
const
|
| 574 |
res.writeHead(200, { "Content-Type": "application/json" });
|
| 575 |
res.end(
|
| 576 |
JSON.stringify({
|
| 577 |
model: LLM_MODEL,
|
| 578 |
-
whatsapp:
|
| 579 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 580 |
sync: readSyncStatus(),
|
| 581 |
uptime: uptimeHuman,
|
| 582 |
}),
|
|
|
|
| 2 |
const http = require("http");
|
| 3 |
const fs = require("fs");
|
| 4 |
const net = require("net");
|
|
|
|
| 5 |
|
| 6 |
const PORT = 7861;
|
| 7 |
const GATEWAY_PORT = 7860;
|
| 8 |
const GATEWAY_HOST = "127.0.0.1";
|
| 9 |
const startTime = Date.now();
|
| 10 |
const LLM_MODEL = process.env.LLM_MODEL || "Not Set";
|
|
|
|
| 11 |
const TELEGRAM_ENABLED = !!process.env.TELEGRAM_BOT_TOKEN;
|
| 12 |
+
const WHATSAPP_ENABLED = /^true$/i.test(process.env.WHATSAPP_ENABLED || "");
|
| 13 |
+
const WHATSAPP_STATUS_FILE = "/tmp/huggingclaw-wa-status.json";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
|
| 15 |
function parseRequestUrl(url) {
|
| 16 |
try {
|
|
|
|
| 65 |
};
|
| 66 |
}
|
| 67 |
|
| 68 |
+
function readGuardianStatus() {
|
| 69 |
+
if (!WHATSAPP_ENABLED) {
|
| 70 |
+
return { configured: false, connected: false, pairing: false };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
}
|
|
|
|
|
|
|
| 72 |
try {
|
| 73 |
+
if (fs.existsSync(WHATSAPP_STATUS_FILE)) {
|
| 74 |
+
const parsed = JSON.parse(fs.readFileSync(WHATSAPP_STATUS_FILE, "utf8"));
|
| 75 |
+
return {
|
| 76 |
+
configured: parsed.configured !== false,
|
| 77 |
+
connected: parsed.connected === true,
|
| 78 |
+
pairing: parsed.pairing === true,
|
| 79 |
+
};
|
| 80 |
+
}
|
| 81 |
+
} catch {}
|
| 82 |
+
return { configured: true, connected: false, pairing: false };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
}
|
| 84 |
|
| 85 |
function renderDashboard() {
|
|
|
|
| 465 |
|
| 466 |
if (pathname === "/status") {
|
| 467 |
void (async () => {
|
| 468 |
+
const guardianStatus = readGuardianStatus();
|
| 469 |
res.writeHead(200, { "Content-Type": "application/json" });
|
| 470 |
res.end(
|
| 471 |
JSON.stringify({
|
| 472 |
model: LLM_MODEL,
|
| 473 |
+
whatsapp: {
|
| 474 |
+
configured: guardianStatus.configured,
|
| 475 |
+
connected: guardianStatus.connected,
|
| 476 |
+
pairing: guardianStatus.pairing,
|
| 477 |
+
},
|
| 478 |
+
telegram: normalizeChannelStatus(null, TELEGRAM_ENABLED),
|
| 479 |
sync: readSyncStatus(),
|
| 480 |
uptime: uptimeHuman,
|
| 481 |
}),
|
start.sh
CHANGED
|
@@ -7,6 +7,8 @@ set -e
|
|
| 7 |
|
| 8 |
# ββ Startup Banner ββ
|
| 9 |
OPENCLAW_VERSION="${OPENCLAW_VERSION:-latest}"
|
|
|
|
|
|
|
| 10 |
echo ""
|
| 11 |
echo " ββββββββββββββββββββββββββββββββββββββββββββ"
|
| 12 |
echo " β π¦ HuggingClaw Gateway β"
|
|
@@ -166,7 +168,7 @@ fi
|
|
| 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..."
|
|
@@ -252,9 +254,11 @@ if [ -n "$TELEGRAM_BOT_TOKEN" ]; then
|
|
| 252 |
fi
|
| 253 |
fi
|
| 254 |
|
| 255 |
-
# WhatsApp
|
| 256 |
-
|
| 257 |
-
CONFIG_JSON=$(echo "$CONFIG_JSON" | jq '.
|
|
|
|
|
|
|
| 258 |
|
| 259 |
# Write config
|
| 260 |
echo "$CONFIG_JSON" > "/home/node/.openclaw/openclaw.json"
|
|
@@ -311,7 +315,11 @@ printf " β %-40s β\n" "Telegram: β
enabled"
|
|
| 311 |
else
|
| 312 |
printf " β %-40s β\n" "Telegram: β not configured"
|
| 313 |
fi
|
|
|
|
| 314 |
printf " β %-40s β\n" "WhatsApp: β
enabled"
|
|
|
|
|
|
|
|
|
|
| 315 |
if [ -n "$HF_USERNAME" ] && [ -n "$HF_TOKEN" ]; then
|
| 316 |
printf " β %-40s β\n" "Backup: β
${HF_USERNAME}/${BACKUP_DATASET:-huggingclaw-backup}"
|
| 317 |
else
|
|
@@ -408,9 +416,11 @@ if ! kill -0 $GATEWAY_PID 2>/dev/null; then
|
|
| 408 |
fi
|
| 409 |
|
| 410 |
# 12. Start WhatsApp Guardian after the gateway is accepting connections
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
|
|
|
|
|
|
| 414 |
|
| 415 |
# 13. Start Workspace Sync after startup settles
|
| 416 |
python3 -u /home/node/app/workspace-sync.py &
|
|
|
|
| 7 |
|
| 8 |
# ββ Startup Banner ββ
|
| 9 |
OPENCLAW_VERSION="${OPENCLAW_VERSION:-latest}"
|
| 10 |
+
WHATSAPP_ENABLED="${WHATSAPP_ENABLED:-false}"
|
| 11 |
+
WHATSAPP_ENABLED_NORMALIZED=$(printf '%s' "$WHATSAPP_ENABLED" | tr '[:upper:]' '[:lower:]')
|
| 12 |
echo ""
|
| 13 |
echo " ββββββββββββββββββββββββββββββββββββββββββββ"
|
| 14 |
echo " β π¦ HuggingClaw Gateway β"
|
|
|
|
| 168 |
# ββ Restore persisted WhatsApp credentials (if present) ββ
|
| 169 |
WA_BACKUP_DIR="/home/node/.openclaw/workspace/.huggingclaw-state/credentials/whatsapp/default"
|
| 170 |
WA_CREDS_DIR="/home/node/.openclaw/credentials/whatsapp/default"
|
| 171 |
+
if [ "$WHATSAPP_ENABLED_NORMALIZED" = "true" ] && [ -d "$WA_BACKUP_DIR" ]; then
|
| 172 |
WA_FILE_COUNT=$(find "$WA_BACKUP_DIR" -type f | wc -l | tr -d ' ')
|
| 173 |
if [ "$WA_FILE_COUNT" -ge 2 ]; then
|
| 174 |
echo "π± Restoring WhatsApp credentials..."
|
|
|
|
| 254 |
fi
|
| 255 |
fi
|
| 256 |
|
| 257 |
+
# WhatsApp (optional)
|
| 258 |
+
if [ "$WHATSAPP_ENABLED_NORMALIZED" = "true" ]; then
|
| 259 |
+
CONFIG_JSON=$(echo "$CONFIG_JSON" | jq '.plugins.entries.whatsapp = {"enabled": true}')
|
| 260 |
+
CONFIG_JSON=$(echo "$CONFIG_JSON" | jq '.channels.whatsapp = {"dmPolicy": "pairing"}')
|
| 261 |
+
fi
|
| 262 |
|
| 263 |
# Write config
|
| 264 |
echo "$CONFIG_JSON" > "/home/node/.openclaw/openclaw.json"
|
|
|
|
| 315 |
else
|
| 316 |
printf " β %-40s β\n" "Telegram: β not configured"
|
| 317 |
fi
|
| 318 |
+
if [ "$WHATSAPP_ENABLED_NORMALIZED" = "true" ]; then
|
| 319 |
printf " β %-40s β\n" "WhatsApp: β
enabled"
|
| 320 |
+
else
|
| 321 |
+
printf " β %-40s β\n" "WhatsApp: β disabled"
|
| 322 |
+
fi
|
| 323 |
if [ -n "$HF_USERNAME" ] && [ -n "$HF_TOKEN" ]; then
|
| 324 |
printf " β %-40s β\n" "Backup: β
${HF_USERNAME}/${BACKUP_DATASET:-huggingclaw-backup}"
|
| 325 |
else
|
|
|
|
| 416 |
fi
|
| 417 |
|
| 418 |
# 12. Start WhatsApp Guardian after the gateway is accepting connections
|
| 419 |
+
if [ "$WHATSAPP_ENABLED_NORMALIZED" = "true" ]; then
|
| 420 |
+
node /home/node/app/wa-guardian.js &
|
| 421 |
+
GUARDIAN_PID=$!
|
| 422 |
+
echo "π‘οΈ WhatsApp Guardian started (PID: $GUARDIAN_PID)"
|
| 423 |
+
fi
|
| 424 |
|
| 425 |
# 13. Start Workspace Sync after startup settles
|
| 426 |
python3 -u /home/node/app/workspace-sync.py &
|
wa-guardian.js
CHANGED
|
@@ -9,13 +9,12 @@
|
|
| 9 |
|
| 10 |
const fs = require("fs");
|
| 11 |
const path = require("path");
|
| 12 |
-
const {
|
| 13 |
-
|
| 14 |
-
} = require("/home/node/.openclaw/openclaw-app/node_modules/ws");
|
| 15 |
-
const { randomUUID } = require("node:crypto");
|
| 16 |
|
| 17 |
const GATEWAY_URL = "ws://127.0.0.1:7860";
|
| 18 |
const GATEWAY_TOKEN = process.env.GATEWAY_TOKEN || "huggingclaw";
|
|
|
|
| 19 |
const CHECK_INTERVAL = 5000;
|
| 20 |
const WAIT_TIMEOUT = 120000;
|
| 21 |
const POST_515_NO_LOGOUT_MS = 90 * 1000;
|
|
@@ -26,6 +25,7 @@ const RESET_MARKER_PATH = path.join(
|
|
| 26 |
"workspace",
|
| 27 |
".reset_credentials",
|
| 28 |
);
|
|
|
|
| 29 |
|
| 30 |
let isWaiting = false;
|
| 31 |
let hasShownWaitMessage = false;
|
|
@@ -36,8 +36,7 @@ let shouldStop = false;
|
|
| 36 |
function extractErrorMessage(msg) {
|
| 37 |
if (!msg || typeof msg !== "object") return "Unknown error";
|
| 38 |
if (typeof msg.error === "string") return msg.error;
|
| 39 |
-
if (msg.error && typeof msg.error.message === "string")
|
| 40 |
-
return msg.error.message;
|
| 41 |
if (typeof msg.message === "string") return msg.message;
|
| 42 |
return "Unknown error";
|
| 43 |
}
|
|
@@ -46,13 +45,28 @@ function writeResetMarker() {
|
|
| 46 |
try {
|
| 47 |
fs.mkdirSync(path.dirname(RESET_MARKER_PATH), { recursive: true });
|
| 48 |
fs.writeFileSync(RESET_MARKER_PATH, "reset\n");
|
| 49 |
-
console.log(
|
| 50 |
-
`[guardian] Created backup reset marker at ${RESET_MARKER_PATH}`,
|
| 51 |
-
);
|
| 52 |
} catch (error) {
|
| 53 |
-
console.log(
|
| 54 |
-
|
| 55 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
}
|
| 57 |
}
|
| 58 |
|
|
@@ -65,32 +79,25 @@ async function createConnection() {
|
|
| 65 |
const msg = JSON.parse(data.toString());
|
| 66 |
|
| 67 |
if (msg.type === "event" && msg.event === "connect.challenge") {
|
| 68 |
-
ws.send(
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
mode: "backend",
|
| 81 |
-
},
|
| 82 |
-
caps: [],
|
| 83 |
-
auth: { token: GATEWAY_TOKEN },
|
| 84 |
-
role: "operator",
|
| 85 |
-
scopes: [
|
| 86 |
-
"operator.read",
|
| 87 |
-
"operator.write",
|
| 88 |
-
"operator.admin",
|
| 89 |
-
"operator.pairing",
|
| 90 |
-
],
|
| 91 |
},
|
| 92 |
-
|
| 93 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
return;
|
| 95 |
}
|
| 96 |
|
|
@@ -107,15 +114,8 @@ async function createConnection() {
|
|
| 107 |
}
|
| 108 |
});
|
| 109 |
|
| 110 |
-
ws.on("error", (e) => {
|
| 111 |
-
|
| 112 |
-
});
|
| 113 |
-
setTimeout(() => {
|
| 114 |
-
if (!resolved) {
|
| 115 |
-
ws.close();
|
| 116 |
-
reject(new Error("Timeout"));
|
| 117 |
-
}
|
| 118 |
-
}, 10000);
|
| 119 |
});
|
| 120 |
}
|
| 121 |
|
|
@@ -135,18 +135,14 @@ async function callRpc(ws, method, params) {
|
|
| 135 |
};
|
| 136 |
ws.on("message", handler);
|
| 137 |
ws.send(JSON.stringify({ type: "req", id, method, params }));
|
| 138 |
-
setTimeout(() => {
|
| 139 |
-
ws.removeListener("message", handler);
|
| 140 |
-
reject(new Error("RPC Timeout"));
|
| 141 |
-
}, WAIT_TIMEOUT + 5000);
|
| 142 |
});
|
| 143 |
}
|
| 144 |
|
| 145 |
async function checkStatus() {
|
| 146 |
if (shouldStop) return;
|
| 147 |
if (isWaiting) return;
|
| 148 |
-
if (lastConnectedAt && Date.now() - lastConnectedAt < SUCCESS_COOLDOWN_MS)
|
| 149 |
-
return;
|
| 150 |
|
| 151 |
let ws;
|
| 152 |
try {
|
|
@@ -158,27 +154,28 @@ async function checkStatus() {
|
|
| 158 |
|
| 159 |
if (!wa) {
|
| 160 |
hasShownWaitMessage = false;
|
|
|
|
| 161 |
return;
|
| 162 |
}
|
| 163 |
|
| 164 |
if (wa.connected) {
|
| 165 |
hasShownWaitMessage = false;
|
| 166 |
lastConnectedAt = Date.now();
|
|
|
|
|
|
|
|
|
|
| 167 |
return;
|
| 168 |
}
|
| 169 |
|
| 170 |
isWaiting = true;
|
|
|
|
| 171 |
if (!hasShownWaitMessage) {
|
| 172 |
-
console.log(
|
| 173 |
-
"\n[guardian] π± WhatsApp pairing in progress. Please scan the QR code in the Control UI.",
|
| 174 |
-
);
|
| 175 |
hasShownWaitMessage = true;
|
| 176 |
}
|
| 177 |
|
| 178 |
console.log("[guardian] Waiting for pairing completion...");
|
| 179 |
-
const waitRes = await callRpc(ws, "web.login.wait", {
|
| 180 |
-
timeoutMs: WAIT_TIMEOUT,
|
| 181 |
-
});
|
| 182 |
const result = waitRes.payload || waitRes.result;
|
| 183 |
const message = result?.message || "";
|
| 184 |
const linkedAfter515 = !result?.connected && message.includes("515");
|
|
@@ -190,41 +187,33 @@ async function checkStatus() {
|
|
| 190 |
if (result && (result.connected || linkedAfter515)) {
|
| 191 |
hasShownWaitMessage = false;
|
| 192 |
lastConnectedAt = Date.now();
|
|
|
|
| 193 |
|
| 194 |
if (linkedAfter515) {
|
| 195 |
-
console.log(
|
| 196 |
-
"[guardian] 515 after scan: credentials saved, reloading config to start WhatsApp...",
|
| 197 |
-
);
|
| 198 |
} else {
|
| 199 |
console.log("[guardian] β
Pairing completed! Reloading config...");
|
| 200 |
}
|
| 201 |
|
| 202 |
const getRes = await callRpc(ws, "config.get", {});
|
| 203 |
if (getRes.payload?.raw && getRes.payload?.hash) {
|
| 204 |
-
await callRpc(ws, "config.apply", {
|
| 205 |
-
raw: getRes.payload.raw,
|
| 206 |
-
baseHash: getRes.payload.hash,
|
| 207 |
-
});
|
| 208 |
console.log("[guardian] Configuration re-applied.");
|
| 209 |
}
|
| 210 |
|
| 211 |
shouldStop = true;
|
| 212 |
setTimeout(() => process.exit(0), 1000);
|
| 213 |
-
} else if (
|
| 214 |
-
!message.includes("No active") &&
|
| 215 |
-
!message.includes("Still waiting")
|
| 216 |
-
) {
|
| 217 |
console.log(`[guardian] Wait result: ${message}`);
|
| 218 |
}
|
|
|
|
| 219 |
} catch (e) {
|
| 220 |
const message = e && e.message ? e.message : "";
|
| 221 |
if (
|
| 222 |
/401|unauthorized|logged out|440|conflict/i.test(message) &&
|
| 223 |
Date.now() - last515At >= POST_515_NO_LOGOUT_MS
|
| 224 |
) {
|
| 225 |
-
console.log(
|
| 226 |
-
"[guardian] Clearing invalid WhatsApp session so a fresh QR can be used...",
|
| 227 |
-
);
|
| 228 |
try {
|
| 229 |
if (ws) {
|
| 230 |
await callRpc(ws, "channels.logout", { channel: "whatsapp" });
|
|
@@ -233,11 +222,12 @@ async function checkStatus() {
|
|
| 233 |
console.log("[guardian] Logged out invalid WhatsApp session.");
|
| 234 |
}
|
| 235 |
} catch (error) {
|
| 236 |
-
console.log(
|
| 237 |
-
`[guardian] Failed to log out invalid session: ${error.message}`,
|
| 238 |
-
);
|
| 239 |
}
|
| 240 |
}
|
|
|
|
|
|
|
|
|
|
| 241 |
// Normal timeout or gateway starting up; retry on the next interval.
|
| 242 |
} finally {
|
| 243 |
isWaiting = false;
|
|
@@ -245,8 +235,12 @@ async function checkStatus() {
|
|
| 245 |
}
|
| 246 |
}
|
| 247 |
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 251 |
setInterval(checkStatus, CHECK_INTERVAL);
|
| 252 |
setTimeout(checkStatus, 15000);
|
|
|
|
| 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 |
|
| 15 |
const GATEWAY_URL = "ws://127.0.0.1:7860";
|
| 16 |
const GATEWAY_TOKEN = process.env.GATEWAY_TOKEN || "huggingclaw";
|
| 17 |
+
const WHATSAPP_ENABLED = /^true$/i.test(process.env.WHATSAPP_ENABLED || "");
|
| 18 |
const CHECK_INTERVAL = 5000;
|
| 19 |
const WAIT_TIMEOUT = 120000;
|
| 20 |
const POST_515_NO_LOGOUT_MS = 90 * 1000;
|
|
|
|
| 25 |
"workspace",
|
| 26 |
".reset_credentials",
|
| 27 |
);
|
| 28 |
+
const STATUS_FILE_PATH = "/tmp/huggingclaw-wa-status.json";
|
| 29 |
|
| 30 |
let isWaiting = false;
|
| 31 |
let hasShownWaitMessage = false;
|
|
|
|
| 36 |
function extractErrorMessage(msg) {
|
| 37 |
if (!msg || typeof msg !== "object") return "Unknown error";
|
| 38 |
if (typeof msg.error === "string") return msg.error;
|
| 39 |
+
if (msg.error && typeof msg.error.message === "string") return msg.error.message;
|
|
|
|
| 40 |
if (typeof msg.message === "string") return msg.message;
|
| 41 |
return "Unknown error";
|
| 42 |
}
|
|
|
|
| 45 |
try {
|
| 46 |
fs.mkdirSync(path.dirname(RESET_MARKER_PATH), { recursive: true });
|
| 47 |
fs.writeFileSync(RESET_MARKER_PATH, "reset\n");
|
| 48 |
+
console.log(`[guardian] Created backup reset marker at ${RESET_MARKER_PATH}`);
|
|
|
|
|
|
|
| 49 |
} catch (error) {
|
| 50 |
+
console.log(`[guardian] Failed to write backup reset marker: ${error.message}`);
|
| 51 |
+
}
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
function writeStatus(partial) {
|
| 55 |
+
try {
|
| 56 |
+
const current = fs.existsSync(STATUS_FILE_PATH)
|
| 57 |
+
? JSON.parse(fs.readFileSync(STATUS_FILE_PATH, "utf8"))
|
| 58 |
+
: {};
|
| 59 |
+
const next = {
|
| 60 |
+
configured: true,
|
| 61 |
+
connected: false,
|
| 62 |
+
pairing: false,
|
| 63 |
+
updatedAt: new Date().toISOString(),
|
| 64 |
+
...current,
|
| 65 |
+
...partial,
|
| 66 |
+
};
|
| 67 |
+
fs.writeFileSync(STATUS_FILE_PATH, JSON.stringify(next));
|
| 68 |
+
} catch (error) {
|
| 69 |
+
console.log(`[guardian] Failed to write status file: ${error.message}`);
|
| 70 |
}
|
| 71 |
}
|
| 72 |
|
|
|
|
| 79 |
const msg = JSON.parse(data.toString());
|
| 80 |
|
| 81 |
if (msg.type === "event" && msg.event === "connect.challenge") {
|
| 82 |
+
ws.send(JSON.stringify({
|
| 83 |
+
type: "req",
|
| 84 |
+
id: randomUUID(),
|
| 85 |
+
method: "connect",
|
| 86 |
+
params: {
|
| 87 |
+
minProtocol: 3,
|
| 88 |
+
maxProtocol: 3,
|
| 89 |
+
client: {
|
| 90 |
+
id: "gateway-client",
|
| 91 |
+
version: "1.0.0",
|
| 92 |
+
platform: "linux",
|
| 93 |
+
mode: "backend",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
},
|
| 95 |
+
caps: [],
|
| 96 |
+
auth: { token: GATEWAY_TOKEN },
|
| 97 |
+
role: "operator",
|
| 98 |
+
scopes: ["operator.read", "operator.write", "operator.admin", "operator.pairing"],
|
| 99 |
+
},
|
| 100 |
+
}));
|
| 101 |
return;
|
| 102 |
}
|
| 103 |
|
|
|
|
| 114 |
}
|
| 115 |
});
|
| 116 |
|
| 117 |
+
ws.on("error", (e) => { if (!resolved) reject(e); });
|
| 118 |
+
setTimeout(() => { if (!resolved) { ws.close(); reject(new Error("Timeout")); } }, 10000);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
});
|
| 120 |
}
|
| 121 |
|
|
|
|
| 135 |
};
|
| 136 |
ws.on("message", handler);
|
| 137 |
ws.send(JSON.stringify({ type: "req", id, method, params }));
|
| 138 |
+
setTimeout(() => { ws.removeListener("message", handler); reject(new Error("RPC Timeout")); }, WAIT_TIMEOUT + 5000);
|
|
|
|
|
|
|
|
|
|
| 139 |
});
|
| 140 |
}
|
| 141 |
|
| 142 |
async function checkStatus() {
|
| 143 |
if (shouldStop) return;
|
| 144 |
if (isWaiting) return;
|
| 145 |
+
if (lastConnectedAt && Date.now() - lastConnectedAt < SUCCESS_COOLDOWN_MS) return;
|
|
|
|
| 146 |
|
| 147 |
let ws;
|
| 148 |
try {
|
|
|
|
| 154 |
|
| 155 |
if (!wa) {
|
| 156 |
hasShownWaitMessage = false;
|
| 157 |
+
writeStatus({ configured: true, connected: false, pairing: false });
|
| 158 |
return;
|
| 159 |
}
|
| 160 |
|
| 161 |
if (wa.connected) {
|
| 162 |
hasShownWaitMessage = false;
|
| 163 |
lastConnectedAt = Date.now();
|
| 164 |
+
writeStatus({ configured: true, connected: true, pairing: false });
|
| 165 |
+
shouldStop = true;
|
| 166 |
+
setTimeout(() => process.exit(0), 1000);
|
| 167 |
return;
|
| 168 |
}
|
| 169 |
|
| 170 |
isWaiting = true;
|
| 171 |
+
writeStatus({ configured: true, connected: false, pairing: true });
|
| 172 |
if (!hasShownWaitMessage) {
|
| 173 |
+
console.log("\n[guardian] π± WhatsApp pairing in progress. Please scan the QR code in the Control UI.");
|
|
|
|
|
|
|
| 174 |
hasShownWaitMessage = true;
|
| 175 |
}
|
| 176 |
|
| 177 |
console.log("[guardian] Waiting for pairing completion...");
|
| 178 |
+
const waitRes = await callRpc(ws, "web.login.wait", { timeoutMs: WAIT_TIMEOUT });
|
|
|
|
|
|
|
| 179 |
const result = waitRes.payload || waitRes.result;
|
| 180 |
const message = result?.message || "";
|
| 181 |
const linkedAfter515 = !result?.connected && message.includes("515");
|
|
|
|
| 187 |
if (result && (result.connected || linkedAfter515)) {
|
| 188 |
hasShownWaitMessage = false;
|
| 189 |
lastConnectedAt = Date.now();
|
| 190 |
+
writeStatus({ configured: true, connected: true, pairing: false });
|
| 191 |
|
| 192 |
if (linkedAfter515) {
|
| 193 |
+
console.log("[guardian] 515 after scan: credentials saved, reloading config to start WhatsApp...");
|
|
|
|
|
|
|
| 194 |
} else {
|
| 195 |
console.log("[guardian] β
Pairing completed! Reloading config...");
|
| 196 |
}
|
| 197 |
|
| 198 |
const getRes = await callRpc(ws, "config.get", {});
|
| 199 |
if (getRes.payload?.raw && getRes.payload?.hash) {
|
| 200 |
+
await callRpc(ws, "config.apply", { raw: getRes.payload.raw, baseHash: getRes.payload.hash });
|
|
|
|
|
|
|
|
|
|
| 201 |
console.log("[guardian] Configuration re-applied.");
|
| 202 |
}
|
| 203 |
|
| 204 |
shouldStop = true;
|
| 205 |
setTimeout(() => process.exit(0), 1000);
|
| 206 |
+
} else if (!message.includes("No active") && !message.includes("Still waiting")) {
|
|
|
|
|
|
|
|
|
|
| 207 |
console.log(`[guardian] Wait result: ${message}`);
|
| 208 |
}
|
| 209 |
+
|
| 210 |
} catch (e) {
|
| 211 |
const message = e && e.message ? e.message : "";
|
| 212 |
if (
|
| 213 |
/401|unauthorized|logged out|440|conflict/i.test(message) &&
|
| 214 |
Date.now() - last515At >= POST_515_NO_LOGOUT_MS
|
| 215 |
) {
|
| 216 |
+
console.log("[guardian] Clearing invalid WhatsApp session so a fresh QR can be used...");
|
|
|
|
|
|
|
| 217 |
try {
|
| 218 |
if (ws) {
|
| 219 |
await callRpc(ws, "channels.logout", { channel: "whatsapp" });
|
|
|
|
| 222 |
console.log("[guardian] Logged out invalid WhatsApp session.");
|
| 223 |
}
|
| 224 |
} catch (error) {
|
| 225 |
+
console.log(`[guardian] Failed to log out invalid session: ${error.message}`);
|
|
|
|
|
|
|
| 226 |
}
|
| 227 |
}
|
| 228 |
+
if (!/RPC Timeout/i.test(message)) {
|
| 229 |
+
writeStatus({ configured: true, connected: false, pairing: false });
|
| 230 |
+
}
|
| 231 |
// Normal timeout or gateway starting up; retry on the next interval.
|
| 232 |
} finally {
|
| 233 |
isWaiting = false;
|
|
|
|
| 235 |
}
|
| 236 |
}
|
| 237 |
|
| 238 |
+
if (!WHATSAPP_ENABLED) {
|
| 239 |
+
writeStatus({ configured: false, connected: false, pairing: false });
|
| 240 |
+
process.exit(0);
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
writeStatus({ configured: true, connected: false, pairing: false });
|
| 244 |
+
console.log("[guardian] βοΈ WhatsApp Guardian active. Monitoring pairing status...");
|
| 245 |
setInterval(checkStatus, CHECK_INTERVAL);
|
| 246 |
setTimeout(checkStatus, 15000);
|
workspace-sync.py
CHANGED
|
@@ -26,6 +26,7 @@ HF_TOKEN = os.environ.get("HF_TOKEN", "")
|
|
| 26 |
HF_USERNAME = os.environ.get("HF_USERNAME", "")
|
| 27 |
BACKUP_DATASET = os.environ.get("BACKUP_DATASET_NAME", "huggingclaw-backup")
|
| 28 |
WEBHOOK_URL = os.environ.get("WEBHOOK_URL", "")
|
|
|
|
| 29 |
|
| 30 |
running = True
|
| 31 |
|
|
@@ -52,6 +53,9 @@ def snapshot_state_into_workspace() -> None:
|
|
| 52 |
with the workspace, without changing the live credentials location.
|
| 53 |
"""
|
| 54 |
try:
|
|
|
|
|
|
|
|
|
|
| 55 |
STATE_DIR.mkdir(parents=True, exist_ok=True)
|
| 56 |
|
| 57 |
if RESET_MARKER.exists():
|
|
|
|
| 26 |
HF_USERNAME = os.environ.get("HF_USERNAME", "")
|
| 27 |
BACKUP_DATASET = os.environ.get("BACKUP_DATASET_NAME", "huggingclaw-backup")
|
| 28 |
WEBHOOK_URL = os.environ.get("WEBHOOK_URL", "")
|
| 29 |
+
WHATSAPP_ENABLED = os.environ.get("WHATSAPP_ENABLED", "").strip().lower() == "true"
|
| 30 |
|
| 31 |
running = True
|
| 32 |
|
|
|
|
| 53 |
with the workspace, without changing the live credentials location.
|
| 54 |
"""
|
| 55 |
try:
|
| 56 |
+
if not WHATSAPP_ENABLED:
|
| 57 |
+
return
|
| 58 |
+
|
| 59 |
STATE_DIR.mkdir(parents=True, exist_ok=True)
|
| 60 |
|
| 61 |
if RESET_MARKER.exists():
|