channel-worker 2.5.76 → 2.5.78
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,41 @@ 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
|
+
//
|
|
52
|
+
// KHÔNG quét cả body rồi lấy match ĐẦU TIÊN: đo thật 2026-09-05 trên kênh
|
|
53
|
+
// "Đồ chơi trẻ em 01" ra đúng "1" — trong trang có sẵn chuỗi "… 1 người theo
|
|
54
|
+
// dõi" ở chỗ khác, nó chặn trước số thật của header. Thứ tự ưu tiên:
|
|
55
|
+
// 1. link /followers của chính header (nguồn chắc nhất)
|
|
56
|
+
// 2. nếu không có, quét phần ĐẦU trang (header nằm ở đây) và lấy số LỚN NHẤT
|
|
57
|
+
// Trả về cả danh sách ứng viên để log soi được khi Facebook đổi giao diện.
|
|
58
|
+
const FOLLOWER_RE = /([\d][\d.,]*\s*(?:N|Tr|K|M|B|nghìn|triệu|tỷ)?)\s*(?:người theo dõi|người follow|followers)/gi;
|
|
59
|
+
|
|
60
|
+
async function scrapeFollowers(page) {
|
|
61
|
+
return page.evaluate((reSrc) => {
|
|
62
|
+
const out = { picked: null, from: '', candidates: [] };
|
|
63
|
+
const mk = () => new RegExp(reSrc, 'gi');
|
|
64
|
+
|
|
65
|
+
// 1. Link "N người theo dõi" trong header — href chứa /followers.
|
|
66
|
+
for (const a of document.querySelectorAll("a[href*='followers']")) {
|
|
67
|
+
const t = (a.innerText || '').replace(/\s+/g, ' ').trim();
|
|
68
|
+
const m = mk().exec(t);
|
|
69
|
+
if (m) { out.picked = m[1].trim(); out.from = 'link'; out.candidates.push(t.slice(0, 60)); return out; }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 2. Phần đầu trang: header đứng trên cùng, nên cắt bớt đuôi để tránh vớ
|
|
73
|
+
// phải chuỗi trong danh sách gợi ý/bình luận phía dưới.
|
|
74
|
+
const head = (document.body?.innerText || '').slice(0, 3000);
|
|
75
|
+
let m; const re = mk();
|
|
76
|
+
while ((m = re.exec(head))) out.candidates.push(m[1].trim());
|
|
77
|
+
if (out.candidates.length) { out.from = 'head'; out.picked = out.candidates.slice(); }
|
|
78
|
+
return out;
|
|
79
|
+
}, FOLLOWER_RE.source).catch(() => ({ picked: null, from: 'error', candidates: [] }));
|
|
80
|
+
}
|
|
81
|
+
|
|
45
82
|
async function scrapeTiles(page) {
|
|
46
83
|
return page.evaluate(() => {
|
|
47
84
|
const out = new Map();
|
|
@@ -67,8 +104,11 @@ async function scrapeTiles(page) {
|
|
|
67
104
|
}
|
|
68
105
|
|
|
69
106
|
async function run({ page, payload, log }) {
|
|
70
|
-
const { channel_id, page_url } = payload || {};
|
|
107
|
+
const { channel_id, page_url, mode = 'deep' } = payload || {};
|
|
71
108
|
if (!channel_id) throw new Error('payload.channel_id required');
|
|
109
|
+
const scrolls = Number.isFinite(Number(payload?.scroll)) && Number(payload.scroll) > 0
|
|
110
|
+
? Number(payload.scroll)
|
|
111
|
+
: (mode === 'fresh' ? 2 : 6);
|
|
72
112
|
let tabUrl = null;
|
|
73
113
|
if (page_url && /sk=reels_tab/.test(page_url)) tabUrl = page_url;
|
|
74
114
|
if (!tabUrl) {
|
|
@@ -77,19 +117,34 @@ async function run({ page, payload, log }) {
|
|
|
77
117
|
tabUrl = await findReelsTabHref(page);
|
|
78
118
|
}
|
|
79
119
|
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)}`);
|
|
120
|
+
log('info', `[fb-stats] ${mode} · reels_tab: ${tabUrl.slice(0, 100)}`);
|
|
81
121
|
await page.goto(tabUrl, { waitUntil: 'domcontentloaded', timeout: 45_000 });
|
|
82
122
|
await page.waitForTimeout(5000);
|
|
83
|
-
|
|
84
|
-
//
|
|
123
|
+
|
|
124
|
+
// 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
|
|
125
|
+
// gần như không đổi trong ngày. Đọc hỏng thì bỏ qua — KHÔNG được làm hỏng
|
|
126
|
+
// lượt đọc view, hai việc độc lập nhau.
|
|
127
|
+
let followers = null;
|
|
128
|
+
if (mode === 'deep') {
|
|
129
|
+
const f = await scrapeFollowers(page);
|
|
130
|
+
// Từ link header thì tin thẳng; quét phần đầu trang thì lấy số LỚN NHẤT
|
|
131
|
+
// trong các ứng viên — chuỗi nhiễu ("1 người theo dõi") luôn thua số thật.
|
|
132
|
+
const nums = (Array.isArray(f.picked) ? f.picked : [f.picked]).map(parseViews).filter((n) => Number.isFinite(n) && n > 0);
|
|
133
|
+
followers = nums.length ? Math.max(...nums) : null;
|
|
134
|
+
log('info', `[fb-stats] follower: ${followers ?? 'không đọc được'} (nguồn ${f.from || 'none'}, ứng viên: ${JSON.stringify(f.candidates).slice(0, 200)})`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Cuộn để tải thêm tile (mỗi lần ~12 reel). 6 lần ≈ 60-80 reel gần nhất — đủ
|
|
138
|
+
// 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
139
|
let last = 0;
|
|
86
|
-
for (let i = 0; i <
|
|
140
|
+
for (let i = 0; i < scrolls; i++) {
|
|
87
141
|
await page.mouse.wheel(0, 2400).catch(() => {});
|
|
88
142
|
await page.waitForTimeout(1800);
|
|
89
143
|
const n = await page.evaluate(() => document.querySelectorAll("a[href*='/reel/']").length).catch(() => 0);
|
|
90
144
|
if (n === last) break;
|
|
91
145
|
last = n;
|
|
92
146
|
}
|
|
147
|
+
const scannedAt = new Date().toISOString();
|
|
93
148
|
const tiles = await scrapeTiles(page);
|
|
94
149
|
const items = tiles.map((t) => ({ ...t, views: parseViews(t.views_text) })).filter((t) => t.views != null);
|
|
95
150
|
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 +155,11 @@ async function run({ page, payload, log }) {
|
|
|
100
155
|
if (typeof log.apiRequest !== 'function') throw new Error('log.apiRequest không có — cần channel-worker ≥ 2.5.55');
|
|
101
156
|
// api-client.request trả thẳng `data` (ném lỗi khi success=false).
|
|
102
157
|
const d = await log.apiRequest('POST', '/analytics/post-stats', {
|
|
103
|
-
channel_id, platform: 'facebook',
|
|
158
|
+
channel_id, platform: 'facebook', mode, at: scannedAt, followers,
|
|
104
159
|
items: items.map(({ post_id, views, views_text }) => ({ post_id, views, views_text })),
|
|
105
160
|
});
|
|
106
|
-
log('info', `[fb-stats] đã ghi ${d?.written}
|
|
107
|
-
return { ok: true, tiles: tiles.length, written: d?.written || 0, matched: d?.matched || 0 };
|
|
161
|
+
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}`);
|
|
162
|
+
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
163
|
}
|
|
109
164
|
|
|
110
|
-
module.exports = { run, __testables: { parseViews } };
|
|
165
|
+
module.exports = { run, __testables: { parseViews, scrapeFollowers } };
|