feat: 部署初版测试
Some checks failed
Extension Build & Release / build (push) Failing after 1m5s
Backend Deploy (Go + Docker) / deploy (push) Failing after 1m40s
Web Console Deploy (Vue 3 + Vite) / deploy (push) Has been cancelled

This commit is contained in:
zs
2026-03-02 21:25:21 +08:00
parent db3abb3174
commit 8cf6cb944b
97 changed files with 10250 additions and 209 deletions

View File

@@ -17,22 +17,48 @@ interface TweetData {
};
}
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 {
// 根据 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;
// 互动数据提取
if (!textElement || !authorElement || !linkElement) return null;
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 {
@@ -51,13 +77,68 @@ const extractTweetData = (tweetElement: HTMLElement): TweetData | 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) => {
// 查找操作栏 (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';
@@ -65,7 +146,6 @@ const injectInsightButton = (tweetElement: HTMLElement) => {
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;">
@@ -77,9 +157,6 @@ const injectInsightButton = (tweetElement: HTMLElement) => {
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 });
}
@@ -92,7 +169,10 @@ const injectInsightButton = (tweetElement: HTMLElement) => {
const scanTweets = () => {
const tweets = document.querySelectorAll('article[data-testid="tweet"]');
tweets.forEach((tweet) => {
injectInsightButton(tweet as HTMLElement);
const el = tweet as HTMLElement;
injectInsightButton(el);
const data = extractTweetData(el);
if (data) injectBadges(el, data);
});
};