channel-worker 2.5.123 → 2.5.126
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/comment_facebook.js +58 -6
- package/scripts/upload_facebook.js +157 -43
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
|
@@ -68,8 +68,15 @@ const BOX_SELECTORS = [
|
|
|
68
68
|
const OPEN_COMMENTS_SELECTORS = [
|
|
69
69
|
"[aria-label='Bình luận'], [aria-label='Comment']",
|
|
70
70
|
"[aria-label*='Để lại bình luận'], [aria-label*='Leave a comment']",
|
|
71
|
+
// Reel có thanh hành động riêng, nhãn khác hai dạng trên.
|
|
72
|
+
"[aria-label*='Xem bình luận'], [aria-label*='View comments']",
|
|
71
73
|
];
|
|
72
74
|
|
|
75
|
+
// Khung bình luận của reel mở bằng animation. Máy đang chạy nhiều lượt đăng
|
|
76
|
+
// song song thì vẽ chậm hẳn — chờ rộng tay, vì hỏng cả lượt tốn hơn nhiều so
|
|
77
|
+
// với đứng thêm mấy giây.
|
|
78
|
+
const OPEN_WAIT_MS = 12_000;
|
|
79
|
+
|
|
73
80
|
function sel(list) { return list.join(', '); }
|
|
74
81
|
|
|
75
82
|
// ─── tìm ô soạn ─────────────────────────────────────────────────────────────
|
|
@@ -93,16 +100,48 @@ async function findCommentBox(page) {
|
|
|
93
100
|
return null;
|
|
94
101
|
}
|
|
95
102
|
|
|
103
|
+
// Bấm xong PHẢI chờ tới khi ô soạn thật sự dựng, đừng ngủ cứng rồi đi tiếp.
|
|
104
|
+
//
|
|
105
|
+
// Bản đầu bấm nút rồi `pause(1800)` và trả về ngay. Khung bình luận của reel
|
|
106
|
+
// mở bằng animation, nên 1,8s chỉ đủ khi máy rảnh; 21/09 hai lượt trượt sạch
|
|
107
|
+
// đúng lúc máy đang chạy 5 lượt đăng song song, log dừng ở "bấm mở khung
|
|
108
|
+
// bình luận" rồi báo "không tìm thấy ô soạn (đã thử 7 nhãn)" — đọc lên tưởng
|
|
109
|
+
// bài tắt bình luận, thật ra khung chưa kịp vẽ. Các lượt thành công cùng ngày
|
|
110
|
+
// đều là bài có sẵn ô soạn, không qua nhánh này lần nào.
|
|
111
|
+
//
|
|
112
|
+
// Chờ tối đa OPEN_WAIT_MS và thử NÚT KHÁC nếu nút đầu không dựng được khung —
|
|
113
|
+
// trên reel có nhiều thứ mang nhãn "Bình luận" (nút ở thanh hành động, nút
|
|
114
|
+
// dưới bài), bấm trúng cái không mở panel là hỏng cả lượt.
|
|
96
115
|
async function openCommentsIfNeeded(page, log) {
|
|
116
|
+
let clicked = false;
|
|
97
117
|
for (const s of OPEN_COMMENTS_SELECTORS) {
|
|
98
118
|
const loc = page.locator(s).first();
|
|
99
119
|
if (!(await loc.isVisible().catch(() => false))) continue;
|
|
100
120
|
log('info', `[fb-cmt] chưa thấy ô soạn — bấm mở khung bình luận ("${s}")`);
|
|
101
121
|
await humanClick(page, loc).catch(() => {});
|
|
102
|
-
|
|
103
|
-
|
|
122
|
+
clicked = true;
|
|
123
|
+
const box = await waitForCommentBox(page, OPEN_WAIT_MS);
|
|
124
|
+
if (box) {
|
|
125
|
+
log('info', '[fb-cmt] khung bình luận đã dựng');
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
log('warn', `[fb-cmt] bấm "${s}" mà khung chưa dựng sau ${Math.round(OPEN_WAIT_MS / 1000)}s — thử nút khác`);
|
|
129
|
+
}
|
|
130
|
+
return clicked;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Dò lại ô soạn theo nhịp cho tới khi thấy hoặc hết giờ. Dùng vòng lặp chứ
|
|
134
|
+
// không `waitForSelector` vì ô soạn phải qua được bộ lọc của findCommentBox
|
|
135
|
+
// (ô soạn BÀI trên tường Page cũng khớp selector — gõ nhầm vào đó là đăng một
|
|
136
|
+
// bài mới toàn link).
|
|
137
|
+
async function waitForCommentBox(page, timeoutMs) {
|
|
138
|
+
const deadline = Date.now() + timeoutMs;
|
|
139
|
+
for (;;) {
|
|
140
|
+
const box = await findCommentBox(page);
|
|
141
|
+
if (box) return box;
|
|
142
|
+
if (Date.now() >= deadline) return null;
|
|
143
|
+
await pause(page, 700);
|
|
104
144
|
}
|
|
105
|
-
return false;
|
|
106
145
|
}
|
|
107
146
|
|
|
108
147
|
// ─── đọc lại để xác minh ────────────────────────────────────────────────────
|
|
@@ -261,13 +300,23 @@ async function run({ page, payload = {}, log }) {
|
|
|
261
300
|
if (gone) throw new Error(`Bài không mở được (đã xoá, đổi quyền riêng tư, hoặc link sai): ${postUrl}`);
|
|
262
301
|
|
|
263
302
|
let box = await findCommentBox(page);
|
|
303
|
+
// Chưa thấy ngay thì chờ một nhịp trước khi đi bấm nút: trang vừa mở, khối
|
|
304
|
+
// bình luận có khi còn đang vẽ.
|
|
305
|
+
if (!box) box = await waitForCommentBox(page, 4000);
|
|
306
|
+
let opened = false;
|
|
264
307
|
if (!box) {
|
|
265
|
-
await openCommentsIfNeeded(page, log);
|
|
308
|
+
opened = await openCommentsIfNeeded(page, log);
|
|
266
309
|
box = await findCommentBox(page);
|
|
267
310
|
}
|
|
268
311
|
if (!box) {
|
|
269
312
|
await assertAccountUsable(page, log);
|
|
270
|
-
|
|
313
|
+
// Nói đúng bệnh. Đã bấm được nút mở khung mà vẫn không có ô soạn thì bài
|
|
314
|
+
// KHÔNG tắt bình luận — nút đó chỉ hiện khi còn bình luận được; bệnh là
|
|
315
|
+
// khung không dựng kịp (máy bận) hoặc Facebook đổi cách dựng panel. Gộp
|
|
316
|
+
// hai chuyện vào một câu là lần sau đọc log đi sai hướng ngay từ đầu.
|
|
317
|
+
throw new Error(opened
|
|
318
|
+
? `Bấm mở khung bình luận rồi mà không có ô soạn sau ${Math.round(OPEN_WAIT_MS / 1000)}s trên ${postUrl} — máy đang bận vẽ chậm, hoặc Facebook đổi cách dựng khung bình luận của reel`
|
|
319
|
+
: `Không tìm thấy ô soạn bình luận trên ${postUrl} (đã thử ${BOX_SELECTORS.length} nhãn, cũng không thấy nút mở khung) — nhiều khả năng bài tắt bình luận hoặc Facebook đổi giao diện`);
|
|
271
320
|
}
|
|
272
321
|
|
|
273
322
|
// Đo NỀN trước khi gõ: link có thể đã nằm sẵn trong caption, hoặc bài đã
|
|
@@ -329,4 +378,7 @@ async function run({ page, payload = {}, log }) {
|
|
|
329
378
|
}
|
|
330
379
|
|
|
331
380
|
module.exports = { run };
|
|
332
|
-
module.exports.__testables = {
|
|
381
|
+
module.exports.__testables = {
|
|
382
|
+
findPostedComment, countLinkOnPage, waitForPostedComment, findCommentBox, BOX_SELECTORS, sel,
|
|
383
|
+
openCommentsIfNeeded, waitForCommentBox, OPEN_COMMENTS_SELECTORS, OPEN_WAIT_MS,
|
|
384
|
+
};
|
|
@@ -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) => {
|
|
@@ -880,7 +932,7 @@ async function discoverReelsTabUrl(page, log = null) {
|
|
|
880
932
|
// một link trông giống", nên lượt lấy link sau khi đăng ưu tiên nó.
|
|
881
933
|
// Page thật sự chưa có reel nào cũng ra verified=false; khi đó ids rỗng là
|
|
882
934
|
// đúng, không phải hỏng.
|
|
883
|
-
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 } = {}) {
|
|
884
936
|
const list = [...new Set((urls || []).filter(Boolean))];
|
|
885
937
|
let lastUrl = null;
|
|
886
938
|
for (let i = 0; i < list.length; i++) {
|
|
@@ -888,13 +940,70 @@ async function openReelsTabWithTiles(page, urls, { log = null, settleMs = 4000,
|
|
|
888
940
|
lastUrl = url;
|
|
889
941
|
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 45_000 });
|
|
890
942
|
if (wait) await wait(page, settleMs); else await page.waitForTimeout(settleMs);
|
|
891
|
-
|
|
892
|
-
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
|
+
}
|
|
893
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})`);
|
|
894
949
|
}
|
|
895
950
|
return { url: lastUrl, ids: [], verified: false, tried: list.length };
|
|
896
951
|
}
|
|
897
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
|
+
|
|
898
1007
|
// Mọi reel ID đang hiện trên tab Reels, theo thứ tự DOM (mới nhất trước).
|
|
899
1008
|
async function scrapeReelTileIds(page) {
|
|
900
1009
|
return page.evaluate(() => {
|
|
@@ -944,9 +1053,18 @@ function pickPostPublishReelsTabUrl({ discovered, remembered, rememberedVerified
|
|
|
944
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
|
|
945
1054
|
// thành "mập mờ". Ảnh nền tab Reels chụp trước khi mở composer là bằng chứng
|
|
946
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.
|
|
947
1064
|
function networkIdVerdict({ id, beforeIds, tabIds }) {
|
|
948
1065
|
if (!id) return 'none';
|
|
949
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
|
|
950
1068
|
if ((tabIds || []).includes(id)) return 'confirmed'; // của page mình và mới
|
|
951
1069
|
return 'candidate'; // chưa thấy trên tab, chờ
|
|
952
1070
|
}
|
|
@@ -1074,7 +1192,7 @@ async function runOnce({ page, payload, log }) {
|
|
|
1074
1192
|
} = payload || {};
|
|
1075
1193
|
if (!video_url) throw new Error('No video_url provided');
|
|
1076
1194
|
|
|
1077
|
-
log('info', '[fb-pw] selectors version=2026.09.14-
|
|
1195
|
+
log('info', '[fb-pw] selectors version=2026.09.14-deep-baseline');
|
|
1078
1196
|
|
|
1079
1197
|
page.on('dialog', (d) => { d.accept().catch(() => {}); });
|
|
1080
1198
|
|
|
@@ -1198,7 +1316,7 @@ async function runOnce({ page, payload, log }) {
|
|
|
1198
1316
|
try {
|
|
1199
1317
|
const candidates = await discoverReelsTabUrls(page, log);
|
|
1200
1318
|
if (candidates.length) {
|
|
1201
|
-
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 });
|
|
1202
1320
|
reelsTabUrlBefore = opened.url;
|
|
1203
1321
|
reelsTabVerifiedBefore = opened.verified;
|
|
1204
1322
|
reelIdsBefore = opened.ids;
|
|
@@ -1465,34 +1583,7 @@ async function runOnce({ page, payload, log }) {
|
|
|
1465
1583
|
// small clips, too short for big ones), POLL for an ENABLED "Tiếp" —
|
|
1466
1584
|
// proceed the instant it's ready, but give large clips up to 120s.
|
|
1467
1585
|
log('info', '[fb-pw] waiting for FB to finish processing the video…');
|
|
1468
|
-
|
|
1469
|
-
const procDeadline = procT0 + 120_000;
|
|
1470
|
-
await pause(page, 2500); // let the upload register first
|
|
1471
|
-
let procReady = false;
|
|
1472
|
-
while (Date.now() < procDeadline) {
|
|
1473
|
-
procReady = await page.evaluate(() => {
|
|
1474
|
-
const dlgs = document.querySelectorAll("[role='dialog']");
|
|
1475
|
-
const roots = dlgs.length ? Array.from(dlgs) : [document];
|
|
1476
|
-
const verbs = ['Tiếp', 'Tiếp theo', 'Next', 'Continue'];
|
|
1477
|
-
for (const root of roots) {
|
|
1478
|
-
for (const el of root.querySelectorAll("button, [role='button']")) {
|
|
1479
|
-
const t = (el.innerText || '').trim();
|
|
1480
|
-
if (!verbs.includes(t)) continue;
|
|
1481
|
-
const r = el.getBoundingClientRect();
|
|
1482
|
-
if (r.width < 8 || r.height < 8) continue;
|
|
1483
|
-
const cs = getComputedStyle(el);
|
|
1484
|
-
if (cs.visibility === 'hidden' || cs.display === 'none') continue;
|
|
1485
|
-
if (el.getAttribute('aria-disabled') === 'true' || el.disabled) continue;
|
|
1486
|
-
return true; // an enabled Tiếp/Next → processing done, can advance
|
|
1487
|
-
}
|
|
1488
|
-
}
|
|
1489
|
-
return false;
|
|
1490
|
-
}).catch(() => false);
|
|
1491
|
-
if (procReady) break;
|
|
1492
|
-
await pause(page, 2500);
|
|
1493
|
-
}
|
|
1494
|
-
if (procReady) log('info', `[fb-pw] video processed in ~${Math.round((Date.now() - procT0) / 1000)}s — caption step ready`);
|
|
1495
|
-
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' });
|
|
1496
1587
|
|
|
1497
1588
|
// 5) Fill metadata. FB Reels has THREE separate fields (verified visually
|
|
1498
1589
|
// by user — different from how YT lumps everything into title + desc):
|
|
@@ -2455,10 +2546,12 @@ async function runOnce({ page, payload, log }) {
|
|
|
2455
2546
|
await chooser.setFiles(thumbPath);
|
|
2456
2547
|
uploaded = true;
|
|
2457
2548
|
log('info', `[fb-pw] custom thumbnail set via <a> "${sel}"`);
|
|
2458
|
-
// FB
|
|
2459
|
-
//
|
|
2460
|
-
//
|
|
2461
|
-
|
|
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' });
|
|
2462
2555
|
break;
|
|
2463
2556
|
} catch (e1) {
|
|
2464
2557
|
log('info', `[fb-pw] <a> click via "${sel}" failed (normal): ${e1.message.slice(0, 80)}`);
|
|
@@ -3378,11 +3471,27 @@ async function runOnce({ page, payload, log }) {
|
|
|
3378
3471
|
// re-scrape cứu được phần lớn ca đó, vẫn giữ luật fresh-timestamp
|
|
3379
3472
|
// nên không thể nhận nhầm tile cũ.
|
|
3380
3473
|
if (!postUrl && reelsTabUrl) {
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
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
|
+
}
|
|
3386
3495
|
if (fresh2) {
|
|
3387
3496
|
postUrl = fresh2.href.startsWith('http') ? fresh2.href : `https://www.facebook.com${fresh2.href}`;
|
|
3388
3497
|
log('info', `[fb-pw] post URL from reels_tab RETRY (id=${fresh2.id}, timestamp="${fresh2.timestamp}"): ${postUrl}`);
|
|
@@ -3394,7 +3503,7 @@ async function runOnce({ page, payload, log }) {
|
|
|
3394
3503
|
const diff2 = diffNewReelIds(reelIdsBefore, after2);
|
|
3395
3504
|
if (diff2.length === 1) {
|
|
3396
3505
|
postUrl = `https://www.facebook.com/reel/${diff2[0]}/`;
|
|
3397
|
-
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}`);
|
|
3398
3507
|
} else {
|
|
3399
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})`);
|
|
3400
3509
|
}
|
|
@@ -3534,6 +3643,9 @@ module.exports = { run };
|
|
|
3534
3643
|
// Exposed so the overlay-click escalation can be exercised against a synthetic
|
|
3535
3644
|
// page (toast covering the CTA) without driving the whole upload flow.
|
|
3536
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,
|
|
3537
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
|
|
3538
3650
|
// được biến thành "chưa áp" rồi kéo theo hành động PHÁ (Hủy = gỡ ảnh bìa).
|
|
3539
3651
|
thumbVerdict,
|
|
@@ -3549,6 +3661,8 @@ module.exports.__testables = {
|
|
|
3549
3661
|
discoverReelsTabUrls,
|
|
3550
3662
|
openReelsTabWithTiles,
|
|
3551
3663
|
networkIdVerdict,
|
|
3664
|
+
shouldKeepScrollingReels,
|
|
3665
|
+
scrollReelsTabForMoreTiles,
|
|
3552
3666
|
classifyComposerText,
|
|
3553
3667
|
readPageAlerts,
|
|
3554
3668
|
composerClosedError,
|