channel-worker 2.5.133 → 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 +660 -1
package/package.json
CHANGED
|
@@ -722,6 +722,643 @@ async function sampleCustomThumb(page, { samples = 3, gapMs = 1500 } = {}) {
|
|
|
722
722
|
return { verdict: thumbVerdict(out), samples: out };
|
|
723
723
|
}
|
|
724
724
|
|
|
725
|
+
// ─── Composer MỚI của Facebook (bắt 22-23/09/2026) ──────────────────────────
|
|
726
|
+
// Ba page (LACCA, Nhà Nhỏ Có Võ, Gà Vườn Mỗi Ngày) hỏng 11/11 lượt với
|
|
727
|
+
// "Custom thumbnail required but not applied". Chuỗi log lượt nào cũng như
|
|
728
|
+
// nhau: nút "Thước phim" trên tường không mở hộp thoại → nhánh dự phòng bấm
|
|
729
|
+
// link "Tạo thước phim" → link đó giờ đưa sang giao diện mới:
|
|
730
|
+
// /post/create form MỘT trang (9 lượt)
|
|
731
|
+
// /reels/create/?surface=ADDL_PROFILE_PLUS wizard "Chỉnh sửa › Cài đặt" (2 lượt)
|
|
732
|
+
// Khối ảnh bìa cũ chỉ chạy khi URL là facebook.com/ trần, nên cả khối bị bỏ
|
|
733
|
+
// qua (log mấy lượt đó không có một dòng thumb nào) và cổng trước Đăng chặn.
|
|
734
|
+
//
|
|
735
|
+
// Giao diện mới không có nút "Chỉnh sửa hình thu nhỏ". Mục "Hình thu nhỏ" là
|
|
736
|
+
// MỘT HÀNG Ô: ô đầu viền nét đứt ghi "Chọn hình thu nhỏ", tiếp theo là các
|
|
737
|
+
// khung FB tự cắt từ video, khung đang chọn viền xanh. KHÔNG được nới regex
|
|
738
|
+
// cho khối cũ: trên /post/create ô video có nút "Chỉnh sửa" riêng, bộ dò
|
|
739
|
+
// "narrow-pill" của khối cũ sẽ bấm nhầm vào trình sửa video.
|
|
740
|
+
const NEW_COMPOSER_URL_RE = /^https?:\/\/(www\.)?facebook\.com\/(post\/create|reels\/create)(\/|\?|#|$)/i;
|
|
741
|
+
function isNewComposerUrl(url) {
|
|
742
|
+
return NEW_COMPOSER_URL_RE.test(String(url || ''));
|
|
743
|
+
}
|
|
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
|
+
// khoảng trắng) để không dính caption hay nhãn mục "Hình thu nhỏ".
|
|
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
|
+
|
|
748
|
+
// Đọc hàng ô ảnh bìa của composer mới. Trả về ảnh chụp trạng thái để so
|
|
749
|
+
// trước/sau khi dán (xem newComposerThumbVerdict), và gắn cờ DOM cho:
|
|
750
|
+
// __fbpw_thumb_pick__ — chỗ bấm của ô "Chọn hình thu nhỏ"
|
|
751
|
+
// __fbpw_thumb_row__ — chính hàng ô (để tìm lại khi ô chọn-ảnh đã biến mất)
|
|
752
|
+
// __fbpw_thumb_input__ — input file CHỈ nhận ảnh nằm trong mục ảnh bìa
|
|
753
|
+
// __fbpw_thumb_tile__ — ô thứ `tagIndex` (dùng khi phải bấm chọn lại)
|
|
754
|
+
// Không bao giờ ném: đọc hỏng trả null, như isCustomThumbApplied.
|
|
755
|
+
async function readNewComposerThumbRow(page, { diag = false, tagIndex = -1 } = {}) {
|
|
756
|
+
return page.evaluate(({ pickSrc, diag, tagIndex }) => {
|
|
757
|
+
const PICK = new RegExp(pickSrc, 'i');
|
|
758
|
+
const norm = (s) => String(s || '').replace(/\s+/g, ' ').trim();
|
|
759
|
+
const vis = (el) => {
|
|
760
|
+
const r = el.getBoundingClientRect();
|
|
761
|
+
if (r.width < 8 || r.height < 8) return false;
|
|
762
|
+
const cs = getComputedStyle(el);
|
|
763
|
+
return cs.visibility !== 'hidden' && cs.display !== 'none' && cs.opacity !== '0';
|
|
764
|
+
};
|
|
765
|
+
for (const a of ['__fbpw_thumb_pick__', '__fbpw_thumb_input__', '__fbpw_thumb_tile__']) {
|
|
766
|
+
document.querySelectorAll(`[${a}]`).forEach((el) => el.removeAttribute(a));
|
|
767
|
+
}
|
|
768
|
+
const isPick = (el) => PICK.test(norm(el.getAttribute && el.getAttribute('aria-label')))
|
|
769
|
+
|| PICK.test(norm(el.innerText));
|
|
770
|
+
// Ảnh của một ô: <img>, <image> trong SVG, hoặc background-image. Bỏ qua
|
|
771
|
+
// icon nhỏ (ô chọn-ảnh có icon ~20px).
|
|
772
|
+
const mediaOf = (el) => {
|
|
773
|
+
for (const m of el.querySelectorAll("img, image, [style*='background-image']")) {
|
|
774
|
+
const r = m.getBoundingClientRect();
|
|
775
|
+
if (r.width < 40 || r.height < 40) continue;
|
|
776
|
+
const tag = m.tagName.toLowerCase();
|
|
777
|
+
let src = '';
|
|
778
|
+
if (tag === 'img') src = m.currentSrc || m.src || '';
|
|
779
|
+
else if (tag === 'image') src = m.getAttribute('href') || m.getAttribute('xlink:href') || '';
|
|
780
|
+
else src = ((m.style.backgroundImage || '').match(/url\(["']?(.*?)["']?\)/) || [])[1] || '';
|
|
781
|
+
if (src) return src.slice(0, 500);
|
|
782
|
+
}
|
|
783
|
+
return '';
|
|
784
|
+
};
|
|
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));
|
|
789
|
+
// Ô đang chọn: viền/bóng xanh của FB (#0866FF, #1877F2) hoặc aria-*.
|
|
790
|
+
const blueish = (c) => {
|
|
791
|
+
const m = String(c || '').match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?/);
|
|
792
|
+
if (!m) return false;
|
|
793
|
+
const [r, g, b] = [+m[1], +m[2], +m[3]];
|
|
794
|
+
const a = m[4] === undefined ? 1 : +m[4];
|
|
795
|
+
return a > 0.5 && b >= 180 && r <= 100 && g >= 60 && g <= 180;
|
|
796
|
+
};
|
|
797
|
+
const selOf = (tile) => {
|
|
798
|
+
for (const e of [tile, ...tile.querySelectorAll('*')].slice(0, 40)) {
|
|
799
|
+
for (const a of ['aria-checked', 'aria-selected', 'aria-pressed', 'aria-current']) {
|
|
800
|
+
if (e.getAttribute(a) === 'true') return true;
|
|
801
|
+
}
|
|
802
|
+
const cs = getComputedStyle(e);
|
|
803
|
+
if (parseFloat(cs.borderTopWidth) >= 2 && cs.borderTopStyle === 'solid' && blueish(cs.borderTopColor)) return true;
|
|
804
|
+
if (cs.boxShadow && cs.boxShadow !== 'none' && blueish(cs.boxShadow)) return true;
|
|
805
|
+
}
|
|
806
|
+
return false;
|
|
807
|
+
};
|
|
808
|
+
// Hàng ô = tổ tiên gần nhất có >= 2 con trông như ô ảnh.
|
|
809
|
+
const rowFrom = (el) => {
|
|
810
|
+
let node = el;
|
|
811
|
+
for (let d = 0; d < 10 && node && node.parentElement; d++) {
|
|
812
|
+
const p = node.parentElement;
|
|
813
|
+
if ([...p.children].filter(tileLike).length >= 2) return p;
|
|
814
|
+
node = p;
|
|
815
|
+
}
|
|
816
|
+
return null;
|
|
817
|
+
};
|
|
818
|
+
|
|
819
|
+
const roots = [...document.querySelectorAll("[role='dialog']")].filter((d) => vis(d)
|
|
820
|
+
&& /thước phim|reel/i.test(`${d.getAttribute('aria-label') || ''}\n${(d.innerText || '').slice(0, 600)}`));
|
|
821
|
+
const main = document.querySelector("[role='main'], main");
|
|
822
|
+
if (main && vis(main) && /^\/(post|reels)\/create/i.test(location.pathname)) roots.push(main);
|
|
823
|
+
|
|
824
|
+
// Đi theo NÚT CHỮ thay vì đọc innerText của mọi phần tử: composer thật có
|
|
825
|
+
// vài nghìn phần tử, innerText mỗi cái bắt trình duyệt dàn trang lại.
|
|
826
|
+
// Leo tối đa 4 tầng vì chữ có thể bị tách ("Chọn hình" / "thu nhỏ").
|
|
827
|
+
const textHits = (root, keyRe, fullRe) => {
|
|
828
|
+
const out = [];
|
|
829
|
+
const tw = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
|
830
|
+
for (let n = tw.nextNode(); n; n = tw.nextNode()) {
|
|
831
|
+
if (!keyRe.test(n.data)) continue;
|
|
832
|
+
for (let el = n.parentElement, d = 0; el && d < 4; el = el.parentElement, d++) {
|
|
833
|
+
if (fullRe.test(norm(el.innerText)) && vis(el)) { out.push(el); break; }
|
|
834
|
+
if (el === root) break;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
return out;
|
|
838
|
+
};
|
|
839
|
+
let pickLeaf = null;
|
|
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
|
+
};
|
|
848
|
+
const hits = [...root.querySelectorAll('[aria-label]')].filter((el) => PICK.test(norm(el.getAttribute('aria-label'))) && vis(el))
|
|
849
|
+
.concat(textHits(root, /hình|ảnh|thumbnail/i, PICK))
|
|
850
|
+
.filter((el) => !inPickerModal(el));
|
|
851
|
+
if (hits.length) { pickLeaf = hits[0]; break; }
|
|
852
|
+
}
|
|
853
|
+
let row = null;
|
|
854
|
+
let via = '';
|
|
855
|
+
if (pickLeaf) { row = rowFrom(pickLeaf); via = 'pick'; }
|
|
856
|
+
if (!row) {
|
|
857
|
+
const tagged = document.querySelector('[__fbpw_thumb_row__]');
|
|
858
|
+
if (tagged && tagged.isConnected && vis(tagged)) { row = tagged; via = 'tagged'; }
|
|
859
|
+
}
|
|
860
|
+
if (!row) {
|
|
861
|
+
// Ô chọn-ảnh đã biến mất (bị ảnh vừa dán thay chỗ) và FB dựng lại hàng:
|
|
862
|
+
// lần theo nhãn mục "Hình thu nhỏ".
|
|
863
|
+
for (const root of roots) {
|
|
864
|
+
// Leo từ nút chữ lên nên được phần tử SÂU nhất. Quan trọng: khi ô
|
|
865
|
+
// chọn-ảnh đã thành ảnh, cả khung mục (nhãn + hàng ô) cũng chỉ còn
|
|
866
|
+
// đúng chữ "Hình thu nhỏ" — ảnh không có chữ — nên dò từ trên xuống
|
|
867
|
+
// sẽ vớ khung mục chứ không phải nhãn (trang giả lập bắt được ca này).
|
|
868
|
+
const head = textHits(root, /^\s*(Hình thu nhỏ|Thumbnail)\s*$/i, /^(Hình thu nhỏ|Thumbnail)$/i)[0];
|
|
869
|
+
if (!head) continue;
|
|
870
|
+
let node = head;
|
|
871
|
+
for (let d = 0; d < 4 && node && !row; d++) {
|
|
872
|
+
for (let sib = node.nextElementSibling; sib && !row; sib = sib.nextElementSibling) {
|
|
873
|
+
const all = [sib, ...sib.querySelectorAll('*')];
|
|
874
|
+
row = all.find((el) => [...el.children].filter(tileLike).length >= 2) || null;
|
|
875
|
+
}
|
|
876
|
+
node = node.parentElement;
|
|
877
|
+
}
|
|
878
|
+
if (row) { via = 'heading'; break; }
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
const customMarker = [...document.querySelectorAll('[aria-label]')].some((el) =>
|
|
882
|
+
/Gỡ hình thu nhỏ tùy chỉnh|Remove( your)? custom thumbnail/i.test(el.getAttribute('aria-label') || '') && vis(el));
|
|
883
|
+
if (!row) {
|
|
884
|
+
const miss = { found: false, pickerPresent: !!pickLeaf, roots: roots.length, customMarker };
|
|
885
|
+
if (diag) {
|
|
886
|
+
// Không thấy hàng ô: kê các nút của composer, như thumb-diag của khối cũ,
|
|
887
|
+
// để biết ảnh bìa nằm ở đâu trên biến thể này.
|
|
888
|
+
miss.inventory = [];
|
|
889
|
+
for (const root of roots) {
|
|
890
|
+
for (const el of root.querySelectorAll("[role='button'], button, [aria-label], input[type='file']")) {
|
|
891
|
+
if (miss.inventory.length >= 45) break;
|
|
892
|
+
const r = el.getBoundingClientRect();
|
|
893
|
+
const t = norm(el.innerText).slice(0, 28);
|
|
894
|
+
const al = (el.getAttribute('aria-label') || '').slice(0, 40);
|
|
895
|
+
const acc = el.tagName === 'INPUT' ? ` accept=${el.getAttribute('accept') || ''}` : '';
|
|
896
|
+
if ((t || al || acc) && (acc || (r.width >= 10 && r.height >= 10))) miss.inventory.push(`<${el.tagName.toLowerCase()}> t="${t}" al="${al}"${acc} ${Math.round(r.width)}x${Math.round(r.height)}`);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
return miss;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
document.querySelectorAll('[__fbpw_thumb_row__]').forEach((el) => { if (el !== row) el.removeAttribute('__fbpw_thumb_row__'); });
|
|
904
|
+
row.setAttribute('__fbpw_thumb_row__', '1');
|
|
905
|
+
const tiles = [...row.children].filter(tileLike);
|
|
906
|
+
const out = tiles.map((t) => ({ img: mediaOf(t), placeholder: isPick(t), selected: selOf(t) }));
|
|
907
|
+
const flagged = out.map((t, i) => (t.selected ? i : -1)).filter((i) => i >= 0);
|
|
908
|
+
|
|
909
|
+
let pickerPresent = false;
|
|
910
|
+
const pickTile = tiles.find((t) => isPick(t));
|
|
911
|
+
if (pickTile) {
|
|
912
|
+
pickerPresent = true;
|
|
913
|
+
// Bấm vào phần tử bấm-được sâu nhất bên trong ô, không có thì bấm cả ô.
|
|
914
|
+
let target = null;
|
|
915
|
+
const leaf = [...pickTile.querySelectorAll('*')].filter((el) => vis(el) && isPick(el)).pop() || pickTile;
|
|
916
|
+
for (let n = leaf; n && n !== row; n = n.parentElement) {
|
|
917
|
+
if (n.matches("[role='button'], button, label, a, [tabindex]")) { target = n; break; }
|
|
918
|
+
}
|
|
919
|
+
(target || pickTile).setAttribute('__fbpw_thumb_pick__', '1');
|
|
920
|
+
}
|
|
921
|
+
let input = null;
|
|
922
|
+
for (let scope = row, d = 0; scope && d < 3 && !input; scope = scope.parentElement, d++) {
|
|
923
|
+
for (const el of scope.querySelectorAll("input[type='file']")) {
|
|
924
|
+
const accept = (el.getAttribute('accept') || '').toLowerCase();
|
|
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;
|
|
928
|
+
el.setAttribute('__fbpw_thumb_input__', '1');
|
|
929
|
+
input = { accept: accept.slice(0, 60) };
|
|
930
|
+
break;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
if (tagIndex >= 0 && tiles[tagIndex]) tiles[tagIndex].setAttribute('__fbpw_thumb_tile__', '1');
|
|
934
|
+
|
|
935
|
+
const res = {
|
|
936
|
+
found: true,
|
|
937
|
+
via,
|
|
938
|
+
pickerPresent,
|
|
939
|
+
tiles: out,
|
|
940
|
+
selectedIndex: flagged.length === 1 ? flagged[0] : null,
|
|
941
|
+
customMarker,
|
|
942
|
+
input,
|
|
943
|
+
};
|
|
944
|
+
if (diag) {
|
|
945
|
+
// Khung xương mục ảnh bìa, để lần sau FB đổi DOM còn có cái mà soi.
|
|
946
|
+
const sec = row.parentElement || row;
|
|
947
|
+
res.skeleton = [sec, ...sec.querySelectorAll('*')].filter((el) => el.tagName !== 'path').slice(0, 60).map((el) => {
|
|
948
|
+
const r = el.getBoundingClientRect();
|
|
949
|
+
const role = el.getAttribute('role');
|
|
950
|
+
const al = el.getAttribute('aria-label');
|
|
951
|
+
const own = el.children.length ? '' : norm(el.innerText).slice(0, 20);
|
|
952
|
+
const acc = el.tagName === 'INPUT' ? ` accept=${el.getAttribute('accept') || ''}` : '';
|
|
953
|
+
return `<${el.tagName.toLowerCase()}${role ? ` role=${role}` : ''}${al ? ` al="${al.slice(0, 30)}"` : ''}${own ? ` t="${own}"` : ''}${acc}> ${Math.round(r.width)}x${Math.round(r.height)}`;
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
return res;
|
|
957
|
+
}, { pickSrc: THUMB_PICKER_TEXT_RE.source, diag, tagIndex }).catch(() => null);
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// Ảnh bìa tự chọn đã VÀO composer mới chưa? So hàng ô trước và sau khi dán
|
|
961
|
+
// (thuần, test được). Chỉ nhận hai dấu hiệu gắn với CHÍNH ô chọn-ảnh, không
|
|
962
|
+
// nhận "có ảnh lạ xuất hiện đâu đó": FB có thể dựng lại các khung tự động với
|
|
963
|
+
// URL mới, coi đó là ảnh của mình thì bài lên với ảnh bìa tự động — đúng thứ
|
|
964
|
+
// cổng trước Đăng sinh ra để chặn.
|
|
965
|
+
// A. ô chọn-ảnh bị thay bằng một ảnh mới (cùng vị trí, hết chữ "Chọn…")
|
|
966
|
+
// B. hàng thêm đúng MỘT ô, ô mới có ảnh chưa từng thấy và nằm sát ô chọn-ảnh
|
|
967
|
+
// Rồi mới xét ô đang chọn: đọc được mà không phải ô của mình → 'not-selected'.
|
|
968
|
+
function newComposerThumbVerdict(before, after) {
|
|
969
|
+
if (after && after.customMarker) return { verdict: 'applied', why: 'nút "Gỡ hình thu nhỏ tùy chỉnh" đang hiện' };
|
|
970
|
+
if (!before || !before.found || !after || !after.found) {
|
|
971
|
+
return { verdict: 'unknown', why: 'không đọc được hàng ô ảnh bìa' };
|
|
972
|
+
}
|
|
973
|
+
const bTiles = before.tiles || [];
|
|
974
|
+
const aTiles = after.tiles || [];
|
|
975
|
+
const seen = new Set(bTiles.map((t) => t.img).filter(Boolean));
|
|
976
|
+
const p = bTiles.findIndex((t) => t.placeholder);
|
|
977
|
+
let custom = -1;
|
|
978
|
+
let how = '';
|
|
979
|
+
if (p >= 0 && aTiles[p] && aTiles[p].img && !aTiles[p].placeholder && !seen.has(aTiles[p].img)) {
|
|
980
|
+
custom = p;
|
|
981
|
+
how = 'ô "Chọn hình thu nhỏ" đã thành ảnh mới';
|
|
982
|
+
}
|
|
983
|
+
if (custom < 0 && aTiles.length === bTiles.length + 1) {
|
|
984
|
+
const anchor = Math.max(p, 0);
|
|
985
|
+
const i = aTiles.findIndex((t, k) => t.img && !seen.has(t.img) && Math.abs(k - anchor) <= 1);
|
|
986
|
+
if (i >= 0) { custom = i; how = `thêm ô ảnh mới ở vị trí ${i}`; }
|
|
987
|
+
}
|
|
988
|
+
if (custom < 0) return { verdict: 'absent', why: 'hàng ô không có ảnh nào mới ở chỗ ô chọn-ảnh' };
|
|
989
|
+
const sel = after.selectedIndex;
|
|
990
|
+
if (sel === null || sel === undefined) {
|
|
991
|
+
return { verdict: 'applied', customIndex: custom, why: `${how}; không đọc được ô đang chọn` };
|
|
992
|
+
}
|
|
993
|
+
if (sel === custom) return { verdict: 'applied', customIndex: custom, why: `${how}, đang được chọn` };
|
|
994
|
+
return { verdict: 'not-selected', customIndex: custom, why: `${how} nhưng ô đang chọn là ${sel}` };
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
// Tóm tắt một hàng ô cho log: "5 ô [chọn-ảnh, *khung, khung…] chọn=1 input=image/*".
|
|
998
|
+
function describeThumbRow(snap) {
|
|
999
|
+
if (!snap) return 'không đọc được';
|
|
1000
|
+
if (!snap.found) return `không thấy hàng ô (composer=${snap.roots || 0}, ô chọn-ảnh=${snap.pickerPresent ? 'có' : 'không'})`;
|
|
1001
|
+
const cells = (snap.tiles || []).map((t) => `${t.selected ? '*' : ''}${t.placeholder ? 'chọn-ảnh' : (t.img ? (/^(blob|data):/.test(t.img) ? 'ảnh-local' : 'khung') : '?')}`);
|
|
1002
|
+
return `${cells.length} ô [${cells.join(', ')}] chọn=${snap.selectedIndex ?? '?'} input=${snap.input ? snap.input.accept || '?' : 'không'} via=${snap.via}`;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// Hộp phụ FB có thể bật lên sau khi nhận ảnh (cắt ảnh / xác nhận). Chưa thấy
|
|
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ữ
|
|
1007
|
+
// có nhắc ảnh bìa, để không bao giờ trúng nút "Lưu" (= lưu nháp) của composer.
|
|
1008
|
+
async function confirmNewComposerThumbDialog(page, log) {
|
|
1009
|
+
const hit = await page.evaluate(({ compSrc, pickSrc }) => {
|
|
1010
|
+
const COMP = new RegExp(compSrc, 'i');
|
|
1011
|
+
const PICK = new RegExp(pickSrc, 'i');
|
|
1012
|
+
for (const dlg of document.querySelectorAll("[role='dialog']")) {
|
|
1013
|
+
const r = dlg.getBoundingClientRect();
|
|
1014
|
+
if (r.width < 8 || r.height < 8) continue;
|
|
1015
|
+
const head = `${dlg.getAttribute('aria-label') || ''} | ${(dlg.innerText || '').slice(0, 300)}`;
|
|
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;
|
|
1020
|
+
if (!/hình thu nhỏ|ảnh thu nhỏ|thumbnail|cắt ảnh|crop/i.test(head)) continue;
|
|
1021
|
+
for (const b of dlg.querySelectorAll("button, [role='button']")) {
|
|
1022
|
+
const t = (b.innerText || '').trim();
|
|
1023
|
+
const br = b.getBoundingClientRect();
|
|
1024
|
+
if (br.width < 8 || br.height < 8 || b.getAttribute('aria-disabled') === 'true') continue;
|
|
1025
|
+
if (!/^(Lưu|Xong|Áp dụng|Save|Done|Apply)$/i.test(t)) continue;
|
|
1026
|
+
b.setAttribute('__fbpw_thumb_confirm__', '1');
|
|
1027
|
+
return { verb: t, head: head.slice(0, 60).replace(/\s+/g, ' ') };
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
return null;
|
|
1031
|
+
}, { compSrc: COMPOSER_SIGNATURE_RE.source, pickSrc: THUMB_PICKER_TEXT_RE.source }).catch(() => null);
|
|
1032
|
+
if (!hit) return false;
|
|
1033
|
+
log('info', `[fb-pw] composer mới — hộp phụ ảnh bìa "${hit.head}" → bấm "${hit.verb}"`);
|
|
1034
|
+
await page.locator("[__fbpw_thumb_confirm__='1']").click({ timeout: 3000 }).catch(() => {});
|
|
1035
|
+
await page.evaluate(() => document.querySelectorAll('[__fbpw_thumb_confirm__]').forEach((el) => el.removeAttribute('__fbpw_thumb_confirm__'))).catch(() => {});
|
|
1036
|
+
return true;
|
|
1037
|
+
}
|
|
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
|
+
|
|
1241
|
+
// Dán ảnh bìa vào composer mới. Trả { applied, reason }. Không ném: thất bại
|
|
1242
|
+
// thì cờ customThumbDone không bật và cổng trước Đăng chặn như cũ.
|
|
1243
|
+
async function applyNewComposerThumb(page, thumbPath, log, { waitPickerMs = 20_000, verifyMs = 30_000 } = {}) {
|
|
1244
|
+
const t0 = Date.now();
|
|
1245
|
+
const ready = (s) => !!(s && s.found && s.pickerPresent);
|
|
1246
|
+
let snap = await readNewComposerThumbRow(page);
|
|
1247
|
+
// 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) {
|
|
1249
|
+
await pause(page, 2500);
|
|
1250
|
+
snap = await readNewComposerThumbRow(page);
|
|
1251
|
+
}
|
|
1252
|
+
if (!ready(snap)) {
|
|
1253
|
+
const d = await readNewComposerThumbRow(page, { diag: true });
|
|
1254
|
+
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)}` : ''}`);
|
|
1255
|
+
return { applied: false, reason: 'no-picker' };
|
|
1256
|
+
}
|
|
1257
|
+
// Lấy mốc "trước" khi số ô đã đứng yên: FB mọc thêm khung dần dần, một khung
|
|
1258
|
+
// mọc muộn không được bị tưởng là ảnh bìa của mình.
|
|
1259
|
+
for (let i = 0; i < 3; i++) {
|
|
1260
|
+
await pause(page, 1500);
|
|
1261
|
+
const again = await readNewComposerThumbRow(page);
|
|
1262
|
+
if (!ready(again)) continue;
|
|
1263
|
+
const settled = again.tiles.length === snap.tiles.length;
|
|
1264
|
+
snap = again;
|
|
1265
|
+
if (settled) break;
|
|
1266
|
+
}
|
|
1267
|
+
const before = snap;
|
|
1268
|
+
log('info', `[fb-pw] composer mới — hàng ảnh bìa trước khi dán: ${describeThumbRow(before)}`);
|
|
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).
|
|
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;
|
|
1298
|
+
if (!via) {
|
|
1299
|
+
await readNewComposerThumbRow(page); // cú bấm có thể làm FB dựng lại DOM → gắn cờ lại
|
|
1300
|
+
const input = page.locator("[__fbpw_thumb_input__='1']").first();
|
|
1301
|
+
if (await input.count().catch(() => 0)) {
|
|
1302
|
+
try {
|
|
1303
|
+
await input.setInputFiles(thumbPath);
|
|
1304
|
+
via = 'input file chỉ nhận ảnh';
|
|
1305
|
+
} catch (e) {
|
|
1306
|
+
log('info', `[fb-pw] composer mới — input ảnh bìa từ chối file: ${String(e && e.message || e).slice(0, 80)}`);
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
if (!via) {
|
|
1311
|
+
const d = await readNewComposerThumbRow(page, { diag: true });
|
|
1312
|
+
log('warn', `[fb-pw] composer mới — KHÔNG đưa được file ảnh bìa vào: ${describeThumbRow(d)}${d && d.skeleton ? ` skeleton=${JSON.stringify(d.skeleton)}` : ''}`);
|
|
1313
|
+
return { applied: false, reason: 'no-upload' };
|
|
1314
|
+
}
|
|
1315
|
+
log('info', `[fb-pw] composer mới — đã đưa file ảnh bìa vào qua ${via}, chờ FB nhận…`);
|
|
1316
|
+
|
|
1317
|
+
let v = { verdict: 'unknown', why: 'chưa đo' };
|
|
1318
|
+
let after = null;
|
|
1319
|
+
// Đòi HAI nhịp liền cùng thấy ảnh đã vào: ô có thể hiện ảnh xem trước ngay
|
|
1320
|
+
// khi nhận file, lúc FB còn đang tải lên và có thể trả lỗi rồi gỡ ra.
|
|
1321
|
+
let appliedStreak = 0;
|
|
1322
|
+
let notSelectedPolls = 0;
|
|
1323
|
+
let selectTried = false;
|
|
1324
|
+
let confirmTried = false;
|
|
1325
|
+
const deadline = Date.now() + verifyMs;
|
|
1326
|
+
while (Date.now() < deadline) {
|
|
1327
|
+
await pause(page, 2500);
|
|
1328
|
+
after = await readNewComposerThumbRow(page);
|
|
1329
|
+
v = newComposerThumbVerdict(before, after);
|
|
1330
|
+
if (v.verdict === 'applied') {
|
|
1331
|
+
if (++appliedStreak >= 2) break;
|
|
1332
|
+
continue;
|
|
1333
|
+
}
|
|
1334
|
+
appliedStreak = 0;
|
|
1335
|
+
if (v.verdict === 'absent' && !confirmTried) {
|
|
1336
|
+
confirmTried = await confirmNewComposerThumbDialog(page, log);
|
|
1337
|
+
continue;
|
|
1338
|
+
}
|
|
1339
|
+
// FB có thể cần một nhịp mới chuyển viền xanh sang ảnh vừa dán; hai nhịp
|
|
1340
|
+
// liền vẫn lệch thì bấm chọn ô đó MỘT lần. Canh sẵn hộp chọn file: nếu cú
|
|
1341
|
+
// bấm lại mở hộp chọn (ô vẫn là nút tải lên), dán lại đúng file đó chứ
|
|
1342
|
+
// không để hộp thoại hệ điều hành treo trên trình duyệt.
|
|
1343
|
+
if (v.verdict === 'not-selected' && ++notSelectedPolls >= 2 && !selectTried) {
|
|
1344
|
+
selectTried = true;
|
|
1345
|
+
await readNewComposerThumbRow(page, { tagIndex: v.customIndex });
|
|
1346
|
+
const chooserP = page.waitForEvent('filechooser', { timeout: 3000 }).catch(() => null);
|
|
1347
|
+
await page.locator("[__fbpw_thumb_tile__='1']").first().click({ timeout: 3000 }).catch(() => {});
|
|
1348
|
+
const ch = await chooserP;
|
|
1349
|
+
if (ch) await ch.setFiles(thumbPath).catch(() => {});
|
|
1350
|
+
log('info', `[fb-pw] composer mới — ${v.why} → bấm chọn ô ${v.customIndex}${ch ? ' (mở lại hộp chọn file, dán lại)' : ''}`);
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
if (v.verdict === 'applied' && appliedStreak >= 2) {
|
|
1354
|
+
log('info', `[fb-pw] composer mới — ảnh bìa đã vào: ${v.why} — ${describeThumbRow(after)}`);
|
|
1355
|
+
return { applied: true, reason: v.why };
|
|
1356
|
+
}
|
|
1357
|
+
const d = await readNewComposerThumbRow(page, { diag: true });
|
|
1358
|
+
log('warn', `[fb-pw] composer mới — dán ảnh bìa KHÔNG xác nhận được (${v.verdict}: ${v.why}) — trước: ${describeThumbRow(before)} — sau: ${describeThumbRow(d)}${d && d.skeleton ? ` skeleton=${JSON.stringify(d.skeleton)}` : ''}`);
|
|
1359
|
+
return { applied: false, reason: v.verdict };
|
|
1360
|
+
}
|
|
1361
|
+
|
|
725
1362
|
// Composer đang ở trạng thái gì? Đọc từ CHỮ trong hộp thoại — ba chữ ký nhìn
|
|
726
1363
|
// được từ ảnh dump 10-11/09 (5 ca "KHÔNG commit" + 2 ca no-advance-3):
|
|
727
1364
|
// rightsCheck : dòng đỏ "Không hoàn tất được quy trình kiểm tra bản quyền."
|
|
@@ -1224,7 +1861,7 @@ async function runOnce({ page, payload, log }) {
|
|
|
1224
1861
|
} = payload || {};
|
|
1225
1862
|
if (!video_url) throw new Error('No video_url provided');
|
|
1226
1863
|
|
|
1227
|
-
log('info', '[fb-pw] selectors version=2026.09.
|
|
1864
|
+
log('info', '[fb-pw] selectors version=2026.09.23-thumb-picker-modal');
|
|
1228
1865
|
|
|
1229
1866
|
page.on('dialog', (d) => { d.accept().catch(() => {}); });
|
|
1230
1867
|
|
|
@@ -2089,6 +2726,15 @@ async function runOnce({ page, payload, log }) {
|
|
|
2089
2726
|
await pause(page, 3000);
|
|
2090
2727
|
await fillMetadata();
|
|
2091
2728
|
|
|
2729
|
+
// COMPOSER MỚI (/post/create, /reels/create…) — ảnh bìa là ô "Chọn hình
|
|
2730
|
+
// thu nhỏ" trong hàng khung, không có nút "Chỉnh sửa hình thu nhỏ". Xem
|
|
2731
|
+
// applyNewComposerThumb. Chạy ở mọi bước cho tới khi dán được: form một
|
|
2732
|
+
// trang chỉ có một bước, wizard có thể để hàng ô ở bước sau.
|
|
2733
|
+
if (!customThumbDone && thumbPath && isNewComposerUrl(page.url())) {
|
|
2734
|
+
const nt = await applyNewComposerThumb(page, thumbPath, log);
|
|
2735
|
+
if (nt.applied) customThumbDone = true;
|
|
2736
|
+
}
|
|
2737
|
+
|
|
2092
2738
|
// PAGE-WALL thumb flow (form 2 has a "Chỉnh sửa" overlay button on the
|
|
2093
2739
|
// video preview → opens "Chỉnh sửa hình thu nhỏ" modal → "Tải lên" →
|
|
2094
2740
|
// file picker → "Lưu"). Per user screenshots, this is the only way to
|
|
@@ -3681,6 +4327,19 @@ module.exports.__testables = {
|
|
|
3681
4327
|
// 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
|
|
3682
4328
|
// được biến thành "chưa áp" rồi kéo theo hành động PHÁ (Hủy = gỡ ảnh bìa).
|
|
3683
4329
|
thumbVerdict,
|
|
4330
|
+
// Composer mới (/post/create, /reels/create…) — ảnh bìa là ô "Chọn hình thu
|
|
4331
|
+
// nhỏ" trong hàng khung. Phán quyết thuần + luồng dán để chạy thử trên trang
|
|
4332
|
+
// giả lập.
|
|
4333
|
+
isNewComposerUrl,
|
|
4334
|
+
THUMB_PICKER_TEXT_RE,
|
|
4335
|
+
newComposerThumbVerdict,
|
|
4336
|
+
describeThumbRow,
|
|
4337
|
+
readNewComposerThumbRow,
|
|
4338
|
+
applyNewComposerThumb,
|
|
4339
|
+
findThumbPickerModal,
|
|
4340
|
+
uploadViaThumbPickerModal,
|
|
4341
|
+
previewMatchesThumb,
|
|
4342
|
+
THUMB_PREVIEW_MAX_DIFF,
|
|
3684
4343
|
// Dùng chung cho comment_facebook: script nào trượt giữa chừng cũng cần chụp
|
|
3685
4344
|
// lại màn hình, viết bản thứ hai là sớm muộn hai bên lệch nhau.
|
|
3686
4345
|
dumpFailure,
|