channel-worker 1.1.2 → 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}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "channel-worker",
3
- "version": "1.1.2",
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": {