channel-worker 2.5.100 → 2.5.102
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 +126 -9
package/package.json
CHANGED
|
@@ -497,7 +497,7 @@ async function waitForPublishEnabled(page, verbs, log, timeoutMs, tag = 'fb-pw',
|
|
|
497
497
|
absentGraceMs = PUBLISH_CTA_ABSENT_GRACE_MS,
|
|
498
498
|
} = {}) {
|
|
499
499
|
const deadline = Date.now() + timeoutMs;
|
|
500
|
-
let announced = false, sawPresent = false, absentSince = 0;
|
|
500
|
+
let announced = false, sawPresent = false, absentSince = 0, stuckSince = 0;
|
|
501
501
|
while (Date.now() < deadline) {
|
|
502
502
|
// The final metadata form mounts lazily on some FB cohorts. The first
|
|
503
503
|
// fillMetadata() immediately after Tiếp can run before its textbox exists,
|
|
@@ -510,7 +510,7 @@ async function waitForPublishEnabled(page, verbs, log, timeoutMs, tag = 'fb-pw',
|
|
|
510
510
|
const dlgs = document.querySelectorAll("[role='dialog']");
|
|
511
511
|
const roots = dlgs.length ? Array.from(dlgs) : [document];
|
|
512
512
|
const vh = window.innerHeight;
|
|
513
|
-
let present = false, enabled = false, rejected = '';
|
|
513
|
+
let present = false, enabled = false, rejected = '', text = '';
|
|
514
514
|
for (const root of roots) {
|
|
515
515
|
for (const el of root.querySelectorAll("button, [role='button']")) {
|
|
516
516
|
const t = (el.innerText || el.textContent || '').trim();
|
|
@@ -532,9 +532,22 @@ async function waitForPublishEnabled(page, verbs, log, timeoutMs, tag = 'fb-pw',
|
|
|
532
532
|
const txt = (root.innerText || '').slice(0, 4000);
|
|
533
533
|
const m = /Không thể tải (?:file|tệp)[^\n]{0,120}|couldn['’]t upload your (?:file|video)[^\n]{0,80}|Unable to upload[^\n]{0,80}/i.exec(txt);
|
|
534
534
|
if (m) rejected = m[0].trim();
|
|
535
|
+
text += txt + '\n';
|
|
535
536
|
}
|
|
536
|
-
return { present, enabled, rejected };
|
|
537
|
-
}, verbs).catch(() => ({ present: false, enabled: false, rejected: '' }));
|
|
537
|
+
return { present, enabled, rejected, text: text.slice(0, 6000) };
|
|
538
|
+
}, verbs).catch(() => ({ present: false, enabled: false, rejected: '', text: '' }));
|
|
539
|
+
// Bảng cài đặt không tải (tooltip "Vui lòng đợi tải xong quyền riêng tư…"):
|
|
540
|
+
// Đăng sẽ xám mãi, đứng đủ 600s là đốt profile vô ích. Thấy liên tục quá
|
|
541
|
+
// SETTINGS_STUCK_MS thì trả về để call site mở lại composer một lượt.
|
|
542
|
+
if (classifyComposerText(st.text).settingsStuck) {
|
|
543
|
+
if (!stuckSince) stuckSince = Date.now();
|
|
544
|
+
if (Date.now() - stuckSince >= SETTINGS_STUCK_MS) {
|
|
545
|
+
log('warn', `[${tag}] bảng cài đặt thước phim không tải quá ${Math.round(SETTINGS_STUCK_MS / 1000)}s (tooltip "đợi tải xong quyền riêng tư") — không chờ hết ${Math.round(timeoutMs / 1000)}s`);
|
|
546
|
+
return { enabled: false, sawPresent, settingsStuck: true };
|
|
547
|
+
}
|
|
548
|
+
} else {
|
|
549
|
+
stuckSince = 0;
|
|
550
|
+
}
|
|
538
551
|
if (st.rejected) {
|
|
539
552
|
log('warn', `[${tag}] Facebook TỪ CHỐI file: "${st.rejected.slice(0, 120)}" — nút "Đăng" sẽ không bật, dừng chờ ngay`);
|
|
540
553
|
return { enabled: false, sawPresent: true, rejected: st.rejected };
|
|
@@ -624,6 +637,51 @@ async function isCustomThumbApplied(page) {
|
|
|
624
637
|
}).catch(() => false);
|
|
625
638
|
}
|
|
626
639
|
|
|
640
|
+
// Composer đang ở trạng thái gì? Đọc từ CHỮ trong hộp thoại — ba chữ ký nhìn
|
|
641
|
+
// được từ ảnh dump 10-11/09 (5 ca "KHÔNG commit" + 2 ca no-advance-3):
|
|
642
|
+
// rightsCheck : dòng đỏ "Không hoàn tất được quy trình kiểm tra bản quyền."
|
|
643
|
+
// trên nút Đăng → FB không commit, bấm mù 3 lần vô ích; ~20'
|
|
644
|
+
// sau đăng lại là được (Chuyện Nhà Nông 07:04, Làm Giàu Từ
|
|
645
|
+
// Chăn Nuôi 07:16 ngày 11/09).
|
|
646
|
+
// publishing : lớp phủ "Đang đăng" trên video → FB ĐANG đăng; bản cũ chỉ
|
|
647
|
+
// chờ 3×8s rồi phán "CHƯA được đăng" → lịch đăng lại → nguy
|
|
648
|
+
// cơ đúp (Bí Quyết Nhà Nông 20:41, Nông Nghiệp Thực Tế 17:54
|
|
649
|
+
// ngày 10/09 — may là lượt đó không hoàn tất).
|
|
650
|
+
// settingsStuck : bảng cài đặt bên trái trống + tooltip "Vui lòng đợi tải
|
|
651
|
+
// xong quyền riêng tư của Thước phim" → Đăng xám mãi; đứng
|
|
652
|
+
// 600s rồi "description field not found" (phụ kiện điện tử
|
|
653
|
+
// 02 10/09, kol du lịch 11/09).
|
|
654
|
+
// Thuần, test được; regex neo đầu dòng cho "Đang đăng" để không dính mô tả
|
|
655
|
+
// bài có chữ "đang đăng" giữa câu.
|
|
656
|
+
const COMPOSER_TEXT_RE = {
|
|
657
|
+
rightsCheck: /Không hoàn tất được quy trình kiểm tra bản quyền|couldn['’]t complete (?:the )?(?:rights|copyright) check/i,
|
|
658
|
+
publishing: /(?:^|\n)\s*(?:Đang đăng|Đang xuất bản|Publishing|Posting)\s*(?:\.{3}|…)?\s*(?:\n|$)/i,
|
|
659
|
+
settingsStuck: /Vui lòng đợi tải xong quyền riêng tư của Thước phim|wait (?:for|until) (?:the )?reel(?:'s)? privacy/i,
|
|
660
|
+
};
|
|
661
|
+
function classifyComposerText(txt) {
|
|
662
|
+
const t = String(txt || '');
|
|
663
|
+
return {
|
|
664
|
+
rightsCheck: COMPOSER_TEXT_RE.rightsCheck.test(t),
|
|
665
|
+
publishing: COMPOSER_TEXT_RE.publishing.test(t),
|
|
666
|
+
settingsStuck: COMPOSER_TEXT_RE.settingsStuck.test(t),
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
async function readComposerText(page) {
|
|
670
|
+
return page.evaluate(() => {
|
|
671
|
+
const out = [];
|
|
672
|
+
for (const dlg of document.querySelectorAll("[role='dialog']")) {
|
|
673
|
+
const r = dlg.getBoundingClientRect();
|
|
674
|
+
if (r.width < 8 || r.height < 8) continue;
|
|
675
|
+
const cs = getComputedStyle(dlg);
|
|
676
|
+
if (cs.visibility === 'hidden' || cs.display === 'none') continue;
|
|
677
|
+
out.push(dlg.innerText || '');
|
|
678
|
+
}
|
|
679
|
+
return out.join('\n').slice(0, 8000);
|
|
680
|
+
}).catch(() => '');
|
|
681
|
+
}
|
|
682
|
+
const PUBLISHING_WAIT_MS = 5 * 60 * 1000; // "Đang đăng": chờ tới 5 phút, không bấm lại
|
|
683
|
+
const SETTINGS_STUCK_MS = 90 * 1000; // bảng cài đặt trống quá 90s: mở lại composer
|
|
684
|
+
|
|
627
685
|
const SAFE_RETRY_COMPOSER_CLOSED = 'FB_SAFE_RETRY_COMPOSER_CLOSED';
|
|
628
686
|
|
|
629
687
|
function safeComposerRetryError(message) {
|
|
@@ -744,6 +802,19 @@ function diffNewReelIds(beforeIds, afterIds) {
|
|
|
744
802
|
return (afterIds || []).filter((id) => !before.has(id));
|
|
745
803
|
}
|
|
746
804
|
|
|
805
|
+
// ID từ gói mạng: tab Reels xác nhận chỉ chứng minh reel THUỘC PAGE MÌNH, không
|
|
806
|
+
// chứng minh nó MỚI. Ca 10/09 16:26 "ẩm thực đồng quê 01": snapshotLen=0 nên cả
|
|
807
|
+
// 40 ID bắt được đều "mới", ID đầu tiên 1600960624772777 có trên tab (là bài
|
|
808
|
+
// 07:55 sáng CỦA CHÍNH KÊNH) → nhận → API thấy link trùng idea khác → gỡ → bài
|
|
809
|
+
// thành "mập mờ". Ảnh nền tab Reels chụp trước khi mở composer là bằng chứng
|
|
810
|
+
// "cũ": ID nằm trong ảnh nền thì KHÔNG BAO GIỜ là bài vừa đăng. Thuần, test được.
|
|
811
|
+
function networkIdVerdict({ id, beforeIds, tabIds }) {
|
|
812
|
+
if (!id) return 'none';
|
|
813
|
+
if ((beforeIds || []).includes(id)) return 'stale'; // đã có trước khi đăng
|
|
814
|
+
if ((tabIds || []).includes(id)) return 'confirmed'; // của page mình và mới
|
|
815
|
+
return 'candidate'; // chưa thấy trên tab, chờ
|
|
816
|
+
}
|
|
817
|
+
|
|
747
818
|
// Chữ ký của chính composer (mọi bước của wizard). Dùng để KHÔNG bao giờ bấm
|
|
748
819
|
// nhầm vào nút của composer khi đang định bấm nút của hộp ảnh bìa.
|
|
749
820
|
const COMPOSER_SIGNATURE_RE = /Cài đặt thước phim|Tạo thước phim|Chỉnh sửa thước phim|Reel settings|Create (?:a )?reel|Edit reel/i;
|
|
@@ -1033,7 +1104,15 @@ async function runOnce({ page, payload, log }) {
|
|
|
1033
1104
|
log('info', `[fb-pw] after Thước phim click — url=${afterUrl}`);
|
|
1034
1105
|
// Sanity check: clicking the sidebar Reels link navigates to /reel/<id>.
|
|
1035
1106
|
// That means we clicked the wrong button — fail fast so the user knows.
|
|
1036
|
-
|
|
1107
|
+
// 10/09 19:12 "phụ kiện điện tử 02": URL thành /reel/1850660615918243 nhưng
|
|
1108
|
+
// ảnh dump cho thấy hộp "Tạo thước phim" (Thêm video / Tải lên) ĐANG MỞ đè
|
|
1109
|
+
// lên feed Reels — Facebook giờ vẽ trình tạo như một lớp phủ trên /reel/<id>
|
|
1110
|
+
// chứ không điều hướng sang /reels/create/. Phán "bị đá về feed" theo URL
|
|
1111
|
+
// là báo oan tài khoản ("cần người đăng nhập thử tay") trong khi composer
|
|
1112
|
+
// đã sẵn sàng. Có composer thì cứ đi tiếp; chỉ khi KHÔNG có mới là bị đá.
|
|
1113
|
+
if (/\/reel\/\d+/.test(afterUrl) && (await hasVisibleReelComposer(page))) {
|
|
1114
|
+
log('info', `[fb-pw] url=/reel/<id> nhưng hộp Tạo thước phim đang mở đè lên feed — không phải bị đá, đi tiếp`);
|
|
1115
|
+
} else if (/\/reel\/\d+/.test(afterUrl)) {
|
|
1037
1116
|
// HAI nguyên nhân hoàn toàn khác nhau, trước đây gộp chung một câu báo
|
|
1038
1117
|
// "wrong button matched" — và câu đó gửi người đọc đi tìm lỗi DOM không
|
|
1039
1118
|
// hề tồn tại (kênh 6a2e66b3 hỏng suốt từ 27/08 vì hiểu nhầm chỗ này):
|
|
@@ -2632,6 +2711,14 @@ async function runOnce({ page, payload, log }) {
|
|
|
2632
2711
|
// đăng nhập), và PublishRetry phân loại đúng "file bị từ chối".
|
|
2633
2712
|
throw new Error(`FB_PUBLISH_REJECTED: Facebook từ chối file video ("${waitResult.rejected.slice(0, 100)}") — thường gặp với file >50 MB đi đường CDP; nén/render lại nhỏ hơn rồi đăng`);
|
|
2634
2713
|
}
|
|
2714
|
+
// Bảng cài đặt không tải → đóng composer, chạy lại cả lượt upload
|
|
2715
|
+
// TRONG PHIÊN (đường safe-retry sẵn có: bắt đầu lại từ trang chủ nên
|
|
2716
|
+
// hộp thoại cũ tự rụng). Chưa bấm Đăng nên không thể đúp. Lượt hai
|
|
2717
|
+
// vẫn kẹt thì ném ra, PublishRetry xếp phiên khác (luật settings_panel_stuck).
|
|
2718
|
+
if (waitResult.settingsStuck) {
|
|
2719
|
+
await page.keyboard.press('Escape').catch(() => {});
|
|
2720
|
+
throw safeComposerRetryError(`FB_SETTINGS_PANEL_STUCK: bảng cài đặt thước phim không tải sau ${Math.round(SETTINGS_STUCK_MS / 1000)}s (tooltip "đợi tải xong quyền riêng tư") — mở lại composer`);
|
|
2721
|
+
}
|
|
2635
2722
|
// Composer biến mất (URL nhảy sang /reel/… vì toast cướp click, hoặc
|
|
2636
2723
|
// FB tự đóng) → CHẠY LẠI cả lượt upload thay vì báo lỗi khó hiểu.
|
|
2637
2724
|
// An toàn vì chưa hề bấm Đăng ở nhánh này.
|
|
@@ -2782,6 +2869,29 @@ async function runOnce({ page, payload, log }) {
|
|
|
2782
2869
|
}, publishVerbs).catch(() => false);
|
|
2783
2870
|
|
|
2784
2871
|
for (let commitTry = 0; commitTry < 3 && (await composerStillOpen()); commitTry++) {
|
|
2872
|
+
// Trước khi bấm lại, ĐỌC composer đang nói gì (classifyComposerText):
|
|
2873
|
+
const state = classifyComposerText(await readComposerText(page));
|
|
2874
|
+
if (state.rightsCheck) {
|
|
2875
|
+
// Dòng đỏ "Không hoàn tất được quy trình kiểm tra bản quyền" — FB không
|
|
2876
|
+
// commit, bấm nữa cũng vậy. Báo đúng tên để PublishRetry xếp lại sau.
|
|
2877
|
+
await dumpFailure(page, 'rights-check-incomplete', log);
|
|
2878
|
+
throw new Error('FB_RIGHTS_CHECK_INCOMPLETE: Facebook không hoàn tất được quy trình kiểm tra bản quyền — video CHƯA được đăng, ~20 phút sau đăng lại thường được');
|
|
2879
|
+
}
|
|
2880
|
+
if (state.publishing) {
|
|
2881
|
+
// "Đang đăng": Facebook ĐANG đăng — cấm bấm lại, cấm kết luận "chưa
|
|
2882
|
+
// đăng". Chờ composer tự đóng, tối đa PUBLISHING_WAIT_MS.
|
|
2883
|
+
log('info', `[fb-pw] composer báo "Đang đăng" — Facebook đang đăng, chờ tới ${Math.round(PUBLISHING_WAIT_MS / 1000)}s, KHÔNG bấm lại`);
|
|
2884
|
+
const until = Date.now() + PUBLISHING_WAIT_MS;
|
|
2885
|
+
let still = true;
|
|
2886
|
+
while (Date.now() < until) {
|
|
2887
|
+
await pause(page, 5000);
|
|
2888
|
+
if (!(await composerStillOpen())) { still = false; break; }
|
|
2889
|
+
if (!classifyComposerText(await readComposerText(page)).publishing) { still = false; break; }
|
|
2890
|
+
}
|
|
2891
|
+
if (!still) continue; // vòng for kiểm lại composerStillOpen: đóng rồi thì coi như đã commit
|
|
2892
|
+
await dumpFailure(page, 'publishing-timeout', log);
|
|
2893
|
+
throw new Error(`FB_PUBLISH_IN_PROGRESS: Facebook vẫn "Đang đăng" sau ${Math.round(PUBLISHING_WAIT_MS / 1000)}s — có thể bài ĐÃ lên, kiểm trên Facebook trước khi đăng lại`);
|
|
2894
|
+
}
|
|
2785
2895
|
log('warn', `[fb-pw] composer STILL open after publish click — re-clicking "Đăng" (retry ${commitTry + 1}/3)`);
|
|
2786
2896
|
const reClicked = await page.evaluate((verbs) => {
|
|
2787
2897
|
const dlgs = document.querySelectorAll("[role='dialog']");
|
|
@@ -3007,14 +3117,19 @@ async function runOnce({ page, payload, log }) {
|
|
|
3007
3117
|
// total=220 → 88 ID mới, cái đầu là reel của page lạ và đã bị ghi vào DB).
|
|
3008
3118
|
// Chỉ nhận khi tab Reels CỦA CHÍNH PAGE xác nhận có ID đó; không xác
|
|
3009
3119
|
// nhận được thì để làm ứng viên, lượt chờ 75s dưới đây kiểm lại.
|
|
3010
|
-
|
|
3120
|
+
// Bỏ qua mọi ID đã có trong ảnh nền (bài cũ của chính mình) — xem
|
|
3121
|
+
// networkIdVerdict. Lấy ID mới đầu tiên KHÔNG nằm trong ảnh nền.
|
|
3011
3122
|
const tabIds = reelsTabUrl ? await scrapeReelTileIds(page) : [];
|
|
3012
|
-
|
|
3123
|
+
const firstNew = trulyNew.find((id) => networkIdVerdict({ id, beforeIds: reelIdsBefore, tabIds }) !== 'stale') || null;
|
|
3124
|
+
const verdict = networkIdVerdict({ id: firstNew, beforeIds: reelIdsBefore, tabIds });
|
|
3125
|
+
if (verdict === 'confirmed') {
|
|
3013
3126
|
postUrl = `https://www.facebook.com/reel/${firstNew}/`;
|
|
3014
3127
|
log('info', `[fb-pw] post URL from network + tab Reels xác nhận: ${postUrl} (snapshotLen=${capturedReelIdsSnapshotLen} total=${capturedReelIds.length} new=${trulyNew.length})`);
|
|
3015
|
-
} else {
|
|
3128
|
+
} else if (verdict === 'candidate') {
|
|
3016
3129
|
networkCandidate = firstNew;
|
|
3017
3130
|
log('warn', `[fb-pw] gói mạng cho ID ${firstNew} nhưng tab Reels của page CHƯA xác nhận (${tabIds.length} tile) — giữ làm ứng viên, chờ kiểm lại`);
|
|
3131
|
+
} else {
|
|
3132
|
+
log('warn', `[fb-pw] gói mạng chỉ có ID đã nằm trong ảnh nền (${trulyNew.length} ID, toàn bài cũ của mình) — không nhận, chờ so trước/sau`);
|
|
3018
3133
|
}
|
|
3019
3134
|
}
|
|
3020
3135
|
}
|
|
@@ -3034,7 +3149,7 @@ async function runOnce({ page, payload, log }) {
|
|
|
3034
3149
|
if (fresh2) {
|
|
3035
3150
|
postUrl = fresh2.href.startsWith('http') ? fresh2.href : `https://www.facebook.com${fresh2.href}`;
|
|
3036
3151
|
log('info', `[fb-pw] post URL from reels_tab RETRY (id=${fresh2.id}, timestamp="${fresh2.timestamp}"): ${postUrl}`);
|
|
3037
|
-
} else if (networkCandidate && (await scrapeReelTileIds(page))
|
|
3152
|
+
} else if (networkCandidate && networkIdVerdict({ id: networkCandidate, beforeIds: reelIdsBefore, tabIds: await scrapeReelTileIds(page) }) === 'confirmed') {
|
|
3038
3153
|
postUrl = `https://www.facebook.com/reel/${networkCandidate}/`;
|
|
3039
3154
|
log('info', `[fb-pw] post URL: ứng viên gói mạng đã được tab Reels xác nhận sau lượt chờ: ${postUrl}`);
|
|
3040
3155
|
} else if (reelIdsBefore) {
|
|
@@ -3181,6 +3296,8 @@ module.exports.__testables = {
|
|
|
3181
3296
|
confirmPublishDialog,
|
|
3182
3297
|
diffNewReelIds,
|
|
3183
3298
|
pickReelsTabUrl,
|
|
3299
|
+
networkIdVerdict,
|
|
3300
|
+
classifyComposerText,
|
|
3184
3301
|
isComposerDialogSignature,
|
|
3185
3302
|
discoverReelsTabUrl,
|
|
3186
3303
|
scrapeReelTileIds,
|