channel-worker 2.5.134 → 2.5.135
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 +255 -15
package/package.json
CHANGED
|
@@ -782,7 +782,10 @@ async function readNewComposerThumbRow(page, { diag = false, tagIndex = -1 } = {
|
|
|
782
782
|
}
|
|
783
783
|
return '';
|
|
784
784
|
};
|
|
785
|
-
|
|
785
|
+
// Ô thật rộng ~90px. Chặn trên 320px để thân hộp thoại "Chọn hình thu nhỏ"
|
|
786
|
+
// (rộng 500, có ảnh xem trước) không bị nhận là một ô — lượt test thật
|
|
787
|
+
// 23/09 đọc nhầm hộp thoại thành "2 ô [chọn-ảnh, ảnh-local]".
|
|
788
|
+
const tileLike = (el) => vis(el) && el.getBoundingClientRect().width <= 320 && (!!mediaOf(el) || isPick(el));
|
|
786
789
|
// Ô đang chọn: viền/bóng xanh của FB (#0866FF, #1877F2) hoặc aria-*.
|
|
787
790
|
const blueish = (c) => {
|
|
788
791
|
const m = String(c || '').match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?/);
|
|
@@ -835,8 +838,16 @@ async function readNewComposerThumbRow(page, { diag = false, tagIndex = -1 } = {
|
|
|
835
838
|
};
|
|
836
839
|
let pickLeaf = null;
|
|
837
840
|
for (const root of roots) {
|
|
841
|
+
// Bỏ qua tiêu đề của hộp thoại "Chọn hình thu nhỏ": cùng chữ với ô, và
|
|
842
|
+
// FB gắn role=dialog + aria-label đó lên phần đầu hộp thoại (đo 23/09).
|
|
843
|
+
const inPickerModal = (el) => {
|
|
844
|
+
if (el.closest('[__fbpw_thumb_modal__]')) return true;
|
|
845
|
+
const d = el.closest("[role='dialog']");
|
|
846
|
+
return !!d && d !== root && PICK.test(norm(d.getAttribute('aria-label')));
|
|
847
|
+
};
|
|
838
848
|
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))
|
|
849
|
+
.concat(textHits(root, /hình|ảnh|thumbnail/i, PICK))
|
|
850
|
+
.filter((el) => !inPickerModal(el));
|
|
840
851
|
if (hits.length) { pickLeaf = hits[0]; break; }
|
|
841
852
|
}
|
|
842
853
|
let row = null;
|
|
@@ -911,7 +922,9 @@ async function readNewComposerThumbRow(page, { diag = false, tagIndex = -1 } = {
|
|
|
911
922
|
for (let scope = row, d = 0; scope && d < 3 && !input; scope = scope.parentElement, d++) {
|
|
912
923
|
for (const el of scope.querySelectorAll("input[type='file']")) {
|
|
913
924
|
const accept = (el.getAttribute('accept') || '').toLowerCase();
|
|
914
|
-
|
|
925
|
+
// Nhận cả dạng đuôi file (".png,.jpg,.jpeg" — đo thật 23/09); input của
|
|
926
|
+
// "Thêm file khác" nhận cả video nên bị loại.
|
|
927
|
+
if (!/image|\.png|\.jpe?g/.test(accept) || /video|\.mp4|\.mov/.test(accept)) continue;
|
|
915
928
|
el.setAttribute('__fbpw_thumb_input__', '1');
|
|
916
929
|
input = { accept: accept.slice(0, 60) };
|
|
917
930
|
break;
|
|
@@ -993,13 +1006,17 @@ function describeThumbRow(snap) {
|
|
|
993
1006
|
// 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
1007
|
// có nhắc ảnh bìa, để không bao giờ trúng nút "Lưu" (= lưu nháp) của composer.
|
|
995
1008
|
async function confirmNewComposerThumbDialog(page, log) {
|
|
996
|
-
const hit = await page.evaluate((compSrc) => {
|
|
1009
|
+
const hit = await page.evaluate(({ compSrc, pickSrc }) => {
|
|
997
1010
|
const COMP = new RegExp(compSrc, 'i');
|
|
1011
|
+
const PICK = new RegExp(pickSrc, 'i');
|
|
998
1012
|
for (const dlg of document.querySelectorAll("[role='dialog']")) {
|
|
999
1013
|
const r = dlg.getBoundingClientRect();
|
|
1000
1014
|
if (r.width < 8 || r.height < 8) continue;
|
|
1001
1015
|
const head = `${dlg.getAttribute('aria-label') || ''} | ${(dlg.innerText || '').slice(0, 300)}`;
|
|
1002
1016
|
if (COMP.test(head)) continue;
|
|
1017
|
+
// Hộp "Chọn hình thu nhỏ" có luồng riêng (chỉ Lưu khi ảnh xem trước đã
|
|
1018
|
+
// đổi) — bấm Lưu ở đây lúc ảnh chưa đổi là lưu khung tự động làm ảnh bìa.
|
|
1019
|
+
if (PICK.test((dlg.getAttribute('aria-label') || '').trim()) || dlg.closest('[__fbpw_thumb_modal__]')) continue;
|
|
1003
1020
|
if (!/hình thu nhỏ|ảnh thu nhỏ|thumbnail|cắt ảnh|crop/i.test(head)) continue;
|
|
1004
1021
|
for (const b of dlg.querySelectorAll("button, [role='button']")) {
|
|
1005
1022
|
const t = (b.innerText || '').trim();
|
|
@@ -1011,7 +1028,7 @@ async function confirmNewComposerThumbDialog(page, log) {
|
|
|
1011
1028
|
}
|
|
1012
1029
|
}
|
|
1013
1030
|
return null;
|
|
1014
|
-
}, COMPOSER_SIGNATURE_RE.source).catch(() => null);
|
|
1031
|
+
}, { compSrc: COMPOSER_SIGNATURE_RE.source, pickSrc: THUMB_PICKER_TEXT_RE.source }).catch(() => null);
|
|
1015
1032
|
if (!hit) return false;
|
|
1016
1033
|
log('info', `[fb-pw] composer mới — hộp phụ ảnh bìa "${hit.head}" → bấm "${hit.verb}"`);
|
|
1017
1034
|
await page.locator("[__fbpw_thumb_confirm__='1']").click({ timeout: 3000 }).catch(() => {});
|
|
@@ -1019,6 +1036,208 @@ async function confirmNewComposerThumbDialog(page, log) {
|
|
|
1019
1036
|
return true;
|
|
1020
1037
|
}
|
|
1021
1038
|
|
|
1039
|
+
// Hộp thoại "Chọn hình thu nhỏ" — thứ THẬT mở ra khi bấm ô chọn-ảnh. Đo trên
|
|
1040
|
+
// Facebook 23/09/2026 (lượt test page LACCA, lệnh 6ab39643): cú bấm KHÔNG mở
|
|
1041
|
+
// hộp chọn file mà mở hộp thoại 500x326 gồm ảnh xem trước 203x360, dải khung để
|
|
1042
|
+
// tua video (input range "Tua trên dòng thời gian…"), input file ẩn
|
|
1043
|
+
// accept=".png,.jpg,.jpeg", nút "Tải lên" và "Lưu". FB gắn role=dialog +
|
|
1044
|
+
// aria-label "Chọn hình thu nhỏ" lên PHẦN ĐẦU (500x60) chứ không lên cả hộp,
|
|
1045
|
+
// 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
|
|
1046
|
+
// "Lưu" (và chưa chạm nút "Đăng" của composer).
|
|
1047
|
+
// Gắn cờ: __fbpw_thumb_modal__ (khối), _upload__ (nút Tải lên), _save__ (mọi
|
|
1048
|
+
// nút Lưu, đánh số từ cuối), _input__ (input ảnh). Trả null nếu không thấy.
|
|
1049
|
+
async function findThumbPickerModal(page) {
|
|
1050
|
+
return page.evaluate(({ pickSrc }) => {
|
|
1051
|
+
const PICK = new RegExp(pickSrc, 'i');
|
|
1052
|
+
const norm = (s) => String(s || '').replace(/\s+/g, ' ').trim();
|
|
1053
|
+
const vis = (el) => {
|
|
1054
|
+
const r = el.getBoundingClientRect();
|
|
1055
|
+
if (r.width < 8 || r.height < 8) return false;
|
|
1056
|
+
const cs = getComputedStyle(el);
|
|
1057
|
+
return cs.visibility !== 'hidden' && cs.display !== 'none' && cs.opacity !== '0';
|
|
1058
|
+
};
|
|
1059
|
+
for (const a of ['__fbpw_thumb_modal__', '__fbpw_thumb_modal_upload__', '__fbpw_thumb_modal_save__', '__fbpw_thumb_modal_input__']) {
|
|
1060
|
+
document.querySelectorAll(`[${a}]`).forEach((el) => el.removeAttribute(a));
|
|
1061
|
+
}
|
|
1062
|
+
const says = (el, re) => re.test(norm(el.getAttribute('aria-label'))) || re.test(norm(el.innerText));
|
|
1063
|
+
const UP = /^(Tải lên|Tải ảnh lên|Upload|Upload image)$/i;
|
|
1064
|
+
const SAVE = /^(Lưu|Save)$/i;
|
|
1065
|
+
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));
|
|
1067
|
+
const tw = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
|
|
1068
|
+
for (let n = tw.nextNode(); n; n = tw.nextNode()) {
|
|
1069
|
+
if (!/hình|thumbnail/i.test(n.data)) continue;
|
|
1070
|
+
const el = n.parentElement;
|
|
1071
|
+
if (el && vis(el) && PICK.test(norm(el.innerText))) heads.push(el);
|
|
1072
|
+
}
|
|
1073
|
+
for (const h of heads) {
|
|
1074
|
+
if (h.closest('[__fbpw_thumb_row__]')) continue; // chính ô chọn-ảnh trong hàng
|
|
1075
|
+
let box = h;
|
|
1076
|
+
for (let d = 0; d < 10 && box && box !== document.body; d++, box = box.parentElement) {
|
|
1077
|
+
const btns = [...box.querySelectorAll("[role='button'], button")].filter(vis);
|
|
1078
|
+
if (btns.some((b) => says(b, POST))) break; // đã leo tới composer
|
|
1079
|
+
const up = btns.find((b) => says(b, UP));
|
|
1080
|
+
const saves = btns.filter((b) => says(b, SAVE));
|
|
1081
|
+
if (!up || !saves.length) continue;
|
|
1082
|
+
box.setAttribute('__fbpw_thumb_modal__', '1');
|
|
1083
|
+
up.setAttribute('__fbpw_thumb_modal_upload__', '1');
|
|
1084
|
+
saves.reverse().forEach((b, i) => b.setAttribute('__fbpw_thumb_modal_save__', String(i)));
|
|
1085
|
+
const inp = [...box.querySelectorAll("input[type='file']")].find((i) => {
|
|
1086
|
+
const a = (i.getAttribute('accept') || '').toLowerCase();
|
|
1087
|
+
return /image|\.png|\.jpe?g/.test(a) && !/video|\.mp4|\.mov/.test(a);
|
|
1088
|
+
});
|
|
1089
|
+
if (inp) inp.setAttribute('__fbpw_thumb_modal_input__', '1');
|
|
1090
|
+
const big = [...box.querySelectorAll('img')].find((i) => {
|
|
1091
|
+
const ir = i.getBoundingClientRect();
|
|
1092
|
+
return ir.width >= 150 && ir.height >= 150;
|
|
1093
|
+
});
|
|
1094
|
+
const r = box.getBoundingClientRect();
|
|
1095
|
+
return {
|
|
1096
|
+
w: Math.round(r.width),
|
|
1097
|
+
h: Math.round(r.height),
|
|
1098
|
+
preview: big ? String(big.currentSrc || big.src || '').slice(0, 500) : '',
|
|
1099
|
+
input: inp ? (inp.getAttribute('accept') || '') : null,
|
|
1100
|
+
saves: saves.length,
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
return null;
|
|
1105
|
+
}, { pickSrc: THUMB_PICKER_TEXT_RE.source }).catch(() => null);
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
// Ảnh xem trước trong hộp thoại có đúng là ảnh bìa của MÌNH không? Thu cả hai
|
|
1109
|
+
// về 16x16 rồi lấy độ lệch trung bình mỗi kênh màu (0-255). "Ảnh xem trước đổi
|
|
1110
|
+
// 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
|
|
1111
|
+
// lúc đó là lưu khung tự động. Ảnh blob:/data: đọc được; ảnh CDN khác nguồn làm
|
|
1112
|
+
// canvas bị khoá → trả { err: 'tainted' } để người gọi tự quyết.
|
|
1113
|
+
const THUMB_PREVIEW_MAX_DIFF = 18;
|
|
1114
|
+
async function previewMatchesThumb(page, thumbDataUrl) {
|
|
1115
|
+
return page.evaluate(async ({ ours }) => {
|
|
1116
|
+
const box = document.querySelector('[__fbpw_thumb_modal__]');
|
|
1117
|
+
if (!box) return { err: 'no-modal' };
|
|
1118
|
+
const prev = [...box.querySelectorAll('img')].find((i) => {
|
|
1119
|
+
const r = i.getBoundingClientRect();
|
|
1120
|
+
return r.width >= 150 && r.height >= 150;
|
|
1121
|
+
});
|
|
1122
|
+
if (!prev || !prev.complete || !prev.naturalWidth) return { err: 'preview-not-loaded' };
|
|
1123
|
+
const sig = (img) => {
|
|
1124
|
+
const c = document.createElement('canvas');
|
|
1125
|
+
c.width = 16; c.height = 16;
|
|
1126
|
+
const x = c.getContext('2d');
|
|
1127
|
+
x.drawImage(img, 0, 0, 16, 16);
|
|
1128
|
+
return x.getImageData(0, 0, 16, 16).data;
|
|
1129
|
+
};
|
|
1130
|
+
let a;
|
|
1131
|
+
try { a = sig(prev); } catch { return { err: 'tainted' }; }
|
|
1132
|
+
const mine = await new Promise((res) => {
|
|
1133
|
+
const i = new Image();
|
|
1134
|
+
i.onload = () => res(i);
|
|
1135
|
+
i.onerror = () => res(null);
|
|
1136
|
+
i.src = ours;
|
|
1137
|
+
});
|
|
1138
|
+
if (!mine) return { err: 'thumb-not-decodable' };
|
|
1139
|
+
const b = sig(mine);
|
|
1140
|
+
let d = 0;
|
|
1141
|
+
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]);
|
|
1142
|
+
return { diff: Math.round(d / (256 * 3)) };
|
|
1143
|
+
}, { ours: thumbDataUrl }).catch((e) => ({ err: String(e && e.message || e).slice(0, 60) }));
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
// Đư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
|
|
1147
|
+
// 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
|
|
1148
|
+
// là lưu khung đó làm ảnh bìa — bài lên với ảnh FB tự chọn, đúng thứ cổng
|
|
1149
|
+
// trước Đăng sinh ra để chặn. Trả { ok, via, reason }.
|
|
1150
|
+
async function uploadViaThumbPickerModal(page, thumbPath, log, modal, { previewMs = 20_000, closeMs = 30_000 } = {}) {
|
|
1151
|
+
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})`);
|
|
1152
|
+
// Mốc "trước" chỉ chốt khi ảnh xem trước ĐÃ ĐỨNG YÊN: chụp lúc nó còn trống
|
|
1153
|
+
// hay đang nạp thì lần nạp xong kế tiếp sẽ bị tưởng là ảnh của mình.
|
|
1154
|
+
let beforePreview = modal.preview;
|
|
1155
|
+
for (let i = 0; i < 5; i++) {
|
|
1156
|
+
await pause(page, 1000);
|
|
1157
|
+
const again = await findThumbPickerModal(page);
|
|
1158
|
+
if (!again) break;
|
|
1159
|
+
const stable = again.preview && again.preview === beforePreview;
|
|
1160
|
+
beforePreview = again.preview;
|
|
1161
|
+
if (stable) break;
|
|
1162
|
+
}
|
|
1163
|
+
let thumbDataUrl = '';
|
|
1164
|
+
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 */ }
|
|
1165
|
+
let via = '';
|
|
1166
|
+
try {
|
|
1167
|
+
const [chooser] = await Promise.all([
|
|
1168
|
+
page.waitForEvent('filechooser', { timeout: 8000 }),
|
|
1169
|
+
page.locator("[__fbpw_thumb_modal__='1'] [__fbpw_thumb_modal_upload__='1']").first().click({ timeout: 5000 }),
|
|
1170
|
+
]);
|
|
1171
|
+
await chooser.setFiles(thumbPath);
|
|
1172
|
+
via = 'nút "Tải lên" trong hộp thoại';
|
|
1173
|
+
} catch (e) {
|
|
1174
|
+
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)}`);
|
|
1175
|
+
}
|
|
1176
|
+
if (!via && await findThumbPickerModal(page)) {
|
|
1177
|
+
const inp = page.locator("[__fbpw_thumb_modal__='1'] [__fbpw_thumb_modal_input__='1']").first();
|
|
1178
|
+
if (await inp.count().catch(() => 0)) {
|
|
1179
|
+
try {
|
|
1180
|
+
await inp.setInputFiles(thumbPath);
|
|
1181
|
+
via = 'input ảnh ẩn trong hộp thoại';
|
|
1182
|
+
} catch (e) {
|
|
1183
|
+
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)}`);
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
if (!via) return { ok: false, reason: 'modal-no-upload' };
|
|
1188
|
+
log('info', `[fb-pw] composer mới — đã đưa file vào hộp thoại qua ${via}, chờ ảnh xem trước đổi…`);
|
|
1189
|
+
|
|
1190
|
+
const t0 = Date.now();
|
|
1191
|
+
let m = null;
|
|
1192
|
+
let changed = false;
|
|
1193
|
+
let lastCmp = null;
|
|
1194
|
+
let loggedMismatch = false;
|
|
1195
|
+
while (Date.now() - t0 < previewMs) {
|
|
1196
|
+
await pause(page, 1500);
|
|
1197
|
+
m = await findThumbPickerModal(page);
|
|
1198
|
+
if (!m) return { ok: true, via, reason: 'hộp thoại tự đóng sau khi nhận ảnh' };
|
|
1199
|
+
if (!m.preview || m.preview === beforePreview) continue;
|
|
1200
|
+
// URL đổi rồi — so NỘI DUNG. Lệch nhiều = FB đổi khung chứ chưa phải ảnh
|
|
1201
|
+
// của mình: chờ tiếp. Không so được (ảnh CDN khoá canvas) thì rơi về luật
|
|
1202
|
+
// đổi-URL, kèm ghi rõ vào log. Ảnh chưa nạp xong thì chờ nhịp sau.
|
|
1203
|
+
lastCmp = await previewMatchesThumb(page, thumbDataUrl);
|
|
1204
|
+
if (lastCmp.err === 'preview-not-loaded') continue;
|
|
1205
|
+
if (lastCmp.diff !== undefined && lastCmp.diff > THUMB_PREVIEW_MAX_DIFF) {
|
|
1206
|
+
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`);
|
|
1207
|
+
loggedMismatch = true;
|
|
1208
|
+
continue;
|
|
1209
|
+
}
|
|
1210
|
+
changed = true;
|
|
1211
|
+
break;
|
|
1212
|
+
}
|
|
1213
|
+
if (!changed) {
|
|
1214
|
+
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)`);
|
|
1215
|
+
return { ok: false, reason: 'modal-preview-unchanged' };
|
|
1216
|
+
}
|
|
1217
|
+
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…`);
|
|
1218
|
+
let saved = false;
|
|
1219
|
+
for (let i = 0; i < (m.saves || 1) && !saved; i++) {
|
|
1220
|
+
try {
|
|
1221
|
+
await page.locator(`[__fbpw_thumb_modal__='1'] [__fbpw_thumb_modal_save__='${i}']`).first().click({ timeout: 5000 });
|
|
1222
|
+
saved = true;
|
|
1223
|
+
} catch (e) {
|
|
1224
|
+
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)}`);
|
|
1225
|
+
await findThumbPickerModal(page);
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
if (!saved) return { ok: false, reason: 'modal-save-failed' };
|
|
1229
|
+
// Đò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.
|
|
1230
|
+
const t1 = Date.now();
|
|
1231
|
+
let gone = 0;
|
|
1232
|
+
while (Date.now() - t1 < closeMs) {
|
|
1233
|
+
await pause(page, 1500);
|
|
1234
|
+
if (await findThumbPickerModal(page)) { gone = 0; continue; }
|
|
1235
|
+
if (++gone >= 2) return { ok: true, via };
|
|
1236
|
+
}
|
|
1237
|
+
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
|
+
return { ok: false, reason: 'modal-not-closed' };
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1022
1241
|
// Dán ảnh bìa vào composer mới. Trả { applied, reason }. Không ném: thất bại
|
|
1023
1242
|
// thì cờ customThumbDone không bật và cổng trước Đăng chặn như cũ.
|
|
1024
1243
|
async function applyNewComposerThumb(page, thumbPath, log, { waitPickerMs = 20_000, verifyMs = 30_000 } = {}) {
|
|
@@ -1048,17 +1267,34 @@ async function applyNewComposerThumb(page, thumbPath, log, { waitPickerMs = 20_0
|
|
|
1048
1267
|
const before = snap;
|
|
1049
1268
|
log('info', `[fb-pw] composer mới — hàng ảnh bìa trước khi dán: ${describeThumbRow(before)}`);
|
|
1050
1269
|
|
|
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).
|
|
1051
1272
|
let via = '';
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
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}` : ''}`);
|
|
1061
1296
|
}
|
|
1297
|
+
void chooserP;
|
|
1062
1298
|
if (!via) {
|
|
1063
1299
|
await readNewComposerThumbRow(page); // cú bấm có thể làm FB dựng lại DOM → gắn cờ lại
|
|
1064
1300
|
const input = page.locator("[__fbpw_thumb_input__='1']").first();
|
|
@@ -1625,7 +1861,7 @@ async function runOnce({ page, payload, log }) {
|
|
|
1625
1861
|
} = payload || {};
|
|
1626
1862
|
if (!video_url) throw new Error('No video_url provided');
|
|
1627
1863
|
|
|
1628
|
-
log('info', '[fb-pw] selectors version=2026.09.23-
|
|
1864
|
+
log('info', '[fb-pw] selectors version=2026.09.23-thumb-picker-modal');
|
|
1629
1865
|
|
|
1630
1866
|
page.on('dialog', (d) => { d.accept().catch(() => {}); });
|
|
1631
1867
|
|
|
@@ -4100,6 +4336,10 @@ module.exports.__testables = {
|
|
|
4100
4336
|
describeThumbRow,
|
|
4101
4337
|
readNewComposerThumbRow,
|
|
4102
4338
|
applyNewComposerThumb,
|
|
4339
|
+
findThumbPickerModal,
|
|
4340
|
+
uploadViaThumbPickerModal,
|
|
4341
|
+
previewMatchesThumb,
|
|
4342
|
+
THUMB_PREVIEW_MAX_DIFF,
|
|
4103
4343
|
// Dùng chung cho comment_facebook: script nào trượt giữa chừng cũng cần chụp
|
|
4104
4344
|
// lại màn hình, viết bản thứ hai là sớm muộn hai bên lệch nhau.
|
|
4105
4345
|
dumpFailure,
|