channel-worker 2.5.53 → 2.5.54

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.53",
3
+ "version": "2.5.54",
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": {
@@ -2232,6 +2232,30 @@ async function runOnce({ page, payload, log }) {
2232
2232
  // - Page wall display order isn't strictly chronological
2233
2233
  // - "Quảng bá thước phim" appears on ALL Page reels, not just new
2234
2234
  // - Title match alone can't distinguish same-title duplicates
2235
+ // Fresh-tile scrape, dùng ở lượt đầu VÀ lượt retry (a.2b) — cùng một luật:
2236
+ // chỉ nhận tile có timestamp tươi ("Vừa xong"/"X phút"), tuyệt đối không
2237
+ // nhận tile cũ.
2238
+ const scrapeFreshReelTile = () => page.evaluate(() => {
2239
+ const FRESH_RE = /vừa xong|vài giây|^\s*\d{1,2}\s*giây|^\s*[1-5]\s*phút\b|\b[1-5]\s*phút trước|just now|few seconds ago|\b[1-5] min(ute)?s? ago/i;
2240
+ const anchors = document.querySelectorAll("a[href*='/reel/']");
2241
+ for (const a of anchors) {
2242
+ const href = a.getAttribute('href') || '';
2243
+ const m = href.match(/\/reel\/(\d{8,18})/);
2244
+ if (!m) continue;
2245
+ const r = a.getBoundingClientRect();
2246
+ if (r.width < 8 || r.height < 8) continue;
2247
+ let ctx = a;
2248
+ for (let depth = 0; depth < 5 && ctx; depth++) {
2249
+ const raw = (ctx.innerText || ctx.textContent || '').slice(0, 500);
2250
+ if (FRESH_RE.test(raw)) {
2251
+ return { href, id: m[1], depth, timestamp: (raw.match(FRESH_RE) || [''])[0] };
2252
+ }
2253
+ ctx = ctx.parentElement;
2254
+ }
2255
+ }
2256
+ return null;
2257
+ }).catch(() => null);
2258
+ let reelsTabUrl = null; // giữ lại cho lượt retry (a.2b)
2235
2259
  try {
2236
2260
  // Find a link to the page's reels tab. Multi-strategy:
2237
2261
  // 1. Any existing href containing "sk=reels_tab"
@@ -2272,35 +2296,14 @@ async function runOnce({ page, payload, log }) {
2272
2296
  return null;
2273
2297
  }).catch(() => null);
2274
2298
  if (reelsTabHref) {
2299
+ reelsTabUrl = reelsTabHref;
2275
2300
  log('info', `[fb-pw] navigating to page reels tab: ${reelsTabHref.slice(0, 100)}`);
2276
2301
  await page.goto(reelsTabHref, { waitUntil: 'domcontentloaded', timeout: 30_000 }).catch(() => {});
2277
2302
  await page.waitForTimeout(5000);
2278
- const fresh = await page.evaluate(() => {
2279
- const norm = (s) => (s || '').toLowerCase().normalize('NFD').replace(/\p{Diacritic}/gu, '');
2280
- const FRESH_RE = /vừa xong|vài giây|^\s*\d{1,2}\s*giây|^\s*[1-5]\s*phút\b|\b[1-5]\s*phút trước|just now|few seconds ago|\b[1-5] min(ute)?s? ago/i;
2281
- // Reel tiles on the reels tab — each is an <a href="/reel/<id>/">
2282
- // wrapping a thumbnail + meta. Iterate in DOM order (newest first
2283
- // on reels tab). For each tile, check if its subtree has a fresh
2284
- // timestamp.
2285
- const anchors = document.querySelectorAll("a[href*='/reel/']");
2286
- for (const a of anchors) {
2287
- const href = a.getAttribute('href') || '';
2288
- const m = href.match(/\/reel\/(\d{8,18})/);
2289
- if (!m) continue;
2290
- const r = a.getBoundingClientRect();
2291
- if (r.width < 8 || r.height < 8) continue;
2292
- // Look within the anchor's subtree + closest meaningful ancestor.
2293
- let ctx = a;
2294
- for (let depth = 0; depth < 5 && ctx; depth++) {
2295
- const raw = (ctx.innerText || ctx.textContent || '').slice(0, 500);
2296
- if (FRESH_RE.test(raw)) {
2297
- return { href, id: m[1], depth, timestamp: (raw.match(FRESH_RE) || [''])[0] };
2298
- }
2299
- ctx = ctx.parentElement;
2300
- }
2301
- }
2302
- return null;
2303
- }).catch(() => null);
2303
+ // Reel tiles on the reels tab — each is an <a href="/reel/<id>/">
2304
+ // wrapping a thumbnail + meta, DOM order = newest first. Only a tile
2305
+ // with a FRESH timestamp counts (see scrapeFreshReelTile).
2306
+ const fresh = await scrapeFreshReelTile();
2304
2307
  if (fresh) {
2305
2308
  const full = fresh.href.startsWith('http') ? fresh.href : `https://www.facebook.com${fresh.href}`;
2306
2309
  postUrl = full;
@@ -2422,24 +2425,52 @@ async function runOnce({ page, payload, log }) {
2422
2425
  }
2423
2426
  }
2424
2427
 
2428
+ // (a.2b) RETRY reels_tab sau khi chờ — reel mới thường cần 1-3 phút xử lý
2429
+ // phía FB rồi mới hiện lên tab, nên lượt scrape đầu (chạy ngay sau
2430
+ // khi bấm Đăng) hay trượt và mọi thứ rơi xuống last-resort bốc nhầm
2431
+ // reel cũ (4 lần gần nhất 2026-08-19 đều thế). Một lượt chờ 75s +
2432
+ // re-scrape cứu được phần lớn ca đó, vẫn giữ luật fresh-timestamp
2433
+ // nên không thể nhận nhầm tile cũ.
2434
+ if (!postUrl && reelsTabUrl) {
2435
+ log('info', '[fb-pw] no post URL yet — waiting 75s for FB to surface the new reel on reels_tab, then re-scraping…');
2436
+ await page.waitForTimeout(75_000);
2437
+ await page.goto(reelsTabUrl, { waitUntil: 'domcontentloaded', timeout: 30_000 }).catch(() => {});
2438
+ await page.waitForTimeout(5000);
2439
+ const fresh2 = await scrapeFreshReelTile();
2440
+ if (fresh2) {
2441
+ postUrl = fresh2.href.startsWith('http') ? fresh2.href : `https://www.facebook.com${fresh2.href}`;
2442
+ log('info', `[fb-pw] post URL from reels_tab RETRY (id=${fresh2.id}, timestamp="${fresh2.timestamp}"): ${postUrl}`);
2443
+ } else {
2444
+ log('warn', '[fb-pw] reels_tab retry: still no fresh tile');
2445
+ }
2446
+ }
2447
+
2425
2448
  // (a.3) LAST-RESORT: first reel tile on the profile reels tab. UNRELIABLE —
2426
2449
  // the just-published reel may not be on the tab yet (still
2427
2450
  // processing), so the first tile can be a STALE older reel. Only used
2428
2451
  // when every authoritative source above (fresh-timestamp tile,
2429
- // title-match, inline CTA, network capture) returned nothing.
2452
+ // title-match, inline CTA, network capture, timed retry) returned
2453
+ // nothing. Provably-stale filter: mọi reel ID đã bắt được TRƯỚC khi
2454
+ // bấm Đăng là reel có sẵn trên page — reel mới không thể là chúng.
2455
+ // Thà post_url rỗng còn hơn ghi sai (2026-08-19: nhánh này ghi reel
2456
+ // 02/08 cho bài vừa đăng → link trong DB trỏ nhầm bài cũ).
2430
2457
  if (!postUrl) {
2431
- const tileHref = await page.evaluate(() => {
2458
+ const preIds = capturedReelIds.slice(0, capturedReelIdsSnapshotLen);
2459
+ const tileHref = await page.evaluate((staleIds) => {
2460
+ const staleSet = new Set(staleIds);
2432
2461
  const anchors = document.querySelectorAll("a[role='link']");
2433
2462
  for (const a of anchors) {
2434
2463
  const aria = (a.getAttribute('aria-label') || '').toLowerCase();
2435
2464
  if (!/bản xem trước ô thước phim|reel tile preview|reel preview/.test(aria)) continue;
2436
2465
  const href = a.getAttribute('href') || '';
2437
2466
  const m = href.match(/\/reel\/(\d{8,20})/);
2438
- if (m) return { href, aria: aria.slice(0, 60) };
2467
+ if (m) return { href, aria: aria.slice(0, 60), id: m[1], provablyStale: staleSet.has(m[1]) };
2439
2468
  }
2440
2469
  return null;
2441
- }).catch(() => null);
2442
- if (tileHref) {
2470
+ }, preIds).catch(() => null);
2471
+ if (tileHref && tileHref.provablyStale) {
2472
+ log('warn', `[fb-pw] LAST-RESORT tile ${tileHref.id} was already captured PRE-publish — provably an OLD reel; leaving post_url empty instead of recording a wrong one`);
2473
+ } else if (tileHref) {
2443
2474
  postUrl = tileHref.href.startsWith('http') ? tileHref.href : `https://www.facebook.com${tileHref.href}`;
2444
2475
  log('warn', `[fb-pw] post URL from FIRST reel tile on reels_tab (LAST-RESORT, may be STALE): ${postUrl}`);
2445
2476
  }