Files
InsightReply/extension/src/content/index.ts
zs 50bd2925c1
All checks were successful
Extension Build & Release / build (push) Successful in 48s
feat: CSP Violation ( 感叹号图标被拦截 )
2026-03-03 00:19:24 +08:00

216 lines
7.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 = `
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor" style="color: #8B5CF6;">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"></path>
</svg>
`;
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();