channel-worker 2.5.144 → 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.
- package/lib/api-client.js +1 -1
- package/lib/command-poller.js +27 -2
- package/lib/playwright-runner.js +33 -15
- package/lib/shopee-scraper.js +71 -3
- package/package.json +1 -1
- package/scripts/facebook_page_check.js +74 -0
- package/scripts/upload_facebook.js +4 -0
package/lib/api-client.js
CHANGED
|
@@ -75,7 +75,7 @@ class ApiClient {
|
|
|
75
75
|
// nào — xảy ra 14/09 với verify_facebook_reels_pw (cổng thứ nhất là enum
|
|
76
76
|
// của app/models/WorkerCommand.js, cũng đã quên một lần cùng ngày).
|
|
77
77
|
// api/tests/publish-verify-wiring.test.js canh cả hai cổng.
|
|
78
|
-
const workerTypes = 'launch_profile,close_profile,launch_veo3_profile,set_profile_proxy,save_file,set_thumbnail,set_tags,set_file_input,click_and_upload,type_text,verify_logins,update_extension,sync_youtube_stats,restart_worker,upload_youtube_pw,upload_tiktok_pw,upload_facebook_pw,upload_facebook_photo_pw,upload_facebook_post_pw,warmup_youtube_pw,warmup_facebook_pw,warmup_tiktok_pw,nurture_facebook_pw,fetch_facebook_reel_stats_pw,verify_facebook_reels_pw,comment_facebook_pw,scrape_affiliate_products,ingest_shopee_product,get_affiliate_link,fetch_affiliate_report';
|
|
78
|
+
const workerTypes = 'launch_profile,close_profile,launch_veo3_profile,set_profile_proxy,save_file,set_thumbnail,set_tags,set_file_input,click_and_upload,type_text,verify_logins,update_extension,sync_youtube_stats,restart_worker,upload_youtube_pw,upload_tiktok_pw,upload_facebook_pw,upload_facebook_photo_pw,upload_facebook_post_pw,warmup_youtube_pw,warmup_facebook_pw,warmup_tiktok_pw,nurture_facebook_pw,fetch_facebook_reel_stats_pw,verify_facebook_reels_pw,comment_facebook_pw,facebook_page_check_pw,scrape_affiliate_products,ingest_shopee_product,get_affiliate_link,fetch_affiliate_report';
|
|
79
79
|
return this.request('GET', `/workers/commands?worker_id=${workerId}&types=${encodeURIComponent(workerTypes)}`);
|
|
80
80
|
}
|
|
81
81
|
|
package/lib/command-poller.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
434
|
+
return { status: 'done', result: { links, sub_ids: res.sub_ids } };
|
|
420
435
|
},
|
|
421
436
|
});
|
|
422
437
|
}
|
|
@@ -530,6 +545,9 @@ class CommandPoller {
|
|
|
530
545
|
// only serializes within a profile. Wait up to 30 min with 5s polling
|
|
531
546
|
// (upload typically 1-3 min); stale-cleanup picks up zombies separately.
|
|
532
547
|
// Lượt mới của cùng profile → huỷ hẹn đóng, giữ browser cho nó dùng lại.
|
|
548
|
+
// Nhớ lại là CÓ hẹn: browser đó do automation mở, lượt này xong phải hẹn
|
|
549
|
+
// đóng lại (xem nhánh runInfo.attached ở cuối hàm).
|
|
550
|
+
const hadCloseTimer = !!(this._pwCloseTimers && this._pwCloseTimers.has(profileId));
|
|
533
551
|
if (this._pwCloseTimers && this._pwCloseTimers.has(profileId)) {
|
|
534
552
|
clearTimeout(this._pwCloseTimers.get(profileId));
|
|
535
553
|
this._pwCloseTimers.delete(profileId);
|
|
@@ -599,6 +617,7 @@ class CommandPoller {
|
|
|
599
617
|
});
|
|
600
618
|
|
|
601
619
|
const keepOpen = await this._keepOpenCfg();
|
|
620
|
+
const runInfo = {};
|
|
602
621
|
|
|
603
622
|
// Nhịp còn-sống SUỐT lúc kịch bản chạy (không chỉ lúc xếp hàng). Bộ dọn của
|
|
604
623
|
// API khai tử lệnh đăng 'running' quá 20 phút không cập nhật; một lượt
|
|
@@ -618,6 +637,7 @@ class CommandPoller {
|
|
|
618
637
|
payload,
|
|
619
638
|
log,
|
|
620
639
|
keepOpen: keepOpen.enabled,
|
|
640
|
+
info: runInfo,
|
|
621
641
|
});
|
|
622
642
|
// postCommandResult (not updateCommand) so the existing extension-side
|
|
623
643
|
// logic mirrors status into ContentJob + ContentIdea.publish_results +
|
|
@@ -655,6 +675,11 @@ class CommandPoller {
|
|
|
655
675
|
this._keptOpenProfiles.set(profileId, Date.now());
|
|
656
676
|
console.log(`[keep-open] giữ profile ${profileId} mở sau ${scriptName} (${this._keptOpenProfiles.size}/${keepOpen.max})`);
|
|
657
677
|
await this._trimKeptProfiles(keepOpen.max).catch((e) => console.warn(`[keep-open] trim lỗi: ${e.message}`));
|
|
678
|
+
} else if (runInfo.attached && !hadCloseTimer) {
|
|
679
|
+
// Script nối vào browser ĐANG MỞ SẴN mà không có hẹn đóng nào của
|
|
680
|
+
// automation (vd "Kiểm tra Page ngay" lúc người vừa tạo Page còn ngồi
|
|
681
|
+
// trong cửa sổ) → không phải browser của mình, để nguyên cho người đó.
|
|
682
|
+
console.log(`[commands/pw] ${scriptName}: nối vào profile ${profileId} đang mở sẵn — để nguyên, không hẹn đóng`);
|
|
658
683
|
} else if (CLOSE_AFTER.has(scriptName) && this.nst) {
|
|
659
684
|
try {
|
|
660
685
|
await this.nst.stopProfile(profileId);
|
package/lib/playwright-runner.js
CHANGED
|
@@ -52,33 +52,35 @@ async function enableFocusEmulation(page, log) {
|
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
55
|
+
// Lấy cổng CDP của profile. Mở mới nếu chưa chạy; đang chạy thì mặc định
|
|
56
|
+
// tắt-mở lại để có endpoint tươi.
|
|
57
|
+
//
|
|
58
|
+
// Hai trường hợp NỐI THẲNG vào browser đang chạy thay vì tắt-mở lại:
|
|
59
|
+
// - keepOpen: máy bị chặn số lượt mở/ngày — relaunch là đốt 1 lượt cho không.
|
|
60
|
+
// - attachIfRunning (script tự khai, vd facebook_page_check): người vừa tạo
|
|
61
|
+
// Page rất có thể còn ngồi trong đúng cửa sổ đó — relaunch là đóng ngang
|
|
62
|
+
// trước mặt họ. Script loại này tự mở tab riêng và chỉ đóng tab của nó.
|
|
63
|
+
// Trả { endpoint, attached } — attached = đã nối vào browser đang chạy sẵn
|
|
64
|
+
// (daemon dựa vào đó để không hẹn đóng một cửa sổ nó không mở).
|
|
65
|
+
async function resolveEndpoint({ nst, profileId, log = () => {}, keepOpen = false, attachIfRunning = false }) {
|
|
63
66
|
// Don't load any Chromium extension — Playwright is the automation, no need
|
|
64
67
|
// for the channel-manager-ext on this path. (The chagpt cgpt path still
|
|
65
68
|
// launches with content-creator-ext via a different code path.)
|
|
66
69
|
const launchRes = await nst.launchProfile(profileId, { /* extensionPath omitted */ });
|
|
67
70
|
let endpoint = launchRes?.wsEndpoint;
|
|
71
|
+
let attached = false;
|
|
68
72
|
|
|
69
73
|
// If the profile was already running, launchProfile returns { alreadyRunning:
|
|
70
74
|
// true } without a wsEndpoint.
|
|
71
75
|
if (!endpoint && launchRes?.alreadyRunning) {
|
|
72
|
-
|
|
73
|
-
// sẵn chỉ để lấy endpoint mới là đốt 1 lượt cho không — nối thẳng vào cổng
|
|
74
|
-
// debug của nó thay vì relaunch.
|
|
75
|
-
if (keepOpen) {
|
|
76
|
+
if (keepOpen || attachIfRunning) {
|
|
76
77
|
const live = await nst.resolveRunningEndpoint(profileId).catch(() => null);
|
|
77
78
|
if (live?.httpEndpoint) {
|
|
78
79
|
endpoint = live.httpEndpoint;
|
|
79
|
-
|
|
80
|
+
attached = true;
|
|
81
|
+
log('info', `[pw] profile đang chạy — nối vào cổng debug ${live.port} (không mở lại${keepOpen ? ', không tốn lượt mở' : ''})`);
|
|
80
82
|
} else {
|
|
81
|
-
log('warn', `[pw] profile đang chạy nhưng không đọc được cổng debug — buộc phải mở lại (tốn 1 lượt)`);
|
|
83
|
+
log('warn', `[pw] profile đang chạy nhưng không đọc được cổng debug — buộc phải mở lại${keepOpen ? ' (tốn 1 lượt)' : ''}`);
|
|
82
84
|
}
|
|
83
85
|
}
|
|
84
86
|
if (!endpoint) {
|
|
@@ -88,6 +90,22 @@ async function runPlaywrightScript({ nst, profileId, scriptName, payload = {}, l
|
|
|
88
90
|
}
|
|
89
91
|
}
|
|
90
92
|
if (!endpoint) throw new Error(`No webSocketDebuggerUrl from NST for profile ${profileId}`);
|
|
93
|
+
return { endpoint, attached };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// `info` (tuỳ chọn) được điền { attached } cho daemon đọc sau lượt chạy.
|
|
97
|
+
async function runPlaywrightScript({ nst, profileId, scriptName, payload = {}, log = () => {}, keepOpen = false, info = null }) {
|
|
98
|
+
let playwright;
|
|
99
|
+
try { playwright = require('playwright-core'); }
|
|
100
|
+
catch (e) { throw new Error(`playwright-core not installed in channel-worker (npm i playwright-core in the worker dir): ${e.message}`); }
|
|
101
|
+
|
|
102
|
+
const script = loadScript(scriptName);
|
|
103
|
+
|
|
104
|
+
log('info', `[pw] launching NST profile ${profileId}…`);
|
|
105
|
+
const { endpoint, attached } = await resolveEndpoint({
|
|
106
|
+
nst, profileId, log, keepOpen, attachIfRunning: !!script.attachIfRunning,
|
|
107
|
+
});
|
|
108
|
+
if (info) info.attached = attached;
|
|
91
109
|
|
|
92
110
|
log('info', `[pw] connectOverCDP…`);
|
|
93
111
|
const browser = await playwright.chromium.connectOverCDP(endpoint);
|
|
@@ -107,4 +125,4 @@ async function runPlaywrightScript({ nst, profileId, scriptName, payload = {}, l
|
|
|
107
125
|
}
|
|
108
126
|
}
|
|
109
127
|
|
|
110
|
-
module.exports = { runPlaywrightScript, enableFocusEmulation };
|
|
128
|
+
module.exports = { runPlaywrightScript, enableFocusEmulation, resolveEndpoint };
|
package/lib/shopee-scraper.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// "Kiểm tra Page ngay" — profile NST của kênh đang dùng Facebook dưới danh
|
|
2
|
+
// nghĩa PAGE hay vẫn là trang CÁ NHÂN?
|
|
3
|
+
//
|
|
4
|
+
// Sinh ra 29/09/2026: người vừa tạo Page vẫn bị API chặn đăng vì cờ
|
|
5
|
+
// `nurture.profile_mode='personal'` do phiên nuôi hôm trước ghi, và cờ đó chỉ
|
|
6
|
+
// phiên nuôi làm mới được (hàng nuôi chạy lần lượt cả đội). Script này là lượt
|
|
7
|
+
// đọc riêng, ~30 giây: mở facebook.com, kiểm tài khoản còn sống, đọc danh tính
|
|
8
|
+
// bằng ĐÚNG bộ đọc mà lượt đăng dùng (lib/fb-guard readProfileMode), rồi thôi.
|
|
9
|
+
// Không lướt, không bấm, không mở web ngoài như phiên nuôi.
|
|
10
|
+
//
|
|
11
|
+
// Người bấm nút này thường VỪA tạo Page trong chính profile đó và cửa sổ có
|
|
12
|
+
// thể còn mở. Vì vậy:
|
|
13
|
+
// - attachIfRunning: runner nối vào browser đang chạy thay vì tắt-mở lại;
|
|
14
|
+
// - làm trong TAB RIÊNG và chỉ đóng tab của mình — không đóng tab nào khác
|
|
15
|
+
// (phiên nuôi thì có dọn tab, ở đây thì không).
|
|
16
|
+
//
|
|
17
|
+
// Contract: run({ page, context, payload, log }) → { profile_mode:
|
|
18
|
+
// 'page'|'personal'|'unknown', why, timeline_label, nav_sample, duration_sec }.
|
|
19
|
+
// Tài khoản chết (đăng xuất / checkpoint / khoá) ném lỗi có mã FB_ACCOUNT_*
|
|
20
|
+
// như mọi script Facebook khác — API ghi trạng thái tài khoản từ đó.
|
|
21
|
+
|
|
22
|
+
const { assertAccountUsable, readProfileMode } = require('./lib/fb-guard');
|
|
23
|
+
const { enableFocusEmulation } = require('../lib/playwright-runner');
|
|
24
|
+
|
|
25
|
+
// Facebook vẽ thanh điều hướng + ô "Tạo bài viết" sau khi tải xong khung
|
|
26
|
+
// trang; đọc quá sớm thì mọi tín hiệu đều rỗng → 'unknown'. Đọc lại vài lượt
|
|
27
|
+
// cách nhau vài giây trước khi chịu là không đọc ra.
|
|
28
|
+
const READ_ATTEMPTS = 3;
|
|
29
|
+
const READ_GAP_MS = 3000;
|
|
30
|
+
|
|
31
|
+
async function openOwnTab(page, context, log) {
|
|
32
|
+
if (!context || typeof context.newPage !== 'function') return { tab: page, own: false };
|
|
33
|
+
try {
|
|
34
|
+
return { tab: await context.newPage(), own: true };
|
|
35
|
+
} catch (e) {
|
|
36
|
+
log('warn', `[fb-page-check] không mở được tab riêng, dùng tab sẵn có (${String(e?.message || e).slice(0, 80)})`);
|
|
37
|
+
return { tab: page, own: false };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function run({ page, context, payload = {}, log = () => {} }) {
|
|
42
|
+
const t0 = Date.now();
|
|
43
|
+
const { tab, own } = await openOwnTab(page, context, log);
|
|
44
|
+
// Tab mới mở trong một browser người đang xem là tab NỀN — Chrome bóp nhịp
|
|
45
|
+
// vẽ của nó, Facebook có thể chưa dựng xong nav khi ta đọc.
|
|
46
|
+
const focus = own ? await enableFocusEmulation(tab, log) : null;
|
|
47
|
+
try {
|
|
48
|
+
await tab.goto('https://www.facebook.com/', { waitUntil: 'domcontentloaded', timeout: 60_000 });
|
|
49
|
+
await tab.waitForLoadState('networkidle', { timeout: 15_000 }).catch(() => {});
|
|
50
|
+
await tab.waitForTimeout(3000);
|
|
51
|
+
await assertAccountUsable(tab, log);
|
|
52
|
+
|
|
53
|
+
let mode = await readProfileMode(tab);
|
|
54
|
+
for (let i = 1; i < READ_ATTEMPTS && mode.mode === 'unknown'; i++) {
|
|
55
|
+
await tab.waitForTimeout(READ_GAP_MS);
|
|
56
|
+
mode = await readProfileMode(tab);
|
|
57
|
+
}
|
|
58
|
+
const durationSec = Math.round((Date.now() - t0) / 1000);
|
|
59
|
+
log('info', `[fb-page-check] ${mode.why}${mode.timelineLabel ? ` timeline="${mode.timelineLabel}"` : ''}`
|
|
60
|
+
+ ` nav="${mode.navSample}" (${durationSec}s, kênh ${payload.channel_id || '?'})`);
|
|
61
|
+
return {
|
|
62
|
+
profile_mode: mode.mode,
|
|
63
|
+
why: mode.why,
|
|
64
|
+
timeline_label: mode.timelineLabel || '',
|
|
65
|
+
nav_sample: mode.navSample || '',
|
|
66
|
+
duration_sec: durationSec,
|
|
67
|
+
};
|
|
68
|
+
} finally {
|
|
69
|
+
if (focus) await focus.detach().catch(() => {});
|
|
70
|
+
if (own) await tab.close().catch(() => {});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = { run, attachIfRunning: true };
|
|
@@ -4691,6 +4691,10 @@ async function runOnce({ page, payload, log }) {
|
|
|
4691
4691
|
reels_before: reelIdsBefore ? reelIdsBefore.slice(0, 300) : undefined,
|
|
4692
4692
|
reels_tab_url: reelsTabUrlBefore || reelsTabUrl || undefined,
|
|
4693
4693
|
video_path_local: videoPath,
|
|
4694
|
+
// Danh tính đọc lúc mở trang (bước identity check ở trên). 'page' → API
|
|
4695
|
+
// ghi cờ Page cho kênh, cờ cũ `personal`/`unknown` tự lành mà không phải
|
|
4696
|
+
// chờ phiên nuôi (xem api services/FacebookPublishGuard).
|
|
4697
|
+
profile_mode: profileMode.mode,
|
|
4694
4698
|
};
|
|
4695
4699
|
} finally {
|
|
4696
4700
|
safeUnlink(videoPath);
|