channel-worker 2.5.111 → 2.5.113

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.111",
3
+ "version": "2.5.113",
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": {
@@ -25,6 +25,25 @@
25
25
  const { assertAccountUsable, readPublishBlock } = require('./lib/fb-guard');
26
26
  const { humanClick, humanType, pause, randInt } = require('./lib/human');
27
27
 
28
+ // Chụp màn hình lúc trượt — mượn của upload_facebook, nhưng require MUỘN và có
29
+ // đường lùi.
30
+ //
31
+ // Vì sao không require ở đầu file: `lib/playwright-runner` chỉ xoá require.cache
32
+ // của ĐÚNG script được gọi. Chép nóng cả hai file vào máy thì bản
33
+ // comment_facebook mới được nạp lại ngay, còn upload_facebook có thể vẫn là bản
34
+ // CŨ còn nằm trong cache từ lượt đăng trước — bản cũ chưa export `dumpFailure`,
35
+ // require ở đầu file là cả script chết ngay dòng đầu. Chụp được ảnh thì tốt,
36
+ // không chụp được thì thôi, đừng vì nó mà hỏng cả lượt thả bình luận.
37
+ async function dumpFailure(page, tag, log) {
38
+ try {
39
+ const f = require('./upload_facebook').__testables?.dumpFailure;
40
+ if (typeof f === 'function') await f(page, tag, log);
41
+ else log('info', '[fb-cmt] bản upload_facebook đang nạp chưa có dumpFailure — bỏ qua bước chụp màn hình');
42
+ } catch (e) {
43
+ log('info', `[fb-cmt] không chụp được màn hình: ${String(e.message || e).slice(0, 80)}`);
44
+ }
45
+ }
46
+
28
47
  const NAV_TIMEOUT_MS = 45_000;
29
48
  // Bình luận hiện ra gần như tức thì, nhưng lượt đầu sau khi mở bài thì Facebook
30
49
  // còn đang dựng khung bình luận. 20 giây là dư cho trường hợp chậm nhất đo được
@@ -94,6 +113,34 @@ async function openCommentsIfNeeded(page, log) {
94
113
  // Facebook đặt tên tác giả ngay trong aria-label của khối bình luận
95
114
  // ("Bình luận của <Tên>" / "Comment by <Name>") — đọc thẳng ở đó chắc hơn dò
96
115
  // anchor đầu tiên, vì trong khối còn có link tới bài, tới trang được nhắc tên.
116
+ // ĐẾM số chỗ trên trang có chứa chuỗi link, BỎ QUA ô soạn.
117
+ //
118
+ // Đây là cách xác minh chắc hơn hẳn việc dò cấu trúc bình luận: mình đo TRƯỚC
119
+ // khi gõ và ĐO LẠI sau khi gửi, thấy tăng lên là bình luận đã lên. Không cần
120
+ // biết Facebook dựng khung bình luận bằng thẻ gì, cũng không vỡ khi họ đổi.
121
+ //
122
+ // Vì sao phải đổi (15/09): bản đầu đòi khối bình luận mang nhãn
123
+ // `aria-label="Bình luận của <Tên>"` — đó là cấu trúc bài trên BẢNG TIN. Trang
124
+ // reel dựng khác, nên 5/5 lượt thả thật đều báo "không đọc lại thấy" trong khi
125
+ // chủ tịch mở bài ra thì bình luận NẰM ĐÓ. Suýt nữa thì mỗi bài hai bình luận
126
+ // vì nút hiện "Thả lại".
127
+ async function countLinkOnPage(page, link) {
128
+ return page.evaluate((needle) => {
129
+ let n = 0;
130
+ const walk = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
131
+ while (walk.nextNode()) {
132
+ const t = walk.currentNode;
133
+ if (!t.nodeValue || !t.nodeValue.includes(needle)) continue;
134
+ // Chữ mình vừa gõ trong ô soạn không tính.
135
+ if (t.parentElement && t.parentElement.closest("[contenteditable='true']")) continue;
136
+ n++;
137
+ }
138
+ return n;
139
+ }, link).catch(() => -1);
140
+ }
141
+
142
+ // Cố lấy TÊN người bình luận + permalink. Best-effort thuần: không lấy được thì
143
+ // để trống chứ KHÔNG coi là thất bại — xác minh đã do phép đếm ở trên lo.
97
144
  async function findPostedComment(page, link) {
98
145
  return page.evaluate((needle) => {
99
146
  const out = { found: false, author: '', comment_url: '' };
@@ -121,16 +168,22 @@ async function findPostedComment(page, link) {
121
168
  }, link).catch(() => ({ found: false, author: '', comment_url: '' }));
122
169
  }
123
170
 
124
- async function waitForPostedComment(page, link, log, timeoutMs = VERIFY_TIMEOUT_MS) {
171
+ async function waitForPostedComment(page, link, log, timeoutMs = VERIFY_TIMEOUT_MS, baseline = 0) {
125
172
  const deadline = Date.now() + timeoutMs;
126
- let last = { found: false, author: '', comment_url: '' };
173
+ let meta = { found: false, author: '', comment_url: '' };
127
174
  while (Date.now() < deadline) {
128
- last = await findPostedComment(page, link);
129
- if (last.found) return last;
175
+ const n = await countLinkOnPage(page, link);
176
+ // Tăng so với lúc trước khi gõ = bình luận đã lên. Đây mới là bằng chứng;
177
+ // tên tác giả và permalink chỉ là thông tin kèm theo.
178
+ if (n > baseline) {
179
+ meta = await findPostedComment(page, link);
180
+ return { ...meta, found: true, count: n };
181
+ }
130
182
  await page.waitForTimeout(1200);
131
183
  }
132
- log('warn', `[fb-cmt] hết ${Math.round(timeoutMs / 1000)}s vẫn chưa đọc lại thấy bình luận mang link`);
133
- return last;
184
+ const n = await countLinkOnPage(page, link);
185
+ log('warn', `[fb-cmt] hết ${Math.round(timeoutMs / 1000)}s: số chỗ có link vẫn là ${n} (trước khi gõ ${baseline}) — chưa thấy bình luận lên`);
186
+ return { ...meta, found: false, count: n };
134
187
  }
135
188
 
136
189
  // ─── ghim ───────────────────────────────────────────────────────────────────
@@ -217,6 +270,11 @@ async function run({ page, payload = {}, log }) {
217
270
  throw new Error(`Không tìm thấy ô soạn bình luận trên ${postUrl} (đã thử ${BOX_SELECTORS.length} nhãn) — nhiều khả năng bài tắt bình luận hoặc Facebook đổi giao diện`);
218
271
  }
219
272
 
273
+ // Đo NỀN trước khi gõ: link có thể đã nằm sẵn trong caption, hoặc bài đã
274
+ // được thả bình luận từ lượt trước.
275
+ const baseline = Math.max(await countLinkOnPage(page, link), 0);
276
+ if (baseline > 0) log('info', `[fb-cmt] link đã xuất hiện ${baseline} chỗ trên bài từ trước — chỉ tính là xong khi tăng thêm`);
277
+
220
278
  await box.scrollIntoViewIfNeeded({ timeout: 5000 }).catch(() => {});
221
279
  await humanClick(page, box).catch(async () => { await box.click({ timeout: 5000 }); });
222
280
  await pause(page, 900);
@@ -240,9 +298,12 @@ async function run({ page, payload = {}, log }) {
240
298
  throw new Error(`FB_COMMENT_RATE_LIMITED: Facebook chặn bình luận vì tần suất/nghi spam — bình luận KHÔNG được nhận. Chuỗi bắt được: "${block}". Dừng thả link trên kênh này cho tới khi người kiểm tra.`);
241
299
  }
242
300
 
243
- const found = await waitForPostedComment(page, link, log);
301
+ const found = await waitForPostedComment(page, link, log, VERIFY_TIMEOUT_MS, baseline);
244
302
  if (!found.found) {
245
303
  await assertAccountUsable(page, log);
304
+ // Chụp lại màn hình: lần trước trượt xác minh mà không có gì để soi, phải
305
+ // đợi chủ tịch tự mở Facebook mới biết bình luận có lên hay không.
306
+ await dumpFailure(page, 'cmt-verify', log);
246
307
  // KHÔNG ném lỗi kiểu "có thể đã thả": trả về verified=false để API ghi
247
308
  // 'failed' kèm nguyên văn. Ném lỗi ở đây thì lượt chạy lại sẽ gõ thêm một
248
309
  // bình luận nữa, mà bình luận trước có thể đã nằm đó rồi.
@@ -268,4 +329,4 @@ async function run({ page, payload = {}, log }) {
268
329
  }
269
330
 
270
331
  module.exports = { run };
271
- module.exports.__testables = { findPostedComment, findCommentBox, BOX_SELECTORS, sel };
332
+ module.exports.__testables = { findPostedComment, countLinkOnPage, waitForPostedComment, findCommentBox, BOX_SELECTORS, sel };
@@ -3425,6 +3425,9 @@ module.exports = { run };
3425
3425
  // Exposed so the overlay-click escalation can be exercised against a synthetic
3426
3426
  // page (toast covering the CTA) without driving the whole upload flow.
3427
3427
  module.exports.__testables = {
3428
+ // Dùng chung cho comment_facebook: script nào trượt giữa chừng cũng cần chụp
3429
+ // lại màn hình, viết bản thứ hai là sớm muộn hai bên lệch nhau.
3430
+ dumpFailure,
3428
3431
  resilientClick,
3429
3432
  confirmPublishDialog,
3430
3433
  diffNewReelIds,