channel-worker 1.1.1 → 1.1.3

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.
@@ -58,6 +58,9 @@ class CommandPoller {
58
58
  case 'set_file_input':
59
59
  await this.handleSetFileInput(command);
60
60
  break;
61
+ case 'click_and_upload':
62
+ await this.handleClickAndUpload(command);
63
+ break;
61
64
  case 'type_text':
62
65
  await this.handleTypeText(command);
63
66
  break;
@@ -550,6 +553,126 @@ class CommandPoller {
550
553
  }
551
554
  }
552
555
 
556
+ async handleClickAndUpload(command) {
557
+ const { profile_id, file_path, click_selector, click_fallback_text, url_match } = command.payload || {};
558
+ console.log(`[commands] Click and upload: ${click_selector} → ${file_path}`);
559
+ try {
560
+ const WebSocket = require('ws');
561
+ if (!this.nst) {
562
+ const apiKey = await this.api.getSetting('nst_api_key');
563
+ if (apiKey) this.nst = new NstManager(apiKey);
564
+ }
565
+ const browsersRes = await fetch('http://localhost:8848/api/v2/browsers', {
566
+ headers: { 'x-api-key': this.nst?.apiKey || '' },
567
+ });
568
+ 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];
569
+ if (!browser) throw new Error('No running browser');
570
+
571
+ const pagesRes = await fetch(`http://localhost:${browser.remoteDebuggingPort}/json/list`);
572
+ const pages = await pagesRes.json();
573
+ const targetPage = pages.find(p => p.type === 'page' && (!url_match || p.url?.includes(url_match)));
574
+ if (!targetPage) throw new Error(`No tab matching ${url_match}`);
575
+
576
+ const result = await new Promise((resolve, reject) => {
577
+ const ws = new WebSocket(targetPage.webSocketDebuggerUrl);
578
+ let msgId = 1;
579
+ function send(method, params = {}) {
580
+ const id = msgId++;
581
+ return new Promise((res, rej) => {
582
+ const handler = (data) => {
583
+ const msg = JSON.parse(data);
584
+ if (msg.id === id) { ws.removeListener('message', handler); msg.error ? rej(new Error(msg.error.message)) : res(msg.result); }
585
+ };
586
+ ws.on('message', handler);
587
+ ws.send(JSON.stringify({ id, method, params }));
588
+ });
589
+ }
590
+
591
+ ws.on('open', async () => {
592
+ try {
593
+ await send('Page.enable');
594
+ await send('DOM.enable');
595
+
596
+ // Enable file chooser interception
597
+ await send('Page.setInterceptFileChooserDialog', { enabled: true });
598
+
599
+ // Listen for file chooser
600
+ const chooserPromise = new Promise((res) => {
601
+ const handler = (data) => {
602
+ const msg = JSON.parse(data);
603
+ if (msg.method === 'Page.fileChooserOpened') {
604
+ ws.removeListener('message', handler);
605
+ res('opened');
606
+ }
607
+ };
608
+ ws.on('message', handler);
609
+ setTimeout(() => { ws.removeListener('message', handler); res('timeout'); }, 10000);
610
+ });
611
+
612
+ // Find element position and click via mouse
613
+ const rawSel = (click_selector || '');
614
+ const isXpath = rawSel.startsWith('xpath:');
615
+ const sel = rawSel.replace('xpath:', '').replace(/'/g, "\\'").replace(/"/g, '\\"');
616
+
617
+ const posResult = await send('Runtime.evaluate', {
618
+ expression: `
619
+ (function() {
620
+ let el = null;
621
+ if (${isXpath} && "${sel}") {
622
+ el = document.evaluate("${sel}", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
623
+ } else if ("${sel}") {
624
+ el = document.querySelector("${sel}");
625
+ }
626
+ if (!el && "${click_fallback_text || ''}") {
627
+ const allEls = document.querySelectorAll('[role="button"], button');
628
+ for (const btn of allEls) {
629
+ if (btn.textContent?.trim()?.toLowerCase() === "${(click_fallback_text || '').toLowerCase()}") { el = btn; break; }
630
+ }
631
+ }
632
+ if (!el) return null;
633
+ const rect = el.getBoundingClientRect();
634
+ return { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 };
635
+ })()
636
+ `,
637
+ returnByValue: true,
638
+ });
639
+
640
+ const pos = posResult?.result?.value;
641
+ if (!pos) { ws.close(); resolve({ success: false, error: 'Element not found' }); return; }
642
+
643
+ // Click with mouse
644
+ await send('Input.dispatchMouseEvent', { type: 'mousePressed', x: pos.x, y: pos.y, button: 'left', clickCount: 1 });
645
+ await send('Input.dispatchMouseEvent', { type: 'mouseReleased', x: pos.x, y: pos.y, button: 'left', clickCount: 1 });
646
+ console.log(`[commands] Mouse clicked Upload at (${pos.x}, ${pos.y})`);
647
+
648
+ // Wait for file chooser
649
+ const chooserResult = await chooserPromise;
650
+ console.log(`[commands] File chooser: ${chooserResult}`);
651
+
652
+ if (chooserResult === 'opened') {
653
+ await send('Page.handleFileChooser', { action: 'accept', files: [file_path] });
654
+ await send('Page.setInterceptFileChooserDialog', { enabled: false });
655
+ ws.close();
656
+ resolve({ success: true, method: 'file_chooser' });
657
+ } else {
658
+ await send('Page.setInterceptFileChooserDialog', { enabled: false });
659
+ ws.close();
660
+ resolve({ success: false, error: 'File chooser timeout' });
661
+ }
662
+ } catch (e) { ws.close(); resolve({ success: false, error: e.message }); }
663
+ });
664
+ ws.on('error', (e) => reject(e));
665
+ setTimeout(() => { ws.close(); reject(new Error('CDP timeout')); }, 20000);
666
+ });
667
+
668
+ console.log(`[commands] Click and upload result:`, result);
669
+ await this.api.updateCommand(command._id, { status: result.success ? 'done' : 'failed', result, error: result.error || null });
670
+ } catch (err) {
671
+ console.error(`[commands] Click and upload failed: ${err.message}`);
672
+ await this.api.updateCommand(command._id, { status: 'failed', error: err.message });
673
+ }
674
+ }
675
+
553
676
  async handleTypeText(command) {
554
677
  const { text, profile_id, url_match, focus_selector, press_enter } = command.payload || {};
555
678
  console.log(`[commands] Typing text (${text?.length} chars) for profile: ${profile_id}`);
@@ -591,6 +714,7 @@ class CommandPoller {
591
714
  // Focus target element via CDP mouse click (trusted event)
592
715
  const rawSel = (focus_selector || '');
593
716
  const isXpath = rawSel.startsWith('xpath:');
717
+ const isTallest = rawSel === 'tallest_editor';
594
718
  const sel = rawSel.replace('xpath:', '').replace(/'/g, "\\'").replace(/"/g, '\\"');
595
719
 
596
720
  // Get element bounding box
@@ -598,7 +722,13 @@ class CommandPoller {
598
722
  expression: `
599
723
  (function() {
600
724
  let el = null;
601
- if (${isXpath} && '${sel}') {
725
+ if (${isTallest}) {
726
+ // Find tallest contenteditable editor (for "Describe your reel" etc.)
727
+ let maxH = 0;
728
+ document.querySelectorAll('[contenteditable="true"], [role="textbox"]').forEach(e => {
729
+ if (e.offsetHeight > maxH) { maxH = e.offsetHeight; el = e; }
730
+ });
731
+ } else if (${isXpath} && '${sel}') {
602
732
  el = document.evaluate("${sel}", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
603
733
  } else if ('${sel}') {
604
734
  el = document.querySelector('${sel}');
@@ -613,7 +743,7 @@ class CommandPoller {
613
743
  }
614
744
  if (!el) return null;
615
745
  const rect = el.getBoundingClientRect();
616
- return { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2, tag: el.tagName, aria: (el.getAttribute('aria-label') || '').substring(0, 30) };
746
+ return { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2, tag: el.tagName, aria: (el.getAttribute('aria-label') || '').substring(0, 30), height: el.offsetHeight };
617
747
  })()
618
748
  `,
619
749
  returnByValue: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "channel-worker",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
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": {