channel-worker 2.5.56 → 2.5.57
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/command-poller.js +38 -2
- package/lib/nst-manager.js +25 -29
- package/package.json +1 -1
- package/scripts/lib/dom-pick.js +53 -0
- package/scripts/lib/human.js +147 -0
- package/scripts/nurture_facebook.js +222 -110
- package/scripts/upload_facebook.js +93 -66
- package/scripts/upload_facebook_photo.js +19 -17
- package/scripts/warmup_facebook.js +58 -65
- package/scripts/warmup_youtube.js +60 -19
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
// surfaces the exact problem.
|
|
17
17
|
|
|
18
18
|
const fs = require('fs');
|
|
19
|
+
const { humanMove, humanClick, humanType, humanWheel, pause, randInt: hRandInt, chance: hChance } = require('./lib/human');
|
|
19
20
|
const path = require('path');
|
|
20
21
|
const { downloadToTemp, safeUnlink } = require('./lib/download');
|
|
21
22
|
|
|
@@ -42,7 +43,7 @@ async function waitAndClick(loc, { timeoutMs = 60_000, log, label = 'button', cl
|
|
|
42
43
|
await loc.click(clickOpts || undefined);
|
|
43
44
|
return;
|
|
44
45
|
}
|
|
45
|
-
await loc.page().waitForTimeout(
|
|
46
|
+
await loc.page().waitForTimeout(hRandInt(560, 1120));
|
|
46
47
|
}
|
|
47
48
|
throw new Error(`${label} never became actionable within ${timeoutMs}ms`);
|
|
48
49
|
}
|
|
@@ -110,11 +111,21 @@ async function unmuteClickInterceptors(page) {
|
|
|
110
111
|
// for "the UI already moved on" so we don't fire a second click on top of it.
|
|
111
112
|
async function resilientClick(page, loc, selector, { timeout = 10_000, log, label = 'button', jsFallback = true, attempts = 3, landedCheck = null } = {}) {
|
|
112
113
|
try { await loc.scrollIntoViewIfNeeded({ timeout: 3000 }); } catch {}
|
|
113
|
-
|
|
114
|
+
// Park the cursor in a corner first — travelled there, not teleported to
|
|
115
|
+
// (2,2) — so no hover-card from the feed behind the modal sits on the CTA.
|
|
116
|
+
await humanMove(page, hRandInt(6, 70), hRandInt(6, 70)).catch(() => {});
|
|
114
117
|
let lastErr = null;
|
|
115
118
|
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
116
119
|
try {
|
|
117
|
-
|
|
120
|
+
if (attempt === 0) {
|
|
121
|
+
// First try: human-shaped click (cursor travels, lands off-centre).
|
|
122
|
+
// Visibility is checked first so a not-yet-rendered button falls
|
|
123
|
+
// through to the actionability-checked native click below.
|
|
124
|
+
await loc.waitFor({ state: 'visible', timeout: Math.min(timeout, 5000) });
|
|
125
|
+
await humanClick(page, loc, { timeout, hoverMs: [150, 500] });
|
|
126
|
+
} else {
|
|
127
|
+
await loc.click({ timeout });
|
|
128
|
+
}
|
|
118
129
|
await unmuteClickInterceptors(page);
|
|
119
130
|
return 'native';
|
|
120
131
|
} catch (e) {
|
|
@@ -126,7 +137,7 @@ async function resilientClick(page, loc, selector, { timeout = 10_000, log, labe
|
|
|
126
137
|
}
|
|
127
138
|
const muted = await muteClickInterceptors(page, selector);
|
|
128
139
|
if (log) log('warn', `[fb-pw] ${label} click attempt ${attempt + 1}/${attempts} failed (${e.message.split('\n')[0].slice(0, 70)}) — muted ${muted} overlay layer(s)`);
|
|
129
|
-
await page
|
|
140
|
+
await pause(page, 500);
|
|
130
141
|
}
|
|
131
142
|
}
|
|
132
143
|
if (jsFallback) {
|
|
@@ -275,7 +286,7 @@ async function dismissBsOnboarding(page, log) {
|
|
|
275
286
|
try {
|
|
276
287
|
log('info', `[fb-pw] dismissing BS onboarding via "${sel}" (dlg="${dlgInfo.preview}")`);
|
|
277
288
|
await hit.click({ timeout: 2500 });
|
|
278
|
-
await page
|
|
289
|
+
await pause(page, 700);
|
|
279
290
|
clicked = true;
|
|
280
291
|
break;
|
|
281
292
|
} catch {}
|
|
@@ -389,7 +400,7 @@ async function waitForPublishEnabled(page, verbs, log, timeoutMs, tag = 'fb-pw',
|
|
|
389
400
|
} else if (!sawPresent && ++absent >= 4) {
|
|
390
401
|
return { enabled: false, sawPresent: false }; // no publish CTA after ~10s → not the final step
|
|
391
402
|
}
|
|
392
|
-
await page
|
|
403
|
+
await pause(page, 2500);
|
|
393
404
|
}
|
|
394
405
|
log('warn', `[${tag}] "Đăng" never became enabled within ${Math.round(timeoutMs / 1000)}s`);
|
|
395
406
|
return { enabled: false, sawPresent };
|
|
@@ -502,9 +513,31 @@ async function runOnce({ page, payload, log }) {
|
|
|
502
513
|
log('info', '[fb-pw] open https://www.facebook.com/ (page wall flow) …');
|
|
503
514
|
await page.goto('https://www.facebook.com/', { waitUntil: 'domcontentloaded', timeout: 60_000 });
|
|
504
515
|
await page.waitForLoadState('networkidle', { timeout: 30_000 }).catch(() => {});
|
|
505
|
-
await page
|
|
516
|
+
await pause(page, 4000);
|
|
506
517
|
log('info', `[fb-pw] home loaded — url=${page.url()}`);
|
|
507
518
|
|
|
519
|
+
// Look around before posting. The old flow was goto → 4s → straight into
|
|
520
|
+
// "Thước phim", every single run: a session whose only actions are
|
|
521
|
+
// open→post→leave. A person glances at the wall first. 2-5 wheel bursts
|
|
522
|
+
// with reading pauses, then back up to the composer (it sits at the top).
|
|
523
|
+
if (hChance(0.85)) {
|
|
524
|
+
const vp = await page.evaluate(() => ({ w: window.innerWidth, h: window.innerHeight })).catch(() => null);
|
|
525
|
+
if (vp) await humanMove(page, Math.floor(vp.w * (0.35 + Math.random() * 0.3)), Math.floor(vp.h * (0.4 + Math.random() * 0.3)));
|
|
526
|
+
const bursts = hRandInt(2, 5);
|
|
527
|
+
let down = 0;
|
|
528
|
+
for (let i = 0; i < bursts; i++) {
|
|
529
|
+
const dy = hRandInt(300, 800);
|
|
530
|
+
down += await humanWheel(page, dy);
|
|
531
|
+
await page.waitForTimeout(hRandInt(1800, 6000));
|
|
532
|
+
if (hChance(0.2)) { down += await humanWheel(page, -hRandInt(120, 300)); await page.waitForTimeout(hRandInt(800, 2200)); }
|
|
533
|
+
}
|
|
534
|
+
// Back to the top in a few bursts (not one jump).
|
|
535
|
+
while (down > 0) { down += await humanWheel(page, -Math.min(down, hRandInt(500, 1100))); await page.waitForTimeout(hRandInt(250, 700)); }
|
|
536
|
+
await page.evaluate(() => { if (window.scrollY > 0) window.scrollTo({ top: 0, behavior: 'smooth' }); }).catch(() => {});
|
|
537
|
+
await page.waitForTimeout(hRandInt(800, 2000));
|
|
538
|
+
log('info', `[fb-pw] browsed the wall for a bit (${bursts} scrolls) before opening the composer`);
|
|
539
|
+
}
|
|
540
|
+
|
|
508
541
|
// 2) Click the "Thước phim" entry inside the Page's "Tạo bài viết" widget.
|
|
509
542
|
// This opens a FRESH Reel composer modal each time (no stale-draft
|
|
510
543
|
// issue that plagued the BS composer). CRITICAL: scope to the "Tạo
|
|
@@ -525,7 +558,7 @@ async function runOnce({ page, payload, log }) {
|
|
|
525
558
|
const btn = await firstVisible(page.locator(sel), 5);
|
|
526
559
|
if (!btn) continue;
|
|
527
560
|
try {
|
|
528
|
-
await btn
|
|
561
|
+
await humanClick(page, btn, { timeout: 3000 });
|
|
529
562
|
log('info', `[fb-pw] Thước phim clicked via "${sel}"`);
|
|
530
563
|
reelOpened = true;
|
|
531
564
|
break;
|
|
@@ -569,7 +602,7 @@ async function runOnce({ page, payload, log }) {
|
|
|
569
602
|
await dumpFailure(page, 'no-reel-entry', log);
|
|
570
603
|
throw new Error('FB home: "Thước phim" entry not found in "Tạo bài viết" widget (sidebar Reels feed link doesn\'t count — it navigates instead of opening composer)');
|
|
571
604
|
}
|
|
572
|
-
await page
|
|
605
|
+
await pause(page, 4000);
|
|
573
606
|
const afterUrl = page.url();
|
|
574
607
|
log('info', `[fb-pw] after Thước phim click — url=${afterUrl}`);
|
|
575
608
|
// Sanity check: clicking the sidebar Reels link navigates to /reel/<id>.
|
|
@@ -614,7 +647,7 @@ async function runOnce({ page, payload, log }) {
|
|
|
614
647
|
}
|
|
615
648
|
} catch (e) { log('warn', `[fb-pw] fallback Reels-link click failed: ${e.message.slice(0, 80)}`); }
|
|
616
649
|
if (xpClicked) {
|
|
617
|
-
await page
|
|
650
|
+
await pause(page, 5000);
|
|
618
651
|
await dismissBsOnboarding(page, log).catch(() => {});
|
|
619
652
|
}
|
|
620
653
|
if (!(await reelsDialog.isVisible({ timeout: 8000 }).catch(() => false))) {
|
|
@@ -685,7 +718,7 @@ async function runOnce({ page, payload, log }) {
|
|
|
685
718
|
log('info', '[fb-pw] waiting for FB to finish processing the video…');
|
|
686
719
|
const procT0 = Date.now();
|
|
687
720
|
const procDeadline = procT0 + 120_000;
|
|
688
|
-
await page
|
|
721
|
+
await pause(page, 2500); // let the upload register first
|
|
689
722
|
let procReady = false;
|
|
690
723
|
while (Date.now() < procDeadline) {
|
|
691
724
|
procReady = await page.evaluate(() => {
|
|
@@ -707,7 +740,7 @@ async function runOnce({ page, payload, log }) {
|
|
|
707
740
|
return false;
|
|
708
741
|
}).catch(() => false);
|
|
709
742
|
if (procReady) break;
|
|
710
|
-
await page
|
|
743
|
+
await pause(page, 2500);
|
|
711
744
|
}
|
|
712
745
|
if (procReady) log('info', `[fb-pw] video processed in ~${Math.round((Date.now() - procT0) / 1000)}s — caption step ready`);
|
|
713
746
|
else log('warn', '[fb-pw] processing wait hit 120s cap — proceeding (Tiếp click auto-waits anyway)');
|
|
@@ -770,7 +803,7 @@ async function runOnce({ page, payload, log }) {
|
|
|
770
803
|
}).catch(() => false);
|
|
771
804
|
if (variantBDeleted) {
|
|
772
805
|
log('info', '[fb-pw] removed stale empty variant-B title row');
|
|
773
|
-
await page
|
|
806
|
+
await pause(page, 800);
|
|
774
807
|
}
|
|
775
808
|
const titleSels = [
|
|
776
809
|
// PAGE-WALL form 1 — "Bạn đang nghĩ gì?" caption textbox is the
|
|
@@ -799,11 +832,11 @@ async function runOnce({ page, payload, log }) {
|
|
|
799
832
|
try {
|
|
800
833
|
await f.click({ timeout: 3000 });
|
|
801
834
|
const tag = await f.evaluate((el) => el.tagName.toLowerCase()).catch(() => '');
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
}
|
|
835
|
+
// Typed with a human cadence whatever the element is (fill() set
|
|
836
|
+
// the value in one synthetic step). typo:0 — the title must land
|
|
837
|
+
// exactly as written. Focus came from the click above.
|
|
838
|
+
void tag;
|
|
839
|
+
await humanType(page, clippedTitle, { typo: 0 });
|
|
807
840
|
// Blur to commit (FB's React validation re-renders on blur).
|
|
808
841
|
await page.keyboard.press('Tab').catch(() => {});
|
|
809
842
|
fillState.title = true;
|
|
@@ -839,15 +872,16 @@ async function runOnce({ page, payload, log }) {
|
|
|
839
872
|
if (probe) {
|
|
840
873
|
log('info', `[fb-pw] title dynamic-probe found <${probe.tag}> attrs="${probe.attrs.slice(0, 80)}"`);
|
|
841
874
|
try {
|
|
842
|
-
await page
|
|
843
|
-
await page.waitForTimeout(
|
|
875
|
+
await humanMove(page, probe.bbox.x + hRandInt(-8, 8), probe.bbox.y + hRandInt(-4, 4));
|
|
876
|
+
await page.mouse.down(); await page.waitForTimeout(hRandInt(55, 130)); await page.mouse.up();
|
|
877
|
+
await pause(page, 300);
|
|
844
878
|
// FB Reels title cap is 80 chars (BS composer shows red invalid
|
|
845
879
|
// state above that and blocks Tiếp). Be conservative — clip with
|
|
846
880
|
// ellipsis suffix so the AI-generated hook is preserved at front.
|
|
847
881
|
const clipped = String(title).length > 80
|
|
848
882
|
? String(title).slice(0, 79) + '…'
|
|
849
883
|
: String(title);
|
|
850
|
-
await page
|
|
884
|
+
await humanType(page, clipped, { typo: 0 });
|
|
851
885
|
// Tab away to blur — commits the value and triggers React's
|
|
852
886
|
// validation re-render, otherwise the form keeps the invalid
|
|
853
887
|
// state from a previous typing session.
|
|
@@ -894,7 +928,7 @@ async function runOnce({ page, payload, log }) {
|
|
|
894
928
|
await f.click({ timeout: 3000 });
|
|
895
929
|
const descText = (fbCaption || description || title || '').toString().slice(0, 2100);
|
|
896
930
|
if (!descText) break;
|
|
897
|
-
await
|
|
931
|
+
await humanType(page, descText, { typo: 0, base: descText.length > 600 ? 60 : 95 }); // long captions: faster cadence, still keystrokes
|
|
898
932
|
// Blur commits the Lexical/React value and re-runs the form's
|
|
899
933
|
// validation. Without this, the text can be visible while Đăng
|
|
900
934
|
// remains disabled against the previous empty state.
|
|
@@ -939,7 +973,7 @@ async function runOnce({ page, payload, log }) {
|
|
|
939
973
|
const f = page.locator(probe);
|
|
940
974
|
const descText = (fbCaption || description || title || '').toString().slice(0, 2100);
|
|
941
975
|
await f.click({ timeout: 3000 });
|
|
942
|
-
await
|
|
976
|
+
await humanType(page, descText, { typo: 0, base: descText.length > 600 ? 60 : 95 }); // long captions: faster cadence, still keystrokes
|
|
943
977
|
await page.keyboard.press('Tab').catch(() => {});
|
|
944
978
|
fillState.description = true;
|
|
945
979
|
log('info', `[fb-pw] description filled (${descText.length} chars) via dynamic-probe`);
|
|
@@ -1021,16 +1055,12 @@ async function runOnce({ page, payload, log }) {
|
|
|
1021
1055
|
if (tagLoc) log('info', `[fb-pw] tag input located via ${tagLoc.via}${tagLoc.sig ? ` (sig=${tagLoc.sig.slice(0, 60)})` : ''}${tagLoc.label ? ` (label="${tagLoc.label}")` : ''}`);
|
|
1022
1056
|
if (tagLoc) {
|
|
1023
1057
|
try {
|
|
1024
|
-
await page
|
|
1058
|
+
await pause(page, 400);
|
|
1025
1059
|
const f = page.locator(tagLoc.selector);
|
|
1026
1060
|
await f.click({ timeout: 5000 });
|
|
1027
1061
|
const cleaned = tags.map((t) => String(t).replace(/^#+/, '').trim()).filter(Boolean);
|
|
1028
1062
|
const tagsStr = cleaned.join(', ');
|
|
1029
|
-
|
|
1030
|
-
await f.fill(tagsStr);
|
|
1031
|
-
} else {
|
|
1032
|
-
await f.type(tagsStr, { delay: 10 });
|
|
1033
|
-
}
|
|
1063
|
+
await humanType(page, tagsStr, { typo: 0 });
|
|
1034
1064
|
fillState.tags = true;
|
|
1035
1065
|
log('info', `[fb-pw] tags filled (${cleaned.length}: ${tagsStr.slice(0, 60)}…) via scrollIntoView`);
|
|
1036
1066
|
await page.evaluate((sel) => document.querySelectorAll(sel).forEach((el) => el.removeAttribute('__fbpw_tag__')), tagLoc.selector).catch(() => {});
|
|
@@ -1049,11 +1079,8 @@ async function runOnce({ page, payload, log }) {
|
|
|
1049
1079
|
const tagsStr = cleaned.join(', ');
|
|
1050
1080
|
await f.click({ timeout: 3000 });
|
|
1051
1081
|
const tag = await f.evaluate((el) => el.tagName.toLowerCase()).catch(() => '');
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
} else {
|
|
1055
|
-
await f.type(tagsStr, { delay: 10 });
|
|
1056
|
-
}
|
|
1082
|
+
void tag;
|
|
1083
|
+
await humanType(page, tagsStr, { typo: 0 });
|
|
1057
1084
|
fillState.tags = true;
|
|
1058
1085
|
log('info', `[fb-pw] tags filled (${cleaned.length}: ${tagsStr.slice(0, 60)}…) via "${sel}"`);
|
|
1059
1086
|
break;
|
|
@@ -1065,7 +1092,7 @@ async function runOnce({ page, payload, log }) {
|
|
|
1065
1092
|
|
|
1066
1093
|
log('info', '[fb-pw] fill metadata (title/description/tags) — step 1…');
|
|
1067
1094
|
await fillMetadata();
|
|
1068
|
-
await page
|
|
1095
|
+
await pause(page, 800);
|
|
1069
1096
|
|
|
1070
1097
|
// 6) Multi-step "Tiếp" → "Tiếp" → "Đăng" loop. FB Reels composer is 3
|
|
1071
1098
|
// pages: caption (now) → thumbnail (auto-selected, just advance) →
|
|
@@ -1183,7 +1210,7 @@ async function runOnce({ page, payload, log }) {
|
|
|
1183
1210
|
let customThumbDone = false;
|
|
1184
1211
|
let pubWaitDone = false; // guard: wait for a disabled "Đăng" at most once
|
|
1185
1212
|
for (let step = 0; step < 7 && !published; step++) {
|
|
1186
|
-
await page
|
|
1213
|
+
await pause(page, 3000);
|
|
1187
1214
|
await fillMetadata();
|
|
1188
1215
|
|
|
1189
1216
|
// PAGE-WALL thumb flow (form 2 has a "Chỉnh sửa" overlay button on the
|
|
@@ -1244,7 +1271,7 @@ async function runOnce({ page, payload, log }) {
|
|
|
1244
1271
|
if (!editBtn) {
|
|
1245
1272
|
log('info', '[fb-pw] thumb-edit pill not visible on this step — polling up to 18s…');
|
|
1246
1273
|
const tdl = Date.now() + 18_000;
|
|
1247
|
-
while (!editBtn && Date.now() < tdl) { await page
|
|
1274
|
+
while (!editBtn && Date.now() < tdl) { await pause(page, 3000); editBtn = await findThumbBtn(); }
|
|
1248
1275
|
if (editBtn) log('info', '[fb-pw] thumb-edit pill appeared after wait');
|
|
1249
1276
|
}
|
|
1250
1277
|
|
|
@@ -1253,7 +1280,7 @@ async function runOnce({ page, payload, log }) {
|
|
|
1253
1280
|
log('info', `[fb-pw] page-wall thumb — opening "Chỉnh sửa hình thu nhỏ" modal (via=${editBtn.via}, aria="${editBtn.aria}", text="${editBtn.text}"${editBtn.size ? `, size=${editBtn.size}` : ''})…`);
|
|
1254
1281
|
try {
|
|
1255
1282
|
await page.locator(editBtn.selector).click({ timeout: 5000 });
|
|
1256
|
-
await page
|
|
1283
|
+
await pause(page, 2500);
|
|
1257
1284
|
|
|
1258
1285
|
// Wait for the thumb editor to mount. FB serves (A/B, seen
|
|
1259
1286
|
// 2026-08-19) TWO variants and any given account can get either:
|
|
@@ -1302,7 +1329,7 @@ async function runOnce({ page, payload, log }) {
|
|
|
1302
1329
|
let thumbEditorVia = await detectThumbEditor();
|
|
1303
1330
|
const mountDeadline = Date.now() + 12_000;
|
|
1304
1331
|
while (!thumbEditorVia && Date.now() < mountDeadline) {
|
|
1305
|
-
await page
|
|
1332
|
+
await pause(page, 2000);
|
|
1306
1333
|
thumbEditorVia = await detectThumbEditor();
|
|
1307
1334
|
}
|
|
1308
1335
|
if (!thumbEditorVia) throw new Error('thumbnail editor dialog did not mount');
|
|
@@ -1331,7 +1358,7 @@ async function runOnce({ page, payload, log }) {
|
|
|
1331
1358
|
await chooser.setFiles(thumbPath);
|
|
1332
1359
|
log('info', `[fb-pw] page-wall thumb — file set via "${sel}"`);
|
|
1333
1360
|
uploaded = true;
|
|
1334
|
-
await page
|
|
1361
|
+
await pause(page, 3500); // FB processes image
|
|
1335
1362
|
break;
|
|
1336
1363
|
} catch (e) {
|
|
1337
1364
|
log('info', `[fb-pw] "Tải lên" via "${sel}" failed: ${e.message.slice(0, 80)}`);
|
|
@@ -1347,7 +1374,7 @@ async function runOnce({ page, payload, log }) {
|
|
|
1347
1374
|
await directInput.setInputFiles(thumbPath);
|
|
1348
1375
|
log('info', '[fb-pw] page-wall thumb — file set via direct input[type=file]');
|
|
1349
1376
|
uploaded = true;
|
|
1350
|
-
await page
|
|
1377
|
+
await pause(page, 3500);
|
|
1351
1378
|
} catch (e) {
|
|
1352
1379
|
log('warn', `[fb-pw] direct file input failed: ${e.message.slice(0, 80)}`);
|
|
1353
1380
|
}
|
|
@@ -1431,7 +1458,7 @@ async function runOnce({ page, payload, log }) {
|
|
|
1431
1458
|
} catch {}
|
|
1432
1459
|
}
|
|
1433
1460
|
}
|
|
1434
|
-
await page
|
|
1461
|
+
await pause(page, 1000);
|
|
1435
1462
|
}
|
|
1436
1463
|
if (modalClosed) {
|
|
1437
1464
|
// Closing the nested thumbnail editor is NOT enough. In the
|
|
@@ -1447,10 +1474,10 @@ async function runOnce({ page, payload, log }) {
|
|
|
1447
1474
|
} else {
|
|
1448
1475
|
log('warn', '[fb-pw] page-wall thumb — modal still open 60s after Lưu click; trying ESC + click Hủy fallback');
|
|
1449
1476
|
await page.keyboard.press('Escape').catch(() => {});
|
|
1450
|
-
await page
|
|
1477
|
+
await pause(page, 800);
|
|
1451
1478
|
const cancel = await firstVisible(page.locator("[__fbpw_thumb_dialog__='1'] div[role='button']:has-text('Hủy'), [__fbpw_thumb_dialog__='1'] button:has-text('Hủy')"), 2);
|
|
1452
1479
|
if (cancel) await cancel.click({ timeout: 2000 }).catch(() => {});
|
|
1453
|
-
await page
|
|
1480
|
+
await pause(page, 1500);
|
|
1454
1481
|
}
|
|
1455
1482
|
} else {
|
|
1456
1483
|
log('warn', '[fb-pw] page-wall thumb upload OK but "Lưu" click failed — modal may stay open');
|
|
@@ -1459,7 +1486,7 @@ async function runOnce({ page, payload, log }) {
|
|
|
1459
1486
|
log('warn', '[fb-pw] page-wall thumb upload failed — closing modal via Hủy to continue');
|
|
1460
1487
|
const cancel = await firstVisible(page.locator("[__fbpw_thumb_dialog__='1'] div[role='button']:has-text('Hủy'), [__fbpw_thumb_dialog__='1'] button:has-text('Hủy')"), 2);
|
|
1461
1488
|
if (cancel) await cancel.click({ timeout: 2000 }).catch(() => {});
|
|
1462
|
-
await page
|
|
1489
|
+
await pause(page, 1500);
|
|
1463
1490
|
}
|
|
1464
1491
|
} catch (e) {
|
|
1465
1492
|
if (e?.code === SAFE_RETRY_COMPOSER_CLOSED) throw e;
|
|
@@ -1541,7 +1568,7 @@ async function runOnce({ page, payload, log }) {
|
|
|
1541
1568
|
await directImgInputs.nth(i).setInputFiles(thumbPath);
|
|
1542
1569
|
uploaded = true;
|
|
1543
1570
|
log('info', `[fb-pw] custom thumbnail set via direct image input[${i}]`);
|
|
1544
|
-
await page
|
|
1571
|
+
await pause(page, 3000);
|
|
1545
1572
|
} catch (e) { log('info', `[fb-pw] direct img input[${i}] failed: ${e.message.slice(0, 80)}`); }
|
|
1546
1573
|
}
|
|
1547
1574
|
|
|
@@ -1557,7 +1584,7 @@ async function runOnce({ page, payload, log }) {
|
|
|
1557
1584
|
await anyFile.nth(i).setInputFiles(thumbPath);
|
|
1558
1585
|
uploaded = true;
|
|
1559
1586
|
log('info', `[fb-pw] custom thumbnail set via any-file input[${i}]`);
|
|
1560
|
-
await page
|
|
1587
|
+
await pause(page, 3000);
|
|
1561
1588
|
} catch (e) { log('info', `[fb-pw] any-file input[${i}] failed: ${e.message.slice(0, 80)}`); }
|
|
1562
1589
|
}
|
|
1563
1590
|
}
|
|
@@ -1591,7 +1618,7 @@ async function runOnce({ page, payload, log }) {
|
|
|
1591
1618
|
}
|
|
1592
1619
|
}
|
|
1593
1620
|
if (tabClicked) {
|
|
1594
|
-
await page
|
|
1621
|
+
await pause(page, 2500); // let pane render fully
|
|
1595
1622
|
// Step 2: click the actual <a> trigger inside the Upload pane.
|
|
1596
1623
|
// Per user's xpath `.//a[contains(text(), "Upload")]`. VN UI
|
|
1597
1624
|
// shows "Tải hình ảnh lên". Try multiple variants + force-click
|
|
@@ -1639,7 +1666,7 @@ async function runOnce({ page, payload, log }) {
|
|
|
1639
1666
|
await chooser.setFiles(thumbPath);
|
|
1640
1667
|
uploaded = true;
|
|
1641
1668
|
log('info', `[fb-pw] custom thumbnail set via <a> "${sel}" (force)`);
|
|
1642
|
-
await page
|
|
1669
|
+
await pause(page, 3000);
|
|
1643
1670
|
break;
|
|
1644
1671
|
} catch (e2) {
|
|
1645
1672
|
log('info', `[fb-pw] <a> click via "${sel}" failed (force): ${e2.message.slice(0, 80)}`);
|
|
@@ -1656,7 +1683,7 @@ async function runOnce({ page, payload, log }) {
|
|
|
1656
1683
|
await chooser.setFiles(thumbPath);
|
|
1657
1684
|
uploaded = true;
|
|
1658
1685
|
log('info', `[fb-pw] custom thumbnail set via <a> "${sel}" (mouse @ bbox)`);
|
|
1659
|
-
await page
|
|
1686
|
+
await pause(page, 3000);
|
|
1660
1687
|
break;
|
|
1661
1688
|
} catch (e3) {
|
|
1662
1689
|
log('info', `[fb-pw] <a> mouse click via "${sel}" failed: ${e3.message.slice(0, 80)}`);
|
|
@@ -1689,14 +1716,14 @@ async function runOnce({ page, payload, log }) {
|
|
|
1689
1716
|
log('info', '[fb-pw] JS-click on Tải hình ảnh lên fired — polling for file input…');
|
|
1690
1717
|
// Wait + poll for input[type=file] to materialize
|
|
1691
1718
|
for (let i = 0; i < 12 && !uploaded; i++) {
|
|
1692
|
-
await page
|
|
1719
|
+
await pause(page, 500);
|
|
1693
1720
|
const lazy = page.locator("input[type='file']").last();
|
|
1694
1721
|
if (await lazy.count().catch(() => 0) > 0) {
|
|
1695
1722
|
try {
|
|
1696
1723
|
await lazy.setInputFiles(thumbPath);
|
|
1697
1724
|
uploaded = true;
|
|
1698
1725
|
log('info', `[fb-pw] custom thumbnail set via JS-click + lazy input (poll #${i + 1})`);
|
|
1699
|
-
await page
|
|
1726
|
+
await pause(page, 3000);
|
|
1700
1727
|
break;
|
|
1701
1728
|
} catch (e) { /* keep polling */ }
|
|
1702
1729
|
}
|
|
@@ -1731,7 +1758,7 @@ async function runOnce({ page, payload, log }) {
|
|
|
1731
1758
|
await chooser.setFiles(thumbPath);
|
|
1732
1759
|
uploaded = true;
|
|
1733
1760
|
log('info', `[fb-pw] custom thumbnail set via CDP-mouse @ (${Math.round(bbox.x)},${Math.round(bbox.y)})`);
|
|
1734
|
-
await page
|
|
1761
|
+
await pause(page, 3000);
|
|
1735
1762
|
} catch (e) {
|
|
1736
1763
|
log('info', `[fb-pw] CDP-mouse + filechooser failed: ${e.message.slice(0, 80)}`);
|
|
1737
1764
|
}
|
|
@@ -1745,7 +1772,7 @@ async function runOnce({ page, payload, log }) {
|
|
|
1745
1772
|
log('warn', '[fb-pw] custom thumb upload failed — falling back to auto-thumb #1');
|
|
1746
1773
|
const auto1 = await firstVisible(page.locator("[aria-label='Hình thu nhỏ tạo tự động 1'], [aria-label*='Auto-generated thumbnail 1']"), 3);
|
|
1747
1774
|
if (auto1) await auto1.click({ timeout: 3000 }).catch(() => {});
|
|
1748
|
-
await page
|
|
1775
|
+
await pause(page, 1500);
|
|
1749
1776
|
}
|
|
1750
1777
|
customThumbDone = true;
|
|
1751
1778
|
} else {
|
|
@@ -1753,7 +1780,7 @@ async function runOnce({ page, payload, log }) {
|
|
|
1753
1780
|
const auto1 = await firstVisible(page.locator("[aria-label='Hình thu nhỏ tạo tự động 1'], [aria-label*='Auto-generated thumbnail 1']"), 3);
|
|
1754
1781
|
if (auto1) {
|
|
1755
1782
|
await auto1.click({ timeout: 3000 }).catch(() => {});
|
|
1756
|
-
await page
|
|
1783
|
+
await pause(page, 1500);
|
|
1757
1784
|
}
|
|
1758
1785
|
customThumbDone = true;
|
|
1759
1786
|
}
|
|
@@ -1796,7 +1823,7 @@ async function runOnce({ page, payload, log }) {
|
|
|
1796
1823
|
log('info', `[fb-pw] pre-publish dismissing overlay via "${dh.verb}"`);
|
|
1797
1824
|
try {
|
|
1798
1825
|
await page.locator(dh.selector).click({ timeout: 3000 });
|
|
1799
|
-
await page
|
|
1826
|
+
await pause(page, 1500);
|
|
1800
1827
|
await page.evaluate(() => document.querySelectorAll("[__fbpw_predismiss__]").forEach((el) => el.removeAttribute('__fbpw_predismiss__'))).catch(() => {});
|
|
1801
1828
|
} catch (e) {
|
|
1802
1829
|
log('warn', `[fb-pw] pre-dismiss click failed: ${e.message.slice(0, 80)}`);
|
|
@@ -1871,7 +1898,7 @@ async function runOnce({ page, payload, log }) {
|
|
|
1871
1898
|
const target = attempt === 0 ? pub : await findByVerbs(publishVerbs, { requireBottomHalf: true });
|
|
1872
1899
|
if (!target) {
|
|
1873
1900
|
if (attemptedClick) { clickedPublish = true; log('info', '[fb-pw] publish button gone after click → post accepted'); break; }
|
|
1874
|
-
await page
|
|
1901
|
+
await pause(page, 1500); continue;
|
|
1875
1902
|
}
|
|
1876
1903
|
attemptedClick = true;
|
|
1877
1904
|
try {
|
|
@@ -1900,7 +1927,7 @@ async function runOnce({ page, payload, log }) {
|
|
|
1900
1927
|
}).catch(() => false);
|
|
1901
1928
|
if (js) { clickedPublish = true; log('info', '[fb-pw] publish via JS-dispatch fallback'); }
|
|
1902
1929
|
}
|
|
1903
|
-
if (!clickedPublish) await page
|
|
1930
|
+
if (!clickedPublish) await pause(page, 2000);
|
|
1904
1931
|
}
|
|
1905
1932
|
}
|
|
1906
1933
|
if (!clickedPublish) throw new Error(`publish button not clickable after 3 attempts`);
|
|
@@ -1938,7 +1965,7 @@ async function runOnce({ page, payload, log }) {
|
|
|
1938
1965
|
}
|
|
1939
1966
|
return null;
|
|
1940
1967
|
}, confirmVerbs).catch(() => null);
|
|
1941
|
-
if (!confirmHit) await page
|
|
1968
|
+
if (!confirmHit) await pause(page, 600);
|
|
1942
1969
|
}
|
|
1943
1970
|
if (confirmHit) {
|
|
1944
1971
|
log('info', `[fb-pw] confirming publish via dialog button "${confirmHit.verb}"`);
|
|
@@ -1955,7 +1982,7 @@ async function runOnce({ page, payload, log }) {
|
|
|
1955
1982
|
label: `confirm "${confirmHit.verb}"`,
|
|
1956
1983
|
attempts: 2,
|
|
1957
1984
|
}).catch((e) => log('warn', `[fb-pw] confirm click failed: ${e.message.split('\n')[0].slice(0, 80)}`));
|
|
1958
|
-
await page
|
|
1985
|
+
await pause(page, 3000);
|
|
1959
1986
|
} else {
|
|
1960
1987
|
log('info', '[fb-pw] no confirmation dialog detected after 8s — assuming direct publish');
|
|
1961
1988
|
}
|
|
@@ -1998,7 +2025,7 @@ async function runOnce({ page, payload, log }) {
|
|
|
1998
2025
|
log('info', `[fb-pw] dismissing post-publish prompt via "${dismissHit.verb}"`);
|
|
1999
2026
|
try {
|
|
2000
2027
|
await page.locator(dismissHit.selector).click({ timeout: 3000 });
|
|
2001
|
-
await page
|
|
2028
|
+
await pause(page, 2000);
|
|
2002
2029
|
await page.evaluate(() => document.querySelectorAll("[__fbpw_dismiss__]").forEach((el) => el.removeAttribute('__fbpw_dismiss__'))).catch(() => {});
|
|
2003
2030
|
} catch (e) {
|
|
2004
2031
|
log('warn', `[fb-pw] dismiss click failed: ${e.message.slice(0, 80)}`);
|
|
@@ -2109,7 +2136,7 @@ async function runOnce({ page, payload, log }) {
|
|
|
2109
2136
|
if (jumpBbox) {
|
|
2110
2137
|
await page.mouse.click(jumpBbox.x, jumpBbox.y);
|
|
2111
2138
|
log('info', `[fb-pw] jumped to Chia sẻ step @ (${Math.round(jumpBbox.x)},${Math.round(jumpBbox.y)})`);
|
|
2112
|
-
await page
|
|
2139
|
+
await pause(page, 3000);
|
|
2113
2140
|
}
|
|
2114
2141
|
}
|
|
2115
2142
|
}
|
|
@@ -2202,7 +2229,7 @@ async function runOnce({ page, payload, log }) {
|
|
|
2202
2229
|
await page.locator("[__fbpw_recommit__='1']").click({ timeout: 5000 }).catch(() => {});
|
|
2203
2230
|
await page.evaluate(() => document.querySelectorAll("[__fbpw_recommit__]").forEach((el) => el.removeAttribute('__fbpw_recommit__'))).catch(() => {});
|
|
2204
2231
|
}
|
|
2205
|
-
await page
|
|
2232
|
+
await pause(page, 8000);
|
|
2206
2233
|
}
|
|
2207
2234
|
if (await composerStillOpen()) {
|
|
2208
2235
|
await dumpInventory(page, log, 'composer-stuck-open');
|
|
@@ -2299,7 +2326,7 @@ async function runOnce({ page, payload, log }) {
|
|
|
2299
2326
|
reelsTabUrl = reelsTabHref;
|
|
2300
2327
|
log('info', `[fb-pw] navigating to page reels tab: ${reelsTabHref.slice(0, 100)}`);
|
|
2301
2328
|
await page.goto(reelsTabHref, { waitUntil: 'domcontentloaded', timeout: 30_000 }).catch(() => {});
|
|
2302
|
-
await page
|
|
2329
|
+
await pause(page, 5000);
|
|
2303
2330
|
// Reel tiles on the reels tab — each is an <a href="/reel/<id>/">
|
|
2304
2331
|
// wrapping a thumbnail + meta, DOM order = newest first. Only a tile
|
|
2305
2332
|
// with a FRESH timestamp counts (see scrapeFreshReelTile).
|
|
@@ -2435,7 +2462,7 @@ async function runOnce({ page, payload, log }) {
|
|
|
2435
2462
|
log('info', '[fb-pw] no post URL yet — waiting 75s for FB to surface the new reel on reels_tab, then re-scraping…');
|
|
2436
2463
|
await page.waitForTimeout(75_000);
|
|
2437
2464
|
await page.goto(reelsTabUrl, { waitUntil: 'domcontentloaded', timeout: 30_000 }).catch(() => {});
|
|
2438
|
-
await page
|
|
2465
|
+
await pause(page, 5000);
|
|
2439
2466
|
const fresh2 = await scrapeFreshReelTile();
|
|
2440
2467
|
if (fresh2) {
|
|
2441
2468
|
postUrl = fresh2.href.startsWith('http') ? fresh2.href : `https://www.facebook.com${fresh2.href}`;
|
|
@@ -2557,7 +2584,7 @@ async function run(args) {
|
|
|
2557
2584
|
// opens a fresh composer. The first attempt's finally has already removed
|
|
2558
2585
|
// its downloaded temp files; runOnce downloads clean copies on retry.
|
|
2559
2586
|
await page.goto('about:blank', { waitUntil: 'domcontentloaded', timeout: 15_000 }).catch(() => {});
|
|
2560
|
-
await page
|
|
2587
|
+
await pause(page, 1200);
|
|
2561
2588
|
}
|
|
2562
2589
|
}
|
|
2563
2590
|
throw new Error('FB upload exhausted safe retries');
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
const fs = require('fs');
|
|
11
11
|
const path = require('path');
|
|
12
12
|
const os = require('os');
|
|
13
|
+
const { humanType, pause, randInt: hRandInt } = require('./lib/human');
|
|
13
14
|
const { downloadToTemp, safeUnlink } = require('./lib/download');
|
|
14
15
|
|
|
15
16
|
async function firstVisible(locator, max = 8) {
|
|
@@ -36,7 +37,7 @@ async function waitAndClick(el, { timeoutMs = 60_000, log, label = 'button' } =
|
|
|
36
37
|
try { await el.click({ timeout: 3000 }); return; }
|
|
37
38
|
catch { /* retry */ }
|
|
38
39
|
}
|
|
39
|
-
await el.page().waitForTimeout(
|
|
40
|
+
await el.page().waitForTimeout(hRandInt(350, 700));
|
|
40
41
|
}
|
|
41
42
|
throw new Error(`${label} never became actionable within ${timeoutMs}ms`);
|
|
42
43
|
}
|
|
@@ -146,7 +147,7 @@ async function run({ page, payload, log }) {
|
|
|
146
147
|
log('info', '[fbphoto] navigating to facebook.com…');
|
|
147
148
|
await page.goto('https://www.facebook.com/', { waitUntil: 'domcontentloaded', timeout: 60_000 });
|
|
148
149
|
await page.waitForLoadState('networkidle', { timeout: 30_000 }).catch(() => {});
|
|
149
|
-
await page
|
|
150
|
+
await pause(page, 2500);
|
|
150
151
|
|
|
151
152
|
// 2) Open the "Tạo bài viết" composer via the "Ảnh/video" entry (this opens
|
|
152
153
|
// the post composer directly in photo-attach mode). Fall back to the
|
|
@@ -172,14 +173,14 @@ async function run({ page, payload, log }) {
|
|
|
172
173
|
}
|
|
173
174
|
// Fallback: click the status box to open the composer, then click Ảnh/video inside.
|
|
174
175
|
const box = await firstVisible(page.locator("div[role='button']:has-text('Bạn đang nghĩ gì'), div[role='button']:has-text(\"What's on your mind\")"), 3);
|
|
175
|
-
if (box) { await box.click({ timeout: 4000 }).catch(() => {}); await page
|
|
176
|
+
if (box) { await box.click({ timeout: 4000 }).catch(() => {}); await pause(page, 1500); return true; }
|
|
176
177
|
return false;
|
|
177
178
|
};
|
|
178
179
|
if (!(await openPhotoComposer())) {
|
|
179
180
|
await dumpFailure(page, 'no-composer-entry', log);
|
|
180
181
|
throw new Error('FB: không tìm thấy nút "Ảnh/video" / ô "Tạo bài viết" trên trang. Kiểm tra layout Page.');
|
|
181
182
|
}
|
|
182
|
-
await page
|
|
183
|
+
await pause(page, 2500);
|
|
183
184
|
|
|
184
185
|
// 3) Ensure the composer modal is open; click its "Ảnh/video" if the file
|
|
185
186
|
// input isn't ready yet.
|
|
@@ -197,12 +198,12 @@ async function run({ page, payload, log }) {
|
|
|
197
198
|
if (inModalPhoto) {
|
|
198
199
|
await page.locator("[__fbphoto_add__='1']").click({ timeout: 4000 }).catch(() => {});
|
|
199
200
|
await page.evaluate(() => document.querySelectorAll('[__fbphoto_add__]').forEach((e) => e.removeAttribute('__fbphoto_add__'))).catch(() => {});
|
|
200
|
-
await page
|
|
201
|
+
await pause(page, 1500);
|
|
201
202
|
}
|
|
202
203
|
// Wait for the file input to appear.
|
|
203
204
|
for (let i = 0; i < 10 && !fileInput; i++) {
|
|
204
205
|
fileInput = await page.$("input[type='file'][accept*='image']");
|
|
205
|
-
if (!fileInput) await page
|
|
206
|
+
if (!fileInput) await pause(page, 1000);
|
|
206
207
|
}
|
|
207
208
|
}
|
|
208
209
|
if (!fileInput) {
|
|
@@ -214,7 +215,7 @@ async function run({ page, payload, log }) {
|
|
|
214
215
|
log('info', '[fbphoto] attaching image…');
|
|
215
216
|
await setImageFile(page, fileInput, imagePath, log);
|
|
216
217
|
// Wait for the image preview to render inside the composer.
|
|
217
|
-
await page
|
|
218
|
+
await pause(page, 4000);
|
|
218
219
|
|
|
219
220
|
// 5) Fill caption into the composer's contenteditable textbox.
|
|
220
221
|
log('info', '[fbphoto] filling caption…');
|
|
@@ -224,12 +225,13 @@ async function run({ page, payload, log }) {
|
|
|
224
225
|
throw new Error('FB: không tìm thấy ô nhập caption.');
|
|
225
226
|
}
|
|
226
227
|
await editable.click({ timeout: 4000 }).catch(() => {});
|
|
227
|
-
await page
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
228
|
+
await pause(page, 400);
|
|
229
|
+
// Typed, not pasted: insertText dropped the whole caption in one
|
|
230
|
+
// synthetic step with zero keystrokes. typo:0 — caption must be exact.
|
|
231
|
+
await humanType(page, fullCaption, { typo: 0, base: fullCaption.length > 600 ? 60 : 95 }).catch(async () => {
|
|
232
|
+
await editable.type(fullCaption, { delay: 40 }).catch(() => {});
|
|
231
233
|
});
|
|
232
|
-
await page
|
|
234
|
+
await pause(page, 1000);
|
|
233
235
|
|
|
234
236
|
// 6) Click "Đăng".
|
|
235
237
|
log('info', '[fbphoto] clicking publish…');
|
|
@@ -281,7 +283,7 @@ async function run({ page, payload, log }) {
|
|
|
281
283
|
const pubHit = await tagCta(publishVerbs);
|
|
282
284
|
if (pubHit) { log('info', `[fbphoto] publish via "${pubHit}" (step ${step + 1})`); await clickTaggedCta(); published = true; break; }
|
|
283
285
|
const nextHit = await tagCta(nextVerbs);
|
|
284
|
-
if (nextHit) { log('info', `[fbphoto] advance via "${nextHit}" (step ${step + 1})`); await clickTaggedCta(); await page
|
|
286
|
+
if (nextHit) { log('info', `[fbphoto] advance via "${nextHit}" (step ${step + 1})`); await clickTaggedCta(); await pause(page, 2800); stalls = 0; continue; }
|
|
285
287
|
// Neither publish nor Tiếp enabled — FB still ingesting the photo. Wait.
|
|
286
288
|
if (++stalls > 10) break; // ~30s of no CTA → give up
|
|
287
289
|
if (stalls === 1 || stalls === 6) {
|
|
@@ -301,7 +303,7 @@ async function run({ page, payload, log }) {
|
|
|
301
303
|
}
|
|
302
304
|
log('info', `[fbphoto] no CTA yet at step ${step + 1} — waiting…`);
|
|
303
305
|
step--; // don't count a pure wait against the step budget
|
|
304
|
-
await page
|
|
306
|
+
await pause(page, 3000);
|
|
305
307
|
}
|
|
306
308
|
if (!published) {
|
|
307
309
|
await dumpFailure(page, 'no-publish-btn', log);
|
|
@@ -309,11 +311,11 @@ async function run({ page, payload, log }) {
|
|
|
309
311
|
}
|
|
310
312
|
|
|
311
313
|
// Verify commit + retry — a click can register without FB posting.
|
|
312
|
-
await page
|
|
314
|
+
await pause(page, 6000);
|
|
313
315
|
for (let commitTry = 0; commitTry < 3 && (await composerStillOpen(page, publishVerbs)); commitTry++) {
|
|
314
316
|
log('warn', `[fbphoto] composer still open — re-clicking publish (retry ${commitTry + 1}/3)`);
|
|
315
317
|
if (await tagCta(publishVerbs)) await clickTaggedCta();
|
|
316
|
-
await page
|
|
318
|
+
await pause(page, 6000);
|
|
317
319
|
}
|
|
318
320
|
if (await composerStillOpen(page, publishVerbs)) {
|
|
319
321
|
await dumpFailure(page, 'composer-stuck-open', log);
|
|
@@ -321,7 +323,7 @@ async function run({ page, payload, log }) {
|
|
|
321
323
|
}
|
|
322
324
|
|
|
323
325
|
// 8) Best-effort post URL.
|
|
324
|
-
await page
|
|
326
|
+
await pause(page, 4000);
|
|
325
327
|
let postUrl = '';
|
|
326
328
|
if (capturedPostIds.length) {
|
|
327
329
|
postUrl = `https://www.facebook.com/${capturedPostIds[capturedPostIds.length - 1]}`;
|