channel-worker 2.5.139 → 2.5.141
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 +5 -2
- package/lib/nst-manager.js +39 -25
- package/lib/shopee-report.js +13 -4
- package/lib/shopee-scraper.js +23 -2
- package/package.json +1 -1
package/lib/command-poller.js
CHANGED
|
@@ -347,8 +347,11 @@ class CommandPoller {
|
|
|
347
347
|
});
|
|
348
348
|
if (res.status === 'needs_verify') {
|
|
349
349
|
// Anti-bot bounce — báo sạch, KHÔNG thử lại để khỏi đâm thêm captcha.
|
|
350
|
-
|
|
351
|
-
|
|
350
|
+
// Ghi kèm tên profile: có hai phiên (Shopee1, Shopee2) và người xử lý
|
|
351
|
+
// phải biết mở đúng cái nào để kéo captcha.
|
|
352
|
+
const reason = `${res.reason} [${profileName}]`;
|
|
353
|
+
console.warn(`[shopee] lấy link: needs_verify — ${reason}`);
|
|
354
|
+
await this.api.updateCommand(command._id, { status: 'done', result: { needs_verify: true, reason } });
|
|
352
355
|
return;
|
|
353
356
|
}
|
|
354
357
|
if (res.status !== 'ok') {
|
package/lib/nst-manager.js
CHANGED
|
@@ -21,6 +21,43 @@ function nstLaunchError(response, { profileId = '', runningCount = 0 } = {}) {
|
|
|
21
21
|
return error;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
// Every proxy in the fleet exits through Viettel VN, and GeoIP answers
|
|
25
|
+
// "Vietnam / Asia/Bangkok" for those IPs — so letting NST derive locale from
|
|
26
|
+
// the proxy turned every new profile into a Thai browser (th-TH, Asia/Bangkok).
|
|
27
|
+
// Measured 2026-09-25 on win-worker: 6/6 running profiles, FB login page in
|
|
28
|
+
// Thai for Vietnamese accounts. Pin what a Vietnamese user actually runs.
|
|
29
|
+
// If a non-VN proxy is ever added, this has to follow the proxy's country.
|
|
30
|
+
const VN_LOCALIZATION = {
|
|
31
|
+
basedOnProxy: false,
|
|
32
|
+
locale: 'vi-VN',
|
|
33
|
+
languages: ['vi-VN', 'vi', 'en-US', 'en'],
|
|
34
|
+
timezone: 'Asia/Ho_Chi_Minh',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Only pin the noise flags and the locale. CPU count, RAM, kernel version and
|
|
38
|
+
// UA stay with Nstbrowser's own randomiser: hard-coding hardwareConcurrency=8 /
|
|
39
|
+
// deviceMemory=8 / kernel 132 for EVERY profile made 22/22 running browsers
|
|
40
|
+
// identical (win-worker, 2026-08-22), which is exactly what lets a platform
|
|
41
|
+
// cluster a fleet of accounts. All FB/YT selectors are bilingual (vi + en).
|
|
42
|
+
function profileCreateBody(name, platform) {
|
|
43
|
+
return {
|
|
44
|
+
name,
|
|
45
|
+
platform,
|
|
46
|
+
fingerprint: {
|
|
47
|
+
flags: {
|
|
48
|
+
audio: 'Noise',
|
|
49
|
+
canvas: 'Noise',
|
|
50
|
+
fonts: 'Masked',
|
|
51
|
+
gpu: 'Allow',
|
|
52
|
+
webgl: 'Noise',
|
|
53
|
+
localization: 'Custom',
|
|
54
|
+
timezone: 'Custom',
|
|
55
|
+
},
|
|
56
|
+
localization: VN_LOCALIZATION,
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
24
61
|
class NstManager {
|
|
25
62
|
constructor(apiKey, options = {}) {
|
|
26
63
|
this.apiKey = apiKey;
|
|
@@ -232,32 +269,9 @@ class NstManager {
|
|
|
232
269
|
|
|
233
270
|
const platform = (options.os || 'windows').toLowerCase() === 'mac' ? 'MacOS' : 'Windows';
|
|
234
271
|
console.log(`[nst] WARNING: Profile "${name}" NOT FOUND after 3 retries — creating new profile (${platform})...`);
|
|
235
|
-
// Only pin the noise flags. Everything else (CPU count, RAM, kernel
|
|
236
|
-
// version, UA, locale/timezone) is left to Nstbrowser's own randomiser.
|
|
237
|
-
// Previously this hard-coded hardwareConcurrency=8 / deviceMemory=8 /
|
|
238
|
-
// kernel 132 / en-US for EVERY profile, and NST honoured it — measured
|
|
239
|
-
// 2026-08-22 on win-worker: 22 running browsers, 22/22 identical on
|
|
240
|
-
// (chrome, cpu, ram, language, timezone, screen). Same machine fingerprint
|
|
241
|
-
// across a fleet of accounts is exactly what lets a platform cluster them.
|
|
242
|
-
// localization 'BasedOnProxy' lets NST derive locale + timezone from the
|
|
243
|
-
// proxy's geo, so a VN account on a VN proxy stops claiming it only speaks
|
|
244
|
-
// English. All FB/YT selectors are already bilingual (vi + en).
|
|
245
272
|
const res = await this.api('/profiles', {
|
|
246
273
|
method: 'POST',
|
|
247
|
-
body: JSON.stringify(
|
|
248
|
-
name,
|
|
249
|
-
platform,
|
|
250
|
-
fingerprint: {
|
|
251
|
-
flags: {
|
|
252
|
-
audio: 'Noise',
|
|
253
|
-
canvas: 'Noise',
|
|
254
|
-
fonts: 'Masked',
|
|
255
|
-
gpu: 'Allow',
|
|
256
|
-
webgl: 'Noise',
|
|
257
|
-
localization: 'BasedOnProxy',
|
|
258
|
-
},
|
|
259
|
-
},
|
|
260
|
-
}),
|
|
274
|
+
body: JSON.stringify(profileCreateBody(name, platform)),
|
|
261
275
|
});
|
|
262
276
|
|
|
263
277
|
const profileId = res?.data?.profileId || res?.data?._id;
|
|
@@ -446,4 +460,4 @@ class NstManager {
|
|
|
446
460
|
}
|
|
447
461
|
}
|
|
448
462
|
|
|
449
|
-
module.exports = { NstManager, nstLaunchError };
|
|
463
|
+
module.exports = { NstManager, nstLaunchError, profileCreateBody };
|
package/lib/shopee-report.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const { findAffiliatePage, isBlockedState } = require('./shopee-scraper');
|
|
1
|
+
const { findAffiliatePage, isBlockedState, isAntiBotBody, ANTI_BOT_REASON } = require('./shopee-scraper');
|
|
2
2
|
|
|
3
3
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
4
4
|
|
|
@@ -72,7 +72,10 @@ async function fetchConversionReport(ctx, {
|
|
|
72
72
|
}
|
|
73
73
|
}, { pageSize, pageNum, start: window.start, end: window.end });
|
|
74
74
|
|
|
75
|
-
if (
|
|
75
|
+
if (isAntiBotBody(result.json)) {
|
|
76
|
+
return { status: 'needs_verify', rows, reason: ANTI_BOT_REASON };
|
|
77
|
+
}
|
|
78
|
+
if (isBlockedState(page.url(), result)) {
|
|
76
79
|
return { status: 'needs_verify', rows, reason: `blocked while reading report (http ${result.status}, code ${result.json?.code})` };
|
|
77
80
|
}
|
|
78
81
|
if (result.status !== 200 || result.json?.code !== 0) {
|
|
@@ -146,7 +149,10 @@ async function fetchClickReport(ctx, {
|
|
|
146
149
|
}
|
|
147
150
|
}, { pageSize, pageNum, start: window.start, end: window.end });
|
|
148
151
|
|
|
149
|
-
if (
|
|
152
|
+
if (isAntiBotBody(result.json)) {
|
|
153
|
+
return { status: 'needs_verify', rows, reason: ANTI_BOT_REASON };
|
|
154
|
+
}
|
|
155
|
+
if (isBlockedState(page.url(), result)) {
|
|
150
156
|
return { status: 'needs_verify', rows, reason: `blocked while reading click report (http ${result.status}, code ${result.json?.code})` };
|
|
151
157
|
}
|
|
152
158
|
if (result.status !== 200 || result.json?.code !== 0) {
|
|
@@ -228,7 +234,10 @@ async function fetchSocialMediaAccounts(ctx, { timeoutMs = 45000, log = null } =
|
|
|
228
234
|
}
|
|
229
235
|
});
|
|
230
236
|
|
|
231
|
-
if (
|
|
237
|
+
if (isAntiBotBody(result.json)) {
|
|
238
|
+
return { status: 'needs_verify', accounts: [], reason: ANTI_BOT_REASON };
|
|
239
|
+
}
|
|
240
|
+
if (isBlockedState(page.url(), result)) {
|
|
232
241
|
return { status: 'needs_verify', accounts: [], reason: `blocked while reading media info (http ${result.status}, code ${result.json?.code})` };
|
|
233
242
|
}
|
|
234
243
|
if (result.status !== 200 || result.json?.code !== 0) {
|
package/lib/shopee-scraper.js
CHANGED
|
@@ -183,6 +183,21 @@ function isBlockedState(pageUrl, res) {
|
|
|
183
183
|
return false;
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
+
// Thân phản hồi mang mã chặn chống bot của Shopee.
|
|
187
|
+
//
|
|
188
|
+
// 25/09/2026, lượt lấy link thật trên Shopee2: http 200, thân
|
|
189
|
+
// `{ error: 90309999, is_customized: false, is_login: true }`, rồi tab affiliate
|
|
190
|
+
// bị đẩy sang verify/traffic → verify/captcha. Mã nằm ở `error`, KHÔNG ở `code`
|
|
191
|
+
// — các chỗ so `json.code === 90309999` trước đây trượt hết, nên lấy link ra
|
|
192
|
+
// `failed` với lỗi thô "error 90309999" và cửa nghỉ captcha của ShopeeRate
|
|
193
|
+
// (chỉ đọc `result.needs_verify`) không đóng. Xét cả hai chỗ cho chắc.
|
|
194
|
+
const ANTI_BOT_CODE = 90309999;
|
|
195
|
+
function isAntiBotBody(json) {
|
|
196
|
+
if (!json || typeof json !== 'object') return false;
|
|
197
|
+
return Number(json.error) === ANTI_BOT_CODE || Number(json.code) === ANTI_BOT_CODE;
|
|
198
|
+
}
|
|
199
|
+
const ANTI_BOT_REASON = `Shopee chặn chống bot (error ${ANTI_BOT_CODE}) — mở profile kéo captcha rồi thử lại`;
|
|
200
|
+
|
|
186
201
|
// Tab affiliate nào đang mở — so bằng HOSTNAME, không bằng chuỗi con.
|
|
187
202
|
//
|
|
188
203
|
// 11/09/2026, đo trên profile Shopee1 thật: tab captcha cũ
|
|
@@ -385,7 +400,10 @@ async function scrapeOffers(ctx, {
|
|
|
385
400
|
} catch (e) { return { status: 0, error: String(e.message) }; }
|
|
386
401
|
}, { st, offset, page_limit, mid, listType, kw, ft: filter_types });
|
|
387
402
|
|
|
388
|
-
if (
|
|
403
|
+
if (isAntiBotBody(res.json)) {
|
|
404
|
+
return { status: 'needs_verify', products: [...byId.values()], raw_count: rawCount, reason: ANTI_BOT_REASON };
|
|
405
|
+
}
|
|
406
|
+
if (isBlockedState(page.url(), res)) {
|
|
389
407
|
return { status: 'needs_verify', products: [...byId.values()], raw_count: rawCount, reason: `blocked mid-scrape (http ${res.status}, code ${res.json?.code})` };
|
|
390
408
|
}
|
|
391
409
|
if (res.status !== 200 || res.json?.code !== 0) {
|
|
@@ -501,6 +519,9 @@ async function generateOfferLink(ctx, { items = [], subIds = [], timeoutMs = 450
|
|
|
501
519
|
} catch (e) { return { status: 0, error: String(e.message) }; }
|
|
502
520
|
}, { list, subs });
|
|
503
521
|
|
|
522
|
+
if (isAntiBotBody(res.json)) {
|
|
523
|
+
return { status: 'needs_verify', links: [], reason: ANTI_BOT_REASON };
|
|
524
|
+
}
|
|
504
525
|
if (isBlockedState(page.url(), res)) {
|
|
505
526
|
return { status: 'needs_verify', links: [], reason: `blocked (http ${res.status})` };
|
|
506
527
|
}
|
|
@@ -555,4 +576,4 @@ function parseProductUrl(url = '') {
|
|
|
555
576
|
return null;
|
|
556
577
|
}
|
|
557
578
|
|
|
558
|
-
module.exports = { ingestProduct, scrapeOffers, generateOfferLink, linkFailureReason, cleanSubId, parseProductUrl, parseOfferRow, parsePdpItem, categoryGroup, pctToNumber, AFFILIATE_CATEGORIES, applyFilters, isBlockedState, findAffiliatePage, pruneTabs, isJunkTab };
|
|
579
|
+
module.exports = { ingestProduct, scrapeOffers, generateOfferLink, linkFailureReason, cleanSubId, parseProductUrl, parseOfferRow, parsePdpItem, categoryGroup, pctToNumber, AFFILIATE_CATEGORIES, applyFilters, isBlockedState, isAntiBotBody, ANTI_BOT_REASON, findAffiliatePage, pruneTabs, isJunkTab };
|