Initial commit
This commit is contained in:
107
extension/src/content/index.ts
Normal file
107
extension/src/content/index.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* 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;
|
||||
};
|
||||
}
|
||||
|
||||
// 2. 提取推文内容的逻辑
|
||||
const extractTweetData = (tweetElement: HTMLElement): TweetData | null => {
|
||||
try {
|
||||
// 根据 X 的 DOM 结构提取 (可能会随 Twitter 更新而变化)
|
||||
const textElement = tweetElement.querySelector('[data-testid="tweetText"]');
|
||||
const authorElement = tweetElement.querySelector('[data-testid="User-Name"]');
|
||||
const linkElement = tweetElement.querySelector('time')?.parentElement as HTMLAnchorElement;
|
||||
|
||||
// 互动数据提取
|
||||
const getStat = (testid: string) => {
|
||||
const el = tweetElement.querySelector(`[data-testid="${testid}"]`);
|
||||
return el?.getAttribute('aria-label') || '0';
|
||||
};
|
||||
|
||||
if (!textElement || !authorElement || !linkElement) return null;
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
// 3. 注入“Insight”按钮
|
||||
const injectInsightButton = (tweetElement: HTMLElement) => {
|
||||
// 查找操作栏 (Actions bar)
|
||||
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';
|
||||
|
||||
// 按钮内部图标 (简易版)
|
||||
btnContainer.innerHTML = `
|
||||
<div style="padding: 4px; border-radius: 9999px; transition: background 0.2s;" onmouseover="this.style.background='rgba(139, 92, 246, 0.1)'" onmouseout="this.style.background='transparent'">
|
||||
<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>
|
||||
</div>
|
||||
`;
|
||||
|
||||
btnContainer.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
const data = extractTweetData(tweetElement);
|
||||
console.log('Target Tweet Data:', data);
|
||||
|
||||
// 发送消息给插件侧边栏/Popup (后续实现)
|
||||
if (data) {
|
||||
chrome.runtime.sendMessage({ type: 'SHOW_INSIGHT', payload: data });
|
||||
}
|
||||
};
|
||||
|
||||
actionBar.appendChild(btnContainer);
|
||||
};
|
||||
|
||||
// 4. 定时或监听 DOM 变化进行扫描
|
||||
const scanTweets = () => {
|
||||
const tweets = document.querySelectorAll('article[data-testid="tweet"]');
|
||||
tweets.forEach((tweet) => {
|
||||
injectInsightButton(tweet as HTMLElement);
|
||||
});
|
||||
};
|
||||
|
||||
// 使用 MutationObserver 监听动态加载
|
||||
const observer = new MutationObserver(() => {
|
||||
scanTweets();
|
||||
});
|
||||
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
|
||||
// 初始扫描
|
||||
scanTweets();
|
||||
Reference in New Issue
Block a user