channel-worker 2.5.136 → 2.5.137

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.136",
3
+ "version": "2.5.137",
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": {
@@ -741,6 +741,15 @@ const NEW_COMPOSER_URL_RE = /^https?:\/\/(www\.)?facebook\.com\/(post\/create|re
741
741
  function isNewComposerUrl(url) {
742
742
  return NEW_COMPOSER_URL_RE.test(String(url || ''));
743
743
  }
744
+ // /reel/ hoặc /reel/<id>: Facebook có lúc PHỦ hộp thoại composer lên feed
745
+ // Reels (10/09; Nuôi Gà Làm Giàu 23/09 13:16Z — đúng composer KIỂU CŨ "Cài đặt
746
+ // thước phim" + nút "Chỉnh sửa" trên ảnh video, ảnh dump lệnh 6ab3d12d). Trước
747
+ // đây cả hai khối ảnh bìa đều bỏ qua URL này → "Custom thumbnail required".
748
+ // Ở đây phải hỏi DOM xem là composer nào (xem vòng bước đăng).
749
+ const REEL_OVERLAY_URL_RE = /^https?:\/\/(www\.)?facebook\.com\/reel\/(\d+\/?)?($|\?|#)/i;
750
+ function isReelOverlayUrl(url) {
751
+ return REEL_OVERLAY_URL_RE.test(String(url || ''));
752
+ }
744
753
  // Chữ trên ô "tải ảnh bìa của mình lên" — khớp NGUYÊN chuỗi (sau khi gộp
745
754
  // khoảng trắng) để không dính caption hay nhãn mục "Hình thu nhỏ".
746
755
  const THUMB_PICKER_TEXT_RE = /^(?:Chọn hình thu nhỏ|Chọn ảnh thu nhỏ|Thêm hình thu nhỏ|Tải hình thu nhỏ lên|(?:Choose|Select|Add|Upload) (?:a )?(?:custom )?thumbnail)$/i;
@@ -1231,6 +1240,11 @@ async function uploadViaThumbPickerModal(page, thumbPath, log, modal, { previewM
1231
1240
  // đổi-URL, kèm ghi rõ vào log. Ảnh chưa nạp xong thì chờ nhịp sau.
1232
1241
  lastCmp = await previewMatchesThumb(page, thumbDataUrl);
1233
1242
  if (lastCmp.err === 'preview-not-loaded') continue;
1243
+ if (lastCmp.err === 'tainted') {
1244
+ // Ảnh xem trước từ CDN → tải về phía Node mà so, không đoán theo URL.
1245
+ const rm = await remoteImagesMatchThumb(page, [m.preview], thumbDataUrl);
1246
+ if (rm) lastCmp = { diff: rm.diff, via: 'cdn' };
1247
+ }
1234
1248
  if (lastCmp.diff !== undefined && lastCmp.diff > THUMB_PREVIEW_MAX_DIFF) {
1235
1249
  if (!loggedMismatch) log('info', `[fb-pw] composer mới — ảnh xem trước đổi nhưng KHÔNG giống ảnh bìa của mình (lệch ${lastCmp.diff}/255) — chờ tiếp`);
1236
1250
  loggedMismatch = true;
@@ -1304,6 +1318,49 @@ async function clickPickerAndUpload(page, thumbPath, log) {
1304
1318
  return { via: '' };
1305
1319
  }
1306
1320
 
1321
+ // So ảnh nạp từ CDN của Facebook (khác nguồn → canvas bị khoá) với ảnh bìa:
1322
+ // tải bytes bằng page.request phía Node (không qua CORS), đưa lại vào trang
1323
+ // dưới dạng data: URL (cùng nguồn) rồi so 16x16 như previewMatchesThumb. Cần
1324
+ // cho lượt Gà Vườn 23/09 12:55Z: hộp thoại nhận ảnh xong, composer hiện ảnh bìa
1325
+ // bằng URL scontent → "đọc được 0 ảnh, khoá 4" → không so được → chặn oan.
1326
+ // `cache` (Map src→diff) để vòng chờ không tải lại cùng một ảnh mỗi nhịp.
1327
+ async function remoteImagesMatchThumb(page, srcs, thumbDataUrl, cache = new Map()) {
1328
+ let best = null;
1329
+ for (const src of [...new Set(srcs || [])].slice(0, 6)) {
1330
+ if (!/^https?:/i.test(src)) continue;
1331
+ let diff = cache.has(src) ? cache.get(src) : undefined;
1332
+ if (diff === undefined) {
1333
+ diff = null;
1334
+ try {
1335
+ const resp = await page.request.get(src, { timeout: 10_000 });
1336
+ const buf = resp.ok() ? await resp.body() : null;
1337
+ if (buf && buf.length && buf.length < 8 * 1024 * 1024) {
1338
+ const ct = String(resp.headers()['content-type'] || 'image/jpeg').split(';')[0];
1339
+ diff = await page.evaluate(async ({ a, b }) => {
1340
+ const load = (u) => new Promise((res) => { const i = new Image(); i.onload = () => res(i); i.onerror = () => res(null); i.src = u; });
1341
+ const [ia, ib] = await Promise.all([load(a), load(b)]);
1342
+ if (!ia || !ib) return null;
1343
+ const sig = (img) => {
1344
+ const c = document.createElement('canvas');
1345
+ c.width = 16; c.height = 16;
1346
+ const x = c.getContext('2d');
1347
+ x.drawImage(img, 0, 0, 16, 16);
1348
+ return x.getImageData(0, 0, 16, 16).data;
1349
+ };
1350
+ const p = sig(ia), q = sig(ib);
1351
+ let d = 0;
1352
+ for (let k = 0; k < p.length; k += 4) d += Math.abs(p[k] - q[k]) + Math.abs(p[k + 1] - q[k + 1]) + Math.abs(p[k + 2] - q[k + 2]);
1353
+ return Math.round(d / (256 * 3));
1354
+ }, { a: `data:${ct};base64,${buf.toString('base64')}`, b: thumbDataUrl }).catch(() => null);
1355
+ }
1356
+ } catch { /* tải hỏng = không tính ảnh này */ }
1357
+ cache.set(src, diff);
1358
+ }
1359
+ if (diff !== null && diff !== undefined && (!best || diff < best.diff)) best = { diff, src: src.slice(0, 80) };
1360
+ }
1361
+ return best;
1362
+ }
1363
+
1307
1364
  // Ảnh bìa của MÌNH có đang hiện trong composer không? Quét mọi ảnh ≥40px trong
1308
1365
  // composer (dialog Reel + vùng main của trang tạo), trừ hộp thoại chọn ảnh, so
1309
1366
  // nội dung 16x16 với file ảnh bìa như previewMatchesThumb. Dùng cho biến thể nút
@@ -1333,6 +1390,7 @@ async function composerShowsThumb(page, thumbDataUrl) {
1333
1390
  const b = sig(mine);
1334
1391
  const seen = new Set();
1335
1392
  let readable = 0, locked = 0, best = null;
1393
+ const lockedSrcs = [];
1336
1394
  for (const root of roots) {
1337
1395
  for (const im of root.querySelectorAll('img')) {
1338
1396
  if (seen.has(im) || im.closest('[__fbpw_thumb_modal__]')) continue;
@@ -1340,7 +1398,7 @@ async function composerShowsThumb(page, thumbDataUrl) {
1340
1398
  const r = im.getBoundingClientRect();
1341
1399
  if (r.width < 40 || r.height < 40 || !vis(im) || !im.complete || !im.naturalWidth) continue;
1342
1400
  let a;
1343
- try { a = sig(im); } catch { locked++; continue; }
1401
+ try { a = sig(im); } catch { locked++; lockedSrcs.push(String(im.currentSrc || im.src || '')); continue; }
1344
1402
  readable++;
1345
1403
  let d = 0;
1346
1404
  for (let k = 0; k < a.length; k += 4) d += Math.abs(a[k] - b[k]) + Math.abs(a[k + 1] - b[k + 1]) + Math.abs(a[k + 2] - b[k + 2]);
@@ -1348,7 +1406,7 @@ async function composerShowsThumb(page, thumbDataUrl) {
1348
1406
  if (!best || d < best.diff) best = { diff: d, size: `${Math.round(r.width)}x${Math.round(r.height)}` };
1349
1407
  }
1350
1408
  }
1351
- return { readable, locked, best };
1409
+ return { readable, locked, best, lockedSrcs: lockedSrcs.slice(0, 8) };
1352
1410
  }, { ours: thumbDataUrl }).catch((e) => ({ err: String(e && e.message || e).slice(0, 60) }));
1353
1411
  }
1354
1412
 
@@ -1376,15 +1434,22 @@ async function applyCoverButtonThumb(page, thumbPath, log, snap, { verifyMs = 30
1376
1434
  log('info', `[fb-pw] composer mới — đã đưa file ảnh bìa vào qua ${up.via}, tìm chính ảnh đó trong composer…`);
1377
1435
  let streak = 0;
1378
1436
  let scan = null;
1437
+ let remote = null;
1438
+ const remoteCache = new Map();
1379
1439
  const deadline = Date.now() + verifyMs;
1380
1440
  while (Date.now() < deadline) {
1381
1441
  await pause(page, 2500);
1382
1442
  if (await isCustomThumbApplied(page)) { if (++streak >= 2) break; continue; }
1383
1443
  scan = await composerShowsThumb(page, thumbDataUrl);
1384
1444
  if (scan.best && scan.best.diff <= THUMB_PREVIEW_MAX_DIFF) { if (++streak >= 2) break; continue; }
1445
+ // Ảnh CDN khoá canvas → tải về phía Node mà so (xem remoteImagesMatchThumb).
1446
+ if (scan.lockedSrcs && scan.lockedSrcs.length) {
1447
+ remote = await remoteImagesMatchThumb(page, scan.lockedSrcs, thumbDataUrl, remoteCache);
1448
+ if (remote && remote.diff <= THUMB_PREVIEW_MAX_DIFF) { if (++streak >= 2) break; continue; }
1449
+ }
1385
1450
  streak = 0;
1386
1451
  }
1387
- const fmt = (x) => (x && !x.err ? `đọc được ${x.readable} ảnh, khoá ${x.locked}, gần nhất lệch ${x.best ? `${x.best.diff}/255 (${x.best.size})` : '—'}` : `lỗi ${x && x.err}`);
1452
+ const fmt = (x) => (x && !x.err ? `đọc được ${x.readable} ảnh, khoá ${x.locked}, gần nhất lệch ${x.best ? `${x.best.diff}/255 (${x.best.size})` : '—'}${x === scan && remote ? `, ảnh CDN tải về gần nhất lệch ${remote.diff}/255` : ''}` : `lỗi ${x && x.err}`);
1388
1453
  if (streak >= 2) {
1389
1454
  log('info', `[fb-pw] composer mới — ảnh bìa đã vào: composer hiện đúng ảnh của mình (${fmt(scan)}; trước khi dán: ${fmt(base)})`);
1390
1455
  return { applied: true, reason: 'composer hiện đúng ảnh' };
@@ -1994,7 +2059,7 @@ async function runOnce({ page, payload, log }) {
1994
2059
  } = payload || {};
1995
2060
  if (!video_url) throw new Error('No video_url provided');
1996
2061
 
1997
- log('info', '[fb-pw] selectors version=2026.09.23-cover-button');
2062
+ log('info', '[fb-pw] selectors version=2026.09.23-reel-overlay-cdn');
1998
2063
 
1999
2064
  page.on('dialog', (d) => { d.accept().catch(() => {}); });
2000
2065
 
@@ -2863,7 +2928,16 @@ async function runOnce({ page, payload, log }) {
2863
2928
  // thu nhỏ" trong hàng khung, không có nút "Chỉnh sửa hình thu nhỏ". Xem
2864
2929
  // applyNewComposerThumb. Chạy ở mọi bước cho tới khi dán được: form một
2865
2930
  // trang chỉ có một bước, wizard có thể để hàng ô ở bước sau.
2866
- if (!customThumbDone && thumbPath && isNewComposerUrl(page.url())) {
2931
+ // Ở /reel/ (composer phủ lên feed Reels) hỏi DOM: có ô "Chọn hình thu
2932
+ // nhỏ"/nút "Chọn ảnh bìa" thì là composer mới, không thì khối cũ lo.
2933
+ const onReelOverlay = isReelOverlayUrl(page.url());
2934
+ let reelOverlayNew = false;
2935
+ if (!customThumbDone && thumbPath && onReelOverlay) {
2936
+ const probe = await readNewComposerThumbRow(page);
2937
+ reelOverlayNew = !!(probe && probe.pickerPresent);
2938
+ log('info', `[fb-pw] composer phủ trên /reel/ — ${reelOverlayNew ? 'có dấu hiệu composer mới → luồng mới' : 'composer kiểu cũ → khối ảnh bìa cũ'}`);
2939
+ }
2940
+ if (!customThumbDone && thumbPath && (isNewComposerUrl(page.url()) || reelOverlayNew)) {
2867
2941
  const nt = await applyNewComposerThumb(page, thumbPath, log);
2868
2942
  if (nt.applied) customThumbDone = true;
2869
2943
  }
@@ -2872,8 +2946,8 @@ async function runOnce({ page, payload, log }) {
2872
2946
  // video preview → opens "Chỉnh sửa hình thu nhỏ" modal → "Tải lên" →
2873
2947
  // file picker → "Lưu"). Per user screenshots, this is the only way to
2874
2948
  // attach a custom thumbnail in the Page-wall composer.
2875
- if (!customThumbDone && thumbPath && /^https?:\/\/(www\.)?facebook\.com\/?($|\?|#)/.test(page.url())) {
2876
- const findThumbBtn = () => page.evaluate(() => {
2949
+ if (!customThumbDone && thumbPath && (/^https?:\/\/(www\.)?facebook\.com\/?($|\?|#)/.test(page.url()) || (onReelOverlay && !reelOverlayNew))) {
2950
+ const findThumbBtn = () => page.evaluate((guardNew) => {
2877
2951
  // Look for the thumbnail-edit button inside the REEL COMPOSER dialog
2878
2952
  // only — scanning all dialogs risked matching a button on the
2879
2953
  // page-wall photo composer (which would post a separate "Tin dạng
@@ -2893,6 +2967,11 @@ async function runOnce({ page, payload, log }) {
2893
2967
  if (r.width < 8 || r.height < 8) continue;
2894
2968
  const cs = getComputedStyle(dlg);
2895
2969
  if (cs.visibility === 'hidden' || cs.display === 'none') continue;
2970
+ // Trên /reel/: composer MỚI có nút "Chỉnh sửa" riêng trên ô video —
2971
+ // bộ dò narrow-pill dưới đây sẽ bấm nhầm vào trình sửa video. Thấy
2972
+ // dấu hiệu composer mới thì không đụng (luồng mới sẽ lo ở bước sau).
2973
+ if (guardNew && (/Chọn hình thu nhỏ|File phương tiện đã tải lên/i.test(dlg.innerText || '')
2974
+ || dlg.querySelector("[aria-label='Chọn ảnh bìa']"))) continue;
2896
2975
  const cands = dlg.querySelectorAll("[role='button'], button, [aria-label]");
2897
2976
  for (const el of cands) {
2898
2977
  const t = (el.innerText || '').trim();
@@ -2915,7 +2994,7 @@ async function runOnce({ page, payload, log }) {
2915
2994
  }
2916
2995
  }
2917
2996
  return null;
2918
- }).catch(() => null);
2997
+ }, onReelOverlay).catch(() => null);
2919
2998
  let editBtn = await findThumbBtn();
2920
2999
  // The "Chỉnh sửa hình thu nhỏ" pill appears AFTER FB finishes processing
2921
3000
  // the video AND, on the Page-wall composer, only on a LATER step (the
@@ -3928,7 +4007,7 @@ async function runOnce({ page, payload, log }) {
3928
4007
  // any "Chia sẻ" text on the page falls through to a feed-item's share
3929
4008
  // button or sidebar nav, navigating AWAY from the composer and
3930
4009
  // breaking the flow (iter-38 ended up on /profile.php?sk=reels_tab).
3931
- const onPageWall = /^https?:\/\/(www\.)?facebook\.com\/?($|\?|#)/.test(page.url());
4010
+ const onPageWall = /^https?:\/\/(www\.)?facebook\.com\/?($|\?|#)/.test(page.url()) || isReelOverlayUrl(page.url());
3932
4011
  if (step >= 2 && !published && !onPageWall) {
3933
4012
  log('info', '[fb-pw] Tiếp ineffective — trying side-wizard jump to "Chia sẻ" step (BS only)');
3934
4013
  const jumpBbox = await page.evaluate(() => {
@@ -4477,6 +4556,8 @@ module.exports.__testables = {
4477
4556
  COVER_HEAD_RE,
4478
4557
  composerShowsThumb,
4479
4558
  applyCoverButtonThumb,
4559
+ remoteImagesMatchThumb,
4560
+ isReelOverlayUrl,
4480
4561
  clickPickerAndUpload,
4481
4562
  // Dùng chung cho comment_facebook: script nào trượt giữa chừng cũng cần chụp
4482
4563
  // lại màn hình, viết bản thứ hai là sớm muộn hai bên lệch nhau.