channel-worker 2.5.90 → 2.5.92
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/bin/cli.js +40 -0
- package/lib/command-poller.js +18 -6
- package/lib/shopee-scraper.js +61 -14
- package/lib/singleton.js +14 -2
- package/lib/updater.js +42 -0
- package/package.json +1 -1
package/bin/cli.js
CHANGED
|
@@ -159,6 +159,10 @@ if (cmd === 'pair') {
|
|
|
159
159
|
console.log(`[channel-worker] Tiếp quản khoá của PID ${res.stolen.pid} `
|
|
160
160
|
+ `(${args.force ? 'ép bằng --force' : 'đã chết hoặc treo quá lâu'}).`);
|
|
161
161
|
}
|
|
162
|
+
if (res.corrupted) {
|
|
163
|
+
console.log('[channel-worker] daemon.lock cũ hỏng (không đọc ra nội dung — '
|
|
164
|
+
+ 'dấu hiệu điển hình của mất điện giữa chừng) — đã ghi đè và chạy tiếp.');
|
|
165
|
+
}
|
|
162
166
|
// Ghi ĐÚNG pid của daemon thật — updater/restart trước đây ghi pid của
|
|
163
167
|
// tiến trình bọc, nên "stop" bắn trượt và để lại mồ côi.
|
|
164
168
|
fs.writeFileSync(path.join(CONFIG_DIR, 'daemon.pid'), String(process.pid));
|
|
@@ -197,6 +201,42 @@ if (cmd === 'pair') {
|
|
|
197
201
|
process.exit(0);
|
|
198
202
|
}
|
|
199
203
|
|
|
204
|
+
} else if (cmd === 'selftest') {
|
|
205
|
+
// Nạp thử MỌI module trong lib/ rồi thoát 0. Bộ tự-cập-nhật chạy lệnh này
|
|
206
|
+
// sau khi cài và chỉ khởi động lại khi nó xanh.
|
|
207
|
+
//
|
|
208
|
+
// Vì sao cần: 08/09/2026, một bản npm publish làm mọi daemon tự lao vào
|
|
209
|
+
// `npm install -g` ĐÈ LÊN CHÍNH MODULE ĐANG CHẠY. Trên Windows npm xoá cây
|
|
210
|
+
// cũ trước khi ghi cây mới, và lượt đó dừng giữa chừng: lib/daemon.js và
|
|
211
|
+
// lib/command-poller.js biến mất, package.json vẫn ghi version mới nên nhìn
|
|
212
|
+
// vào đó thấy "cài xong". Daemon thay thế chết ngay MODULE_NOT_FOUND, hai
|
|
213
|
+
// máy Windows tắt lịm ĐÚNG LÚC đang đăng 9 video — 9 lượt đăng thành "mập
|
|
214
|
+
// mờ" phải kiểm tay. Đọc version là không đủ; phải nạp thử thật.
|
|
215
|
+
const fs = require('fs');
|
|
216
|
+
const libDir = path.join(__dirname, '..', 'lib');
|
|
217
|
+
const bad = [];
|
|
218
|
+
let files = [];
|
|
219
|
+
try {
|
|
220
|
+
files = fs.readdirSync(libDir).filter((f) => f.endsWith('.js')).sort();
|
|
221
|
+
} catch (e) {
|
|
222
|
+
console.error(`[channel-worker] selftest FAILED — không đọc nổi ${libDir}: ${e.code || e.message}`);
|
|
223
|
+
process.exit(1);
|
|
224
|
+
}
|
|
225
|
+
if (files.length < 10) {
|
|
226
|
+
// Cây đủ có 15 file; ít hơn 10 là chắc chắn cụt, không cần require từng cái.
|
|
227
|
+
console.error(`[channel-worker] selftest FAILED — lib/ chỉ còn ${files.length} file (cây cài dở).`);
|
|
228
|
+
process.exit(1);
|
|
229
|
+
}
|
|
230
|
+
for (const f of files) {
|
|
231
|
+
try { require(path.join(libDir, f)); } catch (e) { bad.push(`${f}: ${e.code || e.message}`); }
|
|
232
|
+
}
|
|
233
|
+
if (bad.length) {
|
|
234
|
+
console.error(`[channel-worker] selftest FAILED — ${bad.length}/${files.length} module không nạp được:\n ${bad.join('\n ')}`);
|
|
235
|
+
process.exit(1);
|
|
236
|
+
}
|
|
237
|
+
console.log(`[channel-worker] selftest OK — v${require('../package.json').version}, ${files.length} module nạp được.`);
|
|
238
|
+
process.exit(0);
|
|
239
|
+
|
|
200
240
|
} else if (cmd === 'update') {
|
|
201
241
|
const { checkAndUpdate, getLocalVersion } = require('../lib/updater');
|
|
202
242
|
console.log(`[channel-worker] Current version: ${getLocalVersion()}`);
|
package/lib/command-poller.js
CHANGED
|
@@ -154,11 +154,20 @@ class CommandPoller {
|
|
|
154
154
|
return true;
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
-
// Ensure a profile (by name) is running and return a
|
|
157
|
+
// Ensure a profile (by name) is running and return a playwright BrowserContext
|
|
158
158
|
// attached over CDP via its local debug port. Worker runs ON the same box as
|
|
159
159
|
// Nstbrowser, so localhost:<remoteDebuggingPort> is reachable directly.
|
|
160
|
+
//
|
|
161
|
+
// Dùng playwright-core, KHÔNG puppeteer-core: gói này chưa bao giờ nằm trong
|
|
162
|
+
// dependencies của worker (07/09: chạy thật lần đầu → MODULE_NOT_FOUND, hai
|
|
163
|
+
// lệnh Shopee chết ngay tại đây). Cả worker đã chạy playwright rồi.
|
|
164
|
+
//
|
|
165
|
+
// Trả CONTEXT chứ không phải browser, và người gọi phải dùng context đó:
|
|
166
|
+
// `browser.newPage()` của playwright đẻ ra một context MỚI — trắng cookie,
|
|
167
|
+
// nghĩa là mất sạch phiên đăng nhập Shopee của profile. Chỉ context sẵn có
|
|
168
|
+
// (contexts()[0]) mới mang cookie thật.
|
|
160
169
|
async _connectNstProfileByName(name) {
|
|
161
|
-
const
|
|
170
|
+
const { chromium } = require('playwright-core');
|
|
162
171
|
const profileId = await this.nst.findProfile(name);
|
|
163
172
|
if (!profileId) throw new Error(`NST profile "${name}" not found`);
|
|
164
173
|
let running = (await this.nst.getRunningBrowsers()).find(b => b.profileId === profileId);
|
|
@@ -172,8 +181,11 @@ class CommandPoller {
|
|
|
172
181
|
}
|
|
173
182
|
const port = running?.remoteDebuggingPort;
|
|
174
183
|
if (!port) throw new Error(`No debug port for profile "${name}" (launch failed?)`);
|
|
175
|
-
const browser = await
|
|
176
|
-
|
|
184
|
+
const browser = await chromium.connectOverCDP(`http://127.0.0.1:${port}`);
|
|
185
|
+
const context = browser.contexts()[0] || await browser.newContext();
|
|
186
|
+
// browser.close() trên kết nối CDP chỉ CẮT kết nối, không tắt Chrome của
|
|
187
|
+
// Nstbrowser (đo thật trên relabs03 07/09: profile vẫn chạy sau khi đóng).
|
|
188
|
+
return { browser, context, disconnect: () => browser.close().catch(() => {}) };
|
|
177
189
|
}
|
|
178
190
|
|
|
179
191
|
// Flow 1 — quét affiliate offer list (sort theo hoa hồng) → upsert vào kho.
|
|
@@ -185,7 +197,7 @@ class CommandPoller {
|
|
|
185
197
|
let conn;
|
|
186
198
|
try {
|
|
187
199
|
conn = await this._connectNstProfileByName(profileName);
|
|
188
|
-
const res = await scraper.scrapeOffers(conn.
|
|
200
|
+
const res = await scraper.scrapeOffers(conn.context, {
|
|
189
201
|
category: payload.category || 'women_clothes', // Tier-1 default: quần áo nữ
|
|
190
202
|
match_id: payload.match_id || null,
|
|
191
203
|
sort_types: payload.sort_types || [2, 3], // commission + sales, merged
|
|
@@ -228,7 +240,7 @@ class CommandPoller {
|
|
|
228
240
|
let conn;
|
|
229
241
|
try {
|
|
230
242
|
conn = await this._connectNstProfileByName(profileName);
|
|
231
|
-
const product = await scraper.ingestProduct(conn.
|
|
243
|
+
const product = await scraper.ingestProduct(conn.context, ids);
|
|
232
244
|
console.log(`[shopee] ingested: ${product.name} (${product.images.length} imgs)`);
|
|
233
245
|
await this.api.ingestShopeeResult(product, { user_id: command.user_id, idea_id: payload.idea_id || null });
|
|
234
246
|
await this.api.updateCommand(command._id, { status: 'done', result: { name: product.name, images: product.images.length } });
|
package/lib/shopee-scraper.js
CHANGED
|
@@ -65,9 +65,13 @@ function parsePdpItem(item) {
|
|
|
65
65
|
|
|
66
66
|
// Open the PDP and resolve with the get_pc payload the page fires. We listen at
|
|
67
67
|
// the response layer because a hand-rolled fetch gets error 90309999 (no sig).
|
|
68
|
-
|
|
68
|
+
//
|
|
69
|
+
// `ctx` là một playwright BrowserContext (KHÔNG phải Browser): trang phải mở
|
|
70
|
+
// trong đúng context của profile thì mới mang cookie đăng nhập Shopee — mở từ
|
|
71
|
+
// Browser là playwright đẻ context mới, trắng phiên.
|
|
72
|
+
async function ingestProduct(ctx, { shop_id, item_id, timeoutMs = 60000 } = {}) {
|
|
69
73
|
if (!shop_id || !item_id) throw new Error('ingestProduct needs shop_id + item_id');
|
|
70
|
-
const page = await
|
|
74
|
+
const page = await ctx.newPage();
|
|
71
75
|
try {
|
|
72
76
|
const captured = new Promise((resolve) => {
|
|
73
77
|
page.on('response', async (res) => {
|
|
@@ -93,6 +97,13 @@ function parseOfferRow(row) {
|
|
|
93
97
|
const card = row.batch_item_for_item_card_full || row.item_card_displayed_asset || {};
|
|
94
98
|
const shopId = String(card.shopid || card.shop_id || '');
|
|
95
99
|
const itemId = String(row.item_id || card.itemid || card.item_id || '');
|
|
100
|
+
// Đo thật 08/09/2026 trên cả tab XTRA lẫn tab ngành hàng:
|
|
101
|
+
// default_commission_rate = TỔNG hoa hồng affiliate (đã gồm Xtra)
|
|
102
|
+
// seller_commission_rate = phần NGƯỜI BÁN tài trợ, tức chính là Xtra
|
|
103
|
+
// default − seller = hoa hồng nền Shopee theo ngành (thời trang 7%,
|
|
104
|
+
// hoá phẩm / điện tử nhỏ 2,5%)
|
|
105
|
+
// max_commission_rate = 0% ở MỌI dòng đo được → đừng tin field này,
|
|
106
|
+
// nó chỉ đứng làm đường lui khi thiếu default.
|
|
96
107
|
const rate = pctToNumber(row.default_commission_rate ?? row.max_commission_rate ?? row.commission_rate);
|
|
97
108
|
const sellerRate = pctToNumber(row.seller_commission_rate);
|
|
98
109
|
const price = (Number(card.price || card.price_min) || 0) / 100000;
|
|
@@ -123,11 +134,26 @@ function parseOfferRow(row) {
|
|
|
123
134
|
// Affiliate offer-page category tabs → match_id (read from rc-tabs node keys,
|
|
124
135
|
// confirmed live). Tier-1 fashion focus first; the offer page has NO dedicated
|
|
125
136
|
// Shoes/Bags/Accessories tab → those come later via keyword search.
|
|
137
|
+
// Đọc thẳng từ dãy tab trên affiliate.shopee.vn/offer/product_offer, đối chiếu
|
|
138
|
+
// lại ngày 08/09/2026 — dãy này ĐỔI theo thời gian: 'grocery' (100629) và
|
|
139
|
+
// Moms/Kids có hồi 06/2026 nay không còn tab, đổi lại có thêm Đồ gia dụng và
|
|
140
|
+
// Máy tính & phụ kiện. Thấy quét ra rỗng thì việc đầu tiên là mở trang xem tab
|
|
141
|
+
// còn không, đừng đổ tại phiên đăng nhập.
|
|
142
|
+
//
|
|
143
|
+
// Hai kiểu danh sách KHÁC NHAU, đừng trộn (đo thật 08/09: truyền XTRA như một
|
|
144
|
+
// ngành hàng thì trả về 0 dòng, http 200 code 0 — hỏng im lặng):
|
|
145
|
+
// • ngành hàng → list_type=3&match_type=2&match_id=<id>
|
|
146
|
+
// • Commissions XTRA → list_type=7, KHÔNG có match_id
|
|
126
147
|
const AFFILIATE_CATEGORIES = {
|
|
127
|
-
women_clothes:
|
|
128
|
-
beauty:
|
|
129
|
-
home_living:
|
|
130
|
-
|
|
148
|
+
women_clothes: { match_id: 100017, label: 'Quần áo nữ', group: 'fashion' },
|
|
149
|
+
beauty: { match_id: 100630, label: 'Làm đẹp', group: 'beauty' },
|
|
150
|
+
home_living: { match_id: 100636, label: 'Nhà cửa & đời sống', group: 'other' },
|
|
151
|
+
home_appliances: { match_id: 100010, label: 'Đồ gia dụng', group: 'other' },
|
|
152
|
+
computer: { match_id: 100644, label: 'Máy tính & phụ kiện', group: 'other' },
|
|
153
|
+
// Không phải ngành hàng mà là "sản phẩm có hoa hồng Xtra" — cắt ngang mọi
|
|
154
|
+
// ngành. Không đặt group để khỏi tự bật lọc nhóm và vứt mất hàng ngoài
|
|
155
|
+
// fashion, vì chính cái tab này mới là chỗ hoa hồng cao nhất.
|
|
156
|
+
xtra: { list_type: 7, label: 'Hoa hồng XTRA' },
|
|
131
157
|
};
|
|
132
158
|
|
|
133
159
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
@@ -147,10 +173,24 @@ function applyFilters(rows, f = {}) {
|
|
|
147
173
|
return rows.filter((p) => {
|
|
148
174
|
if (f.category_groups?.length && !f.category_groups.includes(p.category_group)) return false;
|
|
149
175
|
if (f.min_commission != null && (p.commission_rate || 0) < f.min_commission) return false;
|
|
176
|
+
// Lọc theo phần NGƯỜI BÁN tài trợ (Xtra). Cần cửa riêng vì quét tab XTRA
|
|
177
|
+
// không đồng nghĩa với Xtra cao: đo 08/09 thấy tab ngành hàng thường cũng
|
|
178
|
+
// có dòng tài trợ 10%, còn trong chính tab XTRA vẫn có dòng chỉ 8%.
|
|
179
|
+
if (f.min_seller_commission != null && (p.commission_seller_rate || 0) < f.min_seller_commission) return false;
|
|
180
|
+
// Cổng theo TIỀN mỗi đơn. Ngưỡng phần trăm không so được giữa các ngành:
|
|
181
|
+
// hoa hồng nền của thời trang là 7% còn gia dụng/điện tử chỉ 2,5%, nên bộ
|
|
182
|
+
// Tier-1 (≥12%) vốn chỉnh cho thời trang đem áp lên gia dụng thì quét gần
|
|
183
|
+
// như trắng — đo 08/09/2026 trên tab Đồ gia dụng: 19/20 món rớt ĐÚNG ở
|
|
184
|
+
// cổng 12%, trong khi không món nào rớt vì doanh số, sao hay ảnh.
|
|
185
|
+
if (f.min_commission_value != null && (p.commission_value || 0) < f.min_commission_value) return false;
|
|
150
186
|
if (f.price_min != null && (p.price || 0) < f.price_min) return false;
|
|
151
187
|
if (f.price_max != null && (p.price || 0) > f.price_max) return false;
|
|
152
188
|
if (f.min_sold != null && (p.sold_count || 0) < f.min_sold) return false;
|
|
153
|
-
|
|
189
|
+
// Hàng CHƯA có đánh giá cũng rớt, không được đi cửa sau. Điều kiện cũ là
|
|
190
|
+
// "có rating VÀ rating < ngưỡng thì loại" — nghĩa là sản phẩm rating rỗng
|
|
191
|
+
// lọt thẳng qua cổng chất lượng, đúng loại hàng mới đăng chưa ai mua mà
|
|
192
|
+
// mình không muốn đem đi làm video.
|
|
193
|
+
if (f.min_rating != null && !(Number(p.rating) >= f.min_rating)) return false;
|
|
154
194
|
if (f.min_images != null && (p.images?.length || 0) < f.min_images) return false;
|
|
155
195
|
return true;
|
|
156
196
|
});
|
|
@@ -162,7 +202,9 @@ function applyFilters(rows, f = {}) {
|
|
|
162
202
|
// { status: 'ok'|'needs_verify', products: [...], raw_count }
|
|
163
203
|
// Never throws on a blocked session — returns status:'needs_verify' so the
|
|
164
204
|
// worker reports it cleanly instead of failing + retrying into more captchas.
|
|
165
|
-
|
|
205
|
+
// `ctx` = playwright BrowserContext của profile affiliate đã đăng nhập — xem
|
|
206
|
+
// ghi chú ở ingestProduct về việc vì sao không nhận Browser.
|
|
207
|
+
async function scrapeOffers(ctx, {
|
|
166
208
|
category = null, // key in AFFILIATE_CATEGORIES (e.g. 'women_clothes')
|
|
167
209
|
match_id = null, // explicit category id override
|
|
168
210
|
sort_types = [2], // [2]=commission; pass [2,3] to merge commission+sales
|
|
@@ -180,15 +222,18 @@ async function scrapeOffers(browser, {
|
|
|
180
222
|
// Default the category_groups filter to the category's own group (so a
|
|
181
223
|
// Women-Clothes scrape keeps only fashion rows even though the tab mixes in
|
|
182
224
|
// a few accessories), unless the caller overrides.
|
|
183
|
-
if (cat && !filters.category_groups) filters = { ...filters, category_groups: [cat.group] };
|
|
225
|
+
if (cat?.group && !filters.category_groups) filters = { ...filters, category_groups: [cat.group] };
|
|
226
|
+
const listType = cat?.list_type || null;
|
|
184
227
|
|
|
185
|
-
const pagesOpen =
|
|
228
|
+
const pagesOpen = ctx.pages();
|
|
186
229
|
let page = pagesOpen.find((p) => p.url().includes('affiliate.shopee.vn'));
|
|
187
230
|
let opened = false;
|
|
188
|
-
if (!page) { page = await
|
|
231
|
+
if (!page) { page = await ctx.newPage(); opened = true; }
|
|
189
232
|
try {
|
|
190
233
|
if (!page.url().includes('affiliate.shopee.vn/offer')) {
|
|
191
|
-
|
|
234
|
+
// 'networkidle' (playwright), KHÔNG 'networkidle2' (tên của puppeteer —
|
|
235
|
+
// playwright coi là giá trị lạ và ném lỗi).
|
|
236
|
+
await page.goto('https://affiliate.shopee.vn/offer/product_offer', { waitUntil: 'networkidle', timeout: timeoutMs }).catch(() => {});
|
|
192
237
|
await sleep(1500);
|
|
193
238
|
}
|
|
194
239
|
if (isBlockedState(page.url(), null)) {
|
|
@@ -201,13 +246,15 @@ async function scrapeOffers(browser, {
|
|
|
201
246
|
for (let i = 0; i < pages; i++) {
|
|
202
247
|
const offset = i * page_limit;
|
|
203
248
|
const res = await page.evaluate(async (q) => {
|
|
204
|
-
const cat = q.
|
|
249
|
+
const cat = q.listType
|
|
250
|
+
? `&list_type=${q.listType}`
|
|
251
|
+
: (q.mid ? `&list_type=3&match_type=2&match_id=${q.mid}` : '&list_type=0');
|
|
205
252
|
const url = `/api/v3/offer/product/list?sort_type=${q.st}&page_offset=${q.offset}&page_limit=${q.page_limit}&client_type=1${cat}`;
|
|
206
253
|
try {
|
|
207
254
|
const r = await fetch(url, { credentials: 'include' });
|
|
208
255
|
return { status: r.status, json: await r.json().catch(() => null) };
|
|
209
256
|
} catch (e) { return { status: 0, error: String(e.message) }; }
|
|
210
|
-
}, { st, offset, page_limit, mid });
|
|
257
|
+
}, { st, offset, page_limit, mid, listType });
|
|
211
258
|
|
|
212
259
|
if (isBlockedState(page.url(), res) || res.json?.code === 90309999) {
|
|
213
260
|
return { status: 'needs_verify', products: [...byId.values()], raw_count: rawCount, reason: `blocked mid-scrape (http ${res.status}, code ${res.json?.code})` };
|
package/lib/singleton.js
CHANGED
|
@@ -84,7 +84,7 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
|
84
84
|
|
|
85
85
|
/**
|
|
86
86
|
* Giành khoá cho tiến trình hiện tại.
|
|
87
|
-
* @returns {{ok: true, stolen?: object} | {ok: false, holder: object}}
|
|
87
|
+
* @returns {{ok: true, stolen?: object, corrupted?: boolean} | {ok: false, holder: object}}
|
|
88
88
|
*/
|
|
89
89
|
async function acquire(configDir, { waitMs = 0, force = false, info = {}, pollMs = 1000 } = {}) {
|
|
90
90
|
const started_at = new Date().toISOString();
|
|
@@ -97,7 +97,19 @@ async function acquire(configDir, { waitMs = 0, force = false, info = {}, pollMs
|
|
|
97
97
|
return { ok: true };
|
|
98
98
|
} catch (err) {
|
|
99
99
|
if (err.code !== 'EEXIST') throw err;
|
|
100
|
-
|
|
100
|
+
// File có thật mà readLock không đọc ra. Hai ca khác hẳn nhau:
|
|
101
|
+
// a) đứa khác vừa chen vào giữa hai nhịp → đọc lại ra khoá hợp lệ,
|
|
102
|
+
// quay vòng xử như bình thường (chờ / cướp nếu đã rác);
|
|
103
|
+
// b) file HỎNG, đọc lại vẫn không ra gì → không có chủ nào để chờ.
|
|
104
|
+
// Ca (b) mới là cái giết máy: mất điện 07/09/2026 để lại daemon.lock
|
|
105
|
+
// dài 139 byte TOÀN NUL (NTFS đã cấp cỡ, chưa kịp ghi nội dung).
|
|
106
|
+
// `continue` trần khi ấy thành vòng lặp KHÔNG sleep, không hạn giờ:
|
|
107
|
+
// 3 daemon trên win-worker quay tít 100% CPU suốt 4 giờ, không ghi nổi
|
|
108
|
+
// một dòng log, không bao giờ tới Daemon.start() — cả đàn video ngừng
|
|
109
|
+
// đăng mà nhìn từ ngoài y như "daemon không tự chạy sau khi bật máy".
|
|
110
|
+
if (readLock(configDir)) continue;
|
|
111
|
+
writeLock(configDir, { ...info, started_at }, { overwrite: true });
|
|
112
|
+
return { ok: true, corrupted: true };
|
|
101
113
|
}
|
|
102
114
|
}
|
|
103
115
|
if (lock.pid === process.pid) return { ok: true }; // đã là của mình
|
package/lib/updater.js
CHANGED
|
@@ -31,6 +31,28 @@ function installUpdate(version) {
|
|
|
31
31
|
console.log(`[updater] Installed ${PKG_NAME}@${version}`);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Cây vừa cài có nạp được không? `channel-worker selftest` require từng file
|
|
36
|
+
* trong lib/ và thoát 0 nếu đủ.
|
|
37
|
+
*
|
|
38
|
+
* npm cài ĐÈ LÊN module đang chạy, và trên Windows nó xoá cây cũ trước khi ghi
|
|
39
|
+
* cây mới. Lượt cài dừng giữa chừng để lại package.json version MỚI cạnh một
|
|
40
|
+
* lib/ THIẾU FILE — nhìn version thì thấy "cài xong". 08/09/2026 daemon thay
|
|
41
|
+
* thế chết ngay MODULE_NOT_FOUND (lib/daemon.js + lib/command-poller.js biến
|
|
42
|
+
* mất), hai máy Windows tắt giữa lúc đăng 9 video.
|
|
43
|
+
*/
|
|
44
|
+
function verifyInstall() {
|
|
45
|
+
const { spawnSync } = require('child_process');
|
|
46
|
+
const isWindows = process.platform === 'win32';
|
|
47
|
+
const bin = isWindows ? 'channel-worker.cmd' : 'channel-worker';
|
|
48
|
+
const out = spawnSync(bin, ['selftest'], {
|
|
49
|
+
shell: isWindows, windowsHide: true, encoding: 'utf8', timeout: 120000,
|
|
50
|
+
});
|
|
51
|
+
// status null = không chạy nổi (timeout, binary không thấy) → coi như hỏng.
|
|
52
|
+
const detail = `${out.stdout || ''}${out.stderr || ''}`.trim().split('\n').slice(-6).join(' | ');
|
|
53
|
+
return { ok: out.status === 0, detail: detail.slice(-600) };
|
|
54
|
+
}
|
|
55
|
+
|
|
34
56
|
async function checkAndUpdate({ autoRestart = false } = {}) {
|
|
35
57
|
const local = getLocalVersion();
|
|
36
58
|
const latest = await getLatestVersion();
|
|
@@ -42,6 +64,26 @@ async function checkAndUpdate({ autoRestart = false } = {}) {
|
|
|
42
64
|
console.log(`[updater] Update available: ${local} → ${latest}`);
|
|
43
65
|
installUpdate(latest);
|
|
44
66
|
|
|
67
|
+
// Cài xong CHƯA CHẮC chạy được — xem verifyInstall(). Hỏng thì thử cài lại
|
|
68
|
+
// đú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ũ đã
|
|
69
|
+
// nạp trong bộ nhớ, nên không restart là còn worker; restart vào cây cụt là
|
|
70
|
+
// mất worker. Bản mới sẽ vào ở lượt kiểm kế tiếp, hoặc do người cài tay.
|
|
71
|
+
let check = verifyInstall();
|
|
72
|
+
if (!check.ok) {
|
|
73
|
+
console.warn(`[updater] Bản vừa cài KHÔNG nạp được (${check.detail}) — cài lại một lần.`);
|
|
74
|
+
try {
|
|
75
|
+
installUpdate(latest);
|
|
76
|
+
check = verifyInstall();
|
|
77
|
+
} catch (e) {
|
|
78
|
+
check = { ok: false, detail: `cài lại lỗi: ${e.message}` };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (!check.ok) {
|
|
82
|
+
console.error('[updater] BỎ khởi động lại: cây cài dở, daemon cũ chạy tiếp. '
|
|
83
|
+
+ `Cần cài tay: npm i -g ${PKG_NAME}@${latest} --prefer-online (dừng daemon trước). Chi tiết: ${check.detail}`);
|
|
84
|
+
return { updated: false, local, latest, verifyFailed: true, detail: check.detail };
|
|
85
|
+
}
|
|
86
|
+
|
|
45
87
|
if (autoRestart) {
|
|
46
88
|
console.log('[updater] Restarting daemon with new version...');
|
|
47
89
|
const { spawn } = require('child_process');
|