channel-worker 1.0.18 → 1.0.20
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 +231 -0
- package/package.json +1 -1
package/lib/command-poller.js
CHANGED
|
@@ -49,6 +49,12 @@ class CommandPoller {
|
|
|
49
49
|
case 'save_file':
|
|
50
50
|
await this.handleSaveFile(command);
|
|
51
51
|
break;
|
|
52
|
+
case 'set_thumbnail':
|
|
53
|
+
await this.handleSetThumbnail(command);
|
|
54
|
+
break;
|
|
55
|
+
case 'set_tags':
|
|
56
|
+
await this.handleSetTags(command);
|
|
57
|
+
break;
|
|
52
58
|
default:
|
|
53
59
|
// Other commands (scan_facebook_pages, etc.) handled by extension
|
|
54
60
|
console.log(`[commands] Skipping ${command.type} — handled by extension`);
|
|
@@ -212,6 +218,231 @@ class CommandPoller {
|
|
|
212
218
|
}
|
|
213
219
|
}
|
|
214
220
|
|
|
221
|
+
async handleSetThumbnail(command) {
|
|
222
|
+
const { thumbnail_url, profile_id, file_path } = command.payload || {};
|
|
223
|
+
console.log(`[commands] Setting thumbnail for profile: ${profile_id}`);
|
|
224
|
+
try {
|
|
225
|
+
const fs = require('fs');
|
|
226
|
+
const path = require('path');
|
|
227
|
+
const WebSocket = require('ws');
|
|
228
|
+
|
|
229
|
+
// 1. Download thumbnail to disk
|
|
230
|
+
const dir = path.dirname(file_path);
|
|
231
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
232
|
+
const res = await fetch(thumbnail_url);
|
|
233
|
+
if (!res.ok) throw new Error(`Download failed: ${res.status}`);
|
|
234
|
+
const buffer = Buffer.from(await res.arrayBuffer());
|
|
235
|
+
fs.writeFileSync(file_path, buffer);
|
|
236
|
+
console.log(`[commands] Thumbnail saved: ${file_path} (${buffer.length} bytes)`);
|
|
237
|
+
|
|
238
|
+
// 2. Find running browser for this profile
|
|
239
|
+
if (!this.nst) {
|
|
240
|
+
const apiKey = await this.api.getSetting('nst_api_key');
|
|
241
|
+
if (apiKey) this.nst = new NstManager(apiKey);
|
|
242
|
+
}
|
|
243
|
+
const browsersRes = await fetch('http://localhost:8848/api/v2/browsers', {
|
|
244
|
+
headers: { 'x-api-key': this.nst?.apiKey || '' },
|
|
245
|
+
});
|
|
246
|
+
const browsersData = await browsersRes.json();
|
|
247
|
+
const browsers = browsersData?.data || [];
|
|
248
|
+
const browser = browsers.find(b => b.name === profile_id) || browsers[0];
|
|
249
|
+
if (!browser) throw new Error('No running browser found');
|
|
250
|
+
|
|
251
|
+
const debugPort = browser.remoteDebuggingPort;
|
|
252
|
+
console.log(`[commands] Browser debug port: ${debugPort}`);
|
|
253
|
+
|
|
254
|
+
// 3. Get WebSocket URL for YouTube Studio tab
|
|
255
|
+
const pagesRes = await fetch(`http://localhost:${debugPort}/json/list`);
|
|
256
|
+
const pages = await pagesRes.json();
|
|
257
|
+
const studioPage = pages.find(p => p.url?.includes('studio.youtube.com') && p.type === 'page');
|
|
258
|
+
if (!studioPage) throw new Error('YouTube Studio tab not found');
|
|
259
|
+
|
|
260
|
+
const wsUrl = studioPage.webSocketDebuggerUrl;
|
|
261
|
+
console.log(`[commands] Connecting CDP: ${wsUrl}`);
|
|
262
|
+
|
|
263
|
+
// 4. Use CDP to set thumbnail file
|
|
264
|
+
const result = await new Promise((resolve, reject) => {
|
|
265
|
+
const ws = new WebSocket(wsUrl);
|
|
266
|
+
let msgId = 1;
|
|
267
|
+
|
|
268
|
+
function send(method, params = {}) {
|
|
269
|
+
const id = msgId++;
|
|
270
|
+
return new Promise((res, rej) => {
|
|
271
|
+
const handler = (data) => {
|
|
272
|
+
const msg = JSON.parse(data);
|
|
273
|
+
if (msg.id === id) {
|
|
274
|
+
ws.removeListener('message', handler);
|
|
275
|
+
if (msg.error) rej(new Error(msg.error.message));
|
|
276
|
+
else res(msg.result);
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
ws.on('message', handler);
|
|
280
|
+
ws.send(JSON.stringify({ id, method, params }));
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
ws.on('open', async () => {
|
|
285
|
+
try {
|
|
286
|
+
await send('DOM.enable');
|
|
287
|
+
const doc = await send('DOM.getDocument');
|
|
288
|
+
const node = await send('DOM.querySelector', {
|
|
289
|
+
nodeId: doc.root.nodeId,
|
|
290
|
+
selector: '#file-loader',
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
if (!node?.nodeId) {
|
|
294
|
+
ws.close();
|
|
295
|
+
resolve({ success: false, error: '#file-loader not found' });
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
await send('DOM.setFileInputFiles', {
|
|
300
|
+
nodeId: node.nodeId,
|
|
301
|
+
files: [file_path],
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// Trigger change event
|
|
305
|
+
await send('Runtime.evaluate', {
|
|
306
|
+
expression: `
|
|
307
|
+
const inp = document.querySelector('#file-loader');
|
|
308
|
+
if (inp) {
|
|
309
|
+
inp.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
|
|
310
|
+
inp.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
|
|
311
|
+
}
|
|
312
|
+
'triggered'
|
|
313
|
+
`,
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
ws.close();
|
|
317
|
+
resolve({ success: true, file_path });
|
|
318
|
+
} catch (e) {
|
|
319
|
+
ws.close();
|
|
320
|
+
resolve({ success: false, error: e.message });
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
ws.on('error', (e) => reject(e));
|
|
325
|
+
setTimeout(() => { ws.close(); reject(new Error('CDP timeout')); }, 15000);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
console.log(`[commands] Thumbnail CDP result:`, result);
|
|
329
|
+
await this.api.updateCommand(command._id, {
|
|
330
|
+
status: result.success ? 'done' : 'failed',
|
|
331
|
+
result,
|
|
332
|
+
error: result.error || null,
|
|
333
|
+
});
|
|
334
|
+
} catch (err) {
|
|
335
|
+
console.error(`[commands] Set thumbnail failed: ${err.message}`);
|
|
336
|
+
await this.api.updateCommand(command._id, { status: 'failed', error: err.message });
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async handleSetTags(command) {
|
|
341
|
+
const { tags, profile_id } = command.payload || {};
|
|
342
|
+
console.log(`[commands] Setting tags for profile: ${profile_id}`, tags);
|
|
343
|
+
try {
|
|
344
|
+
const WebSocket = require('ws');
|
|
345
|
+
|
|
346
|
+
// Find browser
|
|
347
|
+
if (!this.nst) {
|
|
348
|
+
const apiKey = await this.api.getSetting('nst_api_key');
|
|
349
|
+
if (apiKey) this.nst = new NstManager(apiKey);
|
|
350
|
+
}
|
|
351
|
+
const browsersRes = await fetch('http://localhost:8848/api/v2/browsers', {
|
|
352
|
+
headers: { 'x-api-key': this.nst?.apiKey || '' },
|
|
353
|
+
});
|
|
354
|
+
const browsersData = await browsersRes.json();
|
|
355
|
+
const browser = (browsersData?.data || []).find(b => b.name === profile_id) || browsersData?.data?.[0];
|
|
356
|
+
if (!browser) throw new Error('No running browser');
|
|
357
|
+
|
|
358
|
+
const pagesRes = await fetch(`http://localhost:${browser.remoteDebuggingPort}/json/list`);
|
|
359
|
+
const pages = await pagesRes.json();
|
|
360
|
+
const studioPage = pages.find(p => p.url?.includes('studio.youtube.com') && p.type === 'page');
|
|
361
|
+
if (!studioPage) throw new Error('YouTube Studio tab not found');
|
|
362
|
+
|
|
363
|
+
const tagStr = tags.join(', ');
|
|
364
|
+
|
|
365
|
+
const result = await new Promise((resolve, reject) => {
|
|
366
|
+
const ws = new WebSocket(studioPage.webSocketDebuggerUrl);
|
|
367
|
+
let msgId = 1;
|
|
368
|
+
function send(method, params = {}) {
|
|
369
|
+
const id = msgId++;
|
|
370
|
+
return new Promise((res, rej) => {
|
|
371
|
+
const handler = (data) => {
|
|
372
|
+
const msg = JSON.parse(data);
|
|
373
|
+
if (msg.id === id) { ws.removeListener('message', handler); msg.error ? rej(new Error(msg.error.message)) : res(msg.result); }
|
|
374
|
+
};
|
|
375
|
+
ws.on('message', handler);
|
|
376
|
+
ws.send(JSON.stringify({ id, method, params }));
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
ws.on('open', async () => {
|
|
381
|
+
try {
|
|
382
|
+
// Click "Show more" button
|
|
383
|
+
await send('Runtime.evaluate', {
|
|
384
|
+
expression: `
|
|
385
|
+
(function() {
|
|
386
|
+
const texts = ['show more', 'hiện thêm', 'hiển thị thêm'];
|
|
387
|
+
const allEls = document.querySelectorAll('*');
|
|
388
|
+
for (const el of allEls) {
|
|
389
|
+
if (el.children.length > 3) continue;
|
|
390
|
+
const t = el.textContent?.trim()?.toLowerCase() || '';
|
|
391
|
+
if (texts.some(x => t === x)) { el.click(); return 'clicked: ' + el.textContent?.trim(); }
|
|
392
|
+
}
|
|
393
|
+
return 'not_found';
|
|
394
|
+
})()
|
|
395
|
+
`,
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
// Wait for section to expand
|
|
399
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
400
|
+
|
|
401
|
+
// Find and fill tags input — placeholder "Add tag"
|
|
402
|
+
const fillResult = await send('Runtime.evaluate', {
|
|
403
|
+
expression: `
|
|
404
|
+
(function() {
|
|
405
|
+
// Find input with placeholder "Add tag" or aria-label containing "tag"
|
|
406
|
+
const inputs = document.querySelectorAll('input');
|
|
407
|
+
for (const inp of inputs) {
|
|
408
|
+
const ph = (inp.placeholder || '').toLowerCase();
|
|
409
|
+
const aria = (inp.getAttribute('aria-label') || '').toLowerCase();
|
|
410
|
+
if (ph.includes('add tag') || ph.includes('thêm thẻ') || aria.includes('tag') || aria.includes('thẻ')) {
|
|
411
|
+
inp.focus();
|
|
412
|
+
inp.value = '${tagStr.replace(/'/g, "\\'")}';
|
|
413
|
+
inp.dispatchEvent(new Event('input', { bubbles: true }));
|
|
414
|
+
inp.dispatchEvent(new Event('change', { bubbles: true }));
|
|
415
|
+
// Press Enter to confirm
|
|
416
|
+
inp.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', keyCode: 13, bubbles: true }));
|
|
417
|
+
inp.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter', code: 'Enter', keyCode: 13, bubbles: true }));
|
|
418
|
+
return 'tags_filled';
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return 'no_tag_input';
|
|
422
|
+
})()
|
|
423
|
+
`,
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
ws.close();
|
|
427
|
+
resolve({ success: true, result: fillResult?.result?.value });
|
|
428
|
+
} catch (e) { ws.close(); resolve({ success: false, error: e.message }); }
|
|
429
|
+
});
|
|
430
|
+
ws.on('error', (e) => reject(e));
|
|
431
|
+
setTimeout(() => { ws.close(); reject(new Error('CDP timeout')); }, 15000);
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
console.log(`[commands] Tags CDP result:`, result);
|
|
435
|
+
await this.api.updateCommand(command._id, {
|
|
436
|
+
status: result.success ? 'done' : 'failed',
|
|
437
|
+
result,
|
|
438
|
+
error: result.error || null,
|
|
439
|
+
});
|
|
440
|
+
} catch (err) {
|
|
441
|
+
console.error(`[commands] Set tags failed: ${err.message}`);
|
|
442
|
+
await this.api.updateCommand(command._id, { status: 'failed', error: err.message });
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
215
446
|
async handleCloseProfile(command) {
|
|
216
447
|
const { profile_id } = command.payload || {};
|
|
217
448
|
console.log(`[commands] Closing profile: ${profile_id}`);
|