channel-worker 2.5.151 → 2.5.153

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.
@@ -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
- const hadCloseTimer = !!(this._pwCloseTimers && this._pwCloseTimers.has(profileId));
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', 'comment_facebook']);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "channel-worker",
3
- "version": "2.5.151",
3
+ "version": "2.5.153",
4
4
  "description": "Channel Manager worker daemon — runs on remote machines to execute video pipeline jobs",
5
5
  "main": "lib/daemon.js",
6
6
  "bin": {
@@ -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
- // Ghé trang chủ TRƯỚC rồi mới sang bài, dù biết thừa link bài.
258
+ // Vào THẲNG bài, kiểm phiên ngay tại đó.
259
259
  //
260
- // Lý do không kiểm tra phiên ngay trên trang bài: lib/fb-guard đọc chữ trong
261
- // body để đoán checkpoint/khoá tài khoản, mà trang bài thì đầy BÌNH LUẬN của
262
- // người lạ — một người bình luận "tài khoản của bạn đã bị vô hiệu hoá" là đủ
263
- // để script tự kết luận kênh mình chết. Trang chủ có feed thật, fb-guard
264
- // thấy feed là trả 'ok' ngay, không cần đọc chữ.
265
- await page.goto('https://www.facebook.com/', { waitUntil: 'domcontentloaded', timeout: NAV_TIMEOUT_MS }).catch(() => {});
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.
@@ -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
- async function readAccountState(page) {
46
- const url = page.url() || '';
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
- const st = await readAccountState(page);
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,
@@ -166,6 +166,7 @@ async function navigateFacebookReelCreate(page, log) {
166
166
  await page.goto(FB_REEL_CREATE_URL, { waitUntil: 'commit', timeout: 60_000 });
167
167
  await page.waitForLoadState('domcontentloaded', { timeout: 60_000 }).catch(() => {});
168
168
  await pause(page, 7000);
169
+ await dismissCookieConsent(page, log);
169
170
  }
170
171
 
171
172
  // FB drops a "Thông báo mới" toast at the BOTTOM-LEFT of the wall (e.g. "… và
@@ -430,6 +431,84 @@ async function dismissBsOnboarding(page, log) {
430
431
  }
431
432
  }
432
433
 
434
+ // Hộp hỏi cookie của Facebook — "Cho phép sử dụng cookie của Facebook trên
435
+ // trình duyệt này?" — là lớp phủ giữa màn hình, che cả trang và ăn mọi cú bấm.
436
+ // Sự cố 06-07/10/2026, kênh "Mẹo làm tóc tại nhà" (profile kenh111,
437
+ // win-worker-2): 7/7 lượt đăng hỏng, cả 14 ảnh dump no-advance-2 đều là hộp
438
+ // này. Bấm "Thước phim" trượt → nhảy /reels/create/ bị đẩy về feed /reel/<id
439
+ // của người khác> → bấm Tiếp trượt → "composer closed (before publish)".
440
+ // Profile có phiên nuôi thì thoát vì nurture/warmup bấm "Cho phép tất cả
441
+ // cookie"; profile không được nuôi thì kẹt mãi, thử lại bao nhiêu lần cũng thế.
442
+ //
443
+ // Bấm CÙNG lựa chọn với nurture_facebook/warmup_facebook (cho phép tất cả) để
444
+ // một profile không mang hai lựa chọn cookie khác nhau; không thấy nút đó mới
445
+ // lui sang "Từ chối cookie không bắt buộc" — việc cần là gỡ lớp phủ.
446
+ const COOKIE_CONSENT_RE = /Cho phép sử dụng cookie của Facebook|Allow (?:the )?use of cookies from Facebook/i;
447
+ const COOKIE_CONSENT_VERBS = ['Cho phép tất cả cookie', 'Allow all cookies', 'Từ chối cookie không bắt buộc', 'Decline optional cookies'];
448
+
449
+ function isCookieConsentText(txt) {
450
+ return COOKIE_CONSENT_RE.test(String(txt || ''));
451
+ }
452
+
453
+ // Tìm hộp hỏi cookie đang hiện; thấy nút thì đánh dấu `__fbpw_cookie__`.
454
+ // Trả { found, verb } — verb null nghĩa là có hộp mà không thấy nút nào khớp.
455
+ // Không ném lỗi: trang vừa đóng / trang giả thiếu evaluate → coi như không có.
456
+ async function findCookieConsent(page) {
457
+ try {
458
+ return await page.evaluate(({ src, verbs }) => {
459
+ document.querySelectorAll('[__fbpw_cookie__]').forEach((el) => el.removeAttribute('__fbpw_cookie__'));
460
+ const re = new RegExp(src, 'i');
461
+ const shown = (el) => {
462
+ const r = el.getBoundingClientRect();
463
+ if (r.width < 8 || r.height < 8) return false;
464
+ const cs = getComputedStyle(el);
465
+ return cs.visibility !== 'hidden' && cs.display !== 'none';
466
+ };
467
+ for (const dlg of document.querySelectorAll("[role='dialog']")) {
468
+ if (!shown(dlg) || !re.test((dlg.innerText || '').slice(0, 600))) continue;
469
+ const btns = [...dlg.querySelectorAll("[role='button'], button")].filter(shown);
470
+ for (const v of verbs) {
471
+ const b = btns.find((x) => (x.innerText || '').trim() === v || (x.getAttribute('aria-label') || '').trim() === v);
472
+ if (b) {
473
+ b.setAttribute('__fbpw_cookie__', '1');
474
+ return { found: true, verb: v };
475
+ }
476
+ }
477
+ return { found: true, verb: null };
478
+ }
479
+ return { found: false, verb: null };
480
+ }, { src: COOKIE_CONSENT_RE.source, verbs: COOKIE_CONSENT_VERBS }) || { found: false, verb: null };
481
+ } catch {
482
+ return { found: false, verb: null };
483
+ }
484
+ }
485
+
486
+ // Gỡ hộp hỏi cookie nếu có. KHÔNG ném lỗi: gỡ không được thì luồng cũ chạy
487
+ // tiếp, và composerClosedError sẽ gọi đúng tên bệnh trong câu lỗi.
488
+ async function dismissCookieConsent(page, log) {
489
+ let hit = await findCookieConsent(page);
490
+ if (!hit.found) return { found: false, dismissed: false, verb: null };
491
+ const verb = hit.verb;
492
+ for (let attempt = 1; attempt <= 2 && hit.verb; attempt++) {
493
+ log('info', `[fb-pw] hộp hỏi cookie của Facebook đang che trang — bấm "${hit.verb}" (${attempt}/2)`);
494
+ try {
495
+ await page.locator("[__fbpw_cookie__='1']").first().click({ timeout: 3000 });
496
+ } catch {
497
+ // Lớp khác đè lên nút → bấm thẳng trong DOM.
498
+ await page.evaluate(() => { const b = document.querySelector("[__fbpw_cookie__='1']"); if (b) b.click(); }).catch(() => {});
499
+ }
500
+ await pause(page, 1500);
501
+ hit = await findCookieConsent(page);
502
+ if (!hit.found) {
503
+ log('info', '[fb-pw] đã gỡ hộp hỏi cookie của Facebook');
504
+ return { found: true, dismissed: true, verb };
505
+ }
506
+ }
507
+ await page.evaluate(() => document.querySelectorAll('[__fbpw_cookie__]').forEach((el) => el.removeAttribute('__fbpw_cookie__'))).catch(() => {});
508
+ log('warn', `[fb-pw] hộp hỏi cookie của Facebook VẪN che trang${verb ? ` sau khi bấm "${verb}"` : ' — không thấy nút "Cho phép tất cả cookie"/"Từ chối cookie không bắt buộc"'}`);
509
+ return { found: true, dismissed: false, verb };
510
+ }
511
+
433
512
  // ── Large-file upload bypass ───────────────────────────────────────────────
434
513
  // Playwright caps file transfers at 50MB when the browser is reached over
435
514
  // connectOverCDP (our Nstbrowser case):
@@ -1813,9 +1892,19 @@ function safeComposerRetryError(message) {
1813
1892
  async function composerClosedError(page, log, fallbackMessage) {
1814
1893
  let fbError = false;
1815
1894
  try { fbError = classifyComposerText(await readPageAlerts(page)).fbError; } catch { /* đọc lỗi thì cứ dùng câu cũ */ }
1816
- if (!fbError) return safeComposerRetryError(fallbackMessage);
1817
- log('warn', '[fb-pw] Facebook hiện toast "Đã xảy ra lỗi… lỗi kỹ thuật" — chính FB hỏng rồi gỡ composer, không phải lỗi bài/tài khoản');
1818
- return safeComposerRetryError(`Facebook báo lỗi kỹ thuật ("Đã xảy ra lỗi… chúng tôi đang khắc phục") rồi tự gỡ trình soạn — ${fallbackMessage}`);
1895
+ if (fbError) {
1896
+ log('warn', '[fb-pw] Facebook hiện toast "Đã xảy ra lỗi… lỗi kỹ thuật" — chính FB hỏng rồi gỡ composer, không phải lỗi bài/tài khoản');
1897
+ return safeComposerRetryError(`Facebook báo lỗi kỹ thuật ("Đã xảy ra lỗi… chúng tôi đang khắc phục") rồi tự gỡ trình soạn — ${fallbackMessage}`);
1898
+ }
1899
+ // Hộp hỏi cookie (xem dismissCookieConsent) vẫn còn = gỡ không được → nói
1900
+ // thẳng tên bệnh, đừng để người đọc đi soi file video / ảnh bìa.
1901
+ let cookieWall = false;
1902
+ try { cookieWall = isCookieConsentText(await readComposerText(page)); } catch { /* đọc lỗi thì cứ dùng câu cũ */ }
1903
+ if (cookieWall) {
1904
+ log('warn', '[fb-pw] hộp hỏi cookie của Facebook vẫn che trang — composer không bấm được, không phải lỗi bài/file');
1905
+ return safeComposerRetryError(`Facebook hiện hộp hỏi cookie ("Cho phép sử dụng cookie của Facebook…") che trang, bot không gỡ được — mở profile bấm "Cho phép tất cả cookie" — ${fallbackMessage}`);
1906
+ }
1907
+ return safeComposerRetryError(fallbackMessage);
1819
1908
  }
1820
1909
 
1821
1910
  // Toast "Thông báo mới" (ai đó thích/bình luận reel) nổi lên góc trái và ĂN
@@ -2365,7 +2454,7 @@ async function runOnce({ page, payload, log }) {
2365
2454
  } = payload || {};
2366
2455
  if (!video_url) throw new Error('No video_url provided');
2367
2456
 
2368
- log('info', '[fb-pw] selectors version=2026.09.26-cover-wait-upload');
2457
+ log('info', '[fb-pw] selectors version=2026.10.07-cookie-consent');
2369
2458
 
2370
2459
  page.on('dialog', (d) => { d.accept().catch(() => {}); });
2371
2460
 
@@ -2442,6 +2531,8 @@ async function runOnce({ page, payload, log }) {
2442
2531
  await page.waitForLoadState('networkidle', { timeout: 30_000 }).catch(() => {});
2443
2532
  await pause(page, 4000);
2444
2533
  log('info', `[fb-pw] home loaded — url=${page.url()}`);
2534
+ // Hộp hỏi cookie che cả trang → gỡ trước khi đọc/bấm bất cứ thứ gì.
2535
+ await dismissCookieConsent(page, log);
2445
2536
  // Fail with a stable account-state code before selectors turn a logout or
2446
2537
  // checkpoint into a generic "Reels entry not found". The API consumes the
2447
2538
  // code and opens the Facebook auto-publish circuit breaker.
@@ -2497,6 +2588,7 @@ async function runOnce({ page, payload, log }) {
2497
2588
  + (candidates.length > 1 ? ` (ứng viên ${opened.tried}/${candidates.length})` : ''));
2498
2589
  await navigateFacebookHome(page, log);
2499
2590
  await pause(page, 2500);
2591
+ await dismissCookieConsent(page, log);
2500
2592
  } else {
2501
2593
  log('warn', '[fb-pw] không tìm được link tab Reels trước khi đăng — bỏ qua ảnh nền');
2502
2594
  }
@@ -4895,6 +4987,11 @@ module.exports.__testables = {
4895
4987
  classifyComposerText,
4896
4988
  readPageAlerts,
4897
4989
  composerClosedError,
4990
+ // Hộp hỏi cookie che cả trang (kenh111, 06-07/10).
4991
+ isCookieConsentText,
4992
+ findCookieConsent,
4993
+ dismissCookieConsent,
4994
+ COOKIE_CONSENT_VERBS,
4898
4995
  isComposerDialogSignature,
4899
4996
  discoverReelsTabUrl,
4900
4997
  scrapeReelTileIds,