channel-worker 2.5.131 → 2.5.132
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 +47 -7
- package/package.json +1 -1
- package/scripts/fetch_facebook_reel_stats.js +15 -1
- package/scripts/upload_facebook.js +68 -0
package/lib/updater.js
CHANGED
|
@@ -54,6 +54,39 @@ function planUpdate({ running, onDisk, latest, restartBlocked = '' }) {
|
|
|
54
54
|
return 'install';
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
// Đang chạy DƯỚI pm2 hay không. pm2 nhét `pm_id` vào env của tiến trình nó
|
|
58
|
+
// quản (đo tay trên relabs03 22/09/2026 bằng một app thử: env có `pm_id`,
|
|
59
|
+
// `pm_exec_path`, `name`, `unique_id`, `PM2_HOME`, `NODE_APP_INSTANCE`).
|
|
60
|
+
// `pm_id` là cái hẹp nhất trong số đó — `PM2_HOME` có thể do shell đặt sẵn mà
|
|
61
|
+
// chẳng có pm2 nào đang quản mình.
|
|
62
|
+
const underPm2 = () => !!process.env.pm_id;
|
|
63
|
+
|
|
64
|
+
// Sau khi cài xong thì khởi động lại KIỂU GÌ. Tách thành hàm thuần để test
|
|
65
|
+
// được: phần còn lại của checkAndUpdate đụng npm, đĩa và process.exit.
|
|
66
|
+
//
|
|
67
|
+
// 'spawn' — đẻ một daemon detached rồi tự thoát (cách chạy độc lập)
|
|
68
|
+
// 'exit' — chỉ thoát, để pm2 dựng lại, hoặc vì mình là bản thừa
|
|
69
|
+
// 'stay' — không khởi động lại gì cả
|
|
70
|
+
//
|
|
71
|
+
// ⚠️ DƯỚI pm2 TUYỆT ĐỐI KHÔNG 'spawn'. pm2 tự dựng lại tiến trình nó quản mỗi
|
|
72
|
+
// khi tiến trình đó thoát, nên spawn thêm một daemon detached rồi exit(0) là đẻ
|
|
73
|
+
// ra HAI ứng viên cho CÙNG MỘT khoá: con detached giành được khoá, con của pm2
|
|
74
|
+
// thua và chết ngay, pm2 lại dựng con mới, cứ thế mãi.
|
|
75
|
+
//
|
|
76
|
+
// Đo trên relabs03 22/09/2026: vòng lặp chạy mỗi ~30 giây, đã cộng dồn 15.242
|
|
77
|
+
// lần restart cho app `channel-worker` và 16.776 cho `shopee-worker`;
|
|
78
|
+
// `channel-worker-error.log` toàn dòng "Đã có daemon khác giữ
|
|
79
|
+
// /Users/…/.channel-worker (PID 50281)". Daemon THẬT vẫn chạy nên không ai để ý
|
|
80
|
+
// suốt mấy ngày — đó mới là chỗ nguy: bộ đếm restart thành vô nghĩa, và một lần
|
|
81
|
+
// chết thật sẽ lẫn vào giữa hàng vạn lần chết giả.
|
|
82
|
+
function restartPlan({ autoRestart, wasOwner, pm2 = underPm2() }) {
|
|
83
|
+
if (!autoRestart) return 'stay';
|
|
84
|
+
// Bản THỪA (không giữ khoá): thoát, không đẻ thêm — nhờ vậy mỗi vòng cập nhật
|
|
85
|
+
// tự rút sĩ số về đúng một daemon, thay vì N con cũ đẻ ra N con mới.
|
|
86
|
+
if (!wasOwner) return 'exit';
|
|
87
|
+
return pm2 ? 'exit' : 'spawn';
|
|
88
|
+
}
|
|
89
|
+
|
|
57
90
|
// 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
91
|
const BLOCKED_WARN_EVERY_MS = 6 * 3600 * 1000;
|
|
59
92
|
let lastBlockedWarnAt = 0;
|
|
@@ -163,7 +196,6 @@ async function checkAndUpdate({ autoRestart = false } = {}) {
|
|
|
163
196
|
|
|
164
197
|
const CONFIG_DIR = path.join(os.homedir(), '.channel-worker');
|
|
165
198
|
const LOG_FILE = path.join(CONFIG_DIR, 'daemon.log');
|
|
166
|
-
const logFd = fs.openSync(LOG_FILE, 'a');
|
|
167
199
|
|
|
168
200
|
// Nhả khoá TRƯỚC khi spawn: con mới phải giành được khoá mới chạy. Nếu
|
|
169
201
|
// tiến trình này là daemon THỪA (không giữ khoá) thì release trả false —
|
|
@@ -172,15 +204,23 @@ async function checkAndUpdate({ autoRestart = false } = {}) {
|
|
|
172
204
|
// thay vì N con cũ đẻ ra N con mới như sự cố relabs03 04/09/2026.
|
|
173
205
|
const singleton = require('./singleton');
|
|
174
206
|
const wasOwner = singleton.release(CONFIG_DIR);
|
|
175
|
-
|
|
207
|
+
const plan = restartPlan({ autoRestart: true, wasOwner });
|
|
208
|
+
|
|
209
|
+
if (plan === 'exit' && !wasOwner) {
|
|
176
210
|
console.log('[updater] Tiến trình này không giữ khoá daemon (bản thừa) — '
|
|
177
211
|
+ 'thoát, không spawn bản thay thế.');
|
|
212
|
+
} else if (plan === 'exit') {
|
|
213
|
+
console.log('[updater] Chạy dưới pm2 — thoát để pm2 dựng lại, KHÔNG spawn bản '
|
|
214
|
+
+ 'thay thế (spawn nữa là hai ứng viên tranh một khoá → pm2 quay vòng vô tận).');
|
|
178
215
|
}
|
|
179
216
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
217
|
+
if (plan === 'spawn') {
|
|
218
|
+
// Mở log CHỈ khi thật sự spawn: fd này là để truyền cho tiến trình con.
|
|
219
|
+
// Mở nó ở nhánh pm2 là giữ khoá file thừa — trên Windows đó đúng là nguồn
|
|
220
|
+
// của EBUSY daemon.log (xem restartBlocker).
|
|
221
|
+
const logFd = fs.openSync(LOG_FILE, 'a');
|
|
222
|
+
const isWindows = process.platform === 'win32';
|
|
223
|
+
const binName = isWindows ? 'channel-worker.cmd' : 'channel-worker';
|
|
184
224
|
const child = spawn(binName, ['start'], {
|
|
185
225
|
detached: true,
|
|
186
226
|
stdio: ['ignore', logFd, logFd],
|
|
@@ -229,4 +269,4 @@ class UpdateChecker {
|
|
|
229
269
|
}
|
|
230
270
|
}
|
|
231
271
|
|
|
232
|
-
module.exports = { checkAndUpdate, getLocalVersion, getInstalledVersion, getLatestVersion, isNewer, planUpdate, restartBlocker, UpdateChecker };
|
|
272
|
+
module.exports = { checkAndUpdate, getLocalVersion, getInstalledVersion, getLatestVersion, isNewer, planUpdate, restartPlan, underPm2, restartBlocker, UpdateChecker };
|
package/package.json
CHANGED
|
@@ -38,7 +38,7 @@ function parseViews(txt) {
|
|
|
38
38
|
// 09/09 tìm được tab để so trước/sau. Bản riêng còn thiếu cả bộ lọc "anchor
|
|
39
39
|
// của bài trong bảng tin" (gốc 2 link page lạ sáng 10/09) nên có thể quét
|
|
40
40
|
// nhầm tab Reels của page KHÁC mà không hay.
|
|
41
|
-
const { discoverReelsTabUrl, discoverReelsTabUrls, openReelsTabWithTiles, parsePageRef } = require('./upload_facebook').__testables;
|
|
41
|
+
const { discoverReelsTabUrl, discoverReelsTabUrls, openReelsTabWithTiles, parsePageRef, readPageIdFromDom } = require('./upload_facebook').__testables;
|
|
42
42
|
|
|
43
43
|
// Số người theo dõi của page, đọc ngay trên chính trang đang đứng (header hiện
|
|
44
44
|
// ở mọi tab của page/profile) — không tốn thêm lượt mở profile nào. Ô
|
|
@@ -821,6 +821,20 @@ async function run({ page, payload, log }) {
|
|
|
821
821
|
// được với danh sách page đã liên kết bên Shopee. Không có thì bỏ qua, lượt
|
|
822
822
|
// ghi số liệu không phụ thuộc vào nó.
|
|
823
823
|
const pageRef = opened.verified ? parsePageRef(opened.url) : null;
|
|
824
|
+
// URL chỉ có TÊN RIÊNG thì chưa đủ để đối chiếu với Shopee: 6/8 page đang liên
|
|
825
|
+
// kết bên đó lưu dạng `profile.php?id=…`, không mang tên riêng. Thiếu số ID là
|
|
826
|
+
// kênh bị báo "chưa liên kết" oan. Đọc thêm từ chính trang đang mở (đã chứng
|
|
827
|
+
// minh có tile ⇒ đúng page của kênh). Đọc hỏng thì giữ nguyên tên riêng, không
|
|
828
|
+
// làm hỏng lượt ghi số liệu.
|
|
829
|
+
if (pageRef && !pageRef.id) {
|
|
830
|
+
const hit = await readPageIdFromDom(page).catch(() => null);
|
|
831
|
+
if (hit) {
|
|
832
|
+
pageRef.id = hit.id;
|
|
833
|
+
log('info', `[fb-stats] page id đọc từ trang: ${hit.id} (${hit.sources} nguồn khớp: ${hit.from.join(', ')})`);
|
|
834
|
+
} else {
|
|
835
|
+
log('warn', '[fb-stats] URL chỉ có tên riêng và KHÔNG đọc được id từ trang — kênh sẽ hiện "chưa rõ Page" thay vì báo sai liên kết');
|
|
836
|
+
}
|
|
837
|
+
}
|
|
824
838
|
if (pageRef) log('info', `[fb-stats] page: ${pageRef.id ? `id ${pageRef.id}` : `handle ${pageRef.handle}`}`);
|
|
825
839
|
|
|
826
840
|
const d = await log.apiRequest('POST', '/analytics/post-stats', {
|
|
@@ -1155,6 +1155,73 @@ function parsePageRef(reelsTabUrl) {
|
|
|
1155
1155
|
return { id: '', handle: seg, url: raw };
|
|
1156
1156
|
}
|
|
1157
1157
|
|
|
1158
|
+
// ID page ĐỌC TỪ CHÍNH TRANG, cho ca URL chỉ có tên riêng (`/<handle>/?sk=reels_tab`).
|
|
1159
|
+
//
|
|
1160
|
+
// Vì sao cần: bên Shopee, 6/8 page đang liên kết được lưu dạng
|
|
1161
|
+
// `profile.php?id=…`, KHÔNG có tên riêng trong link. Kênh nào chỉ biết tên riêng
|
|
1162
|
+
// thì không có gì để so → bị báo "chưa liên kết" OAN (đo bằng
|
|
1163
|
+
// services/AffiliateMediaLink 22/09). Dạng tên riêng có thật: ca "Chuyện Nghề
|
|
1164
|
+
// Nhà Nông" 14/09 — dạng profile.php ra 0 tile, chỉ `/chuyennghenhanong/` chạy.
|
|
1165
|
+
//
|
|
1166
|
+
// CHỈ gọi khi đang đứng trên trang tab Reels ĐÃ chứng minh có tile. Đứng ở trang
|
|
1167
|
+
// khác thì mấy khoá dưới đây là của trang đó, không phải của page mình.
|
|
1168
|
+
//
|
|
1169
|
+
// Đo thật 22/09 trên 6 profile của đàn (Toc-xinh, Meo-luoi, Tam-cai-tao-nha,
|
|
1170
|
+
// Beo-banh-beo, duong-nhan-cung-anh, Do-nghe-len-hinh):
|
|
1171
|
+
// `profile_id` == `actorID` == ID page trong URL ở CẢ 6/6.
|
|
1172
|
+
// ⛔ `delegate_page_id` LUÔN là một số KHÁC (vd page 61591917189886 →
|
|
1173
|
+
// delegate 1240255979169594). Bắt nhầm khoá này là ghi sai danh tính rồi
|
|
1174
|
+
// báo sai liên kết cho đúng cái kênh mình định cứu.
|
|
1175
|
+
//
|
|
1176
|
+
// Cổng an toàn: phải có **ít nhất hai nguồn độc lập cùng ra một số** mới nhận.
|
|
1177
|
+
// Một khoá JSON đứng lẻ không đủ — trang Facebook đầy số 15-16 chữ số, và nếu
|
|
1178
|
+
// profile đang đăng nhập bằng tài khoản CÁ NHÂN quản lý page thì `userID` là của
|
|
1179
|
+
// người, không phải của page. Hai nguồn khớp nhau thì khả năng cùng sai rất thấp.
|
|
1180
|
+
const PAGE_ID_JSON_KEYS = ['profile_id', 'actorID', 'pageID'];
|
|
1181
|
+
|
|
1182
|
+
async function readPageIdFromDom(page) {
|
|
1183
|
+
const votes = await page.evaluate((keys) => {
|
|
1184
|
+
const out = {};
|
|
1185
|
+
const add = (src, v) => {
|
|
1186
|
+
if (!/^\d{5,25}$/.test(String(v || ''))) return;
|
|
1187
|
+
(out[src] = out[src] || []).push(String(v));
|
|
1188
|
+
};
|
|
1189
|
+
// Nguồn 1: anchor profile.php NGOÀI bảng tin. Anchor trong feed là của bài
|
|
1190
|
+
// người khác — đúng cái bẫy đã đẻ ra 2 link sai sáng 10/09.
|
|
1191
|
+
for (const a of document.querySelectorAll('a[href]')) {
|
|
1192
|
+
const href = a.getAttribute('href') || '';
|
|
1193
|
+
if (/[?&]__cft__|[?&]__tn__/.test(href)) continue;
|
|
1194
|
+
if (a.closest("[role='feed'], [role='article'], [aria-posinset]")) continue;
|
|
1195
|
+
const m = href.match(/profile\.php\?id=(\d{5,25})/);
|
|
1196
|
+
if (m) add('anchor', m[1]);
|
|
1197
|
+
}
|
|
1198
|
+
// Nguồn 2..n: khoá JSON trong bundle. `delegate_page_id` cố ý KHÔNG có mặt.
|
|
1199
|
+
const html = document.documentElement.innerHTML;
|
|
1200
|
+
for (const key of keys) {
|
|
1201
|
+
const re = new RegExp(`"${key}"\\s*:\\s*"?(\\d{5,25})"?`, 'g');
|
|
1202
|
+
let m; let n = 0;
|
|
1203
|
+
while ((m = re.exec(html)) && n < 8) { add(key, m[1]); n++; }
|
|
1204
|
+
}
|
|
1205
|
+
return out;
|
|
1206
|
+
}, PAGE_ID_JSON_KEYS).catch(() => ({}));
|
|
1207
|
+
|
|
1208
|
+
// Đếm theo NGUỒN, không theo số lần xuất hiện: một khoá lặp 8 lần trong bundle
|
|
1209
|
+
// vẫn chỉ là MỘT nguồn, không được tự nó đủ phiếu.
|
|
1210
|
+
const bySource = new Map();
|
|
1211
|
+
for (const [src, vals] of Object.entries(votes || {})) {
|
|
1212
|
+
for (const v of new Set(vals)) {
|
|
1213
|
+
if (!bySource.has(v)) bySource.set(v, new Set());
|
|
1214
|
+
bySource.get(v).add(src);
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
let best = null;
|
|
1218
|
+
for (const [id, srcs] of bySource) {
|
|
1219
|
+
if (srcs.size < 2) continue;
|
|
1220
|
+
if (!best || srcs.size > best.sources) best = { id, sources: srcs.size, from: [...srcs].sort() };
|
|
1221
|
+
}
|
|
1222
|
+
return best;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1158
1225
|
async function hasVisibleReelComposer(page) {
|
|
1159
1226
|
return page.evaluate(() => {
|
|
1160
1227
|
for (const dlg of document.querySelectorAll("[role='dialog']")) {
|
|
@@ -3691,6 +3758,7 @@ module.exports.__testables = {
|
|
|
3691
3758
|
pickReelsTabUrl,
|
|
3692
3759
|
pickReelsTabUrls,
|
|
3693
3760
|
parsePageRef,
|
|
3761
|
+
readPageIdFromDom,
|
|
3694
3762
|
discoverReelsTabUrls,
|
|
3695
3763
|
openReelsTabWithTiles,
|
|
3696
3764
|
networkIdVerdict,
|