channel-worker 2.5.131 → 2.5.133

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.
@@ -388,29 +388,8 @@ class CommandPoller {
388
388
  return;
389
389
  }
390
390
 
391
- // Danh sách page đã liên kết với tài khoản Shopee — đi CHUNG lượt này vì
392
- // nó dùng đúng phiên vừa mở, thêm một request chứ không thêm lần mở
393
- // profile. LỖI MỀM: đọc hỏng thì mất phần cảnh báo liên kết, không được
394
- // kéo theo cả lượt đồng bộ click/đơn (đó mới là việc chính).
395
- let mediaAccounts = null;
396
- let mediaError = null;
397
- try {
398
- const media = await report.fetchSocialMediaAccounts(conn.context, {
399
- log: (message) => console.log(message),
400
- });
401
- if (media.status === 'ok') mediaAccounts = media.accounts;
402
- else mediaError = media.reason || media.status;
403
- } catch (err) {
404
- mediaError = String(err.message || err).slice(0, 200);
405
- }
406
- if (mediaError) console.warn(`[shopee-report] không đọc được danh sách liên kết: ${mediaError}`);
407
-
408
391
  const saved = await this.api.affiliateReportResult(result.rows, {
409
392
  clicks: clickResult.rows,
410
- // `null` = KHÔNG đọc được, khác hẳn `[]` = đọc được và tài khoản chưa
411
- // liên kết page nào. API phải phân biệt để không xoá sạch bản ghi cũ
412
- // chỉ vì một lượt đọc hỏng.
413
- media_accounts: mediaAccounts,
414
393
  last_report_date: result.last_report_date,
415
394
  last_update_time: result.last_update_time,
416
395
  from: result.from,
@@ -427,8 +406,6 @@ class CommandPoller {
427
406
  click_saved: saved?.click_saved || 0,
428
407
  click_matched: saved?.click_matched || 0,
429
408
  click_unmatched: saved?.click_unmatched || 0,
430
- media_accounts: mediaAccounts ? mediaAccounts.length : null,
431
- media_error: mediaError || undefined,
432
409
  last_report_date: result.last_report_date,
433
410
  },
434
411
  });
@@ -183,78 +183,4 @@ async function fetchClickReport(ctx, {
183
183
  }
184
184
  }
185
185
 
186
- // Danh sách tài khoản mạng xã hội ĐÃ LIÊN KẾT với tài khoản affiliate — cùng
187
- // thứ trang `account_setting` hiện ở tab "Media Information".
188
- //
189
- // KHÔNG mở account_setting và KHÔNG bấm tab: đo thật 22/09/2026 trên phiên
190
- // Shopee1, endpoint này trả đủ dữ liệu khi gọi từ MỘT tab affiliate bất kỳ
191
- // (thử từ /dashboard). Đi đường DOM thì phải mở thêm trang, bấm đúng nhãn
192
- // tiếng Anh "Media Information", và nhãn đó đổi theo ngôn ngữ giao diện.
193
- //
194
- // Mỗi dòng có `user_id` là ID Facebook THẬT của page (vd 1298751909986110) —
195
- // đúng con số nằm trong link `facebook.com/profile.php?id=…`. Nhờ vậy việc so
196
- // khớp với kênh của mình không phải parse URL, vốn có nhiều biến thể
197
- // (?referral_source=…, /<handle>/, m.facebook.com…).
198
- //
199
- // `platform`: 6 = Facebook, 3 = TikTok (đo được trên tài khoản thật; các mã
200
- // khác giữ nguyên số để sau này đọc lại, không đoán tên).
201
- const SM_PLATFORM = { 6: 'facebook', 3: 'tiktok' };
202
-
203
- async function fetchSocialMediaAccounts(ctx, { timeoutMs = 45000, log = null } = {}) {
204
- let page = findAffiliatePage(ctx.pages());
205
- let opened = false;
206
- if (!page) {
207
- page = await ctx.newPage();
208
- opened = true;
209
- }
210
-
211
- try {
212
- if (!page.url().includes('affiliate.shopee.vn/')) {
213
- await page.goto('https://affiliate.shopee.vn/offer/product_offer', {
214
- waitUntil: 'domcontentloaded', timeout: timeoutMs,
215
- }).catch(() => {});
216
- await sleep(1500);
217
- }
218
- if (isBlockedState(page.url(), null)) {
219
- return { status: 'needs_verify', accounts: [], reason: 'session at captcha/login — re-verify via VNC' };
220
- }
221
-
222
- const result = await page.evaluate(async () => {
223
- try {
224
- const r = await fetch('/api/v3/affiliates/social_media_accounts', { credentials: 'include' });
225
- return { status: r.status, json: await r.json().catch(() => null) };
226
- } catch (e) {
227
- return { status: 0, error: String(e?.message || e) };
228
- }
229
- });
230
-
231
- if (isBlockedState(page.url(), result) || result.json?.code === 90309999) {
232
- return { status: 'needs_verify', accounts: [], reason: `blocked while reading media info (http ${result.status}, code ${result.json?.code})` };
233
- }
234
- if (result.status !== 200 || result.json?.code !== 0) {
235
- return { status: 'failed', accounts: [], reason: result.json?.msg || result.error || `media info HTTP ${result.status}` };
236
- }
237
-
238
- const list = Array.isArray(result.json?.data?.accounts) ? result.json.data.accounts : [];
239
- const accounts = list.map((row) => ({
240
- media_user_id: String(row.user_id || row.unique_id || '').trim(),
241
- media_name: String(row.user_name || '').trim(),
242
- website_url: String(row.website_url || '').trim(),
243
- platform_code: Number(row.platform) || 0,
244
- platform: SM_PLATFORM[Number(row.platform)] || 'other',
245
- link_status: Number(row.link_status) || 0,
246
- facebook_page_type: Number(row.facebook_page_type) || 0,
247
- partner_link_status: Number(row.partner_link_status) || 0,
248
- follower_number: Number(row.follower_number) || 0,
249
- // Shopee trả giây, không phải mili-giây.
250
- verification_time: Number(row.verification_time) || 0,
251
- })).filter((a) => a.media_user_id || a.website_url);
252
-
253
- if (log) log(`[shopee-report] media: ${accounts.length} tài khoản liên kết (${accounts.filter((a) => a.platform === 'facebook').length} Facebook)`);
254
- return { status: 'ok', accounts };
255
- } finally {
256
- if (opened) await page.close().catch(() => {});
257
- }
258
- }
259
-
260
- module.exports = { fetchConversionReport, fetchClickReport, fetchSocialMediaAccounts, reportWindow, SM_PLATFORM };
186
+ module.exports = { fetchConversionReport, fetchClickReport, reportWindow };
@@ -436,6 +436,29 @@ function cleanSubId(v) {
436
436
  // `/opaanlp/` — KHÁC hẳn `long_link` của offer list (universal-link + chữ ký
437
437
  // Google Ads, không qua đường ghi nhận affiliate nên không ra hoa hồng).
438
438
  //
439
+ // Vì sao Shopee không trả link — gom mọi chỗ nó có thể nhét lý do vào.
440
+ //
441
+ // Nó KHÔNG phải lúc nào cũng dùng `errors[]` của GraphQL: đã gặp lượt trả
442
+ // `code 599` + `data: null` (ca sub-id có gạch dưới, 09/09/2026) với
443
+ // `businessCode` nằm thẳng trong thân. Đọc không ra thì in nguyên văn đã cắt
444
+ // ngắn — còn hơn một câu rỗng nghĩa bắt người sau mở trình duyệt ra mò lại.
445
+ function linkFailureReason(res, wanted = 0) {
446
+ const j = res && res.json;
447
+ const bits = [];
448
+ if (res && res.error) bits.push(String(res.error));
449
+ if (res && res.status && res.status !== 200) bits.push(`http ${res.status}`);
450
+ const msg = j?.errors?.[0]?.message || j?.msg || j?.message || j?.error_msg;
451
+ if (msg) bits.push(String(msg));
452
+ for (const k of ['businessCode', 'business_code', 'code', 'error']) {
453
+ const v = j?.[k];
454
+ if (v !== undefined && v !== null && v !== 0 && v !== '') { bits.push(`${k} ${v}`); break; }
455
+ }
456
+ if (!bits.length) {
457
+ bits.push(`Shopee trả 0 link cho ${wanted} sản phẩm: ${JSON.stringify(j ?? res ?? null).slice(0, 300)}`);
458
+ }
459
+ return bits.join(' · ').slice(0, 500);
460
+ }
461
+
439
462
  // subIds: mảng tối đa 5 phần tử. Quy ước của mình: [kênh, video, đợt].
440
463
  async function generateOfferLink(ctx, { items = [], subIds = [], timeoutMs = 45000, log = null } = {}) {
441
464
  const list = (items || [])
@@ -483,27 +506,53 @@ async function generateOfferLink(ctx, { items = [], subIds = [], timeoutMs = 450
483
506
  }
484
507
  const err = res.json?.errors?.[0]?.message;
485
508
  if (err) return { status: 'failed', links: [], reason: err };
486
- const rows = res.json?.data?.productOfferLinks || [];
487
- return {
488
- status: 'ok',
489
- sub_ids: subs,
490
- links: rows.map((r) => ({ item_id: String(r.itemId), shop_id: String(r.shopId), link: r.productOfferLink })),
491
- };
509
+
510
+ // CHỈ nhận link của đúng những món đã hỏi, và chỉ nhận dòng CÓ link.
511
+ // Shopee trả `itemId` kèm từng link nên khớp được — không khớp mà vẫn lấy
512
+ // là có ngày ghi link của món khác vào bài, mà `workerLink` bên API khoá
513
+ // theo chính `item_id` này nên sai một chỗ là sai hết chuỗi.
514
+ const wanted = new Set(list.map((it) => it.itemId));
515
+ const rows = Array.isArray(res.json?.data?.productOfferLinks) ? res.json.data.productOfferLinks : [];
516
+ const links = rows
517
+ .map((r) => ({
518
+ item_id: String(r?.itemId ?? ''),
519
+ shop_id: String(r?.shopId ?? ''),
520
+ link: String(r?.productOfferLink || ''),
521
+ }))
522
+ .filter((r) => wanted.has(r.item_id) && r.link);
523
+ const missing = list.map((it) => it.itemId).filter((id) => !links.some((l) => l.item_id === id));
524
+
525
+ // KHÔNG có link nào = THẤT BẠI, không phải "xong với 0 kết quả". Bản đầu
526
+ // trả `status: 'ok'` cho ca này nên lệnh ghi 'done' và giao diện chỉ nói
527
+ // được "Worker không trả về link" — mù nguyên nhân. Mọi kiểu Shopee từ
528
+ // chối mà không đi qua `errors[]` (http 5xx, `data: null`, businessCode
529
+ // nằm trong thân) đều rơi vào đây.
530
+ if (!links.length) {
531
+ return { status: 'failed', links: [], sub_ids: subs, reason: linkFailureReason(res, list.length) };
532
+ }
533
+ if (missing.length && log) log(`[shopee] thiếu link cho ${missing.length}/${list.length} món: ${missing.join(', ')}`);
534
+ return { status: 'ok', sub_ids: subs, links, missing };
492
535
  } finally {
493
536
  if (opened) await page.close().catch(() => {});
494
537
  }
495
538
  }
496
539
 
540
+ // Dạng `-i.<shop>.<item>` neo hai đầu: mở bằng `-` hoặc `/`, đóng ở cuối
541
+ // đường dẫn. Cùng một luật với `api/services/ShopeeUrl.js` (ghi chú dài ở đó)
542
+ // — hai bên lệch nhau thì cùng một link cho ra hai mã sản phẩm khác nhau tuỳ
543
+ // đường đi. Đo 23/09/2026: bản cũ đọc "Mini.2.4G" ra shop 2 / item 4.
544
+ const I_FORM = /[-/]i\.(\d+)\.(\d+)\/?(?:[?#]|$)/;
545
+
497
546
  // Parse a Shopee product URL → { shop_id, item_id }. Accepts
498
547
  // /product/<shop>/<item> and i.<shop>.<item> forms.
499
548
  function parseProductUrl(url = '') {
500
549
  // `/opaanlp/` là đích của link affiliate (đo 12/09: link rút gọn s.shopee.vn
501
550
  // đá về đúng dạng này) — vẫn là một sản phẩm, vẫn lấy được shop/item.
502
- let m = url.match(/\/(?:product|opaanlp)\/(\d+)\/(\d+)/);
551
+ let m = String(url || '').match(/\/(?:product|opaanlp)\/(\d+)\/(\d+)/);
503
552
  if (m) return { shop_id: m[1], item_id: m[2] };
504
- m = url.match(/i\.(\d+)\.(\d+)/);
553
+ m = String(url || '').match(I_FORM);
505
554
  if (m) return { shop_id: m[1], item_id: m[2] };
506
555
  return null;
507
556
  }
508
557
 
509
- module.exports = { ingestProduct, scrapeOffers, generateOfferLink, cleanSubId, parseProductUrl, parseOfferRow, parsePdpItem, categoryGroup, pctToNumber, AFFILIATE_CATEGORIES, applyFilters, isBlockedState, findAffiliatePage, pruneTabs, isJunkTab };
558
+ module.exports = { ingestProduct, scrapeOffers, generateOfferLink, linkFailureReason, cleanSubId, parseProductUrl, parseOfferRow, parsePdpItem, categoryGroup, pctToNumber, AFFILIATE_CATEGORIES, applyFilters, isBlockedState, findAffiliatePage, pruneTabs, isJunkTab };
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
- if (!wasOwner) {
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
- // Spawn new daemon using global binary (picks up new version)
181
- const isWindows = process.platform === 'win32';
182
- const binName = isWindows ? 'channel-worker.cmd' : 'channel-worker';
183
- if (wasOwner) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "channel-worker",
3
- "version": "2.5.131",
3
+ "version": "2.5.133",
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": {