channel-worker 2.5.135 → 2.5.136

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.135",
3
+ "version": "2.5.136",
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": {
@@ -744,6 +744,14 @@ function isNewComposerUrl(url) {
744
744
  // 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
745
  // khoảng trắng) để không dính caption hay nhãn mục "Hình thu nhỏ".
746
746
  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;
747
+ // Biến thể thứ ba (đo thật 23/09, page Gà Vườn Mỗi Ngày, wizard
748
+ // /reels/create/?surface=ADDL_PROFILE_PLUS): KHÔNG có hàng ô, ảnh bìa là một
749
+ // nút nhỏ 72x20 chữ "Ảnh bìa", aria-label "Chọn ảnh bìa", có ở cả bước
750
+ // "Chỉnh sửa" lẫn "Cài đặt". Tách regex riêng: nút này không nằm trong hàng ô
751
+ // nào, đi luồng riêng (applyCoverButtonThumb) và xác minh bằng nội dung ảnh.
752
+ const COVER_BUTTON_RE = /^(?:Chọn ảnh bìa|(?:Choose|Select|Add|Edit) (?:a )?cover(?: photo| image)?)$/i;
753
+ // Tiêu đề hộp thoại có thể mở ra từ nút đó — chưa thấy tận mắt, nhận rộng.
754
+ const COVER_HEAD_RE = /^(?:Chọn ảnh bìa|Ảnh bìa|Chỉnh sửa ảnh bìa|(?:Choose |Select |Edit )?(?:a )?cover(?: photo| image)?)$/i;
747
755
 
748
756
  // Đọc hàng ô ảnh bìa của composer mới. Trả về ảnh chụp trạng thái để so
749
757
  // trước/sau khi dán (xem newComposerThumbVerdict), và gắn cờ DOM cho:
@@ -753,7 +761,7 @@ const THUMB_PICKER_TEXT_RE = /^(?:Chọn hình thu nhỏ|Chọn ảnh thu nhỏ|
753
761
  // __fbpw_thumb_tile__ — ô thứ `tagIndex` (dùng khi phải bấm chọn lại)
754
762
  // Không bao giờ ném: đọc hỏng trả null, như isCustomThumbApplied.
755
763
  async function readNewComposerThumbRow(page, { diag = false, tagIndex = -1 } = {}) {
756
- return page.evaluate(({ pickSrc, diag, tagIndex }) => {
764
+ return page.evaluate(({ pickSrc, coverSrc, diag, tagIndex }) => {
757
765
  const PICK = new RegExp(pickSrc, 'i');
758
766
  const norm = (s) => String(s || '').replace(/\s+/g, ' ').trim();
759
767
  const vis = (el) => {
@@ -880,6 +888,23 @@ async function readNewComposerThumbRow(page, { diag = false, tagIndex = -1 } = {
880
888
  }
881
889
  const customMarker = [...document.querySelectorAll('[aria-label]')].some((el) =>
882
890
  /Gỡ hình thu nhỏ tùy chỉnh|Remove( your)? custom thumbnail/i.test(el.getAttribute('aria-label') || '') && vis(el));
891
+ if (!row && !pickLeaf) {
892
+ // Không có ô "Chọn hình thu nhỏ" → thử nút "Chọn ảnh bìa" (biến thể wizard).
893
+ const COVER = new RegExp(coverSrc, 'i');
894
+ for (const root of roots) {
895
+ const btn = [...root.querySelectorAll('[aria-label]')].find((el) => vis(el)
896
+ && COVER.test(norm(el.getAttribute('aria-label'))) && !el.closest('[__fbpw_thumb_modal__]'));
897
+ if (!btn) continue;
898
+ let target = btn;
899
+ for (let n = btn, d = 0; n && d < 3; n = n.parentElement, d++) {
900
+ if (n.matches("[role='button'], button, a, [tabindex]")) { target = n; break; }
901
+ }
902
+ target.setAttribute('__fbpw_thumb_pick__', '1');
903
+ const r = btn.getBoundingClientRect();
904
+ return { found: false, pickerPresent: true, mode: 'cover-button', roots: roots.length, customMarker,
905
+ cover: { aria: norm(btn.getAttribute('aria-label')), text: norm(btn.innerText).slice(0, 30), size: `${Math.round(r.width)}x${Math.round(r.height)}` } };
906
+ }
907
+ }
883
908
  if (!row) {
884
909
  const miss = { found: false, pickerPresent: !!pickLeaf, roots: roots.length, customMarker };
885
910
  if (diag) {
@@ -954,7 +979,7 @@ async function readNewComposerThumbRow(page, { diag = false, tagIndex = -1 } = {
954
979
  });
955
980
  }
956
981
  return res;
957
- }, { pickSrc: THUMB_PICKER_TEXT_RE.source, diag, tagIndex }).catch(() => null);
982
+ }, { pickSrc: THUMB_PICKER_TEXT_RE.source, coverSrc: COVER_BUTTON_RE.source, diag, tagIndex }).catch(() => null);
958
983
  }
959
984
 
960
985
  // Ảnh bìa tự chọn đã VÀO composer mới chưa? So hàng ô trước và sau khi dán
@@ -997,6 +1022,7 @@ function newComposerThumbVerdict(before, after) {
997
1022
  // Tóm tắt một hàng ô cho log: "5 ô [chọn-ảnh, *khung, khung…] chọn=1 input=image/*".
998
1023
  function describeThumbRow(snap) {
999
1024
  if (!snap) return 'không đọc được';
1025
+ if (snap.mode === 'cover-button') return `không có hàng ô — nút ảnh bìa "${snap.cover.text || snap.cover.aria}" (aria "${snap.cover.aria}", ${snap.cover.size})`;
1000
1026
  if (!snap.found) return `không thấy hàng ô (composer=${snap.roots || 0}, ô chọn-ảnh=${snap.pickerPresent ? 'có' : 'không'})`;
1001
1027
  const cells = (snap.tiles || []).map((t) => `${t.selected ? '*' : ''}${t.placeholder ? 'chọn-ảnh' : (t.img ? (/^(blob|data):/.test(t.img) ? 'ảnh-local' : 'khung') : '?')}`);
1002
1028
  return `${cells.length} ô [${cells.join(', ')}] chọn=${snap.selectedIndex ?? '?'} input=${snap.input ? snap.input.accept || '?' : 'không'} via=${snap.via}`;
@@ -1047,8 +1073,10 @@ async function confirmNewComposerThumbDialog(page, log) {
1047
1073
  // Gắn cờ: __fbpw_thumb_modal__ (khối), _upload__ (nút Tải lên), _save__ (mọi
1048
1074
  // nút Lưu, đánh số từ cuối), _input__ (input ảnh). Trả null nếu không thấy.
1049
1075
  async function findThumbPickerModal(page) {
1050
- return page.evaluate(({ pickSrc }) => {
1076
+ return page.evaluate(({ pickSrc, coverHeadSrc }) => {
1051
1077
  const PICK = new RegExp(pickSrc, 'i');
1078
+ const COVER_HEAD = new RegExp(coverHeadSrc, 'i');
1079
+ const isHead = (t) => PICK.test(t) || COVER_HEAD.test(t);
1052
1080
  const norm = (s) => String(s || '').replace(/\s+/g, ' ').trim();
1053
1081
  const vis = (el) => {
1054
1082
  const r = el.getBoundingClientRect();
@@ -1061,17 +1089,18 @@ async function findThumbPickerModal(page) {
1061
1089
  }
1062
1090
  const says = (el, re) => re.test(norm(el.getAttribute('aria-label'))) || re.test(norm(el.innerText));
1063
1091
  const UP = /^(Tải lên|Tải ảnh lên|Upload|Upload image)$/i;
1064
- const SAVE = /^(Lưu|Save)$/i;
1092
+ const SAVE = /^(Lưu|Save|Xong|Done|Áp dụng|Apply)$/i;
1065
1093
  const POST = /^(Đăng|Post|Share now)$/i;
1066
- const heads = [...document.querySelectorAll('[aria-label]')].filter((el) => PICK.test(norm(el.getAttribute('aria-label'))) && vis(el));
1094
+ const heads = [...document.querySelectorAll('[aria-label]')].filter((el) => isHead(norm(el.getAttribute('aria-label'))) && vis(el));
1067
1095
  const tw = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
1068
1096
  for (let n = tw.nextNode(); n; n = tw.nextNode()) {
1069
- if (!/hình|thumbnail/i.test(n.data)) continue;
1097
+ if (!/hình|thumbnail|ảnh bìa|cover/i.test(n.data)) continue;
1070
1098
  const el = n.parentElement;
1071
- if (el && vis(el) && PICK.test(norm(el.innerText))) heads.push(el);
1099
+ if (el && vis(el) && isHead(norm(el.innerText))) heads.push(el);
1072
1100
  }
1073
1101
  for (const h of heads) {
1074
1102
  if (h.closest('[__fbpw_thumb_row__]')) continue; // chính ô chọn-ảnh trong hàng
1103
+ if (h.closest('[__fbpw_thumb_pick__]')) continue; // chính nút "Chọn ảnh bìa"
1075
1104
  let box = h;
1076
1105
  for (let d = 0; d < 10 && box && box !== document.body; d++, box = box.parentElement) {
1077
1106
  const btns = [...box.querySelectorAll("[role='button'], button")].filter(vis);
@@ -1102,7 +1131,7 @@ async function findThumbPickerModal(page) {
1102
1131
  }
1103
1132
  }
1104
1133
  return null;
1105
- }, { pickSrc: THUMB_PICKER_TEXT_RE.source }).catch(() => null);
1134
+ }, { pickSrc: THUMB_PICKER_TEXT_RE.source, coverHeadSrc: COVER_HEAD_RE.source }).catch(() => null);
1106
1135
  }
1107
1136
 
1108
1137
  // Ảnh xem trước trong hộp thoại có đúng là ảnh bìa của MÌNH không? Thu cả hai
@@ -1195,7 +1224,7 @@ async function uploadViaThumbPickerModal(page, thumbPath, log, modal, { previewM
1195
1224
  while (Date.now() - t0 < previewMs) {
1196
1225
  await pause(page, 1500);
1197
1226
  m = await findThumbPickerModal(page);
1198
- if (!m) return { ok: true, via, reason: 'hộp thoại tự đóng sau khi nhận ảnh' };
1227
+ if (!m) return { ok: true, via, verified: false, reason: 'hộp thoại tự đóng sau khi nhận ảnh' };
1199
1228
  if (!m.preview || m.preview === beforePreview) continue;
1200
1229
  // URL đổi rồi — so NỘI DUNG. Lệch nhiều = FB đổi khung chứ chưa phải ảnh
1201
1230
  // của mình: chờ tiếp. Không so được (ảnh CDN khoá canvas) thì rơi về luật
@@ -1232,23 +1261,152 @@ async function uploadViaThumbPickerModal(page, thumbPath, log, modal, { previewM
1232
1261
  while (Date.now() - t1 < closeMs) {
1233
1262
  await pause(page, 1500);
1234
1263
  if (await findThumbPickerModal(page)) { gone = 0; continue; }
1235
- if (++gone >= 2) return { ok: true, via };
1264
+ if (++gone >= 2) return { ok: true, via, verified: !!(lastCmp && lastCmp.diff !== undefined) };
1236
1265
  }
1237
1266
  log('warn', `[fb-pw] composer mới — hộp thoại "Chọn hình thu nhỏ" chưa đóng ${Math.round(closeMs / 1000)}s sau khi bấm Lưu`);
1238
1267
  return { ok: false, reason: 'modal-not-closed' };
1239
1268
  }
1240
1269
 
1270
+ // Bấm nút/ô chọn ảnh bìa (đã gắn cờ __fbpw_thumb_pick__) rồi chờ MỘT trong
1271
+ // hai: hộp chọn file (biến thể mở thẳng) hoặc hộp thoại chọn ảnh (biến thể đo
1272
+ // thật 23/09). Trả { via, verified, failed, reason }: `via` rỗng = chưa đưa được
1273
+ // file vào (người gọi còn đường dự phòng); `failed` = hộp thoại đã mở mà dán hỏng.
1274
+ async function clickPickerAndUpload(page, thumbPath, log) {
1275
+ let chooser = null;
1276
+ const chooserP = page.waitForEvent('filechooser', { timeout: 8000 }).then((c) => { chooser = c; }).catch(() => {});
1277
+ let clickErr = '';
1278
+ await page.locator("[__fbpw_thumb_pick__='1']").first().click({ timeout: 5000 }).catch((e) => { clickErr = String(e && e.message || e).slice(0, 80); });
1279
+ let modal = null;
1280
+ const tClick = Date.now();
1281
+ while (!chooser && !modal && Date.now() - tClick < 8000) {
1282
+ await pause(page, 800);
1283
+ modal = await findThumbPickerModal(page);
1284
+ }
1285
+ void chooserP;
1286
+ if (chooser) {
1287
+ try {
1288
+ await chooser.setFiles(thumbPath);
1289
+ return { via: 'hộp chọn file', verified: false };
1290
+ } catch (e) {
1291
+ log('info', `[fb-pw] composer mới — hộp chọn file từ chối ảnh: ${String(e && e.message || e).slice(0, 80)}`);
1292
+ return { via: '' };
1293
+ }
1294
+ }
1295
+ if (modal) {
1296
+ const r = await uploadViaThumbPickerModal(page, thumbPath, log, modal);
1297
+ if (!r.ok) {
1298
+ log('warn', `[fb-pw] composer mới — dán qua hộp thoại chọn ảnh bìa KHÔNG xong (${r.reason})`);
1299
+ return { via: '', failed: true, reason: r.reason };
1300
+ }
1301
+ return { via: r.via + (r.reason ? ` (${r.reason})` : ''), verified: !!r.verified };
1302
+ }
1303
+ log('info', `[fb-pw] composer mới — bấm chỗ chọn ảnh bìa không mở hộp chọn file lẫn hộp thoại${clickErr ? `: ${clickErr}` : ''}`);
1304
+ return { via: '' };
1305
+ }
1306
+
1307
+ // Ảnh bìa của MÌNH có đang hiện trong composer không? Quét mọi ảnh ≥40px trong
1308
+ // composer (dialog Reel + vùng main của trang tạo), trừ hộp thoại chọn ảnh, so
1309
+ // nội dung 16x16 với file ảnh bìa như previewMatchesThumb. Dùng cho biến thể nút
1310
+ // "Chọn ảnh bìa": không có hàng ô để so trước/sau, nên bằng chứng duy nhất đứng
1311
+ // vững là thấy chính ảnh đó. Ảnh CDN khác nguồn khoá canvas → không tính.
1312
+ async function composerShowsThumb(page, thumbDataUrl) {
1313
+ return page.evaluate(async ({ ours }) => {
1314
+ const vis = (el) => {
1315
+ const r = el.getBoundingClientRect();
1316
+ if (r.width < 8 || r.height < 8) return false;
1317
+ const cs = getComputedStyle(el);
1318
+ return cs.visibility !== 'hidden' && cs.display !== 'none' && cs.opacity !== '0';
1319
+ };
1320
+ const roots = [...document.querySelectorAll("[role='dialog']")].filter((d) => vis(d)
1321
+ && /thước phim|reel/i.test(`${d.getAttribute('aria-label') || ''}\n${(d.innerText || '').slice(0, 600)}`));
1322
+ const main = document.querySelector("[role='main'], main");
1323
+ if (main && vis(main)) roots.push(main);
1324
+ const sig = (img) => {
1325
+ const c = document.createElement('canvas');
1326
+ c.width = 16; c.height = 16;
1327
+ const x = c.getContext('2d');
1328
+ x.drawImage(img, 0, 0, 16, 16);
1329
+ return x.getImageData(0, 0, 16, 16).data;
1330
+ };
1331
+ const mine = await new Promise((res) => { const i = new Image(); i.onload = () => res(i); i.onerror = () => res(null); i.src = ours; });
1332
+ if (!mine) return { err: 'thumb-not-decodable' };
1333
+ const b = sig(mine);
1334
+ const seen = new Set();
1335
+ let readable = 0, locked = 0, best = null;
1336
+ for (const root of roots) {
1337
+ for (const im of root.querySelectorAll('img')) {
1338
+ if (seen.has(im) || im.closest('[__fbpw_thumb_modal__]')) continue;
1339
+ seen.add(im);
1340
+ const r = im.getBoundingClientRect();
1341
+ if (r.width < 40 || r.height < 40 || !vis(im) || !im.complete || !im.naturalWidth) continue;
1342
+ let a;
1343
+ try { a = sig(im); } catch { locked++; continue; }
1344
+ readable++;
1345
+ let d = 0;
1346
+ 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]);
1347
+ d = Math.round(d / (256 * 3));
1348
+ if (!best || d < best.diff) best = { diff: d, size: `${Math.round(r.width)}x${Math.round(r.height)}` };
1349
+ }
1350
+ }
1351
+ return { readable, locked, best };
1352
+ }, { ours: thumbDataUrl }).catch((e) => ({ err: String(e && e.message || e).slice(0, 60) }));
1353
+ }
1354
+
1355
+ // Biến thể nút "Chọn ảnh bìa" (wizard, không có hàng ô). Chỉ nhận là đã dán khi:
1356
+ // a) hộp thoại chọn ảnh đã cho thấy ĐÚNG ảnh của mình (so nội dung) rồi mới
1357
+ // Lưu và đóng, hoặc
1358
+ // b) sau khi dán, composer hiện một ảnh giống ảnh bìa của mình (lệch ≤ ngưỡng)
1359
+ // hai nhịp liền, hoặc nút "Gỡ hình thu nhỏ tùy chỉnh" hiện.
1360
+ // Không thì trả không-dán, cổng trước Đăng chặn như mọi biến thể khác.
1361
+ async function applyCoverButtonThumb(page, thumbPath, log, snap, { verifyMs = 30_000 } = {}) {
1362
+ log('info', `[fb-pw] composer mới — ${describeThumbRow(snap)} → dán ảnh bìa qua nút này`);
1363
+ let thumbDataUrl = '';
1364
+ try { thumbDataUrl = `data:image/png;base64,${fs.readFileSync(thumbPath).toString('base64')}`; } catch { /* composerShowsThumb sẽ báo thumb-not-decodable */ }
1365
+ const base = await composerShowsThumb(page, thumbDataUrl);
1366
+ const up = await clickPickerAndUpload(page, thumbPath, log);
1367
+ if (up.failed || !up.via) {
1368
+ const d = await readNewComposerThumbRow(page, { diag: true });
1369
+ log('warn', `[fb-pw] composer mới — nút ảnh bìa: KHÔNG đưa được file vào (${up.reason || 'không mở được chỗ chọn ảnh'})${d && d.inventory ? ` nút=${JSON.stringify(d.inventory)}` : ''}`);
1370
+ return { applied: false, reason: up.reason || 'no-upload' };
1371
+ }
1372
+ if (up.verified) {
1373
+ log('info', `[fb-pw] composer mới — ảnh bìa đã vào qua ${up.via}: hộp thoại cho thấy đúng ảnh của mình rồi mới Lưu`);
1374
+ return { applied: true, reason: 'hộp thoại xác nhận đúng ảnh' };
1375
+ }
1376
+ 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
+ let streak = 0;
1378
+ let scan = null;
1379
+ const deadline = Date.now() + verifyMs;
1380
+ while (Date.now() < deadline) {
1381
+ await pause(page, 2500);
1382
+ if (await isCustomThumbApplied(page)) { if (++streak >= 2) break; continue; }
1383
+ scan = await composerShowsThumb(page, thumbDataUrl);
1384
+ if (scan.best && scan.best.diff <= THUMB_PREVIEW_MAX_DIFF) { if (++streak >= 2) break; continue; }
1385
+ streak = 0;
1386
+ }
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}`);
1388
+ if (streak >= 2) {
1389
+ 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
+ return { applied: true, reason: 'composer hiện đúng ảnh' };
1391
+ }
1392
+ const d = await readNewComposerThumbRow(page, { diag: true });
1393
+ log('warn', `[fb-pw] composer mới — nút ảnh bìa: KHÔNG thấy ảnh của mình trong composer sau ${Math.round(verifyMs / 1000)}s (${fmt(scan)}; trước khi dán: ${fmt(base)})${d && d.inventory ? ` nút=${JSON.stringify(d.inventory)}` : ''}`);
1394
+ return { applied: false, reason: 'cover-not-shown' };
1395
+ }
1396
+
1241
1397
  // Dán ảnh bìa vào composer mới. Trả { applied, reason }. Không ném: thất bại
1242
1398
  // thì cờ customThumbDone không bật và cổng trước Đăng chặn như cũ.
1243
1399
  async function applyNewComposerThumb(page, thumbPath, log, { waitPickerMs = 20_000, verifyMs = 30_000 } = {}) {
1244
1400
  const t0 = Date.now();
1245
1401
  const ready = (s) => !!(s && s.found && s.pickerPresent);
1402
+ const coverMode = (s) => !!(s && s.mode === 'cover-button');
1246
1403
  let snap = await readNewComposerThumbRow(page);
1247
1404
  // Hàng ảnh bìa chỉ hiện sau khi FB cắt xong khung từ video.
1248
- while (!ready(snap) && Date.now() - t0 < waitPickerMs) {
1405
+ while (!ready(snap) && !coverMode(snap) && Date.now() - t0 < waitPickerMs) {
1249
1406
  await pause(page, 2500);
1250
1407
  snap = await readNewComposerThumbRow(page);
1251
1408
  }
1409
+ if (coverMode(snap)) return applyCoverButtonThumb(page, thumbPath, log, snap, { verifyMs });
1252
1410
  if (!ready(snap)) {
1253
1411
  const d = await readNewComposerThumbRow(page, { diag: true });
1254
1412
  log('info', `[fb-pw] composer mới — chưa thấy ô "Chọn hình thu nhỏ" sau ${Math.round((Date.now() - t0) / 1000)}s: ${describeThumbRow(d)}${d && d.skeleton ? ` skeleton=${JSON.stringify(d.skeleton)}` : ''}${d && d.inventory ? ` nút=${JSON.stringify(d.inventory)}` : ''}`);
@@ -1267,34 +1425,9 @@ async function applyNewComposerThumb(page, thumbPath, log, { waitPickerMs = 20_0
1267
1425
  const before = snap;
1268
1426
  log('info', `[fb-pw] composer mới — hàng ảnh bìa trước khi dán: ${describeThumbRow(before)}`);
1269
1427
 
1270
- // Bấm ô chọn-ảnh rồi chờ MỘT trong hai: hộp chọn file (biến thể mở thẳng)
1271
- // hoặc hộp thoại "Chọn hình thu nhỏ" (biến thể đo thật 23/09).
1272
- let via = '';
1273
- let chooser = null;
1274
- const chooserP = page.waitForEvent('filechooser', { timeout: 8000 }).then((c) => { chooser = c; }).catch(() => {});
1275
- let clickErr = '';
1276
- await page.locator("[__fbpw_thumb_pick__='1']").first().click({ timeout: 5000 }).catch((e) => { clickErr = String(e && e.message || e).slice(0, 80); });
1277
- let modal = null;
1278
- const tClick = Date.now();
1279
- while (!chooser && !modal && Date.now() - tClick < 8000) {
1280
- await pause(page, 800);
1281
- modal = await findThumbPickerModal(page);
1282
- }
1283
- if (chooser) {
1284
- await chooser.setFiles(thumbPath).then(() => { via = 'hộp chọn file'; })
1285
- .catch((e) => log('info', `[fb-pw] composer mới — hộp chọn file từ chối ảnh: ${String(e && e.message || e).slice(0, 80)}`));
1286
- } else if (modal) {
1287
- const r = await uploadViaThumbPickerModal(page, thumbPath, log, modal);
1288
- if (!r.ok) {
1289
- const d = await readNewComposerThumbRow(page, { diag: true });
1290
- log('warn', `[fb-pw] composer mới — dán qua hộp thoại "Chọn hình thu nhỏ" KHÔNG xong (${r.reason}) — hàng ô: ${describeThumbRow(d)}`);
1291
- return { applied: false, reason: r.reason };
1292
- }
1293
- via = r.via + (r.reason ? ` (${r.reason})` : '');
1294
- } else {
1295
- log('info', `[fb-pw] composer mới — bấm ô "Chọn hình thu nhỏ" không mở hộp chọn file lẫn hộp thoại${clickErr ? `: ${clickErr}` : ''}`);
1296
- }
1297
- void chooserP;
1428
+ const up = await clickPickerAndUpload(page, thumbPath, log);
1429
+ let via = up.via;
1430
+ if (up.failed) return { applied: false, reason: up.reason };
1298
1431
  if (!via) {
1299
1432
  await readNewComposerThumbRow(page); // cú bấm có thể làm FB dựng lại DOM → gắn cờ lại
1300
1433
  const input = page.locator("[__fbpw_thumb_input__='1']").first();
@@ -1861,7 +1994,7 @@ async function runOnce({ page, payload, log }) {
1861
1994
  } = payload || {};
1862
1995
  if (!video_url) throw new Error('No video_url provided');
1863
1996
 
1864
- log('info', '[fb-pw] selectors version=2026.09.23-thumb-picker-modal');
1997
+ log('info', '[fb-pw] selectors version=2026.09.23-cover-button');
1865
1998
 
1866
1999
  page.on('dialog', (d) => { d.accept().catch(() => {}); });
1867
2000
 
@@ -4340,6 +4473,11 @@ module.exports.__testables = {
4340
4473
  uploadViaThumbPickerModal,
4341
4474
  previewMatchesThumb,
4342
4475
  THUMB_PREVIEW_MAX_DIFF,
4476
+ COVER_BUTTON_RE,
4477
+ COVER_HEAD_RE,
4478
+ composerShowsThumb,
4479
+ applyCoverButtonThumb,
4480
+ clickPickerAndUpload,
4343
4481
  // Dùng chung cho comment_facebook: script nào trượt giữa chừng cũng cần chụp
4344
4482
  // lại màn hình, viết bản thứ hai là sớm muộn hai bên lệch nhau.
4345
4483
  dumpFailure,