channel-worker 2.5.97 → 2.5.99
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -421,18 +421,71 @@ async function readAudiencePage(page, log) {
|
|
|
421
421
|
return a;
|
|
422
422
|
}
|
|
423
423
|
|
|
424
|
+
// Trang Insights hỏng thì chụp lại — script này trước 10/09 không dump gì nên
|
|
425
|
+
// 8 kênh kẹt chip "Hôm nay" hai ngày liền mà không có lấy một tấm ảnh để hiểu.
|
|
426
|
+
// Ngoài ảnh, liệt kê mọi chữ có thể là nhãn khoảng thời gian kèm vị trí: để
|
|
427
|
+
// biết chip thật đang ghi gì, và có cái "Hôm nay" lạ nào đứng TRƯỚC chip trong
|
|
428
|
+
// DOM (currentRangeLabel lấy phần tử khớp đầu tiên) hay không.
|
|
429
|
+
async function dumpInsightsFailure(page, tag, log) {
|
|
430
|
+
try {
|
|
431
|
+
const os = require('os'); const fs = require('fs'); const path = require('path');
|
|
432
|
+
const dir = path.join(os.tmpdir(), 'cm-worker-pw');
|
|
433
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
434
|
+
const png = path.join(dir, `fb-${tag}-${Date.now()}.png`);
|
|
435
|
+
await page.screenshot({ path: png, fullPage: false, timeout: 8000 }).catch(() => {});
|
|
436
|
+
const labels = await page.evaluate(() => {
|
|
437
|
+
const clean = (v) => String(v || '').replace(/\s+/g, ' ').trim();
|
|
438
|
+
const out = [];
|
|
439
|
+
for (const e of document.querySelectorAll('div[role="button"],div,span')) {
|
|
440
|
+
const t = clean(e.innerText);
|
|
441
|
+
if (!t || t.length >= 70) continue;
|
|
442
|
+
if (!/(Hôm nay|Today|Tùy chỉnh|Custom|ngày qua|Last \d+ days|Năm nay|This year)/i.test(t)) continue;
|
|
443
|
+
const r = e.getBoundingClientRect();
|
|
444
|
+
out.push(`${e.tagName.toLowerCase()}[${e.getAttribute('role') || ''}] y=${Math.round(r.y)} w=${Math.round(r.width)} "${t}"`);
|
|
445
|
+
if (out.length >= 25) break;
|
|
446
|
+
}
|
|
447
|
+
return out;
|
|
448
|
+
}).catch(() => []);
|
|
449
|
+
labels.forEach((l, i) => log('info', ` ${tag} label[${i}] ${l}`));
|
|
450
|
+
let shot = null;
|
|
451
|
+
if (fs.existsSync(png) && typeof log.uploadDebugScreenshot === 'function') {
|
|
452
|
+
try { shot = await log.uploadDebugScreenshot(png, { tag }); } catch (e) { log('warn', `[insights] tải ảnh dump hỏng: ${e.message}`); }
|
|
453
|
+
}
|
|
454
|
+
log('warn', `[insights] dump ${tag} — url=${page.url()} screenshot=${shot?.signed_url || png}`,
|
|
455
|
+
shot ? { debug_screenshot: shot } : { debug_screenshot_local_path: png });
|
|
456
|
+
} catch {}
|
|
457
|
+
}
|
|
458
|
+
|
|
424
459
|
// Lượt nền: hai trang view + một trang đối tượng, rồi gửi thẳng số thô về API.
|
|
425
460
|
// Việc khớp bài với idea và tính hiệu để lòi ra "bài cũ đang nổ" làm ở API —
|
|
426
461
|
// worker không giữ trạng thái ngày hôm qua.
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
462
|
+
//
|
|
463
|
+
// THỨ TỰ (đổi 10/09): NGÀY HÔM QUA → FOLLOWER → 7 NGÀY, và "7 ngày" hỏng
|
|
464
|
+
// KHÔNG chết cả lượt. Bản cũ đọc "7 ngày qua" trước (làm ấm trang) và ném lỗi
|
|
465
|
+
// khi chip không đổi — mà trang đó chỉ để lấy danh sách bài. Đo 10/09: 8/63
|
|
466
|
+
// kênh (09/09: 11/61) kẹt chip "Hôm nay" đúng ở trang này, trong khi trang
|
|
467
|
+
// ngày hôm qua + follower của CHÍNH các kênh đó vẫn đọc được (log win-worker:
|
|
468
|
+
// chip "Tùy chỉnh: 9 Tháng 9" ra đều). Một chip kẹt làm mất view hôm qua +
|
|
469
|
+
// follower — hai số quan trọng nhất — cho cả ngày, /analytics báo "Chưa quét".
|
|
470
|
+
// Ngày hôm qua tự có vòng nạp lại 3 lần nên không cần trang khác làm ấm.
|
|
471
|
+
const DEFAULT_READERS = { readOneDay, readAudiencePage, readViewsPage, dump: dumpInsightsFailure };
|
|
472
|
+
async function runInsights({ page, payload, log }, R = DEFAULT_READERS) {
|
|
431
473
|
const yIso = vnYesterdayIso();
|
|
432
|
-
const yest = await readOneDay(page, yIso, log);
|
|
433
|
-
const audience = await readAudiencePage(page, log);
|
|
474
|
+
const yest = await R.readOneDay(page, yIso, log);
|
|
475
|
+
const audience = await R.readAudiencePage(page, log);
|
|
476
|
+
let week = { range: 'LAST_7D', label: '', total: null, pct: null, posts: [] };
|
|
477
|
+
let weekError = null;
|
|
478
|
+
try {
|
|
479
|
+
week = await R.readViewsPage(page, 'LAST_7D', log);
|
|
480
|
+
} catch (e) {
|
|
481
|
+
weekError = String((e && e.message) || e);
|
|
482
|
+
log('warn', `[insights] 7 ngày qua hỏng (${weekError}) — vẫn ghi ngày hôm qua + follower, chỉ mất danh sách bài`);
|
|
483
|
+
await R.dump(page, 'insights-week', log);
|
|
484
|
+
}
|
|
434
485
|
if (yest.views == null && week.total == null) {
|
|
435
|
-
throw new Error(
|
|
486
|
+
throw new Error(weekError
|
|
487
|
+
? `không đọc được cả ngày hôm qua lẫn 7 ngày — ${weekError}`
|
|
488
|
+
: 'không đọc được tổng lượt xem trên trang Insights — FB đổi giao diện?');
|
|
436
489
|
}
|
|
437
490
|
if (typeof log.apiRequest !== 'function') throw new Error('log.apiRequest không có — cần channel-worker ≥ 2.5.55');
|
|
438
491
|
const d = await log.apiRequest('POST', '/analytics/fb-insights', {
|
|
@@ -448,6 +501,7 @@ async function runInsights({ page, payload, log }) {
|
|
|
448
501
|
views_yesterday: yest.views, views_7d: week.total,
|
|
449
502
|
posts: week.posts.length, matched: d?.matched ?? null,
|
|
450
503
|
followers: audience.followers, unattributed: d?.unattributed ?? null,
|
|
504
|
+
...(weekError ? { week_error: weekError } : {}),
|
|
451
505
|
};
|
|
452
506
|
}
|
|
453
507
|
|
|
@@ -511,4 +565,4 @@ async function run({ page, payload, log }) {
|
|
|
511
565
|
return { ok: true, mode, tiles: tiles.length, written: d?.written || 0, matched: d?.matched || 0, rising: d?.rising || 0, alerts: d?.alerts || 0, followers };
|
|
512
566
|
}
|
|
513
567
|
|
|
514
|
-
module.exports = { run, __testables: { parseViews, scrapeFollowers, parseExact, parseVnPostTime } };
|
|
568
|
+
module.exports = { run, __testables: { parseViews, scrapeFollowers, parseExact, parseVnPostTime, runInsights, dumpInsightsFailure } };
|
|
@@ -700,7 +700,10 @@ async function discoverReelsTabUrl(page, log = null) {
|
|
|
700
700
|
for (const a of document.querySelectorAll('a[href]')) {
|
|
701
701
|
const href = a.getAttribute('href') || '';
|
|
702
702
|
if (!href || href.startsWith('#') || href.startsWith('javascript:')) continue;
|
|
703
|
-
|
|
703
|
+
// Anchor nằm trong bảng tin / một bài viết → của page KHÁC, không phải
|
|
704
|
+
// của mình (xem ghi chú ở pickReelsTabUrl).
|
|
705
|
+
const inFeed = !!a.closest("[role='feed'], [role='article'], [aria-posinset]");
|
|
706
|
+
out.push({ href: href.slice(0, 200), aria: (a.getAttribute('aria-label') || '').slice(0, 80), inFeed });
|
|
704
707
|
if (out.length >= 400) break;
|
|
705
708
|
}
|
|
706
709
|
return out;
|
|
@@ -754,7 +757,17 @@ function isComposerDialogSignature(text) {
|
|
|
754
757
|
// 2/2 lượt "không tìm được link tab Reels trước khi đăng". Trang chủ của Page
|
|
755
758
|
// có shortcut bên trái trỏ /<id số>/ hoặc /<handle>/, và đôi khi /<id>/reels/.
|
|
756
759
|
function pickReelsTabUrl(anchors) {
|
|
757
|
-
const list = (anchors || [])
|
|
760
|
+
const list = (anchors || [])
|
|
761
|
+
.map((a) => ({ href: String(a.href || ''), aria: String(a.aria || ''), inFeed: !!a.inFeed }))
|
|
762
|
+
// ⛔ LOẠI anchor CỦA BÀI TRONG BẢNG TIN — đây là gốc của 2 link sai sáng
|
|
763
|
+
// 10/09 (kênh "kol du lịch 2", reel 1712630710026375 và 1549470359802337
|
|
764
|
+
// hoá ra là bài của PAGE LẠ). Trang chủ đầy bài của page khác; mỗi bài có
|
|
765
|
+
// link /profile.php?id=<page lạ>&__cft__[0]=… nên chiến lược 2 bốc trúng
|
|
766
|
+
// tab Reels của người ta, rồi "so trước/sau" so hai page khác nhau (log:
|
|
767
|
+
// "trước 10, sau 10, mới 10 — nhiều hơn 1, không nhận") và nhánh gói mạng
|
|
768
|
+
// chốt bằng reel gợi ý trên feed. Dấu nhận biết: tham số __cft__/__tn__
|
|
769
|
+
// (tracking của một bài cụ thể) hoặc anchor nằm trong vùng feed/article.
|
|
770
|
+
.filter((a) => !a.inFeed && !/[?&]__cft__|[?&]__tn__/.test(a.href));
|
|
758
771
|
const abs = (h) => (h.startsWith('http') ? h : `https://www.facebook.com${h}`);
|
|
759
772
|
for (const a of list) if (/sk=reels_tab/.test(a.href)) return abs(a.href);
|
|
760
773
|
for (const a of list) {
|
|
@@ -2845,6 +2858,7 @@ async function runOnce({ page, payload, log }) {
|
|
|
2845
2858
|
return null;
|
|
2846
2859
|
}).catch(() => null);
|
|
2847
2860
|
let reelsTabUrl = null; // giữ lại cho lượt retry (a.2b)
|
|
2861
|
+
let networkCandidate = null; // ID gói mạng chưa được tab Reels xác nhận
|
|
2848
2862
|
try {
|
|
2849
2863
|
// Find a link to the page's reels tab. Multi-strategy:
|
|
2850
2864
|
// 1. Any existing href containing "sk=reels_tab"
|
|
@@ -2987,9 +3001,21 @@ async function runOnce({ page, payload, log }) {
|
|
|
2987
3001
|
const preSet = new Set(capturedReelIds.slice(0, capturedReelIdsSnapshotLen));
|
|
2988
3002
|
const trulyNew = newIds.filter((id) => !preSet.has(id));
|
|
2989
3003
|
if (trulyNew.length) {
|
|
3004
|
+
// ⚠️ ID bắt từ gói mạng KHÔNG chứng minh reel thuộc page mình: đứng ở
|
|
3005
|
+
// trang chủ, feed tải liên tục reel GỢI Ý của page khác, nên "ID mới
|
|
3006
|
+
// đầu tiên sau mốc" có thể là reel người ta (sáng 10/09: snapshotLen=132,
|
|
3007
|
+
// total=220 → 88 ID mới, cái đầu là reel của page lạ và đã bị ghi vào DB).
|
|
3008
|
+
// Chỉ nhận khi tab Reels CỦA CHÍNH PAGE xác nhận có ID đó; không xác
|
|
3009
|
+
// nhận được thì để làm ứng viên, lượt chờ 75s dưới đây kiểm lại.
|
|
2990
3010
|
const firstNew = trulyNew[0];
|
|
2991
|
-
|
|
2992
|
-
|
|
3011
|
+
const tabIds = reelsTabUrl ? await scrapeReelTileIds(page) : [];
|
|
3012
|
+
if (tabIds.includes(firstNew)) {
|
|
3013
|
+
postUrl = `https://www.facebook.com/reel/${firstNew}/`;
|
|
3014
|
+
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 {
|
|
3016
|
+
networkCandidate = firstNew;
|
|
3017
|
+
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`);
|
|
3018
|
+
}
|
|
2993
3019
|
}
|
|
2994
3020
|
}
|
|
2995
3021
|
|
|
@@ -3008,6 +3034,9 @@ async function runOnce({ page, payload, log }) {
|
|
|
3008
3034
|
if (fresh2) {
|
|
3009
3035
|
postUrl = fresh2.href.startsWith('http') ? fresh2.href : `https://www.facebook.com${fresh2.href}`;
|
|
3010
3036
|
log('info', `[fb-pw] post URL from reels_tab RETRY (id=${fresh2.id}, timestamp="${fresh2.timestamp}"): ${postUrl}`);
|
|
3037
|
+
} else if (networkCandidate && (await scrapeReelTileIds(page)).includes(networkCandidate)) {
|
|
3038
|
+
postUrl = `https://www.facebook.com/reel/${networkCandidate}/`;
|
|
3039
|
+
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}`);
|
|
3011
3040
|
} else if (reelIdsBefore) {
|
|
3012
3041
|
const after2 = await scrapeReelTileIds(page);
|
|
3013
3042
|
const diff2 = diffNewReelIds(reelIdsBefore, after2);
|