channel-worker 2.5.60 → 2.5.61

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "channel-worker",
3
- "version": "2.5.60",
3
+ "version": "2.5.61",
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": {
@@ -21,6 +21,17 @@ const CODES = {
21
21
  banned: 'FB_ACCOUNT_BANNED',
22
22
  };
23
23
 
24
+ const PUBLISH_BLOCK_PHRASES = [
25
+ 'giới hạn tần suất',
26
+ 'để bảo vệ cộng đồng khỏi spam',
27
+ 'chỉnh sửa và đăng lại',
28
+ 'spam protection',
29
+ 'temporarily blocked',
30
+ 'you are temporarily blocked',
31
+ 'limit how often',
32
+ 'để giữ cho cộng đồng',
33
+ ];
34
+
24
35
  // Phrases FB shows on a disabled/suspended account (vi + en). Kept narrow on
25
36
  // purpose — a false "banned" would switch off a healthy account's nurture loop.
26
37
  const BANNED_RE = /(tài khoản của bạn đã bị (vô hiệu hoá|vô hiệu hóa|khoá|khóa|đình chỉ))|(your account has been disabled)|(we (have )?(suspended|disabled) your account)|(tài khoản này đã bị vô hiệu hoá)|(account (is )?(disabled|suspended|restricted) )/i;
@@ -97,4 +108,17 @@ async function assertAccountUsable(page, log) {
97
108
  throw err;
98
109
  }
99
110
 
100
- module.exports = { readAccountState, assertAccountUsable, CODES };
111
+ async function readPublishBlock(page) {
112
+ return page.evaluate((phrases) => {
113
+ const text = (document.body?.innerText || '').toLowerCase();
114
+ return phrases.find((p) => text.includes(p)) || null;
115
+ }, PUBLISH_BLOCK_PHRASES).catch(() => null);
116
+ }
117
+
118
+ function publishRateLimitError(blockHit) {
119
+ return new Error(`FB_PUBLISH_RATE_LIMITED: Facebook rate-limited / spam-blocked this account — publish was NOT accepted. Detected phrase: "${blockHit}". Auto-publish must stay paused until manually resumed.`);
120
+ }
121
+
122
+ module.exports = {
123
+ readAccountState, assertAccountUsable, readPublishBlock, publishRateLimitError, CODES,
124
+ };
@@ -17,6 +17,7 @@
17
17
 
18
18
  const fs = require('fs');
19
19
  const { humanMove, humanClick, humanType, humanWheel, pause, randInt: hRandInt, chance: hChance } = require('./lib/human');
20
+ const { assertAccountUsable, readPublishBlock, publishRateLimitError } = require('./lib/fb-guard');
20
21
  const path = require('path');
21
22
  const { downloadToTemp, safeUnlink } = require('./lib/download');
22
23
 
@@ -540,6 +541,10 @@ async function runOnce({ page, payload, log }) {
540
541
  await page.waitForLoadState('networkidle', { timeout: 30_000 }).catch(() => {});
541
542
  await pause(page, 4000);
542
543
  log('info', `[fb-pw] home loaded — url=${page.url()}`);
544
+ // Fail with a stable account-state code before selectors turn a logout or
545
+ // checkpoint into a generic "Reels entry not found". The API consumes the
546
+ // code and opens the Facebook auto-publish circuit breaker.
547
+ await assertAccountUsable(page, log);
543
548
 
544
549
  // Look around before posting. The old flow was goto → 4s → straight into
545
550
  // "Thước phim", every single run: a session whose only actions are
@@ -2210,25 +2215,9 @@ async function runOnce({ page, payload, log }) {
2210
2215
  // giới hạn tần suất bài đăng…" and "Chỉnh sửa và đăng lại" / "Đóng"
2211
2216
  // buttons. If we see this, the reel WAS NOT POSTED — fail loudly so
2212
2217
  // the user knows to wait + cool down the account.
2213
- const blockPhrases = [
2214
- 'giới hạn tần suất',
2215
- 'để bảo vệ cộng đồng khỏi spam',
2216
- 'chỉnh sửa và đăng lại',
2217
- 'spam protection',
2218
- 'temporarily blocked',
2219
- 'you are temporarily blocked',
2220
- 'limit how often',
2221
- 'để giữ cho cộng đồng',
2222
- ];
2223
- const blockHit = await page.evaluate((phrases) => {
2224
- const text = (document.body?.innerText || '').toLowerCase();
2225
- for (const p of phrases) {
2226
- if (text.includes(p)) return p;
2227
- }
2228
- return null;
2229
- }, blockPhrases).catch(() => null);
2218
+ const blockHit = await readPublishBlock(page);
2230
2219
  if (blockHit) {
2231
- throw new Error(`FB rate-limited / spam-blocked this account — publish was NOT accepted. Detected phrase: "${blockHit}". Cool down account before retry.`);
2220
+ throw publishRateLimitError(blockHit);
2232
2221
  }
2233
2222
 
2234
2223
  // 7b) COMMIT VERIFY + RETRY. A Playwright click on an enabled "Đăng" can
@@ -2627,6 +2616,11 @@ async function run(args) {
2627
2616
  try {
2628
2617
  return await runOnce(args);
2629
2618
  } catch (e) {
2619
+ // A checkpoint/logout can appear mid-composer. Prefer the coded account
2620
+ // error over the selector symptom so the server stops unattended retries.
2621
+ if (!/^FB_(?:ACCOUNT|PUBLISH)_/.test(String(e?.message || ''))) {
2622
+ try { await assertAccountUsable(page, log); } catch (accountErr) { throw accountErr; }
2623
+ }
2630
2624
  const safeRetry = e?.code === SAFE_RETRY_COMPOSER_CLOSED;
2631
2625
  if (!safeRetry || attempt === 2) throw e;
2632
2626
  log('warn', `[fb-pw] composer vanished before publish — retrying the full upload once (${attempt}/1)`);
@@ -11,6 +11,7 @@ const fs = require('fs');
11
11
  const path = require('path');
12
12
  const os = require('os');
13
13
  const { humanType, pause, randInt: hRandInt } = require('./lib/human');
14
+ const { assertAccountUsable, readPublishBlock, publishRateLimitError } = require('./lib/fb-guard');
14
15
  const { downloadToTemp, safeUnlink } = require('./lib/download');
15
16
 
16
17
  async function firstVisible(locator, max = 8) {
@@ -148,6 +149,7 @@ async function run({ page, payload, log }) {
148
149
  await page.goto('https://www.facebook.com/', { waitUntil: 'domcontentloaded', timeout: 60_000 });
149
150
  await page.waitForLoadState('networkidle', { timeout: 30_000 }).catch(() => {});
150
151
  await pause(page, 2500);
152
+ await assertAccountUsable(page, log);
151
153
 
152
154
  // 2) Open the "Tạo bài viết" composer via the "Ảnh/video" entry (this opens
153
155
  // the post composer directly in photo-attach mode). Fall back to the
@@ -312,6 +314,8 @@ async function run({ page, payload, log }) {
312
314
 
313
315
  // Verify commit + retry — a click can register without FB posting.
314
316
  await pause(page, 6000);
317
+ const blockHit = await readPublishBlock(page);
318
+ if (blockHit) throw publishRateLimitError(blockHit);
315
319
  for (let commitTry = 0; commitTry < 3 && (await composerStillOpen(page, publishVerbs)); commitTry++) {
316
320
  log('warn', `[fbphoto] composer still open — re-clicking publish (retry ${commitTry + 1}/3)`);
317
321
  if (await tagCta(publishVerbs)) await clickTaggedCta();
@@ -332,6 +336,11 @@ async function run({ page, payload, log }) {
332
336
 
333
337
  log('info', '[fbphoto] done');
334
338
  return { ok: true, post_url: postUrl, caption: fullCaption.slice(0, 80) };
339
+ } catch (e) {
340
+ if (!/^FB_(?:ACCOUNT|PUBLISH)_/.test(String(e?.message || ''))) {
341
+ try { await assertAccountUsable(page, log); } catch (accountErr) { throw accountErr; }
342
+ }
343
+ throw e;
335
344
  } finally {
336
345
  safeUnlink(imagePath);
337
346
  }