channel-worker 2.5.96 → 2.5.98

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "channel-worker",
3
- "version": "2.5.96",
3
+ "version": "2.5.98",
4
4
  "description": "Channel Manager worker daemon — runs on remote machines to execute video pipeline jobs",
5
5
  "main": "lib/daemon.js",
6
6
  "bin": {
@@ -694,33 +694,27 @@ async function confirmPublishDialog({ firstHit, tagConfirmButton, clickTagged, c
694
694
 
695
695
  // Link tab Reels của Page — dùng ở CẢ HAI đầu lượt đăng (chụp ảnh nền trước
696
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
- }
697
+ async function discoverReelsTabUrl(page, log = null) {
698
+ const anchors = await page.evaluate(() => {
699
+ const out = [];
700
+ for (const a of document.querySelectorAll('a[href]')) {
701
+ const href = a.getAttribute('href') || '';
702
+ if (!href || href.startsWith('#') || href.startsWith('javascript:')) continue;
703
+ // Anchor nằm trong bảng tin / một bài viết → của page KHÁC, không phải
704
+ // của mình (xem ghi chú ở pickReelsTabUrl).
705
+ const inFeed = !!a.closest("[role='feed'], [role='article'], [aria-posinset]");
706
+ out.push({ href: href.slice(0, 200), aria: (a.getAttribute('aria-label') || '').slice(0, 80), inFeed });
707
+ if (out.length >= 400) break;
721
708
  }
722
- return null;
723
- }).catch(() => null);
709
+ return out;
710
+ }).catch(() => []);
711
+ const url = pickReelsTabUrl(anchors);
712
+ if (!url && log) {
713
+ // Chẩn đoán: in vài href "giống của Page" để lần sau thêm đúng chiến lược.
714
+ const sample = anchors.map((a) => a.href).filter((h) => /^\/(?:profile\.php|\d{8,}|[A-Za-z0-9._-]{3,}\/?(?:\?|$))/.test(h)).slice(0, 8);
715
+ log('info', `[fb-pw] tìm tab Reels: ${anchors.length} anchor, không khớp chiến lược nào; mẫu: ${JSON.stringify(sample)}`);
716
+ }
717
+ return url;
724
718
  }
725
719
 
726
720
  // Mọi reel ID đang hiện trên tab Reels, theo thứ tự DOM (mới nhất trước).
@@ -750,6 +744,54 @@ function diffNewReelIds(beforeIds, afterIds) {
750
744
  return (afterIds || []).filter((id) => !before.has(id));
751
745
  }
752
746
 
747
+ // Chữ ký của chính composer (mọi bước của wizard). Dùng để KHÔNG bao giờ bấm
748
+ // nhầm vào nút của composer khi đang định bấm nút của hộp ảnh bìa.
749
+ const COMPOSER_SIGNATURE_RE = /Cài đặt thước phim|Tạo thước phim|Chỉnh sửa thước phim|Reel settings|Create (?:a )?reel|Edit reel/i;
750
+ function isComposerDialogSignature(text) {
751
+ return COMPOSER_SIGNATURE_RE.test(String(text || ''));
752
+ }
753
+
754
+ // Chọn link tab Reels từ danh sách anchor của trang (thuần, test được).
755
+ // Chiến lược 1-3 giữ nguyên từ bản cũ; 4-5 thêm 09/09 vì trên TRANG CHỦ (lúc
756
+ // chụp ảnh nền trước khi đăng) không có anchor profile.php/Dòng thời gian —
757
+ // 2/2 lượt "không tìm được link tab Reels trước khi đăng". Trang chủ của Page
758
+ // có shortcut bên trái trỏ /<id số>/ hoặc /<handle>/, và đôi khi /<id>/reels/.
759
+ function pickReelsTabUrl(anchors) {
760
+ const list = (anchors || [])
761
+ .map((a) => ({ href: String(a.href || ''), aria: String(a.aria || ''), inFeed: !!a.inFeed }))
762
+ // ⛔ LOẠI anchor CỦA BÀI TRONG BẢNG TIN — đây là gốc của 2 link sai sáng
763
+ // 10/09 (kênh "kol du lịch 2", reel 1712630710026375 và 1549470359802337
764
+ // hoá ra là bài của PAGE LẠ). Trang chủ đầy bài của page khác; mỗi bài có
765
+ // link /profile.php?id=<page lạ>&__cft__[0]=… nên chiến lược 2 bốc trúng
766
+ // tab Reels của người ta, rồi "so trước/sau" so hai page khác nhau (log:
767
+ // "trước 10, sau 10, mới 10 — nhiều hơn 1, không nhận") và nhánh gói mạng
768
+ // chốt bằng reel gợi ý trên feed. Dấu nhận biết: tham số __cft__/__tn__
769
+ // (tracking của một bài cụ thể) hoặc anchor nằm trong vùng feed/article.
770
+ .filter((a) => !a.inFeed && !/[?&]__cft__|[?&]__tn__/.test(a.href));
771
+ const abs = (h) => (h.startsWith('http') ? h : `https://www.facebook.com${h}`);
772
+ for (const a of list) if (/sk=reels_tab/.test(a.href)) return abs(a.href);
773
+ for (const a of list) {
774
+ const m = a.href.match(/\/profile\.php\?id=(\d+)/);
775
+ if (m) return `https://www.facebook.com/profile.php?id=${m[1]}&sk=reels_tab`;
776
+ }
777
+ for (const a of list) {
778
+ if (!/Dòng thời gian|Timeline/i.test(a.aria)) continue;
779
+ const m = a.href.match(/^(?:https?:\/\/(?:www\.)?facebook\.com)?\/([A-Za-z0-9._-]{3,})\/?(?:\?|$)/);
780
+ if (m && !['profile.php', 'reel', 'video', 'videos', 'reels'].includes(m[1])) return `https://www.facebook.com/${m[1]}/?sk=reels_tab`;
781
+ }
782
+ // 4. /<id số>/reels/ hoặc /<id số>/ — shortcut của Page trên trang chủ.
783
+ for (const a of list) {
784
+ const m = a.href.match(/^(?:https?:\/\/(?:www\.)?facebook\.com)?\/(\d{10,20})(?:\/reels)?\/?(?:\?|$)/);
785
+ if (m) return `https://www.facebook.com/profile.php?id=${m[1]}&sk=reels_tab`;
786
+ }
787
+ // 5. /<handle>/reels/ — Page có tên riêng.
788
+ for (const a of list) {
789
+ const m = a.href.match(/^(?:https?:\/\/(?:www\.)?facebook\.com)?\/([A-Za-z0-9._-]{3,})\/reels\/?(?:\?|$)/);
790
+ if (m && !['profile.php', 'reel', 'video', 'videos'].includes(m[1])) return `https://www.facebook.com/${m[1]}/?sk=reels_tab`;
791
+ }
792
+ return null;
793
+ }
794
+
753
795
  async function hasVisibleReelComposer(page) {
754
796
  return page.evaluate(() => {
755
797
  for (const dlg of document.querySelectorAll("[role='dialog']")) {
@@ -891,7 +933,7 @@ async function runOnce({ page, payload, log }) {
891
933
  // về, ~6-8s. Hỏng thì bỏ qua, không chặn lượt đăng.
892
934
  let reelIdsBefore = null;
893
935
  try {
894
- const tabUrl = await discoverReelsTabUrl(page);
936
+ const tabUrl = await discoverReelsTabUrl(page, log);
895
937
  if (tabUrl) {
896
938
  await page.goto(tabUrl, { waitUntil: 'domcontentloaded', timeout: 30_000 });
897
939
  await pause(page, 3500);
@@ -1884,12 +1926,27 @@ async function runOnce({ page, payload, log }) {
1884
1926
  const head = dlg ? (dlg.innerText || '').slice(0, 40).replace(/\s+/g, ' ') : '';
1885
1927
  return `${aria} | ${head}`;
1886
1928
  }).catch(() => '?');
1929
+ // ĐÃ CÓ BẰNG CHỨNG (2.5.96, 09/09 23:30, 2/2 lượt): nút "Lưu" bấm
1930
+ // lại nằm trong "Cài đặt thước phim" = nút LƯU NHÁP của
1931
+ // composer, không phải của hộp ảnh bìa (hộp đã đóng, nhưng
1932
+ // isThumbnailEditorVisible vẫn thấy chữ "Chỉnh sửa hình thu
1933
+ // nhỏ" trên pill của composer nên tưởng còn mở). Bấm vào là
1934
+ // FB lưu nháp, đóng composer, về trang chủ → "composer đóng
1935
+ // giữa chừng" (3/5 lượt tối 09/09, kể cả file nén 42 MB).
1936
+ // Nút thuộc composer thì KHÔNG bấm; và vì hộp ảnh bìa không
1937
+ // còn nút Lưu nào nghĩa là nó đã đóng → coi như xong.
1938
+ if (isComposerDialogSignature(where)) {
1939
+ log('warn', `[fb-pw] re-click Lưu BỎ QUA: nút thuộc composer (${where.slice(0, 60)}) — hộp ảnh bìa đã đóng, không bấm lưu nháp`);
1940
+ modalClosed = true;
1941
+ break;
1942
+ }
1887
1943
  log('info', `[fb-pw] re-click Lưu → nút nằm trong: ${where}`);
1888
1944
  try {
1889
1945
  await btn.click({ timeout: 2500 });
1890
1946
  break;
1891
1947
  } catch {}
1892
1948
  }
1949
+ if (modalClosed) break;
1893
1950
  }
1894
1951
  await pause(page, 1000);
1895
1952
  }
@@ -2801,6 +2858,7 @@ async function runOnce({ page, payload, log }) {
2801
2858
  return null;
2802
2859
  }).catch(() => null);
2803
2860
  let reelsTabUrl = null; // giữ lại cho lượt retry (a.2b)
2861
+ let networkCandidate = null; // ID gói mạng chưa được tab Reels xác nhận
2804
2862
  try {
2805
2863
  // Find a link to the page's reels tab. Multi-strategy:
2806
2864
  // 1. Any existing href containing "sk=reels_tab"
@@ -2943,9 +3001,21 @@ async function runOnce({ page, payload, log }) {
2943
3001
  const preSet = new Set(capturedReelIds.slice(0, capturedReelIdsSnapshotLen));
2944
3002
  const trulyNew = newIds.filter((id) => !preSet.has(id));
2945
3003
  if (trulyNew.length) {
3004
+ // ⚠️ ID bắt từ gói mạng KHÔNG chứng minh reel thuộc page mình: đứng ở
3005
+ // trang chủ, feed tải liên tục reel GỢI Ý của page khác, nên "ID mới
3006
+ // đầu tiên sau mốc" có thể là reel người ta (sáng 10/09: snapshotLen=132,
3007
+ // total=220 → 88 ID mới, cái đầu là reel của page lạ và đã bị ghi vào DB).
3008
+ // Chỉ nhận khi tab Reels CỦA CHÍNH PAGE xác nhận có ID đó; không xác
3009
+ // nhận được thì để làm ứng viên, lượt chờ 75s dưới đây kiểm lại.
2946
3010
  const firstNew = trulyNew[0];
2947
- postUrl = `https://www.facebook.com/reel/${firstNew}/`;
2948
- log('info', `[fb-pw] post URL from network (FIRST new ID after snapshot): ${postUrl} (snapshotLen=${capturedReelIdsSnapshotLen} total=${capturedReelIds.length} new=${trulyNew.length})`);
3011
+ const tabIds = reelsTabUrl ? await scrapeReelTileIds(page) : [];
3012
+ if (tabIds.includes(firstNew)) {
3013
+ postUrl = `https://www.facebook.com/reel/${firstNew}/`;
3014
+ log('info', `[fb-pw] post URL from network + tab Reels xác nhận: ${postUrl} (snapshotLen=${capturedReelIdsSnapshotLen} total=${capturedReelIds.length} new=${trulyNew.length})`);
3015
+ } else {
3016
+ networkCandidate = firstNew;
3017
+ log('warn', `[fb-pw] gói mạng cho ID ${firstNew} nhưng tab Reels của page CHƯA xác nhận (${tabIds.length} tile) — giữ làm ứng viên, chờ kiểm lại`);
3018
+ }
2949
3019
  }
2950
3020
  }
2951
3021
 
@@ -2964,6 +3034,9 @@ async function runOnce({ page, payload, log }) {
2964
3034
  if (fresh2) {
2965
3035
  postUrl = fresh2.href.startsWith('http') ? fresh2.href : `https://www.facebook.com${fresh2.href}`;
2966
3036
  log('info', `[fb-pw] post URL from reels_tab RETRY (id=${fresh2.id}, timestamp="${fresh2.timestamp}"): ${postUrl}`);
3037
+ } else if (networkCandidate && (await scrapeReelTileIds(page)).includes(networkCandidate)) {
3038
+ postUrl = `https://www.facebook.com/reel/${networkCandidate}/`;
3039
+ log('info', `[fb-pw] post URL: ứng viên gói mạng đã được tab Reels xác nhận sau lượt chờ: ${postUrl}`);
2967
3040
  } else if (reelIdsBefore) {
2968
3041
  const after2 = await scrapeReelTileIds(page);
2969
3042
  const diff2 = diffNewReelIds(reelIdsBefore, after2);
@@ -3107,6 +3180,8 @@ module.exports.__testables = {
3107
3180
  resilientClick,
3108
3181
  confirmPublishDialog,
3109
3182
  diffNewReelIds,
3183
+ pickReelsTabUrl,
3184
+ isComposerDialogSignature,
3110
3185
  discoverReelsTabUrl,
3111
3186
  scrapeReelTileIds,
3112
3187
  muteClickInterceptors,