channel-worker 2.5.54 → 2.5.56
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/lib/api-client.js
CHANGED
|
@@ -68,7 +68,7 @@ class ApiClient {
|
|
|
68
68
|
async getNextCommand(workerId) {
|
|
69
69
|
// Daemon-handled types. `_pw` variants route to the Playwright pipeline
|
|
70
70
|
// (lib/playwright-runner → scripts/<base>.js) instead of the extension.
|
|
71
|
-
const workerTypes = 'launch_profile,close_profile,launch_veo3_profile,set_profile_proxy,save_file,set_thumbnail,set_tags,set_file_input,click_and_upload,type_text,verify_logins,update_extension,sync_youtube_stats,restart_worker,upload_youtube_pw,upload_tiktok_pw,upload_facebook_pw,upload_facebook_photo_pw,warmup_youtube_pw,warmup_facebook_pw,warmup_tiktok_pw,nurture_facebook_pw,scrape_affiliate_products,ingest_shopee_product';
|
|
71
|
+
const workerTypes = 'launch_profile,close_profile,launch_veo3_profile,set_profile_proxy,save_file,set_thumbnail,set_tags,set_file_input,click_and_upload,type_text,verify_logins,update_extension,sync_youtube_stats,restart_worker,upload_youtube_pw,upload_tiktok_pw,upload_facebook_pw,upload_facebook_photo_pw,warmup_youtube_pw,warmup_facebook_pw,warmup_tiktok_pw,nurture_facebook_pw,fetch_facebook_reel_stats_pw,scrape_affiliate_products,ingest_shopee_product';
|
|
72
72
|
return this.request('GET', `/workers/commands?worker_id=${workerId}&types=${encodeURIComponent(workerTypes)}`);
|
|
73
73
|
}
|
|
74
74
|
|
package/lib/command-poller.js
CHANGED
|
@@ -289,6 +289,9 @@ class CommandPoller {
|
|
|
289
289
|
// Publish scripts attach screenshots to warning logs through this helper.
|
|
290
290
|
// Keep the command/job identity here so the API and idea Publish History
|
|
291
291
|
// can associate an artifact with the exact platform attempt.
|
|
292
|
+
// Script chỉ-đọc (fetch_facebook_reel_stats) gửi số liệu về API bằng token
|
|
293
|
+
// của chính worker — không cần biết base URL hay token.
|
|
294
|
+
log.apiRequest = (method, path, body) => this.api.request(method, path, body);
|
|
292
295
|
log.uploadDebugScreenshot = (filePath, meta = {}) => this.api.uploadDebugScreenshot({
|
|
293
296
|
filePath,
|
|
294
297
|
tag: meta.tag || 'failure',
|
package/package.json
CHANGED
|
@@ -0,0 +1,110 @@
|
|
|
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.
|
|
5
|
+
//
|
|
6
|
+
// Payload: { channel_id, profile_id, page_url? }
|
|
7
|
+
// 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").
|
|
9
|
+
//
|
|
10
|
+
// Tile reels_tab: <a href="/reel/<id>/" aria-label="Bản xem trước ô thước phim">
|
|
11
|
+
// 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),
|
|
12
|
+
// hoặc số thường "856". Giao diện tiếng Anh: "1.2K", "3.4M".
|
|
13
|
+
|
|
14
|
+
function parseViews(txt) {
|
|
15
|
+
const t = String(txt || '').replace(/\s+/g, ' ').trim();
|
|
16
|
+
// Lấy cụm số + hậu tố đầu tiên. Dấu phẩy là thập phân ở VN ("1,2 N"), dấu
|
|
17
|
+
// chấm là nghìn ("1.234") — phân biệt bằng hậu tố: có N/Tr/K/M thì là thập phân.
|
|
18
|
+
const m = /(\d+(?:[.,]\d+)?)\s*(N|Tr|K|M|B|tỷ|nghìn|triệu)?\b/i.exec(t);
|
|
19
|
+
if (!m) return null;
|
|
20
|
+
let num = m[1]; const suf = (m[2] || '').toLowerCase();
|
|
21
|
+
if (suf) num = parseFloat(num.replace(',', '.'));
|
|
22
|
+
else num = parseInt(num.replace(/[.,]/g, ''), 10);
|
|
23
|
+
if (!Number.isFinite(num)) return null;
|
|
24
|
+
const mul = { n: 1e3, k: 1e3, 'nghìn': 1e3, tr: 1e6, m: 1e6, 'triệu': 1e6, b: 1e9, 'tỷ': 1e9 }[suf] || 1;
|
|
25
|
+
return Math.round(num * mul);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function findReelsTabHref(page) {
|
|
29
|
+
return page.evaluate(() => {
|
|
30
|
+
const direct = document.querySelectorAll("a[href*='sk=reels_tab']");
|
|
31
|
+
for (const a of direct) { const h = a.getAttribute('href') || ''; if (h) return h.startsWith('http') ? h : `https://www.facebook.com${h}`; }
|
|
32
|
+
for (const a of document.querySelectorAll("a[href*='/profile.php?id=']")) {
|
|
33
|
+
const m = (a.getAttribute('href') || '').match(/\/profile\.php\?id=(\d+)/);
|
|
34
|
+
if (m) return `https://www.facebook.com/profile.php?id=${m[1]}&sk=reels_tab`;
|
|
35
|
+
}
|
|
36
|
+
for (const a of document.querySelectorAll("a[aria-label*='Dòng thời gian'], a[aria-label*='Timeline']")) {
|
|
37
|
+
const h = a.getAttribute('href') || '';
|
|
38
|
+
const m = h.match(/^\/([A-Za-z0-9._-]{3,})\/?(?:\?|$)/);
|
|
39
|
+
if (m && !['profile.php', 'reel', 'video', 'videos'].includes(m[1])) return `https://www.facebook.com/${m[1]}/?sk=reels_tab`;
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}).catch(() => null);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function scrapeTiles(page) {
|
|
46
|
+
return page.evaluate(() => {
|
|
47
|
+
const out = new Map();
|
|
48
|
+
for (const a of document.querySelectorAll("a[href*='/reel/']")) {
|
|
49
|
+
const href = a.getAttribute('href') || '';
|
|
50
|
+
const m = href.match(/\/reel\/(\d{8,20})/);
|
|
51
|
+
if (!m) continue;
|
|
52
|
+
const r = a.getBoundingClientRect();
|
|
53
|
+
if (r.width < 8 || r.height < 8) continue;
|
|
54
|
+
const aria = (a.getAttribute('aria-label') || '').toLowerCase();
|
|
55
|
+
const isTile = /bản xem trước ô thước phim|reel tile preview|reel preview/.test(aria);
|
|
56
|
+
// Số lượt xem nằm trong tile (icon play + số). Lấy text ngắn nhất có số
|
|
57
|
+
// trong subtree để tránh bắt nhầm caption.
|
|
58
|
+
const texts = [];
|
|
59
|
+
const walker = document.createTreeWalker(a, NodeFilter.SHOW_TEXT);
|
|
60
|
+
let n;
|
|
61
|
+
while ((n = walker.nextNode())) { const t = n.textContent.trim(); if (t && /\d/.test(t) && t.length <= 16) texts.push(t); }
|
|
62
|
+
const pick = texts.sort((x, y) => x.length - y.length)[0] || '';
|
|
63
|
+
if (!out.has(m[1]) || (isTile && pick)) out.set(m[1], { post_id: m[1], views_text: pick, is_tile: isTile });
|
|
64
|
+
}
|
|
65
|
+
return [...out.values()];
|
|
66
|
+
}).catch(() => []);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function run({ page, payload, log }) {
|
|
70
|
+
const { channel_id, page_url } = payload || {};
|
|
71
|
+
if (!channel_id) throw new Error('payload.channel_id required');
|
|
72
|
+
let tabUrl = null;
|
|
73
|
+
if (page_url && /sk=reels_tab/.test(page_url)) tabUrl = page_url;
|
|
74
|
+
if (!tabUrl) {
|
|
75
|
+
await page.goto(page_url || 'https://www.facebook.com/', { waitUntil: 'domcontentloaded', timeout: 45_000 });
|
|
76
|
+
await page.waitForTimeout(4000);
|
|
77
|
+
tabUrl = await findReelsTabHref(page);
|
|
78
|
+
}
|
|
79
|
+
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)}`);
|
|
81
|
+
await page.goto(tabUrl, { waitUntil: 'domcontentloaded', timeout: 45_000 });
|
|
82
|
+
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ố.
|
|
85
|
+
let last = 0;
|
|
86
|
+
for (let i = 0; i < 6; i++) {
|
|
87
|
+
await page.mouse.wheel(0, 2400).catch(() => {});
|
|
88
|
+
await page.waitForTimeout(1800);
|
|
89
|
+
const n = await page.evaluate(() => document.querySelectorAll("a[href*='/reel/']").length).catch(() => 0);
|
|
90
|
+
if (n === last) break;
|
|
91
|
+
last = n;
|
|
92
|
+
}
|
|
93
|
+
const tiles = await scrapeTiles(page);
|
|
94
|
+
const items = tiles.map((t) => ({ ...t, views: parseViews(t.views_text) })).filter((t) => t.views != null);
|
|
95
|
+
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})` : ''));
|
|
96
|
+
if (!items.length) {
|
|
97
|
+
throw new Error(`không đọc được lượt xem trên tile nào (${tiles.length} tile) — FB đổi giao diện?`);
|
|
98
|
+
}
|
|
99
|
+
// Gửi về API bằng token của worker (command-poller gắn log.apiRequest).
|
|
100
|
+
if (typeof log.apiRequest !== 'function') throw new Error('log.apiRequest không có — cần channel-worker ≥ 2.5.55');
|
|
101
|
+
// api-client.request trả thẳng `data` (ném lỗi khi success=false).
|
|
102
|
+
const d = await log.apiRequest('POST', '/analytics/post-stats', {
|
|
103
|
+
channel_id, platform: 'facebook',
|
|
104
|
+
items: items.map(({ post_id, views, views_text }) => ({ post_id, views, views_text })),
|
|
105
|
+
});
|
|
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 };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
module.exports = { run, __testables: { parseViews } };
|