channel-worker 2.5.121 → 2.5.123

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.121",
3
+ "version": "2.5.123",
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": {
@@ -640,7 +640,34 @@ async function isCustomThumbApplied(page) {
640
640
  if (/Gỡ hình thu nhỏ tùy chỉnh|Remove( your)? custom thumbnail/i.test(al) && vis(el)) return true;
641
641
  }
642
642
  return false;
643
- }).catch(() => false);
643
+ // `null` = KHÔNG ĐO ĐƯỢC (page.evaluate ném giữa lúc FB dựng lại panel).
644
+ // Bản cũ nuốt thành `false` = "không có thumb", và một mẫu đơn lẻ như thế
645
+ // được quyền bấm Hủy = GỠ ảnh bìa vừa dán. Xem thumbVerdict().
646
+ }).catch(() => null);
647
+ }
648
+
649
+ // Một lần đo KHÔNG được quyết định hành động PHÁ. Đo trên log prod 20/09/2026:
650
+ // trong 152 lượt "hết giờ chờ hộp ảnh bìa đóng", 138 lượt đo thấy thumb đang
651
+ // áp rồi đăng bình thường, còn 14 lượt mẫu CUỐI trả "không áp" (trong khi suốt
652
+ // vòng chờ trước đó mẫu nào cũng "đang áp") → rơi vào nhánh ESC+Hủy, gỡ mất
653
+ // ảnh bìa, rồi chết ở cổng "Custom thumbnail required but not applied".
654
+ // 'applied' — có ít nhất một mẫu thấy nút "Gỡ hình thu nhỏ tùy chỉnh"
655
+ // 'unknown' — không mẫu nào thấy, nhưng có mẫu đo hỏng
656
+ // 'absent' — mọi mẫu đo được và đều không thấy
657
+ function thumbVerdict(samples) {
658
+ const list = Array.isArray(samples) ? samples : [];
659
+ if (list.some((s) => s === true)) return 'applied';
660
+ if (list.some((s) => s === null || s === undefined)) return 'unknown';
661
+ return 'absent';
662
+ }
663
+
664
+ async function sampleCustomThumb(page, { samples = 3, gapMs = 1500 } = {}) {
665
+ const out = [];
666
+ for (let i = 0; i < samples; i++) {
667
+ if (i) await pause(page, gapMs);
668
+ out.push(await isCustomThumbApplied(page));
669
+ }
670
+ return { verdict: thumbVerdict(out), samples: out };
644
671
  }
645
672
 
646
673
  // Composer đang ở trạng thái gì? Đọc từ CHỮ trong hộp thoại — ba chữ ký nhìn
@@ -2136,6 +2163,7 @@ async function runOnce({ page, payload, log }) {
2136
2163
  // doesn't close within 12s of polling, RE-CLICK Lưu (in case
2137
2164
  // the first click didn't register because the button was
2138
2165
  // disabled while FB was still hashing the upload).
2166
+ let thumbTimeoutSt = { verdict: 'unknown', samples: [] };
2139
2167
  const closeDeadline = Date.now() + 60_000;
2140
2168
  let modalClosed = false;
2141
2169
  let closedPolls = 0;
@@ -2230,7 +2258,11 @@ async function runOnce({ page, payload, log }) {
2230
2258
  }
2231
2259
  thumbApplied = true;
2232
2260
  log('info', `[fb-pw] page-wall thumb — modal closed after save (retries=${retryCount})`);
2233
- } else if (await isCustomThumbApplied(page)) {
2261
+ } else if (await (async () => {
2262
+ // Giữ kết quả đo để ghi vào log: bằng chứng vì sao KHÔNG Hủy.
2263
+ thumbTimeoutSt = await sampleCustomThumb(page);
2264
+ return thumbTimeoutSt.verdict !== 'absent';
2265
+ })()) {
2234
2266
  // Hết giờ chờ đóng nhưng bằng chứng áp thumb còn đó — tuyệt
2235
2267
  // đối không ESC/Hủy (Hủy trên panel này là GỠ thumb).
2236
2268
  //
@@ -2241,10 +2273,10 @@ async function runOnce({ page, payload, log }) {
2241
2273
  // "Custom thumbnail required but not applied" (đo 04/09/2026,
2242
2274
  // 3 lượt liên tiếp, log ghi rõ variant=modal). Nút "Gỡ" là
2243
2275
  // bằng chứng vật lý thumb đang áp, biến thể nào cũng đúng.
2244
- log('info', `[fb-pw] page-wall thumb — hết giờ chờ đóng nhưng nút "Gỡ hình thu nhỏ" đang hiện (variant=${thumbEditorVia}) → giữ panel, coi như dán xong`);
2276
+ log('info', `[fb-pw] page-wall thumb — hết giờ chờ đóng, đo 3 nhịp ${JSON.stringify(thumbTimeoutSt.samples)} → ${thumbTimeoutSt.verdict} (variant=${thumbEditorVia}) → giữ panel, coi như dán xong, KHÔNG Hủy`);
2245
2277
  thumbApplied = true;
2246
2278
  } else {
2247
- log('warn', '[fb-pw] page-wall thumb — modal still open 60s after Lưu click; trying ESC + click Hủy fallback');
2279
+ log('warn', `[fb-pw] page-wall thumb — modal still open 60s after Lưu click, đo 3 nhịp ${JSON.stringify(thumbTimeoutSt.samples)} → CHẮC CHẮN chưa áp; ESC + click Hủy fallback`);
2248
2280
  // Chụp lại trước khi ESC/Hủy: đây là nhánh phá lượt nhiều
2249
2281
  // nhất mà lại không có ảnh nào để soi (04/09/2026).
2250
2282
  await dumpFailure(page, 'thumb-modal-stuck-before-cancel', log);
@@ -2625,8 +2657,9 @@ async function runOnce({ page, payload, log }) {
2625
2657
  // customThumbDone lỡ false trong khi nút "Gỡ hình thu nhỏ tùy chỉnh"
2626
2658
  // ngay trên bước này chứng minh thumb ĐANG được áp — cả 5 lượt fail
2627
2659
  // 02-03/09 của idea 6a965a32 đều ở trạng thái này (diag 158386).
2628
- if (await isCustomThumbApplied(page)) {
2629
- log('info', '[fb-pw] pre-publish: nút "Gỡ hình thu nhỏ tùy chỉnh" đang hiện — thumb đã áp dù cờ chưa bật, tiếp tục đăng');
2660
+ const gateSt = await sampleCustomThumb(page);
2661
+ if (gateSt.verdict === 'applied') {
2662
+ log('info', `[fb-pw] pre-publish: nút "Gỡ hình thu nhỏ tùy chỉnh" đang hiện (${JSON.stringify(gateSt.samples)}) — thumb đã áp dù cờ chưa bật, tiếp tục đăng`);
2630
2663
  customThumbDone = true;
2631
2664
  } else {
2632
2665
  // Defensive: we normally fail at the caption step (before any Tiếp)
@@ -3501,6 +3534,9 @@ module.exports = { run };
3501
3534
  // Exposed so the overlay-click escalation can be exercised against a synthetic
3502
3535
  // page (toast covering the CTA) without driving the whole upload flow.
3503
3536
  module.exports.__testables = {
3537
+ // 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
+ // được biến thành "chưa áp" rồi kéo theo hành động PHÁ (Hủy = gỡ ảnh bìa).
3539
+ thumbVerdict,
3504
3540
  // Dùng chung cho comment_facebook: script nào trượt giữa chừng cũng cần chụp
3505
3541
  // lại màn hình, viết bản thứ hai là sớm muộn hai bên lệch nhau.
3506
3542
  dumpFailure,
@@ -11,7 +11,7 @@
11
11
  // caption hơn, API tự biết là không đủ để ghép và để nguyên cho người.
12
12
  //
13
13
  // Payload: { channel_id, post_ids: ['1234567890', …] }
14
- // Trả về: { captions: [{ post_id, og, page_text, url }], read, asked }
14
+ // Trả về: { captions: [{ post_id, og, page_text, blocks, url }], read, asked }
15
15
  //
16
16
  // Việc SO SÁNH nằm ở API (services/ReelCaptionMatch) chứ không ở đây: một bảng
17
17
  // luật, test offline được, và worker không cần biết tiêu đề của idea.
@@ -23,17 +23,52 @@ const SETTLE_MS = 4000;
23
23
  // một kênh hiếm khi quá vài cái; nhiều hơn thì để lượt sau, đừng giữ profile lâu.
24
24
  const MAX_POSTS = 8;
25
25
 
26
- // og:description là mô tả của ĐÚNG reel đang mở. Văn bản trang là đường lui —
27
- // trình xem reel cuộn cả reel kế bên vào DOM nên API chỉ tin phần đầu của nó.
26
+ // Đuôi Facebook thêm vào khi cắt caption dài. Phải gỡ, không thì khối chữ
27
+ // không còn là "khúc đầu của tiêu đề" và luật ghép theo tiền tố trượt.
28
+ const MORE_TAIL_RE = /(?:[…\.]{1,3}\s*)?(?:see more|xem thêm|see translation|xem bản dịch)\s*$/i;
29
+
30
+ function cleanBlock(s) {
31
+ return String(s == null ? '' : s)
32
+ .replace(/\s+/g, ' ')
33
+ .trim()
34
+ .replace(MORE_TAIL_RE, '')
35
+ .replace(/[…\s]+$/, '')
36
+ .trim();
37
+ }
38
+
39
+ // Lấy mọi đường có thể có caption. ĐO TẬN TAY 20/09/2026 trên reel
40
+ // 4007094836262842 (kênh "tán gái", profile kenh-25), trang mở trong phiên ĐÃ
41
+ // ĐĂNG NHẬP:
42
+ // - `document.querySelectorAll('meta[property]')` → **RỖNG**. Trang reel của
43
+ // phiên đăng nhập không có thẻ og nào, nên og luôn 0 ký tự. Giữ lại vì
44
+ // phiên khác (chưa đăng nhập, share link) vẫn có.
45
+ // - `[role='main']` chỉ có **18 ký tự** — caption KHÔNG nằm trong đó. Bản cũ
46
+ // đọc đúng chỗ này nên 3 bài "đăng mập mờ" của kênh treo 2 ngày, ghép=0.
47
+ // - `body.innerText` (331 ký tự) CÓ caption nhưng dính rác đầu trang
48
+ // ("Number of unread notifications 20+ 119 433K 2.4K 1.9K <tên trang> …").
49
+ // - Khối `span[dir='auto']` dài nhất CHÍNH LÀ caption, kèm đuôi "… See more".
50
+ // Nên: trả cả ba — og, page_text (đổi sang body), và `blocks` = các khối
51
+ // dir=auto đã gỡ đuôi, để API so khớp trên từng khối (ReelCaptionMatch).
28
52
  async function readCaption(page) {
29
53
  return page.evaluate(() => {
54
+ const flat = (t) => String(t || '').replace(/\s+/g, ' ').trim();
30
55
  const meta = (p) => (document.querySelector(`meta[property="${p}"]`) || {}).content || '';
31
- const main = document.querySelector("[role='main']") || document.body;
56
+ const blocks = [...document.querySelectorAll("span[dir='auto'], div[dir='auto']")]
57
+ .map((el) => flat(el.innerText))
58
+ .filter((t) => t.length >= 20)
59
+ .sort((a, b) => b.length - a.length)
60
+ .slice(0, 6);
32
61
  return {
33
62
  og: (meta('og:description') || meta('og:title') || '').slice(0, 1000),
34
- page_text: String((main && main.innerText) || '').replace(/\s+/g, ' ').trim().slice(0, 1500),
63
+ page_text: flat(document.body && document.body.innerText).slice(0, 1500),
64
+ blocks,
35
65
  };
36
- }).catch(() => ({ og: '', page_text: '' }));
66
+ }).then((c) => ({
67
+ og: cleanBlock(c.og).slice(0, 1000),
68
+ page_text: c.page_text,
69
+ // Bỏ trùng sau khi gỡ đuôi: khối cha và khối con thường ra cùng một chuỗi.
70
+ blocks: [...new Set((c.blocks || []).map(cleanBlock).filter((t) => t.length >= 20))].slice(0, 6),
71
+ })).catch(() => ({ og: '', page_text: '', blocks: [] }));
37
72
  }
38
73
 
39
74
  async function run({ page, payload = {}, log }) {
@@ -50,9 +85,10 @@ async function run({ page, payload = {}, log }) {
50
85
  await page.goto(url, { waitUntil: 'domcontentloaded', timeout: NAV_TIMEOUT_MS });
51
86
  await page.waitForTimeout(SETTLE_MS);
52
87
  const c = await readCaption(page);
53
- captions.push({ post_id: id, og: c.og, page_text: c.page_text, url });
54
- log('info', `[fb-verify] reel ${id}: og ${c.og.length} ký tự, trang ${c.page_text.length} ký tự`
55
- + (c.og ? ` — "${c.og.slice(0, 60)}"` : ''));
88
+ captions.push({ post_id: id, og: c.og, page_text: c.page_text, blocks: c.blocks, url });
89
+ const best = (c.blocks && c.blocks[0]) || c.og || '';
90
+ log('info', `[fb-verify] reel ${id}: og ${c.og.length} ký tự, trang ${c.page_text.length} ký tự, `
91
+ + `${(c.blocks || []).length} khối` + (best ? ` — "${best.slice(0, 60)}"` : ' — KHÔNG đọc được chữ nào'));
56
92
  } catch (e) {
57
93
  // Một reel đọc hỏng KHÔNG được giết cả lượt: các reel còn lại vẫn ghép được.
58
94
  log('warn', `[fb-verify] reel ${id} đọc hỏng: ${String(e && e.message || e).slice(0, 90)}`);
@@ -63,4 +99,4 @@ async function run({ page, payload = {}, log }) {
63
99
  }
64
100
 
65
101
  module.exports = { run };
66
- module.exports.__testables = { readCaption, MAX_POSTS, SETTLE_MS };
102
+ module.exports.__testables = { readCaption, cleanBlock, MORE_TAIL_RE, MAX_POSTS, SETTLE_MS };