channel-worker 2.5.94 → 2.5.96
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 +11 -0
- package/package.json +1 -1
- package/scripts/upload_facebook.js +148 -36
package/lib/command-poller.js
CHANGED
|
@@ -397,6 +397,16 @@ class CommandPoller {
|
|
|
397
397
|
|
|
398
398
|
const keepOpen = await this._keepOpenCfg();
|
|
399
399
|
|
|
400
|
+
// Nhịp còn-sống SUỐT lúc kịch bản chạy (không chỉ lúc xếp hàng). Bộ dọn của
|
|
401
|
+
// API khai tử lệnh đăng 'running' quá 20 phút không cập nhật; một lượt
|
|
402
|
+
// upload FB thật có thể đứng chờ nút Đăng tới 600s cộng các bước trước đó
|
|
403
|
+
// là vượt 20 phút — ca Chuyện Nhà Nông 09/09 21:46 chạy 17 phút, updatedAt
|
|
404
|
+
// vẫn là 21:46. Chạm vào lệnh mỗi 2 phút là đủ, thưa hơn nhiều so với API.
|
|
405
|
+
const RUN_BEAT_MS = 2 * 60 * 1000;
|
|
406
|
+
const beat = setInterval(() => {
|
|
407
|
+
this.api.updateCommand(command._id, { status: 'running' }).catch(() => {});
|
|
408
|
+
}, RUN_BEAT_MS);
|
|
409
|
+
|
|
400
410
|
try {
|
|
401
411
|
const result = await runPlaywrightScript({
|
|
402
412
|
nst: this.nst,
|
|
@@ -415,6 +425,7 @@ class CommandPoller {
|
|
|
415
425
|
console.error(`[commands/pw] ${command.type} failed: ${err.message}`);
|
|
416
426
|
await this.api.postCommandResult(command._id, { status: 'failed', error: String(err.message || err).slice(0, 500) });
|
|
417
427
|
} finally {
|
|
428
|
+
clearInterval(beat);
|
|
418
429
|
// Always release the per-profile mutex — even on throw — or sibling
|
|
419
430
|
// pw cmds for the same profile would hang forever.
|
|
420
431
|
if (this._pwInFlight) this._pwInFlight.delete(profileId);
|
package/package.json
CHANGED
|
@@ -510,7 +510,7 @@ async function waitForPublishEnabled(page, verbs, log, timeoutMs, tag = 'fb-pw',
|
|
|
510
510
|
const dlgs = document.querySelectorAll("[role='dialog']");
|
|
511
511
|
const roots = dlgs.length ? Array.from(dlgs) : [document];
|
|
512
512
|
const vh = window.innerHeight;
|
|
513
|
-
let present = false, enabled = false;
|
|
513
|
+
let present = false, enabled = false, rejected = '';
|
|
514
514
|
for (const root of roots) {
|
|
515
515
|
for (const el of root.querySelectorAll("button, [role='button']")) {
|
|
516
516
|
const t = (el.innerText || el.textContent || '').trim();
|
|
@@ -523,9 +523,22 @@ async function waitForPublishEnabled(page, verbs, log, timeoutMs, tag = 'fb-pw',
|
|
|
523
523
|
present = true;
|
|
524
524
|
if (!(el.getAttribute('aria-disabled') === 'true' || el.disabled)) enabled = true;
|
|
525
525
|
}
|
|
526
|
+
// FB TỪ CHỐI FILE: dòng đỏ ngay trên nút Lưu/Đăng, khung xem trước
|
|
527
|
+
// trống. Ảnh dump 09/09 21:49 (Góc Trại Gà, file 58 MB, lành, decode
|
|
528
|
+
// sạch): "Không thể tải file của bạn lên: fb-….mp4". Nút Đăng sẽ KHÔNG
|
|
529
|
+
// BAO GIỜ bật — chờ đủ 600s rồi mới báo "remained disabled" là đốt 10
|
|
530
|
+
// phút profile và dễ bị bộ dọn 20 phút khai tử. Đo 24h: 9 ca, 8 ca là
|
|
531
|
+
// file >50 MB (đi đường CDP setFileInputFiles).
|
|
532
|
+
const txt = (root.innerText || '').slice(0, 4000);
|
|
533
|
+
const m = /Không thể tải (?:file|tệp)[^\n]{0,120}|couldn['’]t upload your (?:file|video)[^\n]{0,80}|Unable to upload[^\n]{0,80}/i.exec(txt);
|
|
534
|
+
if (m) rejected = m[0].trim();
|
|
526
535
|
}
|
|
527
|
-
return { present, enabled };
|
|
528
|
-
}, verbs).catch(() => ({ present: false, enabled: false }));
|
|
536
|
+
return { present, enabled, rejected };
|
|
537
|
+
}, verbs).catch(() => ({ present: false, enabled: false, rejected: '' }));
|
|
538
|
+
if (st.rejected) {
|
|
539
|
+
log('warn', `[${tag}] Facebook TỪ CHỐI file: "${st.rejected.slice(0, 120)}" — nút "Đăng" sẽ không bật, dừng chờ ngay`);
|
|
540
|
+
return { enabled: false, sawPresent: true, rejected: st.rejected };
|
|
541
|
+
}
|
|
529
542
|
if (st.enabled) {
|
|
530
543
|
if (announced) log('info', `[${tag}] "Đăng" is now enabled — video finished processing`);
|
|
531
544
|
return { enabled: true, sawPresent: true };
|
|
@@ -534,6 +547,19 @@ async function waitForPublishEnabled(page, verbs, log, timeoutMs, tag = 'fb-pw',
|
|
|
534
547
|
sawPresent = true;
|
|
535
548
|
absentSince = 0;
|
|
536
549
|
if (!announced) { log('info', `[${tag}] final step: "Đăng" disabled (video still processing) — waiting up to ${Math.round(timeoutMs / 1000)}s…`); announced = true; }
|
|
550
|
+
} else if (sawPresent) {
|
|
551
|
+
// ĐÃ THẤY nút rồi mà giờ biến mất = composer đã đóng (FB nhảy về trang
|
|
552
|
+
// chủ / toast cướp click / người mở profile bằng tay). Trước đây nhánh này
|
|
553
|
+
// không có: worker đứng đếm đủ 600s trên một hộp thoại không còn tồn tại
|
|
554
|
+
// rồi mới báo "composer vanished — retrying" (09/09 22:20, chủ tịch nhìn
|
|
555
|
+
// profile Chuyện Nhà Nông thấy đang ở trang chủ trong khi log vẫn ghi
|
|
556
|
+
// "waiting up to 600s"). Cho một khoảng ân hạn ngắn vì FB có lúc dựng lại
|
|
557
|
+
// dialog, rồi trả về để call site chạy lại lượt upload ngay.
|
|
558
|
+
if (!absentSince) absentSince = Date.now();
|
|
559
|
+
if (Date.now() - absentSince >= absentGraceMs) {
|
|
560
|
+
log('warn', `[${tag}] nút "Đăng" đã thấy rồi mà biến mất ${Math.round(absentGraceMs / 1000)}s — composer đã đóng, không chờ hết ${Math.round(timeoutMs / 1000)}s nữa`);
|
|
561
|
+
return { enabled: false, sawPresent: true, gone: true };
|
|
562
|
+
}
|
|
537
563
|
} else if (!sawPresent) {
|
|
538
564
|
if (!absentSince) absentSince = Date.now();
|
|
539
565
|
// The final settings form mounts lazily after the thumbnail editor closes.
|
|
@@ -666,6 +692,64 @@ async function confirmPublishDialog({ firstHit, tagConfirmButton, clickTagged, c
|
|
|
666
692
|
return { confirmed: !pending };
|
|
667
693
|
}
|
|
668
694
|
|
|
695
|
+
// Link tab Reels của Page — dùng ở CẢ HAI đầu lượt đăng (chụp ảnh nền trước
|
|
696
|
+
// khi đăng, quét lại sau khi đăng). Ba chiến lược, giữ nguyên từ bản cũ.
|
|
697
|
+
async function discoverReelsTabUrl(page) {
|
|
698
|
+
return page.evaluate(() => {
|
|
699
|
+
const direct = document.querySelectorAll("a[href*='sk=reels_tab']");
|
|
700
|
+
for (const a of direct) {
|
|
701
|
+
const h = a.getAttribute('href') || '';
|
|
702
|
+
if (h) return h.startsWith('http') ? h : `https://www.facebook.com${h}`;
|
|
703
|
+
}
|
|
704
|
+
const profAnchors = document.querySelectorAll("a[href*='/profile.php?id=']");
|
|
705
|
+
for (const a of profAnchors) {
|
|
706
|
+
const h = a.getAttribute('href') || '';
|
|
707
|
+
const m = h.match(/\/profile\.php\?id=(\d+)/);
|
|
708
|
+
if (m) return `https://www.facebook.com/profile.php?id=${m[1]}&sk=reels_tab`;
|
|
709
|
+
}
|
|
710
|
+
const tlAnchors = document.querySelectorAll("a[aria-label*='Dòng thời gian'], a[aria-label*='Timeline']");
|
|
711
|
+
for (const a of tlAnchors) {
|
|
712
|
+
const h = a.getAttribute('href') || '';
|
|
713
|
+
const m = h.match(/^\/([A-Za-z0-9._-]{3,})\/?(?:\?|$)/);
|
|
714
|
+
if (m && !['profile.php', 'reel', 'video', 'videos'].includes(m[1])) {
|
|
715
|
+
return `https://www.facebook.com/${m[1]}/?sk=reels_tab`;
|
|
716
|
+
}
|
|
717
|
+
if (h.match(/\/profile\.php\?id=(\d+)/)) {
|
|
718
|
+
const pid = h.match(/\/profile\.php\?id=(\d+)/)[1];
|
|
719
|
+
return `https://www.facebook.com/profile.php?id=${pid}&sk=reels_tab`;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
return null;
|
|
723
|
+
}).catch(() => null);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Mọi reel ID đang hiện trên tab Reels, theo thứ tự DOM (mới nhất trước).
|
|
727
|
+
async function scrapeReelTileIds(page) {
|
|
728
|
+
return page.evaluate(() => {
|
|
729
|
+
const out = [];
|
|
730
|
+
for (const a of document.querySelectorAll("a[href*='/reel/']")) {
|
|
731
|
+
const m = (a.getAttribute('href') || '').match(/\/reel\/(\d{8,20})/);
|
|
732
|
+
if (!m) continue;
|
|
733
|
+
const r = a.getBoundingClientRect();
|
|
734
|
+
if (r.width < 8 || r.height < 8) continue;
|
|
735
|
+
if (!out.includes(m[1])) out.push(m[1]);
|
|
736
|
+
}
|
|
737
|
+
return out;
|
|
738
|
+
}).catch(() => []);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// SO TRƯỚC/SAU — nguồn lấy link thứ hai, không phụ thuộc gói mạng.
|
|
742
|
+
// Đo 09/09 sau khi gỡ nhánh "bốc tile đầu": bắt gói mạng trượt 2/3 lượt bấm
|
|
743
|
+
// Đăng thành công → 2 bài thành "mập mờ, chờ người kiểm" dù đã lên thật.
|
|
744
|
+
// Ảnh nền = danh sách reel ID của tab Reels chụp TRƯỚC khi mở composer; sau
|
|
745
|
+
// khi đăng quét lại, ID nào chưa có trong ảnh nền là reel mới. Đúng MỘT ID mới
|
|
746
|
+
// thì mới nhận; 0 hoặc >1 thì trả rỗng (thà rỗng còn hơn sai — luật 19/08).
|
|
747
|
+
// Thuần, test được.
|
|
748
|
+
function diffNewReelIds(beforeIds, afterIds) {
|
|
749
|
+
const before = new Set(beforeIds || []);
|
|
750
|
+
return (afterIds || []).filter((id) => !before.has(id));
|
|
751
|
+
}
|
|
752
|
+
|
|
669
753
|
async function hasVisibleReelComposer(page) {
|
|
670
754
|
return page.evaluate(() => {
|
|
671
755
|
for (const dlg of document.querySelectorAll("[role='dialog']")) {
|
|
@@ -802,6 +886,26 @@ async function runOnce({ page, payload, log }) {
|
|
|
802
886
|
log('info', `[fb-pw] browsed the wall for a bit (${bursts} scrolls) before opening the composer`);
|
|
803
887
|
}
|
|
804
888
|
|
|
889
|
+
// 1b) ẢNH NỀN tab Reels — chụp trước khi mở composer để sau khi đăng so
|
|
890
|
+
// trước/sau mà tìm reel mới (xem diffNewReelIds). Một lần goto + quay
|
|
891
|
+
// về, ~6-8s. Hỏng thì bỏ qua, không chặn lượt đăng.
|
|
892
|
+
let reelIdsBefore = null;
|
|
893
|
+
try {
|
|
894
|
+
const tabUrl = await discoverReelsTabUrl(page);
|
|
895
|
+
if (tabUrl) {
|
|
896
|
+
await page.goto(tabUrl, { waitUntil: 'domcontentloaded', timeout: 30_000 });
|
|
897
|
+
await pause(page, 3500);
|
|
898
|
+
reelIdsBefore = await scrapeReelTileIds(page);
|
|
899
|
+
log('info', `[fb-pw] ảnh nền tab Reels trước khi đăng: ${reelIdsBefore.length} reel`);
|
|
900
|
+
await navigateFacebookHome(page, log);
|
|
901
|
+
await pause(page, 2500);
|
|
902
|
+
} else {
|
|
903
|
+
log('warn', '[fb-pw] không tìm được link tab Reels trước khi đăng — bỏ qua ảnh nền');
|
|
904
|
+
}
|
|
905
|
+
} catch (e) {
|
|
906
|
+
log('warn', `[fb-pw] chụp ảnh nền tab Reels hỏng: ${String(e && e.message || e).slice(0, 80)} — bỏ qua`);
|
|
907
|
+
}
|
|
908
|
+
|
|
805
909
|
// 2) Click the "Thước phim" entry inside the Page's "Tạo bài viết" widget.
|
|
806
910
|
// This opens a FRESH Reel composer modal each time (no stale-draft
|
|
807
911
|
// issue that plagued the BS composer). CRITICAL: scope to the "Tạo
|
|
@@ -1769,6 +1873,18 @@ async function runOnce({ page, payload, log }) {
|
|
|
1769
1873
|
for (const sel of saveCandidates) {
|
|
1770
1874
|
const btn = await firstVisible(page.locator(sel), 2);
|
|
1771
1875
|
if (!btn) continue;
|
|
1876
|
+
// CHẨN ĐOÁN 09/09: nghi cú bấm lại này trúng nút "Lưu" (lưu
|
|
1877
|
+
// nháp) của chính composer chứ không phải của hộp ảnh bìa →
|
|
1878
|
+
// FB lưu nháp, đóng composer, về trang chủ = "composer đóng
|
|
1879
|
+
// giữa chừng". Ghi rõ nút nằm trong dialog nào để có bằng
|
|
1880
|
+
// chứng thay vì đoán (đo được từ log, không cần ảnh).
|
|
1881
|
+
const where = await btn.evaluate((el) => {
|
|
1882
|
+
const dlg = el.closest("[role='dialog']");
|
|
1883
|
+
const aria = dlg ? (dlg.getAttribute('aria-label') || '') : '(không trong dialog)';
|
|
1884
|
+
const head = dlg ? (dlg.innerText || '').slice(0, 40).replace(/\s+/g, ' ') : '';
|
|
1885
|
+
return `${aria} | ${head}`;
|
|
1886
|
+
}).catch(() => '?');
|
|
1887
|
+
log('info', `[fb-pw] re-click Lưu → nút nằm trong: ${where}`);
|
|
1772
1888
|
try {
|
|
1773
1889
|
await btn.click({ timeout: 2500 });
|
|
1774
1890
|
break;
|
|
@@ -2454,6 +2570,11 @@ async function runOnce({ page, payload, log }) {
|
|
|
2454
2570
|
if (waitResult.enabled) { step--; continue; }
|
|
2455
2571
|
await dumpInventory(page, log, `no-advance-${step + 1}`);
|
|
2456
2572
|
await dumpFailure(page, `no-advance-${step + 1}`, log);
|
|
2573
|
+
if (waitResult.rejected) {
|
|
2574
|
+
// Mã FB_PUBLISH_ để run() không đi dò tài khoản (không phải lỗi
|
|
2575
|
+
// đăng nhập), và PublishRetry phân loại đúng "file bị từ chối".
|
|
2576
|
+
throw new Error(`FB_PUBLISH_REJECTED: Facebook từ chối file video ("${waitResult.rejected.slice(0, 100)}") — thường gặp với file >50 MB đi đường CDP; nén/render lại nhỏ hơn rồi đăng`);
|
|
2577
|
+
}
|
|
2457
2578
|
// Composer biến mất (URL nhảy sang /reel/… vì toast cướp click, hoặc
|
|
2458
2579
|
// FB tự đóng) → CHẠY LẠI cả lượt upload thay vì báo lỗi khó hiểu.
|
|
2459
2580
|
// An toàn vì chưa hề bấm Đăng ở nhánh này.
|
|
@@ -2686,39 +2807,7 @@ async function runOnce({ page, payload, log }) {
|
|
|
2686
2807
|
// 2. Extract Page ID from "Dòng thời gian của X" timeline anchor or
|
|
2687
2808
|
// any /profile.php?id=<id> anchor, then build the reels_tab URL
|
|
2688
2809
|
// 3. Extract Page slug from any /<handle>/ anchor
|
|
2689
|
-
const reelsTabHref = await page
|
|
2690
|
-
// Strategy 1: direct reels_tab link.
|
|
2691
|
-
const direct = document.querySelectorAll("a[href*='sk=reels_tab']");
|
|
2692
|
-
for (const a of direct) {
|
|
2693
|
-
const h = a.getAttribute('href') || '';
|
|
2694
|
-
if (h) return h.startsWith('http') ? h : `https://www.facebook.com${h}`;
|
|
2695
|
-
}
|
|
2696
|
-
// Strategy 2: extract Page ID from a /profile.php?id=<id> anchor.
|
|
2697
|
-
// The "Dòng thời gian của X" anchor and the page-shortcut anchor
|
|
2698
|
-
// both have hrefs like /profile.php?id=61590303065684.
|
|
2699
|
-
const profAnchors = document.querySelectorAll("a[href*='/profile.php?id=']");
|
|
2700
|
-
for (const a of profAnchors) {
|
|
2701
|
-
const h = a.getAttribute('href') || '';
|
|
2702
|
-
const m = h.match(/\/profile\.php\?id=(\d+)/);
|
|
2703
|
-
if (m) return `https://www.facebook.com/profile.php?id=${m[1]}&sk=reels_tab`;
|
|
2704
|
-
}
|
|
2705
|
-
// Strategy 3: page handle via "Dòng thời gian của X" anchor (no
|
|
2706
|
-
// profile.php — uses /<handle> instead).
|
|
2707
|
-
const tlAnchors = document.querySelectorAll("a[aria-label*='Dòng thời gian'], a[aria-label*='Timeline']");
|
|
2708
|
-
for (const a of tlAnchors) {
|
|
2709
|
-
const h = a.getAttribute('href') || '';
|
|
2710
|
-
// /<handle> or /<handle>/?...
|
|
2711
|
-
const m = h.match(/^\/([A-Za-z0-9._-]{3,})\/?(?:\?|$)/);
|
|
2712
|
-
if (m && !['profile.php', 'reel', 'video', 'videos'].includes(m[1])) {
|
|
2713
|
-
return `https://www.facebook.com/${m[1]}/?sk=reels_tab`;
|
|
2714
|
-
}
|
|
2715
|
-
if (h.match(/\/profile\.php\?id=(\d+)/)) {
|
|
2716
|
-
const pid = h.match(/\/profile\.php\?id=(\d+)/)[1];
|
|
2717
|
-
return `https://www.facebook.com/profile.php?id=${pid}&sk=reels_tab`;
|
|
2718
|
-
}
|
|
2719
|
-
}
|
|
2720
|
-
return null;
|
|
2721
|
-
}).catch(() => null);
|
|
2810
|
+
const reelsTabHref = await discoverReelsTabUrl(page);
|
|
2722
2811
|
if (reelsTabHref) {
|
|
2723
2812
|
reelsTabUrl = reelsTabHref;
|
|
2724
2813
|
log('info', `[fb-pw] navigating to page reels tab: ${reelsTabHref.slice(0, 100)}`);
|
|
@@ -2735,6 +2824,17 @@ async function runOnce({ page, payload, log }) {
|
|
|
2735
2824
|
} else {
|
|
2736
2825
|
log('warn', '[fb-pw] reels_tab navigated but no tile with fresh timestamp found');
|
|
2737
2826
|
}
|
|
2827
|
+
// (a.1b) SO TRƯỚC/SAU với ảnh nền chụp trước khi mở composer.
|
|
2828
|
+
if (!postUrl && reelIdsBefore) {
|
|
2829
|
+
const after = await scrapeReelTileIds(page);
|
|
2830
|
+
const fresh2 = diffNewReelIds(reelIdsBefore, after);
|
|
2831
|
+
if (fresh2.length === 1) {
|
|
2832
|
+
postUrl = `https://www.facebook.com/reel/${fresh2[0]}/`;
|
|
2833
|
+
log('info', `[fb-pw] post URL từ so trước/sau tab Reels (trước ${reelIdsBefore.length}, sau ${after.length}, mới 1): ${postUrl}`);
|
|
2834
|
+
} else {
|
|
2835
|
+
log('info', `[fb-pw] so trước/sau tab Reels: trước ${reelIdsBefore.length}, sau ${after.length}, mới ${fresh2.length} — ${fresh2.length ? 'nhiều hơn 1, không nhận' : 'chưa thấy reel mới'}`);
|
|
2836
|
+
}
|
|
2837
|
+
}
|
|
2738
2838
|
} else {
|
|
2739
2839
|
log('warn', '[fb-pw] no reels_tab link found on page; skipping reels-tab strategy');
|
|
2740
2840
|
}
|
|
@@ -2864,6 +2964,15 @@ async function runOnce({ page, payload, log }) {
|
|
|
2864
2964
|
if (fresh2) {
|
|
2865
2965
|
postUrl = fresh2.href.startsWith('http') ? fresh2.href : `https://www.facebook.com${fresh2.href}`;
|
|
2866
2966
|
log('info', `[fb-pw] post URL from reels_tab RETRY (id=${fresh2.id}, timestamp="${fresh2.timestamp}"): ${postUrl}`);
|
|
2967
|
+
} else if (reelIdsBefore) {
|
|
2968
|
+
const after2 = await scrapeReelTileIds(page);
|
|
2969
|
+
const diff2 = diffNewReelIds(reelIdsBefore, after2);
|
|
2970
|
+
if (diff2.length === 1) {
|
|
2971
|
+
postUrl = `https://www.facebook.com/reel/${diff2[0]}/`;
|
|
2972
|
+
log('info', `[fb-pw] post URL từ so trước/sau tab Reels (lượt chờ 75s; trước ${reelIdsBefore.length}, sau ${after2.length}): ${postUrl}`);
|
|
2973
|
+
} else {
|
|
2974
|
+
log('warn', `[fb-pw] reels_tab retry: vẫn chưa thấy reel mới (so trước/sau: mới ${diff2.length})`);
|
|
2975
|
+
}
|
|
2867
2976
|
} else {
|
|
2868
2977
|
log('warn', '[fb-pw] reels_tab retry: still no fresh tile');
|
|
2869
2978
|
}
|
|
@@ -2997,6 +3106,9 @@ module.exports = { run };
|
|
|
2997
3106
|
module.exports.__testables = {
|
|
2998
3107
|
resilientClick,
|
|
2999
3108
|
confirmPublishDialog,
|
|
3109
|
+
diffNewReelIds,
|
|
3110
|
+
discoverReelsTabUrl,
|
|
3111
|
+
scrapeReelTileIds,
|
|
3000
3112
|
muteClickInterceptors,
|
|
3001
3113
|
unmuteClickInterceptors,
|
|
3002
3114
|
waitForPublishEnabled,
|