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 +1 -1
- package/scripts/upload_facebook.js +89 -32
package/package.json
CHANGED
|
@@ -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 (
|
|
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
|
|
479
|
+
await inputHandle.setInputFiles(filePath, { timeout: 150000 });
|
|
451
480
|
return;
|
|
452
481
|
} catch (e) {
|
|
453
|
-
|
|
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
|
|
466
|
-
// publish button ever appears (
|
|
467
|
-
async function waitForPublishEnabled(page, verbs, log, timeoutMs, tag = 'fb-pw', {
|
|
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,
|
|
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
|
|
506
|
-
|
|
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.
|
|
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
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
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
|
};
|