channel-worker 2.5.103 → 2.5.105

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.103",
3
+ "version": "2.5.105",
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": {
@@ -651,12 +651,21 @@ async function isCustomThumbApplied(page) {
651
651
  // xong quyền riêng tư của Thước phim" → Đăng xám mãi; đứng
652
652
  // 600s rồi "description field not found" (phụ kiện điện tử
653
653
  // 02 10/09, kol du lịch 11/09).
654
+ // fbError : toast ĐEN góc trái dưới "Đã xảy ra lỗi. Việc này có thể là
655
+ // do lỗi kỹ thuật và chúng tôi đang khắc phục rồi." → chính
656
+ // Facebook hỏng và tự gỡ composer. Toast này nằm NGOÀI mọi
657
+ // [role=dialog] nên readComposerText không thấy — đọc bằng
658
+ // readPageAlerts.
654
659
  // Thuần, test được; regex neo đầu dòng cho "Đang đăng" để không dính mô tả
655
660
  // bài có chữ "đang đăng" giữa câu.
656
661
  const COMPOSER_TEXT_RE = {
657
662
  rightsCheck: /Không hoàn tất được quy trình kiểm tra bản quyền|couldn['’]t complete (?:the )?(?:rights|copyright) check/i,
658
663
  publishing: /(?:^|\n)\s*(?:Đang đăng|Đang xuất bản|Publishing|Posting)\s*(?:\.{3}|…)?\s*(?:\n|$)/i,
659
664
  settingsStuck: /Vui lòng đợi tải xong quyền riêng tư của Thước phim|wait (?:for|until) (?:the )?reel(?:'s)? privacy/i,
665
+ // ĐÒI CẢ HAI VẾ trong cùng một quãng ngắn. Chỉ "Đã xảy ra lỗi" thôi thì quá
666
+ // dễ trúng caption bài trên feed (hàm đọc có nhánh quét cả trang), còn cặp
667
+ // "lỗi … lỗi kỹ thuật/khắc phục" là đúng khuôn toast của Facebook.
668
+ fbError: /Đã xảy ra lỗi[\s\S]{0,160}(?:lỗi kỹ thuật|đang khắc phục)|Something went wrong[\s\S]{0,160}technical (?:error|issue|problem)/i,
660
669
  };
661
670
  function classifyComposerText(txt) {
662
671
  const t = String(txt || '');
@@ -664,6 +673,7 @@ function classifyComposerText(txt) {
664
673
  rightsCheck: COMPOSER_TEXT_RE.rightsCheck.test(t),
665
674
  publishing: COMPOSER_TEXT_RE.publishing.test(t),
666
675
  settingsStuck: COMPOSER_TEXT_RE.settingsStuck.test(t),
676
+ fbError: COMPOSER_TEXT_RE.fbError.test(t),
667
677
  };
668
678
  }
669
679
  async function readComposerText(page) {
@@ -679,6 +689,34 @@ async function readComposerText(page) {
679
689
  return out.join('\n').slice(0, 8000);
680
690
  }).catch(() => '');
681
691
  }
692
+ // Chữ trong các lớp nổi NGOÀI composer: toast, alert, status.
693
+ //
694
+ // Vì sao cần: 13/09 ba kênh (kiến 01, kenh-54, kenh-55) cùng chết ở bước lưu
695
+ // ảnh bìa, ảnh dump cả ba đều có TOAST ĐEN góc trái dưới "Đã xảy ra lỗi… lỗi
696
+ // kỹ thuật… đang khắc phục rồi" — tức Facebook tự hỏng rồi gỡ composer. Câu
697
+ // lỗi cũ chỉ nói "composer closed", nên mỗi lần gặp lại phải mở ảnh dump mới
698
+ // biết là phía FB. Toast nằm ngoài [role=dialog] nên readComposerText mù.
699
+ //
700
+ // Quét theo lớp: role=alert/status + gốc portal `ignore-dynamic` (cùng bộ chọn
701
+ // mà dismissToasts đã dùng). Không thấy gì thì mới đọc cả trang — an toàn vì
702
+ // regex fbError đòi hai vế đi liền nhau.
703
+ async function readPageAlerts(page) {
704
+ return page.evaluate(() => {
705
+ const visible = (el) => {
706
+ const r = el.getBoundingClientRect();
707
+ if (r.width < 8 || r.height < 8) return false;
708
+ const cs = getComputedStyle(el);
709
+ return cs.visibility !== 'hidden' && cs.display !== 'none';
710
+ };
711
+ const out = [];
712
+ for (const el of document.querySelectorAll("[role='alert'], [role='status'], div[data-visualcompletion='ignore-dynamic']")) {
713
+ if (visible(el)) out.push(el.innerText || '');
714
+ }
715
+ const picked = out.join('\n').trim();
716
+ return (picked || (document.body ? document.body.innerText || '' : '')).slice(0, 8000);
717
+ }).catch(() => '');
718
+ }
719
+
682
720
  const PUBLISHING_WAIT_MS = 5 * 60 * 1000; // "Đang đăng": chờ tới 5 phút, không bấm lại
683
721
  const SETTINGS_STUCK_MS = 90 * 1000; // bảng cài đặt trống quá 90s: mở lại composer
684
722
 
@@ -690,6 +728,21 @@ function safeComposerRetryError(message) {
690
728
  return err;
691
729
  }
692
730
 
731
+ // Composer biến mất → nói RÕ vì sao, nếu Facebook có để lại toast lỗi.
732
+ //
733
+ // GIỮ NGUYÊN cụm "composer closed after saving thumbnail" ở cuối câu: luật
734
+ // `composer_closed` bên api/services/PublishRetry khớp theo cụm đó. Worker lên
735
+ // bản mới bằng chép nóng (có hiệu lực ngay), còn API thì deploy riêng — đổi
736
+ // hẳn câu lỗi là lỗi này rơi vào "lỗi lạ" (trần thử lại 1 lần) cho tới khi API
737
+ // kịp lên. Thêm vế giải thích ở ĐẦU, không thay phần đuôi.
738
+ async function composerClosedError(page, log, fallbackMessage) {
739
+ let fbError = false;
740
+ try { fbError = classifyComposerText(await readPageAlerts(page)).fbError; } catch { /* đọc lỗi thì cứ dùng câu cũ */ }
741
+ if (!fbError) return safeComposerRetryError(fallbackMessage);
742
+ log('warn', '[fb-pw] Facebook hiện toast "Đã xảy ra lỗi… lỗi kỹ thuật" — chính FB hỏng rồi gỡ composer, không phải lỗi bài/tài khoản');
743
+ return safeComposerRetryError(`Facebook báo lỗi kỹ thuật ("Đã xảy ra lỗi… chúng tôi đang khắc phục") rồi tự gỡ trình soạn — ${fallbackMessage}`);
744
+ }
745
+
693
746
  // Toast "Thông báo mới" (ai đó thích/bình luận reel) nổi lên góc trái và ĂN
694
747
  // CÚ CLICK: 23/08 hai lần liên tiếp bot bấm "Tiếp" thì FB nhảy sang xem reel
695
748
  // cũ, composer đóng → "final description field not found". Đóng toast trước
@@ -802,6 +855,18 @@ function diffNewReelIds(beforeIds, afterIds) {
802
855
  return (afterIds || []).filter((id) => !before.has(id));
803
856
  }
804
857
 
858
+ // URL tab Reels dùng SAU khi đăng: ưu tiên link dò được trên trang hiện tại,
859
+ // không có thì dùng URL đã tìm được lúc chụp ảnh nền. Sau khi bấm Đăng,
860
+ // Facebook hay đưa về /saved/?cref=28 (đo 04/08→13/09: 48/1829 lượt, 74 lần
861
+ // "no reels_tab link found") — trang đó không có link tab Reels, dò lại là
862
+ // trượt và cả so trước/sau lẫn lượt chờ 75s đều bị bỏ → bài lên thật mà thành
863
+ // "mập mờ" (Makeup tại nhà 2 13/09 18:07, thich-lo-mo 12/09). Thuần, test được.
864
+ function pickPostPublishReelsTabUrl({ discovered, remembered }) {
865
+ if (discovered) return { url: discovered, via: 'page' };
866
+ if (remembered) return { url: remembered, via: 'snapshot' };
867
+ return { url: null, via: null };
868
+ }
869
+
805
870
  // ID từ gói mạng: tab Reels xác nhận chỉ chứng minh reel THUỘC PAGE MÌNH, không
806
871
  // chứng minh nó MỚI. Ca 10/09 16:26 "ẩm thực đồng quê 01": snapshotLen=0 nên cả
807
872
  // 40 ID bắt được đều "mới", ID đầu tiên 1600960624772777 có trên tab (là bài
@@ -895,7 +960,7 @@ async function runOnce({ page, payload, log }) {
895
960
  } = payload || {};
896
961
  if (!video_url) throw new Error('No video_url provided');
897
962
 
898
- log('info', '[fb-pw] selectors version=2026.09.01-thumb-final-cta-race');
963
+ log('info', '[fb-pw] selectors version=2026.09.13-reels-tab-fallback');
899
964
 
900
965
  page.on('dialog', (d) => { d.accept().catch(() => {}); });
901
966
 
@@ -1003,9 +1068,12 @@ async function runOnce({ page, payload, log }) {
1003
1068
  // trước/sau mà tìm reel mới (xem diffNewReelIds). Một lần goto + quay
1004
1069
  // về, ~6-8s. Hỏng thì bỏ qua, không chặn lượt đăng.
1005
1070
  let reelIdsBefore = null;
1071
+ // GIỮ LẠI URL tab Reels cho sau khi đăng — xem pickPostPublishReelsTabUrl.
1072
+ let reelsTabUrlBefore = null;
1006
1073
  try {
1007
1074
  const tabUrl = await discoverReelsTabUrl(page, log);
1008
1075
  if (tabUrl) {
1076
+ reelsTabUrlBefore = tabUrl;
1009
1077
  await page.goto(tabUrl, { waitUntil: 'domcontentloaded', timeout: 30_000 });
1010
1078
  await pause(page, 3500);
1011
1079
  reelIdsBefore = await scrapeReelTileIds(page);
@@ -2036,7 +2104,7 @@ async function runOnce({ page, payload, log }) {
2036
2104
  // safe here because the publish branch has not run yet.
2037
2105
  if (!(await hasVisibleReelComposer(page))) {
2038
2106
  await dumpFailure(page, 'composer-closed-after-thumb-save', log);
2039
- throw safeComposerRetryError('FB Reel composer closed after saving thumbnail (before publish)');
2107
+ throw await composerClosedError(page, log, 'FB Reel composer closed after saving thumbnail (before publish)');
2040
2108
  }
2041
2109
  thumbApplied = true;
2042
2110
  log('info', `[fb-pw] page-wall thumb — modal closed after save (retries=${retryCount})`);
@@ -2723,7 +2791,11 @@ async function runOnce({ page, payload, log }) {
2723
2791
  // FB tự đóng) → CHẠY LẠI cả lượt upload thay vì báo lỗi khó hiểu.
2724
2792
  // An toàn vì chưa hề bấm Đăng ở nhánh này.
2725
2793
  if (!(await hasVisibleReelComposer(page))) {
2726
- throw safeComposerRetryError(`FB composer đóng giữa chừng (đang ${page.url().slice(0, 80)}) — chạy lại lượt đăng`);
2794
+ // Câu lỗi phải chứa "composer closed before publish": luật
2795
+ // `composer_closed` của PublishRetry khớp theo cụm đó. Bản cũ viết
2796
+ // thuần tiếng Việt nên nếu lượt trong phiên cũng hỏng thì lỗi rơi
2797
+ // vào "lỗi lạ" (trần thử lại 1 lần) thay vì được xếp lại 3 lần.
2798
+ throw await composerClosedError(page, log, `FB Reel composer closed (before publish) — đóng giữa chừng ở ${page.url().slice(0, 80)}, chạy lại lượt đăng`);
2727
2799
  }
2728
2800
  if (!fillState.description) {
2729
2801
  throw new Error(`FB final description field not found at step ${step + 1}`);
@@ -2975,7 +3047,14 @@ async function runOnce({ page, payload, log }) {
2975
3047
  // 2. Extract Page ID from "Dòng thời gian của X" timeline anchor or
2976
3048
  // any /profile.php?id=<id> anchor, then build the reels_tab URL
2977
3049
  // 3. Extract Page slug from any /<handle>/ anchor
2978
- const reelsTabHref = await discoverReelsTabUrl(page);
3050
+ const picked = pickPostPublishReelsTabUrl({
3051
+ discovered: await discoverReelsTabUrl(page),
3052
+ remembered: reelsTabUrlBefore,
3053
+ });
3054
+ const reelsTabHref = picked.url;
3055
+ if (picked.via === 'snapshot') {
3056
+ log('info', `[fb-pw] trang hiện tại không có link tab Reels (${String(page.url() || '').slice(0, 70)}) — dùng URL đã biết từ lúc chụp ảnh nền`);
3057
+ }
2979
3058
  if (reelsTabHref) {
2980
3059
  reelsTabUrl = reelsTabHref;
2981
3060
  log('info', `[fb-pw] navigating to page reels tab: ${reelsTabHref.slice(0, 100)}`);
@@ -3256,6 +3335,11 @@ async function runOnce({ page, payload, log }) {
3256
3335
  // post_url để quyết, hai cờ này để người đọc log biết hỏng ở khâu nào.
3257
3336
  confirm_click_failed: confirmClickFailed,
3258
3337
  post_url_captured: !!postUrl,
3338
+ // Ảnh nền tab Reels chụp trước khi mở composer + URL tab. API lưu vào
3339
+ // publish_results[].reels_before để đối chiếu xuôi (PublishReconcile)
3340
+ // biết reel nào là CŨ khi lượt này không lấy được link.
3341
+ reels_before: reelIdsBefore ? reelIdsBefore.slice(0, 300) : undefined,
3342
+ reels_tab_url: reelsTabUrlBefore || reelsTabUrl || undefined,
3259
3343
  video_path_local: videoPath,
3260
3344
  };
3261
3345
  } finally {
@@ -3295,9 +3379,12 @@ module.exports.__testables = {
3295
3379
  resilientClick,
3296
3380
  confirmPublishDialog,
3297
3381
  diffNewReelIds,
3382
+ pickPostPublishReelsTabUrl,
3298
3383
  pickReelsTabUrl,
3299
3384
  networkIdVerdict,
3300
3385
  classifyComposerText,
3386
+ readPageAlerts,
3387
+ composerClosedError,
3301
3388
  isComposerDialogSignature,
3302
3389
  discoverReelsTabUrl,
3303
3390
  scrapeReelTileIds,