channel-worker 2.5.144 → 2.5.145

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 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
 
@@ -530,6 +530,9 @@ class CommandPoller {
530
530
  // only serializes within a profile. Wait up to 30 min with 5s polling
531
531
  // (upload typically 1-3 min); stale-cleanup picks up zombies separately.
532
532
  // Lượt mới của cùng profile → huỷ hẹn đóng, giữ browser cho nó dùng lại.
533
+ // Nhớ lại là CÓ hẹn: browser đó do automation mở, lượt này xong phải hẹn
534
+ // đóng lại (xem nhánh runInfo.attached ở cuối hàm).
535
+ const hadCloseTimer = !!(this._pwCloseTimers && this._pwCloseTimers.has(profileId));
533
536
  if (this._pwCloseTimers && this._pwCloseTimers.has(profileId)) {
534
537
  clearTimeout(this._pwCloseTimers.get(profileId));
535
538
  this._pwCloseTimers.delete(profileId);
@@ -599,6 +602,7 @@ class CommandPoller {
599
602
  });
600
603
 
601
604
  const keepOpen = await this._keepOpenCfg();
605
+ const runInfo = {};
602
606
 
603
607
  // 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
608
  // API khai tử lệnh đăng 'running' quá 20 phút không cập nhật; một lượt
@@ -618,6 +622,7 @@ class CommandPoller {
618
622
  payload,
619
623
  log,
620
624
  keepOpen: keepOpen.enabled,
625
+ info: runInfo,
621
626
  });
622
627
  // postCommandResult (not updateCommand) so the existing extension-side
623
628
  // logic mirrors status into ContentJob + ContentIdea.publish_results +
@@ -655,6 +660,11 @@ class CommandPoller {
655
660
  this._keptOpenProfiles.set(profileId, Date.now());
656
661
  console.log(`[keep-open] giữ profile ${profileId} mở sau ${scriptName} (${this._keptOpenProfiles.size}/${keepOpen.max})`);
657
662
  await this._trimKeptProfiles(keepOpen.max).catch((e) => console.warn(`[keep-open] trim lỗi: ${e.message}`));
663
+ } else if (runInfo.attached && !hadCloseTimer) {
664
+ // Script nối vào browser ĐANG MỞ SẴN mà không có hẹn đóng nào của
665
+ // automation (vd "Kiểm tra Page ngay" lúc người vừa tạo Page còn ngồi
666
+ // trong cửa sổ) → không phải browser của mình, để nguyên cho người đó.
667
+ console.log(`[commands/pw] ${scriptName}: nối vào profile ${profileId} đang mở sẵn — để nguyên, không hẹn đóng`);
658
668
  } else if (CLOSE_AFTER.has(scriptName) && this.nst) {
659
669
  try {
660
670
  await this.nst.stopProfile(profileId);
@@ -52,33 +52,35 @@ async function enableFocusEmulation(page, log) {
52
52
  }
53
53
  }
54
54
 
55
- async function runPlaywrightScript({ nst, profileId, scriptName, payload = {}, log = () => {}, keepOpen = false }) {
56
- let playwright;
57
- try { playwright = require('playwright-core'); }
58
- catch (e) { throw new Error(`playwright-core not installed in channel-worker (npm i playwright-core in the worker dir): ${e.message}`); }
59
-
60
- const script = loadScript(scriptName);
61
-
62
- log('info', `[pw] launching NST profile ${profileId}…`);
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
- // keepOpen: máy bị chặn số lượt mở/ngày. Tắt-mở lại một profile đang chạy
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
- log('info', `[pw] profile đang chạy — nối vào cổng debug ${live.port} (không tốn lượt mở)`);
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "channel-worker",
3
- "version": "2.5.144",
3
+ "version": "2.5.145",
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": {
@@ -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);