channel-worker 2.5.122 → 2.5.125
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/playwright-runner.js +30 -1
- package/package.json +1 -1
- package/scripts/upload_facebook.js +199 -49
package/lib/playwright-runner.js
CHANGED
|
@@ -26,6 +26,32 @@ function loadScript(scriptName) {
|
|
|
26
26
|
return mod;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
// Chrome bóp requestAnimationFrame ở tab nền. Menu/dialog của FB dựng bằng
|
|
30
|
+
// animation, nên một tab không được focus có thể đứng hình giữa chừng: selector
|
|
31
|
+
// chờ cho tới hết timeout rồi báo "không tìm thấy nút", trong khi bệnh thật là
|
|
32
|
+
// TRANG CHƯA VẼ XONG. Emulation.setFocusEmulationEnabled làm tab tự coi là đang
|
|
33
|
+
// được focus mà KHÔNG phải kéo nó lên trước mặt (bringToFront) — nhiều profile
|
|
34
|
+
// chạy song song không giành nhau tab đang hiện, và người ngồi máy mở tab khác
|
|
35
|
+
// cũng không cắt ngang lượt đăng.
|
|
36
|
+
//
|
|
37
|
+
// Đây KHÔNG phải stealth script (xem cảnh báo đầu file): không chạm fingerprint
|
|
38
|
+
// của NST, chỉ gỡ throttle. Trang luôn-focus cũng là trạng thái bình thường của
|
|
39
|
+
// người thật đang thao tác.
|
|
40
|
+
//
|
|
41
|
+
// Session phải SỐNG suốt lượt chạy — detach là Chrome bỏ override ngay.
|
|
42
|
+
// Fail-open: bản Chrome nào không nhận thì chạy tiếp y như cũ.
|
|
43
|
+
async function enableFocusEmulation(page, log) {
|
|
44
|
+
try {
|
|
45
|
+
const cdp = await page.context().newCDPSession(page);
|
|
46
|
+
await cdp.send('Emulation.setFocusEmulationEnabled', { enabled: true });
|
|
47
|
+
log('info', '[pw] focus emulation: bật — tab nền vẫn vẽ, không cần kéo lên trước');
|
|
48
|
+
return cdp;
|
|
49
|
+
} catch (e) {
|
|
50
|
+
log('warn', `[pw] focus emulation: không bật được, chạy tiếp như cũ (${String(e?.message || e).slice(0, 100)})`);
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
29
55
|
async function runPlaywrightScript({ nst, profileId, scriptName, payload = {}, log = () => {}, keepOpen = false }) {
|
|
30
56
|
let playwright;
|
|
31
57
|
try { playwright = require('playwright-core'); }
|
|
@@ -65,17 +91,20 @@ async function runPlaywrightScript({ nst, profileId, scriptName, payload = {}, l
|
|
|
65
91
|
|
|
66
92
|
log('info', `[pw] connectOverCDP…`);
|
|
67
93
|
const browser = await playwright.chromium.connectOverCDP(endpoint);
|
|
94
|
+
let focusCdp = null;
|
|
68
95
|
try {
|
|
69
96
|
const context = browser.contexts()[0] || await browser.newContext();
|
|
70
97
|
const page = context.pages()[0] || await context.newPage();
|
|
98
|
+
focusCdp = await enableFocusEmulation(page, log);
|
|
71
99
|
log('info', `[pw] running script: ${scriptName}`);
|
|
72
100
|
const result = await script.run({ page, context, browser, payload, log });
|
|
73
101
|
log('info', `[pw] script done: ${scriptName}`);
|
|
74
102
|
return result;
|
|
75
103
|
} finally {
|
|
104
|
+
if (focusCdp) await focusCdp.detach().catch(() => {});
|
|
76
105
|
// connectOverCDP: this DETACHES Playwright, the NST browser keeps running.
|
|
77
106
|
try { await browser.close(); } catch {}
|
|
78
107
|
}
|
|
79
108
|
}
|
|
80
109
|
|
|
81
|
-
module.exports = { runPlaywrightScript };
|
|
110
|
+
module.exports = { runPlaywrightScript, enableFocusEmulation };
|
package/package.json
CHANGED
|
@@ -594,6 +594,58 @@ async function waitForPublishEnabled(page, verbs, log, timeoutMs, tag = 'fb-pw',
|
|
|
594
594
|
return { enabled: false, sawPresent };
|
|
595
595
|
}
|
|
596
596
|
|
|
597
|
+
// Poll tới khi có một nút "Tiếp"/"Next" ĐANG BẬT, thay cho ngủ cứng một số giây
|
|
598
|
+
// đoán mò. FB tắt "Tiếp" trong lúc nó xử lý thứ vừa nhận (video sau khi upload,
|
|
599
|
+
// ảnh bìa sau khi thay) rồi bật lại — thời gian xử lý phụ thuộc file, nên số
|
|
600
|
+
// giây cố định vừa phí lúc nhanh vừa hụt lúc chậm.
|
|
601
|
+
//
|
|
602
|
+
// SETTLE TRƯỚC KHI POLL — đừng bỏ: ngay sau cú set file, FB thường CHƯA KỊP tắt
|
|
603
|
+
// "Tiếp". Poll ngay lập tức sẽ thấy nút cũ còn bật và trả về "xong" tức thì,
|
|
604
|
+
// rồi bấm Tiếp khi FB chưa xử lý gì — tức là tệ hơn cả ngủ cứng. Ngủ cứng trước
|
|
605
|
+
// đây vô tình che mất chuyện này. Nhịp settle ngắn cho FB kịp phản ứng, rồi mới
|
|
606
|
+
// tin vào trạng thái nút.
|
|
607
|
+
async function waitForNextEnabled(page, log, {
|
|
608
|
+
timeoutMs = 120_000,
|
|
609
|
+
settleMs = 2500,
|
|
610
|
+
pollMs = 2500,
|
|
611
|
+
tag = 'fb-pw',
|
|
612
|
+
what = 'video',
|
|
613
|
+
} = {}) {
|
|
614
|
+
const t0 = Date.now();
|
|
615
|
+
await pause(page, settleMs);
|
|
616
|
+
const deadline = t0 + timeoutMs;
|
|
617
|
+
while (Date.now() < deadline) {
|
|
618
|
+
const ready = await page.evaluate(() => {
|
|
619
|
+
const dlgs = document.querySelectorAll("[role='dialog']");
|
|
620
|
+
const roots = dlgs.length ? Array.from(dlgs) : [document];
|
|
621
|
+
const verbs = ['Tiếp', 'Tiếp theo', 'Next', 'Continue'];
|
|
622
|
+
for (const root of roots) {
|
|
623
|
+
for (const el of root.querySelectorAll("button, [role='button']")) {
|
|
624
|
+
const t = (el.innerText || '').trim();
|
|
625
|
+
if (!verbs.includes(t)) continue;
|
|
626
|
+
const r = el.getBoundingClientRect();
|
|
627
|
+
if (r.width < 8 || r.height < 8) continue;
|
|
628
|
+
const cs = getComputedStyle(el);
|
|
629
|
+
if (cs.visibility === 'hidden' || cs.display === 'none') continue;
|
|
630
|
+
if (el.getAttribute('aria-disabled') === 'true' || el.disabled) continue;
|
|
631
|
+
return true; // "Tiếp" đang bật → FB xử lý xong, đi tiếp được
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
return false;
|
|
635
|
+
}).catch(() => false);
|
|
636
|
+
const elapsedMs = Date.now() - t0;
|
|
637
|
+
if (ready) {
|
|
638
|
+
log('info', `[${tag}] ${what}: FB xử lý xong sau ~${Math.round(elapsedMs / 1000)}s — "Tiếp" đã bật`);
|
|
639
|
+
return { ready: true, elapsedMs };
|
|
640
|
+
}
|
|
641
|
+
await pause(page, pollMs);
|
|
642
|
+
}
|
|
643
|
+
// Không ném: "Tiếp" có thể đang bật mà chữ trên nút khác bộ verbs, và cú bấm
|
|
644
|
+
// Tiếp phía sau vẫn tự chờ theo cách của nó. Chỉ cảnh báo rồi đi tiếp.
|
|
645
|
+
log('warn', `[${tag}] ${what}: chờ hết ${Math.round(timeoutMs / 1000)}s mà không thấy "Tiếp" bật — đi tiếp`);
|
|
646
|
+
return { ready: false, elapsedMs: Date.now() - t0 };
|
|
647
|
+
}
|
|
648
|
+
|
|
597
649
|
async function isThumbnailEditorVisible(page) {
|
|
598
650
|
return page.evaluate((textMarkers) => {
|
|
599
651
|
const isVisible = (el) => {
|
|
@@ -640,7 +692,34 @@ async function isCustomThumbApplied(page) {
|
|
|
640
692
|
if (/Gỡ hình thu nhỏ tùy chỉnh|Remove( your)? custom thumbnail/i.test(al) && vis(el)) return true;
|
|
641
693
|
}
|
|
642
694
|
return false;
|
|
643
|
-
|
|
695
|
+
// `null` = KHÔNG ĐO ĐƯỢC (page.evaluate ném giữa lúc FB dựng lại panel).
|
|
696
|
+
// Bản cũ nuốt thành `false` = "không có thumb", và một mẫu đơn lẻ như thế
|
|
697
|
+
// được quyền bấm Hủy = GỠ ảnh bìa vừa dán. Xem thumbVerdict().
|
|
698
|
+
}).catch(() => null);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// Một lần đo KHÔNG được quyết định hành động PHÁ. Đo trên log prod 20/09/2026:
|
|
702
|
+
// trong 152 lượt "hết giờ chờ hộp ảnh bìa đóng", 138 lượt đo thấy thumb đang
|
|
703
|
+
// áp rồi đăng bình thường, còn 14 lượt mẫu CUỐI trả "không áp" (trong khi suốt
|
|
704
|
+
// vòng chờ trước đó mẫu nào cũng "đang áp") → rơi vào nhánh ESC+Hủy, gỡ mất
|
|
705
|
+
// ảnh bìa, rồi chết ở cổng "Custom thumbnail required but not applied".
|
|
706
|
+
// 'applied' — có ít nhất một mẫu thấy nút "Gỡ hình thu nhỏ tùy chỉnh"
|
|
707
|
+
// 'unknown' — không mẫu nào thấy, nhưng có mẫu đo hỏng
|
|
708
|
+
// 'absent' — mọi mẫu đo được và đều không thấy
|
|
709
|
+
function thumbVerdict(samples) {
|
|
710
|
+
const list = Array.isArray(samples) ? samples : [];
|
|
711
|
+
if (list.some((s) => s === true)) return 'applied';
|
|
712
|
+
if (list.some((s) => s === null || s === undefined)) return 'unknown';
|
|
713
|
+
return 'absent';
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
async function sampleCustomThumb(page, { samples = 3, gapMs = 1500 } = {}) {
|
|
717
|
+
const out = [];
|
|
718
|
+
for (let i = 0; i < samples; i++) {
|
|
719
|
+
if (i) await pause(page, gapMs);
|
|
720
|
+
out.push(await isCustomThumbApplied(page));
|
|
721
|
+
}
|
|
722
|
+
return { verdict: thumbVerdict(out), samples: out };
|
|
644
723
|
}
|
|
645
724
|
|
|
646
725
|
// Composer đang ở trạng thái gì? Đọc từ CHỮ trong hộp thoại — ba chữ ký nhìn
|
|
@@ -853,7 +932,7 @@ async function discoverReelsTabUrl(page, log = null) {
|
|
|
853
932
|
// một link trông giống", nên lượt lấy link sau khi đăng ưu tiên nó.
|
|
854
933
|
// Page thật sự chưa có reel nào cũng ra verified=false; khi đó ids rỗng là
|
|
855
934
|
// đúng, không phải hỏng.
|
|
856
|
-
async function openReelsTabWithTiles(page, urls, { log = null, settleMs = 4000, wait = null, tag = '[fb-pw]' } = {}) {
|
|
935
|
+
async function openReelsTabWithTiles(page, urls, { log = null, settleMs = 4000, wait = null, tag = '[fb-pw]', deep = false } = {}) {
|
|
857
936
|
const list = [...new Set((urls || []).filter(Boolean))];
|
|
858
937
|
let lastUrl = null;
|
|
859
938
|
for (let i = 0; i < list.length; i++) {
|
|
@@ -861,13 +940,70 @@ async function openReelsTabWithTiles(page, urls, { log = null, settleMs = 4000,
|
|
|
861
940
|
lastUrl = url;
|
|
862
941
|
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 45_000 });
|
|
863
942
|
if (wait) await wait(page, settleMs); else await page.waitForTimeout(settleMs);
|
|
864
|
-
|
|
865
|
-
if (ids.length)
|
|
943
|
+
let ids = await scrapeReelTileIds(page);
|
|
944
|
+
if (ids.length) {
|
|
945
|
+
if (deep) ids = await scrollReelsTabForMoreTiles(page, ids, { log, tag });
|
|
946
|
+
return { url, ids, verified: true, tried: i + 1 };
|
|
947
|
+
}
|
|
866
948
|
if (log && i + 1 < list.length) log('warn', `${tag} tab Reels ${url.slice(0, 80)} không có tile nào — thử ứng viên kế (${i + 2}/${list.length})`);
|
|
867
949
|
}
|
|
868
950
|
return { url: lastUrl, ids: [], verified: false, tried: list.length };
|
|
869
951
|
}
|
|
870
952
|
|
|
953
|
+
// ẢNH NỀN SÂU — gốc ca 14/09 "Ngôi nhà tiện ích 01" (kenh-55, idea
|
|
954
|
+
// 6aa2f071b6ee902c8426c35b). `scrapeReelTileIds` chỉ đọc DOM đang có: ngay sau
|
|
955
|
+
// `goto` tab Reels, FB mới render ~10 tile đầu, nên ảnh nền = 10 reel — đo trên
|
|
956
|
+
// prod 12→14/09: 495/710 lượt chụp ảnh nền ra ĐÚNG 10. Sau khi đăng, trang đã ở
|
|
957
|
+
// tab đó lâu hơn nên quét được 20-33 tile ⇒ mọi bài CŨ nằm ở vị trí 11+ trông
|
|
958
|
+
// như "reel mới". Hai hậu quả đo được cùng ngày:
|
|
959
|
+
// 1. nhánh gói mạng nhận nhầm reel CŨ — cả 3/3 lượt "network + tab Reels xác
|
|
960
|
+
// nhận" đều SAI: kenh-46 (reel 07:55 sáng cùng kênh), kenh-55 (reel bài
|
|
961
|
+
// 10/09), kenh-53 (reel 1094951852951660 đã thấy từ nhiều ngày trước, API
|
|
962
|
+
// không bắt được vì reel đó chưa thuộc idea nào → bài đang mang link SAI
|
|
963
|
+
// mà không ai biết). Đối chiếu: nhánh so trước/sau thắng 623 lượt.
|
|
964
|
+
// 2. so trước/sau ra "trước 10, sau 20, mới 10 — nhiều hơn 1, không nhận"
|
|
965
|
+
// ⇒ bài LÊN THẬT vẫn thành mập mờ, bắt người đi kiểm tay.
|
|
966
|
+
// Cuộn thêm ở ảnh nền an toàn cả hai chiều: ảnh nền càng SÂU thì `diffNewReelIds`
|
|
967
|
+
// càng NHỎ, nên không bao giờ đẻ ra "reel mới" giả — chỉ bớt đi. Trần để khỏi
|
|
968
|
+
// cuộn vô tận trên page có hàng trăm reel.
|
|
969
|
+
const REELS_DEEP_MAX_ROUNDS = 8;
|
|
970
|
+
const REELS_DEEP_MAX_TILES = 60;
|
|
971
|
+
// Thuần, test được: còn nạp thêm được tile thì còn cuộn; tile không tăng nữa
|
|
972
|
+
// (hết reel, hoặc FB ngừng lazy-load) thì dừng.
|
|
973
|
+
function shouldKeepScrollingReels({ round, prevCount, count, maxRounds = REELS_DEEP_MAX_ROUNDS, maxTiles = REELS_DEEP_MAX_TILES }) {
|
|
974
|
+
if (round >= maxRounds) return false;
|
|
975
|
+
if (count >= maxTiles) return false;
|
|
976
|
+
if (round > 0 && count <= prevCount) return false;
|
|
977
|
+
return true;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// Cuộn tab Reels cho tới khi số tile ngừng tăng. Hỏng thì trả lại danh sách
|
|
981
|
+
// đang có — ảnh nền nông vẫn hơn không có ảnh nền.
|
|
982
|
+
async function scrollReelsTabForMoreTiles(page, ids, { log = null, tag = '[fb-pw]', maxRounds = REELS_DEEP_MAX_ROUNDS, maxTiles = REELS_DEEP_MAX_TILES } = {}) {
|
|
983
|
+
const shallow = ids.length;
|
|
984
|
+
let out = ids;
|
|
985
|
+
let round = 0;
|
|
986
|
+
let prevCount = -1;
|
|
987
|
+
try {
|
|
988
|
+
while (shouldKeepScrollingReels({ round, prevCount, count: out.length, maxRounds, maxTiles })) {
|
|
989
|
+
prevCount = out.length;
|
|
990
|
+
await humanWheel(page, hRandInt(600, 1200));
|
|
991
|
+
await page.waitForTimeout(hRandInt(700, 1400));
|
|
992
|
+
const next = await scrapeReelTileIds(page);
|
|
993
|
+
// CHỈ nhận khi dài hơn. `scrapeReelTileIds` nuốt lỗi và trả [] (evaluate
|
|
994
|
+
// hỏng, trang đang điều hướng) — gán thẳng thì một lượt quét trượt biến
|
|
995
|
+
// ảnh nền TỐT thành RỖNG, mà ảnh nền rỗng lại đúng là thứ sinh ra ca bốc
|
|
996
|
+
// nhầm. Danh sách chỉ dài thêm theo lazy-load nên lấy bản dài hơn là đúng.
|
|
997
|
+
if (next.length > out.length) out = next;
|
|
998
|
+
round += 1;
|
|
999
|
+
}
|
|
1000
|
+
} catch (e) {
|
|
1001
|
+
if (log) log('warn', `${tag} cuộn sâu tab Reels hỏng: ${String(e && e.message || e).slice(0, 60)} — giữ ${out.length} tile`);
|
|
1002
|
+
}
|
|
1003
|
+
if (log && out.length !== shallow) log('info', `${tag} ảnh nền tab Reels: cuộn ${round} lượt, ${shallow} → ${out.length} tile`);
|
|
1004
|
+
return out;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
871
1007
|
// Mọi reel ID đang hiện trên tab Reels, theo thứ tự DOM (mới nhất trước).
|
|
872
1008
|
async function scrapeReelTileIds(page) {
|
|
873
1009
|
return page.evaluate(() => {
|
|
@@ -917,9 +1053,18 @@ function pickPostPublishReelsTabUrl({ discovered, remembered, rememberedVerified
|
|
|
917
1053
|
// 07:55 sáng CỦA CHÍNH KÊNH) → nhận → API thấy link trùng idea khác → gỡ → bài
|
|
918
1054
|
// thành "mập mờ". Ảnh nền tab Reels chụp trước khi mở composer là bằng chứng
|
|
919
1055
|
// "cũ": ID nằm trong ảnh nền thì KHÔNG BAO GIỜ là bài vừa đăng. Thuần, test được.
|
|
1056
|
+
// KHÔNG có ảnh nền (chụp hỏng, hoặc tab quét ra 0 tile — 19/710 lượt trên prod
|
|
1057
|
+
// 12→14/09) thì chẳng có bằng chứng "cũ" nào, nên "tab Reels xác nhận" chỉ còn
|
|
1058
|
+
// nghĩa "reel này thuộc page mình" — đúng cái đã bốc nhầm ở kenh-46 và kenh-53
|
|
1059
|
+
// (cả hai snapshotLen=0). Hạ xuống ứng viên: bài lên thật vẫn được lượt chờ 75s
|
|
1060
|
+
// và nhánh so trước/sau cứu; cứu không được thì để post_url rỗng cho API đánh
|
|
1061
|
+
// mập mờ. Thà rỗng còn hơn gắn link SAI (luật 19/08) — link sai TỆ HƠN mập mờ
|
|
1062
|
+
// vì nó im lặng: cổng chống link-trùng của API chỉ bắt được khi reel đó đã
|
|
1063
|
+
// thuộc idea khác, còn reel lạ thì lọt thẳng vào DB.
|
|
920
1064
|
function networkIdVerdict({ id, beforeIds, tabIds }) {
|
|
921
1065
|
if (!id) return 'none';
|
|
922
1066
|
if ((beforeIds || []).includes(id)) return 'stale'; // đã có trước khi đăng
|
|
1067
|
+
if (!beforeIds || !beforeIds.length) return 'candidate'; // không có ảnh nền → không dám chốt
|
|
923
1068
|
if ((tabIds || []).includes(id)) return 'confirmed'; // của page mình và mới
|
|
924
1069
|
return 'candidate'; // chưa thấy trên tab, chờ
|
|
925
1070
|
}
|
|
@@ -1047,7 +1192,7 @@ async function runOnce({ page, payload, log }) {
|
|
|
1047
1192
|
} = payload || {};
|
|
1048
1193
|
if (!video_url) throw new Error('No video_url provided');
|
|
1049
1194
|
|
|
1050
|
-
log('info', '[fb-pw] selectors version=2026.09.14-
|
|
1195
|
+
log('info', '[fb-pw] selectors version=2026.09.14-deep-baseline');
|
|
1051
1196
|
|
|
1052
1197
|
page.on('dialog', (d) => { d.accept().catch(() => {}); });
|
|
1053
1198
|
|
|
@@ -1171,7 +1316,7 @@ async function runOnce({ page, payload, log }) {
|
|
|
1171
1316
|
try {
|
|
1172
1317
|
const candidates = await discoverReelsTabUrls(page, log);
|
|
1173
1318
|
if (candidates.length) {
|
|
1174
|
-
const opened = await openReelsTabWithTiles(page, candidates, { log, settleMs: 3500, wait: pause });
|
|
1319
|
+
const opened = await openReelsTabWithTiles(page, candidates, { log, settleMs: 3500, wait: pause, deep: true });
|
|
1175
1320
|
reelsTabUrlBefore = opened.url;
|
|
1176
1321
|
reelsTabVerifiedBefore = opened.verified;
|
|
1177
1322
|
reelIdsBefore = opened.ids;
|
|
@@ -1438,34 +1583,7 @@ async function runOnce({ page, payload, log }) {
|
|
|
1438
1583
|
// small clips, too short for big ones), POLL for an ENABLED "Tiếp" —
|
|
1439
1584
|
// proceed the instant it's ready, but give large clips up to 120s.
|
|
1440
1585
|
log('info', '[fb-pw] waiting for FB to finish processing the video…');
|
|
1441
|
-
|
|
1442
|
-
const procDeadline = procT0 + 120_000;
|
|
1443
|
-
await pause(page, 2500); // let the upload register first
|
|
1444
|
-
let procReady = false;
|
|
1445
|
-
while (Date.now() < procDeadline) {
|
|
1446
|
-
procReady = await page.evaluate(() => {
|
|
1447
|
-
const dlgs = document.querySelectorAll("[role='dialog']");
|
|
1448
|
-
const roots = dlgs.length ? Array.from(dlgs) : [document];
|
|
1449
|
-
const verbs = ['Tiếp', 'Tiếp theo', 'Next', 'Continue'];
|
|
1450
|
-
for (const root of roots) {
|
|
1451
|
-
for (const el of root.querySelectorAll("button, [role='button']")) {
|
|
1452
|
-
const t = (el.innerText || '').trim();
|
|
1453
|
-
if (!verbs.includes(t)) continue;
|
|
1454
|
-
const r = el.getBoundingClientRect();
|
|
1455
|
-
if (r.width < 8 || r.height < 8) continue;
|
|
1456
|
-
const cs = getComputedStyle(el);
|
|
1457
|
-
if (cs.visibility === 'hidden' || cs.display === 'none') continue;
|
|
1458
|
-
if (el.getAttribute('aria-disabled') === 'true' || el.disabled) continue;
|
|
1459
|
-
return true; // an enabled Tiếp/Next → processing done, can advance
|
|
1460
|
-
}
|
|
1461
|
-
}
|
|
1462
|
-
return false;
|
|
1463
|
-
}).catch(() => false);
|
|
1464
|
-
if (procReady) break;
|
|
1465
|
-
await pause(page, 2500);
|
|
1466
|
-
}
|
|
1467
|
-
if (procReady) log('info', `[fb-pw] video processed in ~${Math.round((Date.now() - procT0) / 1000)}s — caption step ready`);
|
|
1468
|
-
else log('warn', '[fb-pw] processing wait hit 120s cap — proceeding (Tiếp click auto-waits anyway)');
|
|
1586
|
+
await waitForNextEnabled(page, log, { timeoutMs: 120_000, what: 'video' });
|
|
1469
1587
|
|
|
1470
1588
|
// 5) Fill metadata. FB Reels has THREE separate fields (verified visually
|
|
1471
1589
|
// by user — different from how YT lumps everything into title + desc):
|
|
@@ -2136,6 +2254,7 @@ async function runOnce({ page, payload, log }) {
|
|
|
2136
2254
|
// doesn't close within 12s of polling, RE-CLICK Lưu (in case
|
|
2137
2255
|
// the first click didn't register because the button was
|
|
2138
2256
|
// disabled while FB was still hashing the upload).
|
|
2257
|
+
let thumbTimeoutSt = { verdict: 'unknown', samples: [] };
|
|
2139
2258
|
const closeDeadline = Date.now() + 60_000;
|
|
2140
2259
|
let modalClosed = false;
|
|
2141
2260
|
let closedPolls = 0;
|
|
@@ -2230,7 +2349,11 @@ async function runOnce({ page, payload, log }) {
|
|
|
2230
2349
|
}
|
|
2231
2350
|
thumbApplied = true;
|
|
2232
2351
|
log('info', `[fb-pw] page-wall thumb — modal closed after save (retries=${retryCount})`);
|
|
2233
|
-
} else if (await
|
|
2352
|
+
} else if (await (async () => {
|
|
2353
|
+
// Giữ kết quả đo để ghi vào log: bằng chứng vì sao KHÔNG Hủy.
|
|
2354
|
+
thumbTimeoutSt = await sampleCustomThumb(page);
|
|
2355
|
+
return thumbTimeoutSt.verdict !== 'absent';
|
|
2356
|
+
})()) {
|
|
2234
2357
|
// Hết giờ chờ đóng nhưng bằng chứng áp thumb còn đó — tuyệt
|
|
2235
2358
|
// đối không ESC/Hủy (Hủy trên panel này là GỠ thumb).
|
|
2236
2359
|
//
|
|
@@ -2241,10 +2364,10 @@ async function runOnce({ page, payload, log }) {
|
|
|
2241
2364
|
// "Custom thumbnail required but not applied" (đo 04/09/2026,
|
|
2242
2365
|
// 3 lượt liên tiếp, log ghi rõ variant=modal). Nút "Gỡ" là
|
|
2243
2366
|
// bằng chứng vật lý thumb đang áp, biến thể nào cũng đúng.
|
|
2244
|
-
log('info', `[fb-pw] page-wall thumb — hết giờ chờ đóng
|
|
2367
|
+
log('info', `[fb-pw] page-wall thumb — hết giờ chờ đóng, đo 3 nhịp ${JSON.stringify(thumbTimeoutSt.samples)} → ${thumbTimeoutSt.verdict} (variant=${thumbEditorVia}) → giữ panel, coi như dán xong, KHÔNG Hủy`);
|
|
2245
2368
|
thumbApplied = true;
|
|
2246
2369
|
} else {
|
|
2247
|
-
log('warn',
|
|
2370
|
+
log('warn', `[fb-pw] page-wall thumb — modal still open 60s after Lưu click, đo 3 nhịp ${JSON.stringify(thumbTimeoutSt.samples)} → CHẮC CHẮN chưa áp; ESC + click Hủy fallback`);
|
|
2248
2371
|
// Chụp lại trước khi ESC/Hủy: đây là nhánh phá lượt nhiều
|
|
2249
2372
|
// nhất mà lại không có ảnh nào để soi (04/09/2026).
|
|
2250
2373
|
await dumpFailure(page, 'thumb-modal-stuck-before-cancel', log);
|
|
@@ -2423,10 +2546,12 @@ async function runOnce({ page, payload, log }) {
|
|
|
2423
2546
|
await chooser.setFiles(thumbPath);
|
|
2424
2547
|
uploaded = true;
|
|
2425
2548
|
log('info', `[fb-pw] custom thumbnail set via <a> "${sel}"`);
|
|
2426
|
-
// FB
|
|
2427
|
-
//
|
|
2428
|
-
//
|
|
2429
|
-
|
|
2549
|
+
// FB tắt "Tiếp" trong lúc xử lý ảnh bìa vừa nhận rồi bật lại;
|
|
2550
|
+
// findByVerbs bỏ qua nút đang tắt nên bấm sớm là trượt bước.
|
|
2551
|
+
// Trước đây ngủ cứng 10s: ảnh nhỏ phí gần hết, máy chậm thì
|
|
2552
|
+
// vẫn hụt. Poll thoát ngay khi nút bật, trần rộng hơn để cứu
|
|
2553
|
+
// ca chậm.
|
|
2554
|
+
await waitForNextEnabled(page, log, { timeoutMs: 30_000, what: 'ảnh bìa' });
|
|
2430
2555
|
break;
|
|
2431
2556
|
} catch (e1) {
|
|
2432
2557
|
log('info', `[fb-pw] <a> click via "${sel}" failed (normal): ${e1.message.slice(0, 80)}`);
|
|
@@ -2625,8 +2750,9 @@ async function runOnce({ page, payload, log }) {
|
|
|
2625
2750
|
// customThumbDone lỡ false trong khi nút "Gỡ hình thu nhỏ tùy chỉnh"
|
|
2626
2751
|
// ngay trên bước này chứng minh thumb ĐANG được áp — cả 5 lượt fail
|
|
2627
2752
|
// 02-03/09 của idea 6a965a32 đều ở trạng thái này (diag 158386).
|
|
2628
|
-
|
|
2629
|
-
|
|
2753
|
+
const gateSt = await sampleCustomThumb(page);
|
|
2754
|
+
if (gateSt.verdict === 'applied') {
|
|
2755
|
+
log('info', `[fb-pw] pre-publish: nút "Gỡ hình thu nhỏ tùy chỉnh" đang hiện (${JSON.stringify(gateSt.samples)}) — thumb đã áp dù cờ chưa bật, tiếp tục đăng`);
|
|
2630
2756
|
customThumbDone = true;
|
|
2631
2757
|
} else {
|
|
2632
2758
|
// Defensive: we normally fail at the caption step (before any Tiếp)
|
|
@@ -3345,11 +3471,27 @@ async function runOnce({ page, payload, log }) {
|
|
|
3345
3471
|
// re-scrape cứu được phần lớn ca đó, vẫn giữ luật fresh-timestamp
|
|
3346
3472
|
// nên không thể nhận nhầm tile cũ.
|
|
3347
3473
|
if (!postUrl && reelsTabUrl) {
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3474
|
+
// Một lượt chờ 75s rồi scrape ĐÚNG MỘT LẦN sai cả hai chiều: reel lên sau
|
|
3475
|
+
// 20s thì đứng phí gần một phút, còn ca chạm mốc "1-3 phút" ở comment
|
|
3476
|
+
// trên thì 75s là HỤT — scrape trượt, rơi xuống các nhánh đoán phía dưới.
|
|
3477
|
+
// Chia thành nhiều lượt re-scrape: thấy tile là đi ngay, chưa thấy thì
|
|
3478
|
+
// chờ tiếp tới hết 3 phút. Luật fresh-timestamp vẫn nguyên nên không thể
|
|
3479
|
+
// nhận nhầm tile cũ dù scrape thêm bao nhiêu lượt.
|
|
3480
|
+
const SURFACE_TIMEOUT_MS = 180_000;
|
|
3481
|
+
const SURFACE_STEP_MS = 25_000;
|
|
3482
|
+
log('info', `[fb-pw] no post URL yet — re-scraping reels_tab mỗi ~${Math.round(SURFACE_STEP_MS / 1000)}s tới ${Math.round(SURFACE_TIMEOUT_MS / 1000)}s cho tới khi reel mới hiện…`);
|
|
3483
|
+
const surfaceT0 = Date.now();
|
|
3484
|
+
let fresh2 = null;
|
|
3485
|
+
let round = 0;
|
|
3486
|
+
while (Date.now() - surfaceT0 < SURFACE_TIMEOUT_MS) {
|
|
3487
|
+
await pause(page, SURFACE_STEP_MS);
|
|
3488
|
+
round += 1;
|
|
3489
|
+
await page.goto(reelsTabUrl, { waitUntil: 'domcontentloaded', timeout: 30_000 }).catch(() => {});
|
|
3490
|
+
await pause(page, 5000);
|
|
3491
|
+
fresh2 = await scrapeFreshReelTile();
|
|
3492
|
+
if (fresh2) break;
|
|
3493
|
+
log('info', `[fb-pw] lượt ${round} (~${Math.round((Date.now() - surfaceT0) / 1000)}s): tab Reels chưa có reel mới`);
|
|
3494
|
+
}
|
|
3353
3495
|
if (fresh2) {
|
|
3354
3496
|
postUrl = fresh2.href.startsWith('http') ? fresh2.href : `https://www.facebook.com${fresh2.href}`;
|
|
3355
3497
|
log('info', `[fb-pw] post URL from reels_tab RETRY (id=${fresh2.id}, timestamp="${fresh2.timestamp}"): ${postUrl}`);
|
|
@@ -3361,7 +3503,7 @@ async function runOnce({ page, payload, log }) {
|
|
|
3361
3503
|
const diff2 = diffNewReelIds(reelIdsBefore, after2);
|
|
3362
3504
|
if (diff2.length === 1) {
|
|
3363
3505
|
postUrl = `https://www.facebook.com/reel/${diff2[0]}/`;
|
|
3364
|
-
log('info', `[fb-pw] post URL từ so trước/sau tab Reels (lượt
|
|
3506
|
+
log('info', `[fb-pw] post URL từ so trước/sau tab Reels (sau ${round} lượt re-scrape; trước ${reelIdsBefore.length}, sau ${after2.length}): ${postUrl}`);
|
|
3365
3507
|
} else {
|
|
3366
3508
|
log('warn', `[fb-pw] reels_tab retry: vẫn chưa thấy reel mới (so trước/sau: mới ${diff2.length})`);
|
|
3367
3509
|
}
|
|
@@ -3501,6 +3643,12 @@ module.exports = { run };
|
|
|
3501
3643
|
// Exposed so the overlay-click escalation can be exercised against a synthetic
|
|
3502
3644
|
// page (toast covering the CTA) without driving the whole upload flow.
|
|
3503
3645
|
module.exports.__testables = {
|
|
3646
|
+
// Chờ FB xử lý xong bằng cách POLL nút "Tiếp", thay cho ngủ cứng đoán mò.
|
|
3647
|
+
// Dùng ở hai chỗ (video vừa upload, ảnh bìa vừa thay) nên hỏng là hỏng đôi.
|
|
3648
|
+
waitForNextEnabled,
|
|
3649
|
+
// Phán quyết "ảnh bìa đã áp chưa" từ nhiều mẫu đo — một mẫu đo hỏng không
|
|
3650
|
+
// được biến thành "chưa áp" rồi kéo theo hành động PHÁ (Hủy = gỡ ảnh bìa).
|
|
3651
|
+
thumbVerdict,
|
|
3504
3652
|
// Dùng chung cho comment_facebook: script nào trượt giữa chừng cũng cần chụp
|
|
3505
3653
|
// lại màn hình, viết bản thứ hai là sớm muộn hai bên lệch nhau.
|
|
3506
3654
|
dumpFailure,
|
|
@@ -3513,6 +3661,8 @@ module.exports.__testables = {
|
|
|
3513
3661
|
discoverReelsTabUrls,
|
|
3514
3662
|
openReelsTabWithTiles,
|
|
3515
3663
|
networkIdVerdict,
|
|
3664
|
+
shouldKeepScrollingReels,
|
|
3665
|
+
scrollReelsTabForMoreTiles,
|
|
3516
3666
|
classifyComposerText,
|
|
3517
3667
|
readPageAlerts,
|
|
3518
3668
|
composerClosedError,
|