channel-worker 2.5.110 → 2.5.112

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.
@@ -215,6 +215,7 @@ class CommandPoller {
215
215
  page_limit: payload.page_limit ?? 20,
216
216
  delay_ms: payload.delay_ms ?? 2500, // human-like pacing (anti-bot)
217
217
  filters: payload.filters || {},
218
+ log: (m) => console.log(m), // để thấy số tab đã dọn
218
219
  });
219
220
  if (res.status === 'needs_verify') {
220
221
  // Anti-bot bounce — DON'T fail/retry into more captchas. Report cleanly
@@ -304,6 +305,7 @@ class CommandPoller {
304
305
  const res = await scraper.generateOfferLink(conn.context, {
305
306
  items,
306
307
  subIds: Array.isArray(payload.sub_ids) ? payload.sub_ids : [],
308
+ log: (m) => console.log(m), // để thấy số tab đã dọn
307
309
  });
308
310
  if (res.status === 'needs_verify') {
309
311
  // Anti-bot bounce — báo sạch, KHÔNG thử lại để khỏi đâm thêm captcha.
@@ -206,6 +206,51 @@ function findAffiliatePage(pages) {
206
206
  return live.find((p) => urlPath(p.url()).startsWith('/offer')) || live[0] || null;
207
207
  }
208
208
 
209
+ // Tab nào là RÁC của các lượt trước — đóng được mà không mất gì.
210
+ //
211
+ // Chỉ nhận ba loại, cố ý hẹp: trang trắng, tab đang kẹt ở captcha/đăng nhập, và
212
+ // tab Shopee thừa ngoài cái mình sắp dùng. KHÔNG đụng tới tab của trang khác:
213
+ // profile này người vẫn mở tay để xác minh, đóng nhầm là cướp việc của họ.
214
+ function isJunkTab(url, keepUrl) {
215
+ if (url === keepUrl) return false;
216
+ if (!url || url === 'about:blank' || url === 'chrome://newtab/') return true;
217
+ if (isBlockedState(url, null)) return true; // captcha / buyer/login / is_from_login
218
+ try {
219
+ const h = new URL(url).hostname;
220
+ return h === 'affiliate.shopee.vn' || h === 'shopee.vn' || h === 'www.shopee.vn';
221
+ } catch { return false; }
222
+ }
223
+
224
+ // Dọn tab trước khi làm việc.
225
+ //
226
+ // 11/09 đo trên chính profile Shopee1: 16 tab, và tab VỊ TRÍ 0 là captcha cũ từ
227
+ // lượt quét trước — `pages().find(u.includes('affiliate.shopee.vn'))` vớ trúng
228
+ // nó nên máy một mực đòi xác minh trong khi 15 tab kia đăng nhập bình thường.
229
+ // `findAffiliatePage` đã vá chỗ CHỌN, nhưng tab rác vẫn dồn lại mỗi ngày: mỗi
230
+ // lượt bị đá sang captcha để lại một tab, Chrome chậm dần, và người mở profile
231
+ // ra nhìn thì rối.
232
+ //
233
+ // Giữ đúng MỘT tab affiliate (cái sắp dùng) và đóng phần còn lại. Không bao giờ
234
+ // đóng tab cuối cùng — context không còn trang nào thì Chrome của NST có thể tự
235
+ // tắt, mất luôn phiên đăng nhập vừa xác minh tay.
236
+ async function pruneTabs(ctx, { keep = null, log = null } = {}) {
237
+ const pages = ctx.pages() || [];
238
+ if (pages.length <= 1) return 0;
239
+ const keepUrl = keep ? keep.url() : (findAffiliatePage(pages)?.url() || null);
240
+ let closed = 0;
241
+ for (const p of pages) {
242
+ if (p === keep) continue;
243
+ let u = '';
244
+ try { u = p.url(); } catch { continue; }
245
+ if (!isJunkTab(u, keepUrl)) continue;
246
+ if ((ctx.pages() || []).length <= 1) break;
247
+ await p.close().catch(() => {});
248
+ closed++;
249
+ }
250
+ if (closed && log) log(`[shopee] đã đóng ${closed} tab cũ, còn ${(ctx.pages() || []).length}`);
251
+ return closed;
252
+ }
253
+
209
254
  // Apply the Flow-1 hard filters (criteria) to parsed rows. Defaults are lenient;
210
255
  // the controller passes the agreed thresholds (commission/price/sold/rating/imgs).
211
256
  function applyFilters(rows, f = {}) {
@@ -260,6 +305,7 @@ async function scrapeOffers(ctx, {
260
305
  delay_ms = 2500, // human-like pause between calls (anti-bot)
261
306
  filters = {},
262
307
  timeoutMs = 45000,
308
+ log = null,
263
309
  // legacy single-sort param (back-compat with the first build)
264
310
  sort_type = null,
265
311
  } = {}) {
@@ -284,8 +330,10 @@ async function scrapeOffers(ctx, {
284
330
  if (cat?.group && !filters.category_groups) filters = { ...filters, category_groups: [cat.group] };
285
331
  const listType = cat?.list_type || null;
286
332
 
287
- const pagesOpen = ctx.pages();
288
- let page = findAffiliatePage(pagesOpen);
333
+ let page = findAffiliatePage(ctx.pages());
334
+ // Dọn TRƯỚC khi mở thêm: tab rác của lượt trước không giúp gì cho lượt này,
335
+ // mà để lại thì lần sau còn nhiều hơn.
336
+ await pruneTabs(ctx, { keep: page, log }).catch(() => {});
289
337
  let opened = false;
290
338
  if (!page) { page = await ctx.newPage(); opened = true; }
291
339
  try {
@@ -372,7 +420,7 @@ function cleanSubId(v) {
372
420
  // Google Ads, không qua đường ghi nhận affiliate nên không ra hoa hồng).
373
421
  //
374
422
  // subIds: mảng tối đa 5 phần tử. Quy ước của mình: [kênh, video, đợt].
375
- async function generateOfferLink(ctx, { items = [], subIds = [], timeoutMs = 45000 } = {}) {
423
+ async function generateOfferLink(ctx, { items = [], subIds = [], timeoutMs = 45000, log = null } = {}) {
376
424
  const list = (items || [])
377
425
  .map((it) => ({ itemId: String(it.item_id || ''), shopId: Number(it.shop_id) || 0, trace: '' }))
378
426
  .filter((it) => it.itemId && it.shopId);
@@ -381,8 +429,8 @@ async function generateOfferLink(ctx, { items = [], subIds = [], timeoutMs = 450
381
429
  const subs = {};
382
430
  for (let i = 0; i < 5; i++) subs[`subId${i + 1}`] = cleanSubId(subIds[i]);
383
431
 
384
- const pagesOpen = ctx.pages();
385
- let page = findAffiliatePage(pagesOpen);
432
+ let page = findAffiliatePage(ctx.pages());
433
+ await pruneTabs(ctx, { keep: page, log }).catch(() => {});
386
434
  let opened = false;
387
435
  if (!page) { page = await ctx.newPage(); opened = true; }
388
436
  try {
@@ -441,4 +489,4 @@ function parseProductUrl(url = '') {
441
489
  return null;
442
490
  }
443
491
 
444
- module.exports = { ingestProduct, scrapeOffers, generateOfferLink, cleanSubId, parseProductUrl, parseOfferRow, parsePdpItem, categoryGroup, pctToNumber, AFFILIATE_CATEGORIES, applyFilters, isBlockedState, findAffiliatePage };
492
+ module.exports = { ingestProduct, scrapeOffers, generateOfferLink, cleanSubId, parseProductUrl, parseOfferRow, parsePdpItem, categoryGroup, pctToNumber, AFFILIATE_CATEGORIES, applyFilters, isBlockedState, findAffiliatePage, pruneTabs, isJunkTab };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "channel-worker",
3
- "version": "2.5.110",
3
+ "version": "2.5.112",
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": {
@@ -196,15 +196,39 @@ const RANGE_LABEL = {
196
196
  // qua", kiểu B là "Số liệu chia nhỏ về lượt xem" rồi "26.313 -91% Lượt xem"
197
197
  // (không có chữ "Tổng số"). Bám vào dòng đó là ~10/44 kênh kiểu B bị coi như
198
198
  // trang trống — đúng ca "Má hồng phai" 2026-09-06.
199
+ //
200
+ // CHỈ XÉT PHẦN TỬ ĐANG HIỆN (vá 14/09). Bộ chọn khoảng thời gian để sẵn trong
201
+ // DOM mọi lựa chọn của nó, trong đó có "Hôm nay", và mấy phần tử ấy đứng TRƯỚC
202
+ // chip thật. Bản cũ lấy phần tử khớp ĐẦU TIÊN nên đọc trúng lựa chọn ẩn: trang
203
+ // đã ở "7 ngày qua" mà worker vẫn kết luận chip kẹt "Hôm nay" rồi vứt cả danh
204
+ // sách bài. Dump lượt 13:47 ngày 14/09 (kênh Tóc xinh) nói thẳng:
205
+ // label[7..11] span/div y=0 w=0 "Hôm nay" ← ẩn
206
+ // label[12..18] div y=182 w=297 "7 ngày qua: 7 Tháng 9 - 13 Tháng 9"
207
+ // label[19..24] div y=256 w=366 "8.672 -92% Tổng số lượt xem…"
208
+ // Số 7 ngày đã nằm sẵn trên trang. Hôm 14/09 lỗi này ăn 9/74 kênh cả ngày, và
209
+ // thử lại KHÔNG cứu được (9/10 lượt chạy tay lặp y nguyên) vì chẳng có gì kẹt
210
+ // để mà nguôi — phần tử ẩn thì lần nào cũng ẩn.
211
+ //
212
+ // Đo hiển thị bằng getBoundingClientRect (display:none → 0×0) chứ không phải
213
+ // offsetParent: chip nằm trong khối position:fixed thì offsetParent là null
214
+ // dù đang hiện rành rành. Phần tử cuộn khỏi màn hình vẫn có kích thước thật.
215
+ function pickRangeLabelInPage() {
216
+ const clean = (v) => String(v || '').replace(/\s+/g, ' ').trim();
217
+ const RE = /^(Hôm nay$|Today$|Tùy chỉnh:|Custom:|Năm nay:|This year:|\d+ ngày qua:|Last \d+ days:)/i;
218
+ for (const e of document.querySelectorAll('div[role="button"],div,span')) {
219
+ const t = clean(e.innerText);
220
+ if (t.length >= 70 || !RE.test(t)) continue;
221
+ const r = e.getBoundingClientRect();
222
+ if (!r || r.width <= 0 || r.height <= 0) continue;
223
+ return t;
224
+ }
225
+ return '';
226
+ }
227
+
199
228
  async function currentRangeLabel(page) {
200
- return page.evaluate(() => {
201
- const clean = (v) => String(v || '').replace(/\s+/g, ' ').trim();
202
- for (const e of document.querySelectorAll('div[role="button"],div,span')) {
203
- const t = clean(e.innerText);
204
- 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;
205
- }
206
- return '';
207
- }).catch(() => '');
229
+ // Hàm phải TỰ CHỨA: Playwright serialize nó rồi chạy trong trang, closure
230
+ // của module không đi theo.
231
+ return page.evaluate(pickRangeLabelInPage).catch(() => '');
208
232
  }
209
233
 
210
234
  // Tổng lượt xem của khoảng đang hiện, đọc được CẢ HAI kiểu giao diện.
@@ -436,7 +460,10 @@ async function dumpInsightsFailure(page, tag, log) {
436
460
  if (!t || t.length >= 70) continue;
437
461
  if (!/(Hôm nay|Today|Tùy chỉnh|Custom|ngày qua|Last \d+ days|Năm nay|This year)/i.test(t)) continue;
438
462
  const r = e.getBoundingClientRect();
439
- out.push(`${e.tagName.toLowerCase()}[${e.getAttribute('role') || ''}] y=${Math.round(r.y)} w=${Math.round(r.width)} "${t}"`);
463
+ // Ghi thẳng chữ ẨN: dump 14/09 in "y=0 w=0" cho lựa chọn ẩn của bộ
464
+ // chọn, phải soi ba lượt mới nhận ra đó không phải chip thật.
465
+ const hidden = (r.width <= 0 || r.height <= 0) ? ' ẨN' : '';
466
+ out.push(`${e.tagName.toLowerCase()}[${e.getAttribute('role') || ''}] y=${Math.round(r.y)} w=${Math.round(r.width)}${hidden} "${t}"`);
440
467
  if (out.length >= 25) break;
441
468
  }
442
469
  return out;
@@ -564,4 +591,4 @@ async function run({ page, payload, log }) {
564
591
  return { ok: true, mode, tiles: tiles.length, written: d?.written || 0, matched: d?.matched || 0, rising: d?.rising || 0, alerts: d?.alerts || 0, followers };
565
592
  }
566
593
 
567
- module.exports = { run, __testables: { parseViews, scrapeFollowers, parseExact, parseVnPostTime, runInsights, dumpInsightsFailure, discoverReelsTabUrl, discoverReelsTabUrls, openReelsTabWithTiles } };
594
+ module.exports = { run, __testables: { parseViews, scrapeFollowers, parseExact, parseVnPostTime, runInsights, dumpInsightsFailure, pickRangeLabelInPage, discoverReelsTabUrl, discoverReelsTabUrls, openReelsTabWithTiles } };