channel-worker 2.5.76 → 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 } };
|