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.
- package/lib/command-poller.js +27 -5
- package/lib/shopee-scraper.js +33 -5
- package/package.json +1 -1
package/lib/command-poller.js
CHANGED
|
@@ -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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
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) });
|
package/lib/shopee-scraper.js
CHANGED
|
@@ -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
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
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
|
}
|