channel-worker 2.5.81 → 2.5.88
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 +1 -1
- package/scripts/fetch_facebook_reel_stats.js +204 -43
package/package.json
CHANGED
|
@@ -130,31 +130,52 @@ async function scrapeTiles(page) {
|
|
|
130
130
|
// LAST_90D chạy thật. THIS_YEAR và LIFETIME thì trang LẶNG LẼ rơi về 28 ngày —
|
|
131
131
|
// không báo lỗi, nên phải đối chiếu nhãn khoảng thời gian trả về.
|
|
132
132
|
|
|
133
|
+
const vnYesterdayIso = () => new Date(Date.now() + 7 * 3600e3 - 864e5).toISOString().slice(0, 10);
|
|
134
|
+
|
|
133
135
|
const INSIGHTS_BASE = 'https://www.facebook.com/professional_dashboard/profile_insights';
|
|
134
136
|
|
|
135
137
|
// Số trên Insights là số đầy đủ, dấu chấm là phân cách nghìn ("1.333.852").
|
|
136
138
|
// KHÔNG dùng parseViews ở đây — hàm đó dịch "1.2" thành 12 vì phải lo hậu tố K/N.
|
|
139
|
+
// Số trên Insights luôn là số NGUYÊN, chỉ khác dấu ngăn nghìn theo ngôn ngữ
|
|
140
|
+
// của profile: tiếng Việt dùng dấu chấm ("1.333.852"), tiếng Anh dùng dấu phẩy
|
|
141
|
+
// ("312,314"). Bỏ CẢ HAI. Chỉ strip dấu chấm là profile tiếng Anh đọc "312,314"
|
|
142
|
+
// thành 312 — sai 1000 lần mà không có dấu hiệu gì (gặp thật 2026-09-06, 4 kênh).
|
|
137
143
|
function parseExact(txt) {
|
|
138
|
-
const m = /(\d[\d
|
|
144
|
+
const m = /(\d[\d.,]*)/.exec(String(txt || '').replace(/\s+/g, ' '));
|
|
139
145
|
if (!m) return null;
|
|
140
|
-
const n = parseInt(m[1].replace(
|
|
146
|
+
const n = parseInt(m[1].replace(/[.,]/g, ''), 10);
|
|
141
147
|
return Number.isFinite(n) ? n : null;
|
|
142
148
|
}
|
|
143
149
|
|
|
144
150
|
// "31 Tháng 8 11:41" → ISO. Facebook không ghi năm; tháng lớn hơn tháng hiện
|
|
145
151
|
// tại nghĩa là năm ngoái (bài cuối tháng 12 xem vào tháng 1).
|
|
152
|
+
const EN_MONTH = { jan: 1, feb: 2, mar: 3, apr: 4, may: 5, jun: 6, jul: 7, aug: 8, sep: 9, oct: 10, nov: 11, dec: 12 };
|
|
153
|
+
|
|
146
154
|
function parseVnPostTime(txt, now = new Date()) {
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
const
|
|
155
|
+
const t = String(txt || '');
|
|
156
|
+
let d; let mo; let h; let mi;
|
|
157
|
+
const vn = /(\d{1,2})\s*Tháng\s*(\d{1,2})\s+(\d{1,2}):(\d{2})/i.exec(t);
|
|
158
|
+
if (vn) {
|
|
159
|
+
[, d, mo, h, mi] = vn.map(Number);
|
|
160
|
+
} else {
|
|
161
|
+
// Profile tiếng Anh: "Fri Sep 4, 2:33pm" (12 giờ, có am/pm).
|
|
162
|
+
const en = /(?:[A-Za-z]{3},?\s+)?([A-Za-z]{3})\s+(\d{1,2}),?\s+(\d{1,2}):(\d{2})\s*(am|pm)/i.exec(t);
|
|
163
|
+
if (!en) return null;
|
|
164
|
+
mo = EN_MONTH[en[1].toLowerCase()];
|
|
165
|
+
if (!mo) return null;
|
|
166
|
+
d = Number(en[2]); h = Number(en[3]); mi = Number(en[4]);
|
|
167
|
+
const pm = en[5].toLowerCase() === 'pm';
|
|
168
|
+
if (pm && h < 12) h += 12;
|
|
169
|
+
if (!pm && h === 12) h = 0;
|
|
170
|
+
}
|
|
150
171
|
let year = now.getUTCFullYear();
|
|
151
172
|
// Mốc so sánh theo giờ VN vì Facebook hiện giờ địa phương của tài khoản.
|
|
152
173
|
const vnNow = new Date(now.getTime() + 7 * 3600e3);
|
|
153
174
|
if (mo > vnNow.getUTCMonth() + 1) year -= 1;
|
|
154
175
|
const iso = `${year}-${String(mo).padStart(2, '0')}-${String(d).padStart(2, '0')}`
|
|
155
176
|
+ `T${String(h).padStart(2, '0')}:${String(mi).padStart(2, '0')}:00+07:00`;
|
|
156
|
-
const
|
|
157
|
-
return Number.isNaN(
|
|
177
|
+
const parsed = new Date(iso);
|
|
178
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString();
|
|
158
179
|
}
|
|
159
180
|
|
|
160
181
|
// Nhãn khoảng thời gian PHẢI khớp cái đã xin. Trang dựng khung trước rồi mới
|
|
@@ -162,32 +183,101 @@ function parseVnPostTime(txt, now = new Date()) {
|
|
|
162
183
|
// hiệu gì. Đo thật 2026-09-06: 5/43 lượt xin LAST_7D nhận về "Hôm nay", và một
|
|
163
184
|
// kênh ghi được daily_views 279.946 trong khi views_7d chỉ 278.662 — tức là số
|
|
164
185
|
// 28 ngày bị nhét vào ô "hôm nay". Phải ĐỢI nhãn khớp, không thì thà hỏng to.
|
|
186
|
+
// Profile để tiếng Anh thì Facebook trả nhãn tiếng Anh ("Last 7 days: Aug 30 -
|
|
187
|
+
// Sep 5"). Đo 2026-09-06: 4/52 kênh như vậy, parser thuần tiếng Việt coi trang
|
|
188
|
+
// đó là trống hoàn toàn.
|
|
165
189
|
const RANGE_LABEL = {
|
|
166
|
-
TODAY: /hôm nay/i,
|
|
167
|
-
LAST_7D: /7 ngày qua/i,
|
|
168
|
-
LAST_14D: /14 ngày qua/i,
|
|
169
|
-
LAST_28D: /28 ngày qua/i,
|
|
170
|
-
LAST_60D: /60 ngày qua/i,
|
|
171
|
-
LAST_90D: /90 ngày qua/i,
|
|
190
|
+
TODAY: /hôm nay|^today/i,
|
|
191
|
+
LAST_7D: /7 ngày qua|last 7 days/i,
|
|
192
|
+
LAST_14D: /14 ngày qua|last 14 days/i,
|
|
193
|
+
LAST_28D: /28 ngày qua|last 28 days/i,
|
|
194
|
+
LAST_60D: /60 ngày qua|last 60 days/i,
|
|
195
|
+
LAST_90D: /90 ngày qua|last 90 days/i,
|
|
172
196
|
};
|
|
173
197
|
|
|
198
|
+
// Đọc CHIP khoảng thời gian ("7 ngày qua: 30 Tháng 8 - 5 Tháng 9", "Hôm nay",
|
|
199
|
+
// "Tùy chỉnh: …"). KHÔNG đọc dòng "Tổng số lượt xem": Facebook dựng HAI kiểu
|
|
200
|
+
// giao diện cho cùng trang này — kiểu A có "… Tổng số lượt xem trong 7 ngày
|
|
201
|
+
// qua", kiểu B là "Số liệu chia nhỏ về lượt xem" rồi "26.313 -91% Lượt xem"
|
|
202
|
+
// (không có chữ "Tổng số"). Bám vào dòng đó là ~10/44 kênh kiểu B bị coi như
|
|
203
|
+
// trang trống — đúng ca "Má hồng phai" 2026-09-06.
|
|
204
|
+
async function currentRangeLabel(page) {
|
|
205
|
+
return page.evaluate(() => {
|
|
206
|
+
const clean = (v) => String(v || '').replace(/\s+/g, ' ').trim();
|
|
207
|
+
for (const e of document.querySelectorAll('div[role="button"],div,span')) {
|
|
208
|
+
const t = clean(e.innerText);
|
|
209
|
+
if (t.length < 70 && /^(Hôm nay$|Today$|Tùy chỉnh:|Custom:|Năm nay:|This year:|\d+ ngày qua:|Last \d+ days:)/i.test(t)) return t;
|
|
210
|
+
}
|
|
211
|
+
return '';
|
|
212
|
+
}).catch(() => '');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Tổng lượt xem của khoảng đang hiện, đọc được CẢ HAI kiểu giao diện.
|
|
216
|
+
// Phải neo cuối chuỗi ở "Lượt xem": trang còn có "Lượt xem trong tối thiểu 3
|
|
217
|
+
// giây" và "… 1 phút", khớp lỏng là vớ nhầm mấy số đó.
|
|
218
|
+
const TOTAL_PATTERNS = [
|
|
219
|
+
/^([\d.,]+)\s*(?:([-+]?[\d.,]+)%)?\s*Tổng số lượt xem/i,
|
|
220
|
+
/^([\d.,]+)\s*(?:([-+]?[\d.,]+)%)?\s*Total views/i,
|
|
221
|
+
/^([\d.,]+)\s*(?:([-+]?[\d.,]+)%)?\s*Lượt xem$/i,
|
|
222
|
+
/^([\d.,]+)\s*(?:([-+]?[\d.,]+)%)?\s*Views$/i,
|
|
223
|
+
];
|
|
224
|
+
|
|
225
|
+
function pickTotal(chunks) {
|
|
226
|
+
for (const re of TOTAL_PATTERNS) {
|
|
227
|
+
for (const c of chunks) {
|
|
228
|
+
const m = re.exec(c);
|
|
229
|
+
if (m) return { views: parseExact(m[1]), pct: m[2] ? parseFloat(String(m[2]).replace(/,/g, '')) : null };
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return { views: null, pct: null };
|
|
233
|
+
}
|
|
234
|
+
|
|
174
235
|
async function readViewsPage(page, range, log) {
|
|
175
236
|
const url = `${INSIGHTS_BASE}/views/?tab=reels&date_range=${range}`;
|
|
176
|
-
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 45_000 });
|
|
177
237
|
const want = RANGE_LABEL[range];
|
|
178
238
|
let label = '';
|
|
179
|
-
//
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
239
|
+
// LẦN ĐIỀU HƯỚNG ĐẦU TIÊN THƯỜNG BỊ BỎ QUA: trang giữ mặc định 28 ngày và
|
|
240
|
+
// lờ tham số date_range. Đo thật 2026-09-06 lúc 12h trưa: 8/47 kênh xin
|
|
241
|
+
// TODAY mà trang trả về "… trong 28 ngày qua" — không phải hết dữ liệu, mà
|
|
242
|
+
// là tham số không ăn. Nạp lại là lần sau nó nhận, nên thử tới 3 lượt.
|
|
243
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
244
|
+
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 45_000 });
|
|
245
|
+
for (let i = 0; i < 8; i++) {
|
|
246
|
+
await page.waitForTimeout(2000);
|
|
247
|
+
label = await currentRangeLabel(page);
|
|
248
|
+
if (label && (!want || want.test(label))) break;
|
|
249
|
+
}
|
|
250
|
+
if (!want || !label || want.test(label)) break;
|
|
251
|
+
log('info', `[insights] ${range}: lần ${attempt + 1} trang vẫn hiện "${label}", nạp lại`);
|
|
252
|
+
// Facebook NHỚ khoảng đã chọn lần trước theo từng profile, và tham số trên
|
|
253
|
+
// URL không phải lúc nào cũng đè được cái nhớ đó — nạp lại mấy lần vẫn y
|
|
254
|
+
// nguyên (đo 2026-09-06: 4 kênh xin LAST_7D mà cứ hiện "Hôm nay"). Đường
|
|
255
|
+
// chắc chắn là BẤM vào mục trong bộ chọn: chúng là thẻ <a> nên bấm là điều
|
|
256
|
+
// hướng client-side, luôn áp dụng.
|
|
257
|
+
if (attempt === 1) {
|
|
258
|
+
// CHUỘT THẬT của Playwright, không phải element.click() trong evaluate:
|
|
259
|
+
// click tổng hợp trong DOM không phải lúc nào cũng kích được handler của
|
|
260
|
+
// React (đo 2026-09-06: evaluate trả false, log không hề có dòng "đổi
|
|
261
|
+
// khoảng"). page.click gửi đủ chuỗi sự kiện chuột.
|
|
262
|
+
const ok = await (async () => {
|
|
263
|
+
try {
|
|
264
|
+
await page.locator('div[role="button"]').filter({ hasText: /^(Hôm nay|Today|Tùy chỉnh:|Custom:|Năm nay:|This year:|\d+ ngày qua:|Last \d+ days:)/ })
|
|
265
|
+
.first().click({ timeout: 8000 });
|
|
266
|
+
await page.waitForTimeout(2000);
|
|
267
|
+
await page.locator(`a[role="menuitemradio"][href*="date_range=${range}"]`).first().click({ timeout: 8000 });
|
|
268
|
+
return true;
|
|
269
|
+
} catch { return false; }
|
|
270
|
+
})();
|
|
271
|
+
if (ok) {
|
|
272
|
+
log('info', `[insights] ${range}: đổi khoảng bằng cách bấm bộ chọn`);
|
|
273
|
+
for (let i = 0; i < 8; i++) {
|
|
274
|
+
await page.waitForTimeout(2000);
|
|
275
|
+
label = await currentRangeLabel(page);
|
|
276
|
+
if (label && want.test(label)) break;
|
|
277
|
+
}
|
|
278
|
+
if (label && want.test(label)) break;
|
|
187
279
|
}
|
|
188
|
-
|
|
189
|
-
}).catch(() => '');
|
|
190
|
-
if (label && (!want || want.test(label))) break;
|
|
280
|
+
}
|
|
191
281
|
}
|
|
192
282
|
// Hai ca KHÁC HẲN nhau, đừng gộp:
|
|
193
283
|
// nhãn RỖNG = khoảng này chưa có số nào. Chuyện thường, không phải lỗi —
|
|
@@ -208,7 +298,7 @@ async function readViewsPage(page, range, log) {
|
|
|
208
298
|
for (const e of document.querySelectorAll('div,span')) {
|
|
209
299
|
const t = clean(e.innerText);
|
|
210
300
|
if (!t || t.length > 90 || !/\d/.test(t) || seen.has(t)) continue;
|
|
211
|
-
if (/
|
|
301
|
+
if (/[Ll]ượt xem|[Vv]iews/.test(t)) { seen.add(t); chunks.push(t); }
|
|
212
302
|
if (chunks.length > 120) break;
|
|
213
303
|
}
|
|
214
304
|
const label = [...document.querySelectorAll('div,span,button')]
|
|
@@ -217,16 +307,17 @@ async function readViewsPage(page, range, log) {
|
|
|
217
307
|
return { chunks, label };
|
|
218
308
|
}).catch(() => ({ chunks: [], label: '' }));
|
|
219
309
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
for (const c of raw.chunks) {
|
|
223
|
-
const m = /^([\d.]+)\s*(?:(-?[\d.,]+)%)?\s*Tổng số lượt xem/i.exec(c);
|
|
224
|
-
if (m) { total = parseExact(m[1]); pct = m[2] ? parseFloat(String(m[2]).replace(',', '.')) : null; break; }
|
|
225
|
-
}
|
|
310
|
+
const t0 = pickTotal(raw.chunks);
|
|
311
|
+
const total = t0.views; const pct = t0.pct;
|
|
226
312
|
// Dòng bài: "31 Tháng 8 11:41 56.243 Lượt xem"
|
|
227
313
|
const posts = [];
|
|
314
|
+
const POST_LINE = [
|
|
315
|
+
/(\d{1,2}\s*Tháng\s*\d{1,2}\s+\d{1,2}:\d{2})\s+([\d.,]+)\s*Lượt xem/i,
|
|
316
|
+
/((?:[A-Za-z]{3},?\s+)?[A-Za-z]{3}\s+\d{1,2},?\s+\d{1,2}:\d{2}\s*(?:am|pm))\s+([\d.,]+)\s*Views/i,
|
|
317
|
+
];
|
|
228
318
|
for (const c of raw.chunks) {
|
|
229
|
-
|
|
319
|
+
let m = null;
|
|
320
|
+
for (const re of POST_LINE) { m = re.exec(c); if (m) break; }
|
|
230
321
|
if (!m) continue;
|
|
231
322
|
const at = parseVnPostTime(m[1]);
|
|
232
323
|
const views = parseExact(m[2]);
|
|
@@ -236,6 +327,70 @@ async function readViewsPage(page, range, log) {
|
|
|
236
327
|
return { range, label, total, pct, posts };
|
|
237
328
|
}
|
|
238
329
|
|
|
330
|
+
// Đọc ĐÚNG MỘT NGÀY qua khoảng tuỳ chỉnh.
|
|
331
|
+
//
|
|
332
|
+
// Vì sao không dùng date_range=TODAY: Facebook KHÔNG có số của hôm nay. Đo
|
|
333
|
+
// 2026-09-06 lúc 12h trưa trên kênh 1,5 triệu view/7 ngày, trang trả về đúng
|
|
334
|
+
// chữ "--" kèm "Chưa đủ dữ liệu để hiển thị". Chip khoảng thời gian của chính
|
|
335
|
+
// nó cũng kết thúc ở HÔM QUA ("28 ngày qua: 9 Tháng 8 - 5 Tháng 9" trong khi
|
|
336
|
+
// đã là ngày 6) — cửa sổ dữ liệu của Facebook đóng sổ ở hôm qua.
|
|
337
|
+
//
|
|
338
|
+
// Không có preset "Hôm qua" (đã thử YESTERDAY/LAST_1D/PREVIOUS_DAY, cả ba rơi
|
|
339
|
+
// về 28 ngày). Định dạng dưới đây lấy được bằng cách bấm thật vào lịch trong
|
|
340
|
+
// mục "Tùy chỉnh" rồi đọc URL nó sinh ra.
|
|
341
|
+
//
|
|
342
|
+
// BẪY: dòng "… Tổng số lượt xem trong 28 ngày qua" ở khoảng tuỳ chỉnh là NHÃN
|
|
343
|
+
// CŨ chưa kịp đổi, nhưng CON SỐ thì đã đúng của ngày đã chọn. Nên phải xác
|
|
344
|
+
// minh bằng CHIP ("Tùy chỉnh: 5 Tháng 9 – 5 Tháng 9"), không phải bằng nhãn đó.
|
|
345
|
+
async function readOneDay(page, iso, log) {
|
|
346
|
+
const url = `${INSIGHTS_BASE}/views/?tab=reels&date_range=CUSTOM&start_date=${iso}&end_date=${iso}`;
|
|
347
|
+
let chip = '';
|
|
348
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
349
|
+
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 45_000 });
|
|
350
|
+
for (let i = 0; i < 8; i++) {
|
|
351
|
+
await page.waitForTimeout(2000);
|
|
352
|
+
chip = await page.evaluate(() => {
|
|
353
|
+
const clean = (v) => String(v || '').replace(/\s+/g, ' ').trim();
|
|
354
|
+
for (const e of document.querySelectorAll('div[role="button"],span,div')) {
|
|
355
|
+
const t = clean(e.innerText);
|
|
356
|
+
if (t.length < 70 && /^(Tùy chỉnh:|Custom:)/i.test(t)) return t;
|
|
357
|
+
}
|
|
358
|
+
return '';
|
|
359
|
+
}).catch(() => '');
|
|
360
|
+
if (chip) break;
|
|
361
|
+
}
|
|
362
|
+
if (chip) break;
|
|
363
|
+
log('info', `[insights] ngày ${iso}: lần ${attempt + 1} chưa thấy chip tuỳ chỉnh, nạp lại`);
|
|
364
|
+
}
|
|
365
|
+
if (!chip) return { iso, chip: '', views: null };
|
|
366
|
+
// Chip đổi TRƯỚC, con số đổ về SAU — chờ cố định 3s là hên xui: đo 2026-09-06
|
|
367
|
+
// có kênh chip đúng "Custom: Sep 5 - Sep 5" mà số vẫn null. Chờ tới khi thấy
|
|
368
|
+
// số, tối đa ~14s.
|
|
369
|
+
let views = null;
|
|
370
|
+
for (let i = 0; i < 7; i++) {
|
|
371
|
+
await page.waitForTimeout(2000);
|
|
372
|
+
const cs = await readTotalChunks(page);
|
|
373
|
+
views = pickTotal(cs).views;
|
|
374
|
+
if (views != null) break;
|
|
375
|
+
}
|
|
376
|
+
log('info', `[insights] ngày ${iso}: ${views} lượt xem — chip "${chip}"`);
|
|
377
|
+
return { iso, chip, views };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
async function readTotalChunks(page) {
|
|
381
|
+
return page.evaluate(() => {
|
|
382
|
+
const clean = (v) => String(v || '').replace(/\s+/g, ' ').trim();
|
|
383
|
+
const seen = new Set(); const out = [];
|
|
384
|
+
for (const e of document.querySelectorAll('div,span')) {
|
|
385
|
+
const t = clean(e.innerText);
|
|
386
|
+
if (!t || t.length > 90 || !/\d/.test(t) || seen.has(t)) continue;
|
|
387
|
+
if (/[Ll]ượt xem/.test(t)) { seen.add(t); out.push(t); }
|
|
388
|
+
if (out.length > 120) break;
|
|
389
|
+
}
|
|
390
|
+
return out;
|
|
391
|
+
}).catch(() => []);
|
|
392
|
+
}
|
|
393
|
+
|
|
239
394
|
async function readAudiencePage(page, log) {
|
|
240
395
|
await page.goto(`${INSIGHTS_BASE}/audience/`, { waitUntil: 'domcontentloaded', timeout: 45_000 });
|
|
241
396
|
await page.waitForTimeout(7000);
|
|
@@ -245,16 +400,19 @@ async function readAudiencePage(page, log) {
|
|
|
245
400
|
for (const e of document.querySelectorAll('div,span')) {
|
|
246
401
|
const t = clean(e.innerText);
|
|
247
402
|
if (!t || t.length > 90 || !/\d/.test(t) || seen.has(t)) continue;
|
|
248
|
-
if (/người theo dõi|bỏ theo dõi/i.test(t)) { seen.add(t); out.push(t); }
|
|
403
|
+
if (/người theo dõi|bỏ theo dõi|followers|follows|unfollows/i.test(t)) { seen.add(t); out.push(t); }
|
|
249
404
|
if (out.length > 60) break;
|
|
250
405
|
}
|
|
251
406
|
return out;
|
|
252
407
|
}).catch(() => []);
|
|
253
|
-
const pick = (
|
|
408
|
+
const pick = (...res) => {
|
|
409
|
+
for (const re of res) for (const c of chunks) { const m = re.exec(c); if (m) return parseExact(m[1]); }
|
|
410
|
+
return null;
|
|
411
|
+
};
|
|
254
412
|
const a = {
|
|
255
|
-
followers: pick(/^([\d
|
|
256
|
-
net_followers: pick(/^([\d
|
|
257
|
-
unfollows: pick(/([\d
|
|
413
|
+
followers: pick(/^([\d.,]+)\s*(?:[-+][\d.,]+%\s*)?Tổng số người theo dõi/i, /^([\d.,]+)\s*(?:[-+][\d.,]+%\s*)?Total followers/i),
|
|
414
|
+
net_followers: pick(/^([\d.,]+)\s*(?:[-+][\d.,]+%\s*)?Số người theo dõi thực/i, /^([\d.,]+)\s*(?:[-+][\d.,]+%\s*)?Net follows/i),
|
|
415
|
+
unfollows: pick(/([\d.,]+)\s*Lượt bỏ theo dõi/i, /([\d.,]+)\s*Unfollows/i),
|
|
258
416
|
};
|
|
259
417
|
log('info', `[insights] đối tượng: ${a.followers} follower, thực ${a.net_followers}, bỏ ${a.unfollows}`);
|
|
260
418
|
return a;
|
|
@@ -264,24 +422,27 @@ async function readAudiencePage(page, log) {
|
|
|
264
422
|
// Việc khớp bài với idea và tính hiệu để lòi ra "bài cũ đang nổ" làm ở API —
|
|
265
423
|
// worker không giữ trạng thái ngày hôm qua.
|
|
266
424
|
async function runInsights({ page, payload, log }) {
|
|
267
|
-
|
|
425
|
+
// 7 ngày trước để làm ấm trang (lần điều hướng ĐẦU hay bị bỏ qua tham số),
|
|
426
|
+
// rồi mới tới ngày hôm qua.
|
|
268
427
|
const week = await readViewsPage(page, 'LAST_7D', log);
|
|
428
|
+
const yIso = vnYesterdayIso();
|
|
429
|
+
const yest = await readOneDay(page, yIso, log);
|
|
269
430
|
const audience = await readAudiencePage(page, log);
|
|
270
|
-
if (
|
|
431
|
+
if (yest.views == null && week.total == null) {
|
|
271
432
|
throw new Error('không đọc được tổng lượt xem trên trang Insights — FB đổi giao diện?');
|
|
272
433
|
}
|
|
273
434
|
if (typeof log.apiRequest !== 'function') throw new Error('log.apiRequest không có — cần channel-worker ≥ 2.5.55');
|
|
274
435
|
const d = await log.apiRequest('POST', '/analytics/fb-insights', {
|
|
275
436
|
channel_id: payload.channel_id,
|
|
276
437
|
at: new Date().toISOString(),
|
|
277
|
-
|
|
438
|
+
yesterday: { date: yest.iso, views: yest.views, chip: yest.chip },
|
|
278
439
|
week: { views: week.total, posts: week.posts },
|
|
279
440
|
audience,
|
|
280
441
|
});
|
|
281
|
-
log('info', `[insights] đã ghi:
|
|
442
|
+
log('info', `[insights] đã ghi: hôm qua ${d?.prev_day_views}, follower ${d?.followers}, bài khớp ${d?.matched}/${d?.posts}, view bài cũ ${d?.unattributed}`);
|
|
282
443
|
return {
|
|
283
444
|
ok: true, mode: 'insights',
|
|
284
|
-
|
|
445
|
+
views_yesterday: yest.views, views_7d: week.total,
|
|
285
446
|
posts: week.posts.length, matched: d?.matched ?? null,
|
|
286
447
|
followers: audience.followers, unattributed: d?.unattributed ?? null,
|
|
287
448
|
};
|