ckriti/HuggingClaw
0
1#!/usr/bin/env node2 3/**4 * QR Detection Manager for OpenClaw AI5 * MANDATORY QR Wait/Notify Implementation6 * 7 * When WhatsApp login requires QR code scan:8 * - STOP all debug operations9 * - Wait for QR code scan10 * - Clear user prompts11 * - Only continue after successful scan12 */13 14const fs = require('fs');15const path = require('path');16const { WebSocket } = require('ws');17const readline = require('readline');18 19class QRDetectionManager {20 constructor() {21 this.ws = null;22 this.isPaused = false;23 this.qrDetected = false;24 this.qrSourcePath = null;25 this.scanCompleted = false;26 this.timeout = null;27 this.qrTimeout = 300000; // 5 minutes timeout28 29 // Setup structured logging30 this.log = (level, message, data = {}) => {31 const logEntry = {32 timestamp: new Date().toISOString(),33 level,34 module: 'qr-detection-manager',35 message,36 ...data37 };38 console.log(JSON.stringify(logEntry));39 };40 41 this.log('info', 'QR Detection Manager initialized');42 }43 44 async connectWebSocket(spaceUrl) {45 try {46 // Handle spaceUrl being just a hostname or full URL47 let host = spaceUrl.replace(/^https?:\/\//, '').replace(/\/$/, '');48 const wsUrl = `wss://${host}`;49 const fullWsUrl = `${wsUrl}/queue/join`;50 51 this.log('info', 'Connecting to WebSocket', { url: fullWsUrl });52 53 this.ws = new WebSocket(fullWsUrl);54 55 this.ws.on('open', () => {56 this.log('info', 'WebSocket connection established');57 this.startMonitoring();58 });59 60 this.ws.on('message', (data) => {61 this.handleWebSocketMessage(data);62 });63 64 this.ws.on('error', (error) => {65 this.log('error', 'WebSocket error', { error: error.message });66 });67 68 this.ws.on('close', () => {69 this.log('info', 'WebSocket connection closed');70 });71 72 } catch (error) {73 this.log('error', 'Failed to connect to WebSocket', { error: error.message });74 }75 }76 77 handleWebSocketMessage(data) {78 // Placeholder for future WS message handling if needed79 // Currently we rely mostly on log/file monitoring80 }81 82 startMonitoring() {83 this.log('info', 'Starting QR code monitoring');84 85 // Send initial ping to keep connection alive86 const pingInterval = setInterval(() => {87 if (this.ws && this.ws.readyState === WebSocket.OPEN) {88 this.ws.ping();89 } else {90 clearInterval(pingInterval);91 }92 }, 30000);93 94 // Watch for QR code detection95 this.setupQRDetection();96 }97 98 setupQRDetection() {99 this.log('info', 'Setting up QR code detection');100 101 // Start timeout for QR scan102 this.timeout = setTimeout(() => {103 if (!this.scanCompleted) {104 this.log('warning', 'QR scan timeout reached');105 this.outputQRPrompt('❌ QR scan timeout. Please restart the process.', 'timeout');106 process.exit(1);107 }108 }, this.qrTimeout);109 110 // Monitor for QR code in logs or filesystem111 this.monitorForQR();112 }113 114 monitorForQR() {115 const homeDir = process.env.HOME || '/home/node';116 // Check for QR code file in actual HF Spaces paths117 const qrCheckInterval = setInterval(() => {118 if (this.scanCompleted) {119 clearInterval(qrCheckInterval);120 return;121 }122 123 // Check actual QR code file locations for HF Spaces OpenClaw124 const qrPaths = [125 path.join(homeDir, '.openclaw/credentials/whatsapp/qr.png'),126 path.join(homeDir, '.openclaw/workspace/qr.png'),127 path.join(homeDir, 'logs/qr.png'),128 ];129 130 for (const qrPath of qrPaths) {131 if (fs.existsSync(qrPath)) {132 this.qrSourcePath = qrPath;133 this.handleQRDetected(qrPath);134 break;135 }136 }137 138 // Also check for QR code in recent logs139 this.checkLogsForQR();140 }, 2000); // Check every 2 seconds141 }142 143 checkLogsForQR() {144 try {145 const homeDir = process.env.HOME || '/home/node';146 const logPaths = [147 path.join(homeDir, 'logs/app.log'),148 path.join(homeDir, '.openclaw/workspace/startup.log'),149 path.join(homeDir, '.openclaw/workspace/sync.log'),150 ];151 152 for (const logPath of logPaths) {153 if (fs.existsSync(logPath)) {154 const logContent = fs.readFileSync(logPath, 'utf8');155 if (this.isQRInLogContent(logContent)) {156 this.handleQRDetected('log');157 break;158 }159 }160 }161 } catch (error) {162 // Ignore log reading errors163 }164 }165 166 isQRInLogContent(content) {167 // Look for QR-related log entries168 const qrPatterns = [169 /qr code/i,170 /scan.*qr/i,171 /please scan/i,172 /waiting.*qr/i,173 /login.*qr/i,174 /whatsapp.*qr/i,175 /authentication.*qr/i176 ];177 178 return qrPatterns.some(pattern => pattern.test(content));179 }180 181 handleQRDetected(source) {182 if (this.qrDetected) {183 return; // Already detected184 }185 186 this.qrDetected = true;187 this.log('info', 'QR code detected', { source });188 189 // MANDATORY: Stop all debug operations190 this.isPaused = true;191 192 // MANDATORY: Clear user prompts193 this.outputQRPrompt('⏳ Waiting for WhatsApp QR code scan...', 'waiting');194 this.outputQRPrompt('📱 Please scan the QR code with your phone to continue.', 'qr');195 196 // Start monitoring for scan completion197 this.monitorScanCompletion();198 }199 200 outputQRPrompt(message, type) {201 // Clear console for better visibility202 process.stdout.write('\x1b[2J\x1b[0f');203 204 // Output formatted QR prompt205 const separator = '='.repeat(60);206 console.log(`\n${separator}`);207 console.log(`🔐 WHATSAPP LOGIN REQUIRED`);208 console.log(`${separator}\n`);209 console.log(message);210 console.log(`\n${separator}`);211 212 // Add visual indicators based on type213 if (type === 'waiting') {214 console.log('⏳ Operation paused - waiting for QR scan...');215 } else if (type === 'qr') {216 console.log('📱 Use your WhatsApp app to scan the QR code');217 } else if (type === 'success') {218 console.log('✅ QR scan completed successfully!');219 } else if (type === 'timeout') {220 console.log('❌ QR scan timeout - please try again');221 }222 223 console.log(`${separator}\n`);224 225 // Also log as JSON for structured processing226 this.log(type === 'success' ? 'info' : 'warning', 'QR prompt output', { 227 message, 228 type,229 isPaused: this.isPaused 230 });231 }232 233 monitorScanCompletion() {234 this.log('info', 'Monitoring for QR scan completion');235 236 // Monitor for scan completion signals237 const completionCheck = setInterval(() => {238 if (this.checkScanCompletion()) {239 clearInterval(completionCheck);240 this.handleScanCompleted();241 }242 }, 1000);243 }244 245 checkScanCompletion() {246 const homeDir = process.env.HOME || '/home/node';247 248 // 1. Check if QR file was removed (only if we know which file was detected)249 if (this.qrSourcePath && !fs.existsSync(this.qrSourcePath)) {250 return true;251 }252 253 // 2. Check for successful login in logs254 try {255 const logPaths = [256 path.join(homeDir, 'logs/app.log'),257 path.join(homeDir, '.openclaw/workspace/startup.log'),258 path.join(homeDir, '.openclaw/workspace/sync.log'),259 ];260 261 for (const logPath of logPaths) {262 if (fs.existsSync(logPath)) {263 const logContent = fs.readFileSync(logPath, 'utf8');264 if (this.isLoginInLogContent(logContent)) {265 return true;266 }267 }268 }269 } catch (error) {270 // Ignore log reading errors271 }272 273 // 3. Check for WhatsApp session/creds files in actual HF Spaces paths274 const sessionPaths = [275 path.join(homeDir, '.openclaw/credentials/whatsapp/creds.json'),276 path.join(homeDir, '.openclaw/credentials/whatsapp/session.json'),277 ];278 279 for (const sessionPath of sessionPaths) {280 if (fs.existsSync(sessionPath)) {281 return true;282 }283 }284 285 return false;286 }287 288 isLoginInLogContent(content) {289 // Look for successful login patterns290 const loginPatterns = [291 /login.*successful/i,292 /authentication.*success/i,293 /session.*established/i,294 /connected.*whatsapp/i,295 /qr.*scanned/i,296 /scan.*completed/i,297 /user.*authenticated/i298 ];299 300 return loginPatterns.some(pattern => pattern.test(content));301 }302 303 handleScanCompleted() {304 this.scanCompleted = true;305 this.isPaused = false;306 307 // Clear timeout308 if (this.timeout) {309 clearTimeout(this.timeout);310 }311 312 // MANDATORY: Clear success notification313 this.outputQRPrompt('✅ QR code scanned successfully. Login completed.', 'success');314 315 this.log('info', 'QR scan completed, resuming operations');316 317 // Wait a moment for user to see the success message318 setTimeout(() => {319 // Exit the process to allow main application to continue320 process.exit(0);321 }, 3000);322 }323 324 async waitForQRScan() {325 return new Promise((resolve, reject) => {326 const checkInterval = setInterval(() => {327 if (this.scanCompleted) {328 clearInterval(checkInterval);329 resolve();330 }331 }, 1000);332 333 // Timeout after 5 minutes334 setTimeout(() => {335 clearInterval(checkInterval);336 reject(new Error('QR scan timeout'));337 }, this.qrTimeout);338 });339 }340 341 close() {342 if (this.ws) {343 this.ws.close();344 }345 if (this.timeout) {346 clearTimeout(this.timeout);347 }348 this.log('info', 'QR Detection Manager closed');349 }350}351 352// Command line interface353async function main() {354 const args = process.argv.slice(2);355 const spaceUrl = args[0] || process.env.SPACE_HOST || '';356 357 const manager = new QRDetectionManager();358 359 try {360 await manager.connectWebSocket(spaceUrl);361 362 // Keep the process running363 process.on('SIGINT', () => {364 manager.log('info', 'Received SIGINT, shutting down gracefully');365 manager.close();366 process.exit(0);367 });368 369 process.on('SIGTERM', () => {370 manager.log('info', 'Received SIGTERM, shutting down gracefully');371 manager.close();372 process.exit(0);373 });374 375 } catch (error) {376 manager.log('error', 'QR Detection Manager failed', { error: error.message });377 process.exit(1);378 }379}380 381if (require.main === module) {382 main();383}384 385module.exports = QRDetectionManager;