channel-worker 2.5.125 → 2.5.127

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "channel-worker",
3
- "version": "2.5.125",
3
+ "version": "2.5.127",
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": {
@@ -1,20 +1,27 @@
1
- // Thả link affiliate xuống BÌNH LUẬN của một bài Facebook đã đăng, rồi ghim.
1
+ // Thả link affiliate xuống BÌNH LUẬN của một bài Facebook đã đăng.
2
2
  //
3
3
  // Vì sao là bình luận chứ không phải caption: bài đang lên view thì không ai đi
4
4
  // sửa caption của nó — sửa caption là bài quay lại hàng đợi xét duyệt của
5
- // Facebook giữa lúc nó đang được phân phối. Bình luận thì ghim được, sửa được,
6
- // xoá được, không đụng vào bài.
5
+ // Facebook giữa lúc nó đang được phân phối. Bình luận thì sửa được, xoá được,
6
+ // không đụng vào bài.
7
7
  //
8
8
  // Chạy trên chính profile NST của kênh (máy đăng bài) chứ không phải máy
9
- // Shopee: bình luận phải mang danh Page thì mới ghim được, và một link lạ dưới
10
- // bài của mình do tài khoản khác thả thì người xem đọc ra là spam.
9
+ // Shopee: bình luận phải mang danh Page — Facebook mới gắn nhãn "Author" và ưu
10
+ // tiên hiển thị nó, còn một link lạ dưới bài của mình do tài khoản khác thả thì
11
+ // người xem đọc ra là spam.
11
12
  //
12
- // Payload: { post_url, text, link, pin?, idea_id?, channel_id? }
13
+ // Payload: { post_url, text, link, idea_id?, channel_id? }
13
14
  // post_url: link bài đã XÁC MINH (API chặn sẵn ô rỗng — services/AffComment)
14
15
  // text: nguyên câu bình luận sẽ gõ, đã gồm link
15
16
  // link: riêng chuỗi link, dùng để ĐỌC LẠI xác minh — xem `verified`
16
17
  //
17
- // Trả về { verified, comment_url, commented_as, pinned, post_url }.
18
+ // Trả về { verified, comment_url, commented_as, post_url }.
19
+ //
20
+ // KHÔNG có bước ghim. Facebook không cho ghim bình luận dưới reel ở giao diện
21
+ // web — chủ tịch mở tận nơi kiểm 21/09, menu ⋯ của bình luận không có mục đó,
22
+ // kể cả khi bình luận đã mang nhãn "Author" (tức của chính chủ bài). Bản cũ
23
+ // vẫn đi dò nút ấy mỗi lượt rồi ghi một dòng log nghe như mình làm hỏng việc.
24
+ // Bỏ luôn: bình luận của tác giả bài vốn được Facebook ưu tiên hiển thị.
18
25
  //
19
26
  // ⚠️ LUẬT LÕI CỦA SCRIPT NÀY: `verified` chỉ được true khi đã đọc lại thấy một
20
27
  // bình luận THẬT trên trang mang đúng chuỗi link. Gõ xong + bấm Enter KHÔNG
@@ -68,8 +75,15 @@ const BOX_SELECTORS = [
68
75
  const OPEN_COMMENTS_SELECTORS = [
69
76
  "[aria-label='Bình luận'], [aria-label='Comment']",
70
77
  "[aria-label*='Để lại bình luận'], [aria-label*='Leave a comment']",
78
+ // Reel có thanh hành động riêng, nhãn khác hai dạng trên.
79
+ "[aria-label*='Xem bình luận'], [aria-label*='View comments']",
71
80
  ];
72
81
 
82
+ // Khung bình luận của reel mở bằng animation. Máy đang chạy nhiều lượt đăng
83
+ // song song thì vẽ chậm hẳn — chờ rộng tay, vì hỏng cả lượt tốn hơn nhiều so
84
+ // với đứng thêm mấy giây.
85
+ const OPEN_WAIT_MS = 12_000;
86
+
73
87
  function sel(list) { return list.join(', '); }
74
88
 
75
89
  // ─── tìm ô soạn ─────────────────────────────────────────────────────────────
@@ -93,16 +107,48 @@ async function findCommentBox(page) {
93
107
  return null;
94
108
  }
95
109
 
110
+ // Bấm xong PHẢI chờ tới khi ô soạn thật sự dựng, đừng ngủ cứng rồi đi tiếp.
111
+ //
112
+ // Bản đầu bấm nút rồi `pause(1800)` và trả về ngay. Khung bình luận của reel
113
+ // mở bằng animation, nên 1,8s chỉ đủ khi máy rảnh; 21/09 hai lượt trượt sạch
114
+ // đúng lúc máy đang chạy 5 lượt đăng song song, log dừng ở "bấm mở khung
115
+ // bình luận" rồi báo "không tìm thấy ô soạn (đã thử 7 nhãn)" — đọc lên tưởng
116
+ // bài tắt bình luận, thật ra khung chưa kịp vẽ. Các lượt thành công cùng ngày
117
+ // đều là bài có sẵn ô soạn, không qua nhánh này lần nào.
118
+ //
119
+ // Chờ tối đa OPEN_WAIT_MS và thử NÚT KHÁC nếu nút đầu không dựng được khung —
120
+ // trên reel có nhiều thứ mang nhãn "Bình luận" (nút ở thanh hành động, nút
121
+ // dưới bài), bấm trúng cái không mở panel là hỏng cả lượt.
96
122
  async function openCommentsIfNeeded(page, log) {
123
+ let clicked = false;
97
124
  for (const s of OPEN_COMMENTS_SELECTORS) {
98
125
  const loc = page.locator(s).first();
99
126
  if (!(await loc.isVisible().catch(() => false))) continue;
100
127
  log('info', `[fb-cmt] chưa thấy ô soạn — bấm mở khung bình luận ("${s}")`);
101
128
  await humanClick(page, loc).catch(() => {});
102
- await pause(page, 1800);
103
- return true;
129
+ clicked = true;
130
+ const box = await waitForCommentBox(page, OPEN_WAIT_MS);
131
+ if (box) {
132
+ log('info', '[fb-cmt] khung bình luận đã dựng');
133
+ return true;
134
+ }
135
+ log('warn', `[fb-cmt] bấm "${s}" mà khung chưa dựng sau ${Math.round(OPEN_WAIT_MS / 1000)}s — thử nút khác`);
136
+ }
137
+ return clicked;
138
+ }
139
+
140
+ // Dò lại ô soạn theo nhịp cho tới khi thấy hoặc hết giờ. Dùng vòng lặp chứ
141
+ // không `waitForSelector` vì ô soạn phải qua được bộ lọc của findCommentBox
142
+ // (ô soạn BÀI trên tường Page cũng khớp selector — gõ nhầm vào đó là đăng một
143
+ // bài mới toàn link).
144
+ async function waitForCommentBox(page, timeoutMs) {
145
+ const deadline = Date.now() + timeoutMs;
146
+ for (;;) {
147
+ const box = await findCommentBox(page);
148
+ if (box) return box;
149
+ if (Date.now() >= deadline) return null;
150
+ await pause(page, 700);
104
151
  }
105
- return false;
106
152
  }
107
153
 
108
154
  // ─── đọc lại để xác minh ────────────────────────────────────────────────────
@@ -152,16 +198,28 @@ async function findPostedComment(page, link) {
152
198
  if (node.querySelector("[contenteditable='true']") && !node.getAttribute('aria-label')) continue;
153
199
  const label = node.getAttribute('aria-label') || '';
154
200
  const m = /(?:Bình luận của|Comment by)\s+(.+)$/i.exec(label.trim());
155
- // Không có aria-label dạng bình luận = nhiều khả năng đây là khối BÀI
156
- // chứa chuỗi link trong caption, không phải bình luận.
157
- if (!m) continue;
158
- out.found = true;
159
- out.author = m[1].trim().slice(0, 120);
201
+ // Khối bình luận KHÔNG phải lúc nào cũng mang aria-label "Bình luận của
202
+ // <Tên>" — giao diện reel không dùng nhãn đó, nên mọi lượt thả reel đều
203
+ // về `commented_as` rỗng và `comment_url` rỗng (21/09). Bằng chứng chắc
204
+ // chắn khác, không phụ thuộc chữ nghĩa giao diện: khối có link permalink
205
+ // mang `comment_id=`. Khối BÀI không bao giờ có thứ đó, nên vẫn né được
206
+ // đúng cái bẫy mà nhãn kia sinh ra để né — caption bài chứa chuỗi link.
160
207
  const a = node.querySelector("a[href*='comment_id=']");
208
+ if (!m && !a) continue;
209
+ out.found = true;
161
210
  if (a) {
162
211
  const href = a.getAttribute('href') || '';
163
212
  out.comment_url = href.startsWith('http') ? href : `https://www.facebook.com${href}`;
164
213
  }
214
+ if (m) {
215
+ out.author = m[1].trim().slice(0, 120);
216
+ } else {
217
+ // Facebook gắn nhãn "Author"/"Tác giả" cho bình luận của chính chủ bài
218
+ // — với luồng này thì đó CHÍNH LÀ điều cần biết: bình luận mang danh
219
+ // Page chứ không phải một tài khoản lạ đi rải link dưới bài của kênh.
220
+ const badge = /(^|\s)(Author|Tác giả)(\s|$)/i.test(node.innerText || '');
221
+ out.author = badge ? 'Author' : '';
222
+ }
165
223
  return out;
166
224
  }
167
225
  return out;
@@ -186,46 +244,6 @@ async function waitForPostedComment(page, link, log, timeoutMs = VERIFY_TIMEOUT_
186
244
  return { ...meta, found: false, count: n };
187
245
  }
188
246
 
189
- // ─── ghim ───────────────────────────────────────────────────────────────────
190
-
191
- // Ghim là việc CÓ CŨNG TỐT, không có cũng xong: link đã nằm dưới bài rồi. Nên
192
- // mọi lỗi ở đây chỉ ghi log và trả false, KHÔNG ném — hỏng bước ghim mà đánh
193
- // cả lệnh thành thất bại thì lượt chạy lại sẽ thả bình luận thứ hai.
194
- async function pinComment(page, link, log) {
195
- try {
196
- const menuBtn = await page.evaluateHandle((needle) => {
197
- const nodes = [...document.querySelectorAll("[role='article']")];
198
- for (const node of nodes) {
199
- if (!(node.innerText || '').includes(needle)) continue;
200
- if (!/(Bình luận của|Comment by)/i.test(node.getAttribute('aria-label') || '')) continue;
201
- const btn = [...node.querySelectorAll("[role='button'], [aria-haspopup='menu']")]
202
- .find((b) => /hành động|tùy chọn|tuỳ chọn|options|actions/i.test(b.getAttribute('aria-label') || ''));
203
- if (btn) { btn.scrollIntoView({ block: 'center' }); return btn; }
204
- }
205
- return null;
206
- }, link);
207
- const el = menuBtn.asElement();
208
- if (!el) { log('info', '[fb-cmt] không thấy nút ⋯ của bình luận — bỏ qua bước ghim'); return false; }
209
- await el.click({ timeout: 5000 }).catch(() => {});
210
- await pause(page, 1200);
211
-
212
- const item = page.locator("[role='menuitem'], [role='menu'] [role='button']")
213
- .filter({ hasText: /Ghim bình luận|Ghim|Pin comment|Pin/i }).first();
214
- if (!(await item.isVisible().catch(() => false))) {
215
- log('info', '[fb-cmt] menu không có mục Ghim (thường là chưa bình luận bằng danh Page) — bỏ qua');
216
- await page.keyboard.press('Escape').catch(() => {});
217
- return false;
218
- }
219
- await item.click({ timeout: 5000 }).catch(() => {});
220
- await pause(page, 1500);
221
- log('info', '[fb-cmt] đã bấm Ghim bình luận');
222
- return true;
223
- } catch (e) {
224
- log('warn', `[fb-cmt] ghim hỏng (bỏ qua, link vẫn nằm dưới bài): ${e.message.slice(0, 120)}`);
225
- return false;
226
- }
227
- }
228
-
229
247
  // ─── thân ───────────────────────────────────────────────────────────────────
230
248
 
231
249
  async function run({ page, payload = {}, log }) {
@@ -261,13 +279,23 @@ async function run({ page, payload = {}, log }) {
261
279
  if (gone) throw new Error(`Bài không mở được (đã xoá, đổi quyền riêng tư, hoặc link sai): ${postUrl}`);
262
280
 
263
281
  let box = await findCommentBox(page);
282
+ // Chưa thấy ngay thì chờ một nhịp trước khi đi bấm nút: trang vừa mở, khối
283
+ // bình luận có khi còn đang vẽ.
284
+ if (!box) box = await waitForCommentBox(page, 4000);
285
+ let opened = false;
264
286
  if (!box) {
265
- await openCommentsIfNeeded(page, log);
287
+ opened = await openCommentsIfNeeded(page, log);
266
288
  box = await findCommentBox(page);
267
289
  }
268
290
  if (!box) {
269
291
  await assertAccountUsable(page, log);
270
- throw new Error(`Không tìm thấy ô soạn bình luận trên ${postUrl} (đã thử ${BOX_SELECTORS.length} nhãn) — nhiều khả năng bài tắt bình luận hoặc Facebook đổi giao diện`);
292
+ // Nói đúng bệnh. Đã bấm được nút mở khung mà vẫn không có ô soạn thì bài
293
+ // KHÔNG tắt bình luận — nút đó chỉ hiện khi còn bình luận được; bệnh là
294
+ // khung không dựng kịp (máy bận) hoặc Facebook đổi cách dựng panel. Gộp
295
+ // hai chuyện vào một câu là lần sau đọc log đi sai hướng ngay từ đầu.
296
+ throw new Error(opened
297
+ ? `Bấm mở khung bình luận rồi mà không có ô soạn sau ${Math.round(OPEN_WAIT_MS / 1000)}s trên ${postUrl} — máy đang bận vẽ chậm, hoặc Facebook đổi cách dựng khung bình luận của reel`
298
+ : `Không tìm thấy ô soạn bình luận trên ${postUrl} (đã thử ${BOX_SELECTORS.length} nhãn, cũng không thấy nút mở khung) — nhiều khả năng bài tắt bình luận hoặc Facebook đổi giao diện`);
271
299
  }
272
300
 
273
301
  // Đo NỀN trước khi gõ: link có thể đã nằm sẵn trong caption, hoặc bài đã
@@ -308,25 +336,24 @@ async function run({ page, payload = {}, log }) {
308
336
  // 'failed' kèm nguyên văn. Ném lỗi ở đây thì lượt chạy lại sẽ gõ thêm một
309
337
  // bình luận nữa, mà bình luận trước có thể đã nằm đó rồi.
310
338
  log('warn', '[fb-cmt] gõ và gửi xong nhưng KHÔNG đọc lại thấy bình luận — coi như chưa thả, người phải vào kiểm');
311
- return { verified: false, post_url: postUrl, comment_url: '', commented_as: '', pinned: false };
339
+ return { verified: false, post_url: postUrl, comment_url: '', commented_as: '' };
312
340
  }
313
341
 
314
342
  log('info', `[fb-cmt] xác minh được bình luận — danh nghĩa "${found.author}"${found.comment_url ? ` · ${found.comment_url}` : ''}`);
315
343
 
316
- let pinned = false;
317
- if (payload.pin !== false) pinned = await pinComment(page, link, log);
318
-
319
344
  return {
320
345
  verified: true,
321
346
  post_url: postUrl,
322
347
  comment_url: found.comment_url || '',
323
- // Tên hiện trên bình luận. Phải là tên Page — ra tên người thì profile đang
324
- // bình luận bằng tài khoản cá nhân, link nằm dưới bài mà không ghim được và
325
- // người xem đọc ra là spam của người lạ.
348
+ // Tên hiện trên bình luận, hoặc 'Author' khi Facebook gắn nhãn tác giả.
349
+ // Ra tên người lạ nghĩa là profile đang bình luận bằng tài khoản cá nhân —
350
+ // người xem đọc ra là spam dưới bài của kênh.
326
351
  commented_as: found.author || '',
327
- pinned,
328
352
  };
329
353
  }
330
354
 
331
355
  module.exports = { run };
332
- module.exports.__testables = { findPostedComment, countLinkOnPage, waitForPostedComment, findCommentBox, BOX_SELECTORS, sel };
356
+ module.exports.__testables = {
357
+ findPostedComment, countLinkOnPage, waitForPostedComment, findCommentBox, BOX_SELECTORS, sel,
358
+ openCommentsIfNeeded, waitForCommentBox, OPEN_COMMENTS_SELECTORS, OPEN_WAIT_MS,
359
+ };