channel-worker 2.5.120 → 2.5.122
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/updater.js +72 -2
- package/package.json +1 -1
- package/scripts/verify_facebook_reels.js +46 -10
package/lib/updater.js
CHANGED
|
@@ -1,13 +1,63 @@
|
|
|
1
1
|
const { execSync } = require('child_process');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const os = require('os');
|
|
2
4
|
const path = require('path');
|
|
3
5
|
|
|
4
6
|
const PKG_NAME = 'channel-worker';
|
|
5
7
|
|
|
8
|
+
// Bản ĐANG CHẠY: `require` cache giữ package.json từ lúc tiến trình khởi động.
|
|
6
9
|
function getLocalVersion() {
|
|
7
10
|
const pkg = require(path.join(__dirname, '..', 'package.json'));
|
|
8
11
|
return pkg.version;
|
|
9
12
|
}
|
|
10
13
|
|
|
14
|
+
// Bản ĐANG NẰM TRÊN ĐĨA: đọc thẳng file, không qua cache. Hai daemon trên
|
|
15
|
+
// win-worker dùng CHUNG một thư mục cài, nên daemon này có thể đang chạy bản cũ
|
|
16
|
+
// trong khi daemon kia đã cài xong bản mới.
|
|
17
|
+
function getInstalledVersion() {
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8')).version || '';
|
|
20
|
+
} catch {
|
|
21
|
+
return '';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Khởi động lại có làm được không? Bước restart mở daemon.log để nối log của
|
|
26
|
+
// daemon thay thế. Daemon bật qua .bat có `>> daemon.log` thì cmd.exe giữ file
|
|
27
|
+
// đó suốt đời tiến trình → Windows trả EBUSY. Trả mã lỗi, hoặc '' nếu mở được.
|
|
28
|
+
function restartBlocker() {
|
|
29
|
+
const logFile = path.join(os.homedir(), '.channel-worker', 'daemon.log');
|
|
30
|
+
try {
|
|
31
|
+
fs.closeSync(fs.openSync(logFile, 'a'));
|
|
32
|
+
return '';
|
|
33
|
+
} catch (e) {
|
|
34
|
+
return e.code || e.message || 'unknown';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Quyết định THUẦN của một lượt kiểm (test được, không đụng đĩa/mạng):
|
|
39
|
+
// 'none' — đang chạy bản mới nhất
|
|
40
|
+
// 'blocked' — có bản mới nhưng không khởi động lại được → KHÔNG cài
|
|
41
|
+
// 'restart-only' — bản mới đã có sẵn trên đĩa → chỉ kiểm + khởi động lại
|
|
42
|
+
// 'install' — cài rồi kiểm rồi khởi động lại
|
|
43
|
+
//
|
|
44
|
+
// Vì sao phải có 'blocked' và 'restart-only' (sự cố 17-19/09/2026, win-worker):
|
|
45
|
+
// worker-1 được bật tay qua start-worker.bat nên restart luôn chết EBUSY. Bản cũ
|
|
46
|
+
// so bản ĐANG CHẠY (2.5.117) với registry (2.5.120) nên lượt nào cũng
|
|
47
|
+
// `npm i -g` lại từ đầu rồi mới vấp EBUSY: 413 lần cài đè trong ~34 giờ, lên
|
|
48
|
+
// đúng thư mục cài mà worker-2 đang chạy. Cài đè cây đang chạy chính là thứ đã
|
|
49
|
+
// giết worker Windows nhiều lần (lib/ bị xoá giữa chừng).
|
|
50
|
+
function planUpdate({ running, onDisk, latest, restartBlocked = '' }) {
|
|
51
|
+
if (!latest || !isNewer(latest, running)) return 'none';
|
|
52
|
+
if (restartBlocked) return 'blocked';
|
|
53
|
+
if (onDisk && !isNewer(latest, onDisk)) return 'restart-only';
|
|
54
|
+
return 'install';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Cảnh báo 'blocked' mỗi 6 giờ một lần thôi, không phải mỗi 5 phút.
|
|
58
|
+
const BLOCKED_WARN_EVERY_MS = 6 * 3600 * 1000;
|
|
59
|
+
let lastBlockedWarnAt = 0;
|
|
60
|
+
|
|
11
61
|
async function getLatestVersion() {
|
|
12
62
|
const res = await fetch(`https://registry.npmjs.org/${PKG_NAME}/latest`);
|
|
13
63
|
if (!res.ok) throw new Error(`npm registry returned ${res.status}`);
|
|
@@ -61,8 +111,28 @@ async function checkAndUpdate({ autoRestart = false } = {}) {
|
|
|
61
111
|
return { updated: false, local, latest };
|
|
62
112
|
}
|
|
63
113
|
|
|
114
|
+
// Chỉ lượt tự động (autoRestart) mới cần khởi động lại; `channel-worker
|
|
115
|
+
// update` gõ tay thì cứ cài như cũ.
|
|
116
|
+
const onDisk = getInstalledVersion();
|
|
117
|
+
const blocker = autoRestart ? restartBlocker() : '';
|
|
118
|
+
const plan = planUpdate({ running: local, onDisk, latest, restartBlocked: blocker });
|
|
119
|
+
if (plan === 'blocked') {
|
|
120
|
+
if (Date.now() - lastBlockedWarnAt > BLOCKED_WARN_EVERY_MS) {
|
|
121
|
+
lastBlockedWarnAt = Date.now();
|
|
122
|
+
console.warn(`[updater] Có bản ${latest} (đang chạy ${local}, trên đĩa ${onDisk || '?'}) nhưng KHÔNG tự khởi động lại được: `
|
|
123
|
+
+ `daemon.log đang bị tiến trình khác giữ (${blocker}), thường do daemon bật qua file .bat có ">> daemon.log". `
|
|
124
|
+
+ 'KHÔNG cài để khỏi ghi đè thư mục cài đang chạy mỗi 5 phút. '
|
|
125
|
+
+ 'Bật lại daemon bằng scheduled task (ChannelWorker / ChannelWorker2), không dùng .bat có chuyển log.');
|
|
126
|
+
}
|
|
127
|
+
return { updated: false, local, latest, restartBlocked: blocker };
|
|
128
|
+
}
|
|
129
|
+
|
|
64
130
|
console.log(`[updater] Update available: ${local} → ${latest}`);
|
|
65
|
-
|
|
131
|
+
if (plan === 'restart-only') {
|
|
132
|
+
console.log(`[updater] ${latest} đã có sẵn trên đĩa (daemon khác cùng máy vừa cài, hoặc lượt trước cài xong) — không cài lại, chỉ kiểm rồi khởi động lại.`);
|
|
133
|
+
} else {
|
|
134
|
+
installUpdate(latest);
|
|
135
|
+
}
|
|
66
136
|
|
|
67
137
|
// Cài xong CHƯA CHẮC chạy được — xem verifyInstall(). Hỏng thì thử cài lại
|
|
68
138
|
// đúng một lần, vẫn hỏng thì Ở YÊN: tiến trình này vẫn đang chạy code cũ đã
|
|
@@ -159,4 +229,4 @@ class UpdateChecker {
|
|
|
159
229
|
}
|
|
160
230
|
}
|
|
161
231
|
|
|
162
|
-
module.exports = { checkAndUpdate, getLocalVersion, getLatestVersion, isNewer, UpdateChecker };
|
|
232
|
+
module.exports = { checkAndUpdate, getLocalVersion, getInstalledVersion, getLatestVersion, isNewer, planUpdate, restartBlocker, UpdateChecker };
|
package/package.json
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// caption hơn, API tự biết là không đủ để ghép và để nguyên cho người.
|
|
12
12
|
//
|
|
13
13
|
// Payload: { channel_id, post_ids: ['1234567890', …] }
|
|
14
|
-
// Trả về: { captions: [{ post_id, og, page_text, url }], read, asked }
|
|
14
|
+
// Trả về: { captions: [{ post_id, og, page_text, blocks, url }], read, asked }
|
|
15
15
|
//
|
|
16
16
|
// Việc SO SÁNH nằm ở API (services/ReelCaptionMatch) chứ không ở đây: một bảng
|
|
17
17
|
// luật, test offline được, và worker không cần biết tiêu đề của idea.
|
|
@@ -23,17 +23,52 @@ const SETTLE_MS = 4000;
|
|
|
23
23
|
// một kênh hiếm khi quá vài cái; nhiều hơn thì để lượt sau, đừng giữ profile lâu.
|
|
24
24
|
const MAX_POSTS = 8;
|
|
25
25
|
|
|
26
|
-
//
|
|
27
|
-
//
|
|
26
|
+
// Đuôi Facebook thêm vào khi cắt caption dài. Phải gỡ, không thì khối chữ
|
|
27
|
+
// không còn là "khúc đầu của tiêu đề" và luật ghép theo tiền tố trượt.
|
|
28
|
+
const MORE_TAIL_RE = /(?:[…\.]{1,3}\s*)?(?:see more|xem thêm|see translation|xem bản dịch)\s*$/i;
|
|
29
|
+
|
|
30
|
+
function cleanBlock(s) {
|
|
31
|
+
return String(s == null ? '' : s)
|
|
32
|
+
.replace(/\s+/g, ' ')
|
|
33
|
+
.trim()
|
|
34
|
+
.replace(MORE_TAIL_RE, '')
|
|
35
|
+
.replace(/[…\s]+$/, '')
|
|
36
|
+
.trim();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Lấy mọi đường có thể có caption. ĐO TẬN TAY 20/09/2026 trên reel
|
|
40
|
+
// 4007094836262842 (kênh "tán gái", profile kenh-25), trang mở trong phiên ĐÃ
|
|
41
|
+
// ĐĂNG NHẬP:
|
|
42
|
+
// - `document.querySelectorAll('meta[property]')` → **RỖNG**. Trang reel của
|
|
43
|
+
// phiên đăng nhập không có thẻ og nào, nên og luôn 0 ký tự. Giữ lại vì
|
|
44
|
+
// phiên khác (chưa đăng nhập, share link) vẫn có.
|
|
45
|
+
// - `[role='main']` chỉ có **18 ký tự** — caption KHÔNG nằm trong đó. Bản cũ
|
|
46
|
+
// đọc đúng chỗ này nên 3 bài "đăng mập mờ" của kênh treo 2 ngày, ghép=0.
|
|
47
|
+
// - `body.innerText` (331 ký tự) CÓ caption nhưng dính rác đầu trang
|
|
48
|
+
// ("Number of unread notifications 20+ 119 433K 2.4K 1.9K <tên trang> …").
|
|
49
|
+
// - Khối `span[dir='auto']` dài nhất CHÍNH LÀ caption, kèm đuôi "… See more".
|
|
50
|
+
// Nên: trả cả ba — og, page_text (đổi sang body), và `blocks` = các khối
|
|
51
|
+
// dir=auto đã gỡ đuôi, để API so khớp trên từng khối (ReelCaptionMatch).
|
|
28
52
|
async function readCaption(page) {
|
|
29
53
|
return page.evaluate(() => {
|
|
54
|
+
const flat = (t) => String(t || '').replace(/\s+/g, ' ').trim();
|
|
30
55
|
const meta = (p) => (document.querySelector(`meta[property="${p}"]`) || {}).content || '';
|
|
31
|
-
const
|
|
56
|
+
const blocks = [...document.querySelectorAll("span[dir='auto'], div[dir='auto']")]
|
|
57
|
+
.map((el) => flat(el.innerText))
|
|
58
|
+
.filter((t) => t.length >= 20)
|
|
59
|
+
.sort((a, b) => b.length - a.length)
|
|
60
|
+
.slice(0, 6);
|
|
32
61
|
return {
|
|
33
62
|
og: (meta('og:description') || meta('og:title') || '').slice(0, 1000),
|
|
34
|
-
page_text:
|
|
63
|
+
page_text: flat(document.body && document.body.innerText).slice(0, 1500),
|
|
64
|
+
blocks,
|
|
35
65
|
};
|
|
36
|
-
}).
|
|
66
|
+
}).then((c) => ({
|
|
67
|
+
og: cleanBlock(c.og).slice(0, 1000),
|
|
68
|
+
page_text: c.page_text,
|
|
69
|
+
// Bỏ trùng sau khi gỡ đuôi: khối cha và khối con thường ra cùng một chuỗi.
|
|
70
|
+
blocks: [...new Set((c.blocks || []).map(cleanBlock).filter((t) => t.length >= 20))].slice(0, 6),
|
|
71
|
+
})).catch(() => ({ og: '', page_text: '', blocks: [] }));
|
|
37
72
|
}
|
|
38
73
|
|
|
39
74
|
async function run({ page, payload = {}, log }) {
|
|
@@ -50,9 +85,10 @@ async function run({ page, payload = {}, log }) {
|
|
|
50
85
|
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: NAV_TIMEOUT_MS });
|
|
51
86
|
await page.waitForTimeout(SETTLE_MS);
|
|
52
87
|
const c = await readCaption(page);
|
|
53
|
-
captions.push({ post_id: id, og: c.og, page_text: c.page_text, url });
|
|
54
|
-
|
|
55
|
-
|
|
88
|
+
captions.push({ post_id: id, og: c.og, page_text: c.page_text, blocks: c.blocks, url });
|
|
89
|
+
const best = (c.blocks && c.blocks[0]) || c.og || '';
|
|
90
|
+
log('info', `[fb-verify] reel ${id}: og ${c.og.length} ký tự, trang ${c.page_text.length} ký tự, `
|
|
91
|
+
+ `${(c.blocks || []).length} khối` + (best ? ` — "${best.slice(0, 60)}"` : ' — KHÔNG đọc được chữ nào'));
|
|
56
92
|
} catch (e) {
|
|
57
93
|
// Một reel đọc hỏng KHÔNG được giết cả lượt: các reel còn lại vẫn ghép được.
|
|
58
94
|
log('warn', `[fb-verify] reel ${id} đọc hỏng: ${String(e && e.message || e).slice(0, 90)}`);
|
|
@@ -63,4 +99,4 @@ async function run({ page, payload = {}, log }) {
|
|
|
63
99
|
}
|
|
64
100
|
|
|
65
101
|
module.exports = { run };
|
|
66
|
-
module.exports.__testables = { readCaption, MAX_POSTS, SETTLE_MS };
|
|
102
|
+
module.exports.__testables = { readCaption, cleanBlock, MORE_TAIL_RE, MAX_POSTS, SETTLE_MS };
|