channel-worker 1.1.2 → 1.1.4
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 +135 -0
- 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,138 @@ 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
|
+
// File chooser intercepted — now set file via DOM.setFileInputFiles
|
|
654
|
+
// Find the file input that triggered the chooser
|
|
655
|
+
const doc = await send('DOM.getDocument');
|
|
656
|
+
const fileNode = await send('DOM.querySelector', { nodeId: doc.root.nodeId, selector: 'input[type="file"][accept*="image"]' });
|
|
657
|
+
if (!fileNode?.nodeId) {
|
|
658
|
+
// Fallback: any file input
|
|
659
|
+
const fallbackNode = await send('DOM.querySelector', { nodeId: doc.root.nodeId, selector: 'input[type="file"]' });
|
|
660
|
+
if (fallbackNode?.nodeId) {
|
|
661
|
+
await send('DOM.setFileInputFiles', { nodeId: fallbackNode.nodeId, files: [file_path] });
|
|
662
|
+
}
|
|
663
|
+
} else {
|
|
664
|
+
await send('DOM.setFileInputFiles', { nodeId: fileNode.nodeId, files: [file_path] });
|
|
665
|
+
}
|
|
666
|
+
await send('Page.setInterceptFileChooserDialog', { enabled: false });
|
|
667
|
+
ws.close();
|
|
668
|
+
resolve({ success: true, method: 'file_chooser_intercepted' });
|
|
669
|
+
} else {
|
|
670
|
+
await send('Page.setInterceptFileChooserDialog', { enabled: false });
|
|
671
|
+
ws.close();
|
|
672
|
+
resolve({ success: false, error: 'File chooser timeout' });
|
|
673
|
+
}
|
|
674
|
+
} catch (e) { ws.close(); resolve({ success: false, error: e.message }); }
|
|
675
|
+
});
|
|
676
|
+
ws.on('error', (e) => reject(e));
|
|
677
|
+
setTimeout(() => { ws.close(); reject(new Error('CDP timeout')); }, 20000);
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
console.log(`[commands] Click and upload result:`, result);
|
|
681
|
+
await this.api.updateCommand(command._id, { status: result.success ? 'done' : 'failed', result, error: result.error || null });
|
|
682
|
+
} catch (err) {
|
|
683
|
+
console.error(`[commands] Click and upload failed: ${err.message}`);
|
|
684
|
+
await this.api.updateCommand(command._id, { status: 'failed', error: err.message });
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
553
688
|
async handleTypeText(command) {
|
|
554
689
|
const { text, profile_id, url_match, focus_selector, press_enter } = command.payload || {};
|
|
555
690
|
console.log(`[commands] Typing text (${text?.length} chars) for profile: ${profile_id}`);
|