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.
package/lib/command-poller.js
CHANGED
|
@@ -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
|
@@ -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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
|
230
|
+
for (const re of TOTAL_LABELS) {
|
|
227
231
|
for (const c of chunks) {
|
|
228
232
|
const m = re.exec(c);
|
|
229
|
-
if (m)
|
|
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
|
-
|
|
305
|
-
|
|
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;
|