channel-worker 2.5.75 → 2.5.77
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
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
// Đọc LƯỢT XEM từng reel trên tab Reels của page (profile.php?id=…&sk=reels_tab)
|
|
2
|
-
// rồi gửi về API /analytics/post-stats.
|
|
3
|
-
// từ KOL nào có view để tự chỉnh trọng số. 1 lượt/kênh/ngày, chỉ đọc — không
|
|
4
|
-
// bấm gì ngoài cuộn trang.
|
|
2
|
+
// rồi gửi về API /analytics/post-stats. Chỉ đọc — không bấm gì ngoài cuộn trang.
|
|
5
3
|
//
|
|
6
|
-
// Payload: { channel_id, profile_id, page_url
|
|
4
|
+
// Payload: { channel_id, profile_id, page_url?, mode, scroll }
|
|
7
5
|
// page_url: link page (nếu có) — không có thì tự tìm reels_tab từ trang chủ
|
|
8
|
-
//
|
|
6
|
+
// như upload_facebook (anchor /profile.php?id=… hoặc "Dòng thời gian của X").
|
|
7
|
+
// mode: 'deep' (mặc định) = lượt nền 06:30, cuộn sâu cả page + đọc follower
|
|
8
|
+
// 'fresh' = lượt bám bài (giờ đăng +2h/+6h), cuộn nông, bài
|
|
9
|
+
// mới nằm ngay đầu tab nên không cần cuộn xa
|
|
10
|
+
// scroll: số lần cuộn, ghi đè mặc định theo mode (6 / 2)
|
|
9
11
|
//
|
|
10
12
|
// Tile reels_tab: <a href="/reel/<id>/" aria-label="Bản xem trước ô thước phim">
|
|
11
13
|
// chứa số lượt xem dạng rút gọn tiếng Việt: "1,2 N" (nghìn), "3,4 Tr" (triệu),
|
|
@@ -42,6 +44,19 @@ async function findReelsTabHref(page) {
|
|
|
42
44
|
}).catch(() => null);
|
|
43
45
|
}
|
|
44
46
|
|
|
47
|
+
// Số người theo dõi của page, đọc ngay trên chính trang đang đứng (header hiện
|
|
48
|
+
// ở mọi tab của page/profile) — không tốn thêm lượt mở profile nào. Ô
|
|
49
|
+
// platform_accounts.stats.followers có sẵn từ lâu nhưng trống 279/279 kênh vì
|
|
50
|
+
// chưa từng có ai ghi vào.
|
|
51
|
+
async function scrapeFollowers(page) {
|
|
52
|
+
return page.evaluate(() => {
|
|
53
|
+
const txt = document.body?.innerText || '';
|
|
54
|
+
// "1,2 N người theo dõi" · "12.345 người theo dõi" · "1.2K followers"
|
|
55
|
+
const m = /([\d][\d.,]*\s*(?:N|Tr|K|M|B|nghìn|triệu|tỷ)?)\s*(?:người theo dõi|followers)/i.exec(txt);
|
|
56
|
+
return m ? m[1].trim() : null;
|
|
57
|
+
}).catch(() => null);
|
|
58
|
+
}
|
|
59
|
+
|
|
45
60
|
async function scrapeTiles(page) {
|
|
46
61
|
return page.evaluate(() => {
|
|
47
62
|
const out = new Map();
|
|
@@ -67,8 +82,11 @@ async function scrapeTiles(page) {
|
|
|
67
82
|
}
|
|
68
83
|
|
|
69
84
|
async function run({ page, payload, log }) {
|
|
70
|
-
const { channel_id, page_url } = payload || {};
|
|
85
|
+
const { channel_id, page_url, mode = 'deep' } = payload || {};
|
|
71
86
|
if (!channel_id) throw new Error('payload.channel_id required');
|
|
87
|
+
const scrolls = Number.isFinite(Number(payload?.scroll)) && Number(payload.scroll) > 0
|
|
88
|
+
? Number(payload.scroll)
|
|
89
|
+
: (mode === 'fresh' ? 2 : 6);
|
|
72
90
|
let tabUrl = null;
|
|
73
91
|
if (page_url && /sk=reels_tab/.test(page_url)) tabUrl = page_url;
|
|
74
92
|
if (!tabUrl) {
|
|
@@ -77,19 +95,31 @@ async function run({ page, payload, log }) {
|
|
|
77
95
|
tabUrl = await findReelsTabHref(page);
|
|
78
96
|
}
|
|
79
97
|
if (!tabUrl) throw new Error('không tìm được link tab Reels của page');
|
|
80
|
-
log('info', `[fb-stats] reels_tab: ${tabUrl.slice(0, 100)}`);
|
|
98
|
+
log('info', `[fb-stats] ${mode} · reels_tab: ${tabUrl.slice(0, 100)}`);
|
|
81
99
|
await page.goto(tabUrl, { waitUntil: 'domcontentloaded', timeout: 45_000 });
|
|
82
100
|
await page.waitForTimeout(5000);
|
|
83
|
-
|
|
84
|
-
//
|
|
101
|
+
|
|
102
|
+
// Follower chỉ đọc ở lượt nền: lượt bám bài chạy 2-3 lần/ngày mà con số này
|
|
103
|
+
// gần như không đổi trong ngày. Đọc hỏng thì bỏ qua — KHÔNG được làm hỏng
|
|
104
|
+
// lượt đọc view, hai việc độc lập nhau.
|
|
105
|
+
let followers = null;
|
|
106
|
+
if (mode === 'deep') {
|
|
107
|
+
const raw = await scrapeFollowers(page);
|
|
108
|
+
followers = parseViews(raw);
|
|
109
|
+
log('info', raw ? `[fb-stats] follower: "${raw}" → ${followers}` : '[fb-stats] không đọc được số người theo dõi');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Cuộn để tải thêm tile (mỗi lần ~12 reel). 6 lần ≈ 60-80 reel gần nhất — đủ
|
|
113
|
+
// cho cửa sổ 28 ngày của bộ chỉnh trọng số. Lượt bám bài chỉ cần 2.
|
|
85
114
|
let last = 0;
|
|
86
|
-
for (let i = 0; i <
|
|
115
|
+
for (let i = 0; i < scrolls; i++) {
|
|
87
116
|
await page.mouse.wheel(0, 2400).catch(() => {});
|
|
88
117
|
await page.waitForTimeout(1800);
|
|
89
118
|
const n = await page.evaluate(() => document.querySelectorAll("a[href*='/reel/']").length).catch(() => 0);
|
|
90
119
|
if (n === last) break;
|
|
91
120
|
last = n;
|
|
92
121
|
}
|
|
122
|
+
const scannedAt = new Date().toISOString();
|
|
93
123
|
const tiles = await scrapeTiles(page);
|
|
94
124
|
const items = tiles.map((t) => ({ ...t, views: parseViews(t.views_text) })).filter((t) => t.views != null);
|
|
95
125
|
log('info', `[fb-stats] ${tiles.length} tile, đọc được lượt xem ${items.length}` + (items[0] ? ` (vd ${items[0].post_id} "${items[0].views_text}" → ${items[0].views})` : ''));
|
|
@@ -100,11 +130,11 @@ async function run({ page, payload, log }) {
|
|
|
100
130
|
if (typeof log.apiRequest !== 'function') throw new Error('log.apiRequest không có — cần channel-worker ≥ 2.5.55');
|
|
101
131
|
// api-client.request trả thẳng `data` (ném lỗi khi success=false).
|
|
102
132
|
const d = await log.apiRequest('POST', '/analytics/post-stats', {
|
|
103
|
-
channel_id, platform: 'facebook',
|
|
133
|
+
channel_id, platform: 'facebook', mode, at: scannedAt, followers,
|
|
104
134
|
items: items.map(({ post_id, views, views_text }) => ({ post_id, views, views_text })),
|
|
105
135
|
});
|
|
106
|
-
log('info', `[fb-stats] đã ghi ${d?.written}
|
|
107
|
-
return { ok: true, tiles: tiles.length, written: d?.written || 0, matched: d?.matched || 0 };
|
|
136
|
+
log('info', `[fb-stats] đã ghi ${d?.written} điểm đo, khớp idea ${d?.matched}, đang lên ${d?.rising || 0}, chuông ${d?.alerts || 0}`);
|
|
137
|
+
return { ok: true, mode, tiles: tiles.length, written: d?.written || 0, matched: d?.matched || 0, rising: d?.rising || 0, alerts: d?.alerts || 0, followers };
|
|
108
138
|
}
|
|
109
139
|
|
|
110
|
-
module.exports = { run, __testables: { parseViews } };
|
|
140
|
+
module.exports = { run, __testables: { parseViews, scrapeFollowers } };
|
|
@@ -1750,13 +1750,24 @@ async function runOnce({ page, payload, log }) {
|
|
|
1750
1750
|
}
|
|
1751
1751
|
thumbApplied = true;
|
|
1752
1752
|
log('info', `[fb-pw] page-wall thumb — modal closed after save (retries=${retryCount})`);
|
|
1753
|
-
} else if (
|
|
1753
|
+
} else if (await isCustomThumbApplied(page)) {
|
|
1754
1754
|
// Hết giờ chờ đóng nhưng bằng chứng áp thumb còn đó — tuyệt
|
|
1755
|
-
// đối không ESC/Hủy (Hủy trên panel
|
|
1756
|
-
|
|
1755
|
+
// đối không ESC/Hủy (Hủy trên panel này là GỠ thumb).
|
|
1756
|
+
//
|
|
1757
|
+
// KHÔNG khoá theo biến thể nữa (2.5.76). Trước đây nhánh cứu
|
|
1758
|
+
// chỉ nhận `variant === 'nested'`, nên những lượt FB trả
|
|
1759
|
+
// `variant=modal` mà modal vẫn không đóng sau 60s rơi thẳng
|
|
1760
|
+
// xuống ESC+Hủy → gỡ mất ảnh bìa vừa dán → chết ở cổng
|
|
1761
|
+
// "Custom thumbnail required but not applied" (đo 04/09/2026,
|
|
1762
|
+
// 3 lượt liên tiếp, log ghi rõ variant=modal). Nút "Gỡ" là
|
|
1763
|
+
// bằng chứng vật lý thumb đang áp, biến thể nào cũng đúng.
|
|
1764
|
+
log('info', `[fb-pw] page-wall thumb — hết giờ chờ đóng nhưng nút "Gỡ hình thu nhỏ" đang hiện (variant=${thumbEditorVia}) → giữ panel, coi như dán xong`);
|
|
1757
1765
|
thumbApplied = true;
|
|
1758
1766
|
} else {
|
|
1759
1767
|
log('warn', '[fb-pw] page-wall thumb — modal still open 60s after Lưu click; trying ESC + click Hủy fallback');
|
|
1768
|
+
// Chụp lại trước khi ESC/Hủy: đây là nhánh phá lượt nhiều
|
|
1769
|
+
// nhất mà lại không có ảnh nào để soi (04/09/2026).
|
|
1770
|
+
await dumpFailure(page, 'thumb-modal-stuck-before-cancel', log);
|
|
1760
1771
|
await page.keyboard.press('Escape').catch(() => {});
|
|
1761
1772
|
await pause(page, 800);
|
|
1762
1773
|
const cancel = await firstVisible(page.locator("[__fbpw_thumb_dialog__='1'] div[role='button']:has-text('Hủy'), [__fbpw_thumb_dialog__='1'] button:has-text('Hủy')"), 2);
|