智能提取网站站长联系方式(邮箱、QQ),识别站点类型,一键复制导出
// ==UserScript==
// @name MailScout
// @namespace https://xpornkit.com/zh
// @version 1.0.0
// @description 智能提取网站站长联系方式(邮箱、QQ),识别站点类型,一键复制导出
// @author MailScout
// @match *://*/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_setClipboard
// @grant GM_addStyle
// @grant GM_registerMenuCommand
// @run-at document-idle
// @license MIT
// ==/UserScript==
(function () {
'use strict';
// ============================================================
// 提取引擎
// ============================================================
const Extractor = {
EMAIL_REGEX: /[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}/g,
EMAIL_VARIANT_PATTERNS: [
/[a-zA-Z0-9._%+\-]+#[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}/g,
/[a-zA-Z0-9._%+\-]+\s*\[at\]\s*[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}/gi,
/[a-zA-Z0-9._%+\-]+\s*\(at\)\s*[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}/gi,
/[a-zA-Z0-9._%+\-]+\s+AT\s+[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}/g,
/[a-zA-Z0-9._%+\-]+\s*[\[\(]?at[\]\)]?\s*[a-zA-Z0-9\-]+\s*[\[\(]?dot[\]\)]?\s*[a-zA-Z]{2,}/gi,
],
INVALID_EMAIL_PREFIXES: [
'noreply', 'no-reply', 'no_reply',
'mailer-daemon', 'postmaster',
'example', 'test', 'demo', 'sample',
'[email protected]', 'email@example',
'your@email', 'name@domain', 'user@example',
'[email protected]',
],
normalizeEmailVariant(raw) {
let email = raw.trim();
email = email.replace(/#/, '@');
email = email.replace(/\s*[\[\(]?at[\]\)]?\s*/gi, '@');
email = email.replace(/\s*[\[\(]?dot[\]\)]?\s*/gi, '.');
email = email.replace(/\s+/g, '');
return email.toLowerCase();
},
isValidEmail(email) {
const lower = email.toLowerCase();
for (const prefix of this.INVALID_EMAIL_PREFIXES) {
if (lower.startsWith(prefix) || lower === prefix) return false;
}
const parts = lower.split('@');
if (parts.length !== 2) return false;
if (parts[0].length < 1 || parts[1].length < 3) return false;
return true;
},
extractEmails() {
if (!document.body) return [];
const text = document.body.innerText || '';
const results = new Set();
const standardMatches = text.match(this.EMAIL_REGEX) || [];
standardMatches.forEach(e => results.add(e.toLowerCase()));
const mailtoLinks = document.querySelectorAll('a[href^="mailto:"]');
mailtoLinks.forEach(link => {
const href = link.getAttribute('href');
const email = href.replace(/^mailto:/i, '').split('?')[0].trim();
if (email) results.add(email.toLowerCase());
});
this.EMAIL_VARIANT_PATTERNS.forEach(pattern => {
const matches = text.match(pattern) || [];
matches.forEach(raw => {
const normalized = this.normalizeEmailVariant(raw);
if (normalized.includes('@') && normalized.includes('.')) {
results.add(normalized);
}
});
});
return [...results].filter(e => this.isValidEmail(e));
},
groupEmailsByDomain(emails) {
const groups = {};
const domainLabels = {
'gmail.com': 'Gmail', 'qq.com': 'QQ邮箱', '163.com': '163邮箱',
'126.com': '126邮箱', 'sina.com': '新浪邮箱', 'outlook.com': 'Outlook',
'hotmail.com': 'Hotmail', 'yahoo.com': 'Yahoo', 'foxmail.com': 'Foxmail',
};
emails.forEach(email => {
const domain = email.split('@')[1];
const label = domainLabels[domain] || '企业邮箱 (' + domain + ')';
if (!groups[label]) groups[label] = [];
groups[label].push(email);
});
return groups;
},
QQ_REGEX: /(?<![\d])[1-9]\d{4,10}(?![\d])/g,
QQ_KEYWORDS: ['qq', 'QQ', '扣扣', '企鹅', 'tencent'],
isLikelyQQ(numStr, context) {
const num = parseInt(numStr, 10);
const len = numStr.length;
if (len === 11 && numStr.startsWith('1')) return false;
if (len === 4 && num >= 1990 && num <= 2099) return false;
if (len === 6 && num >= 100000 && num <= 999999) {
if (context && /邮编|zip|postal/i.test(context)) return false;
}
if (len >= 8 && /^20\d{6,}$/.test(numStr)) return false;
if (context && /[¥$¥]\s*$/.test(context)) return false;
return true;
},
isInQQContext(numStr) {
const selectors = [
'[class*="qq"]', '[id*="qq"]',
'[class*="QQ"]', '[id*="QQ"]',
'[class*="Qq"]', '[id*="Qq"]'
];
try {
const elements = document.querySelectorAll(selectors.join(','));
for (const el of elements) {
if (el.textContent && el.textContent.includes(numStr)) return true;
}
} catch (e) {}
return false;
},
extractQQs() {
if (!document.body) return [];
const text = document.body.innerText || '';
const results = [];
const seen = new Set();
const qqLinks = document.querySelectorAll('a[href*="wpa.qq.com"], a[href*="tencent://message"]');
qqLinks.forEach(link => {
const href = link.getAttribute('href');
const match = href.match(/uin=(\d{5,11})/);
if (match && !seen.has(match[1])) {
seen.add(match[1]);
results.push({ qq: match[1], qqEmail: match[1] + '@qq.com', confidence: 'high', source: 'link' });
}
});
const lines = text.split('\n');
lines.forEach(line => {
const matches = line.match(this.QQ_REGEX);
if (!matches) return;
matches.forEach(numStr => {
if (seen.has(numStr)) return;
const idx = line.indexOf(numStr);
const before = line.substring(Math.max(0, idx - 20), idx);
const after = line.substring(idx + numStr.length, idx + numStr.length + 20);
const context = (before + after).toLowerCase();
if (!this.isLikelyQQ(numStr, before)) return;
const hasKeyword = this.QQ_KEYWORDS.some(k => context.includes(k.toLowerCase()));
const inQQDom = this.isInQQContext(numStr);
let confidence = 'low';
if (hasKeyword || inQQDom) confidence = 'high';
else if (numStr.length >= 5 && numStr.length <= 10) confidence = 'medium';
if (confidence !== 'low') {
seen.add(numStr);
results.push({ qq: numStr, qqEmail: numStr + '@qq.com', confidence, source: hasKeyword ? 'keyword' : 'text' });
}
});
});
return results;
},
IMAGE_KEYWORDS: [
'email', 'mail', 'qq', 'contact', 'lx', 'yxh',
'contactus', '邮箱', '联系', '联系方式', '联系我们',
'wechat', '微信', 'qrcode', '二维码'
],
scanContactImages() {
const images = document.querySelectorAll('img');
const suspects = [];
images.forEach(img => {
if (suspects.length >= 10) return;
let rawSrc = img.src || '';
let dataSrc = img.getAttribute('data-src') || '';
const safeSrc = rawSrc.startsWith('data:') ? rawSrc.substring(0, 60) : rawSrc.substring(0, 200);
const safeDataSrc = dataSrc.startsWith('data:') ? dataSrc.substring(0, 60) : dataSrc.substring(0, 200);
const combinedText = [safeSrc, img.alt || '', img.className || '', img.title || '', safeDataSrc].join(' ').toLowerCase();
if (this.IMAGE_KEYWORDS.some(k => combinedText.includes(k))) {
let displaySrc = rawSrc || dataSrc;
if (displaySrc.startsWith('data:')) displaySrc = '[base64图片]';
else if (displaySrc.length > 150) displaySrc = displaySrc.substring(0, 150) + '...';
suspects.push({ src: displaySrc, alt: (img.alt || '').substring(0, 100), hint: '发现疑似联系方式图片,请手动整理' });
}
});
return suspects;
},
extractSiteInfo() {
const title = document.title || '';
const metaDesc = document.querySelector('meta[name="description"]');
const description = metaDesc ? metaDesc.getAttribute('content') || '' : '';
let footerText = '';
const footer = document.querySelector('footer') || document.getElementById('footer');
if (footer) {
footerText = (footer.innerText || '').replace(/\s+/g, ' ').trim();
if (footerText.length > 200) footerText = footerText.substring(0, 200) + '...';
}
return { title: title.trim(), description: description.trim(), footerText: footerText.trim(), url: window.location.href, domain: window.location.hostname };
},
scanPage() {
const emails = this.extractEmails();
const qqs = this.extractQQs();
const images = this.scanContactImages();
const siteInfo = this.extractSiteInfo();
const qqFromEmails = new Set();
emails.forEach(e => {
if (e.endsWith('@qq.com')) {
const prefix = e.split('@')[0];
if (/^[1-9]\d{4,10}$/.test(prefix)) qqFromEmails.add(prefix);
}
});
const dedupedQQs = qqs.filter(item => !qqFromEmails.has(item.qq));
return { emails, emailGroups: this.groupEmailsByDomain(emails), qqs: dedupedQQs, images, siteInfo, scanTime: new Date().toISOString() };
}
};
// ============================================================
// 分类引擎
// ============================================================
const Classifier = {
URL_PATTERNS: {
'导航/目录': ['nav', 'dir', 'site', 'links', 'dh', 'daohang', 'hao', 'catalog', 'directory', 'fenlei'],
'博客': ['blog', 'post', 'article', 'diary', 'journal', 'boke', 'wordpress'],
'论坛/社区': ['forum', 'bbs', 'community', 'discuss', 'tieba', 'luntan'],
'关于/联系': ['about', 'contact', 'aboutus', 'contactus', 'lianxi', 'guanyu', 'join'],
'新闻/资讯': ['news', 'press', 'zixun', 'xinwen'],
'工具/服务': ['tool', 'service', 'api', 'app'],
},
classifyByURL() {
const combined = (window.location.hostname + window.location.pathname).toLowerCase();
const matched = [];
for (const [type, keywords] of Object.entries(this.URL_PATTERNS)) {
for (const kw of keywords) {
if (combined.includes(kw)) { matched.push(type); break; }
}
}
return matched;
},
classifyByDOM() {
const tags = [];
if (document.querySelectorAll('article').length > 0) tags.push('博客');
const commentSelectors = ['#comments', '.comments', '#comment', '.comment-list', '#disqus_thread', '.ds-thread'];
for (const sel of commentSelectors) { if (document.querySelector(sel)) { tags.push('博客'); break; } }
const allLinks = document.querySelectorAll('a[href^="http"]');
const externalLinks = [...allLinks].filter(a => { try { return new URL(a.href).hostname !== window.location.hostname; } catch { return false; } });
if (externalLinks.length > 30) tags.push('导航/目录');
if (document.querySelector('meta[name="generator"][content*="WordPress"]')) tags.push('WordPress');
return [...new Set(tags)];
},
findContactPageLinks() {
const contactKeywords = ['contact', 'about', 'aboutus', 'contactus', 'lianxi', 'guanyu', 'join', 'cooperation', '联系', '关于', '联系我们', '关于我们', '合作'];
const links = document.querySelectorAll('a[href]');
const contactLinks = [];
links.forEach(link => {
const href = (link.getAttribute('href') || '').toLowerCase();
const text = (link.textContent || '').trim().toLowerCase();
const combined = href + ' ' + text;
if (!href || href === '#' || href.startsWith('javascript:') || href.startsWith('mailto:')) return;
for (const kw of contactKeywords) {
if (combined.includes(kw)) {
let fullUrl = href;
try { fullUrl = new URL(href, window.location.origin).href; } catch { continue; }
if (fullUrl !== window.location.href) {
contactLinks.push({ url: fullUrl, text: link.textContent.trim() || href, keyword: kw });
}
break;
}
}
});
const seen = new Set();
return contactLinks.filter(item => { if (seen.has(item.url)) return false; seen.add(item.url); return true; });
},
classify() {
const urlTags = this.classifyByURL();
const domTags = this.classifyByDOM();
const contactLinks = this.findContactPageLinks();
const allTags = [...new Set([...urlTags, ...domTags])];
return { tags: allTags, contactLinks, isContactPage: urlTags.includes('关于/联系'), isBlog: allTags.includes('博客'), isNavSite: allTags.includes('导航/目录') };
}
};
// ============================================================
// 过滤引擎
// ============================================================
const Filter = {
WECHAT_KEYWORDS: ['微信', 'wechat', 'wxid', 'weixin', '公众号'],
detectWeChat() {
if (!document.body) return { found: false, wechatId: null };
const text = (document.body.innerText || '').toLowerCase();
const found = this.WECHAT_KEYWORDS.some(kw => text.includes(kw.toLowerCase()));
let wechatId = null;
const wxMatch = text.match(/微信[号:]?\s*[::]?\s*([a-zA-Z0-9_\-]{5,20})/);
if (wxMatch) wechatId = wxMatch[1];
return { found, wechatId };
},
applyFilters(scanResult) {
const { emails, qqs, images } = scanResult;
const wechat = this.detectWeChat();
const hasEmail = emails && emails.length > 0;
const hasQQ = qqs && qqs.length > 0;
const hasImages = images && images.length > 0;
let status = 'found', message = '';
if (!hasEmail && !hasQQ) {
if (wechat.found) { status = 'wechat_only'; message = '仅检测到微信联系方式,未发现邮箱或QQ'; }
else if (hasImages) { status = 'image_only'; message = '发现疑似联系方式图片,请手动整理'; }
else { status = 'empty'; message = '未检测到任何联系方式'; }
} else {
message = hasImages ? '已提取联系方式,另有疑似图片联系方式需手动查看' : '已成功提取联系方式';
}
const copyFormats = this.generateCopyFormats(scanResult);
return { ...scanResult, wechat, status, message, copyFormats };
},
generateCopyFormats(scanResult) {
const { emails, qqs, siteInfo } = scanResult;
const siteName = siteInfo.title || siteInfo.domain || '';
const formats = [];
if (emails && emails.length > 0) {
emails.forEach(email => { formats.push({ type: 'email', text: `${siteName} | ${email}`, email }); });
}
if (qqs && qqs.length > 0) {
qqs.forEach(item => { formats.push({ type: 'qq', text: `${siteName} | ${item.qqEmail}`, email: item.qqEmail, qq: item.qq, confidence: item.confidence }); });
}
return formats;
}
};
// ============================================================
// 存储(GM_setValue / GM_getValue)
// ============================================================
const Storage = {
getHistory() {
return GM_getValue('mailscout_history', {});
},
saveRecord(domain, data) {
const history = this.getHistory();
history[domain] = { data, lastScan: new Date().toISOString() };
GM_setValue('mailscout_history', history);
},
getRecord(domain) {
const history = this.getHistory();
return history[domain] || null;
},
clearHistory() {
GM_setValue('mailscout_history', {});
}
};
// ============================================================
// 工具函数
// ============================================================
function esc(str) {
if (!str) return '';
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
}
function copyText(text) {
GM_setClipboard(text, 'text');
}
// ============================================================
// UI 面板
// ============================================================
const PANEL_ID = 'mailscout-panel';
const FAB_ID = 'mailscout-fab';
let currentResult = null;
let panelVisible = false;
GM_addStyle(`
#${FAB_ID} {
position: fixed; top: 120px; right: 24px; width: 40px; height: 40px;
background: #333; border-radius: 50%; display: flex; align-items: center; justify-content: center;
cursor: pointer; z-index: 2147483646; transition: transform 0.2s, opacity 0.2s;
opacity: 0.7; user-select: none; box-shadow: 0 2px 8px rgba(0,0,0,0.25);
font-size: 16px; color: #fff; font-weight: bold; line-height: 1;
}
#${FAB_ID}:hover { opacity: 1; transform: scale(1.1); }
#${FAB_ID} .ms-dot {
position: absolute; top: 0; right: 0; width: 10px; height: 10px;
background: #4caf50; border-radius: 50%; border: 2px solid #fff;
}
#${PANEL_ID} {
position: fixed; top: 0; right: -380px; width: 360px; height: 100vh;
background: #fff; z-index: 2147483647; transition: right 0.3s ease;
box-shadow: -4px 0 20px rgba(0,0,0,0.12); font-family: -apple-system, "Segoe UI", sans-serif;
display: flex; flex-direction: column; font-size: 13px; color: #333;
overflow: hidden;
}
#${PANEL_ID}.ms-open { right: 0; }
#${PANEL_ID} * { box-sizing: border-box; margin: 0; padding: 0; }
#${PANEL_ID} .ms-header {
display: flex; align-items: center; justify-content: space-between;
padding: 14px 20px; border-bottom: 1px solid #e0e0e0; flex-shrink: 0;
}
#${PANEL_ID} .ms-header h1 { font-size: 16px; font-weight: 700; color: #222; }
#${PANEL_ID} .ms-header-btns { display: flex; gap: 8px; }
#${PANEL_ID} .ms-btn {
padding: 6px 14px; font-size: 12px; font-weight: 600; border: none;
border-radius: 4px; cursor: pointer; background: #333; color: #fff;
}
#${PANEL_ID} .ms-btn:hover { background: #555; }
#${PANEL_ID} .ms-btn-sm {
padding: 4px 10px; font-size: 11px; border: 1px solid #ccc;
border-radius: 3px; background: #fff; color: #555; cursor: pointer;
}
#${PANEL_ID} .ms-btn-sm:hover { background: #f5f5f5; }
#${PANEL_ID} .ms-btn-close {
width: 28px; height: 28px; border: none; background: transparent;
font-size: 18px; color: #999; cursor: pointer; border-radius: 4px;
}
#${PANEL_ID} .ms-btn-close:hover { background: #f0f0f0; color: #333; }
#${PANEL_ID} .ms-body { flex: 1; overflow-y: auto; }
#${PANEL_ID} .ms-body::-webkit-scrollbar { width: 4px; }
#${PANEL_ID} .ms-body::-webkit-scrollbar-thumb { background: #ddd; border-radius: 2px; }
#${PANEL_ID} .ms-status {
padding: 10px 20px; font-size: 12px; border-bottom: 1px solid #e0e0e0;
}
#${PANEL_ID} .ms-status.found { background: #f0faf0; color: #2e7d32; }
#${PANEL_ID} .ms-status.empty { background: #fafafa; color: #999; }
#${PANEL_ID} .ms-status.wechat { background: #fff8e1; color: #f57f17; }
#${PANEL_ID} .ms-status.image { background: #fff3e0; color: #e65100; }
#${PANEL_ID} .ms-history {
padding: 10px 20px; font-size: 12px; color: #666;
background: #f9f9f9; border-bottom: 1px solid #e0e0e0;
display: flex; align-items: center; justify-content: space-between;
}
#${PANEL_ID} .ms-section { padding: 14px 20px; border-bottom: 1px solid #f0f0f0; }
#${PANEL_ID} .ms-section-title {
font-size: 12px; font-weight: 700; color: #999; margin-bottom: 10px; letter-spacing: 0.5px;
}
#${PANEL_ID} .ms-info-row { display: flex; gap: 8px; padding: 4px 0; font-size: 12px; }
#${PANEL_ID} .ms-info-label { color: #999; min-width: 36px; flex-shrink: 0; }
#${PANEL_ID} .ms-info-value { color: #333; word-break: break-all; }
#${PANEL_ID} .ms-tag {
display: inline-block; padding: 2px 8px; margin: 2px 4px 2px 0;
font-size: 11px; background: #f0f0f0; color: #555; border-radius: 3px;
}
#${PANEL_ID} .ms-row {
display: flex; align-items: center; justify-content: space-between; gap: 8px;
padding: 6px 0; border-bottom: 1px solid #f5f5f5;
}
#${PANEL_ID} .ms-row:last-child { border-bottom: none; }
#${PANEL_ID} .ms-text { font-size: 13px; color: #333; word-break: break-all; }
#${PANEL_ID} .ms-sub { font-size: 11px; color: #999; margin-top: 2px; }
#${PANEL_ID} .ms-badge {
padding: 1px 6px; font-size: 10px; background: #e8f5e9; color: #4caf50;
border-radius: 3px; flex-shrink: 0;
}
#${PANEL_ID} .ms-copy {
padding: 3px 8px; font-size: 11px; border: 1px solid #ddd;
border-radius: 3px; background: #fff; color: #555; cursor: pointer; flex-shrink: 0;
}
#${PANEL_ID} .ms-copy:hover { background: #f5f5f5; }
#${PANEL_ID} .ms-copy.copied { background: #e8f5e9; color: #4caf50; border-color: #c8e6c9; }
#${PANEL_ID} .ms-warn {
padding: 8px 10px; font-size: 12px; color: #666; background: #fafafa;
border-radius: 4px; margin-bottom: 6px;
}
#${PANEL_ID} .ms-link-row {
display: flex; align-items: center; justify-content: space-between; gap: 8px;
padding: 4px 0; border-bottom: 1px solid #f5f5f5;
}
#${PANEL_ID} .ms-link-row a { color: #1a73e8; text-decoration: none; font-size: 12px; word-break: break-all; }
#${PANEL_ID} .ms-link-row a:hover { text-decoration: underline; }
#${PANEL_ID} .ms-footer {
display: flex; align-items: center; justify-content: space-between; gap: 8px;
padding: 12px 20px; border-top: 1px solid #e0e0e0; flex-shrink: 0;
}
#${PANEL_ID} .ms-btn-outline {
padding: 6px 12px; font-size: 12px; border: 1px solid #ccc;
border-radius: 4px; background: #fff; color: #555; cursor: pointer;
}
#${PANEL_ID} .ms-btn-outline:hover { background: #f5f5f5; }
#${PANEL_ID} .ms-btn-danger {
padding: 4px 10px; font-size: 11px; border: 1px solid #e57373;
border-radius: 3px; background: #fff; color: #e57373; cursor: pointer;
}
#${PANEL_ID} .ms-btn-danger:hover { background: #ffebee; }
#${PANEL_ID} .ms-toast {
position: fixed; bottom: 30px; left: 50%; transform: translateX(-50%);
padding: 8px 20px; background: #333; color: #fff; border-radius: 4px;
font-size: 12px; z-index: 2147483647; opacity: 0; transition: opacity 0.3s;
pointer-events: none;
}
#${PANEL_ID} .ms-toast.show { opacity: 1; }
.ms-hidden { display: none !important; }
`);
function createPanel() {
const panel = document.createElement('div');
panel.id = PANEL_ID;
panel.innerHTML = `
<div class="ms-header">
<h1>MailScout</h1>
<div class="ms-header-btns">
<button class="ms-btn" id="ms-btn-scan">扫描</button>
<button class="ms-btn-close" id="ms-btn-close">×</button>
</div>
</div>
<div class="ms-body">
<div id="ms-status" class="ms-status ms-hidden"></div>
<div id="ms-history" class="ms-history ms-hidden">
<span>该站点有历史扫描记录</span>
<button class="ms-btn-sm" id="ms-btn-history">查看</button>
</div>
<div id="ms-section-site" class="ms-section ms-hidden">
<div class="ms-section-title">网站资料</div>
<div id="ms-site-info"></div>
</div>
<div id="ms-section-tags" class="ms-section ms-hidden">
<div class="ms-section-title">站点类型</div>
<div id="ms-site-tags"></div>
</div>
<div id="ms-section-emails" class="ms-section ms-hidden">
<div class="ms-section-title">邮箱</div>
<div id="ms-email-list"></div>
</div>
<div id="ms-section-qqs" class="ms-section ms-hidden">
<div class="ms-section-title">QQ / QQ邮箱</div>
<div id="ms-qq-list"></div>
</div>
<div id="ms-section-images" class="ms-section ms-hidden">
<div class="ms-section-title">疑似图片联系方式</div>
<div id="ms-image-list"></div>
</div>
<div id="ms-section-links" class="ms-section ms-hidden">
<div class="ms-section-title">推荐扫描页面</div>
<div id="ms-contact-links"></div>
</div>
<div id="ms-section-copy" class="ms-section ms-hidden">
<div class="ms-section-title">一键复制</div>
<div id="ms-copy-list"></div>
</div>
</div>
<div class="ms-footer">
<button class="ms-btn-outline" id="ms-btn-export">导出 CSV</button>
<button class="ms-btn-danger" id="ms-btn-clear">清空历史</button>
</div>
<div style="text-align:center;padding:6px 20px 10px;font-size:11px;">
<a href="https://xpornkit.com/zh" target="_blank" rel="noopener" style="color:#999;text-decoration:none;">成人导航</a>
</div>
<div class="ms-toast" id="ms-toast"></div>
`;
document.body.appendChild(panel);
panel.querySelector('#ms-btn-scan').addEventListener('click', doScan);
panel.querySelector('#ms-btn-close').addEventListener('click', togglePanel);
panel.querySelector('#ms-btn-history').addEventListener('click', showHistory);
panel.querySelector('#ms-btn-export').addEventListener('click', doExport);
panel.querySelector('#ms-btn-clear').addEventListener('click', doClear);
panel.addEventListener('click', (e) => {
const copyBtn = e.target.closest('[data-copy]');
if (copyBtn) {
copyText(copyBtn.dataset.copy);
copyBtn.textContent = '已复制';
copyBtn.classList.add('copied');
setTimeout(() => { copyBtn.textContent = '复制'; copyBtn.classList.remove('copied'); }, 1200);
showToast('已复制到剪贴板');
return;
}
const openBtn = e.target.closest('[data-open]');
if (openBtn) {
window.open(openBtn.dataset.open, '_blank');
}
});
return panel;
}
function togglePanel() {
const panel = document.getElementById(PANEL_ID) || createPanel();
panelVisible = !panelVisible;
if (panelVisible) {
panel.classList.add('ms-open');
doScan();
} else {
panel.classList.remove('ms-open');
}
}
function showToast(text) {
const toast = document.getElementById('ms-toast');
if (!toast) return;
toast.textContent = text;
toast.classList.add('show');
setTimeout(() => toast.classList.remove('show'), 1500);
}
function resetUI() {
const ids = ['ms-status', 'ms-history', 'ms-section-site', 'ms-section-tags', 'ms-section-emails', 'ms-section-qqs', 'ms-section-images', 'ms-section-links', 'ms-section-copy'];
ids.forEach(id => {
const el = document.getElementById(id);
if (el) el.classList.add('ms-hidden');
});
}
function showStatus(cls, text) {
const el = document.getElementById('ms-status');
if (!el) return;
el.className = 'ms-status ' + cls;
el.textContent = text;
}
function doScan() {
resetUI();
const domain = window.location.hostname;
const record = Storage.getRecord(domain);
if (record) {
const historyEl = document.getElementById('ms-history');
if (historyEl) historyEl.classList.remove('ms-hidden');
}
try {
const rawResult = Extractor.scanPage();
const classification = Classifier.classify();
const filteredResult = Filter.applyFilters(rawResult);
const finalResult = { ...filteredResult, classification };
currentResult = finalResult;
renderResult(finalResult);
Storage.saveRecord(domain, finalResult);
updateFabDot();
} catch (err) {
showStatus('empty', '扫描出错: ' + err.message);
}
}
function showHistory() {
const domain = window.location.hostname;
const record = Storage.getRecord(domain);
if (record && record.data) {
currentResult = record.data;
renderResult(record.data);
showToast('已加载历史记录 (' + (record.lastScan || '').split('T')[0] + ')');
}
}
function renderResult(data) {
const statusMap = {
'found': 'found', 'wechat_only': 'wechat', 'image_only': 'image', 'empty': 'empty'
};
showStatus(statusMap[data.status] || 'empty', data.message);
if (data.siteInfo) {
const el = document.getElementById('ms-section-site');
el.classList.remove('ms-hidden');
let html = '';
if (data.siteInfo.title) html += `<div class="ms-info-row"><span class="ms-info-label">标题</span><span class="ms-info-value">${esc(data.siteInfo.title)}</span></div>`;
if (data.siteInfo.domain) html += `<div class="ms-info-row"><span class="ms-info-label">域名</span><span class="ms-info-value">${esc(data.siteInfo.domain)}</span></div>`;
if (data.siteInfo.description) html += `<div class="ms-info-row"><span class="ms-info-label">描述</span><span class="ms-info-value">${esc(data.siteInfo.description)}</span></div>`;
if (data.siteInfo.footerText) html += `<div class="ms-info-row"><span class="ms-info-label">底部</span><span class="ms-info-value">${esc(data.siteInfo.footerText)}</span></div>`;
document.getElementById('ms-site-info').innerHTML = html;
}
if (data.classification && data.classification.tags.length > 0) {
document.getElementById('ms-section-tags').classList.remove('ms-hidden');
document.getElementById('ms-site-tags').innerHTML = data.classification.tags.map(t => `<span class="ms-tag">${esc(t)}</span>`).join('');
}
if (data.emails && data.emails.length > 0) {
document.getElementById('ms-section-emails').classList.remove('ms-hidden');
let html = '';
if (data.emailGroups && Object.keys(data.emailGroups).length > 0) {
for (const [label, list] of Object.entries(data.emailGroups)) {
list.forEach(email => {
html += `<div class="ms-row"><div><div class="ms-text">${esc(email)}</div><div class="ms-sub">${esc(label)}</div></div><button class="ms-copy" data-copy="${esc(email)}">复制</button></div>`;
});
}
} else {
data.emails.forEach(email => {
html += `<div class="ms-row"><div class="ms-text">${esc(email)}</div><button class="ms-copy" data-copy="${esc(email)}">复制</button></div>`;
});
}
document.getElementById('ms-email-list').innerHTML = html;
}
if (data.qqs && data.qqs.length > 0) {
document.getElementById('ms-section-qqs').classList.remove('ms-hidden');
document.getElementById('ms-qq-list').innerHTML = data.qqs.map(item => {
const badgeText = item.confidence === 'high' ? '高' : '中';
return `<div class="ms-row"><div><div class="ms-text">${esc(item.qq)}</div><div class="ms-sub">${esc(item.qqEmail)}</div></div><span class="ms-badge">${badgeText}</span><button class="ms-copy" data-copy="${esc(item.qqEmail)}">复制邮箱</button></div>`;
}).join('');
}
if (data.images && data.images.length > 0) {
document.getElementById('ms-section-images').classList.remove('ms-hidden');
document.getElementById('ms-image-list').innerHTML = data.images.map(img => `<div class="ms-warn"><div>${esc(img.hint)}</div><div style="font-size:11px;color:#999;margin-top:4px">${esc(img.alt || img.src)}</div></div>`).join('');
}
if (data.classification && data.classification.contactLinks.length > 0) {
document.getElementById('ms-section-links').classList.remove('ms-hidden');
document.getElementById('ms-contact-links').innerHTML = data.classification.contactLinks.map(link => `<div class="ms-link-row"><a href="${esc(link.url)}" target="_blank">${esc(link.text)}</a><button class="ms-btn-sm" data-open="${esc(link.url)}">打开</button></div>`).join('');
}
if (data.copyFormats && data.copyFormats.length > 0) {
document.getElementById('ms-section-copy').classList.remove('ms-hidden');
document.getElementById('ms-copy-list').innerHTML = data.copyFormats.map(item => {
const typeLabel = item.type === 'email' ? '邮箱' : 'QQ邮箱';
return `<div class="ms-row"><span class="ms-tag">${typeLabel}</span><div class="ms-text" style="flex:1">${esc(item.text)}</div><button class="ms-copy" data-copy="${esc(item.text)}">复制</button></div>`;
}).join('');
}
}
function doExport() {
if (!currentResult) { showToast('暂无数据可导出'); return; }
const lines = [];
const siteName = currentResult.siteInfo ? currentResult.siteInfo.title : '';
const domain = currentResult.siteInfo ? currentResult.siteInfo.domain : '';
lines.push('网站名称,域名,联系方式类型,联系方式,QQ邮箱');
if (currentResult.emails) currentResult.emails.forEach(email => { lines.push(`"${siteName}","${domain}","邮箱","${email}",""`); });
if (currentResult.qqs) currentResult.qqs.forEach(item => { lines.push(`"${siteName}","${domain}","QQ","${item.qq}","${item.qqEmail}"`); });
const csvContent = '\uFEFF' + lines.join('\n');
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = `MailScout_${domain}_${new Date().toISOString().split('T')[0]}.csv`;
a.click(); URL.revokeObjectURL(url);
showToast('已导出 CSV 文件');
}
function doClear() {
if (confirm('确定要清空所有扫描历史记录吗?')) {
Storage.clearHistory();
showToast('历史记录已清空');
const historyEl = document.getElementById('ms-history');
if (historyEl) historyEl.classList.add('ms-hidden');
updateFabDot();
markSearchResults();
}
}
// ============================================================
// 悬浮按钮
// ============================================================
function createFab() {
if (document.getElementById(FAB_ID)) return;
const fab = document.createElement('div');
fab.id = FAB_ID;
fab.title = 'MailScout';
fab.textContent = 'M';
let isDragging = false;
let offsetX, offsetY;
fab.addEventListener('mousedown', (e) => {
isDragging = false;
offsetX = e.clientX - fab.getBoundingClientRect().left;
offsetY = e.clientY - fab.getBoundingClientRect().top;
const onMove = (e) => {
isDragging = true;
fab.style.right = 'auto';
fab.style.left = (e.clientX - offsetX) + 'px';
fab.style.top = (e.clientY - offsetY) + 'px';
};
const onUp = () => {
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
};
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
});
fab.addEventListener('click', () => {
if (isDragging) return;
togglePanel();
});
document.body.appendChild(fab);
updateFabDot();
}
function updateFabDot() {
const fab = document.getElementById(FAB_ID);
if (!fab) return;
const existing = fab.querySelector('.ms-dot');
if (existing) existing.remove();
const domain = window.location.hostname;
const record = Storage.getRecord(domain);
if (record) {
const dot = document.createElement('div');
dot.className = 'ms-dot';
dot.title = '该站点已扫描过 (' + (record.lastScan || '').split('T')[0] + ')';
fab.appendChild(dot);
}
}
// ============================================================
// 搜索结果标记(Google / Baidu / Bing)
// ============================================================
function markSearchResults() {
const host = window.location.hostname;
const path = window.location.pathname;
let engine = null;
if (host.includes('google.') && path.startsWith('/search')) engine = 'google';
else if (host.includes('baidu.com') && path.startsWith('/s')) engine = 'baidu';
else if (host.includes('bing.com') && path.startsWith('/search')) engine = 'bing';
if (!engine) return;
const skipHosts = { google: 'google.', baidu: 'baidu.com', bing: 'bing.com' };
const skipHost = skipHosts[engine];
const history = Storage.getHistory();
const scannedDomains = new Set(Object.keys(history));
if (scannedDomains.size === 0) return;
const badgeStyle = 'display:inline-block;margin-left:8px;padding:1px 6px;font-size:11px;font-weight:500;color:#4caf50;background:#e8f5e9;border:1px solid #c8e6c9;border-radius:3px;vertical-align:middle;line-height:18px;';
function getDomain(link, parentEl) {
try {
let href = link.href || '';
if (engine === 'baidu') {
const realUrl = link.getAttribute('data-url') || link.getAttribute('mu') || (parentEl && parentEl.getAttribute('mu'));
if (realUrl && realUrl.startsWith('http')) href = realUrl;
}
const url = new URL(href);
if (url.hostname.includes(skipHost)) return null;
return url.hostname.replace(/^www\./, '');
} catch { return null; }
}
function addBadge(titleEl) {
const badge = document.createElement('span');
badge.textContent = '已扫描';
badge.style.cssText = badgeStyle;
titleEl.appendChild(badge);
}
function doMark() {
if (engine === 'google') {
document.querySelectorAll('h3').forEach(h3 => {
if (h3.dataset.mailscoutMarked) return;
const link = h3.closest('a[href]');
if (!link) return;
const domain = getDomain(link, null);
if (!domain) return;
if (!scannedDomains.has(domain) && !scannedDomains.has('www.' + domain)) return;
h3.dataset.mailscoutMarked = '1';
addBadge(h3);
});
} else if (engine === 'baidu') {
document.querySelectorAll('div.result, div.c-container').forEach(result => {
if (result.dataset.mailscoutMarked) return;
const link = result.querySelector('a[href]');
if (!link) return;
const domain = getDomain(link, result);
if (!domain) return;
if (!scannedDomains.has(domain) && !scannedDomains.has('www.' + domain)) return;
result.dataset.mailscoutMarked = '1';
const titleEl = result.querySelector('h3, .t');
if (titleEl) addBadge(titleEl);
});
} else if (engine === 'bing') {
document.querySelectorAll('li.b_algo').forEach(result => {
if (result.dataset.mailscoutMarked) return;
const link = result.querySelector('a[href]');
if (!link) return;
const domain = getDomain(link, null);
if (!domain) return;
if (!scannedDomains.has(domain) && !scannedDomains.has('www.' + domain)) return;
result.dataset.mailscoutMarked = '1';
const titleEl = result.querySelector('h2');
if (titleEl) addBadge(titleEl);
});
}
}
doMark();
const observer = new MutationObserver(() => doMark());
const container = document.getElementById('search') || document.getElementById('rso')
|| document.getElementById('content_left') || document.getElementById('b_results') || document.body;
observer.observe(container, { childList: true, subtree: true });
}
// ============================================================
// 初始化
// ============================================================
GM_registerMenuCommand('MailScout - 打开面板', togglePanel);
createFab();
markSearchResults();
})();