channel-worker 2.5.59 → 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
package/scripts/lib/fb-guard.js
CHANGED
|
@@ -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
|
-
|
|
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
|
+
};
|
package/scripts/lib/human.js
CHANGED
|
@@ -96,6 +96,18 @@ async function humanClick(page, target, { hoverMs = [250, 900], timeout = 4000,
|
|
|
96
96
|
await page.waitForTimeout(randInt(hoverMs[0], hoverMs[1]));
|
|
97
97
|
// Tiny drift while "deciding" — people rarely hold a pixel.
|
|
98
98
|
if (chance(0.5)) await page.mouse.move(p.x + rand(-1.5, 1.5), p.y + rand(-1.5, 1.5)).catch(() => {});
|
|
99
|
+
// KIỂM TRA VẬT CẢN trước khi nhấn — thứ Playwright làm ở loc.click() mà
|
|
100
|
+
// đường "kiểu người" này bỏ qua. 23/08 đăng FB fail 3 lượt liên tiếp: toast
|
|
101
|
+
// "Thông báo mới" nổi vài giây đúng góc dưới-trái, trùng nút "Tiếp" của
|
|
102
|
+
// composer; mouse.down rơi vào toast → FB mở reel, composer kẹt ở bước 1.
|
|
103
|
+
// Vật cản thường tự tan → chờ tới 6s; vẫn còn thì giao cho click chuẩn (có
|
|
104
|
+
// actionability + retry của Playwright) thay vì nhấn bừa.
|
|
105
|
+
const blocked = () => target.evaluate((el, pt) => {
|
|
106
|
+
const h = document.elementFromPoint(pt.x, pt.y);
|
|
107
|
+
return !(h && (el === h || el.contains(h) || h.contains(el)));
|
|
108
|
+
}, p).catch(() => false);
|
|
109
|
+
for (let i = 0; i < 12 && await blocked(); i++) await page.waitForTimeout(500);
|
|
110
|
+
if (await blocked()) { await target.click({ timeout }); return; }
|
|
99
111
|
await page.mouse.down();
|
|
100
112
|
await page.waitForTimeout(randInt(55, 140));
|
|
101
113
|
await page.mouse.up();
|
|
@@ -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
|
|
@@ -1233,7 +1238,8 @@ async function runOnce({ page, payload, log }) {
|
|
|
1233
1238
|
// iterations, throw with diagnostics.
|
|
1234
1239
|
let published = false;
|
|
1235
1240
|
let customThumbDone = false;
|
|
1236
|
-
let pubWaitDone = false;
|
|
1241
|
+
let pubWaitDone = false;
|
|
1242
|
+
let reelViewerRecoveries = 0; // bấm Tiếp trượt vào toast → FB mở reel viewer, xem bên dưới
|
|
1237
1243
|
for (let step = 0; step < 7 && !published; step++) {
|
|
1238
1244
|
await pause(page, 3000);
|
|
1239
1245
|
await fillMetadata();
|
|
@@ -2145,6 +2151,21 @@ async function runOnce({ page, payload, log }) {
|
|
|
2145
2151
|
await dumpFailure(page, `tiep-not-clickable-${step + 1}`, log);
|
|
2146
2152
|
throw new Error(`FB Tiếp not clickable at step ${step + 1}: ${e.message}`);
|
|
2147
2153
|
}
|
|
2154
|
+
// Cú bấm trượt vào thứ khác (toast thông báo / mục thông báo) → FB mở
|
|
2155
|
+
// reel viewer đè lên, nhưng composer VẪN CÒN ở bước cũ (kiểm kê 23/08:
|
|
2156
|
+
// dialog "Tạo thước phim… Tiếp" vẫn đó bên dưới viewer). Đóng viewer
|
|
2157
|
+
// bằng Escape rồi bấm lại — khỏi phải chạy lại cả lượt upload.
|
|
2158
|
+
if (/\/reel\/\d+/.test(page.url()) && (await composerStepLabel()) === stepLabelBefore && stepLabelBefore) {
|
|
2159
|
+
reelViewerRecoveries++;
|
|
2160
|
+
log('warn', `[fb-pw] bấm Tiếp bị trượt → FB mở reel viewer (${page.url().slice(0, 60)}), composer vẫn ở bước cũ — đóng viewer, bấm lại (${reelViewerRecoveries}/3)`);
|
|
2161
|
+
await page.keyboard.press('Escape').catch(() => {});
|
|
2162
|
+
await page.waitForTimeout(1500);
|
|
2163
|
+
if (/\/reel\/\d+/.test(page.url())) {
|
|
2164
|
+
await page.locator("[aria-label='Đóng'], [aria-label='Close']").first().click({ timeout: 3000 }).catch(() => {});
|
|
2165
|
+
await page.waitForTimeout(1500);
|
|
2166
|
+
}
|
|
2167
|
+
if (reelViewerRecoveries <= 3 && await hasVisibleReelComposer(page)) { step--; continue; }
|
|
2168
|
+
}
|
|
2148
2169
|
|
|
2149
2170
|
// BS-only side-wizard jump fallback — DO NOT run in Page-wall mode.
|
|
2150
2171
|
// The BS composer has a left-side wizard with clickable step headers
|
|
@@ -2194,25 +2215,9 @@ async function runOnce({ page, payload, log }) {
|
|
|
2194
2215
|
// giới hạn tần suất bài đăng…" and "Chỉnh sửa và đăng lại" / "Đóng"
|
|
2195
2216
|
// buttons. If we see this, the reel WAS NOT POSTED — fail loudly so
|
|
2196
2217
|
// the user knows to wait + cool down the account.
|
|
2197
|
-
const
|
|
2198
|
-
'giới hạn tần suất',
|
|
2199
|
-
'để bảo vệ cộng đồng khỏi spam',
|
|
2200
|
-
'chỉnh sửa và đăng lại',
|
|
2201
|
-
'spam protection',
|
|
2202
|
-
'temporarily blocked',
|
|
2203
|
-
'you are temporarily blocked',
|
|
2204
|
-
'limit how often',
|
|
2205
|
-
'để giữ cho cộng đồng',
|
|
2206
|
-
];
|
|
2207
|
-
const blockHit = await page.evaluate((phrases) => {
|
|
2208
|
-
const text = (document.body?.innerText || '').toLowerCase();
|
|
2209
|
-
for (const p of phrases) {
|
|
2210
|
-
if (text.includes(p)) return p;
|
|
2211
|
-
}
|
|
2212
|
-
return null;
|
|
2213
|
-
}, blockPhrases).catch(() => null);
|
|
2218
|
+
const blockHit = await readPublishBlock(page);
|
|
2214
2219
|
if (blockHit) {
|
|
2215
|
-
throw
|
|
2220
|
+
throw publishRateLimitError(blockHit);
|
|
2216
2221
|
}
|
|
2217
2222
|
|
|
2218
2223
|
// 7b) COMMIT VERIFY + RETRY. A Playwright click on an enabled "Đăng" can
|
|
@@ -2611,6 +2616,11 @@ async function run(args) {
|
|
|
2611
2616
|
try {
|
|
2612
2617
|
return await runOnce(args);
|
|
2613
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
|
+
}
|
|
2614
2624
|
const safeRetry = e?.code === SAFE_RETRY_COMPOSER_CLOSED;
|
|
2615
2625
|
if (!safeRetry || attempt === 2) throw e;
|
|
2616
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
|
}
|