channel-worker 2.5.145 → 2.5.146

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.
@@ -397,6 +397,9 @@ class CommandPoller {
397
397
  const res = await scraper.generateOfferLink(context, {
398
398
  items,
399
399
  subIds: Array.isArray(payload.sub_ids) ? payload.sub_ids : [],
400
+ // API bật `need_product` khi món CHƯA có tên ở đâu cả (món dán link
401
+ // tay, chưa nhập kho). Món đã biết tên thì không tốn thêm request.
402
+ productIds: payload.need_product ? items.map((it) => String(it.item_id || '')) : [],
400
403
  log: (m) => console.log(m), // để thấy số tab đã dọn
401
404
  });
402
405
  if (res.status === 'needs_verify') {
@@ -411,12 +414,24 @@ class CommandPoller {
411
414
  return { status: 'failed', error: String(res.reason || 'không lấy được link').slice(0, 500) };
412
415
  }
413
416
  console.log(`[shopee] lấy được ${res.links.length} link (sub: ${JSON.stringify(res.sub_ids)})`);
414
- await this.api.affiliateLinkResult(res.links, {
417
+ // Món vừa đọc được thông tin → vào kho TRƯỚC khi báo link: `workerLink`
418
+ // bên API đọc tên món từ kho để AI viết câu bình luận, và bảng "Món đã
419
+ // dùng" bù giá/lãi/ảnh cũng từ kho. Bỏ `affiliate_link` (parseOfferRow
420
+ // để chuỗi rỗng) — gửi nguyên là xoá trắng link cũ của món trong kho.
421
+ const products = (res.products || []).map(({ affiliate_link: _link, ...p }) => p);
422
+ if (products.length) {
423
+ await this.api.upsertAffiliateProducts(products, { user_id: command.user_id })
424
+ .catch((e) => console.warn(`[shopee] ghi kho món vừa đọc lỗi: ${e.message}`));
425
+ console.log(`[shopee] đọc được tên ${products.length} món: ${products.map((p) => p.name.slice(0, 40)).join(' | ')}`);
426
+ }
427
+ const nameOf = new Map(products.map((p) => [p.item_id, p.name]));
428
+ const links = res.links.map((l) => (nameOf.get(l.item_id) ? { ...l, name: nameOf.get(l.item_id) } : l));
429
+ await this.api.affiliateLinkResult(links, {
415
430
  user_id: command.user_id,
416
431
  sub_ids: res.sub_ids,
417
432
  idea_id: payload.idea_id || null,
418
433
  });
419
- return { status: 'done', result: { links: res.links, sub_ids: res.sub_ids } };
434
+ return { status: 'done', result: { links, sub_ids: res.sub_ids } };
420
435
  },
421
436
  });
422
437
  }
@@ -146,6 +146,27 @@ function parseOfferRow(row) {
146
146
  };
147
147
  }
148
148
 
149
+ // `data` của /api/v3/offer/product?item_id=<id> — trang chi tiết MỘT món trên
150
+ // affiliate.shopee.vn. Thẻ món (`batch_item_for_item_card_full`) cùng dạng với
151
+ // một dòng của offer list, chỉ khác chỗ để tỉ lệ hoa hồng: ở đây
152
+ // `commission_rate` là một OBJECT gom mọi loại tỉ lệ, còn offer list để
153
+ // `default_commission_rate` nằm thẳng trên dòng. Đưa nguyên `data` vào
154
+ // parseOfferRow là `pctToNumber(object)` ra 0 — món có tên mà lãi 0đ.
155
+ // Đo 30/09/2026 trên Shopee1: 25451550752 giá 154.999đ, default 9%, Shopee
156
+ // ghi "commission":"₫13.950" — khớp đúng phép tính của parseOfferRow.
157
+ function parseOfferDetail(data) {
158
+ if (!data || typeof data !== 'object') return null;
159
+ const cr = data.commission_rate && typeof data.commission_rate === 'object' ? data.commission_rate : {};
160
+ const row = parseOfferRow({
161
+ ...data,
162
+ commission_rate: undefined,
163
+ default_commission_rate: cr.default_commission_rate,
164
+ max_commission_rate: cr.max_commission_rate,
165
+ seller_commission_rate: cr.seller_commission_rate,
166
+ });
167
+ return row.item_id && row.shop_id && row.name ? row : null;
168
+ }
169
+
149
170
  // Affiliate offer-page category tabs → match_id (read from rc-tabs node keys,
150
171
  // confirmed live). Tier-1 fashion focus first; the offer page has NO dedicated
151
172
  // Shoes/Bags/Accessories tab → those come later via keyword search.
@@ -517,8 +538,52 @@ function linkFailureReason(res, wanted = 0) {
517
538
  return bits.join(' · ').slice(0, 500);
518
539
  }
519
540
 
541
+ // Tên + giá + hoa hồng + ảnh của từng món, đọc trên CHÍNH tab affiliate vừa
542
+ // lấy link.
543
+ //
544
+ // Vì sao cần: GQL lấy link chỉ trả itemId/shopId/link, không có tên. Món dán
545
+ // link rút gọn s.shopee.vn (đường gắn chính từ 25/09) lại chưa nằm trong kho,
546
+ // nên cứ ~1/3 món mang tên rỗng mãi — bảng "Món đã dùng" chỉ hiện được URL,
547
+ // giá/lãi 0đ, không ảnh (đo prod 29/09: 40/130 món).
548
+ //
549
+ // Đây đúng là request trang chi tiết `affiliate.shopee.vn/offer/product_offer/
550
+ // <item_id>` tự gọi khi người mở món đó ra bấm Get Link — không phải đường lạ.
551
+ // Đo 30/09/2026 trên Shopee1: fetch tay trong tab /offer trả 200 code 0 cho
552
+ // 2/2 món, không bị đá sang captcha.
553
+ //
554
+ // LỖI MỀM toàn phần: không bao giờ ném, hỏng món nào thì bỏ món đó. Chỗ gọi
555
+ // đã cầm link thật trong tay — mất tên là chuyện nhỏ, mất link là mất lượt bán.
556
+ // Gặp mã chặn chống bot thì DỪNG luôn, không gõ tiếp vào cửa đang đóng.
557
+ async function readOfferProducts(page, itemIds = [], { evalTimeoutMs = EVAL_TIMEOUT_MS, pauseMs = 1200, log = null } = {}) {
558
+ const ids = [...new Set((itemIds || []).map((x) => String(x || '')).filter((x) => /^\d+$/.test(x)))];
559
+ const products = [];
560
+ for (const id of ids) {
561
+ try {
562
+ if (pauseMs) await sleep(pauseMs);
563
+ const res = await evaluateWithin(page, async (itemId) => {
564
+ try {
565
+ const r = await fetch(`/api/v3/offer/product?item_id=${encodeURIComponent(itemId)}`, { credentials: 'include' });
566
+ return { status: r.status, json: await r.json().catch(() => null) };
567
+ } catch (e) { return { status: 0, error: String(e.message) }; }
568
+ }, id, evalTimeoutMs);
569
+ if (isAntiBotBody(res?.json) || isBlockedState(page.url(), res)) {
570
+ if (log) log(`[shopee] đọc tên món ${id}: bị chặn (http ${res?.status}) — dừng, link vẫn giữ`);
571
+ break;
572
+ }
573
+ const p = res?.status === 200 && res.json?.code === 0 ? parseOfferDetail(res.json.data) : null;
574
+ if (p && p.item_id === id) products.push(p);
575
+ else if (log) log(`[shopee] đọc tên món ${id}: không ra (http ${res?.status}, code ${res?.json?.code})`);
576
+ } catch (e) {
577
+ if (log) log(`[shopee] đọc tên món ${id} lỗi: ${e.message}`);
578
+ }
579
+ }
580
+ return products;
581
+ }
582
+
520
583
  // subIds: mảng tối đa 5 phần tử. Quy ước của mình: [kênh, video, đợt].
521
- async function generateOfferLink(ctx, { items = [], subIds = [], timeoutMs = 45000, evalTimeoutMs = EVAL_TIMEOUT_MS, log = null } = {}) {
584
+ // productIds: món nào cần đọc thêm tên/giá/hoa hồng (xem readOfferProducts) —
585
+ // để trống thì không tốn thêm request nào, đúng như trước.
586
+ async function generateOfferLink(ctx, { items = [], subIds = [], productIds = [], timeoutMs = 45000, evalTimeoutMs = EVAL_TIMEOUT_MS, pauseMs = 1200, log = null } = {}) {
522
587
  const list = (items || [])
523
588
  .map((it) => ({ itemId: String(it.item_id || ''), shopId: Number(it.shop_id) || 0, trace: '' }))
524
589
  .filter((it) => it.itemId && it.shopId);
@@ -592,7 +657,10 @@ async function generateOfferLink(ctx, { items = [], subIds = [], timeoutMs = 450
592
657
  return { status: 'failed', links: [], sub_ids: subs, reason: linkFailureReason(res, list.length) };
593
658
  }
594
659
  if (missing.length && log) log(`[shopee] thiếu link cho ${missing.length}/${list.length} món: ${missing.join(', ')}`);
595
- return { status: 'ok', sub_ids: subs, links, missing };
660
+ // Chỉ đọc món ĐÃ có link, và làm SAU khi link đã nằm trong tay.
661
+ const want = (productIds || []).map(String).filter((id) => links.some((l) => l.item_id === id));
662
+ const products = want.length ? await readOfferProducts(page, want, { evalTimeoutMs, pauseMs, log }) : [];
663
+ return { status: 'ok', sub_ids: subs, links, missing, products };
596
664
  } finally {
597
665
  if (opened) await page.close().catch(() => {});
598
666
  }
@@ -616,4 +684,4 @@ function parseProductUrl(url = '') {
616
684
  return null;
617
685
  }
618
686
 
619
- module.exports = { ingestProduct, scrapeOffers, generateOfferLink, linkFailureReason, cleanSubId, parseProductUrl, parseOfferRow, parsePdpItem, categoryGroup, pctToNumber, AFFILIATE_CATEGORIES, applyFilters, isBlockedState, isAntiBotBody, ANTI_BOT_REASON, findAffiliatePage, pruneTabs, isJunkTab, evaluateWithin, EVAL_TIMEOUT_MS };
687
+ module.exports = { ingestProduct, scrapeOffers, generateOfferLink, readOfferProducts, linkFailureReason, cleanSubId, parseProductUrl, parseOfferRow, parseOfferDetail, parsePdpItem, categoryGroup, pctToNumber, AFFILIATE_CATEGORIES, applyFilters, isBlockedState, isAntiBotBody, ANTI_BOT_REASON, findAffiliatePage, pruneTabs, isJunkTab, evaluateWithin, EVAL_TIMEOUT_MS };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "channel-worker",
3
- "version": "2.5.145",
3
+ "version": "2.5.146",
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": {