channel-worker 2.5.68 → 2.5.70

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "channel-worker",
3
- "version": "2.5.68",
3
+ "version": "2.5.70",
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": {
@@ -76,6 +76,22 @@ const REEL_ENTRY_SELECTORS = [
76
76
  // 20 minutes, so give the real upload enough room instead of failing while FB
77
77
  // is visibly still processing it.
78
78
  const PUBLISH_ENABLE_TIMEOUT_MS = 10 * 60 * 1000;
79
+ // Facebook can spend several seconds tearing down the nested custom-thumbnail
80
+ // editor before it mounts the final Reel settings CTA. During that transition
81
+ // there is neither a visible "Tiếp" nor "Đăng" button. The old four-poll
82
+ // (~10s) shortcut returned false just before the CTA appeared, producing a
83
+ // false no-advance failure even though the final blue "Đăng" button was ready.
84
+ const PUBLISH_CTA_ABSENT_GRACE_MS = 30 * 1000;
85
+
86
+ // Text observed in the two thumbnail-editor cohorts. The nested variant has no
87
+ // useful dialog aria-label, so checking only "Chỉnh sửa hình thu nhỏ" misses it
88
+ // while Facebook is processing the saved image.
89
+ const THUMBNAIL_EDITOR_TEXT_MARKERS = [
90
+ 'Chỉnh sửa hình thu nhỏ',
91
+ 'Edit thumbnail',
92
+ 'Tải hình thu nhỏ của riêng bạn lên',
93
+ 'Upload your own thumbnail',
94
+ ];
79
95
 
80
96
  async function firstVisible(locator, max = 5) {
81
97
  const n = Math.min(await locator.count().catch(() => 0), max);
@@ -434,7 +450,7 @@ async function cdpSetInputFiles(page, inputHandle, filePath, log, tag) {
434
450
  });
435
451
  if (!nodeId) throw new Error('CDP querySelector: tagged input not found');
436
452
  await cdp.send('DOM.setFileInputFiles', { files: [filePath], nodeId });
437
- log('info', `[${tag}] file set via CDP DOM.setFileInputFiles (>50MB bypass)`);
453
+ log('info', `[${tag}] file set via CDP DOM.setFileInputFiles (đường truyền path, mọi dung lượng)`);
438
454
  } finally {
439
455
  await inputHandle.evaluate((el, a) => el.removeAttribute(a), ATTR).catch(() => {});
440
456
  if (cdp) await cdp.detach().catch(() => {});
@@ -446,12 +462,24 @@ async function cdpSetInputFiles(page, inputHandle, filePath, log, tag) {
446
462
  // falls back to the CDP command only on the size error.
447
463
  async function setVideoFile(page, inputHandle, filePath, log, tag = 'fb-pw') {
448
464
  if (!inputHandle) throw new Error('setVideoFile: no input handle');
465
+ // VÙNG CHẾT 20-50MB (đo thật 01/09): native setInputFiles đẩy TOÀN BỘ byte
466
+ // qua CDP dưới dạng base64, nên file vừa đủ lớn là quá 30s mặc định của
467
+ // Playwright và ném TimeoutError — trong khi >50MB lại ném đúng chữ "larger
468
+ // than 50MB" nên được rơi về CDP và chạy ngon. Kết quả: video 44,1MB fail,
469
+ // còn 163MB cùng kênh đăng bình thường.
470
+ //
471
+ // DOM.setFileInputFiles truyền ĐƯỜNG DẪN chứ không đẩy byte, nên hợp mọi
472
+ // dung lượng. Vì vậy: native chỉ được thử nhanh (12s) như đường tắt, hỏng vì
473
+ // BẤT KỲ lý do gì cũng rơi về CDP — không lọc theo thông điệp lỗi nữa, chính
474
+ // cái lọc đó là thứ giấu mất đường cứu.
475
+ // Vì browser nối qua CDP nên Playwright coi nó là "remote" và đẩy file dưới
476
+ // dạng base64 — 44MB thành ~59MB trên dây, quá 30s mặc định. Cho hẳn 150s:
477
+ // native là đường ĐÃ CHẠY ĐƯỢC 829 lượt, chỉ thiếu giờ chứ không hỏng.
449
478
  try {
450
- await inputHandle.setInputFiles(filePath); // native works for <50MB
479
+ await inputHandle.setInputFiles(filePath, { timeout: 150000 });
451
480
  return;
452
481
  } catch (e) {
453
- if (!/larger than 50\s?mb|not connected to the server/i.test(String(e.message || ''))) throw e;
454
- log('info', `[${tag}] native setInputFiles hit the 50MB CDP cap — switching to DOM.setFileInputFiles…`);
482
+ log('info', `[${tag}] native setInputFiles không xong (${String(e.message || '').slice(0, 70)}) chuyển sang DOM.setFileInputFiles…`);
455
483
  }
456
484
  await cdpSetInputFiles(page, inputHandle, filePath, log, tag);
457
485
  }
@@ -462,11 +490,14 @@ async function setVideoFile(page, inputHandle, filePath, log, tag = 'fb-pw') {
462
490
  // well past the 30s upload-wait, so findByVerbs (which skips disabled buttons)
463
491
  // finds no publish CTA — and with no "Tiếp" on the last step the loop would
464
492
  // throw no-advance. Poll until a bottom-half publish-verb button is present
465
- // AND enabled. Returns true once enabled; false on timeout, or fast-false if no
466
- // publish button ever appears (we're not actually on the final step).
467
- async function waitForPublishEnabled(page, verbs, log, timeoutMs, tag = 'fb-pw', { onPoll = null } = {}) {
493
+ // AND enabled. Returns true once enabled; false on timeout, or after a guarded
494
+ // absence window when no publish button ever appears (not the final step).
495
+ async function waitForPublishEnabled(page, verbs, log, timeoutMs, tag = 'fb-pw', {
496
+ onPoll = null,
497
+ absentGraceMs = PUBLISH_CTA_ABSENT_GRACE_MS,
498
+ } = {}) {
468
499
  const deadline = Date.now() + timeoutMs;
469
- let announced = false, sawPresent = false, absent = 0;
500
+ let announced = false, sawPresent = false, absentSince = 0;
470
501
  while (Date.now() < deadline) {
471
502
  // The final metadata form mounts lazily on some FB cohorts. The first
472
503
  // fillMetadata() immediately after Tiếp can run before its textbox exists,
@@ -501,9 +532,16 @@ async function waitForPublishEnabled(page, verbs, log, timeoutMs, tag = 'fb-pw',
501
532
  }
502
533
  if (st.present) {
503
534
  sawPresent = true;
535
+ absentSince = 0;
504
536
  if (!announced) { log('info', `[${tag}] final step: "Đăng" disabled (video still processing) — waiting up to ${Math.round(timeoutMs / 1000)}s…`); announced = true; }
505
- } else if (!sawPresent && ++absent >= 4) {
506
- return { enabled: false, sawPresent: false }; // no publish CTA after ~10s → not the final step
537
+ } else if (!sawPresent) {
538
+ if (!absentSince) absentSince = Date.now();
539
+ // The final settings form mounts lazily after the thumbnail editor closes.
540
+ // Give that transition a real time-based grace period instead of assuming
541
+ // four polls means this is not the final step.
542
+ if (Date.now() - absentSince >= absentGraceMs) {
543
+ return { enabled: false, sawPresent: false };
544
+ }
507
545
  }
508
546
  await pause(page, 2500);
509
547
  }
@@ -511,6 +549,31 @@ async function waitForPublishEnabled(page, verbs, log, timeoutMs, tag = 'fb-pw',
511
549
  return { enabled: false, sawPresent };
512
550
  }
513
551
 
552
+ async function isThumbnailEditorVisible(page) {
553
+ return page.evaluate((textMarkers) => {
554
+ const isVisible = (el) => {
555
+ const r = el.getBoundingClientRect();
556
+ if (r.width < 8 || r.height < 8) return false;
557
+ const cs = getComputedStyle(el);
558
+ return cs.visibility !== 'hidden' && cs.display !== 'none' && cs.opacity !== '0';
559
+ };
560
+ const markers = textMarkers.map((s) => s.toLocaleLowerCase());
561
+ for (const dlg of document.querySelectorAll("[role='dialog']")) {
562
+ if (!isVisible(dlg)) continue;
563
+ const txt = (dlg.innerText || '').toLocaleLowerCase();
564
+ if (markers.some((marker) => txt.includes(marker))) return true;
565
+ }
566
+ // Some cohorts render the editor as a nested form instead of a dialog.
567
+ // Its upload control is the most stable signal while the save is pending.
568
+ for (const el of document.querySelectorAll('[aria-label]')) {
569
+ if (!isVisible(el)) continue;
570
+ const al = (el.getAttribute('aria-label') || '').toLocaleLowerCase();
571
+ if ((/tải hình thu nhỏ|thumbnail/.test(al)) && (/tải|upload/.test(al))) return true;
572
+ }
573
+ return false;
574
+ }, THUMBNAIL_EDITOR_TEXT_MARKERS).catch(() => false);
575
+ }
576
+
514
577
  const SAFE_RETRY_COMPOSER_CLOSED = 'FB_SAFE_RETRY_COMPOSER_CLOSED';
515
578
 
516
579
  function safeComposerRetryError(message) {
@@ -568,7 +631,7 @@ async function runOnce({ page, payload, log }) {
568
631
  } = payload || {};
569
632
  if (!video_url) throw new Error('No video_url provided');
570
633
 
571
- log('info', '[fb-pw] selectors version=2026.08.28-icon-only-create-route-fallback');
634
+ log('info', '[fb-pw] selectors version=2026.09.01-thumb-final-cta-race');
572
635
 
573
636
  page.on('dialog', (d) => { d.accept().catch(() => {}); });
574
637
 
@@ -1590,6 +1653,7 @@ async function runOnce({ page, payload, log }) {
1590
1653
  // disabled while FB was still hashing the upload).
1591
1654
  const closeDeadline = Date.now() + 60_000;
1592
1655
  let modalClosed = false;
1656
+ let closedPolls = 0;
1593
1657
  let retryCount = 0;
1594
1658
  const RETRY_AFTER_MS = 12_000;
1595
1659
  let lastRetryAt = Date.now();
@@ -1599,27 +1663,16 @@ async function runOnce({ page, payload, log }) {
1599
1663
  // nested form (no own dialog — its upload button, aria
1600
1664
  // "Tải hình thu nhỏ tùy chỉnh lên…", only exists while the
1601
1665
  // editor is showing).
1602
- const still = await page.evaluate(() => {
1603
- const dlgs = document.querySelectorAll("[role='dialog']");
1604
- for (const dlg of dlgs) {
1605
- const txt = (dlg.innerText || '').slice(0, 200);
1606
- if (/Chỉnh sửa hình thu nhỏ|Edit thumbnail/i.test(txt)) {
1607
- const r = dlg.getBoundingClientRect();
1608
- if (r.width > 8 && r.height > 8) return true;
1609
- }
1610
- }
1611
- for (const el of document.querySelectorAll('[aria-label]')) {
1612
- const al = el.getAttribute('aria-label') || '';
1613
- if (!(/Tải hình thu nhỏ|thumbnail/i.test(al) && /Tải|upload/i.test(al))) continue;
1614
- const r = el.getBoundingClientRect();
1615
- if (r.width < 8 || r.height < 8) continue;
1616
- const cs = getComputedStyle(el);
1617
- if (cs.visibility === 'hidden' || cs.display === 'none' || cs.opacity === '0') continue;
1618
- return true;
1619
- }
1620
- return false;
1621
- }).catch(() => false);
1622
- if (!still) { modalClosed = true; break; }
1666
+ const still = await isThumbnailEditorVisible(page);
1667
+ if (!still) {
1668
+ // Require two consecutive closed samples. Facebook briefly
1669
+ // unmounts the nested form while swapping it for a
1670
+ // processing overlay; treating that single gap as closed
1671
+ // races the final "Đăng" button mount.
1672
+ if (++closedPolls >= 2) { modalClosed = true; break; }
1673
+ } else {
1674
+ closedPolls = 0;
1675
+ }
1623
1676
  // Retry Lưu click after RETRY_AFTER_MS without progress —
1624
1677
  // limit to 3 retries (so we still leave time for FB to
1625
1678
  // finish processing on the last attempt).
@@ -2789,13 +2842,17 @@ module.exports.__testables = {
2789
2842
  muteClickInterceptors,
2790
2843
  unmuteClickInterceptors,
2791
2844
  waitForPublishEnabled,
2845
+ isThumbnailEditorVisible,
2792
2846
  hasVisibleReelComposer,
2793
2847
  SAFE_RETRY_COMPOSER_CLOSED,
2794
2848
  navigateFacebookHome,
2795
2849
  navigateFacebookReelCreate,
2796
2850
  PUBLISH_ENABLE_TIMEOUT_MS,
2851
+ PUBLISH_CTA_ABSENT_GRACE_MS,
2852
+ THUMBNAIL_EDITOR_TEXT_MARKERS,
2797
2853
  REEL_CREATE_SELECTORS,
2798
2854
  REEL_ENTRY_SELECTORS,
2799
2855
  ADD_VIDEO_SELECTORS,
2856
+ setVideoFile,
2800
2857
  FB_REEL_CREATE_URL,
2801
2858
  };