channel-worker 2.5.69 → 2.5.71
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/daemon.js +19 -3
- package/lib/nst-manager.js +25 -4
- package/package.json +1 -1
- package/scripts/upload_facebook.js +72 -28
package/lib/daemon.js
CHANGED
|
@@ -14,6 +14,12 @@ class Daemon {
|
|
|
14
14
|
// Nstbrowser agent of THIS Windows session. On a multi-user box the second
|
|
15
15
|
// session's agent binds 8849+ because 8848 is taken machine-wide.
|
|
16
16
|
if (config.nst_api_address) process.env.NST_API_ADDRESS = config.nst_api_address;
|
|
17
|
+
// Tell every NstManager we are pinned to one Windows user's agent, so an
|
|
18
|
+
// unresolved address REFUSES commands instead of defaulting to 8848
|
|
19
|
+
// (= the other session's agent on a multi-user box).
|
|
20
|
+
if (config.nst_agent_user && !config.nst_api_address) {
|
|
21
|
+
process.env.NST_AGENT_PINNED_USER = config.nst_agent_user;
|
|
22
|
+
}
|
|
17
23
|
this.api = new ApiClient(config.api_url, config.worker_token);
|
|
18
24
|
this.heartbeat = new Heartbeat(this.api, config.worker_id, 30000, config);
|
|
19
25
|
this.poller = new JobPoller(this.api, config);
|
|
@@ -46,9 +52,16 @@ class Daemon {
|
|
|
46
52
|
return;
|
|
47
53
|
}
|
|
48
54
|
if (!found) {
|
|
49
|
-
|
|
55
|
+
// The pinned user's agent is GONE. Drop a previously resolved address —
|
|
56
|
+
// after a relog the same port can belong to the other session's agent,
|
|
57
|
+
// and driving it opens blank profiles. Fail-closed until it re-appears.
|
|
58
|
+
if (process.env.NST_API_ADDRESS) {
|
|
59
|
+
console.warn(`[daemon] NST agent for "${user}" disappeared `
|
|
60
|
+
+ `(was ${process.env.NST_API_ADDRESS}) — refusing NST commands until it is back.`);
|
|
61
|
+
delete process.env.NST_API_ADDRESS;
|
|
62
|
+
} else if (!quiet) {
|
|
50
63
|
console.warn(`[daemon] No Nstbrowser agent for Windows user "${user}" — `
|
|
51
|
-
+ 'NST commands
|
|
64
|
+
+ 'NST commands are refused (fail-closed) until that user is logged in.');
|
|
52
65
|
}
|
|
53
66
|
return;
|
|
54
67
|
}
|
|
@@ -73,7 +86,10 @@ class Daemon {
|
|
|
73
86
|
╚══════════════════════════════════════╝
|
|
74
87
|
`);
|
|
75
88
|
console.log(`[daemon] cache server port: ${this.config.cache_port || 8849}`);
|
|
76
|
-
console.log(`[daemon] NST agent: ${process.env.NST_API_ADDRESS
|
|
89
|
+
console.log(`[daemon] NST agent: ${process.env.NST_API_ADDRESS
|
|
90
|
+
|| (this.config.nst_agent_user
|
|
91
|
+
? `UNRESOLVED — refusing NST commands until agent of "${this.config.nst_agent_user}" is found`
|
|
92
|
+
: 'http://localhost:8848/api/v2 (default)')}`);
|
|
77
93
|
|
|
78
94
|
// Start local cache server (extension shares this across all profiles)
|
|
79
95
|
try {
|
package/lib/nst-manager.js
CHANGED
|
@@ -24,10 +24,31 @@ function nstLaunchError(response, { profileId = '', runningCount = 0 } = {}) {
|
|
|
24
24
|
class NstManager {
|
|
25
25
|
constructor(apiKey, options = {}) {
|
|
26
26
|
this.apiKey = apiKey;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
this._apiAddress = options.apiAddress || null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Multi-user Windows: each logged-in session runs its OWN Nstbrowser agent,
|
|
31
|
+
// and the second one lands on 8849+ because 8848 is already taken machine-wide.
|
|
32
|
+
// Resolved at CALL time, not construct time: the daemon's agent watcher
|
|
33
|
+
// rewrites NST_API_ADDRESS whenever ports move, and an instance built before
|
|
34
|
+
// the first resolution must not keep a stale target forever.
|
|
35
|
+
//
|
|
36
|
+
// Fail-closed: when the daemon is pinned to a Windows user
|
|
37
|
+
// (NST_AGENT_PINNED_USER) and that user's agent hasn't been located, REFUSE
|
|
38
|
+
// instead of falling back to 8848 — on a two-session box 8848 is the OTHER
|
|
39
|
+
// user's agent, and opening a profile there starts a blank Chrome: FB shows
|
|
40
|
+
// a login form and fb-guard wrongly flags the account as logged out
|
|
41
|
+
// (35 channels hit 2026-08-31…09-02).
|
|
42
|
+
get baseUrl() {
|
|
43
|
+
if (this._apiAddress) return this._apiAddress;
|
|
44
|
+
if (process.env.NST_API_ADDRESS) return process.env.NST_API_ADDRESS;
|
|
45
|
+
const pinned = process.env.NST_AGENT_PINNED_USER;
|
|
46
|
+
if (pinned) {
|
|
47
|
+
throw new Error(`NST_AGENT_UNRESOLVED: chưa thấy Nstbrowser agent của Windows user "${pinned}" `
|
|
48
|
+
+ '— không fallback :8848 (agent của session khác → profile trắng → báo logout oan). '
|
|
49
|
+
+ 'Lệnh CHƯA chạm nền tảng; sẽ tự chạy lại khi agent của user đó lên.');
|
|
50
|
+
}
|
|
51
|
+
return 'http://localhost:8848/api/v2';
|
|
31
52
|
}
|
|
32
53
|
|
|
33
54
|
async api(path, options = {}) {
|
package/package.json
CHANGED
|
@@ -76,6 +76,22 @@ const REEL_ENTRY_SELECTORS = [
|
|
|
76
76
|
// 20 minutes, so give the real upload enough room instead of failing while FB
|
|
77
77
|
// is visibly still processing it.
|
|
78
78
|
const PUBLISH_ENABLE_TIMEOUT_MS = 10 * 60 * 1000;
|
|
79
|
+
// Facebook can spend several seconds tearing down the nested custom-thumbnail
|
|
80
|
+
// editor before it mounts the final Reel settings CTA. During that transition
|
|
81
|
+
// there is neither a visible "Tiếp" nor "Đăng" button. The old four-poll
|
|
82
|
+
// (~10s) shortcut returned false just before the CTA appeared, producing a
|
|
83
|
+
// false no-advance failure even though the final blue "Đăng" button was ready.
|
|
84
|
+
const PUBLISH_CTA_ABSENT_GRACE_MS = 30 * 1000;
|
|
85
|
+
|
|
86
|
+
// Text observed in the two thumbnail-editor cohorts. The nested variant has no
|
|
87
|
+
// useful dialog aria-label, so checking only "Chỉnh sửa hình thu nhỏ" misses it
|
|
88
|
+
// while Facebook is processing the saved image.
|
|
89
|
+
const THUMBNAIL_EDITOR_TEXT_MARKERS = [
|
|
90
|
+
'Chỉnh sửa hình thu nhỏ',
|
|
91
|
+
'Edit thumbnail',
|
|
92
|
+
'Tải hình thu nhỏ của riêng bạn lên',
|
|
93
|
+
'Upload your own thumbnail',
|
|
94
|
+
];
|
|
79
95
|
|
|
80
96
|
async function firstVisible(locator, max = 5) {
|
|
81
97
|
const n = Math.min(await locator.count().catch(() => 0), max);
|
|
@@ -474,11 +490,14 @@ async function setVideoFile(page, inputHandle, filePath, log, tag = 'fb-pw') {
|
|
|
474
490
|
// well past the 30s upload-wait, so findByVerbs (which skips disabled buttons)
|
|
475
491
|
// finds no publish CTA — and with no "Tiếp" on the last step the loop would
|
|
476
492
|
// throw no-advance. Poll until a bottom-half publish-verb button is present
|
|
477
|
-
// AND enabled. Returns true once enabled; false on timeout, or
|
|
478
|
-
// publish button ever appears (
|
|
479
|
-
async function waitForPublishEnabled(page, verbs, log, timeoutMs, tag = 'fb-pw', {
|
|
493
|
+
// AND enabled. Returns true once enabled; false on timeout, or after a guarded
|
|
494
|
+
// absence window when no publish button ever appears (not the final step).
|
|
495
|
+
async function waitForPublishEnabled(page, verbs, log, timeoutMs, tag = 'fb-pw', {
|
|
496
|
+
onPoll = null,
|
|
497
|
+
absentGraceMs = PUBLISH_CTA_ABSENT_GRACE_MS,
|
|
498
|
+
} = {}) {
|
|
480
499
|
const deadline = Date.now() + timeoutMs;
|
|
481
|
-
let announced = false, sawPresent = false,
|
|
500
|
+
let announced = false, sawPresent = false, absentSince = 0;
|
|
482
501
|
while (Date.now() < deadline) {
|
|
483
502
|
// The final metadata form mounts lazily on some FB cohorts. The first
|
|
484
503
|
// fillMetadata() immediately after Tiếp can run before its textbox exists,
|
|
@@ -513,9 +532,16 @@ async function waitForPublishEnabled(page, verbs, log, timeoutMs, tag = 'fb-pw',
|
|
|
513
532
|
}
|
|
514
533
|
if (st.present) {
|
|
515
534
|
sawPresent = true;
|
|
535
|
+
absentSince = 0;
|
|
516
536
|
if (!announced) { log('info', `[${tag}] final step: "Đăng" disabled (video still processing) — waiting up to ${Math.round(timeoutMs / 1000)}s…`); announced = true; }
|
|
517
|
-
} else if (!sawPresent
|
|
518
|
-
|
|
537
|
+
} else if (!sawPresent) {
|
|
538
|
+
if (!absentSince) absentSince = Date.now();
|
|
539
|
+
// The final settings form mounts lazily after the thumbnail editor closes.
|
|
540
|
+
// Give that transition a real time-based grace period instead of assuming
|
|
541
|
+
// four polls means this is not the final step.
|
|
542
|
+
if (Date.now() - absentSince >= absentGraceMs) {
|
|
543
|
+
return { enabled: false, sawPresent: false };
|
|
544
|
+
}
|
|
519
545
|
}
|
|
520
546
|
await pause(page, 2500);
|
|
521
547
|
}
|
|
@@ -523,6 +549,31 @@ async function waitForPublishEnabled(page, verbs, log, timeoutMs, tag = 'fb-pw',
|
|
|
523
549
|
return { enabled: false, sawPresent };
|
|
524
550
|
}
|
|
525
551
|
|
|
552
|
+
async function isThumbnailEditorVisible(page) {
|
|
553
|
+
return page.evaluate((textMarkers) => {
|
|
554
|
+
const isVisible = (el) => {
|
|
555
|
+
const r = el.getBoundingClientRect();
|
|
556
|
+
if (r.width < 8 || r.height < 8) return false;
|
|
557
|
+
const cs = getComputedStyle(el);
|
|
558
|
+
return cs.visibility !== 'hidden' && cs.display !== 'none' && cs.opacity !== '0';
|
|
559
|
+
};
|
|
560
|
+
const markers = textMarkers.map((s) => s.toLocaleLowerCase());
|
|
561
|
+
for (const dlg of document.querySelectorAll("[role='dialog']")) {
|
|
562
|
+
if (!isVisible(dlg)) continue;
|
|
563
|
+
const txt = (dlg.innerText || '').toLocaleLowerCase();
|
|
564
|
+
if (markers.some((marker) => txt.includes(marker))) return true;
|
|
565
|
+
}
|
|
566
|
+
// Some cohorts render the editor as a nested form instead of a dialog.
|
|
567
|
+
// Its upload control is the most stable signal while the save is pending.
|
|
568
|
+
for (const el of document.querySelectorAll('[aria-label]')) {
|
|
569
|
+
if (!isVisible(el)) continue;
|
|
570
|
+
const al = (el.getAttribute('aria-label') || '').toLocaleLowerCase();
|
|
571
|
+
if ((/tải hình thu nhỏ|thumbnail/.test(al)) && (/tải|upload/.test(al))) return true;
|
|
572
|
+
}
|
|
573
|
+
return false;
|
|
574
|
+
}, THUMBNAIL_EDITOR_TEXT_MARKERS).catch(() => false);
|
|
575
|
+
}
|
|
576
|
+
|
|
526
577
|
const SAFE_RETRY_COMPOSER_CLOSED = 'FB_SAFE_RETRY_COMPOSER_CLOSED';
|
|
527
578
|
|
|
528
579
|
function safeComposerRetryError(message) {
|
|
@@ -580,7 +631,7 @@ async function runOnce({ page, payload, log }) {
|
|
|
580
631
|
} = payload || {};
|
|
581
632
|
if (!video_url) throw new Error('No video_url provided');
|
|
582
633
|
|
|
583
|
-
log('info', '[fb-pw] selectors version=2026.
|
|
634
|
+
log('info', '[fb-pw] selectors version=2026.09.01-thumb-final-cta-race');
|
|
584
635
|
|
|
585
636
|
page.on('dialog', (d) => { d.accept().catch(() => {}); });
|
|
586
637
|
|
|
@@ -1602,6 +1653,7 @@ async function runOnce({ page, payload, log }) {
|
|
|
1602
1653
|
// disabled while FB was still hashing the upload).
|
|
1603
1654
|
const closeDeadline = Date.now() + 60_000;
|
|
1604
1655
|
let modalClosed = false;
|
|
1656
|
+
let closedPolls = 0;
|
|
1605
1657
|
let retryCount = 0;
|
|
1606
1658
|
const RETRY_AFTER_MS = 12_000;
|
|
1607
1659
|
let lastRetryAt = Date.now();
|
|
@@ -1611,27 +1663,16 @@ async function runOnce({ page, payload, log }) {
|
|
|
1611
1663
|
// nested form (no own dialog — its upload button, aria
|
|
1612
1664
|
// "Tải hình thu nhỏ tùy chỉnh lên…", only exists while the
|
|
1613
1665
|
// editor is showing).
|
|
1614
|
-
const still = await page
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
const al = el.getAttribute('aria-label') || '';
|
|
1625
|
-
if (!(/Tải hình thu nhỏ|thumbnail/i.test(al) && /Tải|upload/i.test(al))) continue;
|
|
1626
|
-
const r = el.getBoundingClientRect();
|
|
1627
|
-
if (r.width < 8 || r.height < 8) continue;
|
|
1628
|
-
const cs = getComputedStyle(el);
|
|
1629
|
-
if (cs.visibility === 'hidden' || cs.display === 'none' || cs.opacity === '0') continue;
|
|
1630
|
-
return true;
|
|
1631
|
-
}
|
|
1632
|
-
return false;
|
|
1633
|
-
}).catch(() => false);
|
|
1634
|
-
if (!still) { modalClosed = true; break; }
|
|
1666
|
+
const still = await isThumbnailEditorVisible(page);
|
|
1667
|
+
if (!still) {
|
|
1668
|
+
// Require two consecutive closed samples. Facebook briefly
|
|
1669
|
+
// unmounts the nested form while swapping it for a
|
|
1670
|
+
// processing overlay; treating that single gap as closed
|
|
1671
|
+
// races the final "Đăng" button mount.
|
|
1672
|
+
if (++closedPolls >= 2) { modalClosed = true; break; }
|
|
1673
|
+
} else {
|
|
1674
|
+
closedPolls = 0;
|
|
1675
|
+
}
|
|
1635
1676
|
// Retry Lưu click after RETRY_AFTER_MS without progress —
|
|
1636
1677
|
// limit to 3 retries (so we still leave time for FB to
|
|
1637
1678
|
// finish processing on the last attempt).
|
|
@@ -2801,11 +2842,14 @@ module.exports.__testables = {
|
|
|
2801
2842
|
muteClickInterceptors,
|
|
2802
2843
|
unmuteClickInterceptors,
|
|
2803
2844
|
waitForPublishEnabled,
|
|
2845
|
+
isThumbnailEditorVisible,
|
|
2804
2846
|
hasVisibleReelComposer,
|
|
2805
2847
|
SAFE_RETRY_COMPOSER_CLOSED,
|
|
2806
2848
|
navigateFacebookHome,
|
|
2807
2849
|
navigateFacebookReelCreate,
|
|
2808
2850
|
PUBLISH_ENABLE_TIMEOUT_MS,
|
|
2851
|
+
PUBLISH_CTA_ABSENT_GRACE_MS,
|
|
2852
|
+
THUMBNAIL_EDITOR_TEXT_MARKERS,
|
|
2809
2853
|
REEL_CREATE_SELECTORS,
|
|
2810
2854
|
REEL_ENTRY_SELECTORS,
|
|
2811
2855
|
ADD_VIDEO_SELECTORS,
|