channel-worker 2.5.111 → 2.5.113
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
CHANGED
|
@@ -215,6 +215,7 @@ class CommandPoller {
|
|
|
215
215
|
page_limit: payload.page_limit ?? 20,
|
|
216
216
|
delay_ms: payload.delay_ms ?? 2500, // human-like pacing (anti-bot)
|
|
217
217
|
filters: payload.filters || {},
|
|
218
|
+
log: (m) => console.log(m), // để thấy số tab đã dọn
|
|
218
219
|
});
|
|
219
220
|
if (res.status === 'needs_verify') {
|
|
220
221
|
// Anti-bot bounce — DON'T fail/retry into more captchas. Report cleanly
|
|
@@ -304,6 +305,7 @@ class CommandPoller {
|
|
|
304
305
|
const res = await scraper.generateOfferLink(conn.context, {
|
|
305
306
|
items,
|
|
306
307
|
subIds: Array.isArray(payload.sub_ids) ? payload.sub_ids : [],
|
|
308
|
+
log: (m) => console.log(m), // để thấy số tab đã dọn
|
|
307
309
|
});
|
|
308
310
|
if (res.status === 'needs_verify') {
|
|
309
311
|
// Anti-bot bounce — báo sạch, KHÔNG thử lại để khỏi đâm thêm captcha.
|
package/lib/shopee-scraper.js
CHANGED
|
@@ -206,6 +206,51 @@ function findAffiliatePage(pages) {
|
|
|
206
206
|
return live.find((p) => urlPath(p.url()).startsWith('/offer')) || live[0] || null;
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
+
// Tab nào là RÁC của các lượt trước — đóng được mà không mất gì.
|
|
210
|
+
//
|
|
211
|
+
// Chỉ nhận ba loại, cố ý hẹp: trang trắng, tab đang kẹt ở captcha/đăng nhập, và
|
|
212
|
+
// tab Shopee thừa ngoài cái mình sắp dùng. KHÔNG đụng tới tab của trang khác:
|
|
213
|
+
// profile này người vẫn mở tay để xác minh, đóng nhầm là cướp việc của họ.
|
|
214
|
+
function isJunkTab(url, keepUrl) {
|
|
215
|
+
if (url === keepUrl) return false;
|
|
216
|
+
if (!url || url === 'about:blank' || url === 'chrome://newtab/') return true;
|
|
217
|
+
if (isBlockedState(url, null)) return true; // captcha / buyer/login / is_from_login
|
|
218
|
+
try {
|
|
219
|
+
const h = new URL(url).hostname;
|
|
220
|
+
return h === 'affiliate.shopee.vn' || h === 'shopee.vn' || h === 'www.shopee.vn';
|
|
221
|
+
} catch { return false; }
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Dọn tab trước khi làm việc.
|
|
225
|
+
//
|
|
226
|
+
// 11/09 đo trên chính profile Shopee1: 16 tab, và tab VỊ TRÍ 0 là captcha cũ từ
|
|
227
|
+
// lượt quét trước — `pages().find(u.includes('affiliate.shopee.vn'))` vớ trúng
|
|
228
|
+
// nó nên máy một mực đòi xác minh trong khi 15 tab kia đăng nhập bình thường.
|
|
229
|
+
// `findAffiliatePage` đã vá chỗ CHỌN, nhưng tab rác vẫn dồn lại mỗi ngày: mỗi
|
|
230
|
+
// lượt bị đá sang captcha để lại một tab, Chrome chậm dần, và người mở profile
|
|
231
|
+
// ra nhìn thì rối.
|
|
232
|
+
//
|
|
233
|
+
// Giữ đúng MỘT tab affiliate (cái sắp dùng) và đóng phần còn lại. Không bao giờ
|
|
234
|
+
// đóng tab cuối cùng — context không còn trang nào thì Chrome của NST có thể tự
|
|
235
|
+
// tắt, mất luôn phiên đăng nhập vừa xác minh tay.
|
|
236
|
+
async function pruneTabs(ctx, { keep = null, log = null } = {}) {
|
|
237
|
+
const pages = ctx.pages() || [];
|
|
238
|
+
if (pages.length <= 1) return 0;
|
|
239
|
+
const keepUrl = keep ? keep.url() : (findAffiliatePage(pages)?.url() || null);
|
|
240
|
+
let closed = 0;
|
|
241
|
+
for (const p of pages) {
|
|
242
|
+
if (p === keep) continue;
|
|
243
|
+
let u = '';
|
|
244
|
+
try { u = p.url(); } catch { continue; }
|
|
245
|
+
if (!isJunkTab(u, keepUrl)) continue;
|
|
246
|
+
if ((ctx.pages() || []).length <= 1) break;
|
|
247
|
+
await p.close().catch(() => {});
|
|
248
|
+
closed++;
|
|
249
|
+
}
|
|
250
|
+
if (closed && log) log(`[shopee] đã đóng ${closed} tab cũ, còn ${(ctx.pages() || []).length}`);
|
|
251
|
+
return closed;
|
|
252
|
+
}
|
|
253
|
+
|
|
209
254
|
// Apply the Flow-1 hard filters (criteria) to parsed rows. Defaults are lenient;
|
|
210
255
|
// the controller passes the agreed thresholds (commission/price/sold/rating/imgs).
|
|
211
256
|
function applyFilters(rows, f = {}) {
|
|
@@ -260,6 +305,7 @@ async function scrapeOffers(ctx, {
|
|
|
260
305
|
delay_ms = 2500, // human-like pause between calls (anti-bot)
|
|
261
306
|
filters = {},
|
|
262
307
|
timeoutMs = 45000,
|
|
308
|
+
log = null,
|
|
263
309
|
// legacy single-sort param (back-compat with the first build)
|
|
264
310
|
sort_type = null,
|
|
265
311
|
} = {}) {
|
|
@@ -284,8 +330,10 @@ async function scrapeOffers(ctx, {
|
|
|
284
330
|
if (cat?.group && !filters.category_groups) filters = { ...filters, category_groups: [cat.group] };
|
|
285
331
|
const listType = cat?.list_type || null;
|
|
286
332
|
|
|
287
|
-
|
|
288
|
-
|
|
333
|
+
let page = findAffiliatePage(ctx.pages());
|
|
334
|
+
// Dọn TRƯỚC khi mở thêm: tab rác của lượt trước không giúp gì cho lượt này,
|
|
335
|
+
// mà để lại thì lần sau còn nhiều hơn.
|
|
336
|
+
await pruneTabs(ctx, { keep: page, log }).catch(() => {});
|
|
289
337
|
let opened = false;
|
|
290
338
|
if (!page) { page = await ctx.newPage(); opened = true; }
|
|
291
339
|
try {
|
|
@@ -372,7 +420,7 @@ function cleanSubId(v) {
|
|
|
372
420
|
// Google Ads, không qua đường ghi nhận affiliate nên không ra hoa hồng).
|
|
373
421
|
//
|
|
374
422
|
// subIds: mảng tối đa 5 phần tử. Quy ước của mình: [kênh, video, đợt].
|
|
375
|
-
async function generateOfferLink(ctx, { items = [], subIds = [], timeoutMs = 45000 } = {}) {
|
|
423
|
+
async function generateOfferLink(ctx, { items = [], subIds = [], timeoutMs = 45000, log = null } = {}) {
|
|
376
424
|
const list = (items || [])
|
|
377
425
|
.map((it) => ({ itemId: String(it.item_id || ''), shopId: Number(it.shop_id) || 0, trace: '' }))
|
|
378
426
|
.filter((it) => it.itemId && it.shopId);
|
|
@@ -381,8 +429,8 @@ async function generateOfferLink(ctx, { items = [], subIds = [], timeoutMs = 450
|
|
|
381
429
|
const subs = {};
|
|
382
430
|
for (let i = 0; i < 5; i++) subs[`subId${i + 1}`] = cleanSubId(subIds[i]);
|
|
383
431
|
|
|
384
|
-
|
|
385
|
-
|
|
432
|
+
let page = findAffiliatePage(ctx.pages());
|
|
433
|
+
await pruneTabs(ctx, { keep: page, log }).catch(() => {});
|
|
386
434
|
let opened = false;
|
|
387
435
|
if (!page) { page = await ctx.newPage(); opened = true; }
|
|
388
436
|
try {
|
|
@@ -441,4 +489,4 @@ function parseProductUrl(url = '') {
|
|
|
441
489
|
return null;
|
|
442
490
|
}
|
|
443
491
|
|
|
444
|
-
module.exports = { ingestProduct, scrapeOffers, generateOfferLink, cleanSubId, parseProductUrl, parseOfferRow, parsePdpItem, categoryGroup, pctToNumber, AFFILIATE_CATEGORIES, applyFilters, isBlockedState, findAffiliatePage };
|
|
492
|
+
module.exports = { ingestProduct, scrapeOffers, generateOfferLink, cleanSubId, parseProductUrl, parseOfferRow, parsePdpItem, categoryGroup, pctToNumber, AFFILIATE_CATEGORIES, applyFilters, isBlockedState, findAffiliatePage, pruneTabs, isJunkTab };
|
package/package.json
CHANGED
|
@@ -25,6 +25,25 @@
|
|
|
25
25
|
const { assertAccountUsable, readPublishBlock } = require('./lib/fb-guard');
|
|
26
26
|
const { humanClick, humanType, pause, randInt } = require('./lib/human');
|
|
27
27
|
|
|
28
|
+
// Chụp màn hình lúc trượt — mượn của upload_facebook, nhưng require MUỘN và có
|
|
29
|
+
// đường lùi.
|
|
30
|
+
//
|
|
31
|
+
// Vì sao không require ở đầu file: `lib/playwright-runner` chỉ xoá require.cache
|
|
32
|
+
// của ĐÚNG script được gọi. Chép nóng cả hai file vào máy thì bản
|
|
33
|
+
// comment_facebook mới được nạp lại ngay, còn upload_facebook có thể vẫn là bản
|
|
34
|
+
// CŨ còn nằm trong cache từ lượt đăng trước — bản cũ chưa export `dumpFailure`,
|
|
35
|
+
// require ở đầu file là cả script chết ngay dòng đầu. Chụp được ảnh thì tốt,
|
|
36
|
+
// không chụp được thì thôi, đừng vì nó mà hỏng cả lượt thả bình luận.
|
|
37
|
+
async function dumpFailure(page, tag, log) {
|
|
38
|
+
try {
|
|
39
|
+
const f = require('./upload_facebook').__testables?.dumpFailure;
|
|
40
|
+
if (typeof f === 'function') await f(page, tag, log);
|
|
41
|
+
else log('info', '[fb-cmt] bản upload_facebook đang nạp chưa có dumpFailure — bỏ qua bước chụp màn hình');
|
|
42
|
+
} catch (e) {
|
|
43
|
+
log('info', `[fb-cmt] không chụp được màn hình: ${String(e.message || e).slice(0, 80)}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
28
47
|
const NAV_TIMEOUT_MS = 45_000;
|
|
29
48
|
// Bình luận hiện ra gần như tức thì, nhưng lượt đầu sau khi mở bài thì Facebook
|
|
30
49
|
// còn đang dựng khung bình luận. 20 giây là dư cho trường hợp chậm nhất đo được
|
|
@@ -94,6 +113,34 @@ async function openCommentsIfNeeded(page, log) {
|
|
|
94
113
|
// Facebook đặt tên tác giả ngay trong aria-label của khối bình luận
|
|
95
114
|
// ("Bình luận của <Tên>" / "Comment by <Name>") — đọc thẳng ở đó chắc hơn dò
|
|
96
115
|
// anchor đầu tiên, vì trong khối còn có link tới bài, tới trang được nhắc tên.
|
|
116
|
+
// ĐẾM số chỗ trên trang có chứa chuỗi link, BỎ QUA ô soạn.
|
|
117
|
+
//
|
|
118
|
+
// Đây là cách xác minh chắc hơn hẳn việc dò cấu trúc bình luận: mình đo TRƯỚC
|
|
119
|
+
// khi gõ và ĐO LẠI sau khi gửi, thấy tăng lên là bình luận đã lên. Không cần
|
|
120
|
+
// biết Facebook dựng khung bình luận bằng thẻ gì, cũng không vỡ khi họ đổi.
|
|
121
|
+
//
|
|
122
|
+
// Vì sao phải đổi (15/09): bản đầu đòi khối bình luận mang nhãn
|
|
123
|
+
// `aria-label="Bình luận của <Tên>"` — đó là cấu trúc bài trên BẢNG TIN. Trang
|
|
124
|
+
// reel dựng khác, nên 5/5 lượt thả thật đều báo "không đọc lại thấy" trong khi
|
|
125
|
+
// chủ tịch mở bài ra thì bình luận NẰM ĐÓ. Suýt nữa thì mỗi bài hai bình luận
|
|
126
|
+
// vì nút hiện "Thả lại".
|
|
127
|
+
async function countLinkOnPage(page, link) {
|
|
128
|
+
return page.evaluate((needle) => {
|
|
129
|
+
let n = 0;
|
|
130
|
+
const walk = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
|
|
131
|
+
while (walk.nextNode()) {
|
|
132
|
+
const t = walk.currentNode;
|
|
133
|
+
if (!t.nodeValue || !t.nodeValue.includes(needle)) continue;
|
|
134
|
+
// Chữ mình vừa gõ trong ô soạn không tính.
|
|
135
|
+
if (t.parentElement && t.parentElement.closest("[contenteditable='true']")) continue;
|
|
136
|
+
n++;
|
|
137
|
+
}
|
|
138
|
+
return n;
|
|
139
|
+
}, link).catch(() => -1);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Cố lấy TÊN người bình luận + permalink. Best-effort thuần: không lấy được thì
|
|
143
|
+
// để trống chứ KHÔNG coi là thất bại — xác minh đã do phép đếm ở trên lo.
|
|
97
144
|
async function findPostedComment(page, link) {
|
|
98
145
|
return page.evaluate((needle) => {
|
|
99
146
|
const out = { found: false, author: '', comment_url: '' };
|
|
@@ -121,16 +168,22 @@ async function findPostedComment(page, link) {
|
|
|
121
168
|
}, link).catch(() => ({ found: false, author: '', comment_url: '' }));
|
|
122
169
|
}
|
|
123
170
|
|
|
124
|
-
async function waitForPostedComment(page, link, log, timeoutMs = VERIFY_TIMEOUT_MS) {
|
|
171
|
+
async function waitForPostedComment(page, link, log, timeoutMs = VERIFY_TIMEOUT_MS, baseline = 0) {
|
|
125
172
|
const deadline = Date.now() + timeoutMs;
|
|
126
|
-
let
|
|
173
|
+
let meta = { found: false, author: '', comment_url: '' };
|
|
127
174
|
while (Date.now() < deadline) {
|
|
128
|
-
|
|
129
|
-
|
|
175
|
+
const n = await countLinkOnPage(page, link);
|
|
176
|
+
// Tăng so với lúc trước khi gõ = bình luận đã lên. Đây mới là bằng chứng;
|
|
177
|
+
// tên tác giả và permalink chỉ là thông tin kèm theo.
|
|
178
|
+
if (n > baseline) {
|
|
179
|
+
meta = await findPostedComment(page, link);
|
|
180
|
+
return { ...meta, found: true, count: n };
|
|
181
|
+
}
|
|
130
182
|
await page.waitForTimeout(1200);
|
|
131
183
|
}
|
|
132
|
-
|
|
133
|
-
|
|
184
|
+
const n = await countLinkOnPage(page, link);
|
|
185
|
+
log('warn', `[fb-cmt] hết ${Math.round(timeoutMs / 1000)}s: số chỗ có link vẫn là ${n} (trước khi gõ ${baseline}) — chưa thấy bình luận lên`);
|
|
186
|
+
return { ...meta, found: false, count: n };
|
|
134
187
|
}
|
|
135
188
|
|
|
136
189
|
// ─── ghim ───────────────────────────────────────────────────────────────────
|
|
@@ -217,6 +270,11 @@ async function run({ page, payload = {}, log }) {
|
|
|
217
270
|
throw new Error(`Không tìm thấy ô soạn bình luận trên ${postUrl} (đã thử ${BOX_SELECTORS.length} nhãn) — nhiều khả năng bài tắt bình luận hoặc Facebook đổi giao diện`);
|
|
218
271
|
}
|
|
219
272
|
|
|
273
|
+
// Đo NỀN trước khi gõ: link có thể đã nằm sẵn trong caption, hoặc bài đã
|
|
274
|
+
// được thả bình luận từ lượt trước.
|
|
275
|
+
const baseline = Math.max(await countLinkOnPage(page, link), 0);
|
|
276
|
+
if (baseline > 0) log('info', `[fb-cmt] link đã xuất hiện ${baseline} chỗ trên bài từ trước — chỉ tính là xong khi tăng thêm`);
|
|
277
|
+
|
|
220
278
|
await box.scrollIntoViewIfNeeded({ timeout: 5000 }).catch(() => {});
|
|
221
279
|
await humanClick(page, box).catch(async () => { await box.click({ timeout: 5000 }); });
|
|
222
280
|
await pause(page, 900);
|
|
@@ -240,9 +298,12 @@ async function run({ page, payload = {}, log }) {
|
|
|
240
298
|
throw new Error(`FB_COMMENT_RATE_LIMITED: Facebook chặn bình luận vì tần suất/nghi spam — bình luận KHÔNG được nhận. Chuỗi bắt được: "${block}". Dừng thả link trên kênh này cho tới khi người kiểm tra.`);
|
|
241
299
|
}
|
|
242
300
|
|
|
243
|
-
const found = await waitForPostedComment(page, link, log);
|
|
301
|
+
const found = await waitForPostedComment(page, link, log, VERIFY_TIMEOUT_MS, baseline);
|
|
244
302
|
if (!found.found) {
|
|
245
303
|
await assertAccountUsable(page, log);
|
|
304
|
+
// Chụp lại màn hình: lần trước trượt xác minh mà không có gì để soi, phải
|
|
305
|
+
// đợi chủ tịch tự mở Facebook mới biết bình luận có lên hay không.
|
|
306
|
+
await dumpFailure(page, 'cmt-verify', log);
|
|
246
307
|
// KHÔNG ném lỗi kiểu "có thể đã thả": trả về verified=false để API ghi
|
|
247
308
|
// '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
|
|
248
309
|
// bình luận nữa, mà bình luận trước có thể đã nằm đó rồi.
|
|
@@ -268,4 +329,4 @@ async function run({ page, payload = {}, log }) {
|
|
|
268
329
|
}
|
|
269
330
|
|
|
270
331
|
module.exports = { run };
|
|
271
|
-
module.exports.__testables = { findPostedComment, findCommentBox, BOX_SELECTORS, sel };
|
|
332
|
+
module.exports.__testables = { findPostedComment, countLinkOnPage, waitForPostedComment, findCommentBox, BOX_SELECTORS, sel };
|
|
@@ -3425,6 +3425,9 @@ module.exports = { run };
|
|
|
3425
3425
|
// Exposed so the overlay-click escalation can be exercised against a synthetic
|
|
3426
3426
|
// page (toast covering the CTA) without driving the whole upload flow.
|
|
3427
3427
|
module.exports.__testables = {
|
|
3428
|
+
// Dùng chung cho comment_facebook: script nào trượt giữa chừng cũng cần chụp
|
|
3429
|
+
// lại màn hình, viết bản thứ hai là sớm muộn hai bên lệch nhau.
|
|
3430
|
+
dumpFailure,
|
|
3428
3431
|
resilientClick,
|
|
3429
3432
|
confirmPublishDialog,
|
|
3430
3433
|
diffNewReelIds,
|