Spaces:
Runtime error
Runtime error
| /** | |
| * QR Detection Manager for OpenClaw AI | |
| * MANDATORY QR Wait/Notify Implementation | |
| * | |
| * When WhatsApp login requires QR code scan: | |
| * - STOP all debug operations | |
| * - Wait for QR code scan | |
| * - Clear user prompts | |
| * - Only continue after successful scan | |
| */ | |
| const fs = require('fs'); | |
| const path = require('path'); | |
| const { WebSocket } = require('ws'); | |
| const readline = require('readline'); | |
| class QRDetectionManager { | |
| constructor() { | |
| this.ws = null; | |
| this.isPaused = false; | |
| this.qrDetected = false; | |
| this.qrSourcePath = null; | |
| this.scanCompleted = false; | |
| this.timeout = null; | |
| this.qrTimeout = 300000; // 5 minutes timeout | |
| // Setup structured logging | |
| this.log = (level, message, data = {}) => { | |
| const logEntry = { | |
| timestamp: new Date().toISOString(), | |
| level, | |
| module: 'qr-detection-manager', | |
| message, | |
| ...data | |
| }; | |
| console.log(JSON.stringify(logEntry)); | |
| }; | |
| this.log('info', 'QR Detection Manager initialized'); | |
| } | |
| async connectWebSocket(spaceUrl) { | |
| try { | |
| // Handle spaceUrl being just a hostname or full URL | |
| let host = spaceUrl.replace(/^https?:\/\//, '').replace(/\/$/, ''); | |
| const wsUrl = `wss://${host}`; | |
| const fullWsUrl = `${wsUrl}/queue/join`; | |
| this.log('info', 'Connecting to WebSocket', { url: fullWsUrl }); | |
| this.ws = new WebSocket(fullWsUrl); | |
| this.ws.on('open', () => { | |
| this.log('info', 'WebSocket connection established'); | |
| this.startMonitoring(); | |
| }); | |
| this.ws.on('message', (data) => { | |
| this.handleWebSocketMessage(data); | |
| }); | |
| this.ws.on('error', (error) => { | |
| this.log('error', 'WebSocket error', { error: error.message }); | |
| }); | |
| this.ws.on('close', () => { | |
| this.log('info', 'WebSocket connection closed'); | |
| }); | |
| } catch (error) { | |
| this.log('error', 'Failed to connect to WebSocket', { error: error.message }); | |
| } | |
| } | |
| handleWebSocketMessage(data) { | |
| // Placeholder for future WS message handling if needed | |
| // Currently we rely mostly on log/file monitoring | |
| } | |
| startMonitoring() { | |
| this.log('info', 'Starting QR code monitoring'); | |
| // Send initial ping to keep connection alive | |
| const pingInterval = setInterval(() => { | |
| if (this.ws && this.ws.readyState === WebSocket.OPEN) { | |
| this.ws.ping(); | |
| } else { | |
| clearInterval(pingInterval); | |
| } | |
| }, 30000); | |
| // Watch for QR code detection | |
| this.setupQRDetection(); | |
| } | |
| setupQRDetection() { | |
| this.log('info', 'Setting up QR code detection'); | |
| // Start timeout for QR scan | |
| this.timeout = setTimeout(() => { | |
| if (!this.scanCompleted) { | |
| this.log('warning', 'QR scan timeout reached'); | |
| this.outputQRPrompt('❌ QR scan timeout. Please restart the process.', 'timeout'); | |
| process.exit(1); | |
| } | |
| }, this.qrTimeout); | |
| // Monitor for QR code in logs or filesystem | |
| this.monitorForQR(); | |
| } | |
| monitorForQR() { | |
| const homeDir = process.env.HOME || '/home/node'; | |
| // Check for QR code file in actual HF Spaces paths | |
| const qrCheckInterval = setInterval(() => { | |
| if (this.scanCompleted) { | |
| clearInterval(qrCheckInterval); | |
| return; | |
| } | |
| // Check actual QR code file locations for HF Spaces OpenClaw | |
| const qrPaths = [ | |
| path.join(homeDir, '.openclaw/credentials/whatsapp/qr.png'), | |
| path.join(homeDir, '.openclaw/workspace/qr.png'), | |
| path.join(homeDir, 'logs/qr.png'), | |
| ]; | |
| for (const qrPath of qrPaths) { | |
| if (fs.existsSync(qrPath)) { | |
| this.qrSourcePath = qrPath; | |
| this.handleQRDetected(qrPath); | |
| break; | |
| } | |
| } | |
| // Also check for QR code in recent logs | |
| this.checkLogsForQR(); | |
| }, 2000); // Check every 2 seconds | |
| } | |
| checkLogsForQR() { | |
| try { | |
| const homeDir = process.env.HOME || '/home/node'; | |
| const logPaths = [ | |
| path.join(homeDir, 'logs/app.log'), | |
| path.join(homeDir, '.openclaw/workspace/startup.log'), | |
| path.join(homeDir, '.openclaw/workspace/sync.log'), | |
| ]; | |
| for (const logPath of logPaths) { | |
| if (fs.existsSync(logPath)) { | |
| const logContent = fs.readFileSync(logPath, 'utf8'); | |
| if (this.isQRInLogContent(logContent)) { | |
| this.handleQRDetected('log'); | |
| break; | |
| } | |
| } | |
| } | |
| } catch (error) { | |
| // Ignore log reading errors | |
| } | |
| } | |
| isQRInLogContent(content) { | |
| // Look for QR-related log entries | |
| const qrPatterns = [ | |
| /qr code/i, | |
| /scan.*qr/i, | |
| /please scan/i, | |
| /waiting.*qr/i, | |
| /login.*qr/i, | |
| /whatsapp.*qr/i, | |
| /authentication.*qr/i | |
| ]; | |
| return qrPatterns.some(pattern => pattern.test(content)); | |
| } | |
| handleQRDetected(source) { | |
| if (this.qrDetected) { | |
| return; // Already detected | |
| } | |
| this.qrDetected = true; | |
| this.log('info', 'QR code detected', { source }); | |
| // MANDATORY: Stop all debug operations | |
| this.isPaused = true; | |
| // MANDATORY: Clear user prompts | |
| this.outputQRPrompt('⏳ Waiting for WhatsApp QR code scan...', 'waiting'); | |
| this.outputQRPrompt('📱 Please scan the QR code with your phone to continue.', 'qr'); | |
| // Start monitoring for scan completion | |
| this.monitorScanCompletion(); | |
| } | |
| outputQRPrompt(message, type) { | |
| // Clear console for better visibility | |
| process.stdout.write('\x1b[2J\x1b[0f'); | |
| // Output formatted QR prompt | |
| const separator = '='.repeat(60); | |
| console.log(`\n${separator}`); | |
| console.log(`🔐 WHATSAPP LOGIN REQUIRED`); | |
| console.log(`${separator}\n`); | |
| console.log(message); | |
| console.log(`\n${separator}`); | |
| // Add visual indicators based on type | |
| if (type === 'waiting') { | |
| console.log('⏳ Operation paused - waiting for QR scan...'); | |
| } else if (type === 'qr') { | |
| console.log('📱 Use your WhatsApp app to scan the QR code'); | |
| } else if (type === 'success') { | |
| console.log('✅ QR scan completed successfully!'); | |
| } else if (type === 'timeout') { | |
| console.log('❌ QR scan timeout - please try again'); | |
| } | |
| console.log(`${separator}\n`); | |
| // Also log as JSON for structured processing | |
| this.log(type === 'success' ? 'info' : 'warning', 'QR prompt output', { | |
| message, | |
| type, | |
| isPaused: this.isPaused | |
| }); | |
| } | |
| monitorScanCompletion() { | |
| this.log('info', 'Monitoring for QR scan completion'); | |
| // Monitor for scan completion signals | |
| const completionCheck = setInterval(() => { | |
| if (this.checkScanCompletion()) { | |
| clearInterval(completionCheck); | |
| this.handleScanCompleted(); | |
| } | |
| }, 1000); | |
| } | |
| checkScanCompletion() { | |
| const homeDir = process.env.HOME || '/home/node'; | |
| // 1. Check if QR file was removed (only if we know which file was detected) | |
| if (this.qrSourcePath && !fs.existsSync(this.qrSourcePath)) { | |
| return true; | |
| } | |
| // 2. Check for successful login in logs | |
| try { | |
| const logPaths = [ | |
| path.join(homeDir, 'logs/app.log'), | |
| path.join(homeDir, '.openclaw/workspace/startup.log'), | |
| path.join(homeDir, '.openclaw/workspace/sync.log'), | |
| ]; | |
| for (const logPath of logPaths) { | |
| if (fs.existsSync(logPath)) { | |
| const logContent = fs.readFileSync(logPath, 'utf8'); | |
| if (this.isLoginInLogContent(logContent)) { | |
| return true; | |
| } | |
| } | |
| } | |
| } catch (error) { | |
| // Ignore log reading errors | |
| } | |
| // 3. Check for WhatsApp session/creds files in actual HF Spaces paths | |
| const sessionPaths = [ | |
| path.join(homeDir, '.openclaw/credentials/whatsapp/creds.json'), | |
| path.join(homeDir, '.openclaw/credentials/whatsapp/session.json'), | |
| ]; | |
| for (const sessionPath of sessionPaths) { | |
| if (fs.existsSync(sessionPath)) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| isLoginInLogContent(content) { | |
| // Look for successful login patterns | |
| const loginPatterns = [ | |
| /login.*successful/i, | |
| /authentication.*success/i, | |
| /session.*established/i, | |
| /connected.*whatsapp/i, | |
| /qr.*scanned/i, | |
| /scan.*completed/i, | |
| /user.*authenticated/i | |
| ]; | |
| return loginPatterns.some(pattern => pattern.test(content)); | |
| } | |
| handleScanCompleted() { | |
| this.scanCompleted = true; | |
| this.isPaused = false; | |
| // Clear timeout | |
| if (this.timeout) { | |
| clearTimeout(this.timeout); | |
| } | |
| // MANDATORY: Clear success notification | |
| this.outputQRPrompt('✅ QR code scanned successfully. Login completed.', 'success'); | |
| this.log('info', 'QR scan completed, resuming operations'); | |
| // Wait a moment for user to see the success message | |
| setTimeout(() => { | |
| // Exit the process to allow main application to continue | |
| process.exit(0); | |
| }, 3000); | |
| } | |
| async waitForQRScan() { | |
| return new Promise((resolve, reject) => { | |
| const checkInterval = setInterval(() => { | |
| if (this.scanCompleted) { | |
| clearInterval(checkInterval); | |
| resolve(); | |
| } | |
| }, 1000); | |
| // Timeout after 5 minutes | |
| setTimeout(() => { | |
| clearInterval(checkInterval); | |
| reject(new Error('QR scan timeout')); | |
| }, this.qrTimeout); | |
| }); | |
| } | |
| close() { | |
| if (this.ws) { | |
| this.ws.close(); | |
| } | |
| if (this.timeout) { | |
| clearTimeout(this.timeout); | |
| } | |
| this.log('info', 'QR Detection Manager closed'); | |
| } | |
| } | |
| // Command line interface | |
| async function main() { | |
| const args = process.argv.slice(2); | |
| const spaceUrl = args[0] || process.env.SPACE_HOST || ''; | |
| const manager = new QRDetectionManager(); | |
| try { | |
| await manager.connectWebSocket(spaceUrl); | |
| // Keep the process running | |
| process.on('SIGINT', () => { | |
| manager.log('info', 'Received SIGINT, shutting down gracefully'); | |
| manager.close(); | |
| process.exit(0); | |
| }); | |
| process.on('SIGTERM', () => { | |
| manager.log('info', 'Received SIGTERM, shutting down gracefully'); | |
| manager.close(); | |
| process.exit(0); | |
| }); | |
| } catch (error) { | |
| manager.log('error', 'QR Detection Manager failed', { error: error.message }); | |
| process.exit(1); | |
| } | |
| } | |
| if (require.main === module) { | |
| main(); | |
| } | |
| module.exports = QRDetectionManager; |