yusef commited on
Commit
c43d9c3
·
1 Parent(s): a93356a

Add Lukas Worker

Browse files
Files changed (4) hide show
  1. Dockerfile +55 -0
  2. README.md +39 -11
  3. index.js +341 -0
  4. package.json +20 -0
Dockerfile ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Lukas Worker - Docker Image for Hugging Face Spaces
2
+ # This runs the "Muscles" - Browser automation with live streaming
3
+
4
+ FROM node:20-slim
5
+
6
+ # Install system dependencies for Playwright/Chromium
7
+ RUN apt-get update && apt-get install -y \
8
+ wget \
9
+ gnupg \
10
+ ca-certificates \
11
+ fonts-liberation \
12
+ libasound2 \
13
+ libatk-bridge2.0-0 \
14
+ libatk1.0-0 \
15
+ libcups2 \
16
+ libdbus-1-3 \
17
+ libdrm2 \
18
+ libgbm1 \
19
+ libgtk-3-0 \
20
+ libnspr4 \
21
+ libnss3 \
22
+ libx11-xcb1 \
23
+ libxcomposite1 \
24
+ libxdamage1 \
25
+ libxfixes3 \
26
+ libxrandr2 \
27
+ xdg-utils \
28
+ xvfb \
29
+ --no-install-recommends \
30
+ && rm -rf /var/lib/apt/lists/*
31
+
32
+ # Create app directory
33
+ WORKDIR /app
34
+
35
+ # Copy package files
36
+ COPY package*.json ./
37
+
38
+ # Install dependencies
39
+ RUN npm install
40
+
41
+ # Install Playwright browsers
42
+ RUN npx playwright install chromium
43
+
44
+ # Copy app source
45
+ COPY . .
46
+
47
+ # Expose the port for Socket.io
48
+ EXPOSE 7860
49
+
50
+ # Set environment variables
51
+ ENV PORT=7860
52
+ ENV NODE_ENV=production
53
+
54
+ # Start the worker with Xvfb (virtual display)
55
+ CMD ["sh", "-c", "xvfb-run --server-args='-screen 0 1920x1080x24' node index.js"]
README.md CHANGED
@@ -1,11 +1,39 @@
1
- ---
2
- title: Lukas Worker
3
- emoji: 🏆
4
- colorFrom: indigo
5
- colorTo: blue
6
- sdk: docker
7
- pinned: false
8
- short_description: Lukas AI Browser Worker
9
- ---
10
-
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Lukas Worker - Browser Automation Engine
2
+
3
+ هذا المجلد يحتوي على "عضلات" لوكاس - السيرفر المسؤول عن تشغيل المتصفح والبث المباشر.
4
+
5
+ ## 🚀 الرفع على Hugging Face Spaces
6
+
7
+ 1. أنشئ Space جديد على [Hugging Face](https://huggingface.co/new-space)
8
+ 2. اختر **Docker** كـ SDK
9
+ 3. ارفع محتويات هذا المجلد
10
+ 4. أضف Environment Variable:
11
+ - `WORKER_SECRET` = (نفس القيمة في Vercel)
12
+
13
+ ## ⚙️ Environment Variables
14
+
15
+ | المتغير | الوصف |
16
+ |---------|-------|
17
+ | `WORKER_SECRET` | كلمة السر للاتصال الآمن مع "المخ" |
18
+ | `PORT` | البورت (افتراضي: 7860) |
19
+
20
+ ## 🔌 الأوامر المتاحة (Socket.io Events)
21
+
22
+ | الحدث | الوصف |
23
+ |-------|-------|
24
+ | `browser:goto` | الذهاب لرابط معين |
25
+ | `browser:click` | الضغط على عنصر |
26
+ | `browser:type` | الكتابة في حقل |
27
+ | `browser:scroll` | التمرير لأعلى/لأسفل |
28
+ | `browser:screenshot` | أخذ لقطة شاشة |
29
+ | `browser:getContent` | جلب محتوى الصفحة |
30
+ | `stream:frame` | (صادر) إطار البث المباشر |
31
+
32
+ ## 🧪 التشغيل المحلي
33
+
34
+ ```bash
35
+ cd worker
36
+ npm install
37
+ npx playwright install chromium
38
+ npm start
39
+ ```
index.js ADDED
@@ -0,0 +1,341 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Lukas Worker - The Muscles
3
+ * Browser automation server with Socket.io for real-time control and streaming
4
+ * Deploy this to Hugging Face Spaces as a Docker container
5
+ */
6
+
7
+ import express from 'express';
8
+ import { createServer } from 'http';
9
+ import { Server } from 'socket.io';
10
+ import { chromium } from 'playwright';
11
+ import dotenv from 'dotenv';
12
+
13
+ dotenv.config();
14
+
15
+ const PORT = process.env.PORT || 7860;
16
+ const WORKER_SECRET = process.env.WORKER_SECRET || 'lukas-dev-secret';
17
+
18
+ const app = express();
19
+ const httpServer = createServer(app);
20
+
21
+ // Socket.io server with CORS for Vercel
22
+ const io = new Server(httpServer, {
23
+ cors: {
24
+ origin: ['https://luks-pied.vercel.app', 'http://localhost:5173', 'http://localhost:3000'],
25
+ methods: ['GET', 'POST'],
26
+ credentials: true
27
+ },
28
+ transports: ['websocket', 'polling']
29
+ });
30
+
31
+ // Health check endpoint (Required for Hugging Face)
32
+ app.get('/', (req, res) => {
33
+ res.json({
34
+ status: 'ok',
35
+ service: 'Lukas Worker (The Muscles)',
36
+ version: '1.0.0',
37
+ ready: true
38
+ });
39
+ });
40
+
41
+ app.get('/health', (req, res) => {
42
+ res.json({ status: 'healthy', timestamp: new Date().toISOString() });
43
+ });
44
+
45
+ // =============================================================================
46
+ // BROWSER MANAGEMENT
47
+ // =============================================================================
48
+
49
+ let browser = null;
50
+ let browserContext = null;
51
+ let activePage = null;
52
+ let streamInterval = null;
53
+ let connectedClient = null;
54
+
55
+ async function initBrowser() {
56
+ if (browser) return;
57
+
58
+ console.log('🚀 Launching browser...');
59
+ browser = await chromium.launch({
60
+ headless: true,
61
+ args: [
62
+ '--no-sandbox',
63
+ '--disable-setuid-sandbox',
64
+ '--disable-dev-shm-usage',
65
+ '--disable-accelerated-2d-canvas',
66
+ '--no-first-run',
67
+ '--no-zygote',
68
+ '--disable-gpu'
69
+ ]
70
+ });
71
+
72
+ browserContext = await browser.newContext({
73
+ viewport: { width: 1280, height: 720 },
74
+ userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
75
+ });
76
+
77
+ activePage = await browserContext.newPage();
78
+ console.log('✅ Browser ready');
79
+ }
80
+
81
+ async function closeBrowser() {
82
+ if (browser) {
83
+ await browser.close();
84
+ browser = null;
85
+ browserContext = null;
86
+ activePage = null;
87
+ console.log('🔴 Browser closed');
88
+ }
89
+ }
90
+
91
+ // =============================================================================
92
+ // STREAMING
93
+ // =============================================================================
94
+
95
+ async function startStreaming(socket) {
96
+ if (streamInterval) clearInterval(streamInterval);
97
+ if (!activePage) return;
98
+
99
+ console.log('📺 Starting live stream...');
100
+
101
+ streamInterval = setInterval(async () => {
102
+ try {
103
+ if (!activePage) return;
104
+
105
+ const screenshot = await activePage.screenshot({
106
+ type: 'jpeg',
107
+ quality: 60,
108
+ fullPage: false
109
+ });
110
+
111
+ const base64 = screenshot.toString('base64');
112
+ socket.emit('stream:frame', { image: base64 });
113
+ } catch (error) {
114
+ // Page might be navigating, ignore errors
115
+ }
116
+ }, 200); // ~5 FPS for smooth streaming
117
+ }
118
+
119
+ function stopStreaming() {
120
+ if (streamInterval) {
121
+ clearInterval(streamInterval);
122
+ streamInterval = null;
123
+ console.log('📺 Stream stopped');
124
+ }
125
+ }
126
+
127
+ // =============================================================================
128
+ // SOCKET HANDLERS
129
+ // =============================================================================
130
+
131
+ io.use((socket, next) => {
132
+ const token = socket.handshake.auth?.token;
133
+
134
+ if (token === WORKER_SECRET) {
135
+ console.log('✅ Client authenticated');
136
+ next();
137
+ } else {
138
+ console.log('❌ Authentication failed');
139
+ next(new Error('Authentication failed'));
140
+ }
141
+ });
142
+
143
+ io.on('connection', async (socket) => {
144
+ console.log('🔗 Client connected:', socket.id);
145
+
146
+ // Only allow one client at a time
147
+ if (connectedClient && connectedClient !== socket.id) {
148
+ socket.emit('error', { message: 'Another client is already connected' });
149
+ socket.disconnect();
150
+ return;
151
+ }
152
+
153
+ connectedClient = socket.id;
154
+
155
+ // Initialize browser on first connection
156
+ await initBrowser();
157
+
158
+ // Start streaming automatically
159
+ startStreaming(socket);
160
+
161
+ // =========================================================================
162
+ // COMMAND HANDLERS
163
+ // =========================================================================
164
+
165
+ socket.on('browser:goto', async (data, callback) => {
166
+ try {
167
+ const { url } = data;
168
+ console.log(`🌐 Navigating to: ${url}`);
169
+
170
+ await activePage.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
171
+
172
+ const title = await activePage.title();
173
+ callback({ success: true, title });
174
+ } catch (error) {
175
+ console.error('❌ Navigation error:', error.message);
176
+ callback({ success: false, error: error.message });
177
+ }
178
+ });
179
+
180
+ socket.on('browser:click', async (data, callback) => {
181
+ try {
182
+ const { selector } = data;
183
+ console.log(`🖱️ Clicking: ${selector}`);
184
+
185
+ await activePage.click(selector, { timeout: 10000 });
186
+ callback({ success: true });
187
+ } catch (error) {
188
+ console.error('❌ Click error:', error.message);
189
+ callback({ success: false, error: error.message });
190
+ }
191
+ });
192
+
193
+ socket.on('browser:type', async (data, callback) => {
194
+ try {
195
+ const { selector, text, delay = 50 } = data;
196
+ console.log(`⌨️ Typing in: ${selector}`);
197
+
198
+ await activePage.fill(selector, text);
199
+ callback({ success: true });
200
+ } catch (error) {
201
+ console.error('❌ Type error:', error.message);
202
+ callback({ success: false, error: error.message });
203
+ }
204
+ });
205
+
206
+ socket.on('browser:scroll', async (data, callback) => {
207
+ try {
208
+ const { direction = 'down', amount = 500 } = data;
209
+ console.log(`📜 Scrolling ${direction}`);
210
+
211
+ await activePage.evaluate((dir, amt) => {
212
+ window.scrollBy(0, dir === 'down' ? amt : -amt);
213
+ }, direction, amount);
214
+
215
+ callback({ success: true });
216
+ } catch (error) {
217
+ callback({ success: false, error: error.message });
218
+ }
219
+ });
220
+
221
+ socket.on('browser:screenshot', async (data, callback) => {
222
+ try {
223
+ console.log('📸 Taking screenshot...');
224
+
225
+ const screenshot = await activePage.screenshot({
226
+ type: 'png',
227
+ fullPage: data?.fullPage || false
228
+ });
229
+
230
+ callback({ success: true, image: screenshot.toString('base64') });
231
+ } catch (error) {
232
+ callback({ success: false, error: error.message });
233
+ }
234
+ });
235
+
236
+ socket.on('browser:getContent', async (data, callback) => {
237
+ try {
238
+ console.log('📄 Getting page content...');
239
+
240
+ const content = await activePage.content();
241
+ const title = await activePage.title();
242
+ const url = activePage.url();
243
+
244
+ // Get text content for AI analysis
245
+ const textContent = await activePage.evaluate(() => {
246
+ return document.body.innerText.substring(0, 10000);
247
+ });
248
+
249
+ callback({ success: true, content, title, url, textContent });
250
+ } catch (error) {
251
+ callback({ success: false, error: error.message });
252
+ }
253
+ });
254
+
255
+ socket.on('browser:getAccessibility', async (data, callback) => {
256
+ try {
257
+ console.log('🌳 Getting accessibility tree...');
258
+
259
+ const tree = await activePage.accessibility.snapshot();
260
+ callback({ success: true, tree });
261
+ } catch (error) {
262
+ callback({ success: false, error: error.message });
263
+ }
264
+ });
265
+
266
+ socket.on('browser:execute', async (data, callback) => {
267
+ try {
268
+ const { action, params } = data;
269
+ console.log(`⚡ Executing action: ${action}`);
270
+
271
+ let result = null;
272
+
273
+ switch (action) {
274
+ case 'waitForSelector':
275
+ await activePage.waitForSelector(params.selector, { timeout: params.timeout || 10000 });
276
+ result = { found: true };
277
+ break;
278
+
279
+ case 'pressKey':
280
+ await activePage.keyboard.press(params.key);
281
+ result = { pressed: params.key };
282
+ break;
283
+
284
+ case 'goBack':
285
+ await activePage.goBack();
286
+ result = { navigated: true };
287
+ break;
288
+
289
+ case 'goForward':
290
+ await activePage.goForward();
291
+ result = { navigated: true };
292
+ break;
293
+
294
+ case 'reload':
295
+ await activePage.reload();
296
+ result = { reloaded: true };
297
+ break;
298
+
299
+ default:
300
+ throw new Error(`Unknown action: ${action}`);
301
+ }
302
+
303
+ callback({ success: true, result });
304
+ } catch (error) {
305
+ callback({ success: false, error: error.message });
306
+ }
307
+ });
308
+
309
+ // =========================================================================
310
+ // DISCONNECT HANDLER
311
+ // =========================================================================
312
+
313
+ socket.on('disconnect', () => {
314
+ console.log('🔌 Client disconnected:', socket.id);
315
+ stopStreaming();
316
+ connectedClient = null;
317
+
318
+ // Don't close browser immediately, keep it warm for reconnection
319
+ // closeBrowser();
320
+ });
321
+ });
322
+
323
+ // =============================================================================
324
+ // START SERVER
325
+ // =============================================================================
326
+
327
+ httpServer.listen(PORT, '0.0.0.0', () => {
328
+ console.log('═══════════════════════════════════════════════════════════════');
329
+ console.log(` 🦾 Lukas Worker (The Muscles) is running`);
330
+ console.log(` 📡 Socket.io server: http://0.0.0.0:${PORT}`);
331
+ console.log(` 🔐 Secret required for connection`);
332
+ console.log('═══════════════════════════════════════════════════════════════');
333
+ });
334
+
335
+ // Graceful shutdown
336
+ process.on('SIGTERM', async () => {
337
+ console.log('🛑 Shutting down...');
338
+ stopStreaming();
339
+ await closeBrowser();
340
+ process.exit(0);
341
+ });
package.json ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "lukas-worker",
3
+ "version": "1.0.0",
4
+ "description": "Lukas AI Browser Worker - The Muscles (Hugging Face Deployment)",
5
+ "main": "index.js",
6
+ "type": "module",
7
+ "scripts": {
8
+ "start": "node index.js",
9
+ "dev": "node index.js"
10
+ },
11
+ "dependencies": {
12
+ "playwright": "^1.40.0",
13
+ "socket.io": "^4.7.2",
14
+ "express": "^4.18.2",
15
+ "dotenv": "^16.3.1"
16
+ },
17
+ "engines": {
18
+ "node": ">=18.0.0"
19
+ }
20
+ }