channel-worker 1.0.19 → 1.0.21

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.
@@ -52,6 +52,12 @@ class CommandPoller {
52
52
  case 'set_thumbnail':
53
53
  await this.handleSetThumbnail(command);
54
54
  break;
55
+ case 'set_tags':
56
+ await this.handleSetTags(command);
57
+ break;
58
+ case 'set_file_input':
59
+ await this.handleSetFileInput(command);
60
+ break;
55
61
  default:
56
62
  // Other commands (scan_facebook_pages, etc.) handled by extension
57
63
  console.log(`[commands] Skipping ${command.type} — handled by extension`);
@@ -334,6 +340,177 @@ class CommandPoller {
334
340
  }
335
341
  }
336
342
 
343
+ async handleSetTags(command) {
344
+ const { tags, profile_id } = command.payload || {};
345
+ console.log(`[commands] Setting tags for profile: ${profile_id}`, tags);
346
+ try {
347
+ const WebSocket = require('ws');
348
+
349
+ // Find browser
350
+ if (!this.nst) {
351
+ const apiKey = await this.api.getSetting('nst_api_key');
352
+ if (apiKey) this.nst = new NstManager(apiKey);
353
+ }
354
+ const browsersRes = await fetch('http://localhost:8848/api/v2/browsers', {
355
+ headers: { 'x-api-key': this.nst?.apiKey || '' },
356
+ });
357
+ const browsersData = await browsersRes.json();
358
+ const browser = (browsersData?.data || []).find(b => b.name === profile_id) || browsersData?.data?.[0];
359
+ if (!browser) throw new Error('No running browser');
360
+
361
+ const pagesRes = await fetch(`http://localhost:${browser.remoteDebuggingPort}/json/list`);
362
+ const pages = await pagesRes.json();
363
+ const studioPage = pages.find(p => p.url?.includes('studio.youtube.com') && p.type === 'page');
364
+ if (!studioPage) throw new Error('YouTube Studio tab not found');
365
+
366
+ const tagStr = tags.join(', ');
367
+
368
+ const result = await new Promise((resolve, reject) => {
369
+ const ws = new WebSocket(studioPage.webSocketDebuggerUrl);
370
+ let msgId = 1;
371
+ function send(method, params = {}) {
372
+ const id = msgId++;
373
+ return new Promise((res, rej) => {
374
+ const handler = (data) => {
375
+ const msg = JSON.parse(data);
376
+ if (msg.id === id) { ws.removeListener('message', handler); msg.error ? rej(new Error(msg.error.message)) : res(msg.result); }
377
+ };
378
+ ws.on('message', handler);
379
+ ws.send(JSON.stringify({ id, method, params }));
380
+ });
381
+ }
382
+
383
+ ws.on('open', async () => {
384
+ try {
385
+ // Click "Show more" button
386
+ await send('Runtime.evaluate', {
387
+ expression: `
388
+ (function() {
389
+ const texts = ['show more', 'hiện thêm', 'hiển thị thêm'];
390
+ const allEls = document.querySelectorAll('*');
391
+ for (const el of allEls) {
392
+ if (el.children.length > 3) continue;
393
+ const t = el.textContent?.trim()?.toLowerCase() || '';
394
+ if (texts.some(x => t === x)) { el.click(); return 'clicked: ' + el.textContent?.trim(); }
395
+ }
396
+ return 'not_found';
397
+ })()
398
+ `,
399
+ });
400
+
401
+ // Wait for section to expand
402
+ await new Promise(r => setTimeout(r, 2000));
403
+
404
+ // Find and fill tags input — placeholder "Add tag"
405
+ const fillResult = await send('Runtime.evaluate', {
406
+ expression: `
407
+ (function() {
408
+ // Find input with placeholder "Add tag" or aria-label containing "tag"
409
+ const inputs = document.querySelectorAll('input');
410
+ for (const inp of inputs) {
411
+ const ph = (inp.placeholder || '').toLowerCase();
412
+ const aria = (inp.getAttribute('aria-label') || '').toLowerCase();
413
+ if (ph.includes('add tag') || ph.includes('thêm thẻ') || aria.includes('tag') || aria.includes('thẻ')) {
414
+ inp.focus();
415
+ inp.value = '${tagStr.replace(/'/g, "\\'")}';
416
+ inp.dispatchEvent(new Event('input', { bubbles: true }));
417
+ inp.dispatchEvent(new Event('change', { bubbles: true }));
418
+ // Press Enter to confirm
419
+ inp.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', keyCode: 13, bubbles: true }));
420
+ inp.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter', code: 'Enter', keyCode: 13, bubbles: true }));
421
+ return 'tags_filled';
422
+ }
423
+ }
424
+ return 'no_tag_input';
425
+ })()
426
+ `,
427
+ });
428
+
429
+ ws.close();
430
+ resolve({ success: true, result: fillResult?.result?.value });
431
+ } catch (e) { ws.close(); resolve({ success: false, error: e.message }); }
432
+ });
433
+ ws.on('error', (e) => reject(e));
434
+ setTimeout(() => { ws.close(); reject(new Error('CDP timeout')); }, 15000);
435
+ });
436
+
437
+ console.log(`[commands] Tags CDP result:`, result);
438
+ await this.api.updateCommand(command._id, {
439
+ status: result.success ? 'done' : 'failed',
440
+ result,
441
+ error: result.error || null,
442
+ });
443
+ } catch (err) {
444
+ console.error(`[commands] Set tags failed: ${err.message}`);
445
+ await this.api.updateCommand(command._id, { status: 'failed', error: err.message });
446
+ }
447
+ }
448
+
449
+ async handleSetFileInput(command) {
450
+ const { profile_id, file_path, selector, fallback_selector, url_match } = command.payload || {};
451
+ console.log(`[commands] Setting file input: ${selector} → ${file_path}`);
452
+ try {
453
+ const WebSocket = require('ws');
454
+ if (!this.nst) {
455
+ const apiKey = await this.api.getSetting('nst_api_key');
456
+ if (apiKey) this.nst = new NstManager(apiKey);
457
+ }
458
+ const browsersRes = await fetch('http://localhost:8848/api/v2/browsers', {
459
+ headers: { 'x-api-key': this.nst?.apiKey || '' },
460
+ });
461
+ const browser = ((await browsersRes.json())?.data || []).find(b => b.name === profile_id) || (await browsersRes.json())?.data?.[0];
462
+ if (!browser) throw new Error('No running browser');
463
+
464
+ const pagesRes = await fetch(`http://localhost:${browser.remoteDebuggingPort}/json/list`);
465
+ const pages = await pagesRes.json();
466
+ const targetPage = pages.find(p => p.type === 'page' && (!url_match || p.url?.includes(url_match)));
467
+ if (!targetPage) throw new Error(`No tab matching ${url_match}`);
468
+
469
+ const result = await new Promise((resolve, reject) => {
470
+ const ws = new WebSocket(targetPage.webSocketDebuggerUrl);
471
+ let msgId = 1;
472
+ function send(method, params = {}) {
473
+ const id = msgId++;
474
+ return new Promise((res, rej) => {
475
+ const handler = (data) => {
476
+ const msg = JSON.parse(data);
477
+ if (msg.id === id) { ws.removeListener('message', handler); msg.error ? rej(new Error(msg.error.message)) : res(msg.result); }
478
+ };
479
+ ws.on('message', handler);
480
+ ws.send(JSON.stringify({ id, method, params }));
481
+ });
482
+ }
483
+
484
+ ws.on('open', async () => {
485
+ try {
486
+ 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
+
494
+ await send('DOM.setFileInputFiles', { nodeId: node.nodeId, files: [file_path] });
495
+ await send('Runtime.evaluate', {
496
+ expression: `document.querySelector('${selector.replace(/'/g, "\\'")}')?.dispatchEvent(new Event('change', { bubbles: true })); 'ok'`,
497
+ });
498
+
499
+ ws.close();
500
+ resolve({ success: true });
501
+ } catch (e) { ws.close(); resolve({ success: false, error: e.message }); }
502
+ });
503
+ ws.on('error', (e) => reject(e));
504
+ setTimeout(() => { ws.close(); reject(new Error('Timeout')); }, 15000);
505
+ });
506
+
507
+ await this.api.updateCommand(command._id, { status: result.success ? 'done' : 'failed', result, error: result.error || null });
508
+ } catch (err) {
509
+ console.error(`[commands] Set file input failed: ${err.message}`);
510
+ await this.api.updateCommand(command._id, { status: 'failed', error: err.message });
511
+ }
512
+ }
513
+
337
514
  async handleCloseProfile(command) {
338
515
  const { profile_id } = command.payload || {};
339
516
  console.log(`[commands] Closing profile: ${profile_id}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "channel-worker",
3
- "version": "1.0.19",
3
+ "version": "1.0.21",
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": {