/** * InsightReply Content Script * 负责解析 X (Twitter) 页面 DOM,提取推文内容并注入交互按钮 */ console.log('InsightReply Content Script Loaded'); // 1. 定义推文数据结构 interface TweetData { id: string; author: string; content: string; stats: { replies: string; retweets: string; likes: string; }; } let relevanceKeywords: string[] = []; chrome.storage.local.get(['relevance_keywords'], (res) => { if (res.relevance_keywords) { relevanceKeywords = String(res.relevance_keywords).split(',').map((k: string) => k.trim().toLowerCase()).filter(Boolean); } }); chrome.storage.onChanged.addListener((changes, namespace) => { if (namespace === 'local' && changes.relevance_keywords) { const val = String(changes.relevance_keywords.newValue || ''); relevanceKeywords = val.split(',').map((k: string) => k.trim().toLowerCase()).filter(Boolean); } }); // 2. 提取推文内容的逻辑 const parseStat = (statString: string): number => { if (!statString) return 0; const match = statString.match(/([\d,\.]+)\s*([KkMmB])?/i); if (!match) return 0; const baseStr = match[1] || '0'; let num = parseFloat(baseStr.replace(/,/g, '')); const multiplier = match[2] ? match[2].toUpperCase() : ''; if (multiplier === 'K') num *= 1000; if (multiplier === 'M') num *= 1000000; if (multiplier === 'B') num *= 1000000000; return num; }; const extractTweetData = (tweetElement: HTMLElement): TweetData | null => { try { const textElement = tweetElement.querySelector('[data-testid="tweetText"]'); const authorElement = tweetElement.querySelector('[data-testid="User-Name"]'); const linkElement = tweetElement.querySelector('time')?.parentElement as HTMLAnchorElement; if (!textElement || !authorElement || !linkElement) { console.debug('[InsightReply] Missing elements for tweet extraction:', { hasText: !!textElement, hasAuthor: !!authorElement, hasLink: !!linkElement }); return null; } const getStat = (testid: string) => { const el = tweetElement.querySelector(`[data-testid="${testid}"]`); return el?.getAttribute('aria-label') || '0'; }; const tweetId = linkElement.href.split('/').pop() || ''; return { id: tweetId, author: authorElement.textContent || 'Unknown', content: textElement.textContent || '', stats: { replies: getStat('reply'), retweets: getStat('retweet'), likes: getStat('like'), } }; } catch (e) { console.error('Failed to extract tweet data:', e); return null; } }; const injectBadges = (tweetElement: HTMLElement, data: TweetData) => { if (tweetElement.dataset.insightBadged === 'true') return; tweetElement.dataset.insightBadged = 'true'; const likes = parseStat(data.stats.likes); const retweets = parseStat(data.stats.retweets); const replies = parseStat(data.stats.replies); const heatScore = likes * 1 + retweets * 2 + replies * 3; let badgeText = ''; let badgeStyle = ''; if (heatScore > 50000) { badgeText = '🔥 Trending'; badgeStyle = 'color: #f97316; background: rgba(249, 115, 22, 0.1); border: 1px solid rgba(249, 115, 22, 0.2);'; } else if (heatScore > 5000) { badgeText = '⚡ Rising'; badgeStyle = 'color: #3b82f6; background: rgba(59, 130, 246, 0.1); border: 1px solid rgba(59, 130, 246, 0.2);'; } let isRelevant = false; if (relevanceKeywords.length > 0 && data.content) { const contentLower = data.content.toLowerCase(); isRelevant = relevanceKeywords.some(kw => contentLower.includes(kw)); } if (!badgeText && !isRelevant) return; const authorSection = tweetElement.querySelector('[data-testid="User-Name"]'); if (!authorSection) return; const container = document.createElement('div'); container.className = 'insight-badges-container'; container.style.cssText = `display: inline-flex; align-items: center; gap: 4px; margin-left: 8px;`; if (badgeText) { const heatBadge = document.createElement('span'); heatBadge.style.cssText = `padding: 2px 6px; border-radius: 4px; font-size: 10px; font-weight: bold; ${badgeStyle}`; heatBadge.innerText = badgeText; container.appendChild(heatBadge); } if (isRelevant) { const relBadge = document.createElement('span'); relBadge.style.cssText = `padding: 2px 6px; border-radius: 4px; font-size: 10px; font-weight: bold; color: #10b981; background: rgba(16, 185, 129, 0.1); border: 1px solid rgba(16, 185, 129, 0.2);`; relBadge.innerText = '🎯 Relevant'; container.appendChild(relBadge); } const firstLine = authorSection.firstElementChild; if (firstLine) { firstLine.appendChild(container); } else { authorSection.appendChild(container); } }; // 3. 注入“Insight”按钮 const injectInsightButton = (tweetElement: HTMLElement) => { const actionBar = tweetElement.querySelector('[role="group"]'); if (!actionBar || actionBar.querySelector('.insight-reply-btn')) return; const btnContainer = document.createElement('div'); btnContainer.className = 'insight-reply-btn'; btnContainer.style.display = 'flex'; btnContainer.style.alignItems = 'center'; btnContainer.style.marginLeft = '12px'; btnContainer.style.cursor = 'pointer'; const innerDiv = document.createElement('div'); innerDiv.style.cssText = 'padding: 4px; border-radius: 9999px; transition: background 0.2s;'; // Use event listeners instead of inline handlers to comply with Content Security Policy innerDiv.addEventListener('mouseover', () => { innerDiv.style.background = 'rgba(139, 92, 246, 0.1)'; }); innerDiv.addEventListener('mouseout', () => { innerDiv.style.background = 'transparent'; }); innerDiv.innerHTML = ` `; btnContainer.appendChild(innerDiv); btnContainer.onclick = (e) => { e.stopPropagation(); e.preventDefault(); console.log('[InsightReply] Insight button clicked'); const data = extractTweetData(tweetElement); if (data) { console.log('[InsightReply] Extracted Tweet Data:', data); chrome.runtime.sendMessage({ type: 'SHOW_INSIGHT', payload: data }, (response) => { console.log('[InsightReply] Background script responsed SHOW_INSIGHT with:', response); if (chrome.runtime.lastError) { console.error('[InsightReply] Error sending SHOW_INSIGHT message:', chrome.runtime.lastError); } }); } else { console.warn('[InsightReply] Failed to extract tweet data on click'); } }; actionBar.appendChild(btnContainer); }; // 4. 定时或监听 DOM 变化进行扫描 const scanTweets = () => { const tweets = document.querySelectorAll('article[data-testid="tweet"]'); tweets.forEach((tweet) => { const el = tweet as HTMLElement; injectInsightButton(el); const data = extractTweetData(el); if (data) injectBadges(el, data); }); }; // 使用 MutationObserver 监听动态加载 const observer = new MutationObserver(() => { scanTweets(); }); observer.observe(document.body, { childList: true, subtree: true }); // 初始扫描 scanTweets();