channel-worker 2.5.137 → 2.5.138

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,8 +388,29 @@ 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
+
391
408
  const saved = await this.api.affiliateReportResult(result.rows, {
392
409
  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,
393
414
  last_report_date: result.last_report_date,
394
415
  last_update_time: result.last_update_time,
395
416
  from: result.from,
@@ -406,6 +427,8 @@ class CommandPoller {
406
427
  click_saved: saved?.click_saved || 0,
407
428
  click_matched: saved?.click_matched || 0,
408
429
  click_unmatched: saved?.click_unmatched || 0,
430
+ media_accounts: mediaAccounts ? mediaAccounts.length : null,
431
+ media_error: mediaError || undefined,
409
432
  last_report_date: result.last_report_date,
410
433
  },
411
434
  });
@@ -183,4 +183,78 @@ async function fetchClickReport(ctx, {
183
183
  }
184
184
  }
185
185
 
186
- module.exports = { fetchConversionReport, fetchClickReport, reportWindow };
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "channel-worker",
3
- "version": "2.5.137",
3
+ "version": "2.5.138",
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": {
@@ -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, readDelegatePageId } = 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,29 @@ 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
+ }
838
+ // MÃ TRANG Shopee dùng để đối chiếu (`delegate_page_id`) — KHÁC profile id.
839
+ // Không có nó thì API không kết luận được kênh đã liên kết hay chưa, và cố
840
+ // tình để ở "chưa rõ" còn hơn báo sai. Xem readDelegatePageId.
841
+ if (pageRef) {
842
+ const dg = await readDelegatePageId(page, pageRef.id).catch(() => ({ id: '', checked: false }));
843
+ pageRef.delegate_id = dg.id;
844
+ pageRef.delegate_checked = dg.checked;
845
+ log('info', `[fb-stats] mã Trang (Shopee dùng): ${dg.id || (dg.checked ? 'không có — page kiểu cổ điển' : 'CHƯA rõ')}`);
846
+ }
824
847
  if (pageRef) log('info', `[fb-stats] page: ${pageRef.id ? `id ${pageRef.id}` : `handle ${pageRef.handle}`}`);
825
848
 
826
849
  const d = await log.apiRequest('POST', '/analytics/post-stats', {
@@ -1990,6 +1990,115 @@ function parsePageRef(reelsTabUrl) {
1990
1990
  return { id: '', handle: seg, url: raw };
1991
1991
  }
1992
1992
 
1993
+ // ID page ĐỌC TỪ CHÍNH TRANG, cho ca URL chỉ có tên riêng (`/<handle>/?sk=reels_tab`).
1994
+ //
1995
+ // Vì sao cần: bên Shopee, 6/8 page đang liên kết được lưu dạng
1996
+ // `profile.php?id=…`, KHÔNG có tên riêng trong link. Kênh nào chỉ biết tên riêng
1997
+ // thì không có gì để so → bị báo "chưa liên kết" OAN (đo bằng
1998
+ // services/AffiliateMediaLink 22/09). Dạng tên riêng có thật: ca "Chuyện Nghề
1999
+ // Nhà Nông" 14/09 — dạng profile.php ra 0 tile, chỉ `/chuyennghenhanong/` chạy.
2000
+ //
2001
+ // CHỈ gọi khi đang đứng trên trang tab Reels ĐÃ chứng minh có tile. Đứng ở trang
2002
+ // khác thì mấy khoá dưới đây là của trang đó, không phải của page mình.
2003
+ //
2004
+ // Đo thật 22/09 trên 6 profile của đàn (Toc-xinh, Meo-luoi, Tam-cai-tao-nha,
2005
+ // Beo-banh-beo, duong-nhan-cung-anh, Do-nghe-len-hinh):
2006
+ // `profile_id` == `actorID` == ID page trong URL ở CẢ 6/6.
2007
+ // ⛔ `delegate_page_id` LUÔN là một số KHÁC (vd page 61591917189886 →
2008
+ // delegate 1240255979169594). Bắt nhầm khoá này là ghi sai danh tính rồi
2009
+ // báo sai liên kết cho đúng cái kênh mình định cứu.
2010
+ //
2011
+ // 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.
2012
+ // Một khoá JSON đứng lẻ không đủ — trang Facebook đầy số 15-16 chữ số, và nếu
2013
+ // profile đang đăng nhập bằng tài khoản CÁ NHÂN quản lý page thì `userID` là của
2014
+ // 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.
2015
+ const PAGE_ID_JSON_KEYS = ['profile_id', 'actorID', 'pageID'];
2016
+
2017
+ async function readPageIdFromDom(page) {
2018
+ const votes = await page.evaluate((keys) => {
2019
+ const out = {};
2020
+ const add = (src, v) => {
2021
+ if (!/^\d{5,25}$/.test(String(v || ''))) return;
2022
+ (out[src] = out[src] || []).push(String(v));
2023
+ };
2024
+ // Nguồn 1: anchor profile.php NGOÀI bảng tin. Anchor trong feed là của bài
2025
+ // người khác — đúng cái bẫy đã đẻ ra 2 link sai sáng 10/09.
2026
+ for (const a of document.querySelectorAll('a[href]')) {
2027
+ const href = a.getAttribute('href') || '';
2028
+ if (/[?&]__cft__|[?&]__tn__/.test(href)) continue;
2029
+ if (a.closest("[role='feed'], [role='article'], [aria-posinset]")) continue;
2030
+ const m = href.match(/profile\.php\?id=(\d{5,25})/);
2031
+ if (m) add('anchor', m[1]);
2032
+ }
2033
+ // Nguồn 2..n: khoá JSON trong bundle. `delegate_page_id` cố ý KHÔNG có mặt.
2034
+ const html = document.documentElement.innerHTML;
2035
+ for (const key of keys) {
2036
+ const re = new RegExp(`"${key}"\\s*:\\s*"?(\\d{5,25})"?`, 'g');
2037
+ let m; let n = 0;
2038
+ while ((m = re.exec(html)) && n < 8) { add(key, m[1]); n++; }
2039
+ }
2040
+ return out;
2041
+ }, PAGE_ID_JSON_KEYS).catch(() => ({}));
2042
+
2043
+ // Đếm theo NGUỒN, không theo số lần xuất hiện: một khoá lặp 8 lần trong bundle
2044
+ // vẫn chỉ là MỘT nguồn, không được tự nó đủ phiếu.
2045
+ const bySource = new Map();
2046
+ for (const [src, vals] of Object.entries(votes || {})) {
2047
+ for (const v of new Set(vals)) {
2048
+ if (!bySource.has(v)) bySource.set(v, new Set());
2049
+ bySource.get(v).add(src);
2050
+ }
2051
+ }
2052
+ let best = null;
2053
+ for (const [id, srcs] of bySource) {
2054
+ if (srcs.size < 2) continue;
2055
+ if (!best || srcs.size > best.sources) best = { id, sources: srcs.size, from: [...srcs].sort() };
2056
+ }
2057
+ return best;
2058
+ }
2059
+
2060
+ // MÃ TRANG mà Shopee dùng để đối chiếu — `delegate_page_id`.
2061
+ //
2062
+ // ⚠️ Page Facebook kiểu mới (New Pages Experience) mang HAI số: `profile id`
2063
+ // nằm trong URL (14 số, mở đầu `61…`) và `delegate_page_id` là ID Trang cổ
2064
+ // điển nằm sau nó (15-16 số, mở đầu `1…`). **Shopee lưu số THỨ HAI.**
2065
+ // Đo 22/09: page `chuyennghenhanong` (profile 61591308775391) hiện trên Shopee
2066
+ // là 1165088293355326 = đúng delegate của nó. So bằng profile id là KHÔNG BAO
2067
+ // GIỜ khớp — lỗi đã lỡ đưa lên prod một lần.
2068
+ //
2069
+ // Khác `profile_id`/`actorID` (là của NGƯỜI ĐANG XEM), `delegate_page_id` nói
2070
+ // về TRANG ĐANG MỞ: đo trên 3 page lạ, nó trả đúng delegate của trang đó.
2071
+ //
2072
+ // Trả { id, checked }:
2073
+ // - đúng MỘT giá trị → id = nó, checked = true
2074
+ // - KHÔNG có giá trị nào → page kiểu CỔ ĐIỂN (profile id chính là khoá
2075
+ // Shopee) → id rỗng, checked = true. Chỉ dám kết luận vậy khi profile id
2076
+ // KHÔNG mang hình dạng page kiểu mới; nếu nó là `61…` mà không đọc được
2077
+ // delegate thì đó là đọc HỤT, không phải page cổ điển.
2078
+ // - NHIỀU giá trị → profile quản nhiều page (đo thật ở
2079
+ // `duong-nhan-cung-anh`: 2 delegate). Không đoán → checked = false, để bên
2080
+ // API giữ trạng thái "chưa rõ" thay vì báo sai.
2081
+ const NEW_STYLE_PROFILE_ID = /^61\d{12}$/;
2082
+
2083
+ async function readDelegatePageId(page, profileId = '') {
2084
+ const found = await page.evaluate(() => {
2085
+ const out = new Set();
2086
+ const re = /"delegate_page_id"\s*:\s*"?(\d{5,25})"?/g;
2087
+ const html = document.documentElement.innerHTML;
2088
+ let m; let n = 0;
2089
+ while ((m = re.exec(html)) && n < 20) { out.add(m[1]); n++; }
2090
+ return [...out];
2091
+ }).catch(() => null);
2092
+
2093
+ if (found === null) return { id: '', checked: false };
2094
+ if (found.length === 1) return { id: found[0], checked: true };
2095
+ if (found.length === 0) {
2096
+ // Không có delegate. Chỉ coi là page cổ điển khi profile id không phải dạng mới.
2097
+ return { id: '', checked: !NEW_STYLE_PROFILE_ID.test(String(profileId || '')) };
2098
+ }
2099
+ return { id: '', checked: false }; // nhiều page trong cùng profile — không đoán
2100
+ }
2101
+
1993
2102
  async function hasVisibleReelComposer(page) {
1994
2103
  return page.evaluate(() => {
1995
2104
  for (const dlg of document.querySelectorAll("[role='dialog']")) {
@@ -4569,6 +4678,8 @@ module.exports.__testables = {
4569
4678
  pickReelsTabUrl,
4570
4679
  pickReelsTabUrls,
4571
4680
  parsePageRef,
4681
+ readPageIdFromDom,
4682
+ readDelegatePageId,
4572
4683
  discoverReelsTabUrls,
4573
4684
  openReelsTabWithTiles,
4574
4685
  networkIdVerdict,