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.
- package/lib/command-poller.js +102 -10
- 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 '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
|
-
|
|
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('${
|
|
512
|
+
expression: `(document.querySelector('${sel}') || document.querySelector('${fbSel}'))?.click(); 'clicked'`,
|
|
497
513
|
});
|
|
498
514
|
|
|
499
|
-
|
|
500
|
-
|
|
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}`);
|