channel-worker 1.0.21 → 1.0.23

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.
Files changed (2) hide show
  1. package/lib/command-poller.js +102 -10
  2. package/package.json +1 -1
@@ -58,6 +58,9 @@ class CommandPoller {
58
58
  case 'set_file_input':
59
59
  await this.handleSetFileInput(command);
60
60
  break;
61
+ case 'type_text':
62
+ await this.handleTypeText(command);
63
+ break;
61
64
  default:
62
65
  // Other commands (scan_facebook_pages, etc.) handled by extension
63
66
  console.log(`[commands] Skipping ${command.type} — handled by extension`);
@@ -483,21 +486,56 @@ class CommandPoller {
483
486
 
484
487
  ws.on('open', async () => {
485
488
  try {
489
+ await send('Page.enable');
486
490
  await send('DOM.enable');
487
- const doc = await send('DOM.getDocument');
488
- let node = await send('DOM.querySelector', { nodeId: doc.root.nodeId, selector });
489
- if (!node?.nodeId && fallback_selector) {
490
- node = await send('DOM.querySelector', { nodeId: doc.root.nodeId, selector: fallback_selector });
491
- }
492
- if (!node?.nodeId) { ws.close(); resolve({ success: false, error: 'Input not found' }); return; }
493
491
 
494
- await send('DOM.setFileInputFiles', { nodeId: node.nodeId, files: [file_path] });
492
+ // Enable file chooser interception
493
+ await send('Page.setInterceptFileChooserDialog', { enabled: true });
494
+
495
+ // Listen for file chooser event
496
+ const chooserPromise = new Promise((res) => {
497
+ const eventHandler = (data) => {
498
+ const msg = JSON.parse(data);
499
+ if (msg.method === 'Page.fileChooserOpened') {
500
+ ws.removeListener('message', eventHandler);
501
+ res('opened');
502
+ }
503
+ };
504
+ ws.on('message', eventHandler);
505
+ setTimeout(() => { ws.removeListener('message', eventHandler); res('timeout'); }, 10000);
506
+ });
507
+
508
+ // Click the file input to trigger native file dialog
509
+ const sel = selector.replace(/'/g, "\\'");
510
+ const fbSel = fallback_selector ? fallback_selector.replace(/'/g, "\\'") : '';
495
511
  await send('Runtime.evaluate', {
496
- expression: `document.querySelector('${selector.replace(/'/g, "\\'")}')?.dispatchEvent(new Event('change', { bubbles: true })); 'ok'`,
512
+ expression: `(document.querySelector('${sel}') || document.querySelector('${fbSel}'))?.click(); 'clicked'`,
497
513
  });
498
514
 
499
- ws.close();
500
- resolve({ success: true });
515
+ // Wait for file chooser
516
+ const chooserResult = await chooserPromise;
517
+ console.log(`[commands] File chooser: ${chooserResult}`);
518
+
519
+ if (chooserResult === 'opened') {
520
+ // Accept with our file
521
+ await send('Page.handleFileChooser', { action: 'accept', files: [file_path] });
522
+ await send('Page.setInterceptFileChooserDialog', { enabled: false });
523
+ ws.close();
524
+ resolve({ success: true, method: 'file_chooser' });
525
+ } else {
526
+ // Fallback to DOM.setFileInputFiles
527
+ console.log('[commands] File chooser timeout, falling back to DOM.setFileInputFiles');
528
+ await send('Page.setInterceptFileChooserDialog', { enabled: false });
529
+ const doc = await send('DOM.getDocument');
530
+ let node = await send('DOM.querySelector', { nodeId: doc.root.nodeId, selector });
531
+ if (!node?.nodeId && fallback_selector) {
532
+ node = await send('DOM.querySelector', { nodeId: doc.root.nodeId, selector: fallback_selector });
533
+ }
534
+ if (!node?.nodeId) { ws.close(); resolve({ success: false, error: 'Input not found' }); return; }
535
+ await send('DOM.setFileInputFiles', { nodeId: node.nodeId, files: [file_path] });
536
+ ws.close();
537
+ resolve({ success: true, method: 'dom_set_files' });
538
+ }
501
539
  } catch (e) { ws.close(); resolve({ success: false, error: e.message }); }
502
540
  });
503
541
  ws.on('error', (e) => reject(e));
@@ -511,6 +549,60 @@ class CommandPoller {
511
549
  }
512
550
  }
513
551
 
552
+ async handleTypeText(command) {
553
+ const { text, profile_id, url_match } = command.payload || {};
554
+ console.log(`[commands] Typing text (${text?.length} chars) for profile: ${profile_id}`);
555
+ try {
556
+ const WebSocket = require('ws');
557
+ if (!this.nst) {
558
+ const apiKey = await this.api.getSetting('nst_api_key');
559
+ if (apiKey) this.nst = new NstManager(apiKey);
560
+ }
561
+ const browsersRes = await fetch('http://localhost:8848/api/v2/browsers', {
562
+ headers: { 'x-api-key': this.nst?.apiKey || '' },
563
+ });
564
+ const browser = ((await browsersRes.json())?.data || []).find(b => b.name === profile_id) || (await (await fetch('http://localhost:8848/api/v2/browsers', { headers: { 'x-api-key': this.nst?.apiKey || '' } })).json())?.data?.[0];
565
+ if (!browser) throw new Error('No running browser');
566
+
567
+ const pagesRes = await fetch(`http://localhost:${browser.remoteDebuggingPort}/json/list`);
568
+ const pages = await pagesRes.json();
569
+ const targetPage = pages.find(p => p.type === 'page' && (!url_match || p.url?.includes(url_match)));
570
+ if (!targetPage) throw new Error(`No tab matching ${url_match}`);
571
+
572
+ const result = await new Promise((resolve, reject) => {
573
+ const ws = new WebSocket(targetPage.webSocketDebuggerUrl);
574
+ let msgId = 1;
575
+ function send(method, params = {}) {
576
+ const id = msgId++;
577
+ return new Promise((res, rej) => {
578
+ const handler = (data) => {
579
+ const msg = JSON.parse(data);
580
+ if (msg.id === id) { ws.removeListener('message', handler); msg.error ? rej(new Error(msg.error.message)) : res(msg.result); }
581
+ };
582
+ ws.on('message', handler);
583
+ ws.send(JSON.stringify({ id, method, params }));
584
+ });
585
+ }
586
+
587
+ ws.on('open', async () => {
588
+ try {
589
+ // Use Input.insertText — types text as if user typed it
590
+ await send('Input.insertText', { text });
591
+ ws.close();
592
+ resolve({ success: true, length: text.length });
593
+ } catch (e) { ws.close(); resolve({ success: false, error: e.message }); }
594
+ });
595
+ ws.on('error', (e) => reject(e));
596
+ setTimeout(() => { ws.close(); reject(new Error('Timeout')); }, 10000);
597
+ });
598
+
599
+ await this.api.updateCommand(command._id, { status: result.success ? 'done' : 'failed', result, error: result.error || null });
600
+ } catch (err) {
601
+ console.error(`[commands] Type text failed: ${err.message}`);
602
+ await this.api.updateCommand(command._id, { status: 'failed', error: err.message });
603
+ }
604
+ }
605
+
514
606
  async handleCloseProfile(command) {
515
607
  const { profile_id } = command.payload || {};
516
608
  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.21",
3
+ "version": "1.0.23",
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": {