channel-worker 2.5.129 → 2.5.131
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
|
@@ -127,6 +127,214 @@ async function scrapeTiles(page) {
|
|
|
127
127
|
|
|
128
128
|
const vnYesterdayIso = () => new Date(Date.now() + 7 * 3600e3 - 864e5).toISOString().slice(0, 10);
|
|
129
129
|
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
// THƯ VIỆN NỘI DUNG (professional_dashboard/content/content_library)
|
|
132
|
+
//
|
|
133
|
+
// Trang Insights ở trên chỉ liệt kê bài ĐĂNG trong kỳ, nên bài cũ đang nổ view
|
|
134
|
+
// không bao giờ có tên — chuông chỉ nói được "có bài cũ nào đó đang lên". Trang
|
|
135
|
+
// này liệt kê MỌI bài trong khoảng, mỗi bài một dòng, số chính xác tới đơn vị.
|
|
136
|
+
// Dò thật 22/09/2026 (kênh Nhà Nông Chia Sẻ, 29 bài/28 ngày). Bốn điều đã đo,
|
|
137
|
+
// sai cái nào cũng ra số rác:
|
|
138
|
+
//
|
|
139
|
+
// 1. SỐ LÀ LUỸ KẾ, không phải theo kỳ. Bài 16/09 hiện 185.522 ở khoảng 90 ngày
|
|
140
|
+
// VÀ 185.522 ở khoảng tuỳ chỉnh 16–17/09 (chỉ phủ 2 ngày đầu đời nó). Đổi
|
|
141
|
+
// khoảng chỉ đổi DANH SÁCH bài. → view theo ngày = hiệu hai mốc, làm ở API.
|
|
142
|
+
// 2. Bộ lọc khoảng lọc theo NGÀY ĐĂNG, giờ Mỹ, END LOẠI TRỪ (xin 21–21/09 ra 0
|
|
143
|
+
// bài; xin 20–21/09 ra đúng bài đăng 21/09). Đừng dùng nó để xin "view ngày
|
|
144
|
+
// X" — nó không làm việc đó. Chỉ dùng khoảng RỘNG để lấy đủ danh sách bài.
|
|
145
|
+
// 3. Bảng ẢO HOÁ, dòng bị TÁI DÙNG khi cuộn (đo: 10 → 36 → 32 dòng). Đọc một
|
|
146
|
+
// phát sau khi cuộn xong là mất khúc giữa → phải nhặt TỪNG BƯỚC rồi gộp.
|
|
147
|
+
// Cuộn khối chứa bảng KHÔNG ăn (mọi khối cha overflow:visible, cửa sổ cuộn
|
|
148
|
+
// 69px rồi đứng). Cách ăn: đưa DÒNG CUỐI vào tầm nhìn + PageDown.
|
|
149
|
+
// 4. Mỗi hàng có 14 ô nhưng 2 ô RỖNG (ô tích chọn + ô ảnh), và vị trí của chúng
|
|
150
|
+
// ĐỔI THEO CỠ CỬA SỔ. Đánh số theo ô thô là đọc caption vào ô "Lượt xem" —
|
|
151
|
+
// đã dính thật lúc dò. Phải bỏ ô rỗng RỒI mới đánh số.
|
|
152
|
+
//
|
|
153
|
+
// KHÔNG moi được reel id ở đây: hàng chỉ có một link `/content/insights/?content_id=…`,
|
|
154
|
+
// trang chi tiết cũng chỉ trỏ `/reel/?s=tab`, và content_id giải mã ra chỉ có
|
|
155
|
+
// profile id + story id. Khớp bài làm ở API bằng CAPTION + giờ đăng.
|
|
156
|
+
const LIBRARY_URL = 'https://www.facebook.com/professional_dashboard/content/content_library/';
|
|
157
|
+
const LIB_RANGE = process.env.FB_LIB_RANGE || 'LAST_28D';
|
|
158
|
+
// Trần vòng cuộn. Đo 22/09: kênh 35 bài ăn hết 14 vòng (mỗi vòng lộ ~5 dòng)
|
|
159
|
+
// nên 14 là CHẠM TRẦN, không phải đủ — kênh đăng đều 28 ngày có thể 40-50 bài.
|
|
160
|
+
// Cổng dừng-sớm mới là thứ giữ thời gian, trần chỉ là dây an toàn.
|
|
161
|
+
const LIB_MAX_STEPS = Number(process.env.FB_LIB_MAX_STEPS || 26);
|
|
162
|
+
// Trần THỜI GIAN cho cả vòng cuộn, tính từ lúc bảng đã vẽ xong. Trần vòng lặp
|
|
163
|
+
// không đủ: một vòng có thể kéo dài bất thường khi trang nghẽn, mà lượt này
|
|
164
|
+
// chạy cho 44 kênh mỗi ngày nên phải có điểm dừng đo bằng giây.
|
|
165
|
+
const LIB_BUDGET_MS = Number(process.env.FB_LIB_BUDGET_MS || 75_000);
|
|
166
|
+
|
|
167
|
+
// "16 tháng 9 lúc 15:37" · "Hôm nay lúc 11:50" · "Hôm qua lúc 11:59"
|
|
168
|
+
// · "Sep 16 at 3:37 PM" (profile tiếng Anh — 4/52 kênh, xem ghi chú parseVnPostTime).
|
|
169
|
+
// Trả ISO, hoặc null nếu không đọc được (API tự bỏ dòng không có giờ).
|
|
170
|
+
function parseLibraryTime(txt, now = new Date()) {
|
|
171
|
+
const t = String(txt || '').replace(/\s+/g, ' ').trim();
|
|
172
|
+
const vnNow = new Date(now.getTime() + 7 * 3600e3);
|
|
173
|
+
const hm = /(\d{1,2}):(\d{2})\s*(am|pm|AM|PM)?/.exec(t);
|
|
174
|
+
if (!hm) return null;
|
|
175
|
+
let h = Number(hm[1]); const mi = Number(hm[2]);
|
|
176
|
+
const ap = (hm[3] || '').toLowerCase();
|
|
177
|
+
if (ap === 'pm' && h < 12) h += 12;
|
|
178
|
+
if (ap === 'am' && h === 12) h = 0;
|
|
179
|
+
|
|
180
|
+
let y = vnNow.getUTCFullYear(); let mo; let d;
|
|
181
|
+
const rel = /^(Hôm nay|Hôm qua|Today|Yesterday)/i.exec(t);
|
|
182
|
+
if (rel) {
|
|
183
|
+
const back = /qua|yesterday/i.test(rel[1]) ? 864e5 : 0;
|
|
184
|
+
const day = new Date(vnNow.getTime() - back);
|
|
185
|
+
y = day.getUTCFullYear(); mo = day.getUTCMonth() + 1; d = day.getUTCDate();
|
|
186
|
+
} else {
|
|
187
|
+
const vn = /(\d{1,2})\s*tháng\s*(\d{1,2})/i.exec(t);
|
|
188
|
+
if (vn) { d = Number(vn[1]); mo = Number(vn[2]); } else {
|
|
189
|
+
const en = /([A-Za-z]{3})[a-z]*\s+(\d{1,2})/.exec(t);
|
|
190
|
+
if (!en) return null;
|
|
191
|
+
mo = EN_MONTH[en[1].toLowerCase()]; d = Number(en[2]);
|
|
192
|
+
if (!mo) return null;
|
|
193
|
+
}
|
|
194
|
+
// Facebook không ghi năm. Tháng lớn hơn tháng hiện tại = năm ngoái.
|
|
195
|
+
if (mo > vnNow.getUTCMonth() + 1) y -= 1;
|
|
196
|
+
}
|
|
197
|
+
const iso = `${y}-${String(mo).padStart(2, '0')}-${String(d).padStart(2, '0')}`
|
|
198
|
+
+ `T${String(h).padStart(2, '0')}:${String(mi).padStart(2, '0')}:00+07:00`;
|
|
199
|
+
const parsed = new Date(iso);
|
|
200
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Hàm chạy TRONG TRANG — phải tự chứa, closure của module không đi theo.
|
|
204
|
+
function readLibraryRowsInPage() {
|
|
205
|
+
const clean = (v) => String(v || '').replace(/\s+/g, ' ').trim();
|
|
206
|
+
let chip = '';
|
|
207
|
+
const CHIP = /^(Hôm nay$|Today$|Tùy chỉnh:|Custom:|Năm nay:|This year:|\d+ ngày qua:|Last \d+ days:)/i;
|
|
208
|
+
for (const e of document.querySelectorAll('div[role="button"],span,div')) {
|
|
209
|
+
const t = clean(e.innerText);
|
|
210
|
+
if (t.length >= 70 || !CHIP.test(t)) continue;
|
|
211
|
+
const r = e.getBoundingClientRect();
|
|
212
|
+
if (r && r.width > 0 && r.height > 0) { chip = t; break; }
|
|
213
|
+
}
|
|
214
|
+
const rows = [];
|
|
215
|
+
for (const e of document.querySelectorAll('[role="row"]')) {
|
|
216
|
+
// BỎ Ô RỖNG trước khi đánh số — xem bẫy 4 ở đầu khối.
|
|
217
|
+
const cells = [...e.querySelectorAll('[role="gridcell"],[role="cell"],td')]
|
|
218
|
+
.map((c) => clean(c.innerText)).filter(Boolean);
|
|
219
|
+
if (cells.length < 3) continue;
|
|
220
|
+
const head = cells[0];
|
|
221
|
+
if (/^(Bản xem trước|Preview)/i.test(head)) continue; // hàng tiêu đề
|
|
222
|
+
const m = /Đã đăng\s*•?\s*(.+)$/i.exec(head) || /Published\s*•?\s*(.+)$/i.exec(head);
|
|
223
|
+
if (!m) continue; // chưa đăng (nháp/lên lịch)
|
|
224
|
+
const a = e.querySelector('a[href*="content_id="]');
|
|
225
|
+
rows.push({
|
|
226
|
+
posted_txt: clean(m[1]).slice(0, 40),
|
|
227
|
+
caption: clean(head.replace(/\s*Đã đăng[\s\S]*$/i, '').replace(/\s*Published[\s\S]*$/i, '')).slice(0, 200),
|
|
228
|
+
views_text: cells[1] || '',
|
|
229
|
+
viewers_text: cells[2] || '',
|
|
230
|
+
engagements_text: cells[3] || '',
|
|
231
|
+
net_followers_text: cells[4] || '',
|
|
232
|
+
content_id: a ? ((a.getAttribute('href') || '').match(/content_id=([^&]+)/) || [])[1] || '' : '',
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
return { chip, rows };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Kéo thêm dòng: đưa dòng cuối vào tầm nhìn (cuộn khối không ăn — xem bẫy 3).
|
|
239
|
+
function scrollLibraryInPage() {
|
|
240
|
+
for (const el of [document.scrollingElement, ...document.querySelectorAll('div,main,section')]) {
|
|
241
|
+
if (!el || el.scrollHeight <= el.clientHeight + 30) continue;
|
|
242
|
+
const before = el.scrollTop;
|
|
243
|
+
el.scrollTop = before + 1800;
|
|
244
|
+
if (el.scrollTop !== before) break;
|
|
245
|
+
}
|
|
246
|
+
const rows = document.querySelectorAll('[role="row"]');
|
|
247
|
+
const last = rows[rows.length - 1];
|
|
248
|
+
if (last) last.scrollIntoView({ block: 'end' });
|
|
249
|
+
return rows.length;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function readLibraryPage(page, log, { range = LIB_RANGE, maxSteps = LIB_MAX_STEPS } = {}) {
|
|
253
|
+
// ĐẶT CỠ CỬA SỔ TRƯỚC KHI TẢI TRANG. Đặt sau thì bảng đã dựng xong ở cỡ mặc
|
|
254
|
+
// định và không dựng lại: đo 22/09 kênh Nhà Nông ra 10/29 bài, cuộn thêm 4
|
|
255
|
+
// vòng không ra thêm dòng nào. Cùng đoạn code, chỉ đổi thứ tự hai dòng này,
|
|
256
|
+
// là 29/29.
|
|
257
|
+
await page.setViewportSize({ width: 1400, height: 1000 }).catch(() => {});
|
|
258
|
+
await page.goto(`${LIBRARY_URL}?date_range=${range}`, { waitUntil: 'domcontentloaded', timeout: 60_000 });
|
|
259
|
+
|
|
260
|
+
// Chờ bảng vẽ XONG, không phải chờ "có dòng nào chưa".
|
|
261
|
+
//
|
|
262
|
+
// Bản đầu thoát ngay khi `rows.length` khác 0 và nó SAI THẬT: đo 22/09 trên
|
|
263
|
+
// kênh Nhà Nông Chia Sẻ ra 5 bài trong khi bảng có 29 — trang đổ dòng dần,
|
|
264
|
+
// bắt được lứa đầu rồi bỏ đi thì mất phần lớn bảng, mà không có dấu hiệu gì
|
|
265
|
+
// vì 5 bài vẫn là một kết quả "hợp lệ".
|
|
266
|
+
//
|
|
267
|
+
// Bảng đầy đủ luôn render 10 dòng một lúc (trần một trang là 50), nên mốc
|
|
268
|
+
// "đủ 10 dòng" là dấu hiệu chắc chắn. Kênh ít bài hơn 10 thì không bao giờ
|
|
269
|
+
// đạt mốc đó — với chúng, dấu hiệu là số dòng ĐỨNG YÊN ba vòng liền.
|
|
270
|
+
let first = { chip: '', rows: [] };
|
|
271
|
+
let same = 0;
|
|
272
|
+
for (let i = 0; i < 15; i++) {
|
|
273
|
+
await page.waitForTimeout(2000);
|
|
274
|
+
const got = await page.evaluate(readLibraryRowsInPage).catch(() => ({ chip: '', rows: [] }));
|
|
275
|
+
same = got.rows.length === first.rows.length && got.rows.length > 0 ? same + 1 : 0;
|
|
276
|
+
first = got;
|
|
277
|
+
if (first.rows.length >= 10 || same >= 3) break;
|
|
278
|
+
}
|
|
279
|
+
if (!first.rows.length) {
|
|
280
|
+
return { range, chip: first.chip || '', posts: [], steps: 0, note: 'không đọc được dòng nào' };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Nhặt từng bước rồi gộp — bảng tái dùng dòng, đọc một phát là mất khúc giữa.
|
|
284
|
+
const seen = new Map();
|
|
285
|
+
const key = (r) => `${r.posted_txt}|${r.caption.slice(0, 40)}`;
|
|
286
|
+
for (const r of first.rows) seen.set(key(r), r);
|
|
287
|
+
let chip = first.chip; let stable = 0; let steps = 0; let grew = false;
|
|
288
|
+
const deadline = Date.now() + LIB_BUDGET_MS;
|
|
289
|
+
for (let i = 0; i < maxSteps; i++) {
|
|
290
|
+
if (Date.now() > deadline) break;
|
|
291
|
+
await page.evaluate(scrollLibraryInPage).catch(() => 0);
|
|
292
|
+
await page.keyboard.press('PageDown').catch(() => {});
|
|
293
|
+
await page.waitForTimeout(2200);
|
|
294
|
+
const got = await page.evaluate(readLibraryRowsInPage).catch(() => ({ rows: [] }));
|
|
295
|
+
chip = got.chip || chip;
|
|
296
|
+
const before = seen.size;
|
|
297
|
+
for (const r of got.rows || []) seen.set(key(r), r);
|
|
298
|
+
steps = i + 1;
|
|
299
|
+
if (seen.size > before) grew = true;
|
|
300
|
+
// Ba vòng liền không thêm bài nào = hết bảng. Dừng sớm để khỏi đốt thời
|
|
301
|
+
// gian của 44 kênh mỗi ngày.
|
|
302
|
+
//
|
|
303
|
+
// `grew`: chỉ được tin cổng dừng SAU KHI đã thấy bảng lớn lên ít nhất một
|
|
304
|
+
// lần. Không có nó thì mấy vòng đầu — lúc trang còn đang đổ dòng — tự cộng
|
|
305
|
+
// dồn thành "hết bảng" và cắt lượt đọc ngay từ vòng 3 (đúng lỗi 5/29 bài).
|
|
306
|
+
//
|
|
307
|
+
// Nhưng kênh ít bài thì bảng KHÔNG BAO GIỜ lớn lên (6 bài, hiện hết ngay từ
|
|
308
|
+
// đầu) — chỉ đòi `grew` là nó cuộn hết 26 vòng cho 6 bài, tốn 73 giây vô
|
|
309
|
+
// ích (đo thật 22/09, kênh meo-nho-khoe-to). Nên sau 5 vòng vẫn đứng yên
|
|
310
|
+
// thì cũng là hết bảng.
|
|
311
|
+
// Ngưỡng 5 chứ không phải 3: bảng có lúc NGHỈ một hai vòng rồi mới nạp
|
|
312
|
+
// tiếp. Đo 22/09 kênh Nhà Nông — ngưỡng 3 cắt ở 25/29 bài, ngưỡng 5 ra đủ.
|
|
313
|
+
// Bài thiếu không kêu lỗi ở đâu cả, nên chỗ này phải rộng tay.
|
|
314
|
+
stable = seen.size === before ? stable + 1 : 0;
|
|
315
|
+
if (stable >= 5 && (grew || i >= 5)) break;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const now = new Date();
|
|
319
|
+
const posts = [];
|
|
320
|
+
for (const r of seen.values()) {
|
|
321
|
+
const views = parseExact(r.views_text);
|
|
322
|
+
if (views == null) continue; // dòng chưa đổ số — bỏ, đừng ghi 0
|
|
323
|
+
posts.push({
|
|
324
|
+
at: parseLibraryTime(r.posted_txt, now),
|
|
325
|
+
posted_txt: r.posted_txt,
|
|
326
|
+
caption: r.caption,
|
|
327
|
+
views,
|
|
328
|
+
viewers: parseExact(r.viewers_text),
|
|
329
|
+
engagements: parseExact(r.engagements_text),
|
|
330
|
+
net_followers: parseExact(r.net_followers_text),
|
|
331
|
+
content_id: String(r.content_id || '').slice(0, 120),
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
log('info', `[library] ${range}: ${posts.length} bài sau ${steps} vòng cuộn — "${chip}"`);
|
|
335
|
+
return { range, chip, posts, steps };
|
|
336
|
+
}
|
|
337
|
+
|
|
130
338
|
const INSIGHTS_BASE = 'https://www.facebook.com/professional_dashboard/profile_insights';
|
|
131
339
|
|
|
132
340
|
// Số trên Insights là số đầy đủ, dấu chấm là phân cách nghìn ("1.333.852").
|
|
@@ -490,7 +698,7 @@ async function dumpInsightsFailure(page, tag, log) {
|
|
|
490
698
|
// chip "Tùy chỉnh: 9 Tháng 9" ra đều). Một chip kẹt làm mất view hôm qua +
|
|
491
699
|
// follower — hai số quan trọng nhất — cho cả ngày, /analytics báo "Chưa quét".
|
|
492
700
|
// Ngày hôm qua tự có vòng nạp lại 3 lần nên không cần trang khác làm ấm.
|
|
493
|
-
const DEFAULT_READERS = { readOneDay, readAudiencePage, readViewsPage, dump: dumpInsightsFailure };
|
|
701
|
+
const DEFAULT_READERS = { readOneDay, readAudiencePage, readViewsPage, readLibraryPage, dump: dumpInsightsFailure };
|
|
494
702
|
async function runInsights({ page, payload, log }, R = DEFAULT_READERS) {
|
|
495
703
|
const yIso = vnYesterdayIso();
|
|
496
704
|
const yest = await R.readOneDay(page, yIso, log);
|
|
@@ -509,6 +717,25 @@ async function runInsights({ page, payload, log }, R = DEFAULT_READERS) {
|
|
|
509
717
|
? `không đọc được cả ngày hôm qua lẫn 7 ngày — ${weekError}`
|
|
510
718
|
: 'không đọc được tổng lượt xem trên trang Insights — FB đổi giao diện?');
|
|
511
719
|
}
|
|
720
|
+
|
|
721
|
+
// Thư viện nội dung: bảng MỌI bài, số luỹ kế chính xác tới đơn vị. Đây là chỗ
|
|
722
|
+
// duy nhất nhìn thấy bài CŨ, nên nó gánh việc gọi tên bài đang nổ.
|
|
723
|
+
//
|
|
724
|
+
// ĐỌC SAU CÙNG và hỏng thì CHỈ MẤT NÓ (cùng lý do như 7 ngày, vá 10/09): ba
|
|
725
|
+
// số quan trọng nhất — view hôm qua, follower, tổng 7 ngày — đã nằm trong tay
|
|
726
|
+
// rồi, không được để một trang mới làm hỏng cả lượt.
|
|
727
|
+
let library = { range: LIB_RANGE, chip: '', posts: [] };
|
|
728
|
+
let libraryError = null;
|
|
729
|
+
try {
|
|
730
|
+
// Bộ đọc THIẾU không phải lỗi — bên gọi truyền vào bộ đọc rút gọn (test
|
|
731
|
+
// hiện có cho phần 7-ngày làm đúng vậy) thì chỉ là không có phần này.
|
|
732
|
+
if (typeof R.readLibraryPage === 'function') library = await R.readLibraryPage(page, log);
|
|
733
|
+
} catch (e) {
|
|
734
|
+
libraryError = String((e && e.message) || e);
|
|
735
|
+
log('warn', `[library] hỏng (${libraryError}) — vẫn ghi phần Insights, chỉ mất số từng bài`);
|
|
736
|
+
await R.dump(page, 'content-library', log);
|
|
737
|
+
}
|
|
738
|
+
|
|
512
739
|
if (typeof log.apiRequest !== 'function') throw new Error('log.apiRequest không có — cần channel-worker ≥ 2.5.55');
|
|
513
740
|
const d = await log.apiRequest('POST', '/analytics/fb-insights', {
|
|
514
741
|
channel_id: payload.channel_id,
|
|
@@ -516,14 +743,19 @@ async function runInsights({ page, payload, log }, R = DEFAULT_READERS) {
|
|
|
516
743
|
yesterday: { date: yest.iso, views: yest.views, chip: yest.chip },
|
|
517
744
|
week: { views: week.total, posts: week.posts },
|
|
518
745
|
audience,
|
|
746
|
+
library: { range: library.range, chip: library.chip, posts: library.posts },
|
|
519
747
|
});
|
|
520
|
-
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}`
|
|
748
|
+
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}`
|
|
749
|
+
+ `, thư viện ${library.posts.length} bài (khớp ${d?.library_matched ?? '?'}, bài cũ đang lên ${d?.spike_posts ?? 0})`);
|
|
521
750
|
return {
|
|
522
751
|
ok: true, mode: 'insights',
|
|
523
752
|
views_yesterday: yest.views, views_7d: week.total,
|
|
524
753
|
posts: week.posts.length, matched: d?.matched ?? null,
|
|
525
754
|
followers: audience.followers, unattributed: d?.unattributed ?? null,
|
|
755
|
+
library_posts: library.posts.length,
|
|
756
|
+
library_matched: d?.library_matched ?? null,
|
|
526
757
|
...(weekError ? { week_error: weekError } : {}),
|
|
758
|
+
...(libraryError ? { library_error: libraryError } : {}),
|
|
527
759
|
};
|
|
528
760
|
}
|
|
529
761
|
|
|
@@ -600,4 +832,4 @@ async function run({ page, payload, log }) {
|
|
|
600
832
|
return { ok: true, mode, tiles: tiles.length, written: d?.written || 0, matched: d?.matched || 0, rising: d?.rising || 0, alerts: d?.alerts || 0, followers };
|
|
601
833
|
}
|
|
602
834
|
|
|
603
|
-
module.exports = { run, __testables: { parseViews, scrapeFollowers, parseExact, parseVnPostTime, runInsights, dumpInsightsFailure, pickRangeLabelInPage, discoverReelsTabUrl, discoverReelsTabUrls, openReelsTabWithTiles } };
|
|
835
|
+
module.exports = { run, __testables: { parseLibraryTime, readLibraryPage, parseViews, scrapeFollowers, parseExact, parseVnPostTime, runInsights, dumpInsightsFailure, pickRangeLabelInPage, discoverReelsTabUrl, discoverReelsTabUrls, openReelsTabWithTiles } };
|
package/scripts/lib/warm-web.js
CHANGED
|
@@ -167,15 +167,22 @@ async function visitSite(page, url, log) {
|
|
|
167
167
|
|
|
168
168
|
async function warmWeb({ page, cfg = {}, log }) {
|
|
169
169
|
const out = { ran: false, sites: 0, seconds: 0, last_origin: '' };
|
|
170
|
+
// EVERY exit logs, including the ones that do nothing. A probability branch
|
|
171
|
+
// that stays silent when it skips is indistinguishable from a branch that is
|
|
172
|
+
// never reached: measured 2026-09-22, four sessions in a row rolled a skip
|
|
173
|
+
// (0.4^4 = 2.6%) and the only way to tell bad luck from a dead code path was
|
|
174
|
+
// to re-run the module by hand on each machine. One line is cheaper.
|
|
175
|
+
const say = (m) => { if (typeof log === 'function') log('info', `[warm-web] ${m}`); };
|
|
170
176
|
const prob = cfg.warm_prob ?? 0.6;
|
|
171
|
-
if (!(prob > 0)
|
|
177
|
+
if (!(prob > 0)) { say(`tắt (warm_prob=${prob}) — không lướt web ngoài`); return out; }
|
|
178
|
+
if (!chance(prob)) { say(`bỏ qua lượt này (quay xác suất trượt, warm_prob=${prob})`); return out; }
|
|
172
179
|
|
|
173
180
|
// A MISSING list means "the API is older than this script" → use the built-in
|
|
174
181
|
// one. An EMPTY list means the caller said no sites, so browse none: a config
|
|
175
182
|
// that clears the list must not quietly fall back to six hardcoded domains.
|
|
176
183
|
const pool = (Array.isArray(cfg.warm_sites) ? cfg.warm_sites : DEFAULT_SITES)
|
|
177
184
|
.filter((u) => typeof u === 'string' && /^https?:\/\//i.test(u));
|
|
178
|
-
if (!pool.length) return out;
|
|
185
|
+
if (!pool.length) { say('danh sách site rỗng — không lướt web ngoài'); return out; }
|
|
179
186
|
|
|
180
187
|
const want = randInt(Math.max(1, cfg.warm_sites_min ?? 1), Math.max(1, cfg.warm_sites_max ?? 3));
|
|
181
188
|
const budgetSec = randInt(cfg.warm_sec_min ?? 60, Math.max(cfg.warm_sec_min ?? 60, cfg.warm_sec_max ?? 150));
|