channel-worker 2.5.134 → 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 +1 -1
- package/scripts/upload_facebook.js +398 -20
package/package.json
CHANGED
|
@@ -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) => {
|
|
@@ -782,7 +790,10 @@ async function readNewComposerThumbRow(page, { diag = false, tagIndex = -1 } = {
|
|
|
782
790
|
}
|
|
783
791
|
return '';
|
|
784
792
|
};
|
|
785
|
-
|
|
793
|
+
// Ô thật rộng ~90px. Chặn trên 320px để thân hộp thoại "Chọn hình thu nhỏ"
|
|
794
|
+
// (rộng 500, có ảnh xem trước) không bị nhận là một ô — lượt test thật
|
|
795
|
+
// 23/09 đọc nhầm hộp thoại thành "2 ô [chọn-ảnh, ảnh-local]".
|
|
796
|
+
const tileLike = (el) => vis(el) && el.getBoundingClientRect().width <= 320 && (!!mediaOf(el) || isPick(el));
|
|
786
797
|
// Ô đang chọn: viền/bóng xanh của FB (#0866FF, #1877F2) hoặc aria-*.
|
|
787
798
|
const blueish = (c) => {
|
|
788
799
|
const m = String(c || '').match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?/);
|
|
@@ -835,8 +846,16 @@ async function readNewComposerThumbRow(page, { diag = false, tagIndex = -1 } = {
|
|
|
835
846
|
};
|
|
836
847
|
let pickLeaf = null;
|
|
837
848
|
for (const root of roots) {
|
|
849
|
+
// Bỏ qua tiêu đề của hộp thoại "Chọn hình thu nhỏ": cùng chữ với ô, và
|
|
850
|
+
// FB gắn role=dialog + aria-label đó lên phần đầu hộp thoại (đo 23/09).
|
|
851
|
+
const inPickerModal = (el) => {
|
|
852
|
+
if (el.closest('[__fbpw_thumb_modal__]')) return true;
|
|
853
|
+
const d = el.closest("[role='dialog']");
|
|
854
|
+
return !!d && d !== root && PICK.test(norm(d.getAttribute('aria-label')));
|
|
855
|
+
};
|
|
838
856
|
const hits = [...root.querySelectorAll('[aria-label]')].filter((el) => PICK.test(norm(el.getAttribute('aria-label'))) && vis(el))
|
|
839
|
-
.concat(textHits(root, /hình|ảnh|thumbnail/i, PICK))
|
|
857
|
+
.concat(textHits(root, /hình|ảnh|thumbnail/i, PICK))
|
|
858
|
+
.filter((el) => !inPickerModal(el));
|
|
840
859
|
if (hits.length) { pickLeaf = hits[0]; break; }
|
|
841
860
|
}
|
|
842
861
|
let row = null;
|
|
@@ -869,6 +888,23 @@ async function readNewComposerThumbRow(page, { diag = false, tagIndex = -1 } = {
|
|
|
869
888
|
}
|
|
870
889
|
const customMarker = [...document.querySelectorAll('[aria-label]')].some((el) =>
|
|
871
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
|
+
}
|
|
872
908
|
if (!row) {
|
|
873
909
|
const miss = { found: false, pickerPresent: !!pickLeaf, roots: roots.length, customMarker };
|
|
874
910
|
if (diag) {
|
|
@@ -911,7 +947,9 @@ async function readNewComposerThumbRow(page, { diag = false, tagIndex = -1 } = {
|
|
|
911
947
|
for (let scope = row, d = 0; scope && d < 3 && !input; scope = scope.parentElement, d++) {
|
|
912
948
|
for (const el of scope.querySelectorAll("input[type='file']")) {
|
|
913
949
|
const accept = (el.getAttribute('accept') || '').toLowerCase();
|
|
914
|
-
|
|
950
|
+
// Nhận cả dạng đuôi file (".png,.jpg,.jpeg" — đo thật 23/09); input của
|
|
951
|
+
// "Thêm file khác" nhận cả video nên bị loại.
|
|
952
|
+
if (!/image|\.png|\.jpe?g/.test(accept) || /video|\.mp4|\.mov/.test(accept)) continue;
|
|
915
953
|
el.setAttribute('__fbpw_thumb_input__', '1');
|
|
916
954
|
input = { accept: accept.slice(0, 60) };
|
|
917
955
|
break;
|
|
@@ -941,7 +979,7 @@ async function readNewComposerThumbRow(page, { diag = false, tagIndex = -1 } = {
|
|
|
941
979
|
});
|
|
942
980
|
}
|
|
943
981
|
return res;
|
|
944
|
-
}, { 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);
|
|
945
983
|
}
|
|
946
984
|
|
|
947
985
|
// Ảnh bìa tự chọn đã VÀO composer mới chưa? So hàng ô trước và sau khi dán
|
|
@@ -984,6 +1022,7 @@ function newComposerThumbVerdict(before, after) {
|
|
|
984
1022
|
// Tóm tắt một hàng ô cho log: "5 ô [chọn-ảnh, *khung, khung…] chọn=1 input=image/*".
|
|
985
1023
|
function describeThumbRow(snap) {
|
|
986
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})`;
|
|
987
1026
|
if (!snap.found) return `không thấy hàng ô (composer=${snap.roots || 0}, ô chọn-ảnh=${snap.pickerPresent ? 'có' : 'không'})`;
|
|
988
1027
|
const cells = (snap.tiles || []).map((t) => `${t.selected ? '*' : ''}${t.placeholder ? 'chọn-ảnh' : (t.img ? (/^(blob|data):/.test(t.img) ? 'ảnh-local' : 'khung') : '?')}`);
|
|
989
1028
|
return `${cells.length} ô [${cells.join(', ')}] chọn=${snap.selectedIndex ?? '?'} input=${snap.input ? snap.input.accept || '?' : 'không'} via=${snap.via}`;
|
|
@@ -993,13 +1032,17 @@ function describeThumbRow(snap) {
|
|
|
993
1032
|
// tận mắt — chỉ bấm nút xác nhận của một dialog KHÁC composer mà trong chữ
|
|
994
1033
|
// có nhắc ảnh bìa, để không bao giờ trúng nút "Lưu" (= lưu nháp) của composer.
|
|
995
1034
|
async function confirmNewComposerThumbDialog(page, log) {
|
|
996
|
-
const hit = await page.evaluate((compSrc) => {
|
|
1035
|
+
const hit = await page.evaluate(({ compSrc, pickSrc }) => {
|
|
997
1036
|
const COMP = new RegExp(compSrc, 'i');
|
|
1037
|
+
const PICK = new RegExp(pickSrc, 'i');
|
|
998
1038
|
for (const dlg of document.querySelectorAll("[role='dialog']")) {
|
|
999
1039
|
const r = dlg.getBoundingClientRect();
|
|
1000
1040
|
if (r.width < 8 || r.height < 8) continue;
|
|
1001
1041
|
const head = `${dlg.getAttribute('aria-label') || ''} | ${(dlg.innerText || '').slice(0, 300)}`;
|
|
1002
1042
|
if (COMP.test(head)) continue;
|
|
1043
|
+
// Hộp "Chọn hình thu nhỏ" có luồng riêng (chỉ Lưu khi ảnh xem trước đã
|
|
1044
|
+
// đổi) — bấm Lưu ở đây lúc ảnh chưa đổi là lưu khung tự động làm ảnh bìa.
|
|
1045
|
+
if (PICK.test((dlg.getAttribute('aria-label') || '').trim()) || dlg.closest('[__fbpw_thumb_modal__]')) continue;
|
|
1003
1046
|
if (!/hình thu nhỏ|ảnh thu nhỏ|thumbnail|cắt ảnh|crop/i.test(head)) continue;
|
|
1004
1047
|
for (const b of dlg.querySelectorAll("button, [role='button']")) {
|
|
1005
1048
|
const t = (b.innerText || '').trim();
|
|
@@ -1011,7 +1054,7 @@ async function confirmNewComposerThumbDialog(page, log) {
|
|
|
1011
1054
|
}
|
|
1012
1055
|
}
|
|
1013
1056
|
return null;
|
|
1014
|
-
}, COMPOSER_SIGNATURE_RE.source).catch(() => null);
|
|
1057
|
+
}, { compSrc: COMPOSER_SIGNATURE_RE.source, pickSrc: THUMB_PICKER_TEXT_RE.source }).catch(() => null);
|
|
1015
1058
|
if (!hit) return false;
|
|
1016
1059
|
log('info', `[fb-pw] composer mới — hộp phụ ảnh bìa "${hit.head}" → bấm "${hit.verb}"`);
|
|
1017
1060
|
await page.locator("[__fbpw_thumb_confirm__='1']").click({ timeout: 3000 }).catch(() => {});
|
|
@@ -1019,17 +1062,351 @@ async function confirmNewComposerThumbDialog(page, log) {
|
|
|
1019
1062
|
return true;
|
|
1020
1063
|
}
|
|
1021
1064
|
|
|
1065
|
+
// Hộp thoại "Chọn hình thu nhỏ" — thứ THẬT mở ra khi bấm ô chọn-ảnh. Đo trên
|
|
1066
|
+
// Facebook 23/09/2026 (lượt test page LACCA, lệnh 6ab39643): cú bấm KHÔNG mở
|
|
1067
|
+
// hộp chọn file mà mở hộp thoại 500x326 gồm ảnh xem trước 203x360, dải khung để
|
|
1068
|
+
// tua video (input range "Tua trên dòng thời gian…"), input file ẩn
|
|
1069
|
+
// accept=".png,.jpg,.jpeg", nút "Tải lên" và "Lưu". FB gắn role=dialog +
|
|
1070
|
+
// aria-label "Chọn hình thu nhỏ" lên PHẦN ĐẦU (500x60) chứ không lên cả hộp,
|
|
1071
|
+
// nên không dò theo role: leo từ chữ tiêu đề lên tới khối có cả "Tải lên" lẫn
|
|
1072
|
+
// "Lưu" (và chưa chạm nút "Đăng" của composer).
|
|
1073
|
+
// Gắn cờ: __fbpw_thumb_modal__ (khối), _upload__ (nút Tải lên), _save__ (mọi
|
|
1074
|
+
// nút Lưu, đánh số từ cuối), _input__ (input ảnh). Trả null nếu không thấy.
|
|
1075
|
+
async function findThumbPickerModal(page) {
|
|
1076
|
+
return page.evaluate(({ pickSrc, coverHeadSrc }) => {
|
|
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);
|
|
1080
|
+
const norm = (s) => String(s || '').replace(/\s+/g, ' ').trim();
|
|
1081
|
+
const vis = (el) => {
|
|
1082
|
+
const r = el.getBoundingClientRect();
|
|
1083
|
+
if (r.width < 8 || r.height < 8) return false;
|
|
1084
|
+
const cs = getComputedStyle(el);
|
|
1085
|
+
return cs.visibility !== 'hidden' && cs.display !== 'none' && cs.opacity !== '0';
|
|
1086
|
+
};
|
|
1087
|
+
for (const a of ['__fbpw_thumb_modal__', '__fbpw_thumb_modal_upload__', '__fbpw_thumb_modal_save__', '__fbpw_thumb_modal_input__']) {
|
|
1088
|
+
document.querySelectorAll(`[${a}]`).forEach((el) => el.removeAttribute(a));
|
|
1089
|
+
}
|
|
1090
|
+
const says = (el, re) => re.test(norm(el.getAttribute('aria-label'))) || re.test(norm(el.innerText));
|
|
1091
|
+
const UP = /^(Tải lên|Tải ảnh lên|Upload|Upload image)$/i;
|
|
1092
|
+
const SAVE = /^(Lưu|Save|Xong|Done|Áp dụng|Apply)$/i;
|
|
1093
|
+
const POST = /^(Đăng|Post|Share now)$/i;
|
|
1094
|
+
const heads = [...document.querySelectorAll('[aria-label]')].filter((el) => isHead(norm(el.getAttribute('aria-label'))) && vis(el));
|
|
1095
|
+
const tw = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
|
|
1096
|
+
for (let n = tw.nextNode(); n; n = tw.nextNode()) {
|
|
1097
|
+
if (!/hình|thumbnail|ảnh bìa|cover/i.test(n.data)) continue;
|
|
1098
|
+
const el = n.parentElement;
|
|
1099
|
+
if (el && vis(el) && isHead(norm(el.innerText))) heads.push(el);
|
|
1100
|
+
}
|
|
1101
|
+
for (const h of heads) {
|
|
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"
|
|
1104
|
+
let box = h;
|
|
1105
|
+
for (let d = 0; d < 10 && box && box !== document.body; d++, box = box.parentElement) {
|
|
1106
|
+
const btns = [...box.querySelectorAll("[role='button'], button")].filter(vis);
|
|
1107
|
+
if (btns.some((b) => says(b, POST))) break; // đã leo tới composer
|
|
1108
|
+
const up = btns.find((b) => says(b, UP));
|
|
1109
|
+
const saves = btns.filter((b) => says(b, SAVE));
|
|
1110
|
+
if (!up || !saves.length) continue;
|
|
1111
|
+
box.setAttribute('__fbpw_thumb_modal__', '1');
|
|
1112
|
+
up.setAttribute('__fbpw_thumb_modal_upload__', '1');
|
|
1113
|
+
saves.reverse().forEach((b, i) => b.setAttribute('__fbpw_thumb_modal_save__', String(i)));
|
|
1114
|
+
const inp = [...box.querySelectorAll("input[type='file']")].find((i) => {
|
|
1115
|
+
const a = (i.getAttribute('accept') || '').toLowerCase();
|
|
1116
|
+
return /image|\.png|\.jpe?g/.test(a) && !/video|\.mp4|\.mov/.test(a);
|
|
1117
|
+
});
|
|
1118
|
+
if (inp) inp.setAttribute('__fbpw_thumb_modal_input__', '1');
|
|
1119
|
+
const big = [...box.querySelectorAll('img')].find((i) => {
|
|
1120
|
+
const ir = i.getBoundingClientRect();
|
|
1121
|
+
return ir.width >= 150 && ir.height >= 150;
|
|
1122
|
+
});
|
|
1123
|
+
const r = box.getBoundingClientRect();
|
|
1124
|
+
return {
|
|
1125
|
+
w: Math.round(r.width),
|
|
1126
|
+
h: Math.round(r.height),
|
|
1127
|
+
preview: big ? String(big.currentSrc || big.src || '').slice(0, 500) : '',
|
|
1128
|
+
input: inp ? (inp.getAttribute('accept') || '') : null,
|
|
1129
|
+
saves: saves.length,
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
return null;
|
|
1134
|
+
}, { pickSrc: THUMB_PICKER_TEXT_RE.source, coverHeadSrc: COVER_HEAD_RE.source }).catch(() => null);
|
|
1135
|
+
}
|
|
1136
|
+
|
|
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
|
|
1138
|
+
// về 16x16 rồi lấy độ lệch trung bình mỗi kênh màu (0-255). "Ảnh xem trước đổi
|
|
1139
|
+
// URL" chưa đủ: FB có thể tự thay bằng bản nét hơn của khung đang chọn, và Lưu
|
|
1140
|
+
// lúc đó là lưu khung tự động. Ảnh blob:/data: đọc được; ảnh CDN khác nguồn làm
|
|
1141
|
+
// canvas bị khoá → trả { err: 'tainted' } để người gọi tự quyết.
|
|
1142
|
+
const THUMB_PREVIEW_MAX_DIFF = 18;
|
|
1143
|
+
async function previewMatchesThumb(page, thumbDataUrl) {
|
|
1144
|
+
return page.evaluate(async ({ ours }) => {
|
|
1145
|
+
const box = document.querySelector('[__fbpw_thumb_modal__]');
|
|
1146
|
+
if (!box) return { err: 'no-modal' };
|
|
1147
|
+
const prev = [...box.querySelectorAll('img')].find((i) => {
|
|
1148
|
+
const r = i.getBoundingClientRect();
|
|
1149
|
+
return r.width >= 150 && r.height >= 150;
|
|
1150
|
+
});
|
|
1151
|
+
if (!prev || !prev.complete || !prev.naturalWidth) return { err: 'preview-not-loaded' };
|
|
1152
|
+
const sig = (img) => {
|
|
1153
|
+
const c = document.createElement('canvas');
|
|
1154
|
+
c.width = 16; c.height = 16;
|
|
1155
|
+
const x = c.getContext('2d');
|
|
1156
|
+
x.drawImage(img, 0, 0, 16, 16);
|
|
1157
|
+
return x.getImageData(0, 0, 16, 16).data;
|
|
1158
|
+
};
|
|
1159
|
+
let a;
|
|
1160
|
+
try { a = sig(prev); } catch { return { err: 'tainted' }; }
|
|
1161
|
+
const mine = await new Promise((res) => {
|
|
1162
|
+
const i = new Image();
|
|
1163
|
+
i.onload = () => res(i);
|
|
1164
|
+
i.onerror = () => res(null);
|
|
1165
|
+
i.src = ours;
|
|
1166
|
+
});
|
|
1167
|
+
if (!mine) return { err: 'thumb-not-decodable' };
|
|
1168
|
+
const b = sig(mine);
|
|
1169
|
+
let d = 0;
|
|
1170
|
+
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]);
|
|
1171
|
+
return { diff: Math.round(d / (256 * 3)) };
|
|
1172
|
+
}, { ours: thumbDataUrl }).catch((e) => ({ err: String(e && e.message || e).slice(0, 60) }));
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
// Đưa ảnh bìa vào hộp thoại "Chọn hình thu nhỏ" rồi Lưu. Chỉ bấm Lưu khi ẢNH
|
|
1176
|
+
// XEM TRƯỚC ĐÃ ĐỔI: hộp này mở sẵn một khung cắt từ video, Lưu lúc ảnh chưa đổi
|
|
1177
|
+
// là lưu khung đó làm ảnh bìa — bài lên với ảnh FB tự chọn, đúng thứ cổng
|
|
1178
|
+
// trước Đăng sinh ra để chặn. Trả { ok, via, reason }.
|
|
1179
|
+
async function uploadViaThumbPickerModal(page, thumbPath, log, modal, { previewMs = 20_000, closeMs = 30_000 } = {}) {
|
|
1180
|
+
log('info', `[fb-pw] composer mới — ô "Chọn hình thu nhỏ" mở hộp thoại chọn ảnh bìa (${modal.w}x${modal.h}, input=${modal.input === null ? 'không' : modal.input || '?'}, nút Lưu=${modal.saves})`);
|
|
1181
|
+
// Mốc "trước" chỉ chốt khi ảnh xem trước ĐÃ ĐỨNG YÊN: chụp lúc nó còn trống
|
|
1182
|
+
// hay đang nạp thì lần nạp xong kế tiếp sẽ bị tưởng là ảnh của mình.
|
|
1183
|
+
let beforePreview = modal.preview;
|
|
1184
|
+
for (let i = 0; i < 5; i++) {
|
|
1185
|
+
await pause(page, 1000);
|
|
1186
|
+
const again = await findThumbPickerModal(page);
|
|
1187
|
+
if (!again) break;
|
|
1188
|
+
const stable = again.preview && again.preview === beforePreview;
|
|
1189
|
+
beforePreview = again.preview;
|
|
1190
|
+
if (stable) break;
|
|
1191
|
+
}
|
|
1192
|
+
let thumbDataUrl = '';
|
|
1193
|
+
try { thumbDataUrl = `data:image/png;base64,${fs.readFileSync(thumbPath).toString('base64')}`; } catch { /* so nội dung sẽ báo lỗi, rơi về luật đổi-URL */ }
|
|
1194
|
+
let via = '';
|
|
1195
|
+
try {
|
|
1196
|
+
const [chooser] = await Promise.all([
|
|
1197
|
+
page.waitForEvent('filechooser', { timeout: 8000 }),
|
|
1198
|
+
page.locator("[__fbpw_thumb_modal__='1'] [__fbpw_thumb_modal_upload__='1']").first().click({ timeout: 5000 }),
|
|
1199
|
+
]);
|
|
1200
|
+
await chooser.setFiles(thumbPath);
|
|
1201
|
+
via = 'nút "Tải lên" trong hộp thoại';
|
|
1202
|
+
} catch (e) {
|
|
1203
|
+
log('info', `[fb-pw] composer mới — nút "Tải lên" không mở hộp chọn file: ${String(e && e.message || e).slice(0, 80)}`);
|
|
1204
|
+
}
|
|
1205
|
+
if (!via && await findThumbPickerModal(page)) {
|
|
1206
|
+
const inp = page.locator("[__fbpw_thumb_modal__='1'] [__fbpw_thumb_modal_input__='1']").first();
|
|
1207
|
+
if (await inp.count().catch(() => 0)) {
|
|
1208
|
+
try {
|
|
1209
|
+
await inp.setInputFiles(thumbPath);
|
|
1210
|
+
via = 'input ảnh ẩn trong hộp thoại';
|
|
1211
|
+
} catch (e) {
|
|
1212
|
+
log('info', `[fb-pw] composer mới — input ảnh trong hộp thoại từ chối file: ${String(e && e.message || e).slice(0, 80)}`);
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
if (!via) return { ok: false, reason: 'modal-no-upload' };
|
|
1217
|
+
log('info', `[fb-pw] composer mới — đã đưa file vào hộp thoại qua ${via}, chờ ảnh xem trước đổi…`);
|
|
1218
|
+
|
|
1219
|
+
const t0 = Date.now();
|
|
1220
|
+
let m = null;
|
|
1221
|
+
let changed = false;
|
|
1222
|
+
let lastCmp = null;
|
|
1223
|
+
let loggedMismatch = false;
|
|
1224
|
+
while (Date.now() - t0 < previewMs) {
|
|
1225
|
+
await pause(page, 1500);
|
|
1226
|
+
m = await findThumbPickerModal(page);
|
|
1227
|
+
if (!m) return { ok: true, via, verified: false, reason: 'hộp thoại tự đóng sau khi nhận ảnh' };
|
|
1228
|
+
if (!m.preview || m.preview === beforePreview) continue;
|
|
1229
|
+
// URL đổi rồi — so NỘI DUNG. Lệch nhiều = FB đổi khung chứ chưa phải ảnh
|
|
1230
|
+
// của mình: chờ tiếp. Không so được (ảnh CDN khoá canvas) thì rơi về luật
|
|
1231
|
+
// đổi-URL, kèm ghi rõ vào log. Ảnh chưa nạp xong thì chờ nhịp sau.
|
|
1232
|
+
lastCmp = await previewMatchesThumb(page, thumbDataUrl);
|
|
1233
|
+
if (lastCmp.err === 'preview-not-loaded') continue;
|
|
1234
|
+
if (lastCmp.diff !== undefined && lastCmp.diff > THUMB_PREVIEW_MAX_DIFF) {
|
|
1235
|
+
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
|
+
loggedMismatch = true;
|
|
1237
|
+
continue;
|
|
1238
|
+
}
|
|
1239
|
+
changed = true;
|
|
1240
|
+
break;
|
|
1241
|
+
}
|
|
1242
|
+
if (!changed) {
|
|
1243
|
+
log('warn', `[fb-pw] composer mới — ảnh xem trước chưa thành ảnh bìa của mình sau ${Math.round(previewMs / 1000)}s${lastCmp && lastCmp.diff !== undefined ? ` (lệch ${lastCmp.diff}/255)` : ''} → KHÔNG bấm Lưu (Lưu lúc này là lưu khung tự động làm ảnh bìa)`);
|
|
1244
|
+
return { ok: false, reason: 'modal-preview-unchanged' };
|
|
1245
|
+
}
|
|
1246
|
+
log('info', `[fb-pw] composer mới — ảnh xem trước đã thành ảnh bìa của mình sau ${Math.round((Date.now() - t0) / 1000)}s (${lastCmp && lastCmp.diff !== undefined ? `lệch ${lastCmp.diff}/255` : `không so được nội dung: ${lastCmp && lastCmp.err}`}), bấm Lưu…`);
|
|
1247
|
+
let saved = false;
|
|
1248
|
+
for (let i = 0; i < (m.saves || 1) && !saved; i++) {
|
|
1249
|
+
try {
|
|
1250
|
+
await page.locator(`[__fbpw_thumb_modal__='1'] [__fbpw_thumb_modal_save__='${i}']`).first().click({ timeout: 5000 });
|
|
1251
|
+
saved = true;
|
|
1252
|
+
} catch (e) {
|
|
1253
|
+
log('info', `[fb-pw] composer mới — nút Lưu #${i} không bấm được: ${String(e && e.message || e).slice(0, 80)}`);
|
|
1254
|
+
await findThumbPickerModal(page);
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
if (!saved) return { ok: false, reason: 'modal-save-failed' };
|
|
1258
|
+
// Đòi hai nhịp liền không thấy hộp thoại — FB có thể gỡ rồi dựng lại khung.
|
|
1259
|
+
const t1 = Date.now();
|
|
1260
|
+
let gone = 0;
|
|
1261
|
+
while (Date.now() - t1 < closeMs) {
|
|
1262
|
+
await pause(page, 1500);
|
|
1263
|
+
if (await findThumbPickerModal(page)) { gone = 0; continue; }
|
|
1264
|
+
if (++gone >= 2) return { ok: true, via, verified: !!(lastCmp && lastCmp.diff !== undefined) };
|
|
1265
|
+
}
|
|
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`);
|
|
1267
|
+
return { ok: false, reason: 'modal-not-closed' };
|
|
1268
|
+
}
|
|
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
|
+
|
|
1022
1397
|
// Dán ảnh bìa vào composer mới. Trả { applied, reason }. Không ném: thất bại
|
|
1023
1398
|
// thì cờ customThumbDone không bật và cổng trước Đăng chặn như cũ.
|
|
1024
1399
|
async function applyNewComposerThumb(page, thumbPath, log, { waitPickerMs = 20_000, verifyMs = 30_000 } = {}) {
|
|
1025
1400
|
const t0 = Date.now();
|
|
1026
1401
|
const ready = (s) => !!(s && s.found && s.pickerPresent);
|
|
1402
|
+
const coverMode = (s) => !!(s && s.mode === 'cover-button');
|
|
1027
1403
|
let snap = await readNewComposerThumbRow(page);
|
|
1028
1404
|
// Hàng ảnh bìa chỉ hiện sau khi FB cắt xong khung từ video.
|
|
1029
|
-
while (!ready(snap) && Date.now() - t0 < waitPickerMs) {
|
|
1405
|
+
while (!ready(snap) && !coverMode(snap) && Date.now() - t0 < waitPickerMs) {
|
|
1030
1406
|
await pause(page, 2500);
|
|
1031
1407
|
snap = await readNewComposerThumbRow(page);
|
|
1032
1408
|
}
|
|
1409
|
+
if (coverMode(snap)) return applyCoverButtonThumb(page, thumbPath, log, snap, { verifyMs });
|
|
1033
1410
|
if (!ready(snap)) {
|
|
1034
1411
|
const d = await readNewComposerThumbRow(page, { diag: true });
|
|
1035
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)}` : ''}`);
|
|
@@ -1048,17 +1425,9 @@ async function applyNewComposerThumb(page, thumbPath, log, { waitPickerMs = 20_0
|
|
|
1048
1425
|
const before = snap;
|
|
1049
1426
|
log('info', `[fb-pw] composer mới — hàng ảnh bìa trước khi dán: ${describeThumbRow(before)}`);
|
|
1050
1427
|
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
page.waitForEvent('filechooser', { timeout: 8000 }),
|
|
1055
|
-
page.locator("[__fbpw_thumb_pick__='1']").first().click({ timeout: 5000 }),
|
|
1056
|
-
]);
|
|
1057
|
-
await chooser.setFiles(thumbPath);
|
|
1058
|
-
via = 'hộp chọn file';
|
|
1059
|
-
} catch (e) {
|
|
1060
|
-
log('info', `[fb-pw] composer mới — bấm ô "Chọn hình thu nhỏ" không mở hộp chọn file: ${String(e && e.message || e).slice(0, 80)}`);
|
|
1061
|
-
}
|
|
1428
|
+
const up = await clickPickerAndUpload(page, thumbPath, log);
|
|
1429
|
+
let via = up.via;
|
|
1430
|
+
if (up.failed) return { applied: false, reason: up.reason };
|
|
1062
1431
|
if (!via) {
|
|
1063
1432
|
await readNewComposerThumbRow(page); // cú bấm có thể làm FB dựng lại DOM → gắn cờ lại
|
|
1064
1433
|
const input = page.locator("[__fbpw_thumb_input__='1']").first();
|
|
@@ -1625,7 +1994,7 @@ async function runOnce({ page, payload, log }) {
|
|
|
1625
1994
|
} = payload || {};
|
|
1626
1995
|
if (!video_url) throw new Error('No video_url provided');
|
|
1627
1996
|
|
|
1628
|
-
log('info', '[fb-pw] selectors version=2026.09.23-
|
|
1997
|
+
log('info', '[fb-pw] selectors version=2026.09.23-cover-button');
|
|
1629
1998
|
|
|
1630
1999
|
page.on('dialog', (d) => { d.accept().catch(() => {}); });
|
|
1631
2000
|
|
|
@@ -4100,6 +4469,15 @@ module.exports.__testables = {
|
|
|
4100
4469
|
describeThumbRow,
|
|
4101
4470
|
readNewComposerThumbRow,
|
|
4102
4471
|
applyNewComposerThumb,
|
|
4472
|
+
findThumbPickerModal,
|
|
4473
|
+
uploadViaThumbPickerModal,
|
|
4474
|
+
previewMatchesThumb,
|
|
4475
|
+
THUMB_PREVIEW_MAX_DIFF,
|
|
4476
|
+
COVER_BUTTON_RE,
|
|
4477
|
+
COVER_HEAD_RE,
|
|
4478
|
+
composerShowsThumb,
|
|
4479
|
+
applyCoverButtonThumb,
|
|
4480
|
+
clickPickerAndUpload,
|
|
4103
4481
|
// Dùng chung cho comment_facebook: script nào trượt giữa chừng cũng cần chụp
|
|
4104
4482
|
// lại màn hình, viết bản thứ hai là sớm muộn hai bên lệch nhau.
|
|
4105
4483
|
dumpFailure,
|