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,6 +1,6 @@
1
1
  {
2
2
  "name": "channel-worker",
3
- "version": "2.5.76",
3
+ "version": "2.5.77",
4
4
  "description": "Channel Manager worker daemon — runs on remote machines to execute video pipeline jobs",
5
5
  "main": "lib/daemon.js",
6
6
  "bin": {
@@ -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. Giai đoạn 2 của bộ nhặt KOL: biết bài
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 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
- // như upload_facebook (anchor /profile.php?id=… hoặc "Dòng thời gian của X").
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
- // Cuộn vài lần để tải thêm tile (mỗi lần ~12 reel). 6 lần ≈ 60-80 reel gần
84
- // nhất đủ cho cửa sổ 28 ngày của bộ chỉnh trọng số.
101
+
102
+ // Follower chỉ đọc lượt nền: lượt bám bài chạy 2-3 lần/ngày 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 < 6; 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} bài, khớp idea ${d?.matched}`);
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 } };