channel-worker 2.5.106 → 2.5.107

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.
@@ -200,10 +200,17 @@ class CommandPoller {
200
200
  let conn;
201
201
  try {
202
202
  conn = await this._connectNstProfileByName(profileName);
203
+ // Từ khoá và ngành hàng loại trừ nhau: có keyword thì KHÔNG được để
204
+ // `category` rơi về mặc định 'women_clothes', không thì scrapeOffers nhận
205
+ // cả hai và lượt tìm im lặng biến thành lượt duyệt tab quần áo nữ.
206
+ const keyword = String(payload.keyword || '').trim();
203
207
  const res = await scraper.scrapeOffers(conn.context, {
204
- category: payload.category || 'women_clothes', // Tier-1 default: quần áo nữ
205
- match_id: payload.match_id || null,
206
- sort_types: payload.sort_types || [2, 3], // commission + sales, merged
208
+ keyword: keyword || null,
209
+ category: keyword ? null : (payload.category || 'women_clothes'), // Tier-1 default: quần áo nữ
210
+ match_id: keyword ? null : (payload.match_id || null),
211
+ // Ở chế độ từ khoá, sort_type 3 là GIÁ GIẢM DẦN chứ không phải doanh số
212
+ // (đo 09/09) — scrapeOffers tự cắt, đây chỉ là không đưa vào cho đỡ rối.
213
+ sort_types: payload.sort_types || (keyword ? [2] : [2, 3]), // commission + sales, merged
207
214
  pages: payload.pages ?? 2,
208
215
  page_limit: payload.page_limit ?? 20,
209
216
  delay_ms: payload.delay_ms ?? 2500, // human-like pacing (anti-bot)
@@ -217,9 +224,24 @@ class CommandPoller {
217
224
  await this.api.updateCommand(command._id, { status: 'done', result: { needs_verify: true, reason: res.reason, count: res.products?.length || 0 } });
218
225
  return;
219
226
  }
220
- console.log(`[shopee] scraped ${res.raw_count} raw → ${res.products.length} after filters from ${profileName}`);
227
+ console.log(`[shopee] scraped ${res.raw_count} raw → ${res.products.length} after filters from ${profileName}${keyword ? ` (từ khoá "${keyword}")` : ''}`);
221
228
  await this.api.upsertAffiliateProducts(res.products, { user_id: command.user_id });
222
- await this.api.updateCommand(command._id, { status: 'done', result: { count: res.products.length, raw: res.raw_count } });
229
+ // Lượt tìm theo từ khoá trả kèm CHÍNH danh sách tìm được, không chỉ đếm:
230
+ // bên gọi phải chấm chọn đúng trên những món của lượt này. Đọc lại từ kho
231
+ // thì lẫn hàng của các lượt trước và mất luôn thứ tự Shopee xếp.
232
+ // Cắt gọn để kết quả lệnh không phình (ảnh/biến thể ở lại trong kho).
233
+ const slim = keyword
234
+ ? res.products.slice(0, 20).map((r) => ({
235
+ shop_id: r.shop_id, item_id: r.item_id, name: r.name, price: r.price,
236
+ commission_rate: r.commission_rate, commission_value: r.commission_value,
237
+ sold_count: r.sold_count, rating: r.rating, cover_image: r.cover_image,
238
+ product_url: r.product_url, category_group: r.category_group,
239
+ }))
240
+ : undefined;
241
+ await this.api.updateCommand(command._id, {
242
+ status: 'done',
243
+ result: { count: res.products.length, raw: res.raw_count, keyword: res.keyword || null, ...(slim ? { products: slim } : {}) },
244
+ });
223
245
  } catch (err) {
224
246
  console.error(`[shopee] scrape failed: ${err.message}`);
225
247
  await this.api.updateCommand(command._id, { status: 'failed', error: String(err.message || err).slice(0, 500) });
@@ -246,6 +246,14 @@ function applyFilters(rows, f = {}) {
246
246
  async function scrapeOffers(ctx, {
247
247
  category = null, // key in AFFILIATE_CATEGORIES (e.g. 'women_clothes')
248
248
  match_id = null, // explicit category id override
249
+ // Tìm theo TỪ KHOÁ thay vì duyệt tab ngành hàng. Cùng endpoint, khác tham số:
250
+ // `list_type=0&keyword=<kw>&filter_types=2` (đo 09/09/2026 — ghi chú cũ bảo
251
+ // "cần signed header" là sai/lạc hậu).
252
+ //
253
+ // Đâ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
254
+ // nhau nên kho đứng yên ở 34 sản phẩm. Từ khoá thì mở ra đúng món hợp với
255
+ // từng video.
256
+ keyword = null,
249
257
  sort_types = [2], // [2]=commission; pass [2,3] to merge commission+sales
250
258
  pages = 2,
251
259
  page_limit = 20,
@@ -256,6 +264,18 @@ async function scrapeOffers(ctx, {
256
264
  sort_type = null,
257
265
  } = {}) {
258
266
  if (sort_type != null) sort_types = [sort_type];
267
+ const kw = String(keyword || '').trim();
268
+ // ⚠️ Ý NGHĨA CỦA sort_type ĐỔI KHI TÌM THEO TỪ KHOÁ (đo 09/09/2026):
269
+ // 2 = bán chạy ← thứ mình muốn
270
+ // 3 = GIÁ GIẢM DẦN, không phải doanh số (khác hẳn chế độ duyệt tab)
271
+ // 1 = liên quan, trả về toàn hàng bán 0 / 0 sao
272
+ // Mặc định của hàm là [2] nên chế độ từ khoá đã đúng sẵn; nhưng bên gọi quen
273
+ // tay truyền [2,3] như lúc quét tab thì một nửa kết quả là hàng đắt nhất chứ
274
+ // không phải hàng bán chạy. Cắt 3 đi và nói ra, đừng lặng lẽ quét rác.
275
+ if (kw && sort_types.includes(3)) {
276
+ sort_types = sort_types.filter((x) => x !== 3);
277
+ if (!sort_types.length) sort_types = [2];
278
+ }
259
279
  const cat = category && AFFILIATE_CATEGORIES[category] ? AFFILIATE_CATEGORIES[category] : null;
260
280
  const mid = match_id || cat?.match_id || null;
261
281
  // Default the category_groups filter to the category's own group (so a
@@ -285,15 +305,20 @@ async function scrapeOffers(ctx, {
285
305
  for (let i = 0; i < pages; i++) {
286
306
  const offset = i * page_limit;
287
307
  const res = await page.evaluate(async (q) => {
288
- const cat = q.listType
289
- ? `&list_type=${q.listType}`
290
- : (q.mid ? `&list_type=3&match_type=2&match_id=${q.mid}` : '&list_type=0');
308
+ // Từ khoá đứng TRƯỚC ngành hàng: hai chế độ loại trừ nhau, gửi cả hai
309
+ // thì Shopee bỏ qua keyword và mình tưởng đang tìm mà thật ra vẫn
310
+ // đang duyệt tab cũ.
311
+ const cat = q.kw
312
+ ? `&list_type=0&keyword=${encodeURIComponent(q.kw)}&filter_types=2`
313
+ : (q.listType
314
+ ? `&list_type=${q.listType}`
315
+ : (q.mid ? `&list_type=3&match_type=2&match_id=${q.mid}` : '&list_type=0'));
291
316
  const url = `/api/v3/offer/product/list?sort_type=${q.st}&page_offset=${q.offset}&page_limit=${q.page_limit}&client_type=1${cat}`;
292
317
  try {
293
318
  const r = await fetch(url, { credentials: 'include' });
294
319
  return { status: r.status, json: await r.json().catch(() => null) };
295
320
  } catch (e) { return { status: 0, error: String(e.message) }; }
296
- }, { st, offset, page_limit, mid, listType });
321
+ }, { st, offset, page_limit, mid, listType, kw });
297
322
 
298
323
  if (isBlockedState(page.url(), res) || res.json?.code === 90309999) {
299
324
  return { status: 'needs_verify', products: [...byId.values()], raw_count: rawCount, reason: `blocked mid-scrape (http ${res.status}, code ${res.json?.code})` };
@@ -314,7 +339,10 @@ async function scrapeOffers(ctx, {
314
339
  }
315
340
 
316
341
  const products = applyFilters([...byId.values()], filters);
317
- return { status: 'ok', products, raw_count: rawCount };
342
+ // `keyword` đi kèm kết quả để bên gọi ghi lại sản phẩm tìm được theo từ
343
+ // khoá nào — không có nó thì kho chỉ biết "có món này", không biết vì sao
344
+ // nó được nhặt về, và lượt chấm điểm hợp-video sau này mất căn cứ.
345
+ return { status: 'ok', products, raw_count: rawCount, keyword: kw || null };
318
346
  } finally {
319
347
  if (opened) await page.close().catch(() => {});
320
348
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "channel-worker",
3
- "version": "2.5.106",
3
+ "version": "2.5.107",
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": {