channel-worker 2.5.126 → 2.5.128

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.
@@ -292,13 +292,30 @@ async function scrapeOffers(ctx, {
292
292
  category = null, // key in AFFILIATE_CATEGORIES (e.g. 'women_clothes')
293
293
  match_id = null, // explicit category id override
294
294
  // Tìm theo TỪ KHOÁ thay vì duyệt tab ngành hàng. Cùng endpoint, khác tham số:
295
- // `list_type=0&keyword=<kw>&filter_types=2` (đo 09/09/2026 — ghi chú cũ bảo
296
- // "cần signed header" là sai/lạc hậu).
295
+ // `list_type=0&keyword=<kw>` (đo 09/09/2026 — ghi chú cũ bảo "cần signed
296
+ // header" là sai/lạc hậu). Bản đầu còn kèm `filter_types=2`; 22/09 đo ra nó
297
+ // cắt mất 5/7 món và giao diện thật không hề gửi — xem ghi chú ở tham số
298
+ // `filter_types` bên dưới.
297
299
  //
298
300
  // Đây là thứ gỡ bế tắc kho bão hoà: 6 tab ngành hàng luôn trả về top N giống
299
301
  // nhau nên kho đứng yên ở 34 sản phẩm. Từ khoá thì mở ra đúng món hợp với
300
302
  // từng video.
301
303
  keyword = null,
304
+ // Bộ lọc của chính trang affiliate. Để null = KHÔNG lọc, đúng như giao diện.
305
+ //
306
+ // Đo 22/09/2026 trên profile Shopee1, từ khoá "vợt bắt hải sản":
307
+ // filter_types=2 → 2 món ← thứ mình gắn cứng từ 09/09
308
+ // không filter → 7 món ← thứ GIAO DIỆN thật sự gọi
309
+ // filter_types=0/1/3 → 0 món
310
+ // `filter_types=2` = chỉ lấy món người bán TỰ CỘNG hoa hồng
311
+ // (`seller_commission_rate > 0`): 2 món lọt có seller 7% và 4%, 5 món bị
312
+ // giấu đều seller 0% nhưng default 7%. Tức nó vứt phần lớn thị trường.
313
+ //
314
+ // Bắt tận tay bằng cách cho giao diện tự tìm rồi nghe request nó gửi:
315
+ // /api/v3/offer/product/list?list_type=0&keyword=…&sort_type=1
316
+ // &page_offset=0&page_limit=20&client_type=1
317
+ // — không có filter_types. Mình tự thêm vào, và tự làm hẹp kết quả.
318
+ filter_types = null,
302
319
  sort_types = [2], // [2]=commission; pass [2,3] to merge commission+sales
303
320
  pages = 2,
304
321
  page_limit = 20,
@@ -357,7 +374,7 @@ async function scrapeOffers(ctx, {
357
374
  // thì Shopee bỏ qua keyword và mình tưởng đang tìm mà thật ra vẫn
358
375
  // đang duyệt tab cũ.
359
376
  const cat = q.kw
360
- ? `&list_type=0&keyword=${encodeURIComponent(q.kw)}&filter_types=2`
377
+ ? `&list_type=0&keyword=${encodeURIComponent(q.kw)}${q.ft != null ? `&filter_types=${q.ft}` : ''}`
361
378
  : (q.listType
362
379
  ? `&list_type=${q.listType}`
363
380
  : (q.mid ? `&list_type=3&match_type=2&match_id=${q.mid}` : '&list_type=0'));
@@ -366,7 +383,7 @@ async function scrapeOffers(ctx, {
366
383
  const r = await fetch(url, { credentials: 'include' });
367
384
  return { status: r.status, json: await r.json().catch(() => null) };
368
385
  } catch (e) { return { status: 0, error: String(e.message) }; }
369
- }, { st, offset, page_limit, mid, listType, kw });
386
+ }, { st, offset, page_limit, mid, listType, kw, ft: filter_types });
370
387
 
371
388
  if (isBlockedState(page.url(), res) || res.json?.code === 90309999) {
372
389
  return { status: 'needs_verify', products: [...byId.values()], raw_count: rawCount, reason: `blocked mid-scrape (http ${res.status}, code ${res.json?.code})` };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "channel-worker",
3
- "version": "2.5.126",
3
+ "version": "2.5.128",
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": {
@@ -1,20 +1,27 @@
1
- // Thả link affiliate xuống BÌNH LUẬN của một bài Facebook đã đăng, rồi ghim.
1
+ // Thả link affiliate xuống BÌNH LUẬN của một bài Facebook đã đăng.
2
2
  //
3
3
  // Vì sao là bình luận chứ không phải caption: bài đang lên view thì không ai đi
4
4
  // sửa caption của nó — sửa caption là bài quay lại hàng đợi xét duyệt của
5
- // Facebook giữa lúc nó đang được phân phối. Bình luận thì ghim được, sửa được,
6
- // xoá được, không đụng vào bài.
5
+ // Facebook giữa lúc nó đang được phân phối. Bình luận thì sửa được, xoá được,
6
+ // không đụng vào bài.
7
7
  //
8
8
  // Chạy trên chính profile NST của kênh (máy đăng bài) chứ không phải máy
9
- // Shopee: bình luận phải mang danh Page thì mới ghim được, và một link lạ dưới
10
- // bài của mình do tài khoản khác thả thì người xem đọc ra là spam.
9
+ // Shopee: bình luận phải mang danh Page — Facebook mới gắn nhãn "Author" và ưu
10
+ // tiên hiển thị nó, còn một link lạ dưới bài của mình do tài khoản khác thả thì
11
+ // người xem đọc ra là spam.
11
12
  //
12
- // Payload: { post_url, text, link, pin?, idea_id?, channel_id? }
13
+ // Payload: { post_url, text, link, idea_id?, channel_id? }
13
14
  // post_url: link bài đã XÁC MINH (API chặn sẵn ô rỗng — services/AffComment)
14
15
  // text: nguyên câu bình luận sẽ gõ, đã gồm link
15
16
  // link: riêng chuỗi link, dùng để ĐỌC LẠI xác minh — xem `verified`
16
17
  //
17
- // Trả về { verified, comment_url, commented_as, pinned, post_url }.
18
+ // Trả về { verified, comment_url, commented_as, post_url }.
19
+ //
20
+ // KHÔNG có bước ghim. Facebook không cho ghim bình luận dưới reel ở giao diện
21
+ // web — chủ tịch mở tận nơi kiểm 21/09, menu ⋯ của bình luận không có mục đó,
22
+ // kể cả khi bình luận đã mang nhãn "Author" (tức của chính chủ bài). Bản cũ
23
+ // vẫn đi dò nút ấy mỗi lượt rồi ghi một dòng log nghe như mình làm hỏng việc.
24
+ // Bỏ luôn: bình luận của tác giả bài vốn được Facebook ưu tiên hiển thị.
18
25
  //
19
26
  // ⚠️ LUẬT LÕI CỦA SCRIPT NÀY: `verified` chỉ được true khi đã đọc lại thấy một
20
27
  // bình luận THẬT trên trang mang đúng chuỗi link. Gõ xong + bấm Enter KHÔNG
@@ -191,16 +198,28 @@ async function findPostedComment(page, link) {
191
198
  if (node.querySelector("[contenteditable='true']") && !node.getAttribute('aria-label')) continue;
192
199
  const label = node.getAttribute('aria-label') || '';
193
200
  const m = /(?:Bình luận của|Comment by)\s+(.+)$/i.exec(label.trim());
194
- // Không có aria-label dạng bình luận = nhiều khả năng đây là khối BÀI
195
- // chứa chuỗi link trong caption, không phải bình luận.
196
- if (!m) continue;
197
- out.found = true;
198
- out.author = m[1].trim().slice(0, 120);
201
+ // Khối bình luận KHÔNG phải lúc nào cũng mang aria-label "Bình luận của
202
+ // <Tên>" — giao diện reel không dùng nhãn đó, nên mọi lượt thả reel đều
203
+ // về `commented_as` rỗng và `comment_url` rỗng (21/09). Bằng chứng chắc
204
+ // chắn khác, không phụ thuộc chữ nghĩa giao diện: khối có link permalink
205
+ // mang `comment_id=`. Khối BÀI không bao giờ có thứ đó, nên vẫn né được
206
+ // đúng cái bẫy mà nhãn kia sinh ra để né — caption bài chứa chuỗi link.
199
207
  const a = node.querySelector("a[href*='comment_id=']");
208
+ if (!m && !a) continue;
209
+ out.found = true;
200
210
  if (a) {
201
211
  const href = a.getAttribute('href') || '';
202
212
  out.comment_url = href.startsWith('http') ? href : `https://www.facebook.com${href}`;
203
213
  }
214
+ if (m) {
215
+ out.author = m[1].trim().slice(0, 120);
216
+ } else {
217
+ // Facebook gắn nhãn "Author"/"Tác giả" cho bình luận của chính chủ bài
218
+ // — với luồng này thì đó CHÍNH LÀ điều cần biết: bình luận mang danh
219
+ // Page chứ không phải một tài khoản lạ đi rải link dưới bài của kênh.
220
+ const badge = /(^|\s)(Author|Tác giả)(\s|$)/i.test(node.innerText || '');
221
+ out.author = badge ? 'Author' : '';
222
+ }
204
223
  return out;
205
224
  }
206
225
  return out;
@@ -225,46 +244,6 @@ async function waitForPostedComment(page, link, log, timeoutMs = VERIFY_TIMEOUT_
225
244
  return { ...meta, found: false, count: n };
226
245
  }
227
246
 
228
- // ─── ghim ───────────────────────────────────────────────────────────────────
229
-
230
- // Ghim là việc CÓ CŨNG TỐT, không có cũng xong: link đã nằm dưới bài rồi. Nên
231
- // mọi lỗi ở đây chỉ ghi log và trả false, KHÔNG ném — hỏng bước ghim mà đánh
232
- // cả lệnh thành thất bại thì lượt chạy lại sẽ thả bình luận thứ hai.
233
- async function pinComment(page, link, log) {
234
- try {
235
- const menuBtn = await page.evaluateHandle((needle) => {
236
- const nodes = [...document.querySelectorAll("[role='article']")];
237
- for (const node of nodes) {
238
- if (!(node.innerText || '').includes(needle)) continue;
239
- if (!/(Bình luận của|Comment by)/i.test(node.getAttribute('aria-label') || '')) continue;
240
- const btn = [...node.querySelectorAll("[role='button'], [aria-haspopup='menu']")]
241
- .find((b) => /hành động|tùy chọn|tuỳ chọn|options|actions/i.test(b.getAttribute('aria-label') || ''));
242
- if (btn) { btn.scrollIntoView({ block: 'center' }); return btn; }
243
- }
244
- return null;
245
- }, link);
246
- const el = menuBtn.asElement();
247
- if (!el) { log('info', '[fb-cmt] không thấy nút ⋯ của bình luận — bỏ qua bước ghim'); return false; }
248
- await el.click({ timeout: 5000 }).catch(() => {});
249
- await pause(page, 1200);
250
-
251
- const item = page.locator("[role='menuitem'], [role='menu'] [role='button']")
252
- .filter({ hasText: /Ghim bình luận|Ghim|Pin comment|Pin/i }).first();
253
- if (!(await item.isVisible().catch(() => false))) {
254
- log('info', '[fb-cmt] menu không có mục Ghim (thường là chưa bình luận bằng danh Page) — bỏ qua');
255
- await page.keyboard.press('Escape').catch(() => {});
256
- return false;
257
- }
258
- await item.click({ timeout: 5000 }).catch(() => {});
259
- await pause(page, 1500);
260
- log('info', '[fb-cmt] đã bấm Ghim bình luận');
261
- return true;
262
- } catch (e) {
263
- log('warn', `[fb-cmt] ghim hỏng (bỏ qua, link vẫn nằm dưới bài): ${e.message.slice(0, 120)}`);
264
- return false;
265
- }
266
- }
267
-
268
247
  // ─── thân ───────────────────────────────────────────────────────────────────
269
248
 
270
249
  async function run({ page, payload = {}, log }) {
@@ -357,23 +336,19 @@ async function run({ page, payload = {}, log }) {
357
336
  // '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
358
337
  // bình luận nữa, mà bình luận trước có thể đã nằm đó rồi.
359
338
  log('warn', '[fb-cmt] gõ và gửi xong nhưng KHÔNG đọc lại thấy bình luận — coi như chưa thả, người phải vào kiểm');
360
- return { verified: false, post_url: postUrl, comment_url: '', commented_as: '', pinned: false };
339
+ return { verified: false, post_url: postUrl, comment_url: '', commented_as: '' };
361
340
  }
362
341
 
363
342
  log('info', `[fb-cmt] xác minh được bình luận — danh nghĩa "${found.author}"${found.comment_url ? ` · ${found.comment_url}` : ''}`);
364
343
 
365
- let pinned = false;
366
- if (payload.pin !== false) pinned = await pinComment(page, link, log);
367
-
368
344
  return {
369
345
  verified: true,
370
346
  post_url: postUrl,
371
347
  comment_url: found.comment_url || '',
372
- // Tên hiện trên bình luận. Phải là tên Page — ra tên người thì profile đang
373
- // bình luận bằng tài khoản cá nhân, link nằm dưới bài mà không ghim được và
374
- // người xem đọc ra là spam của người lạ.
348
+ // Tên hiện trên bình luận, hoặc 'Author' khi Facebook gắn nhãn tác giả.
349
+ // Ra tên người lạ nghĩa là profile đang bình luận bằng tài khoản cá nhân —
350
+ // người xem đọc ra là spam dưới bài của kênh.
375
351
  commented_as: found.author || '',
376
- pinned,
377
352
  };
378
353
  }
379
354