channel-worker 2.5.88 → 2.5.90

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.
@@ -2,6 +2,11 @@ const { NstManager } = require('./nst-manager');
2
2
  const { checkAndUpdateExtension } = require('./extension-updater');
3
3
  const { StatsSyncer } = require('./stats-syncer');
4
4
 
5
+ // Quãng RẢNH trước khi đóng profile của luồng đăng bài. Phải đủ dài để lượt
6
+ // upload nền tảng kế tiếp (facebook → youtube → tiktok của cùng một idea) kịp
7
+ // tới và dùng lại browser, nhưng không được để mở qua đêm.
8
+ const PW_IDLE_CLOSE_MS = Number(process.env.PW_IDLE_CLOSE_MS || 5 * 60 * 1000);
9
+
5
10
  class CommandPoller {
6
11
  constructor(api, config) {
7
12
  this.api = api;
@@ -253,6 +258,11 @@ class CommandPoller {
253
258
  // publish click). Different profiles still run in parallel — this lock
254
259
  // only serializes within a profile. Wait up to 30 min with 5s polling
255
260
  // (upload typically 1-3 min); stale-cleanup picks up zombies separately.
261
+ // Lượt mới của cùng profile → huỷ hẹn đóng, giữ browser cho nó dùng lại.
262
+ if (this._pwCloseTimers && this._pwCloseTimers.has(profileId)) {
263
+ clearTimeout(this._pwCloseTimers.get(profileId));
264
+ this._pwCloseTimers.delete(profileId);
265
+ }
256
266
  if (!this._pwInFlight) this._pwInFlight = new Map();
257
267
  const MUTEX_MAX_WAIT_MS = 30 * 60 * 1000;
258
268
  const waitStart = Date.now();
@@ -341,10 +351,45 @@ class CommandPoller {
341
351
  } catch (e) {
342
352
  console.warn(`[commands/pw] close after ${scriptName} failed: ${e.message}`);
343
353
  }
354
+ } else if (this.nst) {
355
+ // ĐĂNG BÀI: giữ profile mở để lượt upload nền tảng kế tiếp dùng lại,
356
+ // nhưng phải có hạn. Trước 2026-09-07 KHÔNG AI đóng nó cả — cả ba đường
357
+ // đều hụt: không nằm trong CLOSE_AFTER, lệnh close_profile của API chỉ
358
+ // phát trong nhánh nuôi acc, còn bộ quét profile rảnh thì bỏ qua vì
359
+ // luồng _pw không ghi _profileLastActivity. Đo 24h: 78 lượt đăng, 0
360
+ // lệnh đóng, 141 tiến trình Nstbrowser còn sống trên win-worker.
361
+ //
362
+ // KHÔNG dùng bộ quét rảnh sẵn có: nó thoát sớm khi hàng đợi render còn
363
+ // việc, mà win-worker vừa đăng bài vừa render — profile đăng bài sẽ kẹt
364
+ // mở vô hạn đúng vào lúc máy bận nhất.
365
+ this._schedulePwClose(profileId, scriptName);
344
366
  }
345
367
  }
346
368
  }
347
369
 
370
+ // Hẹn đóng profile sau một quãng RẢNH. Lượt _pw kế tiếp của cùng profile huỷ
371
+ // hẹn (xem đầu handlePlaywrightCommand), nên chuỗi upload nhiều nền tảng liên
372
+ // tiếp vẫn dùng chung một browser như thiết kế cũ.
373
+ _schedulePwClose(profileId, scriptName) {
374
+ if (!this._pwCloseTimers) this._pwCloseTimers = new Map();
375
+ const prev = this._pwCloseTimers.get(profileId);
376
+ if (prev) clearTimeout(prev);
377
+ const t = setTimeout(async () => {
378
+ this._pwCloseTimers.delete(profileId);
379
+ // Có lệnh khác vừa chiếm profile này → để yên, lượt đó sẽ tự hẹn lại.
380
+ if (this._pwInFlight && this._pwInFlight.has(profileId)) return;
381
+ try {
382
+ await this.nst.stopProfile(profileId);
383
+ console.log(`[commands/pw] đóng profile ${profileId} sau ${PW_IDLE_CLOSE_MS / 1000}s rảnh (${scriptName})`);
384
+ } catch (e) {
385
+ console.warn(`[commands/pw] đóng trễ ${profileId} lỗi: ${e.message}`);
386
+ }
387
+ }, PW_IDLE_CLOSE_MS);
388
+ // Đừng giữ tiến trình sống chỉ vì một cái hẹn đóng browser.
389
+ if (typeof t.unref === 'function') t.unref();
390
+ this._pwCloseTimers.set(profileId, t);
391
+ }
392
+
348
393
  async handleLaunchProfile(command) {
349
394
  const { profile_id, channel_id } = command.payload || {};
350
395
  console.log(`[commands] Launching Nstbrowser profile: ${profile_id}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "channel-worker",
3
- "version": "2.5.88",
3
+ "version": "2.5.90",
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": {
@@ -215,18 +215,24 @@ async function currentRangeLabel(page) {
215
215
  // Tổng lượt xem của khoảng đang hiện, đọc được CẢ HAI kiểu giao diện.
216
216
  // Phải neo cuối chuỗi ở "Lượt xem": trang còn có "Lượt xem trong tối thiểu 3
217
217
  // giây" và "… 1 phút", khớp lỏng là vớ nhầm mấy số đó.
218
- const TOTAL_PATTERNS = [
219
- /^([\d.,]+)\s*(?:([-+]?[\d.,]+)%)?\s*Tổng số lượt xem/i,
220
- /^([\d.,]+)\s*(?:([-+]?[\d.,]+)%)?\s*Total views/i,
221
- /^([\d.,]+)\s*(?:([-+]?[\d.,]+)%)?\s*Lượt xem$/i,
222
- /^([\d.,]+)\s*(?:([-+]?[\d.,]+)%)?\s*Views$/i,
218
+ // Giữa CON SỐ và NHÃN, Facebook nhét đủ thứ không phải chữ: phần trăm, và có
219
+ // khi cả MŨI TÊN. Ảnh chụp 2026-09-06 kênh "lặn biển 01":
220
+ // "105,474 ↓ -46% Total views from previous 28 days"
221
+ // Regex cũ chỉ cho khoảng trắng ở đó nên trượt sạch — trong khi số nằm ngay
222
+ // đấy. Bản tiếng Việt và khoảng 7 ngày không kèm mũi tên nên vẫn chạy, chỉ
223
+ // khoảng tuỳ chỉnh (có % giảm) mới lộ. `[^\p{L}]*` nuốt mọi thứ không phải chữ.
224
+ const TOTAL_LABELS = [
225
+ /^([\d.,]+)([^\p{L}]*)(?:Tổng số lượt xem|Total views)/iu,
226
+ /^([\d.,]+)([^\p{L}]*)(?:Lượt xem|Views)$/iu,
223
227
  ];
224
228
 
225
229
  function pickTotal(chunks) {
226
- for (const re of TOTAL_PATTERNS) {
230
+ for (const re of TOTAL_LABELS) {
227
231
  for (const c of chunks) {
228
232
  const m = re.exec(c);
229
- if (m) return { views: parseExact(m[1]), pct: m[2] ? parseFloat(String(m[2]).replace(/,/g, '')) : null };
233
+ if (!m) continue;
234
+ const pm = /(-?[\d.,]+)%/.exec(m[2] || '');
235
+ return { views: parseExact(m[1]), pct: pm ? parseFloat(pm[1].replace(/,/g, '')) : null };
230
236
  }
231
237
  }
232
238
  return { views: null, pct: null };
@@ -301,11 +307,8 @@ async function readViewsPage(page, range, log) {
301
307
  if (/[Ll]ượt xem|[Vv]iews/.test(t)) { seen.add(t); chunks.push(t); }
302
308
  if (chunks.length > 120) break;
303
309
  }
304
- const label = [...document.querySelectorAll('div,span,button')]
305
- .map((e) => clean(e.innerText))
306
- .find((t) => t.length < 60 && /ngày qua:|hôm nay/i.test(t)) || '';
307
- return { chunks, label };
308
- }).catch(() => ({ chunks: [], label: '' }));
310
+ return { chunks };
311
+ }).catch(() => ({ chunks: [] }));
309
312
 
310
313
  const t0 = pickTotal(raw.chunks);
311
314
  const total = t0.views; const pct = t0.pct;
@@ -384,7 +387,7 @@ async function readTotalChunks(page) {
384
387
  for (const e of document.querySelectorAll('div,span')) {
385
388
  const t = clean(e.innerText);
386
389
  if (!t || t.length > 90 || !/\d/.test(t) || seen.has(t)) continue;
387
- if (/[Ll]ượt xem/.test(t)) { seen.add(t); out.push(t); }
390
+ if (/[Ll]ượt xem|[Vv]iews/.test(t)) { seen.add(t); out.push(t); }
388
391
  if (out.length > 120) break;
389
392
  }
390
393
  return out;