channel-worker 1.0.20 → 1.0.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -55,6 +55,9 @@ class CommandPoller {
55
55
  case 'set_tags':
56
56
  await this.handleSetTags(command);
57
57
  break;
58
+ case 'set_file_input':
59
+ await this.handleSetFileInput(command);
60
+ break;
58
61
  default:
59
62
  // Other commands (scan_facebook_pages, etc.) handled by extension
60
63
  console.log(`[commands] Skipping ${command.type} — handled by extension`);
@@ -443,6 +446,106 @@ class CommandPoller {
443
446
  }
444
447
  }
445
448
 
449
+ async handleSetFileInput(command) {
450
+ const { profile_id, file_path, selector, fallback_selector, url_match } = command.payload || {};
451
+ console.log(`[commands] Setting file input: ${selector} → ${file_path}`);
452
+ try {
453
+ const WebSocket = require('ws');
454
+ if (!this.nst) {
455
+ const apiKey = await this.api.getSetting('nst_api_key');
456
+ if (apiKey) this.nst = new NstManager(apiKey);
457
+ }
458
+ const browsersRes = await fetch('http://localhost:8848/api/v2/browsers', {
459
+ headers: { 'x-api-key': this.nst?.apiKey || '' },
460
+ });
461
+ const browser = ((await browsersRes.json())?.data || []).find(b => b.name === profile_id) || (await browsersRes.json())?.data?.[0];
462
+ if (!browser) throw new Error('No running browser');
463
+
464
+ const pagesRes = await fetch(`http://localhost:${browser.remoteDebuggingPort}/json/list`);
465
+ const pages = await pagesRes.json();
466
+ const targetPage = pages.find(p => p.type === 'page' && (!url_match || p.url?.includes(url_match)));
467
+ if (!targetPage) throw new Error(`No tab matching ${url_match}`);
468
+
469
+ const result = await new Promise((resolve, reject) => {
470
+ const ws = new WebSocket(targetPage.webSocketDebuggerUrl);
471
+ let msgId = 1;
472
+ function send(method, params = {}) {
473
+ const id = msgId++;
474
+ return new Promise((res, rej) => {
475
+ const handler = (data) => {
476
+ const msg = JSON.parse(data);
477
+ if (msg.id === id) { ws.removeListener('message', handler); msg.error ? rej(new Error(msg.error.message)) : res(msg.result); }
478
+ };
479
+ ws.on('message', handler);
480
+ ws.send(JSON.stringify({ id, method, params }));
481
+ });
482
+ }
483
+
484
+ ws.on('open', async () => {
485
+ try {
486
+ await send('Page.enable');
487
+ await send('DOM.enable');
488
+
489
+ // Enable file chooser interception
490
+ await send('Page.setInterceptFileChooserDialog', { enabled: true });
491
+
492
+ // Listen for file chooser event
493
+ const chooserPromise = new Promise((res) => {
494
+ const eventHandler = (data) => {
495
+ const msg = JSON.parse(data);
496
+ if (msg.method === 'Page.fileChooserOpened') {
497
+ ws.removeListener('message', eventHandler);
498
+ res('opened');
499
+ }
500
+ };
501
+ ws.on('message', eventHandler);
502
+ setTimeout(() => { ws.removeListener('message', eventHandler); res('timeout'); }, 10000);
503
+ });
504
+
505
+ // Click the file input to trigger native file dialog
506
+ const sel = selector.replace(/'/g, "\\'");
507
+ const fbSel = fallback_selector ? fallback_selector.replace(/'/g, "\\'") : '';
508
+ await send('Runtime.evaluate', {
509
+ expression: `(document.querySelector('${sel}') || document.querySelector('${fbSel}'))?.click(); 'clicked'`,
510
+ });
511
+
512
+ // Wait for file chooser
513
+ const chooserResult = await chooserPromise;
514
+ console.log(`[commands] File chooser: ${chooserResult}`);
515
+
516
+ if (chooserResult === 'opened') {
517
+ // Accept with our file
518
+ await send('Page.handleFileChooser', { action: 'accept', files: [file_path] });
519
+ await send('Page.setInterceptFileChooserDialog', { enabled: false });
520
+ ws.close();
521
+ resolve({ success: true, method: 'file_chooser' });
522
+ } else {
523
+ // Fallback to DOM.setFileInputFiles
524
+ console.log('[commands] File chooser timeout, falling back to DOM.setFileInputFiles');
525
+ await send('Page.setInterceptFileChooserDialog', { enabled: false });
526
+ const doc = await send('DOM.getDocument');
527
+ let node = await send('DOM.querySelector', { nodeId: doc.root.nodeId, selector });
528
+ if (!node?.nodeId && fallback_selector) {
529
+ node = await send('DOM.querySelector', { nodeId: doc.root.nodeId, selector: fallback_selector });
530
+ }
531
+ if (!node?.nodeId) { ws.close(); resolve({ success: false, error: 'Input not found' }); return; }
532
+ await send('DOM.setFileInputFiles', { nodeId: node.nodeId, files: [file_path] });
533
+ ws.close();
534
+ resolve({ success: true, method: 'dom_set_files' });
535
+ }
536
+ } catch (e) { ws.close(); resolve({ success: false, error: e.message }); }
537
+ });
538
+ ws.on('error', (e) => reject(e));
539
+ setTimeout(() => { ws.close(); reject(new Error('Timeout')); }, 15000);
540
+ });
541
+
542
+ await this.api.updateCommand(command._id, { status: result.success ? 'done' : 'failed', result, error: result.error || null });
543
+ } catch (err) {
544
+ console.error(`[commands] Set file input failed: ${err.message}`);
545
+ await this.api.updateCommand(command._id, { status: 'failed', error: err.message });
546
+ }
547
+ }
548
+
446
549
  async handleCloseProfile(command) {
447
550
  const { profile_id } = command.payload || {};
448
551
  console.log(`[commands] Closing profile: ${profile_id}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "channel-worker",
3
- "version": "1.0.20",
3
+ "version": "1.0.22",
4
4
  "description": "Channel Manager worker daemon — runs on remote machines to execute video pipeline jobs",
5
5
  "main": "lib/daemon.js",
6
6
  "bin": {