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.
- package/lib/command-poller.js +132 -2
- package/package.json +1 -1
package/lib/command-poller.js
CHANGED
|
@@ -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 (${
|
|
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,
|