channel-worker 2.5.151 → 2.5.152
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 +19 -2
- package/package.json +1 -1
- package/scripts/comment_facebook.js +9 -11
- package/scripts/lib/fb-guard.js +42 -4
package/lib/command-poller.js
CHANGED
|
@@ -9,6 +9,14 @@ const { ApiUnreachableError } = require('./api-client');
|
|
|
9
9
|
// tới và dùng lại browser, nhưng không được để mở qua đêm.
|
|
10
10
|
const PW_IDLE_CLOSE_MS = Number(process.env.PW_IDLE_CLOSE_MS || 5 * 60 * 1000);
|
|
11
11
|
|
|
12
|
+
// Quãng rảnh trước khi đóng profile sau một lượt THẢ BÌNH LUẬN. Trước
|
|
13
|
+
// 07/10/2026 lượt thả đóng profile ngay, nên lấy link + thả món 2, món 3 của
|
|
14
|
+
// cùng bài là mỗi món mở lại profile từ đầu — ~10-12s mỗi lần trên win-worker
|
|
15
|
+
// (đo prod 06/10: launch → connectOverCDP mất 9-12s trong lượt 50-80s). Một
|
|
16
|
+
// phút đủ cho người chọn món tiếp; ngắn hơn hẳn 5 phút của lượt đăng bài vì
|
|
17
|
+
// bình luận rải rác trên nhiều kênh, giữ lâu là chất đống browser.
|
|
18
|
+
const COMMENT_IDLE_CLOSE_MS = Number(process.env.COMMENT_IDLE_CLOSE_MS || 60 * 1000);
|
|
19
|
+
|
|
12
20
|
// Người bấm "Mở trình duyệt" trên web → profile đó là của NGƯỜI trong quãng
|
|
13
21
|
// này, automation không được đóng ngang. Trước 16/09/2026 không có hàng rào
|
|
14
22
|
// nào: hẹn đóng 300s của lượt đăng bài trước đó vẫn chạy và đóng profile ngay
|
|
@@ -677,7 +685,7 @@ class CommandPoller {
|
|
|
677
685
|
// Lượt mới của cùng profile → huỷ hẹn đóng, giữ browser cho nó dùng lại.
|
|
678
686
|
// Nhớ lại là CÓ hẹn: browser đó do automation mở, lượt này xong phải hẹn
|
|
679
687
|
// đóng lại (xem nhánh runInfo.attached ở cuối hàm).
|
|
680
|
-
|
|
688
|
+
let hadCloseTimer = !!(this._pwCloseTimers && this._pwCloseTimers.has(profileId));
|
|
681
689
|
if (this._pwCloseTimers && this._pwCloseTimers.has(profileId)) {
|
|
682
690
|
clearTimeout(this._pwCloseTimers.get(profileId));
|
|
683
691
|
this._pwCloseTimers.delete(profileId);
|
|
@@ -709,6 +717,11 @@ class CommandPoller {
|
|
|
709
717
|
}
|
|
710
718
|
await new Promise(r => setTimeout(r, 5000));
|
|
711
719
|
}
|
|
720
|
+
// Lượt trước của CÙNG profile vừa xong trong lúc mình xếp hàng thì nó hẹn
|
|
721
|
+
// đóng SAU lúc mình hỏi ở đầu hàm. Huỷ hẹn đó và nhớ là có: để nguyên thì
|
|
722
|
+
// hẹn ấy nổ giữa lượt này (thấy profile bận, tự bỏ), còn cuối lượt mình
|
|
723
|
+
// tưởng browser là của người (nhánh runInfo.attached) → profile mở mãi.
|
|
724
|
+
if (this._cancelPwClose(profileId)) hadCloseTimer = true;
|
|
712
725
|
this._pwInFlight.set(profileId, command.type);
|
|
713
726
|
console.log(`[commands/pw] ${command.type} → scripts/${scriptName}.js (profile=${profileId})`);
|
|
714
727
|
|
|
@@ -814,7 +827,7 @@ class CommandPoller {
|
|
|
814
827
|
//
|
|
815
828
|
// Scoped to the browse-only scripts. Publish keeps its profile open on
|
|
816
829
|
// purpose: one profile serves several platform uploads back-to-back.
|
|
817
|
-
const CLOSE_AFTER = new Set(['nurture_facebook', 'warmup_facebook', 'warmup_youtube', 'warmup_tiktok', 'fetch_facebook_reel_stats'
|
|
830
|
+
const CLOSE_AFTER = new Set(['nurture_facebook', 'warmup_facebook', 'warmup_youtube', 'warmup_tiktok', 'fetch_facebook_reel_stats']);
|
|
818
831
|
// Cờ vừa bị TẮT trên web → bỏ profile khỏi sổ giữ mở, để nó quay về vòng
|
|
819
832
|
// đời đóng-mở bình thường ngay từ lượt này.
|
|
820
833
|
if (!keepOpen.enabled && this._keptOpenProfiles) this._keptOpenProfiles.delete(profileId);
|
|
@@ -830,6 +843,10 @@ class CommandPoller {
|
|
|
830
843
|
// automation (vd "Kiểm tra Page ngay" lúc người vừa tạo Page còn ngồi
|
|
831
844
|
// trong cửa sổ) → không phải browser của mình, để nguyên cho người đó.
|
|
832
845
|
console.log(`[commands/pw] ${scriptName}: nối vào profile ${profileId} đang mở sẵn — để nguyên, không hẹn đóng`);
|
|
846
|
+
} else if (scriptName === 'comment_facebook' && this.nst) {
|
|
847
|
+
// Hẹn đóng NGẮN thay vì đóng ngay — món kế của cùng bài dùng lại
|
|
848
|
+
// browser này (xem COMMENT_IDLE_CLOSE_MS).
|
|
849
|
+
this._schedulePwClose(profileId, scriptName, COMMENT_IDLE_CLOSE_MS);
|
|
833
850
|
} else if (CLOSE_AFTER.has(scriptName) && this.nst) {
|
|
834
851
|
try {
|
|
835
852
|
await this.nst.stopProfile(profileId);
|
package/package.json
CHANGED
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
// script khai thành công theo thao tác của chính nó, không theo thứ trang web
|
|
30
30
|
// hiện ra. API hạ mọi kết quả thiếu `verified` xuống 'failed'.
|
|
31
31
|
|
|
32
|
-
const { assertAccountUsable, readPublishBlock } = require('./lib/fb-guard');
|
|
32
|
+
const { assertAccountUsable, assertSessionOnContentPage, readPublishBlock } = require('./lib/fb-guard');
|
|
33
33
|
const { humanClick, humanType, pause, randInt } = require('./lib/human');
|
|
34
34
|
|
|
35
35
|
// Chụp màn hình lúc trượt — mượn của upload_facebook, nhưng require MUỘN và có
|
|
@@ -255,20 +255,18 @@ async function run({ page, payload = {}, log }) {
|
|
|
255
255
|
if (!link) throw new Error('thiếu link — không có gì để đọc lại xác minh');
|
|
256
256
|
if (!text.includes(link)) throw new Error('text không chứa link — API dựng sai câu bình luận');
|
|
257
257
|
|
|
258
|
-
//
|
|
258
|
+
// Vào THẲNG bài, kiểm phiên ngay tại đó.
|
|
259
259
|
//
|
|
260
|
-
//
|
|
261
|
-
//
|
|
262
|
-
// người lạ —
|
|
263
|
-
//
|
|
264
|
-
//
|
|
265
|
-
|
|
266
|
-
await pause(page, 2200);
|
|
267
|
-
await assertAccountUsable(page, log);
|
|
268
|
-
|
|
260
|
+
// Bản trước ghé trang chủ trước để fb-guard đọc phiên trên một trang có feed
|
|
261
|
+
// thật: kiểm đủ (đọc chữ trong body) trên trang bài là dính bẫy BÌNH LUẬN của
|
|
262
|
+
// người lạ — ai đó viết "tài khoản của bạn đã bị vô hiệu hoá" là script tự
|
|
263
|
+
// kết luận kênh mình chết. Chuyến ghé đó tốn 5-8s mỗi lượt thả (đo prod
|
|
264
|
+
// 06/10/2026, lượt 50s trên win-worker). assertSessionOnContentPage chỉ xét
|
|
265
|
+
// URL bị đá đi và form đăng nhập — hai thứ người lạ không bình luận ra được.
|
|
269
266
|
log('info', `[fb-cmt] mở bài ${postUrl}`);
|
|
270
267
|
await page.goto(postUrl, { waitUntil: 'domcontentloaded', timeout: NAV_TIMEOUT_MS });
|
|
271
268
|
await pause(page, 2600);
|
|
269
|
+
await assertSessionOnContentPage(page, log);
|
|
272
270
|
|
|
273
271
|
// Trang bài mở ra là trang LỖI / bài đã xoá thì dừng ngay, đừng đi tìm ô soạn
|
|
274
272
|
// rồi báo "không thấy ô bình luận" — hai chuyện khác hẳn nhau khi đọc log.
|
package/scripts/lib/fb-guard.js
CHANGED
|
@@ -42,13 +42,19 @@ const CHECKPOINT_RE = /(xác nhận danh tính)|(xác minh danh tính)|(hãy xá
|
|
|
42
42
|
|
|
43
43
|
// Read the account state of whatever page is currently open.
|
|
44
44
|
// Returns { status: 'ok'|'logged_out'|'checkpoint'|'banned', detail }.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
// URL is the strongest signal — FB redirects hard on all three states.
|
|
45
|
+
// URL is the strongest signal — FB redirects hard on all three states.
|
|
46
|
+
function readRedirect(url) {
|
|
48
47
|
if (/\/checkpoint\//i.test(url)) return { status: 'checkpoint', detail: `redirected to ${url.slice(0, 120)}` };
|
|
49
48
|
if (/facebook\.com\/(login|recover)\b|login\.php|\/login\/device-based/i.test(url)) {
|
|
50
49
|
return { status: 'logged_out', detail: `redirected to ${url.slice(0, 120)}` };
|
|
51
50
|
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function readAccountState(page) {
|
|
55
|
+
const url = page.url() || '';
|
|
56
|
+
const redirected = readRedirect(url);
|
|
57
|
+
if (redirected) return redirected;
|
|
52
58
|
|
|
53
59
|
const dom = await page.evaluate(() => {
|
|
54
60
|
const vis = (el) => {
|
|
@@ -95,7 +101,37 @@ async function readAccountState(page) {
|
|
|
95
101
|
// checkpoint is common — FB throws one the moment automated-looking activity
|
|
96
102
|
// trips a threshold).
|
|
97
103
|
async function assertAccountUsable(page, log) {
|
|
98
|
-
|
|
104
|
+
return throwIfUnusable(await readAccountState(page), log);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Kiểm phiên NGAY TRÊN trang bài/reel, khỏi ghé trang chủ trước — lượt thả
|
|
108
|
+
// bình luận mất 5-8s cho riêng chuyến ghé đó (đo prod 06/10/2026).
|
|
109
|
+
//
|
|
110
|
+
// Chỉ dùng tín hiệu KHÔNG thể đến từ bình luận của người lạ: URL bị đá sang
|
|
111
|
+
// checkpoint/đăng nhập, và form đăng nhập hiện trên trang (trang của người đã
|
|
112
|
+
// đăng nhập không có ô email/mật khẩu). KHÔNG đọc chữ trong body như
|
|
113
|
+
// readAccountState: trang bài đầy chữ người lạ, một bình luận "tài khoản của
|
|
114
|
+
// bạn đã bị vô hiệu hoá" là đủ để kết luận oan kênh mình chết. Form đăng nhập
|
|
115
|
+
// xét mà KHÔNG cần vắng bài: reel công khai vẫn vẽ bài cho người chưa đăng
|
|
116
|
+
// nhập, kèm hộp mời đăng nhập — "có bài" ở đây không chứng minh phiên sống.
|
|
117
|
+
async function readSessionOnContentPage(page) {
|
|
118
|
+
const redirected = readRedirect(page.url() || '');
|
|
119
|
+
if (redirected) return redirected;
|
|
120
|
+
const hasLoginForm = await page.evaluate(() => {
|
|
121
|
+
const el = document.querySelector("input[name='email'], input[name='pass'], input#email, input#pass");
|
|
122
|
+
if (!el) return false;
|
|
123
|
+
const r = el.getBoundingClientRect();
|
|
124
|
+
return r.width > 8 && r.height > 8 && el.offsetParent !== null;
|
|
125
|
+
}).catch(() => false);
|
|
126
|
+
if (hasLoginForm) return { status: 'logged_out', detail: 'login form on post page (session expired)' };
|
|
127
|
+
return { status: 'ok', detail: '' };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function assertSessionOnContentPage(page, log) {
|
|
131
|
+
return throwIfUnusable(await readSessionOnContentPage(page), log);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function throwIfUnusable(st, log) {
|
|
99
135
|
if (st.status === 'ok') return st;
|
|
100
136
|
const code = CODES[st.status];
|
|
101
137
|
const human = st.status === 'banned'
|
|
@@ -199,6 +235,8 @@ function publishRateLimitError(blockHit) {
|
|
|
199
235
|
module.exports = {
|
|
200
236
|
readAccountState,
|
|
201
237
|
assertAccountUsable,
|
|
238
|
+
readSessionOnContentPage,
|
|
239
|
+
assertSessionOnContentPage,
|
|
202
240
|
readProfileMode,
|
|
203
241
|
classifyProfileModeSignals,
|
|
204
242
|
pageNotConfiguredError,
|