Update
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
// ===== 1. TẠO CONTEXT MENU =====
|
||||
chrome.runtime.onInstalled.addListener(() => {
|
||||
chrome.contextMenus.create({
|
||||
id: 'writeComment',
|
||||
title: '✍️ Viết comment',
|
||||
contexts: ['all'],
|
||||
documentUrlPatterns: ['https://x.com/*', 'https://twitter.com/*']
|
||||
});
|
||||
});
|
||||
|
||||
// ===== 2. KHI CLICK VÀO MENU =====
|
||||
chrome.contextMenus.onClicked.addListener((info, tab) => {
|
||||
if (info.menuItemId === 'writeComment') {
|
||||
chrome.tabs.sendMessage(tab.id, { action: 'OPEN_FORM' }).catch(() => {
|
||||
// Nếu content script chưa sẵn sàng (hiếm) thì bỏ qua
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ===== 3. NHẬN YÊU CẦU TỪ CONTENT SCRIPT VÀ GỌI API =====
|
||||
chrome.runtime.onMessage.addListener((request, sender) => {
|
||||
if (request.action === 'GENERATE_COMMENT') {
|
||||
callYourAPI(request.data, sender.tab.id);
|
||||
}
|
||||
});
|
||||
|
||||
async function callYourAPI({
|
||||
text,
|
||||
tone,
|
||||
angle,
|
||||
language
|
||||
}, tabId) {
|
||||
try {
|
||||
// ⚠️ THAY BẰNG ENDPOINT CỦA BẠN
|
||||
const API_URL = 'https://gamerdota0042-himalayas.nord:3000';
|
||||
const commentApi='/content-writer/comment'
|
||||
const API_KEY = 'YOUR_API_KEY_HERE'; // Nên chuyển sang chrome.storage nếu publish
|
||||
|
||||
const res = await fetch(`${API_URL}${commentApi}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${API_KEY}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
originalPost: text,
|
||||
tone: tone,
|
||||
angle: angle,
|
||||
language,
|
||||
})
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
|
||||
const data = await res.json();
|
||||
// Giả định API trả về: { comment: "..." }
|
||||
const comment = data.comment || data.text || JSON.stringify(data);
|
||||
|
||||
chrome.tabs.sendMessage(tabId, { action: 'SHOW_RESULT', comment });
|
||||
} catch (err) {
|
||||
chrome.tabs.sendMessage(tabId, { action: 'SHOW_ERROR', error: err.message });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
(() => {
|
||||
'use strict';
|
||||
|
||||
let selectedTweetText = '';
|
||||
|
||||
// ===== A. LẮNG NGHE CHUỘT PHẢI ĐỂ LẤY TWEET TEXT =====
|
||||
document.addEventListener('contextmenu', (e) => {
|
||||
const article = e.target.closest('article[data-testid="tweet"]');
|
||||
if (!article) {
|
||||
selectedTweetText = '';
|
||||
return;
|
||||
}
|
||||
const textContainer = article.querySelector('[data-testid="tweetText"]');
|
||||
selectedTweetText = textContainer ? textContainer.innerText.trim() : '';
|
||||
});
|
||||
|
||||
// ===== B. LẮNG NGHE MESSAGE TỪ BACKGROUND =====
|
||||
chrome.runtime.onMessage.addListener((req, sender, sendResponse) => {
|
||||
if (req.action === 'OPEN_FORM') {
|
||||
if (!selectedTweetText) {
|
||||
alert('Không tìm thấy nội dung tweet. Hãy chuột phải vào vùng văn bản của tweet.');
|
||||
return;
|
||||
}
|
||||
openModal(selectedTweetText);
|
||||
} else if (req.action === 'SHOW_RESULT') {
|
||||
showResult(req.comment);
|
||||
} else if (req.action === 'SHOW_ERROR') {
|
||||
showError(req.error);
|
||||
}
|
||||
});
|
||||
|
||||
// ===== C. MỞ MODAL (DÙNG SHADOW DOM ĐỂ TRÁNH CSS XUNG ĐỘT) =====
|
||||
function openModal(tweetText) {
|
||||
removeModal(); // Xóa cũ nếu có
|
||||
|
||||
const host = document.createElement('div');
|
||||
host.id = 'x-comment-ai-host';
|
||||
Object.assign(host.style, {
|
||||
position: 'fixed',
|
||||
top: '0',
|
||||
left: '0',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
zIndex: '999999',
|
||||
pointerEvents: 'none'
|
||||
});
|
||||
document.body.appendChild(host);
|
||||
|
||||
const shadow = host.attachShadow({ mode: 'open' });
|
||||
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
.overlay {
|
||||
position: fixed; inset: 0;
|
||||
background: rgba(0,0,0,0.55);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.modal {
|
||||
background: #fff; color: #0f1419;
|
||||
width: 420px; max-width: 92vw;
|
||||
border-radius: 16px; padding: 24px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
box-shadow: 0 25px 50px rgba(0,0,0,0.25);
|
||||
display: flex; flex-direction: column; gap: 14px;
|
||||
animation: popIn 0.2s ease-out;
|
||||
}
|
||||
@keyframes popIn { from {opacity:0; transform:scale(0.96)} to {opacity:1; transform:scale(1)} }
|
||||
h3 { margin: 0; font-size: 20px; }
|
||||
.tweet-box {
|
||||
background: #f7f9f9; border: 1px solid #cfd9de; border-radius: 10px;
|
||||
padding: 12px; font-size: 14px; line-height: 1.4;
|
||||
max-height: 120px; overflow-y: auto; color: #333;
|
||||
}
|
||||
label { font-size: 13px; font-weight: 700; color: #536471; }
|
||||
select, button {
|
||||
width: 100%; padding: 10px 12px; border-radius: 8px;
|
||||
border: 1px solid #cfd9de; font-size: 14px; outline: none;
|
||||
}
|
||||
button.primary {
|
||||
background: #1d9bf0; color: #fff; border: none;
|
||||
font-weight: 700; cursor: pointer; margin-top: 4px;
|
||||
}
|
||||
button.primary:hover { background: #1a8cd8; }
|
||||
button.primary:disabled { background: #8ecdf7; cursor: default; }
|
||||
button.ghost {
|
||||
background: transparent; color: #536471; border: 1px solid #cfd9de; margin-top: 6px;
|
||||
}
|
||||
.status {
|
||||
display: none; padding: 12px; border-radius: 10px; font-size: 14px; line-height: 1.5; white-space: pre-wrap;
|
||||
}
|
||||
.status.ok { background: #e8f6fe; border: 1px solid #b4dffc; color: #0f1419; }
|
||||
.status.err { background: #ffeaea; border: 1px solid #ffc5c5; color: #b00; }
|
||||
.copy-hint { font-size: 12px; color: #536471; margin-top: 6px; text-align: center; }
|
||||
`;
|
||||
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'overlay';
|
||||
overlay.addEventListener('click', (e) => {
|
||||
if (e.target === overlay) removeModal();
|
||||
});
|
||||
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'modal';
|
||||
modal.innerHTML = `
|
||||
<h3>✍️ Viết Comment AI</h3>
|
||||
<div>
|
||||
<label>Nội dung Tweet</label>
|
||||
<div class="tweet-box" id="ai-tweet"></div>
|
||||
</div>
|
||||
<div>
|
||||
<label>Ngôn ngữ (Language)</label>
|
||||
<select id="ai-language">
|
||||
<option value="en">Anh</option>
|
||||
<option value="ja">Nhật</option>
|
||||
<option value="vi">Việt</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label>Tone (Giọng điệu)</label>
|
||||
<select id="ai-tone">
|
||||
<option value="funny">😂 Hài hước</option>
|
||||
<option value="professional">💼 Chuyên nghiệp</option>
|
||||
<option value="friendly">😊 Thân thiện</option>
|
||||
<option value="sarcastic">🌶️ Châm biếm</option>
|
||||
<option value="neutral">😐 Trung lập</option>
|
||||
<option value="excited">🤩 Hào hứng</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label>Angle (Góc tiếp cận)</label>
|
||||
<select id="ai-angle">
|
||||
<option value="agree">👍 Đồng tình</option>
|
||||
<option value="disagree">👎 Phản biện</option>
|
||||
<option value="question">❓ Đặt câu hỏi</option>
|
||||
<option value="add_info">➕ Bổ sung thông tin</option>
|
||||
<option value="joke">🃏 Câu đùa / Meme</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="primary" id="ai-run">🚀 Tạo Comment</button>
|
||||
<div class="status" id="ai-status"></div>
|
||||
<div class="copy-hint" id="ai-hint" style="display:none;">📋 Copy kết quả và tự dán vào ô comment nhé!</div>
|
||||
<button class="ghost" id="ai-close">Đóng</button>
|
||||
`;
|
||||
|
||||
overlay.appendChild(modal);
|
||||
shadow.appendChild(style);
|
||||
shadow.appendChild(overlay);
|
||||
|
||||
// Điền text tweet
|
||||
shadow.getElementById('ai-tweet').textContent = tweetText;
|
||||
|
||||
// Events
|
||||
shadow.getElementById('ai-close').addEventListener('click', removeModal);
|
||||
|
||||
shadow.getElementById('ai-run').addEventListener('click', () => {
|
||||
const language = shadow.getElementById('ai-language').value;
|
||||
const tone = shadow.getElementById('ai-tone').value;
|
||||
const angle = shadow.getElementById('ai-angle').value;
|
||||
const status = shadow.getElementById('ai-status');
|
||||
const btn = shadow.getElementById('ai-run');
|
||||
|
||||
btn.disabled = true;
|
||||
status.style.display = 'block';
|
||||
status.className = 'status ok';
|
||||
status.innerHTML = '<em>⏳ Đang tạo comment...</em>';
|
||||
|
||||
chrome.runtime.sendMessage({
|
||||
action: 'GENERATE_COMMENT',
|
||||
data: { text: tweetText, tone, angle, language }
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ===== D. HIỂN THỊ KẾT QUẢ / LỖI =====
|
||||
function showResult(comment) {
|
||||
const host = document.getElementById('x-comment-ai-host');
|
||||
if (!host) return;
|
||||
const s = host.shadowRoot;
|
||||
const status = s.getElementById('ai-status');
|
||||
const btn = s.getElementById('ai-run');
|
||||
const hint = s.getElementById('ai-hint');
|
||||
|
||||
status.style.display = 'block';
|
||||
status.className = 'status ok';
|
||||
status.textContent = comment;
|
||||
|
||||
hint.style.display = 'block';
|
||||
if (btn) btn.disabled = false;
|
||||
}
|
||||
|
||||
function showError(msg) {
|
||||
const host = document.getElementById('x-comment-ai-host');
|
||||
if (!host) return;
|
||||
const s = host.shadowRoot;
|
||||
const status = s.getElementById('ai-status');
|
||||
const btn = s.getElementById('ai-run');
|
||||
|
||||
status.style.display = 'block';
|
||||
status.className = 'status err';
|
||||
status.textContent = 'Lỗi: ' + msg;
|
||||
|
||||
if (btn) btn.disabled = false;
|
||||
}
|
||||
|
||||
function removeModal() {
|
||||
const el = document.getElementById('x-comment-ai-host');
|
||||
if (el) el.remove();
|
||||
}
|
||||
})();
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "X Comment AI",
|
||||
"version": "1.0.0",
|
||||
"description": "Tạo comment cho X.com bằng AI của bạn",
|
||||
"permissions": [
|
||||
"contextMenus",
|
||||
"storage"
|
||||
],
|
||||
"host_permissions": [
|
||||
"https://x.com/*",
|
||||
"https://twitter.com/*",
|
||||
"https://api.yoursite.com/*"
|
||||
],
|
||||
"background": {
|
||||
"service_worker": "background.js"
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": [
|
||||
"https://x.com/*",
|
||||
"https://twitter.com/*"
|
||||
],
|
||||
"js": [
|
||||
"content.js"
|
||||
],
|
||||
"run_at": "document_idle"
|
||||
}
|
||||
],
|
||||
"icons": {
|
||||
"16": "icons/icon.webp",
|
||||
"48": "icons/icon.webp",
|
||||
"128": "icons/icon.webp"
|
||||
}
|
||||
}
|
||||
@@ -103,7 +103,10 @@ export class SqsPosterWorker {
|
||||
// await postToX(data.content);
|
||||
|
||||
// giả lập delay
|
||||
await this.sleep(rand(7, 10) * 1000); //nghỉ 10s
|
||||
const sleepS = rand(12, 21) * 1000;
|
||||
this.logger.log(`Nghỉ ${sleepS} second`);
|
||||
|
||||
await this.sleep(sleepS);
|
||||
}
|
||||
|
||||
private sleep(ms: number) {
|
||||
|
||||
@@ -358,7 +358,7 @@ export class XBrowserService implements OnModuleInit, OnModuleDestroy {
|
||||
// await page.mouse.click(btnBox?.x + btnBox.width / 2, btnBox.y + btnBox.height / 2);
|
||||
await page.keyboard.press('Control+Enter');
|
||||
this.logger.debug('Nhấn Control+Enter done ...');
|
||||
await page.waitForTimeout(5000);
|
||||
await page.waitForTimeout(10000);
|
||||
|
||||
// Chờ request CreateTweet hoàn tất
|
||||
// await page.waitForResponse(
|
||||
@@ -484,7 +484,7 @@ export class XBrowserService implements OnModuleInit, OnModuleDestroy {
|
||||
this.logger.debug('❌ Nut click khong duoc, thử dùng bàn phím Control+Enter');
|
||||
await page.keyboard.press('Control+Enter');
|
||||
});
|
||||
await page.waitForTimeout(rand(4000, 6000));
|
||||
await page.waitForTimeout(rand(6000, 10000));
|
||||
|
||||
this.logger.debug('✅ Quoted thành công');
|
||||
} else {
|
||||
@@ -581,8 +581,8 @@ export class XBrowserService implements OnModuleInit, OnModuleDestroy {
|
||||
}
|
||||
|
||||
await btn.click();
|
||||
this.logger.debug(`nhấn nút gửi ...`)
|
||||
await page.waitForTimeout(3000);
|
||||
this.logger.debug(`Đã nhấn nút gửi ...`)
|
||||
await page.waitForTimeout(10000);
|
||||
|
||||
this.logger.debug('✅ Reply OK');
|
||||
return {success: true, error: ''};
|
||||
|
||||
Reference in New Issue
Block a user