channel-worker 2.5.147 → 2.5.149
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/command-poller.js +48 -11
- package/lib/shopee-report.js +124 -42
- package/package.json +1 -1
- package/scripts/fetch_facebook_reel_stats.js +5 -2
- package/scripts/upload_facebook.js +69 -25
package/lib/command-poller.js
CHANGED
|
@@ -27,6 +27,8 @@ const SHOPEE_DEADLINE_MS = {
|
|
|
27
27
|
get_affiliate_link: 3 * 60 * 1000,
|
|
28
28
|
ingest_shopee_product: 3 * 60 * 1000,
|
|
29
29
|
};
|
|
30
|
+
// Mỗi lượt gửi báo cáo về API mang tối đa ngần này đơn và ngần này click.
|
|
31
|
+
const REPORT_INGEST_BATCH = 5000;
|
|
30
32
|
// Tripo Studio: một lượt HD multiview gồm tải ảnh + tạo (vài phút tới hơn
|
|
31
33
|
// chục phút tuỳ hàng chờ của Tripo) + xuất GLB. Hết hạn thì bỏ lượt, Studio
|
|
32
34
|
// vẫn chạy nốt trong tab — người mở profile vẫn thấy model.
|
|
@@ -549,14 +551,31 @@ class CommandPoller {
|
|
|
549
551
|
days: payload.days || 90,
|
|
550
552
|
log: (message) => console.log(message),
|
|
551
553
|
});
|
|
554
|
+
// Click hỏng thì ĐƠN đã đọc xong vẫn phải lưu — 03/10/2026 click Shopee2
|
|
555
|
+
// vượt trần 100 trang, ba lượt sáng vứt luôn phần đơn đọc được. Biểu đồ
|
|
556
|
+
// tính mốc chốt click theo giờ đồng bộ của CLICK, nên lưu riêng đơn không
|
|
557
|
+
// làm ngày chưa có click bị gắn "đã chốt".
|
|
552
558
|
if (clickResult.status === 'needs_verify') {
|
|
559
|
+
const saved = await this._sendAffiliateReport(result.rows, [], {
|
|
560
|
+
last_report_date: result.last_report_date,
|
|
561
|
+
last_update_time: result.last_update_time,
|
|
562
|
+
});
|
|
553
563
|
return {
|
|
554
564
|
status: 'done',
|
|
555
|
-
result: {
|
|
565
|
+
result: {
|
|
566
|
+
needs_verify: true, reason: clickResult.reason,
|
|
567
|
+
count: result.rows.length, saved: saved.saved,
|
|
568
|
+
click_count: clickResult.rows?.length || 0,
|
|
569
|
+
},
|
|
556
570
|
};
|
|
557
571
|
}
|
|
558
572
|
if (clickResult.status !== 'ok') {
|
|
559
|
-
|
|
573
|
+
const saved = await this._sendAffiliateReport(result.rows, [], {
|
|
574
|
+
last_report_date: result.last_report_date,
|
|
575
|
+
last_update_time: result.last_update_time,
|
|
576
|
+
});
|
|
577
|
+
const why = clickResult.reason || 'Không đọc được báo cáo click Shopee';
|
|
578
|
+
return { status: 'failed', error: `Click lỗi: ${why}; đã lưu ${saved.saved}/${result.rows.length} đơn`.slice(0, 500) };
|
|
560
579
|
}
|
|
561
580
|
|
|
562
581
|
// Danh sách page đã liên kết với tài khoản Shopee — đi CHUNG lượt này vì
|
|
@@ -576,8 +595,7 @@ class CommandPoller {
|
|
|
576
595
|
}
|
|
577
596
|
if (mediaError) console.warn(`[shopee-report] không đọc được danh sách liên kết: ${mediaError}`);
|
|
578
597
|
|
|
579
|
-
const saved = await this.
|
|
580
|
-
clicks: clickResult.rows,
|
|
598
|
+
const saved = await this._sendAffiliateReport(result.rows, clickResult.rows, {
|
|
581
599
|
// `null` = KHÔNG đọc được, khác hẳn `[]` = đọc được và tài khoản chưa
|
|
582
600
|
// liên kết page nào. API phải phân biệt để không xoá sạch bản ghi cũ
|
|
583
601
|
// chỉ vì một lượt đọc hỏng.
|
|
@@ -587,18 +605,18 @@ class CommandPoller {
|
|
|
587
605
|
from: result.from,
|
|
588
606
|
to: result.to,
|
|
589
607
|
});
|
|
590
|
-
console.log(`[shopee-report] đã lưu ${saved
|
|
608
|
+
console.log(`[shopee-report] đã lưu ${saved.saved}/${result.rows.length} đơn, ${saved.click_saved}/${clickResult.rows.length} click`);
|
|
591
609
|
return {
|
|
592
610
|
status: 'done',
|
|
593
611
|
result: {
|
|
594
612
|
count: result.rows.length,
|
|
595
|
-
saved: saved
|
|
596
|
-
matched: saved
|
|
597
|
-
unmatched: saved
|
|
613
|
+
saved: saved.saved,
|
|
614
|
+
matched: saved.matched,
|
|
615
|
+
unmatched: saved.unmatched,
|
|
598
616
|
click_count: clickResult.rows.length,
|
|
599
|
-
click_saved: saved
|
|
600
|
-
click_matched: saved
|
|
601
|
-
click_unmatched: saved
|
|
617
|
+
click_saved: saved.click_saved,
|
|
618
|
+
click_matched: saved.click_matched,
|
|
619
|
+
click_unmatched: saved.click_unmatched,
|
|
602
620
|
media_accounts: mediaAccounts ? mediaAccounts.length : null,
|
|
603
621
|
media_error: mediaError || undefined,
|
|
604
622
|
last_report_date: result.last_report_date,
|
|
@@ -606,6 +624,25 @@ class CommandPoller {
|
|
|
606
624
|
};
|
|
607
625
|
}
|
|
608
626
|
|
|
627
|
+
// Gửi đơn + click về API theo lô. API chỉ nhận 10.000 đơn / 20.000 click
|
|
628
|
+
// mỗi lượt gửi (AffiliateReportController.workerIngest cắt phần thừa, không
|
|
629
|
+
// báo gì) — đọc được quá trần 100 trang Shopee rồi mà gửi một cục thì vẫn mất.
|
|
630
|
+
// `media_accounts` chỉ đi ở lô đầu: lô sau thiếu trường đó = API giữ nguyên.
|
|
631
|
+
async _sendAffiliateReport(orders, clicks, { media_accounts, ...meta } = {}) {
|
|
632
|
+
const sum = { saved: 0, matched: 0, unmatched: 0, click_saved: 0, click_matched: 0, click_unmatched: 0 };
|
|
633
|
+
const lots = Math.max(1, Math.ceil(orders.length / REPORT_INGEST_BATCH), Math.ceil(clicks.length / REPORT_INGEST_BATCH));
|
|
634
|
+
for (let i = 0; i < lots; i++) {
|
|
635
|
+
const cut = (list) => list.slice(i * REPORT_INGEST_BATCH, (i + 1) * REPORT_INGEST_BATCH);
|
|
636
|
+
const res = await this.api.affiliateReportResult(cut(orders), {
|
|
637
|
+
...meta,
|
|
638
|
+
clicks: cut(clicks),
|
|
639
|
+
...(i === 0 && media_accounts !== undefined ? { media_accounts } : {}),
|
|
640
|
+
});
|
|
641
|
+
for (const key of Object.keys(sum)) sum[key] += Number(res?.[key] || 0);
|
|
642
|
+
}
|
|
643
|
+
return sum;
|
|
644
|
+
}
|
|
645
|
+
|
|
609
646
|
async handlePlaywrightCommand(command) {
|
|
610
647
|
const { runPlaywrightScript } = require('./playwright-runner');
|
|
611
648
|
const payload = command.payload || {};
|
package/lib/shopee-report.js
CHANGED
|
@@ -13,6 +13,87 @@ function reportWindow(days = 90, nowMs = Date.now()) {
|
|
|
13
13
|
return { start, end, days: safeDays };
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
// Shopee chỉ cho mỗi truy vấn danh sách báo cáo đọc tới trang 100. Đo 03/10/2026
|
|
17
|
+
// trên store maithithu (Shopee2): 10.410 click trong 90 ngày, page_size 100 →
|
|
18
|
+
// trang 1–100 trả đủ, trang 101 trả `msg: "page num out of limit"` và cả lượt
|
|
19
|
+
// đồng bộ chết. Chưa thử page_size khác nên chưa biết trần tính theo số trang
|
|
20
|
+
// hay theo vị trí dòng — lấy mức thấp hơn của hai cách hiểu.
|
|
21
|
+
const SHOPEE_MAX_PAGE = 100;
|
|
22
|
+
const SHOPEE_MAX_ROWS = 10000;
|
|
23
|
+
const PAGE_LIMIT_RE = /page num out of limit/i;
|
|
24
|
+
// Khoảng ngắn hơn mức này mà vẫn quá trần thì thôi chẻ, báo lỗi.
|
|
25
|
+
const MIN_SPLIT_SEC = 3600;
|
|
26
|
+
|
|
27
|
+
function vnDay(sec) {
|
|
28
|
+
return new Date(sec * 1000 + 7 * 3600e3).toISOString().slice(0, 10);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Đọc mọi dòng của khoảng [start, end] (giây). `readPage(pageNum, start, end)`
|
|
32
|
+
// gọi Shopee một trang, trả { status: 'ok', batch, total } hoặc
|
|
33
|
+
// { status: 'needs_verify' | 'failed', reason }. Khoảng nào Shopee báo nhiều
|
|
34
|
+
// dòng hơn một truy vấn đọc được thì chẻ đôi rồi đọc từng nửa. Hai nửa dùng
|
|
35
|
+
// CHUNG mốc giữa (chưa rõ Shopee có tính dòng nằm đúng mốc cuối không) nên
|
|
36
|
+
// dòng ở mốc đó có thể về hai lần — lọc trùng bằng `keyOf`.
|
|
37
|
+
async function readRange(readPage, {
|
|
38
|
+
start, end, pageSize, maxPages, delayMs, keyOf, name, log,
|
|
39
|
+
}) {
|
|
40
|
+
const cap = Math.min(pageSize * SHOPEE_MAX_PAGE, SHOPEE_MAX_ROWS);
|
|
41
|
+
const rows = [];
|
|
42
|
+
const seen = new Set();
|
|
43
|
+
|
|
44
|
+
const take = (batch) => {
|
|
45
|
+
for (const row of batch) {
|
|
46
|
+
const key = keyOf(row);
|
|
47
|
+
if (key) {
|
|
48
|
+
if (seen.has(key)) continue;
|
|
49
|
+
seen.add(key);
|
|
50
|
+
}
|
|
51
|
+
rows.push(row);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
async function split(s, e, why) {
|
|
56
|
+
const mid = Math.floor((s + e) / 2);
|
|
57
|
+
if (log) log(`[shopee-report] ${name} ${vnDay(s)}→${vnDay(e)}: ${why} — chia đôi khoảng đọc`);
|
|
58
|
+
const first = await read(s, mid);
|
|
59
|
+
if (first.status !== 'ok') return first;
|
|
60
|
+
await sleep(delayMs);
|
|
61
|
+
return read(mid, e);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function read(s, e) {
|
|
65
|
+
let got = 0;
|
|
66
|
+
let total = null;
|
|
67
|
+
for (let pageNum = 1; pageNum <= maxPages; pageNum++) {
|
|
68
|
+
const r = await readPage(pageNum, s, e);
|
|
69
|
+
if (r.status !== 'ok') {
|
|
70
|
+
// Lưới thứ hai: trần thật thấp hơn mức đã đo thì Shopee vẫn chặn giữa chừng.
|
|
71
|
+
if (r.status === 'failed' && PAGE_LIMIT_RE.test(r.reason || '') && e - s > MIN_SPLIT_SEC) {
|
|
72
|
+
return split(s, e, `Shopee chặn ở trang ${pageNum}`);
|
|
73
|
+
}
|
|
74
|
+
return r;
|
|
75
|
+
}
|
|
76
|
+
total = r.total;
|
|
77
|
+
if (pageNum === 1 && total > cap) {
|
|
78
|
+
if (e - s > MIN_SPLIT_SEC) return split(s, e, `${total} dòng > ${cap} dòng/lượt đọc`);
|
|
79
|
+
return { status: 'failed', reason: `${name} ${vnDay(s)}: ${total} dòng trong chưa tới 1 giờ, vượt trần Shopee ${cap} dòng/lượt đọc` };
|
|
80
|
+
}
|
|
81
|
+
got += r.batch.length;
|
|
82
|
+
take(r.batch);
|
|
83
|
+
if (log) log(`[shopee-report] ${name} trang ${pageNum}: ${r.batch.length} dòng (${got}/${total})`);
|
|
84
|
+
if (!r.batch.length || got >= total || r.batch.length < pageSize) break;
|
|
85
|
+
await sleep(delayMs);
|
|
86
|
+
}
|
|
87
|
+
if (total != null && got < total) {
|
|
88
|
+
return { status: 'failed', reason: `${name}: phân trang dừng ở ${got}/${total} dòng` };
|
|
89
|
+
}
|
|
90
|
+
return { status: 'ok' };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const outcome = await read(start, end);
|
|
94
|
+
return { ...outcome, rows };
|
|
95
|
+
}
|
|
96
|
+
|
|
16
97
|
async function fetchConversionReport(ctx, {
|
|
17
98
|
days = 90,
|
|
18
99
|
pageSize = 100,
|
|
@@ -41,8 +122,6 @@ async function fetchConversionReport(ctx, {
|
|
|
41
122
|
}
|
|
42
123
|
|
|
43
124
|
const window = reportWindow(days);
|
|
44
|
-
const rows = [];
|
|
45
|
-
let total = null;
|
|
46
125
|
let lastReportDate = null;
|
|
47
126
|
let lastUpdateTime = null;
|
|
48
127
|
|
|
@@ -68,7 +147,7 @@ async function fetchConversionReport(ctx, {
|
|
|
68
147
|
lastReportDate = updated?.data?.conversionReportUpdateTime?.lastReportDate || null;
|
|
69
148
|
lastUpdateTime = updated?.data?.conversionReportUpdateTime?.lastUpdateTime || null;
|
|
70
149
|
|
|
71
|
-
|
|
150
|
+
const readPage = async (pageNum, start, end) => {
|
|
72
151
|
const result = await evaluateWithin(page, async (q) => {
|
|
73
152
|
const url = `/api/v3/report/list?page_size=${q.pageSize}&page_num=${q.pageNum}`
|
|
74
153
|
+ `&purchase_time_s=${q.start}&purchase_time_e=${q.end}&version=1`;
|
|
@@ -78,33 +157,35 @@ async function fetchConversionReport(ctx, {
|
|
|
78
157
|
} catch (e) {
|
|
79
158
|
return { status: 0, error: String(e?.message || e) };
|
|
80
159
|
}
|
|
81
|
-
}, { pageSize, pageNum, start
|
|
160
|
+
}, { pageSize, pageNum, start, end }, evalTimeoutMs);
|
|
82
161
|
|
|
83
162
|
if (isAntiBotBody(result.json)) {
|
|
84
|
-
return { status: 'needs_verify',
|
|
163
|
+
return { status: 'needs_verify', reason: ANTI_BOT_REASON };
|
|
85
164
|
}
|
|
86
165
|
if (isBlockedState(page.url(), result)) {
|
|
87
|
-
return { status: 'needs_verify',
|
|
166
|
+
return { status: 'needs_verify', reason: `blocked while reading report (http ${result.status}, code ${result.json?.code})` };
|
|
88
167
|
}
|
|
89
168
|
if (result.status !== 200 || result.json?.code !== 0) {
|
|
90
|
-
return { status: 'failed',
|
|
169
|
+
return { status: 'failed', reason: result.json?.msg || result.error || `report HTTP ${result.status}` };
|
|
91
170
|
}
|
|
92
171
|
|
|
93
172
|
const data = result.json?.data || {};
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
173
|
+
return {
|
|
174
|
+
status: 'ok',
|
|
175
|
+
batch: Array.isArray(data.list) ? data.list : [],
|
|
176
|
+
total: Number(data.total_count || 0),
|
|
177
|
+
};
|
|
178
|
+
};
|
|
101
179
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
180
|
+
const read = await readRange(readPage, {
|
|
181
|
+
start: window.start, end: window.end, pageSize, maxPages, delayMs,
|
|
182
|
+
keyOf: (row) => row?.checkout_id, name: 'đơn', log,
|
|
183
|
+
});
|
|
184
|
+
const { rows } = read;
|
|
185
|
+
if (read.status !== 'ok') return { status: read.status, rows, reason: read.reason };
|
|
105
186
|
|
|
106
187
|
return {
|
|
107
|
-
status: 'ok', rows, total:
|
|
188
|
+
status: 'ok', rows, total: rows.length,
|
|
108
189
|
from: new Date(window.start * 1000).toISOString(),
|
|
109
190
|
to: new Date(window.end * 1000).toISOString(),
|
|
110
191
|
last_report_date: lastReportDate,
|
|
@@ -143,10 +224,8 @@ async function fetchClickReport(ctx, {
|
|
|
143
224
|
}
|
|
144
225
|
|
|
145
226
|
const window = reportWindow(days);
|
|
146
|
-
const rows = [];
|
|
147
|
-
let total = null;
|
|
148
227
|
|
|
149
|
-
|
|
228
|
+
const readPage = async (pageNum, start, end) => {
|
|
150
229
|
const result = await evaluateWithin(page, async (q) => {
|
|
151
230
|
const url = `/api/v1/click_report/list?page_size=${q.pageSize}&page_num=${q.pageNum}`
|
|
152
231
|
+ `&click_time_s=${q.start}&click_time_e=${q.end}`;
|
|
@@ -156,40 +235,43 @@ async function fetchClickReport(ctx, {
|
|
|
156
235
|
} catch (e) {
|
|
157
236
|
return { status: 0, error: String(e?.message || e) };
|
|
158
237
|
}
|
|
159
|
-
}, { pageSize, pageNum, start
|
|
238
|
+
}, { pageSize, pageNum, start, end }, evalTimeoutMs);
|
|
160
239
|
|
|
161
240
|
if (isAntiBotBody(result.json)) {
|
|
162
|
-
return { status: 'needs_verify',
|
|
241
|
+
return { status: 'needs_verify', reason: ANTI_BOT_REASON };
|
|
163
242
|
}
|
|
164
243
|
if (isBlockedState(page.url(), result)) {
|
|
165
|
-
return { status: 'needs_verify',
|
|
244
|
+
return { status: 'needs_verify', reason: `blocked while reading click report (http ${result.status}, code ${result.json?.code})` };
|
|
166
245
|
}
|
|
167
246
|
if (result.status !== 200 || result.json?.code !== 0) {
|
|
168
|
-
return { status: 'failed',
|
|
247
|
+
return { status: 'failed', reason: result.json?.msg || result.error || `click report HTTP ${result.status}` };
|
|
169
248
|
}
|
|
170
249
|
|
|
171
250
|
const data = result.json?.data || {};
|
|
172
251
|
const batch = Array.isArray(data.list) ? data.list : [];
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
}
|
|
252
|
+
return {
|
|
253
|
+
status: 'ok',
|
|
254
|
+
batch: batch.map((row) => ({
|
|
255
|
+
click_id: row.click_id,
|
|
256
|
+
click_time: row.click_time,
|
|
257
|
+
sub_id: row.sub_id,
|
|
258
|
+
click_region: row.click_region,
|
|
259
|
+
referrer: row.referrer,
|
|
260
|
+
direct_source: row.direct_source,
|
|
261
|
+
})),
|
|
262
|
+
total: Number(data.total_count || 0),
|
|
263
|
+
};
|
|
264
|
+
};
|
|
186
265
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
266
|
+
const read = await readRange(readPage, {
|
|
267
|
+
start: window.start, end: window.end, pageSize, maxPages, delayMs,
|
|
268
|
+
keyOf: (row) => row.click_id, name: 'click', log,
|
|
269
|
+
});
|
|
270
|
+
const { rows } = read;
|
|
271
|
+
if (read.status !== 'ok') return { status: read.status, rows, reason: read.reason };
|
|
190
272
|
|
|
191
273
|
return {
|
|
192
|
-
status: 'ok', rows, total:
|
|
274
|
+
status: 'ok', rows, total: rows.length,
|
|
193
275
|
from: new Date(window.start * 1000).toISOString(),
|
|
194
276
|
to: new Date(window.end * 1000).toISOString(),
|
|
195
277
|
};
|
package/package.json
CHANGED
|
@@ -839,10 +839,13 @@ async function run({ page, payload, log }) {
|
|
|
839
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
840
|
// tình để ở "chưa rõ" còn hơn báo sai. Xem readDelegatePageId.
|
|
841
841
|
if (pageRef) {
|
|
842
|
-
const dg = await readDelegatePageId(page, pageRef.id).catch(() => ({ id: '', checked: false }));
|
|
842
|
+
const dg = await readDelegatePageId(page, pageRef.id).catch(() => ({ id: '', checked: false, reason: 'lỗi khi đọc' }));
|
|
843
843
|
pageRef.delegate_id = dg.id;
|
|
844
844
|
pageRef.delegate_checked = dg.checked;
|
|
845
|
-
|
|
845
|
+
// Giữ nguyên tiền tố "mã Trang (Shopee dùng): <số>" — grep log dựa vào nó.
|
|
846
|
+
// Lý do đi sau, để ca hụt nói được vì 0 mã hay vì nhiều mã.
|
|
847
|
+
const what = dg.id || (dg.checked ? 'không có — page kiểu cổ điển' : 'CHƯA rõ');
|
|
848
|
+
log('info', `[fb-stats] mã Trang (Shopee dùng): ${what}${dg.reason ? ` (${dg.reason})` : ''}`);
|
|
846
849
|
}
|
|
847
850
|
if (pageRef) log('info', `[fb-stats] page: ${pageRef.id ? `id ${pageRef.id}` : `handle ${pageRef.handle}`}`);
|
|
848
851
|
|
|
@@ -2153,34 +2153,76 @@ async function readPageIdFromDom(page) {
|
|
|
2153
2153
|
// Khác `profile_id`/`actorID` (là của NGƯỜI ĐANG XEM), `delegate_page_id` nói
|
|
2154
2154
|
// về TRANG ĐANG MỞ: đo trên 3 page lạ, nó trả đúng delegate của trang đó.
|
|
2155
2155
|
//
|
|
2156
|
-
//
|
|
2157
|
-
//
|
|
2158
|
-
//
|
|
2159
|
-
//
|
|
2160
|
-
//
|
|
2161
|
-
//
|
|
2162
|
-
//
|
|
2163
|
-
//
|
|
2164
|
-
//
|
|
2165
|
-
|
|
2156
|
+
// ⚠️ MENU CHUYỂN TÀI KHOẢN cũng mang `delegate_page_id` (đo 02/10/2026, ca
|
|
2157
|
+
// "Bí Quyết Nhà Nông", profile NST khoe-khong-kho). Phiên đang đứng dưới danh
|
|
2158
|
+
// nghĩa page, nên trang nhúng sẵn mục để chuyển về TÀI KHOẢN CÁ NHÂN đã tạo
|
|
2159
|
+
// page (`ProfileSwitcherEligibleProfile`). Tài khoản cá nhân id dạng cũ `100…`
|
|
2160
|
+
// thì mục đó ghi `"delegate_page_id":null` — 5 profile đọc tốt đều vậy. Tài
|
|
2161
|
+
// khoản cá nhân "Lan Linh" (id dạng mới 61590196273118) lại mang một SỐ
|
|
2162
|
+
// (1193846817137262). Đếm chung mọi giá trị thì thành 2 mã → "chưa rõ" ở MỌI
|
|
2163
|
+
// lượt (47/47), và page không bao giờ được đối chiếu với Shopee. Tệ hơn: page
|
|
2164
|
+
// nào không lộ mã của chính nó thì bản cũ nhận nhầm mã của tài khoản cá nhân.
|
|
2165
|
+
//
|
|
2166
|
+
// Nên mỗi lần xuất hiện được phân loại theo VỊ TRÍ, không chỉ theo giá trị:
|
|
2167
|
+
// own — nằm cùng object phẳng với `"id":"<profile id của page>"`
|
|
2168
|
+
// (vd `"user":{"id":"6159…","name":"…","delegate_page_id":"…"}`
|
|
2169
|
+
// ở thanh điều hướng trái). Chắc nhất: gắn thẳng với page đang mở.
|
|
2170
|
+
// switcher — mục menu chuyển tài khoản. Không bao giờ là mã của page.
|
|
2171
|
+
// other — còn lại (vd bài viết trong lưới Reels).
|
|
2172
|
+
//
|
|
2173
|
+
// Hàm THUẦN và TỰ CHỨA (không dùng biến ngoài): `readDelegatePageId` gửi
|
|
2174
|
+
// nguyên văn mã nguồn của nó vào trang, nên test chạy đúng đoạn chạy trên
|
|
2175
|
+
// Facebook.
|
|
2176
|
+
function scanDelegatesInHtml(html, profileId) {
|
|
2177
|
+
const re = /"delegate_page_id"\s*:\s*"?(\d{5,25})"?/g;
|
|
2178
|
+
const pid = String(profileId || '');
|
|
2179
|
+
const own = new Set(); const switcher = new Set(); const other = new Set();
|
|
2180
|
+
let m; let n = 0;
|
|
2181
|
+
while ((m = re.exec(html)) && n < 60) {
|
|
2182
|
+
n++;
|
|
2183
|
+
const before = html.slice(Math.max(0, m.index - 600), m.index);
|
|
2184
|
+
const owner = before.match(/"id"\s*:\s*"(\d{5,25})"[^{}]*$/);
|
|
2185
|
+
if (pid && owner && owner[1] === pid) { own.add(m[1]); continue; }
|
|
2186
|
+
const after = html.slice(m.index + m[0].length, m.index + m[0].length + 400);
|
|
2187
|
+
const sw = after.indexOf('ProfileSwitcherEligibleProfile');
|
|
2188
|
+
const next = after.indexOf('"delegate_page_id"');
|
|
2189
|
+
if (sw !== -1 && (next === -1 || sw < next)) { switcher.add(m[1]); continue; }
|
|
2190
|
+
other.add(m[1]);
|
|
2191
|
+
}
|
|
2192
|
+
return { own: [...own], switcher: [...switcher], other: [...other] };
|
|
2193
|
+
}
|
|
2166
2194
|
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2195
|
+
// Trả { id, checked, reason }:
|
|
2196
|
+
// - đúng MỘT mã gắn với profile id của page → nhận.
|
|
2197
|
+
// - không có mã nào gắn được → luật cũ trên các mã còn lại, ĐÃ BỎ mục menu
|
|
2198
|
+
// chuyển tài khoản: đúng một mã thì nhận; NHIỀU mã thì không đoán
|
|
2199
|
+
// (`duong-nhan-cung-anh` 22/09: 2 mã), checked = false để API giữ "chưa rõ".
|
|
2200
|
+
// - KHÔNG có mã nào → page kiểu CỔ ĐIỂN (profile id chính là khoá Shopee) →
|
|
2201
|
+
// id rỗng, checked = true. Chỉ dám kết luận vậy khi profile id KHÔNG mang
|
|
2202
|
+
// hình dạng page kiểu mới; `61…` mà không có mã là đọc HỤT.
|
|
2203
|
+
// `reason` chỉ để ghi log: trước đây mọi ca hụt đều in "CHƯA rõ" nên không
|
|
2204
|
+
// ai biết hụt vì 0 mã hay vì nhiều mã.
|
|
2205
|
+
const NEW_STYLE_PROFILE_ID = /^61\d{12}$/;
|
|
2176
2206
|
|
|
2177
|
-
|
|
2178
|
-
if (
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2207
|
+
function pickDelegate(scan, profileId = '') {
|
|
2208
|
+
if (!scan) return { id: '', checked: false, reason: 'không đọc được trang' };
|
|
2209
|
+
const own = scan.own || [];
|
|
2210
|
+
const rest = scan.other || [];
|
|
2211
|
+
const sw = scan.switcher || [];
|
|
2212
|
+
if (own.length === 1) return { id: own[0], checked: true, reason: 'gắn với profile id của page' };
|
|
2213
|
+
if (own.length > 1) return { id: '', checked: false, reason: `${own.length} mã cùng gắn với profile id của page` };
|
|
2214
|
+
if (rest.length === 1) return { id: rest[0], checked: true, reason: 'mã duy nhất trên trang' };
|
|
2215
|
+
if (rest.length > 1) return { id: '', checked: false, reason: `${rest.length} mã khác nhau trên trang, không đoán` };
|
|
2216
|
+
if (NEW_STYLE_PROFILE_ID.test(String(profileId || ''))) {
|
|
2217
|
+
return { id: '', checked: false, reason: sw.length ? 'chỉ thấy mã của mục chuyển tài khoản' : 'trang không có mã nào' };
|
|
2182
2218
|
}
|
|
2183
|
-
return { id: '', checked:
|
|
2219
|
+
return { id: '', checked: true, reason: 'page kiểu cổ điển' };
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
async function readDelegatePageId(page, profileId = '') {
|
|
2223
|
+
const expr = `(${scanDelegatesInHtml.toString()})(document.documentElement.innerHTML, ${JSON.stringify(String(profileId || ''))})`;
|
|
2224
|
+
const scan = await page.evaluate(expr).catch(() => null);
|
|
2225
|
+
return pickDelegate(scan, profileId);
|
|
2184
2226
|
}
|
|
2185
2227
|
|
|
2186
2228
|
async function hasVisibleReelComposer(page) {
|
|
@@ -4772,6 +4814,8 @@ module.exports.__testables = {
|
|
|
4772
4814
|
parsePageRef,
|
|
4773
4815
|
readPageIdFromDom,
|
|
4774
4816
|
readDelegatePageId,
|
|
4817
|
+
scanDelegatesInHtml,
|
|
4818
|
+
pickDelegate,
|
|
4775
4819
|
discoverReelsTabUrls,
|
|
4776
4820
|
openReelsTabWithTiles,
|
|
4777
4821
|
networkIdVerdict,
|